1#![allow(deprecated)]
2
3pub mod application_crypto;
4pub mod application_crypto_streams;
5pub mod client;
6pub mod connection;
7pub mod coordination;
8pub mod explicit_transfer_crypto;
9pub(crate) mod generated;
10pub mod heartbeat;
11#[cfg(feature = "iroh-carrier-core")]
12pub mod iroh_carrier;
13#[cfg(feature = "iroh-carrier-core")]
14pub mod iroh_carrier_bootstrap;
15#[cfg(feature = "iroh-carrier-core")]
16pub mod iroh_carrier_kind;
17#[cfg(feature = "iroh-carrier-core")]
18pub mod iroh_carrier_proof;
19pub(crate) mod iroh_connection_policy;
20pub mod key_agreement;
21pub mod lifecycle_reason;
22pub(crate) mod native_moq_policy;
23pub mod native_protocol;
24#[cfg(not(target_arch = "wasm32"))]
25pub(crate) mod native_send_policy;
26pub(crate) mod native_webrtc_policy;
27#[cfg(feature = "iroh-carrier-core")]
28pub mod packet_carrier_transport;
29pub mod presence;
30pub(crate) mod presence_policy;
31pub mod protocol_config;
32pub mod route_policy;
33pub mod runtime_policy;
34pub mod session_token;
35pub mod signaling;
36pub mod stream_metadata;
37pub(crate) mod transport_generation;
38pub(crate) mod transport_label;
39
40#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
41pub mod local_discovery;
42
43#[cfg(all(target_arch = "wasm32", feature = "transport-webrtc"))]
44compile_error!(
45 "feature `transport-webrtc` is native-only and must not be enabled for wasm32 targets"
46);
47
48#[cfg(all(target_arch = "wasm32", feature = "transport-moq"))]
49compile_error!("feature `transport-moq` is native-only and must not be enabled for wasm32 targets");
50
51#[cfg(test)]
55pub mod test_constants {
56 pub const TEST_PROJECT_ID: &str = "test-project";
57 pub const TEST_API_KEY: &str = "pk_test_0000000000000000000000000000000000000000";
58}
59
60pub const LIVE_PROJECT_ID: &str = "pluto-rtc-prod";
62
63pub fn validate_v2_public_api_key(api_key: &str) -> anyhow::Result<&str> {
67 let trimmed = api_key.trim();
68 let valid_prefix = trimmed.starts_with("pk_live_") || trimmed.starts_with("pk_test_");
69 let suffix = trimmed.get(8..).unwrap_or_default();
70 if !valid_prefix || suffix.len() != 40 || !suffix.bytes().all(|byte| byte.is_ascii_hexdigit()) {
71 anyhow::bail!("OpenRTC 2.0 requires a public pk_live_ or pk_test_ API key");
72 }
73 Ok(trimmed)
74}
75
76pub fn app_tag_from_api_key(api_key: &str) -> String {
77 let trimmed = api_key.trim();
78 if trimmed.is_empty() {
79 return "app_anonymous".to_string();
80 }
81
82 let suffix_len = trimmed.len().min(16);
83 format!("app_{}", &trimmed[trimmed.len() - suffix_len..])
84}
85
86pub fn space_app_tag_from_keys(api_key: &str, space_key: &str) -> String {
87 let input = format!("{}:{}", api_key.trim(), space_key.trim());
88 let digest = <sha2::Sha256 as sha2::Digest>::digest(input.as_bytes());
89 format!("space::{}", hex::encode(digest))
90}
91
92#[cfg(test)]
93mod v2_constructor_contract_tests {
94 use super::*;
95
96 #[test]
97 fn rust_v2_constructor_is_provider_neutral_and_side_effect_free() {
98 let api_key = test_constants::TEST_API_KEY;
99 let client = client::Client::new_v2(api_key.to_string()).expect("valid public API key");
100
101 assert_eq!(client.app_tag(), app_tag_from_api_key(api_key));
102 assert!(client::Client::new_v2("firebase-project-id".to_string()).is_err());
103 }
104}
105
106#[cfg(not(target_arch = "wasm32"))]
107pub fn ensure_default_rustls_provider() {
108 if rustls::crypto::CryptoProvider::get_default().is_none() {
109 let _ = rustls::crypto::ring::default_provider().install_default();
110 }
111}
112
113#[cfg(not(target_arch = "wasm32"))]
114pub mod adapters;
115
116#[cfg(not(target_arch = "wasm32"))]
117pub mod native_coordination_gateway;
118
119#[cfg(not(target_arch = "wasm32"))]
120pub mod native_v2;
121
122pub mod connection_manager;
123pub mod logging;
124
125pub mod protocol_registry;
126
127#[cfg(not(target_arch = "wasm32"))]
128pub mod runtime_manager;
129
130#[cfg(not(target_arch = "wasm32"))]
131pub mod transport;
132
133#[cfg(not(target_arch = "wasm32"))]
134pub use client::EndpointHandle;
135
136#[cfg(all(
137 not(target_arch = "wasm32"),
138 not(any(target_os = "ios", target_os = "android"))
139))]
140pub mod sso;
141
142#[cfg(not(target_arch = "wasm32"))]
143pub mod native_node;
144
145#[cfg(not(target_arch = "wasm32"))]
146pub mod native_device;
147
148#[cfg(all(
149 test,
150 not(target_arch = "wasm32"),
151 feature = "iroh-protocols-wasm",
152 any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq")
153))]
154mod native_carrier_protocol_test;
155#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-transport-moq"))]
156pub mod native_moq_carrier;
157#[cfg(all(not(target_arch = "wasm32"), feature = "iroh-transport-webrtc"))]
158pub mod native_webrtc_carrier;
159
160#[cfg(all(target_arch = "wasm32", feature = "iroh-protocols-wasm"))]
161mod wasm_docs_persistence;
162#[cfg(all(target_arch = "wasm32", feature = "iroh-protocols-wasm"))]
163mod wasm_indexeddb_blob_store;
164#[cfg(all(target_arch = "wasm32", feature = "iroh-transport-moq"))]
165pub mod wasm_moq_carrier;
166#[cfg(target_arch = "wasm32")]
167pub mod wasm_node;
168#[cfg(all(target_arch = "wasm32", feature = "iroh-transport-webrtc"))]
169pub mod wasm_webrtc_carrier;
170
171#[cfg(target_arch = "wasm32")]
172#[macro_export]
173macro_rules! console_log {
174 ($($t:tt)*) => (web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format_args!($($t)*).to_string())))
175}
176
177#[cfg(target_arch = "wasm32")]
179pub mod wasm_api {
180 use crate::client::Client;
181 use crate::session_token::split_compound_ticket;
182 use crate::wasm_node::{
183 into_js_readable_stream, peer_uni_stream_from_send, BiStream, PeerUniStream,
184 };
185 use iroh_tickets::endpoint::EndpointTicket;
186 #[cfg(any(
187 feature = "iroh-protocols-wasm",
188 feature = "iroh-transport-webrtc",
189 feature = "iroh-transport-moq"
190 ))]
191 use std::rc::Rc;
192 use std::str::FromStr;
193 use std::sync::{Arc, Mutex};
194 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
195 use std::{cell::RefCell, collections::HashMap};
196 use wasm_bindgen::prelude::*;
197 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
198 use wasm_bindgen_futures::spawn_local;
199 use wasm_streams::readable::sys::ReadableStream as JsReadableStream;
200
201 #[cfg(feature = "iroh-transport-webrtc")]
202 #[derive(Debug, Clone)]
203 struct WasmWebRtcCarrierAttempt {
204 connection_id: String,
205 remote_endpoint_id: iroh::EndpointId,
206 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
207 generation: crate::client::WasmPeerDataGeneration,
208 role: &'static str,
209 offer_started: bool,
210 remote_ready: bool,
211 completion_started: bool,
212 retry_count: u8,
213 external_fallback_allowed: bool,
214 inbound_authorization_expires_at_ms: Option<f64>,
215 }
216
217 #[cfg(feature = "iroh-transport-moq")]
218 #[derive(Debug, Clone)]
219 struct WasmMoqCarrierAttempt {
220 connection_id: String,
221 remote_endpoint_id: iroh::EndpointId,
222 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
223 generation: crate::client::WasmPeerDataGeneration,
224 role: &'static str,
225 retry_count: u8,
226 external_fallback_allowed: bool,
227 inbound_authorization_expires_at_ms: Option<f64>,
228 }
229
230 #[cfg(all(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
231 #[derive(Debug, Clone, Copy)]
232 struct WasmRemoteCarrierCapabilities {
233 webrtc: bool,
234 webrtc_external: bool,
235 moq: bool,
236 moq_external: bool,
237 }
238
239 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
240 fn emit_wasm_carrier_action(
241 handler: &Rc<RefCell<Option<js_sys::Function>>>,
242 action: serde_json::Value,
243 ) {
244 let Some(handler) = handler.borrow().as_ref().cloned() else {
245 return;
246 };
247 let Ok(value) = serde::Serialize::serialize(
253 &action,
254 &serde_wasm_bindgen::Serializer::json_compatible(),
255 ) else {
256 return;
257 };
258 if let Err(error) = handler.call1(&JsValue::UNDEFINED, &value) {
259 web_sys::console::error_2(
260 &JsValue::from_str("[OpenRTC][WASM carrier] action handler failed"),
261 &error,
262 );
263 }
264 }
265
266 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
267 fn wasm_carrier_failure_code(error: &anyhow::Error) -> &'static str {
268 let message = format!("{error:#}");
269 if message.contains("base generation changed before candidate acknowledgement")
270 || message.contains("incumbent generation changed")
271 || message.contains("replacement incumbent is stale")
272 {
273 "carrier-base-generation-stale"
274 } else if message.contains("authorization epoch changed") {
275 "carrier-authorization-stale"
276 } else if message.contains("stale before atomic commit")
277 || message.contains("became stale during atomic commit")
278 {
279 "carrier-logical-generation-stale"
280 } else if message.contains("attempt was retired") || message.contains("retired upgrade") {
281 "carrier-attempt-retired"
282 } else {
283 "carrier-proof-failed"
284 }
285 }
286
287 #[wasm_bindgen]
288 pub struct WasmClient {
289 inner: Arc<Client>,
290 identity_credential: Arc<Mutex<Option<String>>>,
291 last_auth_log: Arc<Mutex<Option<(bool, usize)>>>,
292 #[cfg(feature = "iroh-protocols-wasm")]
293 persistent_protocols:
294 Rc<tokio::sync::Mutex<Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>>>,
295 #[cfg(feature = "iroh-transport-webrtc")]
296 wasm_webrtc_carrier_sessions:
297 Rc<RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>>,
298 #[cfg(feature = "iroh-transport-webrtc")]
299 wasm_webrtc_carrier_attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
300 #[cfg(feature = "iroh-transport-webrtc")]
301 wasm_webrtc_remote_external: Rc<RefCell<HashMap<String, bool>>>,
302 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
303 wasm_carrier_action_handler: Rc<RefCell<Option<js_sys::Function>>>,
304 #[cfg(feature = "iroh-transport-moq")]
305 wasm_moq_carrier_sessions:
306 Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
307 #[cfg(feature = "iroh-transport-moq")]
308 wasm_moq_carrier_attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
309 #[cfg(feature = "iroh-transport-moq")]
310 wasm_moq_remote_external: Rc<RefCell<HashMap<String, bool>>>,
311 #[cfg(all(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
312 wasm_remote_carrier_capabilities:
313 Rc<RefCell<HashMap<String, WasmRemoteCarrierCapabilities>>>,
314 }
315
316 #[cfg(feature = "iroh-transport-webrtc")]
317 async fn fail_wasm_webrtc_attempt(
318 inner: Arc<Client>,
319 attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
320 sessions: Rc<
321 RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>,
322 >,
323 handler: Rc<RefCell<Option<js_sys::Function>>>,
324 attempt: WasmWebRtcCarrierAttempt,
325 failure_code: &'static str,
326 notify_peer: bool,
327 ) {
328 let kind = crate::client::IrohPathKind::IrohWebRtc;
329 if !inner
330 .retire_wasm_carrier_upgrade(
331 &attempt.connection_id,
332 kind,
333 &attempt.bootstrap.upgrade_id,
334 )
335 .await
336 {
337 return;
338 }
339 if attempts
340 .borrow()
341 .get(&attempt.connection_id)
342 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
343 {
344 attempts.borrow_mut().remove(&attempt.connection_id);
345 }
346 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
347 if notify_peer {
348 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
349 &attempt.bootstrap,
350 failure_code,
351 ) {
352 emit_wasm_carrier_action(
353 &handler,
354 serde_json::json!({
355 "type": "send-control",
356 "connectionId": attempt.connection_id,
357 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
358 "envelope": failed,
359 }),
360 );
361 }
362 }
363 emit_wasm_carrier_action(
364 &handler,
365 serde_json::json!({
366 "type": "retire-webrtc",
367 "connectionId": attempt.connection_id,
368 "upgradeId": attempt.bootstrap.upgrade_id,
369 "failureCode": failure_code,
370 }),
371 );
372 let retry_pending = attempt.retry_count == 0
373 && matches!(
374 failure_code,
375 "data-channel-failed"
376 | "ice-failed"
377 | "signaling-failed"
378 | "carrier-base-generation-stale"
379 );
380 if !attempt.external_fallback_allowed && attempt.role == "initiator" && !retry_pending {
381 emit_wasm_carrier_action(
382 &handler,
383 serde_json::json!({
384 "type": "advance-carrier",
385 "connectionId": attempt.connection_id,
386 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
387 "failedRoute": "iroh-webrtc",
388 }),
389 );
390 }
391 if attempt.external_fallback_allowed
392 && attempt.role == "initiator"
393 && matches!(
394 inner
395 .iroh_path_kind(&attempt.remote_endpoint_id.to_string())
396 .await,
397 crate::client::IrohPathKind::Relay | crate::client::IrohPathKind::Unknown
398 )
399 {
400 emit_wasm_carrier_action(
401 &handler,
402 serde_json::json!({
403 "type": "fallback-external-webrtc",
404 "connectionId": attempt.connection_id,
405 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
406 "failureCode": failure_code,
407 }),
408 );
409 }
410 }
411
412 #[cfg(feature = "iroh-transport-webrtc")]
413 impl WasmClient {
414 fn schedule_wasm_webrtc_carrier_watchdog(
415 &self,
416 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
417 ) {
418 let inner = self.inner.clone();
419 let attempts = self.wasm_webrtc_carrier_attempts.clone();
420 let sessions = self.wasm_webrtc_carrier_sessions.clone();
421 let handler = self.wasm_carrier_action_handler.clone();
422 spawn_local(async move {
423 gloo_timers::future::sleep(std::time::Duration::from_secs(30)).await;
424 let attempt = attempts
425 .borrow()
426 .values()
427 .find(|attempt| attempt.bootstrap.upgrade_id == bootstrap.upgrade_id)
428 .cloned();
429 if let Some(attempt) = attempt {
430 if !inner
431 .wasm_carrier_upgrade_is_current(
432 &attempt.connection_id,
433 crate::client::IrohPathKind::IrohWebRtc,
434 &attempt.bootstrap.upgrade_id,
435 )
436 .await
437 {
438 return;
439 }
440 fail_wasm_webrtc_attempt(
441 inner,
442 attempts,
443 sessions,
444 handler,
445 attempt,
446 "carrier-timeout",
447 true,
448 )
449 .await;
450 }
451 });
452 }
453
454 async fn fail_wasm_webrtc_carrier_attempt(
455 &self,
456 attempt: WasmWebRtcCarrierAttempt,
457 failure_code: &'static str,
458 notify_peer: bool,
459 ) {
460 fail_wasm_webrtc_attempt(
461 self.inner.clone(),
462 self.wasm_webrtc_carrier_attempts.clone(),
463 self.wasm_webrtc_carrier_sessions.clone(),
464 self.wasm_carrier_action_handler.clone(),
465 attempt,
466 failure_code,
467 notify_peer,
468 )
469 .await;
470 }
471
472 fn spawn_outbound_wasm_webrtc_carrier_completion(&self, attempt: WasmWebRtcCarrierAttempt) {
473 let inner = self.inner.clone();
474 let attempts = self.wasm_webrtc_carrier_attempts.clone();
475 let sessions = self.wasm_webrtc_carrier_sessions.clone();
476 let handler = self.wasm_carrier_action_handler.clone();
477 spawn_local(async move {
478 let kind = crate::client::IrohPathKind::IrohWebRtc;
479 let result = inner
480 .complete_outbound_wasm_carrier_upgrade(
481 &attempt.connection_id,
482 &attempt.remote_endpoint_id.to_string(),
483 &attempt.bootstrap.upgrade_id,
484 attempt.generation,
485 kind,
486 )
487 .await;
488 if let Err(error) = result {
489 let failure_code = wasm_carrier_failure_code(&error);
490 web_sys::console::error_1(&JsValue::from_str(&format!(
491 "[OpenRTC][WebRTC carrier] outbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
492 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
493 )));
494 fail_wasm_webrtc_attempt(
495 inner,
496 attempts,
497 sessions,
498 handler,
499 attempt,
500 failure_code,
501 true,
502 )
503 .await;
504 return;
505 }
506 inner
507 .retire_wasm_carrier_upgrade(
508 &attempt.connection_id,
509 kind,
510 &attempt.bootstrap.upgrade_id,
511 )
512 .await;
513 emit_wasm_carrier_action(
514 &handler,
515 serde_json::json!({
516 "type": "selected",
517 "connectionId": attempt.connection_id,
518 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
519 "upgradeId": attempt.bootstrap.upgrade_id,
520 "family": "iroh",
521 "carrier": "webrtc",
522 "implementation": "iroh-carrier",
523 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
524 "routeGeneration": 0,
525 }),
526 );
527 });
528 }
529
530 fn take_ready_outbound_wasm_webrtc_carrier_attempt(
531 &self,
532 connection_id: &str,
533 upgrade_id: &str,
534 ) -> Option<WasmWebRtcCarrierAttempt> {
535 if !self
536 .wasm_webrtc_carrier_sessions
537 .borrow()
538 .contains_key(upgrade_id)
539 {
540 return None;
541 }
542 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
543 let attempt = attempts.get_mut(connection_id).filter(|attempt| {
544 attempt.bootstrap.upgrade_id == upgrade_id
545 && attempt.role == "initiator"
546 && attempt.remote_ready
547 && !attempt.completion_started
548 })?;
549 attempt.completion_started = true;
550 Some(attempt.clone())
551 }
552
553 fn spawn_inbound_wasm_webrtc_carrier_completion(&self, attempt: WasmWebRtcCarrierAttempt) {
554 let inner = self.inner.clone();
555 let attempts = self.wasm_webrtc_carrier_attempts.clone();
556 let sessions = self.wasm_webrtc_carrier_sessions.clone();
557 let handler = self.wasm_carrier_action_handler.clone();
558 spawn_local(async move {
559 let kind = crate::client::IrohPathKind::IrohWebRtc;
560 let node = inner.iroh_node.read().await.as_ref().cloned();
561 let result = async {
562 let node = node.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
563 let candidate = node
564 .wait_for_inbound_replacement_candidate(
565 attempt.remote_endpoint_id,
566 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
567 std::time::Duration::from_secs(40),
568 )
569 .await?;
570 let proof = inner.wasm_candidate_proof_probe(
571 &attempt.connection_id,
572 &attempt.bootstrap.upgrade_id,
573 attempt.bootstrap.base.transport_generation,
574 attempt.bootstrap.base.route_generation,
575 kind,
576 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
577 )?;
578 let (send, probe, ack) = inner
579 .receive_inbound_wasm_carrier_candidate_proof(
580 &attempt.connection_id,
581 &candidate,
582 &proof,
583 )
584 .await?;
585 anyhow::ensure!(
586 inner
587 .current_wasm_peer_data_generation(&attempt.connection_id, None)
588 .await
589 == Some(attempt.generation),
590 "browser WebRTC inbound carrier base generation changed before candidate acknowledgement"
591 );
592 inner
593 .send_inbound_wasm_carrier_candidate_ack(send, &ack)
594 .await?;
595 let (commit_send, committed) = inner
596 .receive_inbound_wasm_carrier_commit(
597 &attempt.connection_id,
598 &candidate,
599 &probe,
600 )
601 .await?;
602 let incumbent = inner
603 .commit_proven_wasm_carrier_candidate(
604 &attempt.connection_id,
605 &attempt.bootstrap.upgrade_id,
606 attempt.generation,
607 kind,
608 candidate,
609 )
610 .await?;
611 inner
612 .send_inbound_wasm_carrier_candidate_ack(commit_send, &committed)
613 .await?;
614 incumbent.close(0u8.into(), b"wasm-custom-transport-upgrade");
615 Ok::<(), anyhow::Error>(())
616 }
617 .await;
618 if let Some(expires_at_ms) = attempt.inbound_authorization_expires_at_ms {
619 if let Some(node) = inner.iroh_node.read().await.as_ref().cloned() {
620 node.revoke_inbound_replacement_if_current(
621 attempt.remote_endpoint_id,
622 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
623 expires_at_ms,
624 )
625 .await;
626 }
627 }
628 if let Err(error) = result {
629 let failure_code = wasm_carrier_failure_code(&error);
630 web_sys::console::error_1(&JsValue::from_str(&format!(
631 "[OpenRTC][WebRTC carrier] inbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
632 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
633 )));
634 fail_wasm_webrtc_attempt(
635 inner,
636 attempts,
637 sessions,
638 handler,
639 attempt,
640 failure_code,
641 true,
642 )
643 .await;
644 return;
645 }
646 inner
647 .retire_wasm_carrier_upgrade(
648 &attempt.connection_id,
649 kind,
650 &attempt.bootstrap.upgrade_id,
651 )
652 .await;
653 emit_wasm_carrier_action(
654 &handler,
655 serde_json::json!({
656 "type": "selected",
657 "connectionId": attempt.connection_id,
658 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
659 "upgradeId": attempt.bootstrap.upgrade_id,
660 "family": "iroh",
661 "carrier": "webrtc",
662 "implementation": "iroh-carrier",
663 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
664 "routeGeneration": 0,
665 }),
666 );
667 });
668 }
669 }
670
671 #[cfg(feature = "iroh-transport-moq")]
672 fn wasm_moq_carrier_namespaces(
673 local_endpoint_id: &str,
674 remote_endpoint_id: &str,
675 carrier_session_id: &str,
676 ) -> (String, String, &'static str) {
677 let (first, second) = if local_endpoint_id <= remote_endpoint_id {
678 (local_endpoint_id, remote_endpoint_id)
679 } else {
680 (remote_endpoint_id, local_endpoint_id)
681 };
682 let base = format!("openrtc/iroh-carrier/moq/{first}/{second}/{carrier_session_id}");
683 (
684 format!("{base}/from/{local_endpoint_id}"),
685 format!("{base}/from/{remote_endpoint_id}"),
686 "iroh-packets",
687 )
688 }
689
690 #[cfg(feature = "iroh-transport-moq")]
691 async fn fail_wasm_moq_attempt(
692 inner: Arc<Client>,
693 attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
694 sessions: Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
695 handler: Rc<RefCell<Option<js_sys::Function>>>,
696 attempt: WasmMoqCarrierAttempt,
697 failure_code: &'static str,
698 notify_peer: bool,
699 ) {
700 let kind = crate::client::IrohPathKind::IrohMoq;
701 if !inner
702 .retire_wasm_carrier_upgrade(
703 &attempt.connection_id,
704 kind,
705 &attempt.bootstrap.upgrade_id,
706 )
707 .await
708 {
709 return;
710 }
711 if attempts
712 .borrow()
713 .get(&attempt.connection_id)
714 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
715 {
716 attempts.borrow_mut().remove(&attempt.connection_id);
717 }
718 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
719 if notify_peer {
720 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
721 &attempt.bootstrap,
722 failure_code,
723 ) {
724 emit_wasm_carrier_action(
725 &handler,
726 serde_json::json!({
727 "type": "send-control",
728 "connectionId": attempt.connection_id,
729 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
730 "envelope": failed,
731 }),
732 );
733 }
734 }
735 emit_wasm_carrier_action(
736 &handler,
737 serde_json::json!({
738 "type": "retire-moq",
739 "connectionId": attempt.connection_id,
740 "upgradeId": attempt.bootstrap.upgrade_id,
741 "failureCode": failure_code,
742 }),
743 );
744 let retry_pending = attempt.role == "initiator"
745 && attempt.retry_count == 0
746 && failure_code == "carrier-base-generation-stale";
747 if !attempt.external_fallback_allowed && attempt.role == "initiator" && !retry_pending {
748 emit_wasm_carrier_action(
749 &handler,
750 serde_json::json!({
751 "type": "advance-carrier",
752 "connectionId": attempt.connection_id,
753 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
754 "failedRoute": "iroh-moq",
755 }),
756 );
757 }
758 if attempt.external_fallback_allowed
759 && attempt.role == "initiator"
760 && !retry_pending
761 && matches!(
762 inner
763 .iroh_path_kind(&attempt.remote_endpoint_id.to_string())
764 .await,
765 crate::client::IrohPathKind::Relay | crate::client::IrohPathKind::Unknown
766 )
767 {
768 emit_wasm_carrier_action(
769 &handler,
770 serde_json::json!({
771 "type": "fallback-external-moq",
772 "connectionId": attempt.connection_id,
773 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
774 "failureCode": failure_code,
775 }),
776 );
777 }
778 }
779
780 #[cfg(feature = "iroh-transport-moq")]
781 impl WasmClient {
782 fn schedule_wasm_moq_carrier_watchdog(
783 &self,
784 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
785 ) {
786 let inner = self.inner.clone();
787 let attempts = self.wasm_moq_carrier_attempts.clone();
788 let sessions = self.wasm_moq_carrier_sessions.clone();
789 let handler = self.wasm_carrier_action_handler.clone();
790 spawn_local(async move {
791 gloo_timers::future::sleep(std::time::Duration::from_secs(30)).await;
792 let attempt = attempts
793 .borrow()
794 .values()
795 .find(|attempt| attempt.bootstrap.upgrade_id == bootstrap.upgrade_id)
796 .cloned();
797 if let Some(attempt) = attempt {
798 if !inner
799 .wasm_carrier_upgrade_is_current(
800 &attempt.connection_id,
801 crate::client::IrohPathKind::IrohMoq,
802 &attempt.bootstrap.upgrade_id,
803 )
804 .await
805 {
806 return;
807 }
808 fail_wasm_moq_attempt(
809 inner,
810 attempts,
811 sessions,
812 handler,
813 attempt,
814 "carrier-timeout",
815 true,
816 )
817 .await;
818 }
819 });
820 }
821
822 async fn fail_wasm_moq_carrier_attempt(
823 &self,
824 attempt: WasmMoqCarrierAttempt,
825 failure_code: &'static str,
826 notify_peer: bool,
827 ) {
828 fail_wasm_moq_attempt(
829 self.inner.clone(),
830 self.wasm_moq_carrier_attempts.clone(),
831 self.wasm_moq_carrier_sessions.clone(),
832 self.wasm_carrier_action_handler.clone(),
833 attempt,
834 failure_code,
835 notify_peer,
836 )
837 .await;
838 }
839
840 fn spawn_outbound_wasm_moq_carrier_completion(&self, attempt: WasmMoqCarrierAttempt) {
841 let inner = self.inner.clone();
842 let attempts = self.wasm_moq_carrier_attempts.clone();
843 let sessions = self.wasm_moq_carrier_sessions.clone();
844 let handler = self.wasm_carrier_action_handler.clone();
845 spawn_local(async move {
846 let kind = crate::client::IrohPathKind::IrohMoq;
847 let result = inner
848 .complete_outbound_wasm_carrier_upgrade(
849 &attempt.connection_id,
850 &attempt.remote_endpoint_id.to_string(),
851 &attempt.bootstrap.upgrade_id,
852 attempt.generation,
853 kind,
854 )
855 .await;
856 if let Err(error) = result {
857 let failure_code = wasm_carrier_failure_code(&error);
858 web_sys::console::error_1(&JsValue::from_str(&format!(
859 "[OpenRTC][MoQ carrier] outbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
860 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
861 )));
862 fail_wasm_moq_attempt(
863 inner,
864 attempts,
865 sessions,
866 handler,
867 attempt,
868 failure_code,
869 true,
870 )
871 .await;
872 return;
873 }
874 inner
875 .retire_wasm_carrier_upgrade(
876 &attempt.connection_id,
877 kind,
878 &attempt.bootstrap.upgrade_id,
879 )
880 .await;
881 emit_wasm_carrier_action(
882 &handler,
883 serde_json::json!({
884 "type": "selected",
885 "connectionId": attempt.connection_id,
886 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
887 "upgradeId": attempt.bootstrap.upgrade_id,
888 "family": "iroh",
889 "carrier": "moq",
890 "implementation": "iroh-carrier",
891 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
892 "routeGeneration": 0,
893 }),
894 );
895 });
896 }
897
898 fn spawn_inbound_wasm_moq_carrier_completion(&self, attempt: WasmMoqCarrierAttempt) {
899 let inner = self.inner.clone();
900 let attempts = self.wasm_moq_carrier_attempts.clone();
901 let sessions = self.wasm_moq_carrier_sessions.clone();
902 let handler = self.wasm_carrier_action_handler.clone();
903 spawn_local(async move {
904 let kind = crate::client::IrohPathKind::IrohMoq;
905 let node = inner.iroh_node.read().await.as_ref().cloned();
906 let result = async {
907 let node = node.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
908 let candidate = node
909 .wait_for_inbound_replacement_candidate(
910 attempt.remote_endpoint_id,
911 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
912 std::time::Duration::from_secs(40),
913 )
914 .await?;
915 let proof = inner.wasm_candidate_proof_probe(
916 &attempt.connection_id,
917 &attempt.bootstrap.upgrade_id,
918 attempt.bootstrap.base.transport_generation,
919 attempt.bootstrap.base.route_generation,
920 kind,
921 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
922 )?;
923 let (send, probe, ack) = inner
924 .receive_inbound_wasm_carrier_candidate_proof(
925 &attempt.connection_id,
926 &candidate,
927 &proof,
928 )
929 .await?;
930 anyhow::ensure!(
931 inner
932 .current_wasm_peer_data_generation(&attempt.connection_id, None)
933 .await
934 == Some(attempt.generation),
935 "browser MoQ inbound carrier base generation changed before candidate acknowledgement"
936 );
937 inner
938 .send_inbound_wasm_carrier_candidate_ack(send, &ack)
939 .await?;
940 let (commit_send, committed) = inner
941 .receive_inbound_wasm_carrier_commit(
942 &attempt.connection_id,
943 &candidate,
944 &probe,
945 )
946 .await?;
947 let incumbent = inner
948 .commit_proven_wasm_carrier_candidate(
949 &attempt.connection_id,
950 &attempt.bootstrap.upgrade_id,
951 attempt.generation,
952 kind,
953 candidate,
954 )
955 .await?;
956 inner
957 .send_inbound_wasm_carrier_candidate_ack(commit_send, &committed)
958 .await?;
959 incumbent.close(0u8.into(), b"wasm-custom-transport-upgrade");
960 Ok::<(), anyhow::Error>(())
961 }
962 .await;
963 if let Some(expires_at_ms) = attempt.inbound_authorization_expires_at_ms {
964 if let Some(node) = inner.iroh_node.read().await.as_ref().cloned() {
965 node.revoke_inbound_replacement_if_current(
966 attempt.remote_endpoint_id,
967 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
968 expires_at_ms,
969 )
970 .await;
971 }
972 }
973 if let Err(error) = result {
974 let failure_code = wasm_carrier_failure_code(&error);
975 web_sys::console::error_1(&JsValue::from_str(&format!(
976 "[OpenRTC][MoQ carrier] inbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
977 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
978 )));
979 fail_wasm_moq_attempt(
980 inner,
981 attempts,
982 sessions,
983 handler,
984 attempt,
985 failure_code,
986 true,
987 )
988 .await;
989 return;
990 }
991 inner
992 .retire_wasm_carrier_upgrade(
993 &attempt.connection_id,
994 kind,
995 &attempt.bootstrap.upgrade_id,
996 )
997 .await;
998 emit_wasm_carrier_action(
999 &handler,
1000 serde_json::json!({
1001 "type": "selected",
1002 "connectionId": attempt.connection_id,
1003 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1004 "upgradeId": attempt.bootstrap.upgrade_id,
1005 "family": "iroh",
1006 "carrier": "moq",
1007 "implementation": "iroh-carrier",
1008 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
1009 "routeGeneration": 0,
1010 }),
1011 );
1012 });
1013 }
1014
1015 async fn handle_wasm_moq_bootstrap(
1016 &self,
1017 connection_id: String,
1018 remote_endpoint_id: String,
1019 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1020 ) -> Result<bool, JsValue> {
1021 let endpoint_id = remote_endpoint_id
1022 .parse::<iroh::EndpointId>()
1023 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1024 let kind = crate::client::IrohPathKind::IrohMoq;
1025 match bootstrap.action {
1026 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
1027 let local_endpoint_id = self
1028 .inner
1029 .current_node_id()
1030 .await
1031 .ok_or_else(|| JsValue::from_str("local endpoint id is unavailable"))?;
1032 if local_endpoint_id.as_str() >= remote_endpoint_id.as_str()
1033 || !self.inner.is_iroh_moq_carrier_enabled().await
1034 {
1035 return Ok(true);
1036 }
1037 if !matches!(
1038 self.inner.iroh_path_kind(&remote_endpoint_id).await,
1039 crate::client::IrohPathKind::Relay | crate::client::IrohPathKind::Unknown
1040 ) {
1041 return Ok(true);
1042 }
1043 let base_connection = self
1044 .inner
1045 .get_connection(endpoint_id)
1046 .await
1047 .ok_or_else(|| JsValue::from_str("MoQ carrier base is unavailable"))?;
1048 let generation = self
1049 .inner
1050 .current_wasm_peer_data_generation(&connection_id, None)
1051 .await
1052 .ok_or_else(|| {
1053 JsValue::from_str("MoQ carrier generation is unavailable")
1054 })?;
1055 if crate::transport_generation::for_connection(&base_connection)
1056 != generation.transport_stable_id
1057 {
1058 let failed =
1059 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
1060 &bootstrap,
1061 "carrier-base-generation-stale",
1062 )
1063 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1064 emit_wasm_carrier_action(
1065 &self.wasm_carrier_action_handler,
1066 serde_json::json!({
1067 "type": "send-control",
1068 "connectionId": connection_id,
1069 "remoteEndpointId": remote_endpoint_id,
1070 "envelope": failed,
1071 }),
1072 );
1073 return Ok(true);
1074 }
1075 if !self
1076 .inner
1077 .reserve_wasm_carrier_upgrade(&connection_id, kind, &bootstrap.upgrade_id)
1078 .await
1079 {
1080 return Ok(true);
1081 }
1082 let node = self
1083 .inner
1084 .iroh_node
1085 .read()
1086 .await
1087 .as_ref()
1088 .cloned()
1089 .ok_or_else(|| JsValue::from_str("Iroh node is unavailable"))?;
1090 let authorization_expiry = node
1091 .authorize_pending_inbound_replacement(
1092 endpoint_id,
1093 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1094 std::time::Duration::from_secs(45),
1095 )
1096 .await;
1097 let fallback_allowed = self.inner.wasm_moq_external_fallback_allowed().await
1098 && self
1099 .wasm_moq_remote_external
1100 .borrow()
1101 .get(&connection_id)
1102 .copied()
1103 .unwrap_or(false);
1104 let previous = self.wasm_moq_carrier_attempts.borrow_mut().insert(
1105 connection_id.clone(),
1106 WasmMoqCarrierAttempt {
1107 connection_id: connection_id.clone(),
1108 remote_endpoint_id: endpoint_id,
1109 bootstrap: bootstrap.clone(),
1110 generation,
1111 role: "responder",
1112 retry_count: bootstrap.attempt.saturating_sub(1),
1113 external_fallback_allowed: fallback_allowed,
1114 inbound_authorization_expires_at_ms: Some(authorization_expiry),
1115 },
1116 );
1117 if let Some(previous) = previous {
1118 self.wasm_moq_carrier_sessions
1119 .borrow_mut()
1120 .remove(&previous.bootstrap.upgrade_id);
1121 }
1122 let (publish_namespace, subscribe_namespace, track_name) =
1123 wasm_moq_carrier_namespaces(
1124 &local_endpoint_id,
1125 &remote_endpoint_id,
1126 &bootstrap.carrier_session_id,
1127 );
1128 emit_wasm_carrier_action(
1129 &self.wasm_carrier_action_handler,
1130 serde_json::json!({
1131 "type": "prepare-moq",
1132 "connectionId": connection_id,
1133 "remoteEndpointId": remote_endpoint_id,
1134 "role": "responder",
1135 "upgradeId": bootstrap.upgrade_id,
1136 "carrierSessionId": bootstrap.carrier_session_id,
1137 "transportGeneration": generation.transport_generation.saturating_add(1),
1138 "publishNamespace": publish_namespace,
1139 "subscribeNamespace": subscribe_namespace,
1140 "trackName": track_name,
1141 }),
1142 );
1143 self.schedule_wasm_moq_carrier_watchdog(bootstrap);
1144 }
1145 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
1146 let attempt = self
1147 .wasm_moq_carrier_attempts
1148 .borrow()
1149 .get(&connection_id)
1150 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
1151 .cloned();
1152 if let Some(attempt) = attempt {
1153 emit_wasm_carrier_action(
1154 &self.wasm_carrier_action_handler,
1155 serde_json::json!({
1156 "type": "activate-moq",
1157 "connectionId": attempt.connection_id,
1158 "upgradeId": attempt.bootstrap.upgrade_id,
1159 }),
1160 );
1161 }
1162 }
1163 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
1164 let attempt = self
1165 .wasm_moq_carrier_attempts
1166 .borrow()
1167 .get(&connection_id)
1168 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
1169 .cloned();
1170 if let Some(attempt) = attempt {
1171 let failure_code = match bootstrap.failure_code.as_deref() {
1172 Some("carrier-base-generation-stale") => {
1173 "carrier-base-generation-stale"
1174 }
1175 _ => "peer-rejected-carrier",
1176 };
1177 let should_retry = attempt.role == "initiator"
1178 && attempt.retry_count == 0
1179 && failure_code == "carrier-base-generation-stale";
1180 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
1181 let remote_supports_external = self
1182 .wasm_moq_remote_external
1183 .borrow()
1184 .get(&connection_id)
1185 .copied()
1186 .unwrap_or(false);
1187 self.fail_wasm_moq_carrier_attempt(attempt, failure_code, false)
1188 .await;
1189 if should_retry {
1190 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
1194 let _ = self
1195 .begin_iroh_moq_carrier_attempt(
1196 connection_id,
1197 remote_endpoint_id,
1198 remote_supports_external,
1199 1,
1200 )
1201 .await;
1202 }
1203 }
1204 }
1205 }
1206 Ok(true)
1207 }
1208 }
1209
1210 #[wasm_bindgen]
1211 impl WasmClient {
1212 #[wasm_bindgen(js_name = newV2)]
1216 pub fn new_v2(api_key: String) -> Result<WasmClient, JsValue> {
1217 let api_key = crate::validate_v2_public_api_key(&api_key)
1218 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1219 let app_tag = crate::app_tag_from_api_key(api_key);
1220 let identity_credential: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
1221 let credential_state = identity_credential.clone();
1222 let identity_credential_provider = Box::new(move || -> Option<String> {
1223 credential_state.lock().ok().and_then(|guard| guard.clone())
1224 });
1225
1226 Ok(Self {
1227 inner: Arc::new(Client::new_provider_neutral(
1228 app_tag,
1229 identity_credential_provider,
1230 )),
1231 identity_credential,
1232 last_auth_log: Arc::new(Mutex::new(None)),
1233 #[cfg(feature = "iroh-protocols-wasm")]
1234 persistent_protocols: Rc::new(tokio::sync::Mutex::new(None)),
1235 #[cfg(feature = "iroh-transport-webrtc")]
1236 wasm_webrtc_carrier_sessions: Rc::new(RefCell::new(HashMap::new())),
1237 #[cfg(feature = "iroh-transport-webrtc")]
1238 wasm_webrtc_carrier_attempts: Rc::new(RefCell::new(HashMap::new())),
1239 #[cfg(feature = "iroh-transport-webrtc")]
1240 wasm_webrtc_remote_external: Rc::new(RefCell::new(HashMap::new())),
1241 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
1242 wasm_carrier_action_handler: Rc::new(RefCell::new(None)),
1243 #[cfg(feature = "iroh-transport-moq")]
1244 wasm_moq_carrier_sessions: Rc::new(RefCell::new(HashMap::new())),
1245 #[cfg(feature = "iroh-transport-moq")]
1246 wasm_moq_carrier_attempts: Rc::new(RefCell::new(HashMap::new())),
1247 #[cfg(feature = "iroh-transport-moq")]
1248 wasm_moq_remote_external: Rc::new(RefCell::new(HashMap::new())),
1249 #[cfg(all(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
1250 wasm_remote_carrier_capabilities: Rc::new(RefCell::new(HashMap::new())),
1251 })
1252 }
1253
1254 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
1258 #[wasm_bindgen(js_name = __setIrohCarrierActionHandler)]
1259 pub fn set_iroh_carrier_action_handler(&self, handler: Option<js_sys::Function>) {
1260 *self.wasm_carrier_action_handler.borrow_mut() = handler;
1261 }
1262
1263 #[cfg(feature = "iroh-transport-webrtc")]
1264 #[wasm_bindgen(js_name = __configureIrohWebRtcCarrier)]
1265 pub async fn configure_iroh_webrtc_carrier(
1266 &self,
1267 enabled: bool,
1268 implementation: Option<String>,
1269 privacy_mode: bool,
1270 ) -> Result<(), JsValue> {
1271 let implementation = if !enabled {
1272 None
1273 } else {
1274 Some(match implementation.as_deref().unwrap_or("iroh-carrier") {
1275 "external" => crate::client::TransportImplementation::External,
1276 "iroh" | "iroh-carrier" => crate::client::TransportImplementation::IrohCarrier,
1277 "auto" => crate::client::TransportImplementation::Auto,
1278 _ => {
1279 return Err(JsValue::from_str(
1280 "WebRTC implementation must be external, iroh-carrier, or auto",
1281 ))
1282 }
1283 })
1284 };
1285 self.inner
1286 .configure_wasm_webrtc_implementation(implementation, privacy_mode)
1287 .await;
1288 Ok(())
1289 }
1290
1291 #[cfg(any(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
1294 #[wasm_bindgen(js_name = __configureIrohRoutePolicy)]
1295 pub async fn configure_iroh_route_policy(
1296 &self,
1297 relay_only: bool,
1298 optimize_for: Option<String>,
1299 route_priority: JsValue,
1300 ) -> Result<(), JsValue> {
1301 let optimize_for = match optimize_for.as_deref().unwrap_or("balanced") {
1302 "balanced" => crate::route_policy::TransportOptimization::Balanced,
1303 "lowest-latency" => crate::route_policy::TransportOptimization::LowestLatency,
1304 _ => {
1305 return Err(JsValue::from_str(
1306 "transport optimization must be balanced or lowest-latency",
1307 ))
1308 }
1309 };
1310 let route_priority = if route_priority.is_null() || route_priority.is_undefined() {
1311 Vec::new()
1312 } else {
1313 serde_wasm_bindgen::from_value(route_priority)
1314 .map_err(|error| JsValue::from_str(&error.to_string()))?
1315 };
1316 self.inner
1317 .configure_wasm_route_policy(relay_only, optimize_for, route_priority)
1318 .await;
1319 Ok(())
1320 }
1321
1322 #[cfg(all(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
1325 #[wasm_bindgen(js_name = __beginPreferredIrohCarrier)]
1326 pub async fn begin_preferred_iroh_carrier(
1327 &self,
1328 connection_id: String,
1329 remote_endpoint_id: String,
1330 remote_supports_webrtc: bool,
1331 remote_supports_webrtc_external: bool,
1332 remote_supports_moq: bool,
1333 remote_supports_moq_external: bool,
1334 ) -> Result<bool, JsValue> {
1335 let capabilities = WasmRemoteCarrierCapabilities {
1336 webrtc: remote_supports_webrtc,
1337 webrtc_external: remote_supports_webrtc_external,
1338 moq: remote_supports_moq,
1339 moq_external: remote_supports_moq_external,
1340 };
1341 self.wasm_remote_carrier_capabilities
1342 .borrow_mut()
1343 .insert(connection_id.clone(), capabilities);
1344 let ranked = self
1345 .inner
1346 .ranked_wasm_iroh_carriers(remote_supports_webrtc, remote_supports_moq)
1347 .await;
1348 let has_next = ranked.len() > 1;
1349 match ranked.first() {
1350 Some(crate::route_policy::KnownRoute::IrohWebRtc) => {
1351 self.begin_iroh_webrtc_carrier(
1352 connection_id,
1353 remote_endpoint_id,
1354 remote_supports_webrtc_external && !has_next,
1355 )
1356 .await
1357 }
1358 Some(crate::route_policy::KnownRoute::IrohMoq) => {
1359 self.begin_iroh_moq_carrier(
1360 connection_id,
1361 remote_endpoint_id,
1362 remote_supports_moq_external && !has_next,
1363 )
1364 .await
1365 }
1366 _ => Ok(false),
1367 }
1368 }
1369
1370 #[cfg(all(feature = "iroh-transport-webrtc", feature = "iroh-transport-moq"))]
1373 #[wasm_bindgen(js_name = __advancePreferredIrohCarrier)]
1374 pub async fn advance_preferred_iroh_carrier(
1375 &self,
1376 connection_id: String,
1377 remote_endpoint_id: String,
1378 failed_route: String,
1379 ) -> Result<bool, JsValue> {
1380 let Some(capabilities) = self
1381 .wasm_remote_carrier_capabilities
1382 .borrow()
1383 .get(&connection_id)
1384 .copied()
1385 else {
1386 return Ok(false);
1387 };
1388 let failed = crate::route_policy::normalize_route(&failed_route)
1389 .ok_or_else(|| JsValue::from_str("unknown failed carrier route"))?;
1390 let ranked = self
1391 .inner
1392 .ranked_wasm_iroh_carriers(capabilities.webrtc, capabilities.moq)
1393 .await;
1394 let Some(next_index) = ranked
1395 .iter()
1396 .position(|route| *route == failed)
1397 .map(|index| index + 1)
1398 .filter(|index| *index < ranked.len())
1399 else {
1400 return Ok(false);
1401 };
1402 let next = ranked[next_index];
1403 let allow_external = next_index + 1 == ranked.len();
1404 match next {
1405 crate::route_policy::KnownRoute::IrohWebRtc => {
1406 self.begin_iroh_webrtc_carrier(
1407 connection_id,
1408 remote_endpoint_id,
1409 capabilities.webrtc_external && allow_external,
1410 )
1411 .await
1412 }
1413 crate::route_policy::KnownRoute::IrohMoq => {
1414 self.begin_iroh_moq_carrier(
1415 connection_id,
1416 remote_endpoint_id,
1417 capabilities.moq_external && allow_external,
1418 )
1419 .await
1420 }
1421 _ => Ok(false),
1422 }
1423 }
1424
1425 #[cfg(feature = "iroh-transport-moq")]
1426 #[wasm_bindgen(js_name = __configureIrohMoqCarrier)]
1427 pub async fn configure_iroh_moq_carrier(
1428 &self,
1429 enabled: bool,
1430 implementation: Option<String>,
1431 relay_url: Option<String>,
1432 ) -> Result<(), JsValue> {
1433 let implementation = if !enabled {
1434 None
1435 } else {
1436 Some(match implementation.as_deref().unwrap_or("external") {
1437 "external" => crate::client::TransportImplementation::External,
1438 "iroh" | "iroh-carrier" => crate::client::TransportImplementation::IrohCarrier,
1439 "auto" => crate::client::TransportImplementation::Auto,
1440 _ => {
1441 return Err(JsValue::from_str(
1442 "MoQ implementation must be external, iroh-carrier, or auto",
1443 ))
1444 }
1445 })
1446 };
1447 let relay_url = relay_url.map(|value| value.trim().to_string());
1448 if implementation.is_some()
1449 && implementation.is_some_and(|value| value.allows_iroh_carrier())
1450 && relay_url.as_deref().unwrap_or_default().is_empty()
1451 {
1452 return Err(JsValue::from_str(
1453 "MoQ Iroh carrier requires an explicit relay URL",
1454 ));
1455 }
1456 self.inner
1457 .configure_wasm_moq_implementation(implementation, relay_url)
1458 .await;
1459 Ok(())
1460 }
1461
1462 #[cfg(feature = "iroh-transport-moq")]
1465 #[wasm_bindgen(js_name = __beginIrohMoqCarrier)]
1466 pub async fn begin_iroh_moq_carrier(
1467 &self,
1468 connection_id: String,
1469 remote_endpoint_id: String,
1470 remote_supports_external: bool,
1471 ) -> Result<bool, JsValue> {
1472 self.begin_iroh_moq_carrier_attempt(
1473 connection_id,
1474 remote_endpoint_id,
1475 remote_supports_external,
1476 0,
1477 )
1478 .await
1479 }
1480
1481 async fn begin_iroh_moq_carrier_attempt(
1482 &self,
1483 connection_id: String,
1484 remote_endpoint_id: String,
1485 remote_supports_external: bool,
1486 retry_count: u8,
1487 ) -> Result<bool, JsValue> {
1488 self.wasm_moq_remote_external
1489 .borrow_mut()
1490 .insert(connection_id.clone(), remote_supports_external);
1491 if !self.inner.is_iroh_moq_carrier_enabled().await {
1492 return Ok(false);
1493 }
1494 let local_endpoint_id = self
1495 .inner
1496 .current_node_id()
1497 .await
1498 .ok_or_else(|| JsValue::from_str("local Iroh endpoint id is unavailable"))?;
1499 if local_endpoint_id.as_str() <= remote_endpoint_id.as_str()
1500 || !matches!(
1501 self.inner.iroh_path_kind(&remote_endpoint_id).await,
1502 crate::client::IrohPathKind::Relay | crate::client::IrohPathKind::Unknown
1503 )
1504 {
1505 return Ok(false);
1506 }
1507 let endpoint_id = remote_endpoint_id
1508 .parse::<iroh::EndpointId>()
1509 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1510 self.inner
1511 .get_connection(endpoint_id)
1512 .await
1513 .ok_or_else(|| JsValue::from_str("MoQ carrier base is unavailable"))?;
1514 let generation = self
1515 .inner
1516 .current_wasm_peer_data_generation(&connection_id, None)
1517 .await
1518 .ok_or_else(|| JsValue::from_str("MoQ carrier generation is unavailable"))?;
1519 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
1520 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
1521 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
1522 transport_stable_id: generation.transport_stable_id,
1523 transport_generation: generation.transport_generation,
1524 route_generation: generation.route_generation,
1525 },
1526 retry_count.saturating_add(1),
1527 )
1528 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1529 let kind = crate::client::IrohPathKind::IrohMoq;
1530 if !self
1531 .inner
1532 .reserve_wasm_carrier_upgrade(&connection_id, kind, &bootstrap.upgrade_id)
1533 .await
1534 {
1535 return Ok(false);
1536 }
1537 let fallback_allowed =
1538 self.inner.wasm_moq_external_fallback_allowed().await && remote_supports_external;
1539 let previous = self.wasm_moq_carrier_attempts.borrow_mut().insert(
1540 connection_id.clone(),
1541 WasmMoqCarrierAttempt {
1542 connection_id: connection_id.clone(),
1543 remote_endpoint_id: endpoint_id,
1544 bootstrap: bootstrap.clone(),
1545 generation,
1546 role: "initiator",
1547 retry_count,
1548 external_fallback_allowed: fallback_allowed,
1549 inbound_authorization_expires_at_ms: None,
1550 },
1551 );
1552 if let Some(previous) = previous {
1553 self.wasm_moq_carrier_sessions
1554 .borrow_mut()
1555 .remove(&previous.bootstrap.upgrade_id);
1556 }
1557 let (publish_namespace, subscribe_namespace, track_name) = wasm_moq_carrier_namespaces(
1558 &local_endpoint_id,
1559 &remote_endpoint_id,
1560 &bootstrap.carrier_session_id,
1561 );
1562 emit_wasm_carrier_action(
1563 &self.wasm_carrier_action_handler,
1564 serde_json::json!({
1565 "type": "prepare-moq",
1566 "connectionId": connection_id,
1567 "remoteEndpointId": remote_endpoint_id,
1568 "role": "initiator",
1569 "upgradeId": bootstrap.upgrade_id,
1570 "carrierSessionId": bootstrap.carrier_session_id,
1571 "transportGeneration": generation.transport_generation.saturating_add(1),
1572 "publishNamespace": publish_namespace,
1573 "subscribeNamespace": subscribe_namespace,
1574 "trackName": track_name,
1575 }),
1576 );
1577 self.schedule_wasm_moq_carrier_watchdog(bootstrap);
1578 Ok(true)
1579 }
1580
1581 #[cfg(feature = "iroh-transport-moq")]
1582 #[wasm_bindgen(js_name = __irohMoqCarrierPrepared)]
1583 pub async fn iroh_moq_carrier_prepared(
1584 &self,
1585 connection_id: String,
1586 upgrade_id: String,
1587 ) -> Result<(), JsValue> {
1588 let attempt = self
1589 .wasm_moq_carrier_attempts
1590 .borrow()
1591 .get(&connection_id)
1592 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
1593 .cloned()
1594 .ok_or_else(|| JsValue::from_str("MoQ carrier attempt is stale"))?;
1595 if !self
1596 .inner
1597 .wasm_carrier_upgrade_is_current(
1598 &connection_id,
1599 crate::client::IrohPathKind::IrohMoq,
1600 &upgrade_id,
1601 )
1602 .await
1603 {
1604 return Err(JsValue::from_str("MoQ carrier attempt was retired"));
1605 }
1606 if attempt.role == "initiator" {
1607 emit_wasm_carrier_action(
1608 &self.wasm_carrier_action_handler,
1609 serde_json::json!({
1610 "type": "send-control",
1611 "connectionId": connection_id,
1612 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1613 "envelope": attempt.bootstrap,
1614 }),
1615 );
1616 } else {
1617 let ready = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
1618 &attempt.bootstrap,
1619 )
1620 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1621 emit_wasm_carrier_action(
1622 &self.wasm_carrier_action_handler,
1623 serde_json::json!({
1624 "type": "send-control",
1625 "connectionId": connection_id,
1626 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1627 "envelope": ready,
1628 }),
1629 );
1630 emit_wasm_carrier_action(
1631 &self.wasm_carrier_action_handler,
1632 serde_json::json!({
1633 "type": "activate-moq",
1634 "connectionId": attempt.connection_id,
1635 "upgradeId": upgrade_id,
1636 }),
1637 );
1638 }
1639 Ok(())
1640 }
1641
1642 #[cfg(feature = "iroh-transport-moq")]
1643 #[wasm_bindgen(js_name = __irohMoqCarrierFailed)]
1644 pub async fn iroh_moq_carrier_failed(
1645 &self,
1646 connection_id: String,
1647 upgrade_id: String,
1648 failure_code: String,
1649 ) {
1650 let attempt = self
1651 .wasm_moq_carrier_attempts
1652 .borrow()
1653 .get(&connection_id)
1654 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
1655 .cloned();
1656 if let Some(attempt) = attempt {
1657 let failure_code = match failure_code.as_str() {
1658 "relay-failed" => "relay-failed",
1659 "draft14-datagram-unavailable" => "draft14-datagram-unavailable",
1660 "carrier-backpressure" => "carrier-backpressure",
1661 _ => "browser-adapter-failed",
1662 };
1663 self.fail_wasm_moq_carrier_attempt(attempt, failure_code, true)
1664 .await;
1665 }
1666 }
1667
1668 #[cfg(feature = "iroh-transport-moq")]
1669 #[wasm_bindgen(js_name = __retireIrohMoqCarrier)]
1670 pub async fn retire_iroh_moq_carrier(
1671 &self,
1672 connection_id: String,
1673 terminal_reason: Option<String>,
1674 ) {
1675 self.retire_iroh_moq_carrier_with_reason(connection_id, terminal_reason.as_deref())
1676 .await;
1677 }
1678
1679 #[cfg(feature = "iroh-transport-moq")]
1680 async fn retire_iroh_moq_carrier_with_reason(
1681 &self,
1682 connection_id: String,
1683 terminal_reason: Option<&str>,
1684 ) {
1685 let attempt = self
1686 .wasm_moq_carrier_attempts
1687 .borrow_mut()
1688 .remove(&connection_id);
1689 if let Some(attempt) = attempt {
1690 self.inner
1691 .retire_wasm_carrier_upgrade(
1692 &connection_id,
1693 crate::client::IrohPathKind::IrohMoq,
1694 &attempt.bootstrap.upgrade_id,
1695 )
1696 .await;
1697 let carrier = self
1698 .wasm_moq_carrier_sessions
1699 .borrow_mut()
1700 .remove(&attempt.bootstrap.upgrade_id);
1701 if let (Some(reason), Some(session)) = (terminal_reason, carrier.as_ref()) {
1702 let _ = session.send_terminal(reason).await;
1703 }
1704 emit_wasm_carrier_action(
1705 &self.wasm_carrier_action_handler,
1706 serde_json::json!({
1707 "type": "retire-moq",
1708 "connectionId": connection_id,
1709 "upgradeId": attempt.bootstrap.upgrade_id,
1710 "failureCode": "logical-connection-retired",
1711 }),
1712 );
1713 }
1714 self.wasm_moq_remote_external
1715 .borrow_mut()
1716 .remove(&connection_id);
1717 #[cfg(feature = "iroh-transport-webrtc")]
1718 self.wasm_remote_carrier_capabilities
1719 .borrow_mut()
1720 .remove(&connection_id);
1721 }
1722
1723 #[cfg(feature = "iroh-transport-webrtc")]
1726 #[wasm_bindgen(js_name = __beginIrohWebRtcCarrier)]
1727 pub async fn begin_iroh_webrtc_carrier(
1728 &self,
1729 connection_id: String,
1730 remote_endpoint_id: String,
1731 remote_supports_external: bool,
1732 ) -> Result<bool, JsValue> {
1733 self.begin_iroh_webrtc_carrier_attempt(
1734 connection_id,
1735 remote_endpoint_id,
1736 remote_supports_external,
1737 0,
1738 )
1739 .await
1740 }
1741
1742 async fn begin_iroh_webrtc_carrier_attempt(
1743 &self,
1744 connection_id: String,
1745 remote_endpoint_id: String,
1746 remote_supports_external: bool,
1747 retry_count: u8,
1748 ) -> Result<bool, JsValue> {
1749 self.wasm_webrtc_remote_external
1750 .borrow_mut()
1751 .insert(connection_id.clone(), remote_supports_external);
1752 if !self.inner.is_iroh_webrtc_carrier_enabled().await {
1753 return Ok(false);
1754 }
1755 let local_endpoint_id = self
1756 .inner
1757 .current_node_id()
1758 .await
1759 .ok_or_else(|| JsValue::from_str("local Iroh endpoint id is unavailable"))?;
1760 if local_endpoint_id.as_str() <= remote_endpoint_id.as_str()
1761 || !matches!(
1762 self.inner.iroh_path_kind(&remote_endpoint_id).await,
1763 crate::client::IrohPathKind::Relay | crate::client::IrohPathKind::Unknown
1764 )
1765 {
1766 return Ok(false);
1767 }
1768 let endpoint_id = remote_endpoint_id
1769 .parse::<iroh::EndpointId>()
1770 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1771 self.inner
1772 .get_connection(endpoint_id)
1773 .await
1774 .ok_or_else(|| JsValue::from_str("WebRTC carrier base is unavailable"))?;
1775 let generation = self
1776 .inner
1777 .current_wasm_peer_data_generation(&connection_id, None)
1778 .await
1779 .ok_or_else(|| JsValue::from_str("WebRTC carrier generation is unavailable"))?;
1780 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
1781 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
1782 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
1783 transport_stable_id: generation.transport_stable_id,
1784 transport_generation: generation.transport_generation,
1785 route_generation: generation.route_generation,
1786 },
1787 1,
1788 )
1789 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1790 let kind = crate::client::IrohPathKind::IrohWebRtc;
1791 if !self
1792 .inner
1793 .reserve_wasm_carrier_upgrade(&connection_id, kind, &bootstrap.upgrade_id)
1794 .await
1795 {
1796 return Ok(false);
1797 }
1798 let fallback_allowed = self.inner.wasm_webrtc_external_fallback_allowed().await
1799 && remote_supports_external;
1800 let previous = self.wasm_webrtc_carrier_attempts.borrow_mut().insert(
1801 connection_id.clone(),
1802 WasmWebRtcCarrierAttempt {
1803 connection_id: connection_id.clone(),
1804 remote_endpoint_id: endpoint_id,
1805 bootstrap: bootstrap.clone(),
1806 generation,
1807 role: "initiator",
1808 offer_started: false,
1809 remote_ready: false,
1810 completion_started: false,
1811 retry_count,
1812 external_fallback_allowed: fallback_allowed,
1813 inbound_authorization_expires_at_ms: None,
1814 },
1815 );
1816 if let Some(previous) = previous {
1817 self.wasm_webrtc_carrier_sessions
1818 .borrow_mut()
1819 .remove(&previous.bootstrap.upgrade_id);
1820 }
1821 emit_wasm_carrier_action(
1822 &self.wasm_carrier_action_handler,
1823 serde_json::json!({
1824 "type": "prepare-webrtc",
1825 "connectionId": connection_id,
1826 "remoteEndpointId": remote_endpoint_id,
1827 "role": "initiator",
1828 "upgradeId": bootstrap.upgrade_id,
1829 "carrierSessionId": bootstrap.carrier_session_id,
1830 "transportGeneration": generation.transport_generation.saturating_add(1),
1831 }),
1832 );
1833 self.schedule_wasm_webrtc_carrier_watchdog(bootstrap);
1834 Ok(true)
1835 }
1836
1837 #[cfg(feature = "iroh-transport-webrtc")]
1840 #[wasm_bindgen(js_name = __irohWebRtcCarrierPrepared)]
1841 pub async fn iroh_webrtc_carrier_prepared(
1842 &self,
1843 connection_id: String,
1844 upgrade_id: String,
1845 ) -> Result<(), JsValue> {
1846 let attempt = self
1847 .wasm_webrtc_carrier_attempts
1848 .borrow()
1849 .get(&connection_id)
1850 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
1851 .cloned()
1852 .ok_or_else(|| JsValue::from_str("WebRTC carrier attempt is stale"))?;
1853 let kind = crate::client::IrohPathKind::IrohWebRtc;
1854 if !self
1855 .inner
1856 .wasm_carrier_upgrade_is_current(&connection_id, kind, &upgrade_id)
1857 .await
1858 {
1859 return Err(JsValue::from_str("WebRTC carrier attempt was retired"));
1860 }
1861 if attempt.role == "initiator" {
1862 emit_wasm_carrier_action(
1863 &self.wasm_carrier_action_handler,
1864 serde_json::json!({
1865 "type": "send-control",
1866 "connectionId": connection_id,
1867 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1868 "envelope": attempt.bootstrap,
1869 }),
1870 );
1871 } else {
1872 emit_wasm_carrier_action(
1877 &self.wasm_carrier_action_handler,
1878 serde_json::json!({
1879 "type": "send-control",
1880 "connectionId": attempt.connection_id,
1881 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1882 "envelope": {
1883 "type": "#pluto-signal",
1884 "content": {
1885 "transport": "iroh-webrtc",
1886 "type": "renegotiate",
1887 "negotiationId": upgrade_id,
1888 }
1889 },
1890 }),
1891 );
1892 }
1893 Ok(())
1894 }
1895
1896 #[cfg(feature = "iroh-transport-webrtc")]
1897 #[wasm_bindgen(js_name = __irohWebRtcCarrierFailed)]
1898 pub async fn iroh_webrtc_carrier_failed(
1899 &self,
1900 connection_id: String,
1901 upgrade_id: String,
1902 failure_code: String,
1903 ) {
1904 let attempt = self
1905 .wasm_webrtc_carrier_attempts
1906 .borrow()
1907 .get(&connection_id)
1908 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
1909 .cloned();
1910 if let Some(attempt) = attempt {
1911 let failure_code = match failure_code.as_str() {
1912 "data-channel-failed" => "data-channel-failed",
1913 "ice-failed" => "ice-failed",
1914 "signaling-failed" => "signaling-failed",
1915 "carrier-backpressure" => "carrier-backpressure",
1916 "carrier-base-generation-stale" => "carrier-base-generation-stale",
1917 _ => "browser-adapter-failed",
1918 };
1919 let should_retry = attempt.role == "initiator"
1920 && attempt.retry_count == 0
1921 && matches!(
1922 failure_code,
1923 "data-channel-failed"
1924 | "ice-failed"
1925 | "signaling-failed"
1926 | "carrier-base-generation-stale"
1927 );
1928 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
1929 let remote_supports_external = self
1930 .wasm_webrtc_remote_external
1931 .borrow()
1932 .get(&connection_id)
1933 .copied()
1934 .unwrap_or(false);
1935 self.fail_wasm_webrtc_carrier_attempt(attempt, failure_code, true)
1936 .await;
1937 if should_retry {
1938 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
1943 let _ = self
1944 .begin_iroh_webrtc_carrier_attempt(
1945 connection_id,
1946 remote_endpoint_id,
1947 remote_supports_external,
1948 1,
1949 )
1950 .await;
1951 }
1952 }
1953 }
1954
1955 #[cfg(feature = "iroh-transport-webrtc")]
1956 #[wasm_bindgen(js_name = __retireIrohWebRtcCarrier)]
1957 pub async fn retire_iroh_webrtc_carrier(
1958 &self,
1959 connection_id: String,
1960 terminal_reason: Option<String>,
1961 ) {
1962 let attempt = self
1963 .wasm_webrtc_carrier_attempts
1964 .borrow_mut()
1965 .remove(&connection_id);
1966 if let Some(attempt) = attempt {
1967 self.inner
1968 .retire_wasm_carrier_upgrade(
1969 &connection_id,
1970 crate::client::IrohPathKind::IrohWebRtc,
1971 &attempt.bootstrap.upgrade_id,
1972 )
1973 .await;
1974 let carrier = self
1975 .wasm_webrtc_carrier_sessions
1976 .borrow_mut()
1977 .remove(&attempt.bootstrap.upgrade_id);
1978 if let (Some(reason), Some(session)) =
1979 (terminal_reason.as_deref(), carrier.as_ref())
1980 {
1981 let _ = session.send_terminal(reason).await;
1982 }
1983 emit_wasm_carrier_action(
1984 &self.wasm_carrier_action_handler,
1985 serde_json::json!({
1986 "type": "retire-webrtc",
1987 "connectionId": connection_id,
1988 "upgradeId": attempt.bootstrap.upgrade_id,
1989 "failureCode": "logical-connection-retired",
1990 }),
1991 );
1992 }
1993 self.wasm_webrtc_remote_external
1994 .borrow_mut()
1995 .remove(&connection_id);
1996 #[cfg(feature = "iroh-transport-moq")]
1997 self.wasm_remote_carrier_capabilities
1998 .borrow_mut()
1999 .remove(&connection_id);
2000 }
2001
2002 #[cfg(feature = "iroh-transport-webrtc")]
2005 #[wasm_bindgen(js_name = __handleIrohCarrierControl)]
2006 pub async fn handle_iroh_carrier_control(
2007 &self,
2008 connection_id: String,
2009 remote_endpoint_id: String,
2010 frame: JsValue,
2011 ) -> Result<bool, JsValue> {
2012 let frame: serde_json::Value = serde_wasm_bindgen::from_value(frame)
2013 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2014 if frame.get("type").and_then(serde_json::Value::as_str) == Some("#pluto-signal")
2015 && frame
2016 .get("content")
2017 .and_then(|content| content.get("transport"))
2018 .and_then(serde_json::Value::as_str)
2019 == Some("iroh-webrtc")
2020 {
2021 let negotiation_id = frame
2022 .get("content")
2023 .and_then(|content| content.get("negotiationId"))
2024 .and_then(serde_json::Value::as_str)
2025 .unwrap_or_default();
2026 let current = self
2027 .wasm_webrtc_carrier_attempts
2028 .borrow()
2029 .get(&connection_id)
2030 .is_some_and(|attempt| {
2031 attempt.bootstrap.upgrade_id == negotiation_id
2032 && attempt.remote_endpoint_id.to_string() == remote_endpoint_id
2033 });
2034 if current {
2035 let signal_type = frame
2036 .get("content")
2037 .and_then(|content| content.get("type"))
2038 .and_then(serde_json::Value::as_str);
2039 let start_offer = if signal_type == Some("renegotiate") {
2040 self.wasm_webrtc_carrier_attempts
2041 .borrow_mut()
2042 .get_mut(&connection_id)
2043 .filter(|attempt| {
2044 attempt.bootstrap.upgrade_id == negotiation_id
2045 && attempt.role == "initiator"
2046 && !attempt.offer_started
2047 })
2048 .map(|attempt| {
2049 attempt.offer_started = true;
2050 })
2051 .is_some()
2052 } else {
2053 false
2054 };
2055 if start_offer {
2056 emit_wasm_carrier_action(
2057 &self.wasm_carrier_action_handler,
2058 serde_json::json!({
2059 "type": "start-webrtc",
2060 "connectionId": connection_id,
2061 "upgradeId": negotiation_id,
2062 }),
2063 );
2064 return Ok(true);
2065 }
2066 emit_wasm_carrier_action(
2067 &self.wasm_carrier_action_handler,
2068 serde_json::json!({
2069 "type": "apply-webrtc-signal",
2070 "connectionId": connection_id,
2071 "upgradeId": negotiation_id,
2072 "signal": frame,
2073 }),
2074 );
2075 }
2076 return Ok(true);
2077 }
2078
2079 let Some(bootstrap) =
2080 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(&frame)
2081 else {
2082 return Ok(false);
2083 };
2084 #[cfg(feature = "iroh-transport-moq")]
2085 if bootstrap.carrier == crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14
2086 {
2087 return self
2088 .handle_wasm_moq_bootstrap(connection_id, remote_endpoint_id, bootstrap)
2089 .await;
2090 }
2091 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc {
2092 return Ok(false);
2093 }
2094 let endpoint_id = remote_endpoint_id
2095 .parse::<iroh::EndpointId>()
2096 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2097 let kind = crate::client::IrohPathKind::IrohWebRtc;
2098 match bootstrap.action {
2099 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
2100 let local_endpoint_id = self
2101 .inner
2102 .current_node_id()
2103 .await
2104 .ok_or_else(|| JsValue::from_str("local endpoint id is unavailable"))?;
2105 if local_endpoint_id.as_str() >= remote_endpoint_id.as_str()
2106 || !self.inner.is_iroh_webrtc_carrier_enabled().await
2107 {
2108 return Ok(true);
2109 }
2110 if !matches!(
2111 self.inner.iroh_path_kind(&remote_endpoint_id).await,
2112 crate::client::IrohPathKind::Relay | crate::client::IrohPathKind::Unknown
2113 ) {
2114 return Ok(true);
2115 }
2116 let base_connection = self
2117 .inner
2118 .get_connection(endpoint_id)
2119 .await
2120 .ok_or_else(|| JsValue::from_str("WebRTC carrier base is unavailable"))?;
2121 let generation = self
2122 .inner
2123 .current_wasm_peer_data_generation(&connection_id, None)
2124 .await
2125 .ok_or_else(|| {
2126 JsValue::from_str("WebRTC carrier generation is unavailable")
2127 })?;
2128 if crate::transport_generation::for_connection(&base_connection)
2129 != generation.transport_stable_id
2130 {
2131 let failed =
2136 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
2137 &bootstrap,
2138 "carrier-base-generation-stale",
2139 )
2140 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2141 emit_wasm_carrier_action(
2142 &self.wasm_carrier_action_handler,
2143 serde_json::json!({
2144 "type": "send-control",
2145 "connectionId": connection_id,
2146 "remoteEndpointId": remote_endpoint_id,
2147 "envelope": failed,
2148 }),
2149 );
2150 return Ok(true);
2151 }
2152 if !self
2153 .inner
2154 .reserve_wasm_carrier_upgrade(&connection_id, kind, &bootstrap.upgrade_id)
2155 .await
2156 {
2157 return Ok(true);
2158 }
2159 let node = self
2160 .inner
2161 .iroh_node
2162 .read()
2163 .await
2164 .as_ref()
2165 .cloned()
2166 .ok_or_else(|| JsValue::from_str("Iroh node is unavailable"))?;
2167 let authorization_expiry = node
2168 .authorize_pending_inbound_replacement(
2169 endpoint_id,
2170 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
2171 std::time::Duration::from_secs(45),
2172 )
2173 .await;
2174 let fallback_allowed = self.inner.wasm_webrtc_external_fallback_allowed().await
2175 && self
2176 .wasm_webrtc_remote_external
2177 .borrow()
2178 .get(&connection_id)
2179 .copied()
2180 .unwrap_or(false);
2181 let previous = self.wasm_webrtc_carrier_attempts.borrow_mut().insert(
2182 connection_id.clone(),
2183 WasmWebRtcCarrierAttempt {
2184 connection_id: connection_id.clone(),
2185 remote_endpoint_id: endpoint_id,
2186 bootstrap: bootstrap.clone(),
2187 generation,
2188 role: "responder",
2189 offer_started: false,
2190 remote_ready: false,
2191 completion_started: false,
2192 retry_count: bootstrap.attempt.saturating_sub(1),
2193 external_fallback_allowed: fallback_allowed,
2194 inbound_authorization_expires_at_ms: Some(authorization_expiry),
2195 },
2196 );
2197 if let Some(previous) = previous {
2198 self.wasm_webrtc_carrier_sessions
2199 .borrow_mut()
2200 .remove(&previous.bootstrap.upgrade_id);
2201 }
2202 emit_wasm_carrier_action(
2203 &self.wasm_carrier_action_handler,
2204 serde_json::json!({
2205 "type": "prepare-webrtc",
2206 "connectionId": connection_id,
2207 "remoteEndpointId": remote_endpoint_id,
2208 "role": "responder",
2209 "upgradeId": bootstrap.upgrade_id,
2210 "carrierSessionId": bootstrap.carrier_session_id,
2211 "transportGeneration": generation.transport_generation.saturating_add(1),
2212 }),
2213 );
2214 self.schedule_wasm_webrtc_carrier_watchdog(bootstrap);
2215 }
2216 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
2217 let upgrade_id = {
2218 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
2219 attempts
2220 .get_mut(&connection_id)
2221 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
2222 .map(|attempt| {
2223 attempt.remote_ready = true;
2224 attempt.bootstrap.upgrade_id.clone()
2225 })
2226 };
2227 let attempt = upgrade_id.as_deref().and_then(|upgrade_id| {
2228 self.take_ready_outbound_wasm_webrtc_carrier_attempt(
2229 &connection_id,
2230 upgrade_id,
2231 )
2232 });
2233 if let Some(attempt) = attempt {
2234 self.spawn_outbound_wasm_webrtc_carrier_completion(attempt);
2235 }
2236 }
2237 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
2238 let attempt = self
2239 .wasm_webrtc_carrier_attempts
2240 .borrow()
2241 .get(&connection_id)
2242 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
2243 .cloned();
2244 if let Some(attempt) = attempt {
2245 let failure_code = match bootstrap.failure_code.as_deref() {
2246 Some("carrier-base-generation-stale") => {
2247 "carrier-base-generation-stale"
2248 }
2249 _ => "peer-rejected-carrier",
2250 };
2251 let should_retry = attempt.role == "initiator"
2252 && attempt.retry_count == 0
2253 && failure_code == "carrier-base-generation-stale";
2254 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
2255 let remote_supports_external = self
2256 .wasm_webrtc_remote_external
2257 .borrow()
2258 .get(&connection_id)
2259 .copied()
2260 .unwrap_or(false);
2261 self.fail_wasm_webrtc_carrier_attempt(attempt, failure_code, false)
2262 .await;
2263 if should_retry {
2264 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
2265 let _ = self
2266 .begin_iroh_webrtc_carrier_attempt(
2267 connection_id,
2268 remote_endpoint_id,
2269 remote_supports_external,
2270 1,
2271 )
2272 .await;
2273 }
2274 }
2275 }
2276 }
2277 Ok(true)
2278 }
2279
2280 #[cfg(all(feature = "iroh-transport-moq", not(feature = "iroh-transport-webrtc")))]
2281 #[wasm_bindgen(js_name = __handleIrohCarrierControl)]
2282 pub async fn handle_iroh_carrier_control_moq_only(
2283 &self,
2284 connection_id: String,
2285 remote_endpoint_id: String,
2286 frame: JsValue,
2287 ) -> Result<bool, JsValue> {
2288 let frame: serde_json::Value = serde_wasm_bindgen::from_value(frame)
2289 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2290 let Some(bootstrap) =
2291 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(&frame)
2292 else {
2293 return Ok(false);
2294 };
2295 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14
2296 {
2297 return Ok(false);
2298 }
2299 self.handle_wasm_moq_bootstrap(connection_id, remote_endpoint_id, bootstrap)
2300 .await
2301 }
2302
2303 #[cfg(feature = "iroh-transport-webrtc")]
2307 #[wasm_bindgen(js_name = __attachIrohWebRtcCarrier)]
2308 pub async fn attach_iroh_webrtc_carrier(
2309 &self,
2310 connection_id: String,
2311 remote_endpoint_id: String,
2312 channel: web_sys::RtcDataChannel,
2313 upgrade_id: String,
2314 ) -> Result<(), JsValue> {
2315 let endpoint_id = remote_endpoint_id
2316 .parse::<iroh::EndpointId>()
2317 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2318 let attempt = self
2319 .wasm_webrtc_carrier_attempts
2320 .borrow()
2321 .get(&connection_id)
2322 .filter(|attempt| {
2323 attempt.bootstrap.upgrade_id == upgrade_id
2324 && attempt.remote_endpoint_id == endpoint_id
2325 })
2326 .cloned()
2327 .ok_or_else(|| JsValue::from_str("WebRTC carrier attempt is stale"))?;
2328 if !self
2329 .inner
2330 .wasm_carrier_upgrade_is_current(
2331 &connection_id,
2332 crate::client::IrohPathKind::IrohWebRtc,
2333 &upgrade_id,
2334 )
2335 .await
2336 {
2337 return Err(JsValue::from_str("WebRTC carrier attempt was retired"));
2338 }
2339 let packet_session = self
2340 .inner
2341 .activate_iroh_packet_carrier(
2342 crate::iroh_carrier_kind::IrohCarrierKind::WebRtc,
2343 endpoint_id,
2344 )
2345 .await
2346 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2347 let expected = attempt
2348 .bootstrap
2349 .frame_expectation()
2350 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2351 let application_key = self
2352 .inner
2353 .application_crypto_key_for_connection(Some(&connection_id))
2354 .ok_or_else(|| {
2355 JsValue::from_str("WebRTC carrier requires the admitted application crypto key")
2356 })?;
2357 let terminal_client = self.inner.clone();
2358 let terminal_connection_id = connection_id.clone();
2359 let terminal_generation = attempt.generation.transport_generation.saturating_add(1);
2360 let on_terminal: Rc<dyn Fn(&'static str)> = Rc::new(move |reason| {
2361 let client = terminal_client.clone();
2362 let connection_id = terminal_connection_id.clone();
2363 spawn_local(async move {
2364 let _ = client
2365 .close_current_iroh_carrier_generation_with_reason(
2366 &connection_id,
2367 endpoint_id,
2368 crate::client::IrohPathKind::IrohWebRtc,
2369 terminal_generation,
2370 reason,
2371 )
2372 .await;
2373 });
2374 });
2375 let carrier = crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession::attach(
2376 channel,
2377 packet_session,
2378 expected,
2379 application_key,
2380 on_terminal,
2381 )?;
2382 self.wasm_webrtc_carrier_sessions
2383 .borrow_mut()
2384 .insert(upgrade_id.clone(), carrier);
2385 if attempt.role == "responder" {
2386 let ready = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
2387 &attempt.bootstrap,
2388 )
2389 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2390 emit_wasm_carrier_action(
2391 &self.wasm_carrier_action_handler,
2392 serde_json::json!({
2393 "type": "send-control",
2394 "connectionId": connection_id,
2395 "remoteEndpointId": remote_endpoint_id,
2396 "envelope": ready,
2397 }),
2398 );
2399 self.spawn_inbound_wasm_webrtc_carrier_completion(attempt);
2400 } else if let Some(attempt) =
2401 self.take_ready_outbound_wasm_webrtc_carrier_attempt(&connection_id, &upgrade_id)
2402 {
2403 self.spawn_outbound_wasm_webrtc_carrier_completion(attempt);
2404 }
2405 Ok(())
2406 }
2407
2408 #[cfg(feature = "iroh-transport-moq")]
2412 #[wasm_bindgen(js_name = __attachIrohMoqCarrier)]
2413 pub async fn attach_iroh_moq_carrier(
2414 &self,
2415 connection_id: String,
2416 remote_endpoint_id: String,
2417 datagrams: JsValue,
2418 upgrade_id: String,
2419 ) -> Result<(), JsValue> {
2420 let endpoint_id = remote_endpoint_id
2421 .parse::<iroh::EndpointId>()
2422 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2423 let attempt = self
2424 .wasm_moq_carrier_attempts
2425 .borrow()
2426 .get(&connection_id)
2427 .filter(|attempt| {
2428 attempt.bootstrap.upgrade_id == upgrade_id
2429 && attempt.remote_endpoint_id == endpoint_id
2430 })
2431 .cloned()
2432 .ok_or_else(|| JsValue::from_str("MoQ carrier attempt is stale"))?;
2433 if !self
2434 .inner
2435 .wasm_carrier_upgrade_is_current(
2436 &connection_id,
2437 crate::client::IrohPathKind::IrohMoq,
2438 &upgrade_id,
2439 )
2440 .await
2441 {
2442 return Err(JsValue::from_str("MoQ carrier attempt was retired"));
2443 }
2444 let packet_session = self
2445 .inner
2446 .activate_iroh_packet_carrier(
2447 crate::iroh_carrier_kind::IrohCarrierKind::Moq,
2448 endpoint_id,
2449 )
2450 .await
2451 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2452 let expected = attempt
2453 .bootstrap
2454 .frame_expectation()
2455 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2456 let application_key = self
2457 .inner
2458 .application_crypto_key_for_connection(Some(&connection_id))
2459 .ok_or_else(|| {
2460 JsValue::from_str("MoQ carrier requires an installed application key")
2461 })?;
2462 let terminal_client = self.inner.clone();
2463 let terminal_connection_id = connection_id.clone();
2464 let terminal_generation = attempt.generation.transport_generation.saturating_add(1);
2465 let on_terminal: Rc<dyn Fn(&'static str)> = Rc::new(move |reason| {
2466 let client = terminal_client.clone();
2467 let connection_id = terminal_connection_id.clone();
2468 spawn_local(async move {
2469 let _ = client
2470 .close_current_iroh_carrier_generation_with_reason(
2471 &connection_id,
2472 endpoint_id,
2473 crate::client::IrohPathKind::IrohMoq,
2474 terminal_generation,
2475 reason,
2476 )
2477 .await;
2478 });
2479 });
2480 let carrier = crate::wasm_moq_carrier::WasmMoqCarrierSession::attach(
2481 datagrams,
2482 packet_session,
2483 expected,
2484 application_key,
2485 on_terminal,
2486 )?;
2487 self.wasm_moq_carrier_sessions
2488 .borrow_mut()
2489 .insert(upgrade_id, carrier);
2490 if attempt.role == "initiator" {
2491 self.spawn_outbound_wasm_moq_carrier_completion(attempt);
2492 } else {
2493 self.spawn_inbound_wasm_moq_carrier_completion(attempt);
2494 }
2495 Ok(())
2496 }
2497
2498 #[wasm_bindgen(js_name = setIdentityCredential)]
2499 pub fn set_identity_credential(&self, credential: Option<String>) {
2500 let has_credential = credential
2501 .as_ref()
2502 .map(|value| !value.is_empty())
2503 .unwrap_or(false);
2504 let credential_len = credential.as_ref().map(|value| value.len()).unwrap_or(0);
2505
2506 if let Ok(mut guard) = self.identity_credential.lock() {
2507 *guard = credential.filter(|value| !value.is_empty());
2508 }
2509
2510 let should_log = if let Ok(mut guard) = self.last_auth_log.lock() {
2511 let next = (has_credential, credential_len);
2512 if guard.as_ref() == Some(&next) {
2513 false
2514 } else {
2515 *guard = Some(next);
2516 true
2517 }
2518 } else {
2519 true
2520 };
2521
2522 if should_log {
2523 web_sys::console::log_1(&JsValue::from_str(&format!(
2524 "[OPENRTC][WASM-IDENTITY] credential updated present={} len={}",
2525 has_credential, credential_len
2526 )));
2527 }
2528 }
2529
2530 #[wasm_bindgen(js_name = clearIdentityCredential)]
2536 pub fn clear_identity_credential(&self) {
2537 self.set_identity_credential(None);
2538 }
2539
2540 #[wasm_bindgen(js_name = rankRoutes)]
2543 pub fn rank_routes(
2544 &self,
2545 configured_priority: Vec<String>,
2546 candidates: Vec<String>,
2547 ) -> Vec<String> {
2548 crate::route_policy::rank_routes(&configured_priority, &candidates)
2549 }
2550
2551 pub async fn init_iroh(&self, secret_key: Option<Vec<u8>>) -> Result<String, JsValue> {
2552 let started_at = js_sys::Date::now();
2553 web_sys::console::log_1(&JsValue::from_str(&format!(
2554 "[OPENRTC][WASM-API] init_iroh called has_secret_key={} secret_key_len={}",
2555 secret_key.as_ref().is_some(),
2556 secret_key.as_ref().map(|k| k.len()).unwrap_or(0)
2557 )));
2558 match self.inner.init_iroh(secret_key, vec![]).await {
2559 Ok(node_id) => {
2560 self.inner.clone().start_wasm_accept_bridge();
2561 let elapsed = js_sys::Date::now() - started_at;
2562 web_sys::console::log_1(&JsValue::from_str(&format!(
2563 "[OPENRTC][WASM-API] init_iroh success elapsed_ms={:.0} node_id={}",
2564 elapsed, node_id
2565 )));
2566 Ok(node_id)
2567 }
2568 Err(err) => {
2569 let elapsed = js_sys::Date::now() - started_at;
2570 web_sys::console::error_1(&JsValue::from_str(&format!(
2571 "[OPENRTC][WASM-API] init_iroh failed elapsed_ms={:.0} error={}",
2572 elapsed, err
2573 )));
2574 Err(JsValue::from_str(&err.to_string()))
2575 }
2576 }
2577 }
2578
2579 #[wasm_bindgen(js_name = initIrohWithTestRelay)]
2582 pub async fn init_iroh_with_test_relay(
2583 &self,
2584 secret_key: Option<Vec<u8>>,
2585 test_relay_url: Option<String>,
2586 ) -> Result<String, JsValue> {
2587 let node_id = self
2588 .inner
2589 .init_iroh_with_test_relay(secret_key, vec![], test_relay_url.as_deref())
2590 .await
2591 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2592 self.inner.clone().start_wasm_accept_bridge();
2596 Ok(node_id)
2597 }
2598
2599 pub async fn iroh_secret_key(&self) -> Result<Vec<u8>, JsValue> {
2600 let node_guard = self.inner.iroh_node.read().await;
2601 if let Some(node) = node_guard.as_ref() {
2602 Ok(node.secret_key())
2603 } else {
2604 Err(JsValue::from_str("Iroh node not initialized"))
2605 }
2606 }
2607
2608 pub async fn node_addr(&self) -> Result<String, JsValue> {
2609 let node_guard = self.inner.iroh_node.read().await;
2610 if let Some(node) = node_guard.as_ref() {
2611 let addr = node
2612 .node_addr()
2613 .await
2614 .map_err(|e| JsValue::from_str(&e.to_string()))?;
2615 serde_json::to_string(&addr).map_err(|e| JsValue::from_str(&e.to_string()))
2616 } else {
2617 Err(JsValue::from_str("Iroh node not initialized"))
2618 }
2619 }
2620
2621 pub async fn endpoint_ticket(&self) -> Result<String, JsValue> {
2622 self.inner
2623 .endpoint_ticket()
2624 .await
2625 .map_err(|e| JsValue::from_str(&e.to_string()))
2626 }
2627
2628 pub async fn endpoint_ticket_with_token(
2632 &self,
2633 grant_scope: String,
2634 max_connections: u32,
2635 ) -> Result<String, JsValue> {
2636 self.inner
2637 .endpoint_ticket_with_token(&grant_scope, max_connections)
2638 .await
2639 .map_err(|e| JsValue::from_str(&e.to_string()))
2640 }
2641
2642 pub fn register_session_token(
2644 &self,
2645 token: String,
2646 grant_scope: String,
2647 max_connections: u32,
2648 ) {
2649 self.inner
2650 .register_session_token(token, grant_scope, max_connections);
2651 }
2652
2653 pub fn register_session_token_with_expiry_ms(
2655 &self,
2656 token: String,
2657 grant_scope: String,
2658 max_connections: u32,
2659 expires_at_ms: u64,
2660 ) {
2661 self.inner.register_session_token_with_expiry_ms(
2662 token,
2663 grant_scope,
2664 max_connections,
2665 expires_at_ms,
2666 );
2667 }
2668
2669 #[wasm_bindgen(js_name = setConnectionApplicationCryptoRequired)]
2671 pub fn set_connection_application_crypto_required(
2672 &self,
2673 connection_id: String,
2674 ) -> Result<(), JsValue> {
2675 self.inner
2676 .set_connection_application_crypto_required(&connection_id);
2677 Ok(())
2678 }
2679
2680 #[wasm_bindgen(js_name = setConnectionApplicationCryptoKey)]
2682 pub async fn set_connection_application_crypto_key(
2683 &self,
2684 connection_id: String,
2685 key: Vec<u8>,
2686 ) -> Result<(), JsValue> {
2687 if key.len() != crate::application_crypto::APPLICATION_KEY_BYTES {
2688 return Err(JsValue::from_str("application crypto key must be 32 bytes"));
2689 }
2690 let mut key_bytes = [0u8; crate::application_crypto::APPLICATION_KEY_BYTES];
2691 key_bytes.copy_from_slice(&key);
2692 self.inner
2693 .set_connection_application_crypto_key(&connection_id, key_bytes);
2694 self.inner
2695 .emit_current_wasm_connection_state(&connection_id)
2696 .await;
2697 Ok(())
2698 }
2699
2700 #[wasm_bindgen(js_name = clearConnectionApplicationCryptoKey)]
2705 pub fn clear_connection_application_crypto_key(&self, connection_id: String) {
2706 self.inner
2707 .clear_connection_application_crypto_key(&connection_id);
2708 }
2709
2710 pub fn validate_session_token(&self, token: String) -> Result<String, JsValue> {
2714 self.inner
2715 .validate_session_token(&token)
2716 .map_err(|e| JsValue::from_str(&e))
2717 }
2718
2719 pub async fn validate_session_token_for_connection(
2721 &self,
2722 token: String,
2723 connection_id: String,
2724 ) -> Result<String, JsValue> {
2725 self.inner
2726 .validate_session_token_for_connection(&token, &connection_id)
2727 .await
2728 .map_err(|e| JsValue::from_str(&e))
2729 }
2730
2731 pub async fn validate_session_token_for_connection_with_payload(
2732 &self,
2733 token: String,
2734 connection_id: String,
2735 token_payload: Option<String>,
2736 ) -> Result<String, JsValue> {
2737 self.inner
2738 .validate_session_token_for_connection_with_payload(
2739 &token,
2740 &connection_id,
2741 token_payload.as_deref(),
2742 )
2743 .await
2744 .map_err(|e| JsValue::from_str(&e))
2745 }
2746
2747 pub async fn present_session_token_to_host(
2754 &self,
2755 endpoint_id: String,
2756 token: String,
2757 ) -> Result<String, JsValue> {
2758 self.present_session_token_to_host_with_payload(endpoint_id, token, None)
2759 .await
2760 }
2761
2762 pub async fn present_session_token_to_host_with_payload(
2763 &self,
2764 endpoint_id: String,
2765 token: String,
2766 token_payload: Option<String>,
2767 ) -> Result<String, JsValue> {
2768 self.present_session_token_to_host_with_payload_and_device_id(
2769 endpoint_id,
2770 token,
2771 token_payload,
2772 None,
2773 )
2774 .await
2775 }
2776
2777 pub async fn present_session_token_to_host_with_payload_and_device_id(
2778 &self,
2779 endpoint_id: String,
2780 token: String,
2781 token_payload: Option<String>,
2782 device_id: Option<String>,
2783 ) -> Result<String, JsValue> {
2784 crate::console_log!(
2785 "[OpenRTC][session-admission][wasm-present] endpoint_id={} claimed_local_device_id={}",
2786 endpoint_id,
2787 device_id.as_deref().unwrap_or("<none>")
2788 );
2789 let endpoint_id_parsed: iroh::EndpointId = endpoint_id
2790 .parse()
2791 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
2792 let local_node_id = self.inner.current_node_id().await.ok_or_else(|| {
2793 JsValue::from_str("missing local node id for session-token presentation")
2794 })?;
2795 let connection_id =
2796 crate::client::Client::deterministic_connection_id(&local_node_id, &endpoint_id);
2797 let approval_scope = self
2798 .inner
2799 .present_and_accept_session_token_with_local_claim(
2800 endpoint_id_parsed,
2801 &connection_id,
2802 &token,
2803 token_payload.as_deref(),
2804 None,
2805 device_id,
2806 )
2807 .await
2808 .map_err(|e| JsValue::from_str(&e))?;
2809 self.inner
2810 .emit_current_wasm_connection_state(&connection_id)
2811 .await;
2812
2813 Ok(approval_scope)
2814 }
2815
2816 pub async fn remote_session_admission_ready_for_ticket(
2819 &self,
2820 endpoint_ticket: String,
2821 ) -> Result<bool, JsValue> {
2822 self.inner
2823 .remote_session_admission_ready_for_ticket(&endpoint_ticket)
2824 .await
2825 .map_err(|error| JsValue::from_str(&error.to_string()))
2826 }
2827
2828 #[allow(clippy::too_many_arguments)]
2831 pub async fn prepare_inline_reciprocal_session_admission(
2832 &self,
2833 endpoint_id: String,
2834 expected_transport_stable_id: u64,
2835 stream_instance_id: String,
2836 presentation_id: String,
2837 token: String,
2838 token_payload: String,
2839 device_id: String,
2840 stream_contract: String,
2841 ) -> Result<bool, JsValue> {
2842 let endpoint_id: iroh::EndpointId = endpoint_id
2843 .parse()
2844 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
2845 let stream_contract = match stream_contract.trim() {
2846 "one-shot-admission" => {
2847 crate::native_protocol::SessionTokenStreamContract::OneShotAdmission
2848 }
2849 "persistent-control" => {
2850 crate::native_protocol::SessionTokenStreamContract::PersistentControl
2851 }
2852 other => {
2853 return Err(JsValue::from_str(&format!(
2854 "unsupported reciprocal stream contract: {other}"
2855 )))
2856 }
2857 };
2858 self.inner
2859 .prepare_inline_reciprocal_session_admission(
2860 endpoint_id,
2861 expected_transport_stable_id,
2862 stream_instance_id.as_str(),
2863 presentation_id.as_str(),
2864 token.as_str(),
2865 token_payload.as_str(),
2866 device_id.as_str(),
2867 stream_contract,
2868 )
2869 .await
2870 .map_err(|error| JsValue::from_str(&error))?;
2871 Ok(true)
2872 }
2873
2874 pub async fn confirm_inline_reciprocal_session_admission(
2877 &self,
2878 endpoint_id: String,
2879 expected_transport_stable_id: u64,
2880 stream_instance_id: String,
2881 presentation_id: String,
2882 accepted: bool,
2883 approval_scope: Option<String>,
2884 ) -> Result<bool, JsValue> {
2885 let endpoint_id: iroh::EndpointId = endpoint_id
2886 .parse()
2887 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
2888 self.inner
2889 .confirm_inline_reciprocal_session_admission(
2890 endpoint_id,
2891 expected_transport_stable_id,
2892 stream_instance_id.as_str(),
2893 presentation_id.as_str(),
2894 accepted,
2895 approval_scope.as_deref(),
2896 )
2897 .await
2898 .map_err(|error| JsValue::from_str(&error))?;
2899 self.inner
2900 .emit_current_wasm_connection_state(
2901 &crate::client::Client::deterministic_connection_id(
2902 &self.inner.current_node_id().await.ok_or_else(|| {
2903 JsValue::from_str(
2904 "missing local node id after reciprocal admission ACK",
2905 )
2906 })?,
2907 &endpoint_id.to_string(),
2908 ),
2909 )
2910 .await;
2911 Ok(true)
2912 }
2913
2914 pub fn revoke_session_token(&self, token: String) -> Result<JsValue, JsValue> {
2916 serde_wasm_bindgen::to_value(&self.inner.revoke_session_token(&token))
2917 .map_err(|error| JsValue::from_str(&error.to_string()))
2918 }
2919
2920 pub async fn revoke_tokens_by_scope(
2922 &self,
2923 grant_scope: String,
2924 ) -> Result<JsValue, JsValue> {
2925 let affected = self.inner.begin_revoke_tokens_by_scope(&grant_scope);
2926 for connection_id in &affected {
2931 #[cfg(feature = "iroh-transport-webrtc")]
2932 self.retire_iroh_webrtc_carrier(
2933 connection_id.clone(),
2934 Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED.to_string()),
2935 )
2936 .await;
2937 #[cfg(feature = "iroh-transport-moq")]
2938 self.retire_iroh_moq_carrier_with_reason(
2939 connection_id.clone(),
2940 Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED),
2941 )
2942 .await;
2943 }
2944 self.inner
2945 .finish_revoke_tokens_by_scope(&grant_scope, &affected)
2946 .await;
2947 serde_wasm_bindgen::to_value(&affected).map_err(|e| JsValue::from_str(&e.to_string()))
2948 }
2949
2950 pub fn clear_session_tokens(&self) {
2952 self.inner.clear_session_tokens();
2953 }
2954
2955 pub fn endpoint_id_from_ticket(&self, ticket: String) -> Result<String, JsValue> {
2956 let (iroh_ticket, _token_suffix) = split_compound_ticket(ticket.trim());
2957 let parsed = EndpointTicket::from_str(iroh_ticket)
2958 .map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
2959 Ok(parsed.endpoint_addr().id.to_string())
2960 }
2961
2962 pub async fn connect(&self, ticket: String) -> Result<JsReadableStream, JsValue> {
2966 web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
2967 "[OPENRTC][WASM-API] WasmClient.connect() is deprecated; use connect_device() for managed product dials.",
2968 ));
2969 let (iroh_ticket, _token_suffix) = split_compound_ticket(ticket.trim());
2970 let parsed = EndpointTicket::from_str(iroh_ticket)
2971 .map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
2972 let endpoint_addr = parsed.endpoint_addr().clone();
2973 let endpoint_id = endpoint_addr.id;
2974 let stream = {
2975 let node_guard = self.inner.iroh_node.read().await;
2976 if let Some(node) = node_guard.as_ref() {
2977 node.connect_addr(endpoint_id, endpoint_addr)
2978 } else {
2979 return Err(JsValue::from_str("Iroh node not initialized"));
2980 }
2981 };
2982 Ok(into_js_readable_stream(stream))
2983 }
2984
2985 pub async fn disconnect(&self, endpoint_id: String) -> Result<(), JsValue> {
2986 let endpoint_id: iroh::EndpointId = endpoint_id
2987 .parse()
2988 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
2989 let node_guard = self.inner.iroh_node.read().await;
2990 if let Some(node) = node_guard.as_ref() {
2991 node.disconnect(endpoint_id)
2992 .await
2993 .map_err(|e| JsValue::from_str(&e.to_string()))
2994 } else {
2995 Err(JsValue::from_str("Iroh node not initialized"))
2996 }
2997 }
2998
2999 pub async fn disconnect_transient(&self, endpoint_id: String) -> Result<(), JsValue> {
3012 let endpoint_id: iroh::EndpointId = endpoint_id
3013 .parse()
3014 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
3015 self.inner
3016 .disconnect_with_reason(
3017 endpoint_id,
3018 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
3019 )
3020 .await
3021 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3022 self.inner.wake_browser_auto_connect();
3028 Ok(())
3029 }
3030
3031 pub async fn is_connected(&self, endpoint_id: String) -> Result<bool, JsValue> {
3032 let endpoint_id: iroh::EndpointId = endpoint_id
3033 .parse()
3034 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
3035 Ok(self.inner.is_connected(endpoint_id).await)
3036 }
3037
3038 pub fn runtime_policy(&self) -> Result<JsValue, JsValue> {
3039 serde_wasm_bindgen::to_value(&self.inner.runtime_policy_snapshot())
3040 .map_err(|e| JsValue::from_str(&e.to_string()))
3041 }
3042
3043 pub async fn add_peer_scope(&self, id: String, scope: String) -> Result<JsValue, JsValue> {
3044 let scopes = self.inner.add_peer_scope(&id, &scope).await;
3045 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
3046 }
3047
3048 pub async fn release_peer_scope(
3049 &self,
3050 id: String,
3051 scope: Option<String>,
3052 ) -> Result<JsValue, JsValue> {
3053 let scopes = self.inner.release_peer_scope(&id, scope.as_deref()).await;
3054 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
3055 }
3056
3057 pub async fn peer_scopes(&self, id: String) -> Result<JsValue, JsValue> {
3058 let scopes = self.inner.peer_scopes(&id).await;
3059 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
3060 }
3061
3062 pub async fn same_peer(&self, left: String, right: String) -> Result<bool, JsValue> {
3063 Ok(self.inner.same_peer(&left, &right).await)
3064 }
3065
3066 pub async fn peer_snapshot(&self, id: String) -> Result<JsValue, JsValue> {
3067 let snapshot = self.inner.peer_snapshot(&id).await;
3068 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3069 }
3070
3071 pub async fn peer_session(&self, id: String) -> Result<JsValue, JsValue> {
3075 let snapshot = self.inner.peer_session(&id).await;
3076 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3077 }
3078
3079 pub async fn peer_sessions(&self) -> Result<JsValue, JsValue> {
3080 let snapshots = self.inner.peer_sessions().await;
3081 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
3082 }
3083
3084 pub async fn connection_state(&self, connection_id: String) -> Result<JsValue, JsValue> {
3085 let snapshot = self.inner.connection_state(&connection_id).await;
3086 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3087 }
3088
3089 pub async fn connection_states(&self) -> Result<JsValue, JsValue> {
3090 let snapshots = self.inner.connection_states().await;
3091 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
3092 }
3093
3094 pub async fn wait_for_settled_peer(
3095 &self,
3096 id: String,
3097 timeout_ms: Option<u32>,
3098 ) -> Result<JsValue, JsValue> {
3099 let snapshot = self
3100 .inner
3101 .wait_for_settled_peer(&id, timeout_ms.map(|value| value as u64))
3102 .await;
3103 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3104 }
3105
3106 pub async fn resolve_peer_connection_records(
3107 &self,
3108 id: String,
3109 ) -> Result<JsValue, JsValue> {
3110 let records = self.inner.resolve_peer_connection_records(&id).await;
3111 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
3112 }
3113
3114 pub async fn list_managed_connections(&self) -> Result<JsValue, JsValue> {
3115 let records = self.inner.list_managed_connections().await;
3116 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
3117 }
3118
3119 pub async fn bind_connection_device_id(
3120 &self,
3121 connection_id: String,
3122 device_id: String,
3123 ) -> Result<JsValue, JsValue> {
3124 let snapshot = self
3125 .inner
3126 .bind_connection_device_id(&connection_id, &device_id)
3127 .await;
3128 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3129 }
3130
3131 pub async fn bind_node_device_id(
3132 &self,
3133 node_id: String,
3134 device_id: String,
3135 ) -> Result<(), JsValue> {
3136 self.inner.bind_node_device_id(&node_id, &device_id).await;
3137 Ok(())
3138 }
3139
3140 pub async fn reject_connection_admission(
3141 &self,
3142 connection_id: String,
3143 reason: String,
3144 ) -> Result<JsValue, JsValue> {
3145 self.inner
3146 .reject_session_connection(&connection_id, &reason);
3147 self.inner
3148 .emit_current_wasm_connection_state(&connection_id)
3149 .await;
3150 let snapshot = self.inner.connection_state(&connection_id).await;
3151 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3152 }
3153
3154 pub async fn report_managed_connection_settled(
3155 &self,
3156 connection_id: String,
3157 settled: bool,
3158 device_id: Option<String>,
3159 transport_stable_id: Option<u64>,
3160 transport_generation: Option<u64>,
3161 route_generation: Option<u64>,
3162 ) -> Result<JsValue, JsValue> {
3163 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
3164 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
3165 self.inner
3166 .report_managed_connection_settled_for_transport(
3167 &connection_id,
3168 settled,
3169 transport_stable_id,
3170 transport_generation,
3171 route_generation,
3172 )
3173 .await
3174 }
3175 _ => None,
3176 };
3177 let _ = device_id;
3178 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3179 }
3180
3181 pub async fn report_transport_status(
3182 &self,
3183 connection_id: String,
3184 active_transport: String,
3185 parallel_transport: Option<String>,
3186 transport_stable_id: Option<u64>,
3187 transport_generation: Option<u64>,
3188 route_generation: Option<u64>,
3189 ) -> Result<JsValue, JsValue> {
3190 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
3191 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
3192 self.inner
3193 .report_transport_status_for_generation(
3194 &connection_id,
3195 &active_transport,
3196 parallel_transport.as_deref(),
3197 transport_stable_id,
3198 transport_generation,
3199 route_generation,
3200 )
3201 .await
3202 }
3203 _ => None,
3204 };
3205 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
3206 }
3207
3208 pub async fn is_current_transport_stable_id(
3209 &self,
3210 endpoint_id: String,
3211 transport_stable_id: u64,
3212 ) -> Result<bool, JsValue> {
3213 let endpoint_id = endpoint_id
3214 .parse::<iroh::EndpointId>()
3215 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3216 Ok(self
3217 .inner
3218 .is_current_transport_stable_id(endpoint_id, transport_stable_id)
3219 .await)
3220 }
3221
3222 pub async fn open_bi(&self, endpoint_id: String) -> Result<BiStream, JsValue> {
3223 let endpoint_id: iroh::EndpointId = endpoint_id
3224 .parse()
3225 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
3226 self.inner
3227 .assert_raw_peer_stream_allowed(&endpoint_id)
3228 .await
3229 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3230 self.inner
3239 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
3240 .await
3241 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3242 let (send, recv) = self
3243 .inner
3244 .open_bi_internal(endpoint_id)
3245 .await
3246 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3247 Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
3248 }
3249
3250 pub async fn open_native_main_control_bi(
3279 &self,
3280 endpoint_id: String,
3281 ) -> Result<BiStream, JsValue> {
3282 let endpoint_id: iroh::EndpointId = endpoint_id
3283 .parse()
3284 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
3285 self.inner
3286 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
3287 .await
3288 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3289 let (send, recv) = self
3290 .inner
3291 .open_bi_internal(endpoint_id)
3292 .await
3293 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3294 Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
3295 }
3296
3297 pub async fn send_native_main_control_frame(
3302 &self,
3303 endpoint_id: String,
3304 frame: Vec<u8>,
3305 ) -> Result<(), JsValue> {
3306 let endpoint_id: iroh::EndpointId = endpoint_id
3307 .parse()
3308 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
3309 self.inner
3310 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
3311 .await
3312 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3313 let (mut send, _recv) = self
3314 .inner
3315 .open_bi_internal(endpoint_id)
3316 .await
3317 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3318 let label = b"signal";
3319 send.write_all(&[0x00])
3320 .await
3321 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3322 send.write_all(&(label.len() as u32).to_be_bytes())
3323 .await
3324 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3325 send.write_all(label)
3326 .await
3327 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3328 send.write_all(&frame)
3329 .await
3330 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3331 crate::application_crypto_streams::PeerSendStream::plain(send)
3332 .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
3333 .await
3334 .map_err(|error| JsValue::from_str(&error.to_string()))
3335 }
3336
3337 pub async fn open_peer_bi(
3338 &self,
3339 id: String,
3340 timeout_ms: Option<u32>,
3341 ) -> Result<BiStream, JsValue> {
3342 let (_connection_id, remote_node_id, send, recv) = self
3343 .inner
3344 .open_peer_bi(&id, timeout_ms.map(|value| value as u64))
3345 .await
3346 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3347 Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
3348 }
3349
3350 pub async fn send_peer_application_frame(
3354 &self,
3355 id: String,
3356 frame: Vec<u8>,
3357 timeout_ms: Option<u32>,
3358 ) -> Result<(), JsValue> {
3359 self.inner
3360 .send_peer_application_frame(&id, &frame, timeout_ms.map(|value| value as u64))
3361 .await
3362 .map_err(|error| JsValue::from_str(&error.to_string()))
3363 }
3364
3365 pub async fn open_peer_bi_explicit_file_sender(
3371 &self,
3372 id: String,
3373 timeout_ms: Option<u32>,
3374 ) -> Result<PeerUniStream, JsValue> {
3375 let (_connection_id, _remote_node_id, send) = self
3376 .inner
3377 .open_peer_bi_explicit_file_sender(&id, timeout_ms.map(|value| value as u64))
3378 .await
3379 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3380 Ok(peer_uni_stream_from_send(send))
3381 }
3382
3383 pub async fn open_peer_bi_transport_only(
3387 &self,
3388 id: String,
3389 timeout_ms: Option<u32>,
3390 ) -> Result<BiStream, JsValue> {
3391 let (_connection_id, remote_node_id, send, recv) = self
3392 .inner
3393 .open_peer_bi_transport_only(&id, timeout_ms.map(|value| value as u64))
3394 .await
3395 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3396 Ok(BiStream::from_parts(send, recv, remote_node_id))
3397 }
3398
3399 pub async fn open_peer_native_bi(
3409 &self,
3410 id: String,
3411 label: String,
3412 timeout_ms: Option<u32>,
3413 ) -> Result<BiStream, JsValue> {
3414 if label != "drive-view" {
3415 return Err(JsValue::from_str(
3416 "unsupported native peer stream label; only drive-view is allowed",
3417 ));
3418 }
3419
3420 let (_connection_id, remote_node_id, mut send, recv) = self
3421 .inner
3422 .open_peer_bi(&id, timeout_ms.map(|value| value as u64))
3423 .await
3424 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3425 let envelope = crate::stream_metadata::encode_channel_envelope(&label, None)
3426 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3427 send.write_all(&envelope)
3428 .await
3429 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3430
3431 Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
3432 }
3433
3434 pub async fn open_uni(&self, endpoint_id: String) -> Result<PeerUniStream, JsValue> {
3435 let endpoint_id: iroh::EndpointId = endpoint_id
3436 .parse()
3437 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
3438 self.inner
3439 .assert_raw_peer_stream_allowed(&endpoint_id)
3440 .await
3441 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3442 self.inner
3445 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
3446 .await
3447 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3448 let node_guard = self.inner.iroh_node.read().await;
3449 if let Some(node) = node_guard.as_ref() {
3450 let send = node
3451 .open_uni(endpoint_id.clone())
3452 .await
3453 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3454 Ok(peer_uni_stream_from_send(
3455 crate::application_crypto_streams::PeerSendStream::plain(send),
3456 ))
3457 } else {
3458 Err(JsValue::from_str("Iroh node not initialized"))
3459 }
3460 }
3461
3462 pub async fn open_peer_uni(
3463 &self,
3464 id: String,
3465 timeout_ms: Option<u32>,
3466 ) -> Result<PeerUniStream, JsValue> {
3467 let (_connection_id, _remote_node_id, send) = self
3468 .inner
3469 .open_peer_uni(&id, timeout_ms.map(|value| value as u64))
3470 .await
3471 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3472 Ok(peer_uni_stream_from_send(send))
3473 }
3474
3475 pub async fn send_peer(&self, id: String, data: Vec<u8>) -> Result<(), JsValue> {
3485 self.inner
3486 .send_peer(&id, &data)
3487 .await
3488 .map_err(|e| JsValue::from_str(&e.to_string()))
3489 }
3490
3491 pub async fn iroh_path_kind(&self, peer_id: String) -> String {
3495 match self.inner.iroh_path_kind(&peer_id).await {
3496 crate::client::IrohPathKind::DirectQuic => "direct-quic".to_string(),
3497 crate::client::IrohPathKind::DirectLan => "direct-lan".to_string(),
3498 crate::client::IrohPathKind::Relay => "relay".to_string(),
3499 crate::client::IrohPathKind::Ble => "ble".to_string(),
3500 crate::client::IrohPathKind::IrohWebRtc => "iroh-webrtc".to_string(),
3501 crate::client::IrohPathKind::IrohMoq => "iroh-moq".to_string(),
3502 crate::client::IrohPathKind::Unknown => "unknown".to_string(),
3503 }
3504 }
3505
3506 pub async fn iroh_transport_rtt_ms(&self, peer_id: String) -> Option<u32> {
3509 self.inner
3510 .iroh_transport_rtt_ms(&peer_id)
3511 .await
3512 .map(|value| value.min(u32::MAX as u64) as u32)
3513 }
3514
3515 pub async fn incoming_streams(&self) -> Result<JsReadableStream, JsValue> {
3516 let (node, stream) = {
3517 let node_guard = self.inner.iroh_node.read().await;
3518 if let Some(node) = node_guard.as_ref() {
3519 (node.clone(), node.incoming_streams_stream())
3520 } else {
3521 return Err(JsValue::from_str("Iroh node not initialized"));
3522 }
3523 };
3524
3525 use futures::StreamExt;
3526 let mapped_stream = stream.filter_map(move |incoming| {
3527 let node = node.clone();
3528 async move {
3529 node.incoming_stream_is_current(&incoming)
3530 .await
3531 .then(|| crate::wasm_node::BiStream::incoming_to_js_value(incoming))
3532 }
3533 });
3534
3535 Ok(wasm_streams::ReadableStream::from_stream(mapped_stream).into_raw())
3536 }
3537
3538 pub async fn update_presence(
3539 &self,
3540 user_id: String,
3541 device_name: String,
3542 ticket: String,
3543 metadata: Option<String>,
3544 ttl_ms: Option<u64>,
3545 ) -> Result<(), JsValue> {
3546 self.inner
3547 .update_presence_with_ttl(
3548 &user_id,
3549 &device_name,
3550 &ticket,
3551 ttl_ms.unwrap_or(300_000),
3552 metadata.as_deref(),
3553 )
3554 .await
3555 .map_err(|e| JsValue::from_str(&e.to_string()))
3556 }
3557
3558 pub async fn send_message(
3559 &self,
3560 target_id: String,
3561 payload: String,
3562 state: Option<String>,
3563 reply_payload: Option<String>,
3564 ) -> Result<String, JsValue> {
3565 self.inner
3566 .send_message(
3567 &target_id,
3568 &payload,
3569 state.as_deref(),
3570 reply_payload.as_deref(),
3571 )
3572 .await
3573 .map_err(|e| JsValue::from_str(&e.to_string()))
3574 }
3575
3576 pub async fn set_offline(&self, user_id: String) -> Result<(), JsValue> {
3577 self.inner
3578 .set_offline(&user_id)
3579 .await
3580 .map_err(|e| JsValue::from_str(&e.to_string()))
3581 }
3582
3583 pub async fn update_device(
3584 &self,
3585 user_id: String,
3586 device_id: String,
3587 device_name: Option<String>,
3588 capabilities: Option<JsValue>,
3589 metadata: Option<String>,
3590 ) -> Result<(), JsValue> {
3591 let parsed_capabilities = match capabilities {
3592 Some(value) if !value.is_null() && !value.is_undefined() => Some(
3593 serde_wasm_bindgen::from_value::<crate::signaling::DeviceCapabilities>(value)
3594 .map_err(|e| JsValue::from_str(&e.to_string()))?,
3595 ),
3596 _ => None,
3597 };
3598
3599 self.inner
3600 .update_device(
3601 &user_id,
3602 &device_id,
3603 device_name.as_deref(),
3604 parsed_capabilities,
3605 metadata.as_deref(),
3606 )
3607 .await
3608 .map_err(|e| JsValue::from_str(&e.to_string()))
3609 }
3610
3611 pub async fn delete_device(
3612 &self,
3613 user_id: String,
3614 device_id: String,
3615 ) -> Result<(), JsValue> {
3616 self.inner
3617 .delete_device(&user_id, &device_id)
3618 .await
3619 .map_err(|e| JsValue::from_str(&e.to_string()))
3620 }
3621
3622 pub fn force_reconnect_snapshot(&self) {
3623 self.inner.clone().force_reconnect_snapshot();
3624 }
3625
3626 pub fn stop_presence_loop(&self) {
3627 self.inner.stop_presence_loop();
3628 }
3629
3630 pub fn stop_auto_connect(&self) {
3631 self.inner.stop_auto_connect();
3632 self.inner.stop_browser_auto_connect();
3633 }
3634
3635 pub fn start_auto_connect(
3636 &self,
3637 user_id: String,
3638 local_device_id: String,
3639 ) -> Result<(), JsValue> {
3640 self.inner
3641 .start_browser_auto_connect(user_id, local_device_id)
3642 .map_err(|error| JsValue::from_str(&error.to_string()))
3643 }
3644
3645 pub fn submit_browser_desired_peers(
3646 &self,
3647 revision: u32,
3648 peers_json: String,
3649 ) -> Result<bool, JsValue> {
3650 self.inner
3651 .submit_browser_desired_peers(u64::from(revision), &peers_json)
3652 .map_err(|error| JsValue::from_str(&error.to_string()))
3653 }
3654
3655 pub fn wake_browser_auto_connect(&self) -> bool {
3656 self.inner.wake_browser_auto_connect()
3657 }
3658
3659 pub async fn set_auto_connect_excluded(&self, device_id: String, excluded: bool) {
3660 if excluded {
3661 self.inner.exclude_peer_and_publish(&device_id).await;
3662 } else {
3663 self.inner.unexclude_peer_and_publish(&device_id).await;
3664 }
3665 self.inner.wake_browser_auto_connect();
3666 }
3667
3668 pub fn is_auto_connect_excluded(&self, device_id: String) -> bool {
3669 self.inner.is_auto_connect_excluded(&device_id)
3670 }
3671
3672 pub async fn disconnect_device(
3673 &self,
3674 device_id: String,
3675 node_id_hint: Option<String>,
3676 ) -> Result<JsValue, JsValue> {
3677 let retired = self
3678 .inner
3679 .disconnect_device(&device_id, node_id_hint.as_deref())
3680 .await;
3681 serde_wasm_bindgen::to_value(&retired).map_err(|e| JsValue::from_str(&e.to_string()))
3682 }
3683
3684 pub fn stop_auth_scoped_activity(&self) {
3685 self.inner.stop_auth_scoped_activity();
3686 self.inner.stop_browser_auto_connect();
3687 }
3688
3689 pub fn start_presence_loop(
3690 &self,
3691 user_id: String,
3692 device_name: String,
3693 ticket: String,
3694 metadata: Option<String>,
3695 ) {
3696 self.inner
3697 .clone()
3698 .start_signaling_loop(user_id, device_name, ticket, metadata);
3699 }
3700
3701 pub async fn search_devices(&self, user_id: String) -> Result<JsValue, JsValue> {
3702 let devices = self
3703 .inner
3704 .search_devices(&user_id)
3705 .await
3706 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3707 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
3708 }
3709
3710 pub async fn devices_with_status(&self, user_id: String) -> Result<JsValue, JsValue> {
3711 let devices = self
3712 .inner
3713 .devices_with_status(&user_id)
3714 .await
3715 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3716 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
3717 }
3718
3719 pub async fn connect_device(
3720 &self,
3721 device_id: Option<String>,
3722 endpoint_ticket: String,
3723 ) -> Result<JsValue, JsValue> {
3724 let result = self
3725 .inner
3726 .connect_device(device_id.as_deref(), &endpoint_ticket)
3727 .await
3728 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3729 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
3730 }
3731
3732 pub async fn create_session(&self, session_json: String) -> Result<(), JsValue> {
3733 let session: crate::signaling::SignalingSession =
3734 serde_json::from_str(&session_json)
3735 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3736 self.inner
3737 .create_session(session)
3738 .await
3739 .map_err(|e| JsValue::from_str(&e.to_string()))
3740 }
3741
3742 pub async fn update_session(
3743 &self,
3744 session_id: String,
3745 update_json: String,
3746 ) -> Result<(), JsValue> {
3747 let update_data: serde_json::Value = serde_json::from_str(&update_json)
3748 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3749 self.inner
3750 .update_session(&session_id, update_data)
3751 .await
3752 .map_err(|e| JsValue::from_str(&e.to_string()))
3753 }
3754
3755 pub async fn create_room(
3758 &self,
3759 room_id: String,
3760 user_id: String,
3761 ticket_str: String,
3762 my_node_id: String,
3763 tag: String,
3764 max_members: Option<u32>,
3765 ) -> Result<bool, JsValue> {
3766 self.inner
3767 .room
3768 .create_room(
3769 &room_id,
3770 &user_id,
3771 &ticket_str,
3772 &my_node_id,
3773 &tag,
3774 max_members,
3775 )
3776 .await
3777 .map_err(|e| JsValue::from_str(&e.to_string()))
3778 }
3779
3780 pub async fn join_room(
3781 &self,
3782 room_id: String,
3783 user_id: String,
3784 ticket_str: String,
3785 my_node_id: String,
3786 tag: String,
3787 ) -> Result<(), JsValue> {
3788 self.inner
3789 .room
3790 .join_room(&room_id, &user_id, &ticket_str, &my_node_id, &tag)
3791 .await
3792 .map_err(|e| JsValue::from_str(&e.to_string()))
3793 }
3794
3795 pub async fn get_members(
3796 &self,
3797 room_id: String,
3798 my_node_id: String,
3799 tag: String,
3800 ) -> Result<String, JsValue> {
3801 let members = self
3802 .inner
3803 .room
3804 .get_members(&room_id, &my_node_id, &tag)
3805 .await
3806 .map_err(|e| JsValue::from_str(&e.to_string()))?;
3807 serde_json::to_string(&members).map_err(|e| JsValue::from_str(&e.to_string()))
3808 }
3809
3810 pub async fn leave_room(
3811 &self,
3812 room_id: String,
3813 my_node_id: String,
3814 tag: String,
3815 ) -> Result<(), JsValue> {
3816 self.inner
3817 .room
3818 .leave_room(&room_id, &my_node_id, &tag)
3819 .await
3820 .map_err(|e| JsValue::from_str(&e.to_string()))
3821 }
3822 }
3823
3824 #[cfg(feature = "iroh-protocols-wasm")]
3825 #[wasm_bindgen]
3826 impl WasmClient {
3827 #[wasm_bindgen(js_name = __initPersistentIrohProtocols)]
3831 pub async fn init_persistent_iroh_protocols(
3832 &self,
3833 replica_store: JsValue,
3834 ) -> Result<(), JsValue> {
3835 let mut guard = self.persistent_protocols.lock().await;
3836 if guard.is_some() {
3837 return Ok(());
3838 }
3839 let node = self
3840 .inner
3841 .iroh_node
3842 .read()
3843 .await
3844 .as_ref()
3845 .cloned()
3846 .ok_or_else(|| {
3847 JsValue::from_str("OpenRTC endpoint must be initialized before protocols")
3848 })?;
3849 let store = crate::wasm_docs_persistence::JsReplicaStore::new(replica_store)
3850 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3851 let actor = crate::wasm_docs_persistence::WasmPersistentDocsActor::hydrate(
3852 store,
3853 node.endpoint().clone(),
3854 )
3855 .await
3856 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
3857 node.install_standard_protocols(
3858 actor.docs_protocol(),
3859 actor.blobs_protocol(),
3860 actor.gossip_protocol(),
3861 )
3862 .await
3863 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
3864 *guard = Some(actor);
3865 Ok(())
3866 }
3867
3868 #[wasm_bindgen(js_name = __importPersistentIrohAuthor)]
3869 pub async fn import_persistent_iroh_author(
3870 &self,
3871 author_secret: Vec<u8>,
3872 ) -> Result<String, JsValue> {
3873 let guard = self.persistent_protocols.lock().await;
3874 let actor = persistent_protocols(&guard)?;
3875 actor
3876 .import_author(author_secret)
3877 .await
3878 .map_err(js_protocol_error)
3879 }
3880
3881 #[wasm_bindgen(js_name = __importPersistentIrohNamespace)]
3882 pub async fn import_persistent_iroh_namespace(
3883 &self,
3884 capability_kind: String,
3885 capability: Vec<u8>,
3886 generation: u64,
3887 share_revision: u64,
3888 ) -> Result<String, JsValue> {
3889 let mut guard = self.persistent_protocols.lock().await;
3890 let actor = persistent_protocols_mut(&mut guard)?;
3891 actor
3892 .import_namespace(
3893 capability_kind.as_str(),
3894 capability,
3895 generation,
3896 share_revision,
3897 )
3898 .await
3899 .map_err(js_protocol_error)
3900 }
3901
3902 #[wasm_bindgen(js_name = __createPersistentIrohNamespace)]
3903 pub async fn create_persistent_iroh_namespace(
3904 &self,
3905 generation: u64,
3906 share_revision: u64,
3907 ) -> Result<JsValue, JsValue> {
3908 let mut guard = self.persistent_protocols.lock().await;
3909 let descriptor = persistent_protocols_mut(&mut guard)?
3910 .create_namespace(generation, share_revision)
3911 .await
3912 .map_err(js_protocol_error)?;
3913 serde_wasm_bindgen::to_value(&descriptor)
3914 .map_err(|error| JsValue::from_str(&error.to_string()))
3915 }
3916
3917 #[wasm_bindgen(js_name = __importPersistentIrohTicket)]
3918 pub async fn import_persistent_iroh_ticket(
3919 &self,
3920 ticket: String,
3921 generation: u64,
3922 share_revision: u64,
3923 ) -> Result<String, JsValue> {
3924 let mut guard = self.persistent_protocols.lock().await;
3925 persistent_protocols_mut(&mut guard)?
3926 .import_ticket(&ticket, generation, share_revision)
3927 .await
3928 .map_err(js_protocol_error)
3929 }
3930
3931 #[wasm_bindgen(js_name = __sharePersistentIrohNamespace)]
3932 pub async fn share_persistent_iroh_namespace(
3933 &self,
3934 namespace_id: String,
3935 writable: bool,
3936 ) -> Result<String, JsValue> {
3937 let guard = self.persistent_protocols.lock().await;
3938 persistent_protocols(&guard)?
3939 .share(&namespace_id, writable)
3940 .await
3941 .map_err(js_protocol_error)
3942 }
3943
3944 #[wasm_bindgen(js_name = __removePersistentIrohNamespace)]
3945 pub async fn remove_persistent_iroh_namespace(
3946 &self,
3947 namespace_id: String,
3948 generation: u64,
3949 share_revision: u64,
3950 ) -> Result<(), JsValue> {
3951 let mut guard = self.persistent_protocols.lock().await;
3952 persistent_protocols_mut(&mut guard)?
3953 .remove_namespace(&namespace_id, generation, share_revision)
3954 .await
3955 .map_err(js_protocol_error)
3956 }
3957
3958 #[wasm_bindgen(js_name = __putPersistentIrohBytes)]
3959 pub async fn put_persistent_iroh_bytes(
3960 &self,
3961 namespace_id: String,
3962 key: Vec<u8>,
3963 value: Vec<u8>,
3964 ) -> Result<JsValue, JsValue> {
3965 let guard = self.persistent_protocols.lock().await;
3966 let receipt = persistent_protocols(&guard)?
3967 .set_bytes(&namespace_id, key, value)
3968 .await
3969 .map_err(js_protocol_error)?;
3970 serde_wasm_bindgen::to_value(&receipt)
3971 .map_err(|error| JsValue::from_str(&error.to_string()))
3972 }
3973
3974 #[wasm_bindgen(js_name = __setPersistentIrohHash)]
3975 pub async fn set_persistent_iroh_hash(
3976 &self,
3977 namespace_id: String,
3978 key: Vec<u8>,
3979 content_hash: String,
3980 content_length: u64,
3981 ) -> Result<String, JsValue> {
3982 let guard = self.persistent_protocols.lock().await;
3983 persistent_protocols(&guard)?
3984 .set_hash(&namespace_id, key, &content_hash, content_length)
3985 .await
3986 .map_err(js_protocol_error)
3987 }
3988
3989 #[wasm_bindgen(js_name = __deletePersistentIrohPrefix)]
3990 pub async fn delete_persistent_iroh_prefix(
3991 &self,
3992 namespace_id: String,
3993 prefix: Vec<u8>,
3994 ) -> Result<JsValue, JsValue> {
3995 let guard = self.persistent_protocols.lock().await;
3996 let receipt = persistent_protocols(&guard)?
3997 .delete_prefix(&namespace_id, prefix)
3998 .await
3999 .map_err(js_protocol_error)?;
4000 serde_wasm_bindgen::to_value(&receipt)
4001 .map_err(|error| JsValue::from_str(&error.to_string()))
4002 }
4003
4004 #[wasm_bindgen(js_name = __queryPersistentIrohNamespace)]
4005 pub async fn query_persistent_iroh_namespace(
4006 &self,
4007 namespace_id: String,
4008 key_prefix: Vec<u8>,
4009 ) -> Result<JsValue, JsValue> {
4010 let guard = self.persistent_protocols.lock().await;
4011 let entries = persistent_protocols(&guard)?
4012 .query(&namespace_id, key_prefix)
4013 .await
4014 .map_err(js_protocol_error)?;
4015 serde_wasm_bindgen::to_value(&entries)
4016 .map_err(|error| JsValue::from_str(&error.to_string()))
4017 }
4018
4019 #[wasm_bindgen(js_name = __hydratePersistentIrohBlob)]
4020 pub async fn hydrate_persistent_iroh_blob(
4021 &self,
4022 content_hash: String,
4023 ) -> Result<(), JsValue> {
4024 let guard = self.persistent_protocols.lock().await;
4025 persistent_protocols(&guard)?
4026 .hydrate_blob(&content_hash)
4027 .await
4028 .map_err(js_protocol_error)
4029 }
4030
4031 #[wasm_bindgen(js_name = __acknowledgePersistentIrohOutbox)]
4032 pub async fn acknowledge_persistent_iroh_outbox(
4033 &self,
4034 operation_id: String,
4035 ) -> Result<(), JsValue> {
4036 let guard = self.persistent_protocols.lock().await;
4037 persistent_protocols(&guard)?
4038 .acknowledge_outbox(&operation_id)
4039 .await
4040 .map_err(js_protocol_error)
4041 }
4042
4043 #[wasm_bindgen(js_name = __flushPersistentIrohProtocols)]
4044 pub async fn flush_persistent_iroh_protocols(&self) -> Result<(), JsValue> {
4045 let guard = self.persistent_protocols.lock().await;
4046 persistent_protocols(&guard)?
4047 .flush()
4048 .await
4049 .map_err(js_protocol_error)
4050 }
4051
4052 #[wasm_bindgen(js_name = __shutdownPersistentIrohProtocols)]
4053 pub async fn shutdown_persistent_iroh_protocols(&self) -> Result<(), JsValue> {
4054 if let Some(actor) = self.persistent_protocols.lock().await.take() {
4055 actor.shutdown().await.map_err(js_protocol_error)?;
4056 }
4057 Ok(())
4058 }
4059 }
4060
4061 #[cfg(feature = "iroh-protocols-wasm")]
4062 fn persistent_protocols(
4063 guard: &Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>,
4064 ) -> Result<&crate::wasm_docs_persistence::WasmPersistentDocsActor, JsValue> {
4065 guard
4066 .as_ref()
4067 .ok_or_else(|| JsValue::from_str("persistent Iroh protocols are not initialized"))
4068 }
4069
4070 #[cfg(feature = "iroh-protocols-wasm")]
4071 fn persistent_protocols_mut(
4072 guard: &mut Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>,
4073 ) -> Result<&mut crate::wasm_docs_persistence::WasmPersistentDocsActor, JsValue> {
4074 guard
4075 .as_mut()
4076 .ok_or_else(|| JsValue::from_str("persistent Iroh protocols are not initialized"))
4077 }
4078
4079 #[cfg(feature = "iroh-protocols-wasm")]
4080 fn js_protocol_error(error: impl std::fmt::Display) -> JsValue {
4081 JsValue::from_str(&format!("{error:#}"))
4082 }
4083
4084 #[wasm_bindgen(start)]
4085 pub fn start() {
4086 console_error_panic_hook::set_once();
4087 }
4088}