pub mod application_crypto;
pub mod application_crypto_streams;
pub mod client;
pub mod connection;
pub mod explicit_transfer_crypto;
pub mod firebase;
pub(crate) mod generated;
pub mod heartbeat;
pub(crate) mod iroh_connection_policy;
pub mod key_agreement;
pub mod lifecycle_reason;
pub(crate) mod native_moq_policy;
pub mod native_protocol;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod native_send_policy;
pub(crate) mod native_webrtc_policy;
pub mod presence;
pub mod route_policy;
pub mod runtime_policy;
pub mod session_token;
pub mod signaling;
pub mod stream_metadata;
pub(crate) mod transport_label;
#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
pub mod local_discovery;
#[cfg(all(target_arch = "wasm32", feature = "transport-webrtc"))]
compile_error!(
"feature `transport-webrtc` is native-only and must not be enabled for wasm32 targets"
);
#[cfg(all(target_arch = "wasm32", feature = "transport-moq"))]
compile_error!("feature `transport-moq` is native-only and must not be enabled for wasm32 targets");
#[cfg(test)]
pub mod test_constants {
pub const TEST_PROJECT_ID: &str = "test-project";
pub const TEST_API_KEY: &str = "pk_test_0000000000000000000000000000000000000000";
}
pub const LIVE_PROJECT_ID: &str = "pluto-rtc-prod";
pub fn app_tag_from_api_key(api_key: &str) -> String {
let trimmed = api_key.trim();
if trimmed.is_empty() {
return "app_anonymous".to_string();
}
let suffix_len = trimmed.len().min(16);
format!("app_{}", &trimmed[trimmed.len() - suffix_len..])
}
pub fn space_app_tag_from_keys(api_key: &str, space_key: &str) -> String {
let input = format!("{}:{}", api_key.trim(), space_key.trim());
let digest = <sha2::Sha256 as sha2::Digest>::digest(input.as_bytes());
format!("space::{}", hex::encode(digest))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn ensure_default_rustls_provider() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
}
#[cfg(not(target_arch = "wasm32"))]
pub mod adapters;
pub mod connection_manager;
pub mod logging;
#[cfg(not(target_arch = "wasm32"))]
pub mod protocol_registry;
#[cfg(not(target_arch = "wasm32"))]
pub mod runtime_manager;
#[cfg(not(target_arch = "wasm32"))]
pub mod transport;
#[cfg(not(target_arch = "wasm32"))]
pub use client::EndpointHandle;
#[cfg(all(
not(target_arch = "wasm32"),
not(any(target_os = "ios", target_os = "android"))
))]
pub mod sso;
#[cfg(not(target_arch = "wasm32"))]
pub mod native_node;
#[cfg(not(target_arch = "wasm32"))]
pub mod native_device;
#[cfg(not(target_arch = "wasm32"))]
pub mod native_auth;
#[cfg(target_arch = "wasm32")]
pub mod wasm_node;
#[cfg(target_arch = "wasm32")]
#[macro_export]
macro_rules! console_log {
($($t:tt)*) => (web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format_args!($($t)*).to_string())))
}
#[cfg(target_arch = "wasm32")]
pub mod wasm_api {
use crate::client::Client;
use crate::session_token::split_compound_ticket;
use crate::wasm_node::{
into_js_readable_stream, peer_uni_stream_from_send, BiStream, PeerUniStream,
};
use iroh_tickets::endpoint::EndpointTicket;
use std::str::FromStr;
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::*;
use wasm_streams::readable::sys::ReadableStream as JsReadableStream;
#[wasm_bindgen]
pub struct WasmClient {
inner: Arc<Client>,
auth_token: Arc<Mutex<Option<String>>>,
last_auth_log: Arc<Mutex<Option<(bool, usize)>>>,
}
#[wasm_bindgen]
impl WasmClient {
#[wasm_bindgen(constructor)]
pub fn new(project_id: String, tag: String) -> Result<WasmClient, JsValue> {
Self::new_with_app_tag(project_id, tag)
}
#[wasm_bindgen(js_name = newWithAppTag)]
pub fn new_with_app_tag(
project_id: String,
app_tag: String,
) -> Result<WasmClient, JsValue> {
let auth_token: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let token_state = auth_token.clone();
let token_provider = Box::new(move || -> Option<String> {
token_state.lock().ok().and_then(|guard| guard.clone())
});
Ok(Self {
inner: Arc::new(Client::new_with_app_tag(
project_id,
app_tag,
token_provider,
)),
auth_token,
last_auth_log: Arc::new(Mutex::new(None)),
})
}
pub fn set_auth_token(&self, token: Option<String>) {
let has_token = token.as_ref().map(|t| !t.is_empty()).unwrap_or(false);
let token_len = token.as_ref().map(|t| t.len()).unwrap_or(0);
if let Ok(mut guard) = self.auth_token.lock() {
*guard = token.filter(|t| !t.is_empty());
}
let should_log = if let Ok(mut guard) = self.last_auth_log.lock() {
let next = (has_token, token_len);
if guard.as_ref() == Some(&next) {
false
} else {
*guard = Some(next);
true
}
} else {
true
};
if should_log {
web_sys::console::log_1(&JsValue::from_str(&format!(
"[OPENRTC][WASM-AUTH] set_auth_token called has_token={} token_len={}",
has_token, token_len
)));
}
}
#[wasm_bindgen(js_name = rankRoutes)]
pub fn rank_routes(
&self,
configured_priority: Vec<String>,
candidates: Vec<String>,
) -> Vec<String> {
crate::route_policy::rank_routes(&configured_priority, &candidates)
}
pub async fn init_iroh(&self, secret_key: Option<Vec<u8>>) -> Result<String, JsValue> {
let started_at = js_sys::Date::now();
web_sys::console::log_1(&JsValue::from_str(&format!(
"[OPENRTC][WASM-API] init_iroh called has_secret_key={} secret_key_len={}",
secret_key.as_ref().is_some(),
secret_key.as_ref().map(|k| k.len()).unwrap_or(0)
)));
match self.inner.init_iroh(secret_key, vec![]).await {
Ok(node_id) => {
self.inner.clone().start_wasm_accept_bridge();
let elapsed = js_sys::Date::now() - started_at;
web_sys::console::log_1(&JsValue::from_str(&format!(
"[OPENRTC][WASM-API] init_iroh success elapsed_ms={:.0} node_id={}",
elapsed, node_id
)));
Ok(node_id)
}
Err(err) => {
let elapsed = js_sys::Date::now() - started_at;
web_sys::console::error_1(&JsValue::from_str(&format!(
"[OPENRTC][WASM-API] init_iroh failed elapsed_ms={:.0} error={}",
elapsed, err
)));
Err(JsValue::from_str(&err.to_string()))
}
}
}
pub async fn iroh_secret_key(&self) -> Result<Vec<u8>, JsValue> {
let node_guard = self.inner.iroh_node.read().await;
if let Some(node) = node_guard.as_ref() {
Ok(node.secret_key())
} else {
Err(JsValue::from_str("Iroh node not initialized"))
}
}
pub async fn node_addr(&self) -> Result<String, JsValue> {
let node_guard = self.inner.iroh_node.read().await;
if let Some(node) = node_guard.as_ref() {
let addr = node
.node_addr()
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&addr).map_err(|e| JsValue::from_str(&e.to_string()))
} else {
Err(JsValue::from_str("Iroh node not initialized"))
}
}
pub async fn endpoint_ticket(&self) -> Result<String, JsValue> {
self.inner
.endpoint_ticket()
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn endpoint_ticket_with_token(
&self,
grant_scope: String,
max_connections: u32,
) -> Result<String, JsValue> {
self.inner
.endpoint_ticket_with_token(&grant_scope, max_connections)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub fn register_session_token(
&self,
token: String,
grant_scope: String,
max_connections: u32,
) {
self.inner
.register_session_token(token, grant_scope, max_connections);
}
pub fn register_session_token_with_expiry_ms(
&self,
token: String,
grant_scope: String,
max_connections: u32,
expires_at_ms: u64,
) {
self.inner.register_session_token_with_expiry_ms(
token,
grant_scope,
max_connections,
expires_at_ms,
);
}
#[wasm_bindgen(js_name = setConnectionApplicationCryptoRequired)]
pub fn set_connection_application_crypto_required(
&self,
connection_id: String,
) -> Result<(), JsValue> {
self.inner
.set_connection_application_crypto_required(&connection_id);
Ok(())
}
#[wasm_bindgen(js_name = setConnectionApplicationCryptoKey)]
pub async fn set_connection_application_crypto_key(
&self,
connection_id: String,
key: Vec<u8>,
) -> Result<(), JsValue> {
if key.len() != crate::application_crypto::APPLICATION_KEY_BYTES {
return Err(JsValue::from_str("application crypto key must be 32 bytes"));
}
let mut key_bytes = [0u8; crate::application_crypto::APPLICATION_KEY_BYTES];
key_bytes.copy_from_slice(&key);
self.inner
.set_connection_application_crypto_key(&connection_id, key_bytes);
self.inner
.emit_current_wasm_connection_state(&connection_id)
.await;
Ok(())
}
#[wasm_bindgen(js_name = clearConnectionApplicationCryptoKey)]
pub fn clear_connection_application_crypto_key(&self, connection_id: String) {
self.inner
.clear_connection_application_crypto_key(&connection_id);
}
pub fn validate_session_token(&self, token: String) -> Result<String, JsValue> {
self.inner
.validate_session_token(&token)
.map_err(|e| JsValue::from_str(&e))
}
pub async fn validate_session_token_for_connection(
&self,
token: String,
connection_id: String,
) -> Result<String, JsValue> {
self.inner
.validate_session_token_for_connection(&token, &connection_id)
.await
.map_err(|e| JsValue::from_str(&e))
}
pub async fn validate_session_token_for_connection_with_payload(
&self,
token: String,
connection_id: String,
token_payload: Option<String>,
) -> Result<String, JsValue> {
self.inner
.validate_session_token_for_connection_with_payload(
&token,
&connection_id,
token_payload.as_deref(),
)
.await
.map_err(|e| JsValue::from_str(&e))
}
pub async fn present_session_token_to_host(
&self,
endpoint_id: String,
token: String,
) -> Result<String, JsValue> {
self.present_session_token_to_host_with_payload(endpoint_id, token, None)
.await
}
pub async fn present_session_token_to_host_with_payload(
&self,
endpoint_id: String,
token: String,
token_payload: Option<String>,
) -> Result<String, JsValue> {
self.present_session_token_to_host_with_payload_and_device_id(
endpoint_id,
token,
token_payload,
None,
)
.await
}
pub async fn present_session_token_to_host_with_payload_and_device_id(
&self,
endpoint_id: String,
token: String,
token_payload: Option<String>,
device_id: Option<String>,
) -> Result<String, JsValue> {
crate::console_log!(
"[OpenRTC][session-admission][wasm-present] endpoint_id={} claimed_local_device_id={}",
endpoint_id,
device_id.as_deref().unwrap_or("<none>")
);
let endpoint_id_parsed: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
let local_node_id = self.inner.current_node_id().await.ok_or_else(|| {
JsValue::from_str("missing local node id for session-token presentation")
})?;
let connection_id =
crate::client::Client::deterministic_connection_id(&local_node_id, &endpoint_id);
let approval_scope = self
.inner
.present_and_accept_session_token_with_local_claim(
endpoint_id_parsed,
&connection_id,
&token,
token_payload.as_deref(),
None,
device_id,
)
.await
.map_err(|e| JsValue::from_str(&e))?;
self.inner
.emit_current_wasm_connection_state(&connection_id)
.await;
Ok(approval_scope)
}
pub async fn remote_session_admission_ready_for_ticket(
&self,
endpoint_ticket: String,
) -> Result<bool, JsValue> {
self.inner
.remote_session_admission_ready_for_ticket(&endpoint_ticket)
.await
.map_err(|error| JsValue::from_str(&error.to_string()))
}
#[allow(clippy::too_many_arguments)]
pub async fn prepare_inline_reciprocal_session_admission(
&self,
endpoint_id: String,
expected_transport_stable_id: u64,
stream_instance_id: String,
presentation_id: String,
token: String,
token_payload: String,
device_id: String,
stream_contract: String,
) -> Result<bool, JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|error| JsValue::from_str(&format!("{error}")))?;
let stream_contract = match stream_contract.trim() {
"one-shot-admission" => {
crate::native_protocol::SessionTokenStreamContract::OneShotAdmission
}
"persistent-control" => {
crate::native_protocol::SessionTokenStreamContract::PersistentControl
}
other => {
return Err(JsValue::from_str(&format!(
"unsupported reciprocal stream contract: {other}"
)))
}
};
self.inner
.prepare_inline_reciprocal_session_admission(
endpoint_id,
expected_transport_stable_id,
stream_instance_id.as_str(),
presentation_id.as_str(),
token.as_str(),
token_payload.as_str(),
device_id.as_str(),
stream_contract,
)
.await
.map_err(|error| JsValue::from_str(&error))?;
Ok(true)
}
pub async fn confirm_inline_reciprocal_session_admission(
&self,
endpoint_id: String,
expected_transport_stable_id: u64,
stream_instance_id: String,
presentation_id: String,
accepted: bool,
approval_scope: Option<String>,
) -> Result<bool, JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|error| JsValue::from_str(&format!("{error}")))?;
self.inner
.confirm_inline_reciprocal_session_admission(
endpoint_id,
expected_transport_stable_id,
stream_instance_id.as_str(),
presentation_id.as_str(),
accepted,
approval_scope.as_deref(),
)
.await
.map_err(|error| JsValue::from_str(&error))?;
self.inner
.emit_current_wasm_connection_state(
&crate::client::Client::deterministic_connection_id(
&self.inner.current_node_id().await.ok_or_else(|| {
JsValue::from_str(
"missing local node id after reciprocal admission ACK",
)
})?,
&endpoint_id.to_string(),
),
)
.await;
Ok(true)
}
pub fn revoke_session_token(&self, token: String) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(&self.inner.revoke_session_token(&token))
.map_err(|error| JsValue::from_str(&error.to_string()))
}
pub async fn revoke_tokens_by_scope(
&self,
grant_scope: String,
) -> Result<JsValue, JsValue> {
let affected = self.inner.revoke_tokens_by_scope(&grant_scope).await;
serde_wasm_bindgen::to_value(&affected).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub fn clear_session_tokens(&self) {
self.inner.clear_session_tokens();
}
pub fn endpoint_id_from_ticket(&self, ticket: String) -> Result<String, JsValue> {
let (iroh_ticket, _token_suffix) = split_compound_ticket(ticket.trim());
let parsed = EndpointTicket::from_str(iroh_ticket)
.map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
Ok(parsed.endpoint_addr().id.to_string())
}
pub async fn connect(&self, ticket: String) -> Result<JsReadableStream, JsValue> {
web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
"[OPENRTC][WASM-API] WasmClient.connect() is deprecated; use connect_device() for managed product dials.",
));
let (iroh_ticket, _token_suffix) = split_compound_ticket(ticket.trim());
let parsed = EndpointTicket::from_str(iroh_ticket)
.map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
let endpoint_addr = parsed.endpoint_addr().clone();
let endpoint_id = endpoint_addr.id;
let stream = {
let node_guard = self.inner.iroh_node.read().await;
if let Some(node) = node_guard.as_ref() {
node.connect_addr(endpoint_id, endpoint_addr)
} else {
return Err(JsValue::from_str("Iroh node not initialized"));
}
};
Ok(into_js_readable_stream(stream))
}
pub async fn disconnect(&self, endpoint_id: String) -> Result<(), JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
let node_guard = self.inner.iroh_node.read().await;
if let Some(node) = node_guard.as_ref() {
node.disconnect(endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
} else {
Err(JsValue::from_str("Iroh node not initialized"))
}
}
pub async fn disconnect_transient(&self, endpoint_id: String) -> Result<(), JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
self.inner
.disconnect_with_reason(
endpoint_id,
crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn is_connected(&self, endpoint_id: String) -> Result<bool, JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
Ok(self.inner.is_connected(endpoint_id).await)
}
pub fn runtime_policy(&self) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(&self.inner.runtime_policy_snapshot())
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn add_peer_scope(&self, id: String, scope: String) -> Result<JsValue, JsValue> {
let scopes = self.inner.add_peer_scope(&id, &scope).await;
serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn release_peer_scope(
&self,
id: String,
scope: Option<String>,
) -> Result<JsValue, JsValue> {
let scopes = self.inner.release_peer_scope(&id, scope.as_deref()).await;
serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn peer_scopes(&self, id: String) -> Result<JsValue, JsValue> {
let scopes = self.inner.peer_scopes(&id).await;
serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn same_peer(&self, left: String, right: String) -> Result<bool, JsValue> {
Ok(self.inner.same_peer(&left, &right).await)
}
pub async fn peer_snapshot(&self, id: String) -> Result<JsValue, JsValue> {
let snapshot = self.inner.peer_snapshot(&id).await;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn peer_session(&self, id: String) -> Result<JsValue, JsValue> {
let snapshot = self.inner.peer_session(&id).await;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn peer_sessions(&self) -> Result<JsValue, JsValue> {
let snapshots = self.inner.peer_sessions().await;
serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn connection_state(&self, connection_id: String) -> Result<JsValue, JsValue> {
let snapshot = self.inner.connection_state(&connection_id).await;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn connection_states(&self) -> Result<JsValue, JsValue> {
let snapshots = self.inner.connection_states().await;
serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn wait_for_settled_peer(
&self,
id: String,
timeout_ms: Option<u32>,
) -> Result<JsValue, JsValue> {
let snapshot = self
.inner
.wait_for_settled_peer(&id, timeout_ms.map(|value| value as u64))
.await;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn resolve_peer_connection_records(
&self,
id: String,
) -> Result<JsValue, JsValue> {
let records = self.inner.resolve_peer_connection_records(&id).await;
serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn list_managed_connections(&self) -> Result<JsValue, JsValue> {
let records = self.inner.list_managed_connections().await;
serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn bind_connection_device_id(
&self,
connection_id: String,
device_id: String,
) -> Result<JsValue, JsValue> {
let snapshot = self
.inner
.bind_connection_device_id(&connection_id, &device_id)
.await;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn bind_node_device_id(
&self,
node_id: String,
device_id: String,
) -> Result<(), JsValue> {
self.inner.bind_node_device_id(&node_id, &device_id).await;
Ok(())
}
pub async fn reject_connection_admission(
&self,
connection_id: String,
reason: String,
) -> Result<JsValue, JsValue> {
self.inner
.reject_session_connection(&connection_id, &reason);
self.inner
.emit_current_wasm_connection_state(&connection_id)
.await;
let snapshot = self.inner.connection_state(&connection_id).await;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn report_managed_connection_settled(
&self,
connection_id: String,
settled: bool,
device_id: Option<String>,
transport_stable_id: Option<u64>,
transport_generation: Option<u64>,
route_generation: Option<u64>,
) -> Result<JsValue, JsValue> {
let snapshot = match (transport_stable_id, transport_generation, route_generation) {
(Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
self.inner
.report_managed_connection_settled_for_transport(
&connection_id,
settled,
transport_stable_id,
transport_generation,
route_generation,
)
.await
}
_ => None,
};
let _ = device_id;
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn report_transport_status(
&self,
connection_id: String,
active_transport: String,
parallel_transport: Option<String>,
transport_stable_id: Option<u64>,
transport_generation: Option<u64>,
route_generation: Option<u64>,
) -> Result<JsValue, JsValue> {
let snapshot = match (transport_stable_id, transport_generation, route_generation) {
(Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
self.inner
.report_transport_status_for_generation(
&connection_id,
&active_transport,
parallel_transport.as_deref(),
transport_stable_id,
transport_generation,
route_generation,
)
.await
}
_ => None,
};
serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn is_current_transport_stable_id(
&self,
endpoint_id: String,
transport_stable_id: u64,
) -> Result<bool, JsValue> {
let endpoint_id = endpoint_id
.parse::<iroh::EndpointId>()
.map_err(|error| JsValue::from_str(&error.to_string()))?;
Ok(self
.inner
.is_current_transport_stable_id(endpoint_id, transport_stable_id)
.await)
}
pub async fn open_bi(&self, endpoint_id: String) -> Result<BiStream, JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
self.inner
.assert_raw_peer_stream_allowed(&endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
self.inner
.ensure_connection_manager_record_before_peer_stream(&endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let (send, recv) = self
.inner
.open_bi_internal(endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
}
pub async fn open_native_main_control_bi(
&self,
endpoint_id: String,
) -> Result<BiStream, JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
self.inner
.ensure_connection_manager_record_before_peer_stream(&endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let (send, recv) = self
.inner
.open_bi_internal(endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
}
pub async fn open_peer_bi(
&self,
id: String,
timeout_ms: Option<u32>,
) -> Result<BiStream, JsValue> {
let (_connection_id, remote_node_id, send, recv) = self
.inner
.open_peer_bi(&id, timeout_ms.map(|value| value as u64))
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
}
pub async fn send_peer_application_frame(
&self,
id: String,
frame: Vec<u8>,
timeout_ms: Option<u32>,
) -> Result<(), JsValue> {
self.inner
.send_peer_application_frame(&id, &frame, timeout_ms.map(|value| value as u64))
.await
.map_err(|error| JsValue::from_str(&error.to_string()))
}
pub async fn open_peer_bi_explicit_file_sender(
&self,
id: String,
timeout_ms: Option<u32>,
) -> Result<PeerUniStream, JsValue> {
let (_connection_id, _remote_node_id, send) = self
.inner
.open_peer_bi_explicit_file_sender(&id, timeout_ms.map(|value| value as u64))
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(peer_uni_stream_from_send(send))
}
pub async fn open_peer_bi_transport_only(
&self,
id: String,
timeout_ms: Option<u32>,
) -> Result<BiStream, JsValue> {
let (_connection_id, remote_node_id, send, recv) = self
.inner
.open_peer_bi_transport_only(&id, timeout_ms.map(|value| value as u64))
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(BiStream::from_parts(send, recv, remote_node_id))
}
pub async fn open_peer_native_bi(
&self,
id: String,
label: String,
timeout_ms: Option<u32>,
) -> Result<BiStream, JsValue> {
if label != "drive-view" {
return Err(JsValue::from_str(
"unsupported native peer stream label; only drive-view is allowed",
));
}
let (_connection_id, remote_node_id, mut send, recv) = self
.inner
.open_peer_bi(&id, timeout_ms.map(|value| value as u64))
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let envelope = crate::stream_metadata::encode_channel_envelope(&label, None)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
send.write_all(&envelope)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
}
pub async fn open_uni(&self, endpoint_id: String) -> Result<PeerUniStream, JsValue> {
let endpoint_id: iroh::EndpointId = endpoint_id
.parse()
.map_err(|e| JsValue::from_str(&format!("{}", e)))?;
self.inner
.assert_raw_peer_stream_allowed(&endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
self.inner
.ensure_connection_manager_record_before_peer_stream(&endpoint_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let node_guard = self.inner.iroh_node.read().await;
if let Some(node) = node_guard.as_ref() {
let send = node
.open_uni(endpoint_id.clone())
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(peer_uni_stream_from_send(
crate::application_crypto_streams::PeerSendStream::plain(send),
))
} else {
Err(JsValue::from_str("Iroh node not initialized"))
}
}
pub async fn open_peer_uni(
&self,
id: String,
timeout_ms: Option<u32>,
) -> Result<PeerUniStream, JsValue> {
let (_connection_id, _remote_node_id, send) = self
.inner
.open_peer_uni(&id, timeout_ms.map(|value| value as u64))
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(peer_uni_stream_from_send(send))
}
pub async fn send_peer(&self, id: String, data: Vec<u8>) -> Result<(), JsValue> {
self.inner
.send_peer(&id, &data)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn iroh_path_kind(&self, peer_id: String) -> String {
match self.inner.iroh_path_kind(&peer_id).await {
crate::client::IrohPathKind::DirectQuic => "direct-quic".to_string(),
crate::client::IrohPathKind::DirectLan => "direct-lan".to_string(),
crate::client::IrohPathKind::Relay => "relay".to_string(),
crate::client::IrohPathKind::Ble => "ble".to_string(),
crate::client::IrohPathKind::Unknown => "unknown".to_string(),
}
}
pub async fn iroh_transport_rtt_ms(&self, peer_id: String) -> Option<u32> {
self.inner
.iroh_transport_rtt_ms(&peer_id)
.await
.map(|value| value.min(u32::MAX as u64) as u32)
}
pub async fn incoming_streams(&self) -> Result<JsReadableStream, JsValue> {
let (node, stream) = {
let node_guard = self.inner.iroh_node.read().await;
if let Some(node) = node_guard.as_ref() {
(node.clone(), node.incoming_streams_stream())
} else {
return Err(JsValue::from_str("Iroh node not initialized"));
}
};
use futures::StreamExt;
let mapped_stream = stream.filter_map(move |incoming| {
let node = node.clone();
async move {
node.incoming_stream_is_current(&incoming)
.await
.then(|| crate::wasm_node::BiStream::incoming_to_js_value(incoming))
}
});
Ok(wasm_streams::ReadableStream::from_stream(mapped_stream).into_raw())
}
pub async fn update_presence(
&self,
user_id: String,
device_name: String,
ticket: String,
metadata: Option<String>,
ttl_ms: Option<u64>,
) -> Result<(), JsValue> {
self.inner
.update_presence_with_ttl(
&user_id,
&device_name,
&ticket,
ttl_ms.unwrap_or(300_000),
metadata.as_deref(),
)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn send_message(
&self,
target_id: String,
payload: String,
state: Option<String>,
reply_payload: Option<String>,
) -> Result<String, JsValue> {
self.inner
.send_message(
&target_id,
&payload,
state.as_deref(),
reply_payload.as_deref(),
)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn set_offline(&self, user_id: String) -> Result<(), JsValue> {
self.inner
.set_offline(&user_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn update_device(
&self,
user_id: String,
device_id: String,
device_name: Option<String>,
capabilities: Option<JsValue>,
metadata: Option<String>,
) -> Result<(), JsValue> {
let parsed_capabilities = match capabilities {
Some(value) if !value.is_null() && !value.is_undefined() => Some(
serde_wasm_bindgen::from_value::<crate::signaling::DeviceCapabilities>(value)
.map_err(|e| JsValue::from_str(&e.to_string()))?,
),
_ => None,
};
self.inner
.update_device(
&user_id,
&device_id,
device_name.as_deref(),
parsed_capabilities,
metadata.as_deref(),
)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn delete_device(
&self,
user_id: String,
device_id: String,
) -> Result<(), JsValue> {
self.inner
.delete_device(&user_id, &device_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub fn force_reconnect_snapshot(&self) {
self.inner.clone().force_reconnect_snapshot();
}
pub fn stop_presence_loop(&self) {
self.inner.stop_presence_loop();
}
pub fn stop_auto_connect(&self) {
self.inner.stop_auto_connect();
self.inner.stop_browser_auto_connect();
}
pub fn start_auto_connect(
&self,
user_id: String,
local_device_id: String,
) -> Result<(), JsValue> {
self.inner
.start_browser_auto_connect(user_id, local_device_id)
.map_err(|error| JsValue::from_str(&error.to_string()))
}
pub fn submit_browser_desired_peers(
&self,
revision: u32,
peers_json: String,
) -> Result<bool, JsValue> {
self.inner
.submit_browser_desired_peers(u64::from(revision), &peers_json)
.map_err(|error| JsValue::from_str(&error.to_string()))
}
pub fn wake_browser_auto_connect(&self) -> bool {
self.inner.wake_browser_auto_connect()
}
pub async fn set_auto_connect_excluded(&self, device_id: String, excluded: bool) {
if excluded {
self.inner.exclude_peer_and_publish(&device_id).await;
} else {
self.inner.unexclude_peer_and_publish(&device_id).await;
}
self.inner.wake_browser_auto_connect();
}
pub fn is_auto_connect_excluded(&self, device_id: String) -> bool {
self.inner.is_auto_connect_excluded(&device_id)
}
pub async fn disconnect_device(
&self,
device_id: String,
node_id_hint: Option<String>,
) -> Result<JsValue, JsValue> {
let retired = self
.inner
.disconnect_device(&device_id, node_id_hint.as_deref())
.await;
serde_wasm_bindgen::to_value(&retired).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub fn stop_auth_scoped_activity(&self) {
self.inner.stop_auth_scoped_activity();
self.inner.stop_browser_auto_connect();
}
pub fn start_presence_loop(
&self,
user_id: String,
device_name: String,
ticket: String,
metadata: Option<String>,
) {
self.inner
.clone()
.start_signaling_loop(user_id, device_name, ticket, metadata);
}
pub async fn search_devices(&self, user_id: String) -> Result<JsValue, JsValue> {
let devices = self
.inner
.search_devices(&user_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn devices_with_status(&self, user_id: String) -> Result<JsValue, JsValue> {
let devices = self
.inner
.devices_with_status(&user_id)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn connect_device(
&self,
device_id: Option<String>,
endpoint_ticket: String,
) -> Result<JsValue, JsValue> {
let result = self
.inner
.connect_device(device_id.as_deref(), &endpoint_ticket)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn create_session(&self, session_json: String) -> Result<(), JsValue> {
let session: crate::signaling::SignalingSession =
serde_json::from_str(&session_json)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
self.inner
.create_session(session)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn update_session(
&self,
session_id: String,
update_json: String,
) -> Result<(), JsValue> {
let update_data: serde_json::Value = serde_json::from_str(&update_json)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
self.inner
.update_session(&session_id, update_data)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn create_room(
&self,
room_id: String,
user_id: String,
ticket_str: String,
my_node_id: String,
tag: String,
max_members: Option<u32>,
) -> Result<bool, JsValue> {
self.inner
.room
.create_room(
&room_id,
&user_id,
&ticket_str,
&my_node_id,
&tag,
max_members,
)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn join_room(
&self,
room_id: String,
user_id: String,
ticket_str: String,
my_node_id: String,
tag: String,
) -> Result<(), JsValue> {
self.inner
.room
.join_room(&room_id, &user_id, &ticket_str, &my_node_id, &tag)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn get_members(
&self,
room_id: String,
my_node_id: String,
tag: String,
) -> Result<String, JsValue> {
let members = self
.inner
.room
.get_members(&room_id, &my_node_id, &tag)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
serde_json::to_string(&members).map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn leave_room(
&self,
room_id: String,
my_node_id: String,
tag: String,
) -> Result<(), JsValue> {
self.inner
.room
.leave_room(&room_id, &my_node_id, &tag)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub async fn heartbeat_tick(
&self,
room_id: String,
member_id: String,
tag: String,
) -> Result<(), JsValue> {
self.inner
.room
.heartbeat_tick(&room_id, &member_id, &tag)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))
}
}
#[wasm_bindgen(start)]
pub fn start() {
console_error_panic_hook::set_once();
}
}