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