1use std::collections::{BTreeSet, HashMap};
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::sync::{Arc, RwLock};
5
6use base64::Engine as _;
7use ed25519_dalek::{Signature, VerifyingKey};
8use futures::StreamExt;
9use openrtc::application_crypto_streams::{PeerRecvStream, PeerSendStream};
10use serde::{Deserialize, Serialize};
11#[cfg(feature = "managed-group-encryption")]
12use sha2::{Digest, Sha256};
13use tauri::ipc::{Channel, InvokeBody, Request, Response};
14use tauri::{Emitter, Manager, Runtime, Webview};
15use tokio::sync::Mutex;
16
17mod native_assertion;
18mod native_transfer_peer_demand;
19#[cfg(feature = "native-broadcast-moq")]
20mod native_broadcast_moq;
21mod native_identity;
22pub use native_assertion::{NativeAssertionBridge, NativeAssertionRequest};
23
24const PLUGIN_NAME: &str = "openrtc-tauri-plugin";
25const PEER_BI_STREAM_ID_HEADER: &str = "x-openrtc-stream-id";
26const PEER_ID_HEADER: &str = "x-openrtc-peer-id";
27const FANOUT_CAPABILITY_HEADER: &str = "x-openrtc-fanout-capability";
28const FANOUT_SOURCE_PEER_HEADER: &str = "x-openrtc-fanout-source-peer";
29const FANOUT_APP_TAG_HEADER: &str = "x-openrtc-fanout-app-tag";
30const MEDIA_PUBLICATION_ID_HEADER: &str = "x-openrtc-publication-id";
31const BROADCAST_HANDLE_ID_HEADER: &str = "x-openrtc-broadcast-handle-id";
32const MEDIA_TIMESTAMP_US_HEADER: &str = "x-openrtc-timestamp-us";
33const MEDIA_DURATION_US_HEADER: &str = "x-openrtc-duration-us";
34const MEDIA_KEYFRAME_HEADER: &str = "x-openrtc-keyframe";
35const MEDIA_DISCARDABLE_HEADER: &str = "x-openrtc-discardable";
36
37fn request_body_bytes(request: &Request<'_>) -> Result<Vec<u8>, String> {
38 match request.body() {
39 InvokeBody::Raw(bytes) => Ok(bytes.clone()),
40 InvokeBody::Json(json) => serde_json::from_value::<Vec<u8>>(json.clone())
43 .map_err(|error| format!("invalid binary IPC payload: {error}")),
44 }
45}
46
47fn required_request_header(request: &Request<'_>, name: &str) -> Result<String, String> {
48 request
49 .headers()
50 .get(name)
51 .and_then(|value| value.to_str().ok())
52 .map(str::trim)
53 .filter(|value| !value.is_empty())
54 .map(ToOwned::to_owned)
55 .ok_or_else(|| format!("{name} header is required"))
56}
57
58fn parsed_request_header<T>(request: &Request<'_>, name: &str) -> Result<T, String>
59where
60 T: std::str::FromStr,
61 T::Err: std::fmt::Display,
62{
63 required_request_header(request, name)?
64 .parse::<T>()
65 .map_err(|error| format!("invalid {name} header: {error}"))
66}
67
68pub type InstallFuture = std::pin::Pin<
69 Box<
70 dyn std::future::Future<Output = Result<Box<dyn std::any::Any + Send + Sync>, String>>
71 + Send,
72 >,
73>;
74
75pub type BroadcastAdapterFuture<'a> = std::pin::Pin<
76 Box<
77 dyn std::future::Future<
78 Output = Result<Option<openrtc::broadcast::BroadcastAdapterObservation>, String>,
79 > + Send
80 + 'a,
81 >,
82>;
83
84pub type BroadcastObjectsFuture<'a> =
85 std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<Vec<u8>>, String>> + Send + 'a>>;
86
87pub trait NativeBroadcastAdapter: Send + Sync {
91 fn apply(
92 &self,
93 session: openrtc::broadcast::BroadcastSession,
94 action: openrtc::broadcast::BroadcastAdapterAction,
95 ) -> BroadcastAdapterFuture<'_>;
96
97 fn take_inbound_objects(
101 &self,
102 _session: openrtc::broadcast::BroadcastSession,
103 _max: usize,
104 ) -> BroadcastObjectsFuture<'_> {
105 Box::pin(async { Ok(Vec::new()) })
106 }
107}
108
109#[derive(Debug, Clone)]
110pub struct InstallContext {
111 pub data_dir: PathBuf,
115}
116
117pub trait TransportInstaller: Send + Sync {
121 fn id(&self) -> &'static str;
122 fn is_requested(&self, config: &openrtc::client::TransportConfig) -> bool;
123 fn install(
124 &self,
125 client: Arc<openrtc::client::Client>,
126 config: openrtc::client::TransportConfig,
127 context: InstallContext,
128 ) -> InstallFuture;
129}
130
131pub trait DeviceKeySigner: Send + Sync {
137 fn public_jwk(&self, app_tag: &str) -> Result<serde_json::Value, String>;
138 fn sign(&self, app_tag: &str, challenge: &[u8]) -> Result<Vec<u8>, String>;
139
140 fn offline_assurance(&self, _app_tag: &str) -> openrtc::offline::OfflineAssurance {
143 openrtc::offline::OfflineAssurance::Software
144 }
145 fn delete(&self, app_tag: &str) -> Result<(), String>;
146 fn read_secure_record(&self, _app_tag: &str, _key: &str) -> Result<Option<String>, String> {
147 Ok(None)
148 }
149 fn write_secure_record(&self, _app_tag: &str, _key: &str, _value: &str) -> Result<(), String> {
150 Err("OpenRTC 2.0 native certificate persistence requires a host secure store".to_string())
151 }
152 fn delete_secure_record(&self, _app_tag: &str, _key: &str) -> Result<(), String> {
153 Ok(())
154 }
155}
156
157struct OfflineDeviceSigner<'a> {
158 signer: &'a dyn DeviceKeySigner,
159 app_tag: &'a str,
160}
161
162impl openrtc::offline::OfflineSigner for OfflineDeviceSigner<'_> {
163 fn verifying_key(&self) -> anyhow::Result<VerifyingKey> {
164 let jwk = self
165 .signer
166 .public_jwk(self.app_tag)
167 .map_err(anyhow::Error::msg)?;
168 validate_public_device_jwk(&jwk).map_err(anyhow::Error::msg)?;
169 let encoded = jwk
170 .get("x")
171 .and_then(serde_json::Value::as_str)
172 .ok_or_else(|| anyhow::anyhow!("OpenRTC device JWK is missing x"))?;
173 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
174 .decode(encoded)
175 .map_err(|error| anyhow::anyhow!("decode OpenRTC device JWK: {error}"))?;
176 let bytes: [u8; 32] = bytes
177 .try_into()
178 .map_err(|_| anyhow::anyhow!("OpenRTC device JWK must contain 32 key bytes"))?;
179 VerifyingKey::from_bytes(&bytes)
180 .map_err(|error| anyhow::anyhow!("parse OpenRTC device JWK: {error}"))
181 }
182
183 fn sign(&self, message: &[u8]) -> anyhow::Result<Signature> {
184 let bytes = self
185 .signer
186 .sign(self.app_tag, message)
187 .map_err(anyhow::Error::msg)?;
188 Signature::from_slice(&bytes)
189 .map_err(|error| anyhow::anyhow!("parse OpenRTC device signature: {error}"))
190 }
191}
192
193#[derive(Debug, Clone, Serialize)]
194#[serde(rename_all = "camelCase")]
195struct OfflineRuntimeSupport {
196 provisioning: bool,
197 local_mesh: bool,
198 cloud_required: bool,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 reason: Option<&'static str>,
201}
202
203struct InstalledNativeTransport {
204 client_ptr: usize,
205 _runtime: Box<dyn std::any::Any + Send + Sync>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct OpenRtcTauriConfig {
211 pub api_key: String,
215 #[serde(default)]
216 pub data_dir: Option<PathBuf>,
217 #[serde(default)]
218 pub transport_config: Option<openrtc::client::TransportConfig>,
219}
220
221impl Default for OpenRtcTauriConfig {
222 fn default() -> Self {
223 Self {
224 api_key: String::new(),
225 data_dir: None,
226 transport_config: None,
227 }
228 }
229}
230
231impl OpenRtcTauriConfig {
232 pub fn from_env() -> Self {
233 let api_key = first_env(&[
234 "VITE_OPENRTC_KEY",
235 "VITE_OPENRTC_API_KEY",
236 "VITE_PLUTO_OPENRTC_API_KEY",
237 "OPENRTC_API_KEY",
238 ]);
239 Self {
240 api_key: api_key.unwrap_or_default(),
241 data_dir: None,
242 transport_config: None,
243 }
244 }
245
246 pub fn validated_api_key(&self) -> Result<&str, String> {
247 openrtc::validate_api_key(&self.api_key)
248 .map_err(|error| format!("invalid OpenRTC 2.0 public API key: {error}"))
249 }
250
251 pub fn app_tag(&self) -> Result<String, String> {
252 self.validated_api_key().map(openrtc::app_tag_from_api_key)
253 }
254}
255
256fn first_env(names: &[&str]) -> Option<String> {
257 names.iter().find_map(|name| {
258 std::env::var(name)
259 .ok()
260 .map(|value| value.trim().to_string())
261 .filter(|value| !value.is_empty())
262 })
263}
264
265#[derive(Default)]
266struct TokenRelayState {
267 identity_credential: RwLock<Option<String>>,
268 native_credential: RwLock<Option<(String, Box<dyn Fn() -> Option<String> + Send + Sync>)>>,
269}
270
271impl TokenRelayState {
272 fn token_provider(self: &Arc<Self>) -> Box<dyn Fn() -> Option<String> + Send + Sync> {
273 let relay = self.clone();
274 Box::new(move || {
275 let native = relay.native_credential.read().ok()?;
276 if let Some((_, provider)) = native.as_ref() {
277 return provider();
278 }
279 drop(native);
280 relay
281 .identity_credential
282 .read()
283 .ok()
284 .and_then(|guard| guard.clone())
285 })
286 }
287
288 fn set(&self, identity_credential: Option<String>) {
289 if let Ok(mut guard) = self.identity_credential.write() {
290 *guard = normalize_token(identity_credential);
291 }
292 }
293}
294
295fn normalize_token(value: Option<String>) -> Option<String> {
296 value
297 .map(|value| value.trim().to_string())
298 .filter(|value| !value.is_empty())
299}
300
301#[derive(Debug, Clone)]
302struct NativeCapabilityRegistration {
303 avenue_kind: String,
304 avenue_id: String,
305 sparse_fanout: bool,
306 requested_architecture: Option<openrtc::native::RoomArchitectureMode>,
307 desired_revision: u64,
308 desired_peers: Vec<serde_json::Value>,
309 native_source: Option<NativeRosterSource>,
310 transfer_namespace: String,
311 transfer_demands: HashMap<String, native_transfer_peer_demand::NativeTransferPeerDemand>,
312}
313
314#[derive(Debug, Clone)]
315enum NativeRosterSource {
316 Active(String),
317 Retired,
318}
319
320impl NativeCapabilityRegistration {
321 fn uses_sparse_fanout(&self) -> bool {
322 self.requested_architecture
323 .map(|requested| requested == openrtc::native::RoomArchitectureMode::Sparse)
324 .unwrap_or(self.sparse_fanout)
325 }
326
327 fn owns_native_devices(&self, principal_id: &str) -> bool {
328 self.avenue_kind == "devices" && self.avenue_id == principal_id
329 }
330}
331
332#[derive(Debug, Default)]
333struct NativeCapabilityRegistry {
334 registrations: HashMap<String, NativeCapabilityRegistration>,
335 root_desired_revision: u64,
336}
337
338impl NativeCapabilityRegistry {
339 fn native_snapshot(
340 &mut self,
341 key: &str,
342 source_id: &str,
343 peers: Vec<serde_json::Value>,
344 ) -> Result<Option<(u64, String)>, String> {
345 let Some(registration) = self.registrations.get_mut(key)
346 .filter(|registration| matches!(®istration.native_source, Some(NativeRosterSource::Active(id)) if id == source_id)) else {
347 return Ok(None);
348 };
349 registration.desired_peers = peers;
350 self.aggregate_desired_peers().map(Some)
351 }
352
353 #[cfg(test)]
354 fn register(
355 &mut self,
356 capability_key: String,
357 avenue_kind: String,
358 avenue_id: String,
359 sparse_fanout: bool,
360 ) -> Result<(), String> {
361 self.register_with_architecture(capability_key, avenue_kind, avenue_id, sparse_fanout, None)
362 }
363
364 fn register_with_architecture(
365 &mut self,
366 capability_key: String,
367 avenue_kind: String,
368 avenue_id: String,
369 sparse_fanout: bool,
370 requested_architecture: Option<openrtc::native::RoomArchitectureMode>,
371 ) -> Result<(), String> {
372 if requested_architecture.is_some() && avenue_kind != "room" {
373 return Err("native room architecture is valid only for room avenues".to_string());
374 }
375 if let Some(existing) = self.registrations.get(&capability_key) {
376 if existing.avenue_kind == avenue_kind
377 && existing.avenue_id == avenue_id
378 && existing.sparse_fanout == sparse_fanout
379 && existing.requested_architecture == requested_architecture
380 {
381 return Ok(());
382 }
383 return Err(format!(
384 "native capability key {capability_key} is already registered for another avenue"
385 ));
386 }
387 self.registrations.insert(
388 capability_key,
389 NativeCapabilityRegistration {
390 avenue_kind,
391 avenue_id,
392 sparse_fanout,
393 requested_architecture,
394 desired_revision: 0,
395 desired_peers: Vec::new(),
396 native_source: None,
397 transfer_namespace: uuid::Uuid::new_v4().to_string(),
398 transfer_demands: HashMap::new(),
399 },
400 );
401 Ok(())
402 }
403
404 fn capability_keys_for_identity(
405 &self,
406 connection_id: Option<&str>,
407 device_id: Option<&str>,
408 device_id_hint: Option<&str>,
409 remote_node_id: Option<&str>,
410 ) -> Vec<String> {
411 let identities = [connection_id, device_id, device_id_hint, remote_node_id]
412 .into_iter()
413 .flatten()
414 .map(str::trim)
415 .filter(|value| !value.is_empty())
416 .collect::<BTreeSet<_>>();
417 self.registrations
418 .iter()
419 .filter_map(|(key, registration)| {
420 registration
421 .desired_peers
422 .iter()
423 .any(|peer| {
424 ["connectionId", "deviceId", "nodeId"]
425 .into_iter()
426 .filter_map(|field| peer.get(field).and_then(serde_json::Value::as_str))
427 .map(str::trim)
428 .any(|value| identities.contains(value))
429 })
430 .then(|| key.clone())
431 })
432 .collect::<BTreeSet<_>>()
433 .into_iter()
434 .collect()
435 }
436
437 fn capability_keys_for_state(&self, snapshot: &openrtc::client::StateSnapshot) -> Vec<String> {
438 let mut keys = self
439 .capability_keys_for_identity(
440 Some(&snapshot.connection_id),
441 snapshot.device_id.as_deref(),
442 snapshot.device_id_hint.as_deref(),
443 snapshot.remote_node_id.as_deref(),
444 )
445 .into_iter()
446 .collect::<BTreeSet<_>>();
447 keys.extend(
448 snapshot
449 .scopes
450 .iter()
451 .map(|scope| scope.trim())
452 .filter(|scope| !scope.is_empty() && self.registrations.contains_key(*scope))
453 .map(str::to_string),
454 );
455 keys.into_iter().collect()
456 }
457
458 fn capability_keys_for_peer_data(
459 &self,
460 event: &openrtc::client::NativePeerDataEvent,
461 admitted_scopes: &[String],
462 ) -> Vec<String> {
463 if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&event.payload) {
464 if let Some(capability) = value
465 .get("capability")
466 .and_then(serde_json::Value::as_str)
467 .map(str::trim)
468 .filter(|value| !value.is_empty())
469 {
470 if self.registrations.contains_key(capability)
471 && (!capability.starts_with("ticket:")
472 || admitted_scopes.iter().any(|scope| scope == capability))
473 {
474 return vec![capability.to_string()];
475 }
476 return Vec::new();
477 }
478 }
479 Vec::new()
484 }
485
486 fn projected_stream_capability(
487 &self,
488 channel: &openrtc::stream_metadata::ChannelMetadata,
489 admitted_scopes: &[String],
490 ) -> Option<String> {
491 let explicit = channel
492 .metadata
493 .as_ref()
494 .and_then(|metadata| metadata.get("openrtcCapability"))
495 .and_then(serde_json::Value::as_str)
496 .map(str::trim)
497 .filter(|value| !value.is_empty());
498 if channel.channel_id.starts_with("share/") {
499 let key = match explicit {
502 Some(key)
503 if key.starts_with("ticket:")
504 && admitted_scopes.iter().any(|scope| scope == key) =>
505 {
506 key.to_string()
507 }
508 Some(_) => return None,
509 None => {
510 let tickets = admitted_scopes
511 .iter()
512 .filter(|scope| scope.starts_with("ticket:"))
513 .collect::<Vec<_>>();
514 if tickets.len() != 1 {
515 return None;
516 }
517 tickets[0].clone()
518 }
519 };
520 return self.registrations.contains_key(&key).then_some(key);
521 }
522 match explicit {
523 Some(key)
524 if self.registrations.contains_key(key)
525 && (!key.starts_with("ticket:")
526 || admitted_scopes.iter().any(|scope| scope == key)) =>
527 {
528 Some(key.to_string())
529 }
530 _ => None,
531 }
532 }
533
534 fn aggregate_desired_peers(&mut self) -> Result<(u64, String), String> {
535 let role_priority = |value: &serde_json::Value| match value
536 .get("topologyRole")
537 .and_then(serde_json::Value::as_str)
538 {
539 None => 3_u8,
542 Some("active") => 2,
543 Some("backup") => 1,
544 Some(_) => 0,
545 };
546 let route_evidence = |value: &serde_json::Value| {
547 ["ticket", "nodeId"]
548 .into_iter()
549 .filter(|field| {
550 value
551 .get(field)
552 .and_then(serde_json::Value::as_str)
553 .is_some_and(|part| !part.trim().is_empty())
554 })
555 .count()
556 };
557 let mut references =
558 std::collections::BTreeMap::<String, Vec<(&str, &serde_json::Value)>>::new();
559 let mut registrations = self.registrations.iter().collect::<Vec<_>>();
560 registrations.sort_by(|(left, _), (right, _)| left.cmp(right));
561 for (capability_key, registration) in registrations {
562 for peer in ®istration.desired_peers {
563 let identity = ["deviceId", "nodeId", "ticket"]
564 .into_iter()
565 .find_map(|field| peer.get(field).and_then(serde_json::Value::as_str))
566 .map(str::trim)
567 .filter(|value| !value.is_empty())
568 .ok_or_else(|| "desired peer has no stable identity".to_string())?;
569 references
570 .entry(identity.to_string())
571 .or_default()
572 .push((capability_key.as_str(), peer));
573 }
574 }
575 self.root_desired_revision = self.root_desired_revision.saturating_add(1);
576 let peers = references
577 .into_values()
578 .map(|refs| {
579 let automatic = refs.iter().copied().filter(|(_, peer)|
580 peer.get("connectionIntent").and_then(serde_json::Value::as_str) != Some("observe"))
581 .collect::<Vec<_>>();
582 let refs = if automatic.is_empty() { refs } else { automatic };
583 let semantic_priority = refs
584 .iter()
585 .map(|(_, peer)| role_priority(peer))
586 .max()
587 .unwrap_or_default();
588 let mut evidence = refs[0];
592 for candidate in refs.iter().copied().skip(1) {
593 if (route_evidence(candidate.1), role_priority(candidate.1))
594 > (route_evidence(evidence.1), role_priority(evidence.1))
595 {
596 evidence = candidate;
597 }
598 }
599 let mut peer = evidence.1.clone();
600 if semantic_priority == 3 {
601 if let Some(object) = peer.as_object_mut() {
602 object.remove("topologyRole");
603 object.remove("topologyRevision");
604 }
605 } else {
606 peer["topologyRole"] = serde_json::Value::String(
607 if semantic_priority == 2 {
608 "active"
609 } else {
610 "backup"
611 }
612 .to_string(),
613 );
614 peer["topologyRevision"] = serde_json::Value::from(self.root_desired_revision);
615 }
616 peer
617 })
618 .collect::<Vec<_>>();
619 let payload = serde_json::to_string(&peers)
620 .map_err(|error| format!("serialize aggregated desired peers: {error}"))?;
621 Ok((self.root_desired_revision, payload))
622 }
623}
624
625fn required_native_capability_part(value: String, label: &str) -> Result<String, String> {
626 let value = value.trim().to_string();
627 if value.is_empty()
628 || value.len() > 192
629 || !value
630 .chars()
631 .all(|character| character.is_ascii_alphanumeric() || "_.:@-".contains(character))
632 {
633 return Err(format!("native {label} is invalid"));
634 }
635 Ok(value)
636}
637
638fn normalize_initial_auto_connect_exclusions(
639 excluded_peers: Vec<String>,
640) -> Result<Vec<String>, String> {
641 if excluded_peers.len() > 250 {
642 return Err("native excluded peer list exceeds 250 entries".into());
643 }
644 excluded_peers
645 .into_iter()
646 .map(|device_id| required_native_capability_part(device_id, "excluded peer device id"))
647 .collect::<Result<BTreeSet<_>, _>>()
648 .map(BTreeSet::into_iter)
649 .map(Iterator::collect)
650}
651
652pub struct OpenRtcTauriState {
653 client: RwLock<Arc<openrtc::client::Client>>,
654 config: OpenRtcTauriConfig,
655 token_relay: Arc<TokenRelayState>,
656 native_identity: Mutex<Option<NativeIdentityContext>>,
657 data_dir: Option<PathBuf>,
658 connection_state_forwarder: std::sync::Mutex<Option<tauri::async_runtime::JoinHandle<()>>>,
659 peer_data_forwarder: std::sync::Mutex<Option<tauri::async_runtime::JoinHandle<()>>>,
660 owns_projected_stream_intake: bool,
661 projected_stream_intake: Mutex<Option<tokio::task::JoinHandle<()>>>,
662 presence_loop_active: Mutex<bool>,
663 subscriptions: Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
664 native_projection_subscription: Arc<Mutex<Option<NativeProjectionSubscription>>>,
665 projected_stream_offered: AtomicU64,
666 projected_stream_decoded: AtomicU64,
667 projected_stream_projected: AtomicU64,
668 projected_stream_unhandled_non_channel: AtomicU64,
669 projected_stream_unhandled_other_channel: AtomicU64,
670 projected_stream_unhandled_no_subscription: AtomicU64,
671 projected_stream_failures: AtomicU64,
672 projected_stream_last_transport_stable_id: AtomicU64,
673 projected_stream_validation_checks: AtomicU64,
674 projected_stream_validation_rejections: AtomicU64,
675 projected_stream_last_validated_transport_stable_id: AtomicU64,
676 projected_stream_authorized: AtomicU64,
677 projected_stream_unauthorized: AtomicU64,
678 projected_stream_last_channel: Mutex<Option<String>>,
679 projected_stream_last_protocol: Mutex<Option<String>>,
680 projected_stream_last_connection_id: Mutex<Option<String>>,
681 projected_stream_last_remote_node_id: Mutex<Option<String>>,
682 peer_bi_streams: Mutex<HashMap<String, PeerBiStreamHandle>>,
683 peer_uni_streams: Mutex<HashMap<String, Arc<Mutex<Option<PeerSendStream>>>>>,
684 ticket_meshes: Mutex<HashMap<String, Arc<openrtc::ticket_mesh::TicketMesh>>>,
685 portable_media: Mutex<openrtc::media::PortableMediaSession>,
686 broadcast_sessions: Mutex<HashMap<String, openrtc::broadcast::BroadcastSession>>,
687 broadcast_signers: Mutex<HashMap<String, openrtc::broadcast::BroadcastPublisherSigner>>,
688 broadcast_drivers: Mutex<HashMap<String, Arc<Mutex<()>>>>,
689 managed_session_start_guard: Mutex<()>,
690 managed_session_next_owner_epoch: AtomicU64,
691 managed_session: Mutex<Option<ManagedSessionRecord>>,
692 capability_registry: Arc<Mutex<NativeCapabilityRegistry>>,
693 native_transport_installers: Vec<Arc<dyn TransportInstaller>>,
694 installed_native_transports: Mutex<HashMap<&'static str, InstalledNativeTransport>>,
695 native_device_key_signer: Option<Arc<dyn DeviceKeySigner>>,
696 native_broadcast_adapter: Option<Arc<dyn NativeBroadcastAdapter>>,
697 testing_endpoints: Option<(String, String)>,
698 #[cfg(feature = "managed-group-encryption")]
699 managed_groups: Mutex<HashMap<String, openrtc::native::NativeManagedGroupController>>,
700 #[cfg(feature = "native-broadcast-moq")]
701 native_broadcast_moq_adapter: Option<Arc<native_broadcast_moq::NativeBroadcastMoqAdapter>>,
702}
703
704#[derive(Debug, Clone, serde::Serialize)]
705#[serde(rename_all = "camelCase")]
706struct NativeRuntimeStatusResponse {
707 #[serde(flatten)]
708 status: openrtc::client::RuntimeStatus,
709 product_maturity: openrtc::client::ProductCapabilityMaturity,
710}
711
712impl OpenRtcTauriState {
713 pub fn new(config: OpenRtcTauriConfig) -> Self {
714 openrtc::ensure_rustls();
715
716 let token_relay = Arc::new(TokenRelayState::default());
717 let client = build_client(&config, &token_relay)
718 .expect("OpenRTC Tauri 2.0 requires a valid public API key");
719 let data_dir = config.data_dir.clone();
720 #[cfg(feature = "native-broadcast-moq")]
721 let native_broadcast_moq_adapter =
722 Arc::new(native_broadcast_moq::NativeBroadcastMoqAdapter::new());
723
724 Self {
725 client: RwLock::new(client),
726 config,
727 token_relay,
728 native_identity: Mutex::new(None),
729 data_dir,
730 connection_state_forwarder: std::sync::Mutex::new(None),
731 peer_data_forwarder: std::sync::Mutex::new(None),
732 owns_projected_stream_intake: false,
733 projected_stream_intake: Mutex::new(None),
734 presence_loop_active: Mutex::new(false),
735 subscriptions: Mutex::new(HashMap::new()),
736 native_projection_subscription: Arc::new(Mutex::new(None)),
737 projected_stream_offered: AtomicU64::new(0),
738 projected_stream_decoded: AtomicU64::new(0),
739 projected_stream_projected: AtomicU64::new(0),
740 projected_stream_unhandled_non_channel: AtomicU64::new(0),
741 projected_stream_unhandled_other_channel: AtomicU64::new(0),
742 projected_stream_unhandled_no_subscription: AtomicU64::new(0),
743 projected_stream_failures: AtomicU64::new(0),
744 projected_stream_last_transport_stable_id: AtomicU64::new(0),
745 projected_stream_validation_checks: AtomicU64::new(0),
746 projected_stream_validation_rejections: AtomicU64::new(0),
747 projected_stream_last_validated_transport_stable_id: AtomicU64::new(0),
748 projected_stream_authorized: AtomicU64::new(0),
749 projected_stream_unauthorized: AtomicU64::new(0),
750 projected_stream_last_channel: Mutex::new(None),
751 projected_stream_last_protocol: Mutex::new(None),
752 projected_stream_last_connection_id: Mutex::new(None),
753 projected_stream_last_remote_node_id: Mutex::new(None),
754 peer_bi_streams: Mutex::new(HashMap::new()),
755 peer_uni_streams: Mutex::new(HashMap::new()),
756 ticket_meshes: Mutex::new(HashMap::new()),
757 portable_media: Mutex::new(openrtc::media::PortableMediaSession::default()),
758 broadcast_sessions: Mutex::new(HashMap::new()),
759 broadcast_signers: Mutex::new(HashMap::new()),
760 broadcast_drivers: Mutex::new(HashMap::new()),
761 managed_session_start_guard: Mutex::new(()),
762 managed_session_next_owner_epoch: AtomicU64::new(0),
763 managed_session: Mutex::new(None),
764 capability_registry: Arc::new(Mutex::new(NativeCapabilityRegistry::default())),
765 native_transport_installers: Vec::new(),
766 installed_native_transports: Mutex::new(HashMap::new()),
767 native_device_key_signer: None,
768 testing_endpoints: None,
769 #[cfg(feature = "managed-group-encryption")]
770 managed_groups: Mutex::new(HashMap::new()),
771 native_broadcast_adapter: {
772 #[cfg(feature = "native-broadcast-moq")]
773 {
774 Some(native_broadcast_moq_adapter.clone())
775 }
776 #[cfg(not(feature = "native-broadcast-moq"))]
777 {
778 None
779 }
780 },
781 #[cfg(feature = "native-broadcast-moq")]
782 native_broadcast_moq_adapter: Some(native_broadcast_moq_adapter),
783 }
784 }
785
786 pub fn with_native_transport_installer(
787 mut self,
788 installer: Arc<dyn TransportInstaller>,
789 ) -> Self {
790 self.native_transport_installers.push(installer);
791 self
792 }
793
794 pub fn with_native_device_key_signer(mut self, signer: Arc<dyn DeviceKeySigner>) -> Self {
795 self.native_device_key_signer = Some(signer);
796 self
797 }
798
799 pub fn with_projected_stream_intake(mut self) -> Self {
804 self.owns_projected_stream_intake = true;
805 self
806 }
807
808 pub fn with_testing_endpoints(
812 mut self,
813 control_plane: impl Into<String>,
814 gateway: impl Into<String>,
815 ) -> Self {
816 self.testing_endpoints = Some((control_plane.into(), gateway.into()));
817 self
818 }
819
820 pub fn native_control_plane(
824 &self,
825 assertions: Arc<dyn openrtc::native::AssertionProvider>,
826 ) -> Result<openrtc::native::ControlPlane, String> {
827 let signer = self.native_device_key_signer.clone().ok_or_else(|| {
828 "native control plane requires a host secure-storage signer".to_string()
829 })?;
830 let storage = Arc::new(native_identity::HostIdentityStorage(signer));
831 let control_plane = openrtc::native::ControlPlane::new(
832 self.config.validated_api_key()?,
833 assertions,
834 storage.clone(),
835 storage,
836 )
837 .map_err(|error| error.to_string())?;
838 let control_plane = if let Some((control_plane_endpoint, gateway_endpoint)) =
839 self.testing_endpoints.as_ref()
840 {
841 control_plane
842 .with_testing_endpoints(control_plane_endpoint, gateway_endpoint)
843 .map_err(|error| error.to_string())?
844 } else {
845 control_plane
846 };
847 Ok(control_plane)
848 }
849
850 fn offline_runtime_support(&self) -> OfflineRuntimeSupport {
851 let provisioning = self.native_device_key_signer.is_some();
852 OfflineRuntimeSupport {
853 provisioning,
854 local_mesh: cfg!(feature = "transport-lan"),
855 cloud_required: false,
856 reason: if !provisioning {
857 Some("native host device signer is unavailable")
858 } else {
859 (!cfg!(feature = "transport-lan"))
860 .then_some("native host was built without transport-lan")
861 },
862 }
863 }
864
865 fn project_public_runtime_status(
866 &self,
867 status: openrtc::client::RuntimeStatus,
868 ) -> NativeRuntimeStatusResponse {
869 use openrtc::client::CapabilityMaturity;
870
871 let mut product_maturity = self.client().product_capability_maturity();
872 product_maturity.offline_edge = if self.native_device_key_signer.is_none() {
873 CapabilityMaturity::Unavailable
874 } else if cfg!(feature = "transport-lan") {
875 CapabilityMaturity::Preview
876 } else {
877 CapabilityMaturity::SupportOnly
878 };
879
880 product_maturity.broadcast =
881 if self.native_device_key_signer.is_some() && self.native_broadcast_adapter.is_some() {
882 CapabilityMaturity::Preview
883 } else {
884 CapabilityMaturity::Unavailable
885 };
886 NativeRuntimeStatusResponse {
887 status,
888 product_maturity,
889 }
890 }
891
892 pub fn with_native_broadcast_adapter(
893 mut self,
894 adapter: Arc<dyn NativeBroadcastAdapter>,
895 ) -> Self {
896 self.native_broadcast_adapter = Some(adapter);
897 #[cfg(feature = "native-broadcast-moq")]
898 {
899 self.native_broadcast_moq_adapter = None;
900 }
901 self
902 }
903
904 #[cfg(feature = "native-broadcast-moq")]
905 async fn resolve_native_broadcast_access(
906 &self,
907 grant: &str,
908 publication_verifying_key: Option<&[u8; 32]>,
909 ) -> Result<native_broadcast_moq::ResolvedNativeBroadcastAccess, String> {
910 let api_key = self.config.validated_api_key()?;
911 let app_tag = self.client().app_tag().to_string();
912 let nonce = uuid::Uuid::new_v4().simple().to_string();
913 let issued_at = std::time::SystemTime::now()
914 .duration_since(std::time::UNIX_EPOCH)
915 .unwrap_or_default()
916 .as_secs();
917 let (challenge, publication_key) = native_broadcast_moq::broadcast_access_challenge(
918 api_key,
919 grant,
920 publication_verifying_key,
921 &nonce,
922 issued_at,
923 );
924 let device_proof = native_broadcast_moq::NativeBroadcastDeviceProof {
925 public_key_jwk: self.device_public_key(&app_tag)?,
926 signature: self.sign_device_proof(&app_tag, &challenge)?,
927 nonce,
928 issued_at,
929 };
930 native_broadcast_moq::resolve_broadcast_access(
931 openrtc::native::OPENRTC_PRODUCTION_CONTROL_PLANE,
932 api_key,
933 grant,
934 publication_key.as_deref(),
935 device_proof,
936 )
937 .await
938 }
939
940 fn device_public_key(&self, app_tag: &str) -> Result<serde_json::Value, String> {
941 let app_tag = required_app_tag(app_tag)?;
942 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
943 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
944 })?;
945 let value = signer.public_jwk(app_tag)?;
946 validate_public_device_jwk(&value)?;
947 Ok(value)
948 }
949
950 fn sign_device_proof(&self, app_tag: &str, challenge: &str) -> Result<String, String> {
951 let app_tag = required_app_tag(app_tag)?;
952 if challenge.is_empty() || challenge.len() > 2_048 {
953 return Err("OpenRTC device challenge is invalid".to_string());
954 }
955 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
956 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
957 })?;
958 let signature = signer.sign(app_tag, challenge.as_bytes())?;
959 if signature.len() != 64 {
960 return Err("OpenRTC device signer returned an invalid Ed25519 signature".to_string());
961 }
962 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature))
963 }
964
965 fn sign_device_message(&self, app_tag: &str, message: &[u8]) -> Result<String, String> {
966 let app_tag = required_app_tag(app_tag)?;
967 if message.is_empty() || message.len() > 96 * 1024 {
968 return Err("OpenRTC device message is invalid".to_string());
969 }
970 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
971 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
972 })?;
973 let signature = signer.sign(app_tag, message)?;
974 if signature.len() != 64 {
975 return Err("OpenRTC device signer returned an invalid Ed25519 signature".to_string());
976 }
977 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature))
978 }
979
980 fn delete_device_key(&self, app_tag: &str) -> Result<(), String> {
981 let app_tag = required_app_tag(app_tag)?;
982 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
983 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
984 })?;
985 signer.delete(app_tag)
986 }
987
988 fn read_secure_record(&self, app_tag: &str, key: &str) -> Result<Option<String>, String> {
989 let app_tag = required_app_tag(app_tag)?;
990 let key = required_secure_record_key(key)?;
991 self.native_device_key_signer
992 .as_ref()
993 .ok_or_else(|| {
994 "OpenRTC 2.0 native certificate persistence requires a host secure store"
995 .to_string()
996 })?
997 .read_secure_record(app_tag, key)
998 }
999
1000 fn write_secure_record(&self, app_tag: &str, key: &str, value: &str) -> Result<(), String> {
1001 let app_tag = required_app_tag(app_tag)?;
1002 let key = required_secure_record_key(key)?;
1003 if value.is_empty() || value.len() > 16 * 1024 {
1004 return Err("OpenRTC secure record is invalid".to_string());
1005 }
1006 self.native_device_key_signer
1007 .as_ref()
1008 .ok_or_else(|| {
1009 "OpenRTC 2.0 native certificate persistence requires a host secure store"
1010 .to_string()
1011 })?
1012 .write_secure_record(app_tag, key, value)
1013 }
1014
1015 fn delete_secure_record(&self, app_tag: &str, key: &str) -> Result<(), String> {
1016 let app_tag = required_app_tag(app_tag)?;
1017 let key = required_secure_record_key(key)?;
1018 self.native_device_key_signer
1019 .as_ref()
1020 .ok_or_else(|| {
1021 "OpenRTC 2.0 native certificate persistence requires a host secure store"
1022 .to_string()
1023 })?
1024 .delete_secure_record(app_tag, key)
1025 }
1026
1027 pub fn client(&self) -> Arc<openrtc::client::Client> {
1028 self.client
1029 .read()
1030 .map(|guard| guard.clone())
1031 .expect("OpenRTC Tauri client state is poisoned")
1032 }
1033
1034 pub fn set_identity_credential(&self, identity_credential: Option<String>) {
1035 self.token_relay.set(identity_credential);
1036 }
1037
1038 fn allocate_managed_session_owner_epoch(&self) -> u64 {
1039 self.managed_session_next_owner_epoch
1040 .fetch_add(1, Ordering::SeqCst)
1041 + 1
1042 }
1043
1044 fn replace_connection_state_forwarder(&self, client: Arc<openrtc::client::Client>) {
1045 let capability_registry = self.capability_registry.clone();
1046 let projection_subscription = self.native_projection_subscription.clone();
1047 let next = tauri::async_runtime::spawn(async move {
1048 forward_connection_state_events(client, capability_registry, projection_subscription)
1049 .await;
1050 });
1051 if let Ok(mut guard) = self.connection_state_forwarder.lock() {
1052 if let Some(previous) = guard.replace(next) {
1053 previous.abort();
1054 }
1055 }
1056 }
1057
1058 fn replace_peer_data_forwarder(&self, client: Arc<openrtc::client::Client>) {
1059 let capability_registry = self.capability_registry.clone();
1060 let projection_subscription = self.native_projection_subscription.clone();
1061 let next = tauri::async_runtime::spawn(async move {
1062 forward_peer_data_events(client, capability_registry, projection_subscription).await;
1063 });
1064 if let Ok(mut guard) = self.peer_data_forwarder.lock() {
1065 if let Some(previous) = guard.replace(next) {
1066 previous.abort();
1067 }
1068 }
1069 }
1070}
1071
1072fn build_client(
1073 config: &OpenRtcTauriConfig,
1074 token_relay: &Arc<TokenRelayState>,
1075) -> Result<Arc<openrtc::client::Client>, String> {
1076 let mut builder = openrtc::client::Client::builder(
1077 config.validated_api_key()?.to_string(),
1078 token_relay.token_provider(),
1079 )
1080 .map_err(|error| error.to_string())?;
1081 if let Some(transport_config) = config.transport_config.clone() {
1082 builder = builder.transport_config(transport_config);
1083 }
1084 Ok(Arc::new(builder.build()))
1085}
1086
1087fn requested_local_device_id(value: Option<&str>) -> Option<String> {
1088 value
1089 .map(str::trim)
1090 .filter(|value| !value.is_empty())
1091 .map(ToOwned::to_owned)
1092}
1093
1094fn managed_session_device_id(
1095 _requested: Option<&str>,
1096 native_identity: &openrtc::native_device::NativeDeviceIdentity,
1097) -> String {
1098 native_identity.device_id.clone()
1102}
1103
1104#[cfg(test)]
1105fn metadata_with_authoritative_device_id(
1106 metadata: Option<String>,
1107 device_id: &str,
1108) -> Option<String> {
1109 let device_id = device_id.trim();
1110 if device_id.is_empty() {
1111 return metadata;
1112 }
1113
1114 let Some(raw_metadata) = metadata else {
1115 return Some(serde_json::json!({ "deviceId": device_id }).to_string());
1116 };
1117
1118 match serde_json::from_str::<serde_json::Value>(&raw_metadata) {
1119 Ok(serde_json::Value::Object(mut map)) => {
1120 map.insert(
1121 "deviceId".to_string(),
1122 serde_json::Value::String(device_id.to_string()),
1123 );
1124 Some(serde_json::Value::Object(map).to_string())
1125 }
1126 _ => Some(
1127 serde_json::json!({
1128 "deviceId": device_id,
1129 "metadata": raw_metadata,
1130 })
1131 .to_string(),
1132 ),
1133 }
1134}
1135
1136struct PeerBiStreamHandle {
1137 send: Arc<Mutex<Option<PeerSendStream>>>,
1138 recv: Option<PeerRecvStream>,
1139 read_task: Option<tokio::task::JoinHandle<()>>,
1140}
1141
1142struct NativeProjectionSubscription {
1143 webview_label: String,
1144 request_id: String,
1145 channel: Channel<NativeProjectionEvent>,
1146}
1147
1148#[derive(Serialize, Clone)]
1149#[serde(rename_all = "camelCase")]
1150struct NativeProjectionEvent {
1151 request_id: String,
1152 kind: &'static str,
1153 capability_keys: Vec<String>,
1154 payload: serde_json::Value,
1155}
1156
1157#[derive(Serialize)]
1158#[serde(rename_all = "camelCase")]
1159pub struct OpenBiResult {
1160 stream_id: String,
1161 connection_id: Option<String>,
1162 remote_node_id: String,
1163}
1164
1165#[derive(Serialize)]
1166#[serde(rename_all = "camelCase")]
1167pub struct OpenUniResult {
1168 stream_id: String,
1169 connection_id: Option<String>,
1170 remote_node_id: String,
1171}
1172
1173#[derive(Serialize, Clone)]
1174#[serde(rename_all = "camelCase")]
1175struct IncomingPeerBiStreamEvent {
1176 request_id: String,
1177 stream_id: String,
1178 connection_id: Option<String>,
1179 remote_node_id: String,
1180 transport_stable_id: u64,
1181 channel: Option<openrtc::stream_metadata::ChannelMetadata>,
1182 application_authorized: bool,
1183 capability_key: String,
1184}
1185
1186#[derive(Debug, Clone, Serialize)]
1187#[serde(rename_all = "camelCase")]
1188struct ProjectedStreamDiagnostics {
1189 subscription_active: bool,
1190 offered: u64,
1191 decoded: u64,
1192 projected: u64,
1193 unhandled_non_channel: u64,
1194 unhandled_other_channel: u64,
1195 unhandled_no_subscription: u64,
1196 failures: u64,
1197 last_transport_stable_id: u64,
1198 validation_checks: u64,
1199 validation_rejections: u64,
1200 last_validated_transport_stable_id: u64,
1201 authorized: u64,
1202 unauthorized: u64,
1203 last_channel: Option<String>,
1204 last_protocol: Option<String>,
1205 last_connection_id: Option<String>,
1206 last_remote_node_id: Option<String>,
1207}
1208
1209pub enum ProjectedPeerBiHandoff {
1214 Handled,
1215 Unhandled {
1216 send: PeerSendStream,
1217 recv: PeerRecvStream,
1218 },
1219}
1220
1221#[derive(Serialize, Clone)]
1222#[serde(rename_all = "camelCase")]
1223struct PeerBiStreamClosedEvent {
1224 r#type: &'static str,
1225 error: Option<String>,
1226}
1227
1228#[derive(Debug, Clone, Serialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct StartSessionResult {
1231 local_node_id: String,
1232 ticket_scope: Option<String>,
1233 ticket: Option<String>,
1234 presence_started: bool,
1235 auto_connect_started: bool,
1236 local_device: openrtc::native_device::NativeDeviceIdentity,
1237}
1238
1239#[derive(Clone)]
1240struct ManagedSessionRecord {
1241 key: String,
1242 owner_client: Arc<openrtc::client::Client>,
1243 owner_epoch: u64,
1244 result: StartSessionResult,
1245}
1246
1247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1248enum SessionDisposition {
1249 Start,
1250 Reuse,
1251 Refresh,
1252 Replace,
1253}
1254
1255fn managed_session_disposition(
1256 active_key: Option<&str>,
1257 active_client_matches: bool,
1258 active_presence_started: bool,
1259 active_auto_connect_started: bool,
1260 requested_key: &str,
1261 requested_presence: bool,
1262 requested_auto_connect: bool,
1263) -> SessionDisposition {
1264 let Some(active_key) = active_key else {
1265 return SessionDisposition::Start;
1266 };
1267 if !active_client_matches || active_key != requested_key {
1268 return SessionDisposition::Replace;
1269 }
1270 if (!requested_presence || active_presence_started)
1271 && (!requested_auto_connect || active_auto_connect_started)
1272 {
1273 SessionDisposition::Reuse
1274 } else {
1275 SessionDisposition::Refresh
1276 }
1277}
1278
1279fn revokes_managed_session(scope: &str) -> bool {
1280 scope.trim() == "user-device"
1281}
1282
1283pub fn init<R: Runtime>(config: OpenRtcTauriConfig) -> tauri::plugin::TauriPlugin<R> {
1284 init_with_state(OpenRtcTauriState::new(config))
1285}
1286
1287pub fn init_with_state<R: Runtime>(state: OpenRtcTauriState) -> tauri::plugin::TauriPlugin<R> {
1288 tauri::plugin::Builder::new(PLUGIN_NAME)
1289 .setup(move |app, _api| {
1290 let client = state.client();
1291 let transport_config = state.config.transport_config.clone();
1292 let install_context = InstallContext {
1297 data_dir: app_data_dir(app, &state).map_err(std::io::Error::other)?,
1298 };
1299 tauri::async_runtime::block_on(ensure_requested_native_transports(
1300 &state,
1301 &client,
1302 transport_config.as_ref(),
1303 &install_context,
1304 ))
1305 .map_err(std::io::Error::other)?;
1306 state.replace_connection_state_forwarder(client.clone());
1307 state.replace_peer_data_forwarder(client);
1308 app.manage(state);
1309 Ok(())
1310 })
1311 .invoke_handler(tauri::generate_handler![
1312 openrtc_configure_native_identity,
1313 openrtc_complete_native_assertion,
1314 openrtc_clear_native_identity,
1315 openrtc_start_native_devices,
1316 openrtc_publish_native_devices,
1317 openrtc_list_native_devices,
1318 openrtc_set_identity_credential,
1319 openrtc_device_public_key,
1320 openrtc_sign_device_proof,
1321 openrtc_delete_device_key,
1322 openrtc_read_secure_record,
1323 openrtc_write_secure_record,
1324 openrtc_delete_secure_record,
1325 openrtc_offline_runtime_support,
1326 openrtc_create_offline_enrollment_request,
1327 openrtc_verify_offline_enrollment_request,
1328 rtc_native_status,
1329 get_rtc_local_device_info,
1330 update_rtc_local_device_name,
1331 get_iroh_node_id,
1332 start_iroh_node,
1333 get_iroh_endpoint_ticket,
1334 register_session_token,
1335 get_endpoint_ticket_with_token,
1336 issue_ticket_invite,
1337 connect_ticket_invite,
1338 issue_ticket_mesh,
1339 join_ticket_mesh,
1340 ticket_mesh_peers,
1341 ticket_mesh_issuer_node,
1342 close_ticket_mesh,
1343 validate_session_token,
1344 revoke_session_tokens_by_scope,
1345 stop_rtc_presence_loop,
1346 register_rtc_capability,
1347 openrtc_init_managed_room_group,
1348 openrtc_handle_managed_room_prepare_page,
1349 openrtc_handle_managed_room_artifact_chunk,
1350 openrtc_seal_managed_room_payload,
1351 openrtc_open_managed_room_payload,
1352 openrtc_forget_managed_room_group,
1353 unregister_rtc_capability,
1354 start_rtc_managed_session,
1355 start_rtc_external_auto_connect,
1356 submit_rtc_desired_peers,
1357 request_rtc_transfer_peer,
1358 get_rtc_transfer_peer_demand_status,
1359 release_rtc_transfer_peer,
1360 set_rtc_transfer_peer_budget,
1361 stop_rtc_auto_connect,
1362 notify_rtc_network_change,
1363 set_rtc_transport_priority,
1364 connect_to_device,
1365 connect_to_known_device_with_token,
1366 observe_known_device_endpoint,
1367 disconnect_device,
1368 set_auto_connect_excluded,
1369 set_rtc_external_auto_connect_excluded,
1370 resolve_rtc_peer_connection_records,
1371 resolve_rtc_peer_identity,
1372 get_rtc_peer_session,
1373 list_rtc_peer_sessions,
1374 list_rtc_managed_connections,
1375 wait_for_rtc_settled_peer,
1376 wait_for_rtc_settled_scope,
1377 list_rtc_connection_states,
1378 get_rtc_connection_state,
1379 stop_rtc_subscription,
1380 start_native_projection,
1381 get_projected_stream_diagnostics,
1382 is_current_transport_stable_id,
1383 send_peer_message,
1384 encode_sparse_fanout_message,
1385 accept_sparse_fanout_message,
1386 sparse_fanout_diagnostics,
1387 prepare_openrtc_broadcast_publisher,
1388 release_openrtc_broadcast_publisher,
1389 open_openrtc_broadcast,
1390 begin_openrtc_broadcast_publication,
1391 publish_openrtc_broadcast_sample,
1392 pause_openrtc_broadcast_publication,
1393 retire_openrtc_broadcast_publication,
1394 retire_openrtc_broadcast_receiver,
1395 receive_openrtc_broadcast_media,
1396 get_openrtc_broadcast_stats,
1397 revoke_openrtc_broadcast,
1398 close_openrtc_broadcast,
1399 record_sparse_fanout_forward_queue_drop,
1400 is_peer_connected,
1401 open_peer_bi_stream,
1402 open_peer_bi_transport_only_stream,
1403 open_peer_uni_stream,
1404 write_peer_bi_stream,
1405 finish_peer_bi_stream_send,
1406 start_peer_bi_stream_read,
1407 cancel_peer_bi_stream_read,
1408 close_peer_bi_stream,
1409 write_peer_uni_stream,
1410 close_peer_uni_stream,
1411 begin_openrtc_media_publication,
1412 encode_openrtc_media_sample,
1413 pause_openrtc_media_publication,
1414 retire_openrtc_media_publication,
1415 retire_openrtc_media_receiver,
1416 decode_openrtc_media_chunk,
1417 decode_openrtc_media_control,
1418 ])
1419 .build()
1420}
1421
1422pub fn init_from_env<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
1423 init(OpenRtcTauriConfig::from_env())
1424}
1425
1426async fn send_native_projection<T: Serialize>(
1427 subscription: &Arc<Mutex<Option<NativeProjectionSubscription>>>,
1428 kind: &'static str,
1429 capability_keys: Vec<String>,
1430 payload: &T,
1431) {
1432 if capability_keys.is_empty() {
1433 return;
1434 }
1435 let Ok(payload) = serde_json::to_value(payload) else {
1436 return;
1437 };
1438 let target = subscription.lock().await.as_ref().map(|subscription| {
1439 (
1440 subscription.request_id.clone(),
1441 subscription.channel.clone(),
1442 )
1443 });
1444 let Some((request_id, channel)) = target else {
1445 return;
1446 };
1447 let _ = channel.send(NativeProjectionEvent {
1448 request_id,
1449 kind,
1450 capability_keys,
1451 payload,
1452 });
1453}
1454
1455async fn forward_native_service_error(
1456 registry: &Arc<Mutex<NativeCapabilityRegistry>>,
1457 projection: &Arc<Mutex<Option<NativeProjectionSubscription>>>,
1458 identity_id: &str,
1459 capability_key: &str,
1460 observation: &openrtc::native::NativeServiceErrorObservation,
1461) -> bool {
1462 let registry = registry.lock().await;
1465 let current = registry.registrations.get(capability_key).is_some_and(|entry| {
1466 matches!(&entry.native_source, Some(NativeRosterSource::Active(id)) if id == identity_id)
1467 && observation.avenue.kind == "user"
1468 && entry.owns_native_devices(&observation.avenue.id)
1469 });
1470 if !current { return false; }
1471 send_native_projection(projection, "service-error", vec![capability_key.to_owned()],
1472 &serde_json::json!({
1473 "identityId": identity_id,
1474 "avenue": observation.avenue,
1475 "runtimeInstanceId": observation.runtime_instance_id,
1476 "serviceError": observation.service_error,
1477 })).await;
1478 true
1479}
1480
1481async fn forward_connection_state_events(
1482 client: Arc<openrtc::client::Client>,
1483 capability_registry: Arc<Mutex<NativeCapabilityRegistry>>,
1484 projection_subscription: Arc<Mutex<Option<NativeProjectionSubscription>>>,
1485) {
1486 let mut rx = client.connection_state_updates();
1487 while let Ok(snapshot) = rx.recv().await {
1488 let keys = capability_registry
1489 .lock()
1490 .await
1491 .capability_keys_for_state(&snapshot);
1492 send_native_projection(
1493 &projection_subscription,
1494 "connection-state",
1495 keys,
1496 &snapshot,
1497 )
1498 .await;
1499 }
1500}
1501
1502async fn forward_peer_data_events(
1503 client: Arc<openrtc::client::Client>,
1504 capability_registry: Arc<Mutex<NativeCapabilityRegistry>>,
1505 projection_subscription: Arc<Mutex<Option<NativeProjectionSubscription>>>,
1506) {
1507 let mut rx = client.subscribe_native_peer_data();
1508 while let Ok(event) = rx.recv().await {
1509 let keys = capability_registry
1510 .lock()
1511 .await
1512 .capability_keys_for_peer_data(
1513 &event,
1514 &client.session_admitted_scopes(&event.connection_id),
1515 );
1516 send_native_projection(&projection_subscription, "peer-data", keys, &event).await;
1517 }
1518}
1519
1520async fn replay_current_connection_states(state: &OpenRtcTauriState) {
1521 let snapshots = state.client().connection_states().await;
1522 let registry = state.capability_registry.lock().await;
1523 for snapshot in snapshots {
1524 let keys = registry.capability_keys_for_state(&snapshot);
1525 send_native_projection(
1526 &state.native_projection_subscription,
1527 "connection-state",
1528 keys,
1529 &snapshot,
1530 )
1531 .await;
1532 }
1533}
1534
1535fn app_data_dir<R: Runtime>(
1536 app: &tauri::AppHandle<R>,
1537 state: &OpenRtcTauriState,
1538) -> Result<PathBuf, String> {
1539 state
1540 .data_dir
1541 .clone()
1542 .or_else(|| app.path().app_data_dir().ok())
1543 .ok_or_else(|| "failed to resolve OpenRTC app data directory".to_string())
1544}
1545
1546async fn ensure_iroh_node(client: Arc<openrtc::client::Client>) -> Result<String, String> {
1547 if let Some(node_id) = client.current_node_id().await {
1548 return Ok(node_id);
1549 }
1550 client
1551 .init_iroh(None, Vec::new())
1552 .await
1553 .map_err(|error| format!("failed to initialize OpenRTC Iroh node: {error}"))
1554}
1555
1556async fn ensure_requested_native_transports(
1557 state: &OpenRtcTauriState,
1558 client: &Arc<openrtc::client::Client>,
1559 transports: Option<&openrtc::client::TransportConfig>,
1560 context: &InstallContext,
1561) -> Result<(), String> {
1562 let Some(transports) = transports else {
1563 return Ok(());
1564 };
1565 let ble_requested = transports.ble.as_ref().is_some_and(|config| config.enabled);
1566 if ble_requested
1567 && !state
1568 .native_transport_installers
1569 .iter()
1570 .any(|installer| installer.is_requested(transports))
1571 {
1572 eprintln!(
1573 "[openrtc-tauri][transport] BLE requested but unavailable: this host did not register a BLE transport installer; continuing on the Iroh base route"
1574 );
1575 return Ok(());
1576 }
1577 let client_ptr = Arc::as_ptr(client) as usize;
1578 for installer in &state.native_transport_installers {
1579 if !installer.is_requested(transports) {
1580 continue;
1581 }
1582 let already_installed = state
1583 .installed_native_transports
1584 .lock()
1585 .await
1586 .get(installer.id())
1587 .is_some_and(|installed| installed.client_ptr == client_ptr);
1588 if already_installed {
1589 continue;
1590 }
1591 if client.current_node_id().await.is_some() {
1592 eprintln!(
1593 "[openrtc-tauri][transport] {} requested after native node startup; continuing on the Iroh base route",
1594 installer.id()
1595 );
1596 continue;
1597 }
1598 let runtime = match installer
1599 .install(client.clone(), transports.clone(), context.clone())
1600 .await
1601 {
1602 Ok(runtime) => runtime,
1603 Err(error) => {
1604 eprintln!(
1605 "[openrtc-tauri][transport] {} unavailable: {}; continuing on the Iroh base route",
1606 installer.id(),
1607 error
1608 );
1609 continue;
1610 }
1611 };
1612 state.installed_native_transports.lock().await.insert(
1613 installer.id(),
1614 InstalledNativeTransport {
1615 client_ptr,
1616 _runtime: runtime,
1617 },
1618 );
1619 }
1620 Ok(())
1621}
1622
1623async fn register_peer_bi_stream(
1624 state: &OpenRtcTauriState,
1625 connection_id: Option<String>,
1626 remote_node_id: String,
1627 send: PeerSendStream,
1628 recv: PeerRecvStream,
1629) -> OpenBiResult {
1630 let stream_id = uuid::Uuid::new_v4().to_string();
1631 let send = Arc::new(Mutex::new(Some(send)));
1632
1633 state.peer_bi_streams.lock().await.insert(
1634 stream_id.clone(),
1635 PeerBiStreamHandle {
1636 send,
1637 recv: Some(recv),
1640 read_task: None,
1641 },
1642 );
1643
1644 OpenBiResult {
1645 stream_id,
1646 connection_id,
1647 remote_node_id,
1648 }
1649}
1650
1651async fn read_peer_exact(recv: &mut PeerRecvStream, buffer: &mut [u8]) -> std::io::Result<()> {
1652 let mut offset = 0;
1653 while offset < buffer.len() {
1654 let read = recv.read(&mut buffer[offset..]).await?;
1655 if read == 0 {
1656 return Err(std::io::Error::new(
1657 std::io::ErrorKind::UnexpectedEof,
1658 "peer stream ended during channel classification",
1659 ));
1660 }
1661 offset += read;
1662 }
1663 Ok(())
1664}
1665
1666pub async fn handoff_projected_peer_bi_stream<R: Runtime>(
1676 app: tauri::AppHandle<R>,
1677 connection_id: Option<String>,
1678 remote_node_id: String,
1679 transport_stable_id: u64,
1680 send: PeerSendStream,
1681 mut recv: PeerRecvStream,
1682) -> Result<ProjectedPeerBiHandoff, String> {
1683 let state = app.state::<OpenRtcTauriState>();
1684 state
1685 .projected_stream_offered
1686 .fetch_add(1, Ordering::Relaxed);
1687 state
1688 .projected_stream_last_transport_stable_id
1689 .store(transport_stable_id, Ordering::Relaxed);
1690 *state.projected_stream_last_connection_id.lock().await = connection_id.clone();
1691 *state.projected_stream_last_remote_node_id.lock().await = Some(remote_node_id.clone());
1692 let mut envelope = Vec::new();
1693 let channel = loop {
1694 match openrtc::stream_metadata::decode_prefix(&envelope) {
1695 openrtc::stream_metadata::DecodeDecision::NotMatched => {
1696 state
1697 .projected_stream_unhandled_non_channel
1698 .fetch_add(1, Ordering::Relaxed);
1699 return Ok(ProjectedPeerBiHandoff::Unhandled {
1700 send,
1701 recv: recv.with_plaintext_prefix(envelope),
1702 });
1703 }
1704 openrtc::stream_metadata::DecodeDecision::Decoded(prefix) => break prefix.channel,
1705 openrtc::stream_metadata::DecodeDecision::NeedMore(required) => {
1706 let missing = required.saturating_sub(envelope.len());
1707 if missing == 0 {
1708 return Err("channel-envelope decoder made no progress".to_string());
1709 }
1710 let mut bytes = vec![0u8; missing];
1711 if let Err(error) = read_peer_exact(&mut recv, &mut bytes).await {
1712 state
1713 .projected_stream_failures
1714 .fetch_add(1, Ordering::Relaxed);
1715 return Err(format!("read projected channel envelope: {error}"));
1716 }
1717 envelope.extend_from_slice(&bytes);
1718 }
1719 }
1720 };
1721 state
1722 .projected_stream_decoded
1723 .fetch_add(1, Ordering::Relaxed);
1724 *state.projected_stream_last_channel.lock().await = Some(channel.channel_id.clone());
1725 *state.projected_stream_last_protocol.lock().await = channel
1726 .metadata
1727 .as_ref()
1728 .and_then(|metadata| metadata.get("protocol"))
1729 .and_then(serde_json::Value::as_str)
1730 .map(str::to_string);
1731
1732 let admitted_scopes = connection_id
1733 .as_deref()
1734 .map(|connection_id| state.client().session_admitted_scopes(connection_id))
1735 .unwrap_or_default();
1736 let Some(capability_key) = state
1737 .capability_registry
1738 .lock()
1739 .await
1740 .projected_stream_capability(&channel, &admitted_scopes)
1741 else {
1742 state
1746 .projected_stream_unhandled_other_channel
1747 .fetch_add(1, Ordering::Relaxed);
1748 return Ok(ProjectedPeerBiHandoff::Unhandled {
1749 send,
1750 recv: recv.with_plaintext_prefix(envelope),
1751 });
1752 };
1753
1754 let subscription = state.native_projection_subscription.lock().await;
1755 let Some((request_id, projection_channel)) = subscription.as_ref().map(|subscription| {
1756 (
1757 subscription.request_id.clone(),
1758 subscription.channel.clone(),
1759 )
1760 }) else {
1761 state
1762 .projected_stream_unhandled_no_subscription
1763 .fetch_add(1, Ordering::Relaxed);
1764 return Ok(ProjectedPeerBiHandoff::Unhandled {
1765 send,
1766 recv: recv.with_plaintext_prefix(envelope),
1767 });
1768 };
1769 drop(subscription);
1770
1771 let application_authorized = connection_id.as_deref().is_some_and(|connection_id| {
1772 matches!(
1773 state.client().session_admission(connection_id),
1774 openrtc::session_token::SessionAdmission::Accepted { .. }
1775 )
1776 });
1777 if application_authorized {
1778 state
1779 .projected_stream_authorized
1780 .fetch_add(1, Ordering::Relaxed);
1781 } else {
1782 state
1783 .projected_stream_unauthorized
1784 .fetch_add(1, Ordering::Relaxed);
1785 }
1786
1787 let result = register_peer_bi_stream(
1788 state.inner(),
1789 connection_id,
1790 remote_node_id.clone(),
1791 send,
1792 recv,
1793 )
1794 .await;
1795 let event = IncomingPeerBiStreamEvent {
1796 request_id: request_id.clone(),
1797 stream_id: result.stream_id.clone(),
1798 connection_id: result.connection_id,
1799 remote_node_id,
1800 transport_stable_id,
1801 channel: Some(channel),
1802 application_authorized,
1803 capability_key: capability_key.clone(),
1804 };
1805 let payload = serde_json::to_value(event)
1806 .map_err(|error| format!("serialize projected peer stream: {error}"))?;
1807 if let Err(error) = projection_channel.send(NativeProjectionEvent {
1808 request_id,
1809 kind: "stream",
1810 capability_keys: vec![capability_key],
1811 payload,
1812 }) {
1813 state.peer_bi_streams.lock().await.remove(&result.stream_id);
1814 state
1815 .projected_stream_failures
1816 .fetch_add(1, Ordering::Relaxed);
1817 return Err(format!("send projected peer stream: {error}"));
1818 }
1819 state
1820 .projected_stream_projected
1821 .fetch_add(1, Ordering::Relaxed);
1822 Ok(ProjectedPeerBiHandoff::Handled)
1823}
1824
1825fn native_stream_trace_enabled() -> bool {
1826 cfg!(debug_assertions)
1827 || std::env::var("OPENRTC_NATIVE_STREAM_TRACE").ok().as_deref() == Some("1")
1828}
1829
1830fn spawn_peer_bi_stream_reader(
1831 channel: Channel<Response>,
1832 stream_id: String,
1833 mut recv: PeerRecvStream,
1834) -> tokio::task::JoinHandle<()> {
1835 tokio::spawn(async move {
1836 let mut chunk = vec![0_u8; 64 * 1024];
1837 let mut close_error: Option<String> = None;
1838 loop {
1839 match recv.read(&mut chunk).await {
1840 Ok(0) => break,
1841 Ok(n) => {
1842 if native_stream_trace_enabled() {
1843 eprintln!(
1844 "[openrtc-tauri][peer-bi-stream] stream_id={} phase=plaintext-chunk bytes={}",
1845 stream_id, n
1846 );
1847 }
1848 if channel.send(Response::new(chunk[..n].to_vec())).is_err() {
1849 close_error = Some("native stream IPC channel closed".to_string());
1850 break;
1851 }
1852 }
1853 Err(error) => {
1854 if native_stream_trace_enabled() {
1855 eprintln!(
1856 "[openrtc-tauri][peer-bi-stream] stream_id={} phase=read-error error={}",
1857 stream_id, error
1858 );
1859 }
1860 close_error = Some(error.to_string());
1861 break;
1862 }
1863 }
1864 }
1865 let close = serde_json::to_string(&PeerBiStreamClosedEvent {
1866 r#type: "closed",
1867 error: close_error,
1868 })
1869 .expect("peer stream close event serializes");
1870 let _ = channel.send(Response::new(close));
1871 })
1872}
1873
1874async fn open_peer_bi_with<R: Runtime, F, Fut>(
1875 _app: tauri::AppHandle<R>,
1876 state: tauri::State<'_, OpenRtcTauriState>,
1877 peer_id: String,
1878 timeout_ms: Option<u64>,
1879 open: F,
1880) -> Result<OpenBiResult, String>
1881where
1882 F: FnOnce(Arc<openrtc::client::Client>, String, Option<u64>) -> Fut,
1883 Fut: std::future::Future<
1884 Output = anyhow::Result<(Option<String>, String, PeerSendStream, PeerRecvStream)>,
1885 >,
1886{
1887 let peer_id = peer_id.trim().to_string();
1888 if peer_id.is_empty() {
1889 return Err("peerId is required".to_string());
1890 }
1891
1892 let (connection_id, remote_node_id, send, recv) = open(state.client(), peer_id, timeout_ms)
1893 .await
1894 .map_err(|error| format!("open peer bi stream failed: {error}"))?;
1895 Ok(register_peer_bi_stream(&state, connection_id, remote_node_id, send, recv).await)
1896}
1897
1898#[tauri::command]
1902async fn openrtc_set_identity_credential(
1903 state: tauri::State<'_, OpenRtcTauriState>,
1904 identity_credential: Option<String>,
1905) -> Result<(), String> {
1906 state.set_identity_credential(identity_credential);
1907 if *state.presence_loop_active.lock().await {
1908 state.client().request_presence_update();
1909 }
1910 Ok(())
1911}
1912
1913struct NativeIdentityContext {
1914 id: String,
1915 webview_label: String,
1916 assertions: Arc<NativeAssertionBridge>,
1917 control_plane: openrtc::native::ControlPlane,
1918 devices: Option<Arc<openrtc::native::Devices>>,
1919 roster: Option<NativeDeviceRoster>,
1920}
1921
1922struct NativeDeviceRoster {
1923 capability_key: String,
1924 task: Option<tokio::task::JoinHandle<()>>,
1925}
1926
1927fn native_roster_is_active_for_capability(
1928 roster: Option<&NativeDeviceRoster>,
1929 capability_key: &str,
1930) -> Result<bool, String> {
1931 let Some(roster) = roster else {
1932 return Ok(false);
1933 };
1934 if roster.capability_key != capability_key {
1935 return Err("native devices already have a capability owner".into());
1936 }
1937 Ok(roster.task.as_ref().is_some_and(|task| !task.is_finished()))
1938}
1939
1940impl Drop for NativeDeviceRoster {
1941 fn drop(&mut self) {
1942 if let Some(task) = self.task.take() {
1943 task.abort();
1944 }
1945 }
1946}
1947
1948impl OpenRtcTauriState {
1949 async fn configure_native_identity(
1950 &self,
1951 webview_label: &str,
1952 session_key: String,
1953 sink: impl Fn(NativeAssertionRequest) -> Result<(), String> + Send + Sync + 'static,
1954 ) -> Result<String, String> {
1955 use openrtc::native::AssertionProvider;
1956 let mut current = self.native_identity.lock().await;
1957 if let Some(owner) = current.as_mut() {
1958 if owner.webview_label != webview_label {
1959 return Err("native identity is owned by another webview".into());
1960 }
1961 if owner
1962 .assertions
1963 .session_key()
1964 .map_err(|error| error.to_string())?
1965 .as_deref()
1966 == Some(&session_key)
1967 {
1968 owner
1969 .assertions
1970 .rebind(sink)
1971 .map_err(|error| error.to_string())?;
1972 return Ok(owner.id.clone());
1973 }
1974 }
1975 let assertions = Arc::new(
1976 NativeAssertionBridge::new(session_key, sink).map_err(|error| error.to_string())?,
1977 );
1978 let control_plane = self.native_control_plane(assertions.clone())?;
1979 if let Some(owner) = current.take() {
1980 self.retire_native_identity(owner).await?;
1981 }
1982 let id = uuid::Uuid::new_v4().to_string();
1983 *current = Some(NativeIdentityContext {
1984 id: id.clone(),
1985 webview_label: webview_label.into(),
1986 assertions,
1987 control_plane,
1988 devices: None,
1989 roster: None,
1990 });
1991 Ok(id)
1992 }
1993}
1994
1995#[tauri::command]
1996async fn openrtc_configure_native_identity<R: Runtime>(
1997 webview: Webview<R>,
1998 state: tauri::State<'_, OpenRtcTauriState>,
1999 session_key: String,
2000 requests: Channel<NativeAssertionRequest>,
2001) -> Result<String, String> {
2002 state
2003 .configure_native_identity(webview.label(), session_key, move |request| {
2004 requests.send(request).map_err(|error| error.to_string())
2005 })
2006 .await
2007}
2008
2009#[tauri::command]
2010async fn openrtc_complete_native_assertion<R: Runtime>(
2011 webview: Webview<R>,
2012 state: tauri::State<'_, OpenRtcTauriState>,
2013 identity_id: String,
2014 request_id: String,
2015 token: Option<String>,
2016 provider_id: Option<String>,
2017 error: Option<String>,
2018) -> Result<bool, String> {
2019 let current = state.native_identity.lock().await;
2020 let Some(owner) = current.as_ref().filter(|owner| owner.id == identity_id) else {
2021 return Ok(false);
2022 };
2023 if owner.webview_label != webview.label() {
2024 return Err("native identity is owned by another webview".into());
2025 }
2026 let result = match (token, error) {
2027 (Some(token), None) => Ok(openrtc::native::IdentityAssertion { token, provider_id }),
2028 (None, Some(error)) => Err(error),
2029 _ => return Err("provide either an assertion or an error".into()),
2030 };
2031 owner
2032 .assertions
2033 .complete(&request_id, result)
2034 .map_err(|error| error.to_string())
2035}
2036
2037impl OpenRtcTauriState {
2038 async fn clear_native_identity(
2039 &self,
2040 webview_label: &str,
2041 identity_id: &str,
2042 ) -> Result<bool, String> {
2043 let owner = {
2044 let mut current = self.native_identity.lock().await;
2045 let Some(owner) = current.as_ref().filter(|owner| owner.id == identity_id) else {
2046 return Ok(false);
2047 };
2048 if owner.webview_label != webview_label {
2049 return Err("native identity is owned by another webview".into());
2050 }
2051 current.take().expect("matching identity")
2052 };
2053 self.retire_native_identity(owner).await?;
2054 Ok(true)
2055 }
2056
2057 async fn retire_native_identity(&self, mut owner: NativeIdentityContext) -> Result<(), String> {
2058 drop(owner.roster.take());
2059 owner
2060 .assertions
2061 .retire()
2062 .map_err(|error| error.to_string())?;
2063 {
2064 let mut native = self
2065 .token_relay
2066 .native_credential
2067 .write()
2068 .map_err(|_| "native credential lock poisoned")?;
2069 if native.as_ref().is_some_and(|(id, _)| id == &owner.id) {
2070 *native = None;
2071 self.token_relay.set(None);
2072 }
2073 }
2074 let (update, demands) = {
2075 let mut registry = self.capability_registry.lock().await;
2076 let mut demands = Vec::new();
2077 let mut changed = false;
2078 for registration in registry.registrations.values_mut() {
2079 if matches!(®istration.native_source, Some(NativeRosterSource::Active(id)) if id == &owner.id)
2080 {
2081 registration.native_source = Some(NativeRosterSource::Retired);
2082 demands.extend(registration.transfer_demands.drain().map(|(_, entry)| entry));
2083 registration.desired_peers.clear();
2084 changed = true;
2085 }
2086 }
2087 let update = if changed {
2088 Some(registry.aggregate_desired_peers()?)
2089 } else {
2090 None
2091 };
2092 (update, demands)
2093 };
2094 self.release_native_transfer_peer_demands(demands).await;
2095 if let Some(devices) = owner.devices {
2096 devices.close().await;
2097 }
2098 if let Some((revision, peers)) = update {
2099 self.client()
2100 .withdraw_external_desired_peers(revision, &peers)
2101 .await
2102 .map_err(|error| error.to_string())?;
2103 }
2104 Ok(())
2105 }
2106}
2107
2108#[tauri::command]
2109async fn openrtc_clear_native_identity<R: Runtime>(
2110 webview: Webview<R>,
2111 state: tauri::State<'_, OpenRtcTauriState>,
2112 identity_id: String,
2113) -> Result<bool, String> {
2114 state
2115 .clear_native_identity(webview.label(), &identity_id)
2116 .await
2117}
2118
2119#[derive(Serialize)]
2120#[serde(rename_all = "camelCase")]
2121struct NativeDevicesIdentity {
2122 principal_id: String,
2123 device_id: String,
2124}
2125
2126#[derive(Debug, Serialize)]
2129#[serde(untagged)]
2130enum NativeCommandError {
2131 Service {
2132 #[serde(flatten)]
2133 details: openrtc::service_errors::ServiceError,
2134 message: &'static str,
2135 },
2136 Legacy(String),
2137}
2138
2139impl NativeCommandError {
2140 fn from_runtime(error: anyhow::Error) -> Self {
2141 match openrtc::service_errors::ServiceError::from_error(&error) {
2142 Some(details) => Self::Service {
2143 details: details.clone(),
2144 message: "OpenRTC service request denied",
2145 },
2146 None => Self::Legacy(error.to_string()),
2147 }
2148 }
2149}
2150
2151impl From<String> for NativeCommandError {
2152 fn from(value: String) -> Self { Self::Legacy(value) }
2153}
2154
2155impl From<&str> for NativeCommandError {
2156 fn from(value: &str) -> Self { Self::Legacy(value.to_owned()) }
2157}
2158
2159async fn submit_native_device_snapshot(
2160 client: &Arc<openrtc::client::Client>,
2161 registry: &Arc<Mutex<NativeCapabilityRegistry>>,
2162 source_id: &str,
2163 capability_key: &str,
2164 local_device_id: &str,
2165 devices: &[openrtc::signaling::Device],
2166 auto_connect: bool,
2167) -> Result<bool, String> {
2168 let peers = devices
2169 .iter()
2170 .filter(|device| device.device_id != local_device_id)
2171 .map(|device| {
2172 let mut peer = serde_json::to_value(device)?;
2173 if !auto_connect {
2174 peer["connectionIntent"] = serde_json::Value::String("observe".into());
2175 }
2176 Ok(peer)
2177 })
2178 .collect::<Result<Vec<_>, serde_json::Error>>()
2179 .map_err(|error| error.to_string())?;
2180 let update = registry
2181 .lock()
2182 .await
2183 .native_snapshot(capability_key, source_id, peers)?;
2184 let Some((revision, aggregate)) = update else {
2185 return Ok(false);
2186 };
2187 client
2188 .submit_external_peer_intents(revision, &aggregate)
2189 .await
2190 .map_err(|error| error.to_string())
2191}
2192
2193#[tauri::command]
2194async fn openrtc_list_native_devices<R: Runtime>(
2195 webview: Webview<R>,
2196 state: tauri::State<'_, OpenRtcTauriState>,
2197 identity_id: String,
2198) -> Result<Vec<openrtc::signaling::Device>, String> {
2199 let devices = {
2200 let current = state.native_identity.lock().await;
2201 let owner = current
2202 .as_ref()
2203 .filter(|owner| owner.id == identity_id)
2204 .ok_or("native identity is no longer current")?;
2205 if owner.webview_label != webview.label() {
2206 return Err("native identity is owned by another webview".into());
2207 }
2208 owner
2209 .devices
2210 .clone()
2211 .ok_or("native devices have not started")?
2212 };
2213 devices
2214 .signaling()
2215 .list_devices(&devices.principal_id, None)
2216 .await
2217 .map_err(|error| error.to_string())
2218}
2219
2220#[tauri::command]
2221async fn openrtc_publish_native_devices<R: Runtime>(
2222 webview: Webview<R>,
2223 state: tauri::State<'_, OpenRtcTauriState>,
2224 identity_id: String,
2225 capability_key: String,
2226 device_name: String,
2227 metadata: Option<String>,
2228 excluded_peers: Vec<String>,
2229 auto_connect: bool,
2230) -> Result<(), NativeCommandError> {
2231 let _start = state.managed_session_start_guard.lock().await;
2232 let key = required_native_capability_part(capability_key, "capability key")?;
2233 let (devices, replay_existing) = {
2234 let current = state.native_identity.lock().await;
2235 let owner = current
2236 .as_ref()
2237 .filter(|owner| owner.id == identity_id)
2238 .ok_or("native identity is no longer current")?;
2239 if owner.webview_label != webview.label() {
2240 return Err("native identity is owned by another webview".into());
2241 }
2242 let replay_existing = native_roster_is_active_for_capability(owner.roster.as_ref(), &key)?;
2243 let devices = owner
2244 .devices
2245 .clone()
2246 .ok_or("native devices have not started")?;
2247 *state
2248 .token_relay
2249 .native_credential
2250 .write()
2251 .map_err(|_| "native credential lock poisoned")? =
2252 Some((identity_id.clone(), devices.identity_credential_provider()));
2253 (devices, replay_existing)
2254 };
2255 if replay_existing {
2256 let snapshot = devices
2261 .signaling()
2262 .list_devices(&devices.principal_id, None)
2263 .await
2264 .map_err(|error| error.to_string())?;
2265 send_native_projection(
2266 &state.native_projection_subscription,
2267 "devices",
2268 vec![key],
2269 &serde_json::json!({ "identityId": identity_id, "devices": snapshot }),
2270 )
2271 .await;
2272 return Ok(());
2273 }
2274 let excluded_peers = normalize_initial_auto_connect_exclusions(excluded_peers)?;
2275 let client = state.client();
2276 for device_id in &excluded_peers {
2282 client.set_auto_connect_excluded(device_id, true);
2283 }
2284 let node_id = ensure_iroh_node(client.clone()).await?;
2285 let local_device_id = client
2286 .get_native_device_identity()
2287 .await
2288 .map_err(|error| error.to_string())?
2289 .device_id;
2290 {
2291 let mut current = state.native_identity.lock().await;
2292 let owner = current
2293 .as_mut()
2294 .filter(|owner| owner.id == identity_id)
2295 .ok_or("native identity changed before presence startup")?;
2296 let mut registry = state.capability_registry.lock().await;
2297 let registration = registry
2298 .registrations
2299 .get_mut(&key)
2300 .ok_or("native capability is not registered")?;
2301 if !registration.owns_native_devices(&devices.principal_id) {
2302 return Err("native devices require their own devices capability registration".into());
2303 }
2304 registration.native_source = Some(NativeRosterSource::Active(identity_id.clone()));
2305 owner.roster = Some(NativeDeviceRoster {
2306 capability_key: key.clone(),
2307 task: None,
2308 });
2309 }
2310 client
2311 .start_external_auto_connect(
2312 format!("{}:native-root", client.app_tag()),
2313 local_device_id.clone(),
2314 )
2315 .await
2316 .map_err(|error| error.to_string())?;
2317 let backend = devices.signaling();
2318 let mut events = backend
2321 .subscribe_devices(&devices.principal_id)
2322 .await
2323 .map_err(|error| error.to_string())?;
2324 let mut service_errors = devices.handle.subscribe_service_errors()
2325 .map_err(|error| error.to_string())?;
2326 backend
2327 .set_excluded_peers(&devices.principal_id, &node_id, &excluded_peers)
2328 .await
2329 .map_err(|error| error.to_string())?;
2330 let ticket = client
2331 .endpoint_ticket_with_token("user-device", 0)
2332 .await
2333 .map_err(|error| error.to_string())?;
2334 backend
2335 .update_presence(
2336 &devices.principal_id,
2337 &node_id,
2338 &ticket,
2339 true,
2340 &device_name,
2341 300_000,
2342 metadata.as_deref(),
2343 )
2344 .await
2345 .map_err(NativeCommandError::from_runtime)?;
2346 let snapshot = backend
2347 .list_devices(&devices.principal_id, None)
2348 .await
2349 .map_err(|error| error.to_string())?;
2350 if !submit_native_device_snapshot(
2351 &client,
2352 &state.capability_registry,
2353 &identity_id,
2354 &key,
2355 &local_device_id,
2356 &snapshot,
2357 auto_connect,
2358 )
2359 .await?
2360 {
2361 return Err("native device roster was superseded".into());
2362 }
2363 let mut current = state.native_identity.lock().await;
2364 let Some(owner) = current.as_mut().filter(|owner| owner.id == identity_id) else {
2365 drop(current);
2366 devices.close().await;
2367 return Err("native identity changed during presence startup".into());
2368 };
2369 send_native_projection(
2370 &state.native_projection_subscription,
2371 "devices",
2372 vec![key.clone()],
2373 &serde_json::json!({ "identityId": identity_id, "devices": snapshot }),
2374 )
2375 .await;
2376 let registry = state.capability_registry.clone();
2377 let projection = state.native_projection_subscription.clone();
2378 let task_key = key.clone();
2379 let task = tokio::spawn(async move {
2380 loop {
2381 let event = tokio::select! {
2382 biased;
2383 observation = service_errors.recv() => {
2386 match observation {
2387 Ok(observation) => {
2388 if devices.is_closed() || !forward_native_service_error(
2389 ®istry, &projection, &identity_id, &task_key, &observation,
2390 ).await { break; }
2391 }
2392 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
2393 }
2395 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
2396 }
2397 continue;
2398 }
2399 event = events.next() => match event { Some(event) => event, None => break },
2400 };
2401 if let Err(error) = event {
2402 eprintln!("[openrtc-tauri] native roster notification failed: {error}");
2403 }
2404 let snapshot = match backend.list_devices(&devices.principal_id, None).await {
2405 Ok(snapshot) => snapshot,
2406 Err(error) => {
2407 eprintln!("[openrtc-tauri] native roster read failed: {error}");
2408 continue;
2409 }
2410 };
2411 match submit_native_device_snapshot(
2412 &client,
2413 ®istry,
2414 &identity_id,
2415 &task_key,
2416 &local_device_id,
2417 &snapshot,
2418 auto_connect,
2419 )
2420 .await
2421 {
2422 Ok(true) => {
2423 send_native_projection(
2424 &projection,
2425 "devices",
2426 vec![task_key.clone()],
2427 &serde_json::json!({ "identityId": identity_id, "devices": snapshot }),
2428 )
2429 .await
2430 }
2431 Ok(false) => break,
2432 Err(error) => {
2433 eprintln!("[openrtc-tauri] native desired-peer input failed: {error}");
2434 break;
2435 }
2436 }
2437 }
2438 });
2439 owner.roster = Some(NativeDeviceRoster {
2440 capability_key: key,
2441 task: Some(task),
2442 });
2443 Ok(())
2444}
2445
2446#[tauri::command]
2447async fn openrtc_start_native_devices<R: Runtime>(
2448 app: tauri::AppHandle<R>,
2449 webview: Webview<R>,
2450 state: tauri::State<'_, OpenRtcTauriState>,
2451 identity_id: String,
2452 device_name: Option<String>,
2453) -> Result<NativeDevicesIdentity, NativeCommandError> {
2454 let _start = state.managed_session_start_guard.lock().await;
2455 let device = state
2456 .client()
2457 .init_native_device_identity(app_data_dir(&app, &state)?, device_name.as_deref())
2458 .await
2459 .map_err(|error| error.to_string())?;
2460 let control_plane = {
2461 let current = state.native_identity.lock().await;
2462 let owner = current
2463 .as_ref()
2464 .filter(|owner| owner.id == identity_id)
2465 .ok_or("native identity is no longer current")?;
2466 if owner.webview_label != webview.label() {
2467 return Err("native identity is owned by another webview".into());
2468 }
2469 if let Some(devices) = &owner.devices {
2470 if !devices.is_closed() {
2471 return Ok(NativeDevicesIdentity {
2472 principal_id: devices.principal_id.clone(),
2473 device_id: device.device_id,
2474 });
2475 }
2476 }
2477 owner.control_plane.clone()
2478 };
2479 let devices = Arc::new(
2482 control_plane
2483 .devices(
2484 device.device_id.clone(),
2485 std::env::consts::OS,
2486 openrtc::native::DeviceOptions {
2487 device_profile: Some(openrtc::native::DeviceProfile {
2488 name: device_name,
2489 platform: Some(std::env::consts::OS.into()),
2490 }),
2491 ..Default::default()
2492 },
2493 )
2494 .await
2495 .map_err(NativeCommandError::from_runtime)?,
2496 );
2497 let mut current = state.native_identity.lock().await;
2498 let Some(owner) = current.as_mut().filter(|owner| owner.id == identity_id) else {
2499 drop(current);
2500 devices.close().await;
2501 return Err("native identity changed during device enrollment".into());
2502 };
2503 let result = NativeDevicesIdentity {
2504 principal_id: devices.principal_id.clone(),
2505 device_id: device.device_id,
2506 };
2507 owner.devices = Some(devices);
2508 Ok(result)
2509}
2510
2511fn required_app_tag(value: &str) -> Result<&str, String> {
2512 let value = value.trim();
2513 if value.len() < 5
2514 || value.len() > 80
2515 || !value.bytes().all(|byte| {
2516 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':' | b'@')
2517 })
2518 {
2519 return Err("OpenRTC app tag is invalid".to_string());
2520 }
2521 Ok(value)
2522}
2523
2524fn validate_public_device_jwk(value: &serde_json::Value) -> Result<(), String> {
2525 let object = value
2526 .as_object()
2527 .ok_or_else(|| "OpenRTC device public key is invalid".to_string())?;
2528 let x = object
2529 .get("x")
2530 .and_then(serde_json::Value::as_str)
2531 .unwrap_or_default();
2532 if object.get("kty").and_then(serde_json::Value::as_str) != Some("OKP")
2533 || object.get("crv").and_then(serde_json::Value::as_str) != Some("Ed25519")
2534 || object.contains_key("d")
2535 || x.len() != 43
2536 || !x
2537 .bytes()
2538 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
2539 {
2540 return Err("OpenRTC device public key must be a public Ed25519 JWK".to_string());
2541 }
2542 Ok(())
2543}
2544
2545fn required_secure_record_key(value: &str) -> Result<&str, String> {
2546 let value = value.trim();
2547 if value.is_empty()
2548 || value.len() > 512
2549 || !value
2550 .bytes()
2551 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':'))
2552 {
2553 return Err("OpenRTC secure record key is invalid".to_string());
2554 }
2555 Ok(value)
2556}
2557
2558#[tauri::command]
2559async fn openrtc_device_public_key(
2560 state: tauri::State<'_, OpenRtcTauriState>,
2561 app_tag: String,
2562) -> Result<serde_json::Value, String> {
2563 state.device_public_key(&app_tag)
2564}
2565
2566#[tauri::command]
2567async fn openrtc_sign_device_proof(
2568 state: tauri::State<'_, OpenRtcTauriState>,
2569 app_tag: String,
2570 challenge: String,
2571) -> Result<String, String> {
2572 state.sign_device_proof(&app_tag, &challenge)
2573}
2574
2575#[tauri::command]
2576async fn openrtc_delete_device_key(
2577 state: tauri::State<'_, OpenRtcTauriState>,
2578 app_tag: String,
2579) -> Result<(), String> {
2580 state.delete_device_key(&app_tag)
2581}
2582
2583#[tauri::command]
2584async fn openrtc_read_secure_record(
2585 state: tauri::State<'_, OpenRtcTauriState>,
2586 app_tag: String,
2587 key: String,
2588) -> Result<Option<String>, String> {
2589 state.read_secure_record(&app_tag, &key)
2590}
2591
2592#[tauri::command]
2593async fn openrtc_write_secure_record(
2594 state: tauri::State<'_, OpenRtcTauriState>,
2595 app_tag: String,
2596 key: String,
2597 value: String,
2598) -> Result<(), String> {
2599 state.write_secure_record(&app_tag, &key, &value)
2600}
2601
2602#[tauri::command]
2603async fn openrtc_delete_secure_record(
2604 state: tauri::State<'_, OpenRtcTauriState>,
2605 app_tag: String,
2606 key: String,
2607) -> Result<(), String> {
2608 state.delete_secure_record(&app_tag, &key)
2609}
2610
2611#[tauri::command]
2612async fn openrtc_offline_runtime_support(
2613 state: tauri::State<'_, OpenRtcTauriState>,
2614) -> Result<OfflineRuntimeSupport, String> {
2615 Ok(state.offline_runtime_support())
2616}
2617
2618#[tauri::command]
2619async fn openrtc_create_offline_enrollment_request(
2620 state: tauri::State<'_, OpenRtcTauriState>,
2621 trust_domain: String,
2622 device_id: String,
2623 endpoint_id: String,
2624 enrollment_nonce: String,
2625 requested_roles: Vec<String>,
2626) -> Result<openrtc::offline::OfflineEnrollmentRequest, String> {
2627 let client = state.client();
2628 let current_endpoint_id = client.current_node_id().await.ok_or_else(|| {
2629 "OpenRTC Iroh endpoint must be started before offline enrollment".to_string()
2630 })?;
2631 if current_endpoint_id != endpoint_id.trim() {
2632 return Err(
2633 "offline enrollment endpoint does not match the native Rust endpoint".to_string(),
2634 );
2635 }
2636 let app_tag = client.app_tag();
2637 let signer = state
2638 .native_device_key_signer
2639 .as_deref()
2640 .ok_or_else(|| "offline enrollment requires the native host device signer".to_string())?;
2641 openrtc::offline::OfflineEnrollmentRequest::create(
2642 &OfflineDeviceSigner { signer, app_tag },
2643 &trust_domain,
2644 &device_id,
2645 ¤t_endpoint_id,
2646 &enrollment_nonce,
2647 requested_roles,
2648 signer.offline_assurance(app_tag),
2649 openrtc::session_token::now_unix_ms(),
2650 )
2651 .map_err(|error| error.to_string())
2652}
2653
2654#[tauri::command]
2655async fn openrtc_verify_offline_enrollment_request(
2656 request: openrtc::offline::OfflineEnrollmentRequest,
2657) -> Result<(), String> {
2658 request
2659 .verify()
2660 .map(|_| ())
2661 .map_err(|error| error.to_string())
2662}
2663
2664#[tauri::command]
2665async fn rtc_native_status(
2666 state: tauri::State<'_, OpenRtcTauriState>,
2667) -> Result<NativeRuntimeStatusResponse, String> {
2668 Ok(state.project_public_runtime_status(state.client().runtime_status().await))
2669}
2670
2671#[tauri::command]
2672async fn get_rtc_local_device_info<R: Runtime>(
2673 app: tauri::AppHandle<R>,
2674 state: tauri::State<'_, OpenRtcTauriState>,
2675) -> Result<openrtc::native_device::NativeDeviceIdentity, String> {
2676 let data_dir = app_data_dir(&app, &state)?;
2677 state
2678 .client()
2679 .init_native_device_identity(data_dir, None)
2680 .await
2681 .map_err(|error| error.to_string())
2682}
2683
2684#[tauri::command]
2685async fn update_rtc_local_device_name<R: Runtime>(
2686 app: tauri::AppHandle<R>,
2687 state: tauri::State<'_, OpenRtcTauriState>,
2688 device_name: String,
2689) -> Result<openrtc::native_device::NativeDeviceIdentity, String> {
2690 let data_dir = app_data_dir(&app, &state)?;
2691 let _ = state
2692 .client()
2693 .init_native_device_identity(data_dir, None)
2694 .await
2695 .map_err(|error| error.to_string())?;
2696 state
2697 .client()
2698 .update_native_device_name(&device_name)
2699 .await
2700 .map_err(|error| error.to_string())
2701}
2702
2703#[tauri::command]
2704async fn get_iroh_node_id(
2705 state: tauri::State<'_, OpenRtcTauriState>,
2706) -> Result<Option<String>, String> {
2707 Ok(state.client().current_node_id().await)
2708}
2709
2710#[tauri::command]
2711async fn start_iroh_node(state: tauri::State<'_, OpenRtcTauriState>) -> Result<String, String> {
2712 ensure_iroh_node(state.client()).await
2713}
2714
2715#[tauri::command]
2716async fn get_iroh_endpoint_ticket(
2717 state: tauri::State<'_, OpenRtcTauriState>,
2718) -> Result<String, String> {
2719 ensure_iroh_node(state.client()).await?;
2720 state
2721 .client()
2722 .endpoint_ticket()
2723 .await
2724 .map_err(|error| error.to_string())
2725}
2726
2727#[tauri::command]
2728async fn register_session_token(
2729 state: tauri::State<'_, OpenRtcTauriState>,
2730 token: String,
2731 scope: String,
2732 max_connections: u32,
2733 expires_at_ms: Option<u64>,
2734) -> Result<(), String> {
2735 if let Some(expires_at_ms) = expires_at_ms {
2736 state
2737 .client()
2738 .register_token_until(token, scope, max_connections, expires_at_ms);
2739 } else {
2740 state
2741 .client()
2742 .register_session_token(token, scope, max_connections);
2743 }
2744 Ok(())
2745}
2746
2747#[tauri::command]
2748async fn get_endpoint_ticket_with_token(
2749 state: tauri::State<'_, OpenRtcTauriState>,
2750 scope: String,
2751 max_connections: u32,
2752) -> Result<String, String> {
2753 ensure_iroh_node(state.client()).await?;
2754 state
2755 .client()
2756 .endpoint_ticket_with_token(&scope, max_connections)
2757 .await
2758 .map_err(|error| error.to_string())
2759}
2760
2761#[tauri::command]
2762async fn issue_ticket_invite(
2763 state: tauri::State<'_, OpenRtcTauriState>,
2764 id: String,
2765 max_peers: u32,
2766 mesh: bool,
2767) -> Result<String, String> {
2768 ensure_iroh_node(state.client()).await?;
2769 let invite = if mesh {
2770 state.client().issue_ticket_invite(
2771 &id, openrtc::ticket_mesh::TicketMeshOptions { max_peers },
2772 ).await
2773 } else {
2774 state.client().issue_direct_ticket_invite(&id, max_peers).await
2775 };
2776 invite
2777 .map(|invite| invite.encode())
2778 .map_err(|error| error.to_string())
2779}
2780
2781#[tauri::command]
2782async fn connect_ticket_invite(
2783 state: tauri::State<'_, OpenRtcTauriState>,
2784 invitation: String,
2785) -> Result<openrtc::client::ManagedConnectResult, String> {
2786 ensure_iroh_node(state.client()).await?;
2787 state
2788 .client()
2789 .connect_ticket_invite(&invitation)
2790 .await
2791 .map_err(|error| error.to_string())
2792}
2793
2794#[tauri::command]
2795async fn issue_ticket_mesh<R: Runtime>(
2796 app: tauri::AppHandle<R>,
2797 state: tauri::State<'_, OpenRtcTauriState>,
2798 id: String,
2799 max_peers: u32,
2800) -> Result<String, String> {
2801 let client = state.client();
2802 ensure_iroh_node(client.clone()).await?;
2803 let mut meshes = state.ticket_meshes.lock().await;
2804 if meshes.contains_key(&id) {
2805 return Err("ticket mesh already has an owner on this runtime".into());
2806 }
2807 let mesh = openrtc::ticket_mesh::TicketMesh::issue(
2808 client,
2809 &id,
2810 openrtc::ticket_mesh::TicketMeshOptions { max_peers },
2811 )
2812 .await
2813 .map_err(|error| error.to_string())?;
2814 let invite = mesh.invite();
2815 forward_ticket_mesh_invites(
2816 app,
2817 id.clone(),
2818 mesh.watch_invite(),
2819 mesh.watch_peers(),
2820 mesh.projection_generation(),
2821 );
2822 meshes.insert(id, Arc::new(mesh));
2823 Ok(invite)
2824}
2825
2826#[tauri::command]
2827async fn join_ticket_mesh<R: Runtime>(
2828 app: tauri::AppHandle<R>,
2829 state: tauri::State<'_, OpenRtcTauriState>,
2830 invitation: String,
2831) -> Result<String, String> {
2832 let invite = openrtc::ticket_mesh::TicketInvite::parse(&invitation)
2833 .map_err(|error| error.to_string())?;
2834 let id = invite.id().to_string();
2835 let client = state.client();
2836 ensure_iroh_node(client.clone()).await?;
2837 let mut meshes = state.ticket_meshes.lock().await;
2838 if meshes.contains_key(&id) {
2839 return Err("ticket mesh already has an owner on this runtime".into());
2840 }
2841 let mesh = openrtc::ticket_mesh::TicketMesh::join(client, &invitation)
2842 .await
2843 .map_err(|error| error.to_string())?;
2844 forward_ticket_mesh_invites(
2845 app,
2846 id.clone(),
2847 mesh.watch_invite(),
2848 mesh.watch_peers(),
2849 mesh.projection_generation(),
2850 );
2851 meshes.insert(id.clone(), Arc::new(mesh));
2852 Ok(id)
2853}
2854
2855#[derive(Clone, Serialize)]
2856#[serde(rename_all = "camelCase")]
2857struct TicketMeshInviteEvent {
2858 id: String,
2859 invitation: String,
2860}
2861
2862fn forward_ticket_mesh_invites<R: Runtime>(
2863 app: tauri::AppHandle<R>,
2864 id: String,
2865 mut invites: tokio::sync::watch::Receiver<String>,
2866 mut peers: tokio::sync::watch::Receiver<Vec<openrtc::client::PeerSessionSnapshot>>,
2867 generation: u64,
2868) {
2869 tauri::async_runtime::spawn(async move {
2870 loop {
2871 tokio::select! {
2872 changed = invites.changed() => {
2873 if changed.is_err() { break; }
2874 let invitation = invites.borrow_and_update().clone();
2875 let _ = app.emit("openrtc://ticket-mesh-invite", TicketMeshInviteEvent { id: id.clone(), invitation });
2876 }
2877 changed = peers.changed() => {
2878 if changed.is_err() { break; }
2879 let ready = peers.borrow_and_update().clone();
2882 let _ = app.emit("openrtc://ticket-mesh-peers", serde_json::json!({ "id": id, "generation": generation, "peers": ready }));
2883 }
2884 }
2885 }
2886 });
2887}
2888
2889#[tauri::command]
2890async fn ticket_mesh_peers(
2891 state: tauri::State<'_, OpenRtcTauriState>,
2892 id: String,
2893) -> Result<serde_json::Value, String> {
2894 let mesh = state
2895 .ticket_meshes
2896 .lock()
2897 .await
2898 .get(&id)
2899 .cloned()
2900 .ok_or_else(|| "ticket mesh is not active".to_string())?;
2901 let peers = mesh.watch_peers().borrow().clone();
2902 Ok(serde_json::json!({ "generation": mesh.projection_generation(), "peers": peers }))
2903}
2904
2905#[tauri::command]
2906async fn ticket_mesh_issuer_node(
2907 state: tauri::State<'_, OpenRtcTauriState>,
2908 id: String,
2909) -> Result<String, String> {
2910 let mesh = state.ticket_meshes.lock().await.get(&id).cloned()
2911 .ok_or_else(|| "ticket mesh is not active".to_string())?;
2912 mesh.issuer_node().map_err(|error| error.to_string())
2913}
2914
2915#[tauri::command]
2916async fn close_ticket_mesh(
2917 state: tauri::State<'_, OpenRtcTauriState>,
2918 id: String,
2919) -> Result<(), String> {
2920 let mesh = state.ticket_meshes.lock().await.get(&id).cloned();
2921 if let Some(mesh) = mesh {
2922 mesh.close().await;
2923 let mut meshes = state.ticket_meshes.lock().await;
2924 if meshes
2925 .get(&id)
2926 .is_some_and(|current| Arc::ptr_eq(current, &mesh))
2927 {
2928 meshes.remove(&id);
2929 }
2930 }
2931 Ok(())
2932}
2933
2934#[tauri::command]
2935async fn validate_session_token(
2936 state: tauri::State<'_, OpenRtcTauriState>,
2937 token: String,
2938 connection_id: Option<String>,
2939) -> Result<String, String> {
2940 if let Some(connection_id) = connection_id
2941 .as_deref()
2942 .map(str::trim)
2943 .filter(|value| !value.is_empty())
2944 {
2945 state
2946 .client()
2947 .validate_connection_token(&token, connection_id, None)
2948 .await
2949 .map_err(|error| error.to_string())
2950 } else {
2951 state.client().validate_session_token(&token)
2952 }
2953}
2954
2955#[tauri::command]
2956async fn revoke_session_tokens_by_scope(
2957 state: tauri::State<'_, OpenRtcTauriState>,
2958 scope: String,
2959) -> Result<Vec<String>, String> {
2960 let affected = state.client().revoke_tokens_by_scope(&scope).await;
2961 if revokes_managed_session(&scope) {
2962 if let Some(previous) = state.managed_session.lock().await.take() {
2969 previous.owner_client.stop_presence_loop();
2970 previous.owner_client.stop_auto_connect();
2971 previous.owner_client.stop_external_auto_connect().await;
2972 }
2973 *state.presence_loop_active.lock().await = false;
2974 }
2975 Ok(affected)
2976}
2977
2978#[tauri::command]
2979async fn stop_rtc_presence_loop(state: tauri::State<'_, OpenRtcTauriState>) -> Result<(), String> {
2980 state.client().stop_presence_loop();
2981 *state.presence_loop_active.lock().await = false;
2982 if let Some(active) = state.managed_session.lock().await.as_mut() {
2983 active.result.presence_started = false;
2984 }
2985 stop_subscription_by_id(&state, "internal-presence-loop").await;
2986 Ok(())
2987}
2988
2989#[tauri::command]
2995async fn register_rtc_capability(
2996 state: tauri::State<'_, OpenRtcTauriState>,
2997 capability_key: String,
2998 avenue_kind: String,
2999 avenue_id: String,
3000 sparse_fanout: Option<bool>,
3001 architecture: Option<openrtc::native::RoomArchitectureMode>,
3002) -> Result<String, String> {
3003 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3004 let avenue_kind = required_native_capability_part(avenue_kind, "avenue kind")?;
3005 let avenue_id = required_native_capability_part(avenue_id, "avenue id")?;
3006 if capability_key != format!("{avenue_kind}:{avenue_id}") {
3007 return Err("native capability key does not match its avenue".to_string());
3008 }
3009 let mut registry = state.capability_registry.lock().await;
3010 registry.register_with_architecture(
3011 capability_key.clone(),
3012 avenue_kind,
3013 avenue_id,
3014 sparse_fanout.unwrap_or(false),
3015 architecture,
3016 )?;
3017 Ok(registry.registrations[&capability_key]
3018 .transfer_namespace
3019 .clone())
3020}
3021
3022#[cfg(feature = "managed-group-encryption")]
3023fn managed_group_state_path<R: Runtime>(
3024 app: &tauri::AppHandle<R>,
3025 state: &OpenRtcTauriState,
3026 capability_key: &str,
3027 device_id: &str,
3028) -> Result<PathBuf, String> {
3029 let mut digest = Sha256::new();
3030 digest.update(b"openrtc-tauri-managed-group-state-v1:");
3031 digest.update(state.client().app_tag().as_bytes());
3032 digest.update(device_id.as_bytes());
3033 digest.update(capability_key.as_bytes());
3034 let name = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest.finalize());
3035 Ok(app_data_dir(app, state)?
3036 .join("managed-room-state")
3037 .join(format!("{name}.bin")))
3038}
3039
3040#[cfg(feature = "managed-group-encryption")]
3041fn write_private_managed_group_state(path: &std::path::Path, bytes: &[u8]) -> Result<(), String> {
3042 if bytes.is_empty() || bytes.len() > 16 * 1024 * 1024 {
3043 return Err("managed room state exceeds its protected bound".to_string());
3044 }
3045 let parent = path
3046 .parent()
3047 .ok_or_else(|| "managed room state path has no parent".to_string())?;
3048 std::fs::create_dir_all(parent)
3049 .map_err(|error| format!("create managed room state directory: {error}"))?;
3050 #[cfg(unix)]
3051 {
3052 use std::os::unix::fs::PermissionsExt;
3053 std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))
3054 .map_err(|error| format!("protect managed room state directory: {error}"))?;
3055 }
3056 let temporary = parent.join(format!(
3057 ".{}.{}.tmp",
3058 path.file_name()
3059 .and_then(|name| name.to_str())
3060 .unwrap_or("state"),
3061 uuid::Uuid::new_v4().simple(),
3062 ));
3063 std::fs::write(&temporary, bytes)
3064 .map_err(|error| format!("write managed room state: {error}"))?;
3065 #[cfg(unix)]
3066 {
3067 use std::os::unix::fs::PermissionsExt;
3068 std::fs::set_permissions(&temporary, std::fs::Permissions::from_mode(0o600))
3069 .map_err(|error| format!("protect managed room state: {error}"))?;
3070 }
3071 std::fs::rename(&temporary, path)
3072 .map_err(|error| format!("replace managed room state: {error}"))?;
3073 Ok(())
3074}
3075
3076#[cfg(feature = "managed-group-encryption")]
3077fn persist_managed_group_actions<R: Runtime>(
3078 app: &tauri::AppHandle<R>,
3079 state: &OpenRtcTauriState,
3080 capability_key: &str,
3081 device_id: &str,
3082 actions: Vec<serde_json::Value>,
3083) -> Result<Vec<serde_json::Value>, String> {
3084 let mut outbound = Vec::with_capacity(actions.len());
3085 for action in actions {
3086 if action.get("type").and_then(serde_json::Value::as_str) == Some("persist-state") {
3087 let sealed = action
3088 .get("sealedState")
3089 .and_then(serde_json::Value::as_str)
3090 .ok_or_else(|| "managed room persisted action is invalid".to_string())?;
3091 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
3092 .decode(sealed)
3093 .map_err(|_| "managed room persisted action encoding is invalid".to_string())?;
3094 write_private_managed_group_state(
3095 &managed_group_state_path(app, state, capability_key, device_id)?,
3096 &bytes,
3097 )?;
3098 } else {
3099 outbound.push(action);
3100 }
3101 }
3102 Ok(outbound)
3103}
3104
3105#[cfg(feature = "managed-group-encryption")]
3106fn derive_tauri_managed_group_key(
3107 state: &OpenRtcTauriState,
3108 capability_key: &str,
3109 device_id: &str,
3110) -> Result<[u8; 32], String> {
3111 let client = state.client();
3112 let app_tag = client.app_tag();
3113 let challenge =
3114 format!("openrtc:managed-group-wrapping-key:v1:{app_tag}:{device_id}:{capability_key}",);
3115 let signer = state.native_device_key_signer.as_ref().ok_or_else(|| {
3116 "managed rooms require the host's sign-only native device key".to_string()
3117 })?;
3118 let mut signature = signer.sign(app_tag, challenge.as_bytes())?;
3119 if signature.len() != 64 {
3120 return Err("native device signer returned an invalid managed-room signature".to_string());
3121 }
3122 let mut digest = Sha256::new();
3123 digest.update(b"openrtc:managed-group-key-derivation:v1:");
3124 digest.update(challenge.as_bytes());
3125 digest.update(&signature);
3126 signature.fill(0);
3127 Ok(digest.finalize().into())
3128}
3129
3130#[cfg(feature = "managed-group-encryption")]
3131#[tauri::command]
3132async fn openrtc_init_managed_room_group<R: Runtime>(
3133 app: tauri::AppHandle<R>,
3134 state: tauri::State<'_, OpenRtcTauriState>,
3135 capability_key: String,
3136 device_id: String,
3137) -> Result<serde_json::Value, String> {
3138 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3139 let device_id = required_native_capability_part(device_id, "device id")?;
3140 {
3141 let registry = state.capability_registry.lock().await;
3142 let registration = registry
3143 .registrations
3144 .get(&capability_key)
3145 .ok_or_else(|| "managed room capability is not registered".to_string())?;
3146 if registration.avenue_kind != "room" {
3147 return Err("managed fanout is available only for room capabilities".to_string());
3148 }
3149 }
3150 let path = managed_group_state_path(&app, &state, &capability_key, &device_id)?;
3151 let sealed = match std::fs::read(path) {
3152 Ok(bytes) if bytes.len() <= 16 * 1024 * 1024 => Some(bytes),
3153 Ok(_) => return Err("managed room persisted state exceeds its protected bound".to_string()),
3154 Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
3155 Err(error) => return Err(format!("read managed room persisted state: {error}")),
3156 };
3157 let controller = openrtc::native::NativeManagedGroupController::new(
3158 &device_id,
3159 derive_tauri_managed_group_key(&state, &capability_key, &device_id)?,
3160 sealed.as_deref(),
3161 )
3162 .map_err(|error| format!("initialize managed room group: {error:#}"))?;
3163 let action = controller
3164 .publish_key_package()
3165 .map_err(|error| format!("create managed room KeyPackage: {error:#}"))?;
3166 state
3167 .managed_groups
3168 .lock()
3169 .await
3170 .insert(capability_key, controller);
3171 Ok(action)
3172}
3173
3174#[cfg(feature = "managed-group-encryption")]
3175#[tauri::command]
3176async fn openrtc_handle_managed_room_prepare_page<R: Runtime>(
3177 app: tauri::AppHandle<R>,
3178 state: tauri::State<'_, OpenRtcTauriState>,
3179 capability_key: String,
3180 device_id: String,
3181 page: serde_json::Value,
3182) -> Result<Vec<serde_json::Value>, String> {
3183 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3184 let device_id = required_native_capability_part(device_id, "device id")?;
3185 let actions = state
3186 .managed_groups
3187 .lock()
3188 .await
3189 .get_mut(&capability_key)
3190 .ok_or_else(|| "managed room group is not initialized".to_string())?
3191 .handle_prepare_page(page)
3192 .map_err(|error| format!("handle managed room preparation: {error:#}"))?;
3193 persist_managed_group_actions(&app, &state, &capability_key, &device_id, actions)
3194}
3195
3196#[cfg(feature = "managed-group-encryption")]
3197#[tauri::command]
3198async fn openrtc_handle_managed_room_artifact_chunk<R: Runtime>(
3199 app: tauri::AppHandle<R>,
3200 state: tauri::State<'_, OpenRtcTauriState>,
3201 capability_key: String,
3202 device_id: String,
3203 chunk: serde_json::Value,
3204) -> Result<Vec<serde_json::Value>, String> {
3205 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3206 let device_id = required_native_capability_part(device_id, "device id")?;
3207 let actions = state
3208 .managed_groups
3209 .lock()
3210 .await
3211 .get_mut(&capability_key)
3212 .ok_or_else(|| "managed room group is not initialized".to_string())?
3213 .handle_artifact_chunk(chunk)
3214 .map_err(|error| format!("handle managed room artifact: {error:#}"))?;
3215 persist_managed_group_actions(&app, &state, &capability_key, &device_id, actions)
3216}
3217
3218#[cfg(feature = "managed-group-encryption")]
3219#[tauri::command]
3220#[allow(clippy::too_many_arguments)]
3221async fn openrtc_seal_managed_room_payload<R: Runtime>(
3222 app: tauri::AppHandle<R>,
3223 state: tauri::State<'_, OpenRtcTauriState>,
3224 capability_key: String,
3225 device_id: String,
3226 architecture_epoch: u64,
3227 encryption_epoch: u64,
3228 message_id: String,
3229 channel: String,
3230 priority: u8,
3231 zone_id: Option<String>,
3232 payload: Vec<u8>,
3233) -> Result<openrtc::native::NativeManagedProtectedPayload, String> {
3234 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3235 let device_id = required_native_capability_part(device_id, "device id")?;
3236 let protected = state
3237 .managed_groups
3238 .lock()
3239 .await
3240 .get_mut(&capability_key)
3241 .ok_or_else(|| "managed room group is not initialized".to_string())?
3242 .seal_payload(
3243 architecture_epoch,
3244 encryption_epoch,
3245 &message_id,
3246 &channel,
3247 priority,
3248 zone_id.as_deref(),
3249 &payload,
3250 )
3251 .map_err(|error| format!("seal managed room payload: {error:#}"))?;
3252 let sealed = base64::engine::general_purpose::URL_SAFE_NO_PAD
3253 .decode(&protected.sealed_state)
3254 .map_err(|_| "managed room state encoding is invalid".to_string())?;
3255 write_private_managed_group_state(
3256 &managed_group_state_path(&app, &state, &capability_key, &device_id)?,
3257 &sealed,
3258 )?;
3259 Ok(protected)
3260}
3261
3262#[cfg(feature = "managed-group-encryption")]
3263#[tauri::command]
3264#[allow(clippy::too_many_arguments)]
3265async fn openrtc_open_managed_room_payload<R: Runtime>(
3266 app: tauri::AppHandle<R>,
3267 state: tauri::State<'_, OpenRtcTauriState>,
3268 capability_key: String,
3269 device_id: String,
3270 architecture_epoch: u64,
3271 encryption_epoch: u64,
3272 message_id: String,
3273 channel: String,
3274 priority: u8,
3275 zone_id: Option<String>,
3276 ciphertext: Vec<u8>,
3277) -> Result<openrtc::native::NativeManagedProtectedPayload, String> {
3278 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3279 let device_id = required_native_capability_part(device_id, "device id")?;
3280 let protected = state
3281 .managed_groups
3282 .lock()
3283 .await
3284 .get_mut(&capability_key)
3285 .ok_or_else(|| "managed room group is not initialized".to_string())?
3286 .open_payload(
3287 architecture_epoch,
3288 encryption_epoch,
3289 &message_id,
3290 &channel,
3291 priority,
3292 zone_id.as_deref(),
3293 &ciphertext,
3294 )
3295 .map_err(|error| format!("open managed room payload: {error:#}"))?;
3296 let sealed = base64::engine::general_purpose::URL_SAFE_NO_PAD
3297 .decode(&protected.sealed_state)
3298 .map_err(|_| "managed room state encoding is invalid".to_string())?;
3299 write_private_managed_group_state(
3300 &managed_group_state_path(&app, &state, &capability_key, &device_id)?,
3301 &sealed,
3302 )?;
3303 Ok(protected)
3304}
3305
3306#[cfg(feature = "managed-group-encryption")]
3307#[tauri::command]
3308async fn openrtc_forget_managed_room_group(
3309 state: tauri::State<'_, OpenRtcTauriState>,
3310 capability_key: String,
3311) -> Result<(), String> {
3312 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3313 state.managed_groups.lock().await.remove(&capability_key);
3314 Ok(())
3315}
3316
3317#[cfg(not(feature = "managed-group-encryption"))]
3318macro_rules! unavailable_managed_room_command {
3319 ($name:ident ( $($arg:ident : $type:ty),* ) -> $return:ty) => {
3320 #[tauri::command]
3321 async fn $name($($arg: $type),*) -> Result<$return, String> {
3322 $(let _ = $arg;)*
3323 Err("native managed rooms were not compiled into this Tauri host".to_string())
3324 }
3325 };
3326}
3327
3328#[cfg(not(feature = "managed-group-encryption"))]
3329unavailable_managed_room_command!(openrtc_init_managed_room_group(
3330 capability_key: String, device_id: String
3331) -> serde_json::Value);
3332#[cfg(not(feature = "managed-group-encryption"))]
3333unavailable_managed_room_command!(openrtc_handle_managed_room_prepare_page(
3334 capability_key: String, device_id: String, page: serde_json::Value
3335) -> Vec<serde_json::Value>);
3336#[cfg(not(feature = "managed-group-encryption"))]
3337unavailable_managed_room_command!(openrtc_handle_managed_room_artifact_chunk(
3338 capability_key: String, device_id: String, chunk: serde_json::Value
3339) -> Vec<serde_json::Value>);
3340#[cfg(not(feature = "managed-group-encryption"))]
3341unavailable_managed_room_command!(openrtc_seal_managed_room_payload(
3342 capability_key: String, device_id: String, architecture_epoch: u64,
3343 encryption_epoch: u64, message_id: String, channel: String, priority: u8,
3344 zone_id: Option<String>, payload: Vec<u8>
3345) -> serde_json::Value);
3346#[cfg(not(feature = "managed-group-encryption"))]
3347unavailable_managed_room_command!(openrtc_open_managed_room_payload(
3348 capability_key: String, device_id: String, architecture_epoch: u64,
3349 encryption_epoch: u64, message_id: String, channel: String, priority: u8,
3350 zone_id: Option<String>, ciphertext: Vec<u8>
3351) -> serde_json::Value);
3352#[cfg(not(feature = "managed-group-encryption"))]
3353unavailable_managed_room_command!(openrtc_forget_managed_room_group(
3354 capability_key: String
3355) -> ());
3356
3357impl OpenRtcTauriState {
3358 async fn unregister_native_capability(&self, capability_key: String) -> Result<(), String> {
3359 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3360 let native_devices = {
3361 let mut current = self.native_identity.lock().await;
3362 match current.as_mut().filter(|owner| {
3363 owner
3364 .roster
3365 .as_ref()
3366 .is_some_and(|roster| roster.capability_key == capability_key)
3367 }) {
3368 Some(owner) => {
3369 drop(owner.roster.take());
3370 owner
3371 .devices
3372 .take()
3373 .map(|devices| (owner.id.clone(), devices))
3374 }
3375 None => None,
3376 }
3377 };
3378 let (empty, aggregate, demands) = {
3379 let mut registry = self.capability_registry.lock().await;
3380 let demands = registry
3381 .registrations
3382 .remove(&capability_key)
3383 .map(|registration| registration.transfer_demands.into_values().collect())
3384 .unwrap_or_default();
3385 let empty = registry.registrations.is_empty();
3386 let aggregate = (!empty)
3387 .then(|| registry.aggregate_desired_peers())
3388 .transpose()?;
3389 (empty, aggregate, demands)
3390 };
3391 self.release_native_transfer_peer_demands(demands).await;
3392 if let Some((identity_id, devices)) = native_devices {
3393 {
3394 let mut native = self
3395 .token_relay
3396 .native_credential
3397 .write()
3398 .map_err(|_| "native credential lock poisoned")?;
3399 if native.as_ref().is_some_and(|(id, _)| id == &identity_id) {
3400 *native = None;
3401 }
3402 }
3403 devices.close().await;
3404 }
3405 if empty {
3406 self.client().stop_external_auto_connect().await;
3407 } else if let Some((revision, peers_json)) = aggregate {
3408 self.client()
3409 .withdraw_external_desired_peers(revision, &peers_json)
3410 .await
3411 .map_err(|error| error.to_string())?;
3412 }
3413 Ok(())
3414 }
3415}
3416
3417#[tauri::command]
3418async fn unregister_rtc_capability(
3419 state: tauri::State<'_, OpenRtcTauriState>,
3420 capability_key: String,
3421) -> Result<(), String> {
3422 state.unregister_native_capability(capability_key).await
3423}
3424
3425#[tauri::command]
3426async fn start_rtc_managed_session<R: Runtime>(
3427 app: tauri::AppHandle<R>,
3428 state: tauri::State<'_, OpenRtcTauriState>,
3429 user_id: String,
3430 device_name: Option<String>,
3431 local_device_id: Option<String>,
3432 metadata: Option<String>,
3433 transports: Option<openrtc::client::TransportConfig>,
3434 auto_connect: Option<bool>,
3435 presence: Option<bool>,
3436) -> Result<StartSessionResult, String> {
3437 let command_started = std::time::Instant::now();
3438 let _start_guard = state.managed_session_start_guard.lock().await;
3439 let client = state.client();
3440 let app_tag = client.app_tag().to_string();
3441 eprintln!(
3442 "[openrtc-tauri][managed-session] start user_id={} app_tag={} has_device_name={} has_local_device_id={} has_metadata={} auto_connect={} presence={}",
3443 user_id,
3444 app_tag,
3445 device_name
3446 .as_deref()
3447 .map(str::trim)
3448 .map(|value| !value.is_empty())
3449 .unwrap_or(false),
3450 local_device_id
3451 .as_deref()
3452 .map(str::trim)
3453 .map(|value| !value.is_empty())
3454 .unwrap_or(false),
3455 metadata
3456 .as_deref()
3457 .map(str::trim)
3458 .map(|value| !value.is_empty())
3459 .unwrap_or(false),
3460 auto_connect.unwrap_or(false),
3461 presence.unwrap_or(false)
3462 );
3463 let data_dir = app_data_dir(&app, &state)?;
3464 let install_context = InstallContext {
3465 data_dir: data_dir.clone(),
3466 };
3467 ensure_requested_native_transports(&state, &client, transports.as_ref(), &install_context)
3468 .await?;
3469 if let Some(transport_config) = transports {
3470 client
3471 .update_transport_config(transport_config)
3472 .await
3473 .map_err(|error| error.to_string())?;
3474 }
3475
3476 let local_node_id = ensure_iroh_node(client.clone()).await?;
3477 let local_device = client
3478 .init_native_device_identity(data_dir, device_name.as_deref())
3479 .await
3480 .map_err(|error| error.to_string())?;
3481
3482 let effective_local_device_id =
3483 managed_session_device_id(local_device_id.as_deref(), &local_device);
3484 if let Some(requested) = requested_local_device_id(local_device_id.as_deref()) {
3485 if requested != local_device.device_id {
3486 eprintln!(
3487 "[openrtc-tauri] requested localDeviceId={} is an alias; persisted native identity {} remains authoritative",
3488 requested, local_device.device_id
3489 );
3490 }
3491 }
3492
3493 let should_start_presence = presence.unwrap_or(false);
3494 let should_start_auto_connect = auto_connect.unwrap_or(false);
3495 if should_start_presence || should_start_auto_connect {
3496 return Err(
3497 "native Firebase coordination has been removed; publish gateway presence and submit external desired peers instead"
3498 .to_string(),
3499 );
3500 }
3501 let managed_session_key = format!("{}:{}", app_tag, effective_local_device_id);
3505 let (disposition, reused) = {
3506 let active = state.managed_session.lock().await;
3507 let disposition = managed_session_disposition(
3508 active.as_ref().map(|record| record.key.as_str()),
3509 active
3510 .as_ref()
3511 .is_some_and(|record| Arc::ptr_eq(&record.owner_client, &client)),
3512 active
3513 .as_ref()
3514 .is_some_and(|record| record.result.presence_started),
3515 active
3516 .as_ref()
3517 .is_some_and(|record| record.result.auto_connect_started),
3518 &managed_session_key,
3519 should_start_presence,
3520 should_start_auto_connect,
3521 );
3522 let reused = (disposition == SessionDisposition::Reuse).then(|| {
3523 active
3524 .as_ref()
3525 .expect("managed session reuse requires an active record")
3526 .result
3527 .clone()
3528 });
3529 (disposition, reused)
3530 };
3531 if let Some(reused) = reused {
3532 eprintln!(
3533 "[openrtc-tauri][managed-session] reused key={} elapsed_ms={}",
3534 managed_session_key,
3535 command_started.elapsed().as_millis()
3536 );
3537 return Ok(reused);
3538 }
3539
3540 if disposition == SessionDisposition::Replace {
3543 let previous = state
3544 .managed_session
3545 .lock()
3546 .await
3547 .take()
3548 .expect("managed session replacement requires an active record");
3549 previous.owner_client.stop_presence_loop();
3550 previous.owner_client.stop_auto_connect();
3551 previous.owner_client.stop_external_auto_connect().await;
3552 *state.presence_loop_active.lock().await = false;
3553 }
3554 let ticket_scope = Some("user-device".to_string());
3555 let ticket = client
3556 .endpoint_ticket_with_token("user-device", 0)
3557 .await
3558 .map_err(|error| error.to_string())?;
3559
3560 let logical_local_device = local_device.clone();
3561
3562 let owner_epoch = match disposition {
3563 SessionDisposition::Refresh => {
3564 state
3565 .managed_session
3566 .lock()
3567 .await
3568 .as_ref()
3569 .expect("managed session refresh requires an active record")
3570 .owner_epoch
3571 }
3572 SessionDisposition::Start | SessionDisposition::Replace => {
3573 state.allocate_managed_session_owner_epoch()
3574 }
3575 SessionDisposition::Reuse => unreachable!("reuse returns before session startup"),
3576 };
3577
3578 eprintln!(
3579 "[openrtc-tauri][managed-session] ready user_id={} app_tag={} local_node_id={} local_device_id={} owner_epoch={} presence_started={} auto_connect_started={} ticket_scope={} elapsed_ms={}",
3580 user_id,
3581 app_tag,
3582 local_node_id,
3583 logical_local_device.device_id,
3584 owner_epoch,
3585 should_start_presence,
3586 should_start_auto_connect,
3587 ticket_scope.as_deref().unwrap_or("unrestricted"),
3588 command_started.elapsed().as_millis()
3589 );
3590
3591 let result = StartSessionResult {
3592 local_node_id,
3593 ticket_scope,
3594 ticket: Some(ticket),
3595 presence_started: false,
3596 auto_connect_started: false,
3597 local_device: logical_local_device,
3598 };
3599 *state.managed_session.lock().await = Some(ManagedSessionRecord {
3600 key: managed_session_key,
3601 owner_client: client,
3602 owner_epoch,
3603 result: result.clone(),
3604 });
3605 Ok(result)
3606}
3607
3608#[tauri::command]
3609async fn start_rtc_external_auto_connect(
3610 state: tauri::State<'_, OpenRtcTauriState>,
3611 user_id: String,
3612 local_device_id: String,
3613 capability_key: Option<String>,
3614) -> Result<(), String> {
3615 if let Some(capability_key) = capability_key {
3616 let capability_key = required_native_capability_part(capability_key, "capability key")?;
3617 if !state
3618 .capability_registry
3619 .lock()
3620 .await
3621 .registrations
3622 .contains_key(&capability_key)
3623 {
3624 return Err(format!(
3625 "native capability {capability_key} is not registered"
3626 ));
3627 }
3628 }
3629 let root_user_id = format!("{}:native-root", state.client().app_tag());
3632 let _requested_capability_principal = user_id;
3633 state
3634 .client()
3635 .start_external_auto_connect(root_user_id, local_device_id)
3636 .await
3637 .map_err(|error| error.to_string())
3638}
3639
3640#[tauri::command]
3641async fn request_rtc_transfer_peer(
3642 state: tauri::State<'_, OpenRtcTauriState>,
3643 capability_key: String,
3644 capability_token: String,
3645 request_json: String,
3646) -> Result<openrtc::client::TransferPeerDemandStatus, String> {
3647 if request_json.len() > 2048 {
3648 return Err("transfer peer demand exceeds 2048 bytes".into());
3649 }
3650 let request = serde_json::from_str(&request_json)
3651 .map_err(|error| format!("invalid transfer peer demand: {error}"))?;
3652 state
3653 .request_native_transfer_peer(&capability_key, &capability_token, request)
3654 .await
3655}
3656
3657#[tauri::command]
3658async fn get_rtc_transfer_peer_demand_status(
3659 state: tauri::State<'_, OpenRtcTauriState>,
3660 capability_key: String,
3661 capability_token: String,
3662 id: String,
3663 generation: u64,
3664) -> Result<Option<openrtc::client::TransferPeerDemandStatus>, String> {
3665 state
3666 .native_transfer_peer_demand_status(&capability_key, &capability_token, &id, generation)
3667 .await
3668}
3669
3670#[tauri::command]
3671async fn release_rtc_transfer_peer(
3672 state: tauri::State<'_, OpenRtcTauriState>,
3673 capability_key: String,
3674 capability_token: String,
3675 id: String,
3676 generation: u64,
3677) -> Result<bool, String> {
3678 state
3679 .release_native_transfer_peer(&capability_key, &capability_token, &id, generation)
3680 .await
3681}
3682
3683#[tauri::command]
3684async fn set_rtc_transfer_peer_budget(
3685 state: tauri::State<'_, OpenRtcTauriState>,
3686 capability_key: String,
3687 capability_token: String,
3688 budget: usize,
3689 generation: u64,
3690) -> Result<(), String> {
3691 state
3692 .set_native_transfer_peer_budget(&capability_key, &capability_token, budget, generation)
3693 .await
3694}
3695
3696#[tauri::command]
3697async fn submit_rtc_desired_peers(
3698 state: tauri::State<'_, OpenRtcTauriState>,
3699 revision: u64,
3700 peers_json: String,
3701 capability_key: Option<String>,
3702 local_device_id: Option<String>,
3703 app_tag: Option<String>,
3704) -> Result<bool, String> {
3705 let original_peers = serde_json::from_str::<Vec<serde_json::Value>>(&peers_json)
3706 .map_err(|error| format!("invalid capability desired-peer payload: {error}"))?;
3707 if original_peers.len() > 100 {
3708 return Err("capability desired-peer payload exceeds 100 peers".to_string());
3709 }
3710 let key = {
3711 let registry = state.capability_registry.lock().await;
3712 match capability_key {
3713 Some(key) => required_native_capability_part(key, "capability key")?,
3714 None if registry.registrations.len() == 1 => registry
3715 .registrations
3716 .keys()
3717 .next()
3718 .cloned()
3719 .expect("one registration has one key"),
3720 None => {
3721 return Err(
3722 "capabilityKey is required when multiple native capabilities are active"
3723 .to_string(),
3724 )
3725 }
3726 }
3727 };
3728 let sparse = state
3729 .capability_registry
3730 .lock()
3731 .await
3732 .registrations
3733 .get(&key)
3734 .ok_or_else(|| format!("native capability {key} is not registered"))?
3735 .uses_sparse_fanout();
3736 let peers = if sparse {
3737 let local_device_id = local_device_id
3738 .as_deref()
3739 .map(str::trim)
3740 .filter(|value| !value.is_empty())
3741 .ok_or_else(|| "sparse native capability requires localDeviceId".to_string())?;
3742 let app_tag = app_tag
3743 .as_deref()
3744 .map(str::trim)
3745 .filter(|value| !value.is_empty())
3746 .ok_or_else(|| "sparse native capability requires appTag".to_string())?;
3747 let local_public_jwk = state.device_public_key(app_tag)?;
3748 let local_device_key_x = local_public_jwk
3749 .get("x")
3750 .and_then(serde_json::Value::as_str)
3751 .ok_or_else(|| "native sparse fanout signer returned no public key".to_string())?;
3752 let projected = state
3753 .client()
3754 .configure_sparse_fanout(
3755 &key,
3756 revision,
3757 local_device_id,
3758 local_device_key_x,
3759 &peers_json,
3760 true,
3761 )
3762 .await
3763 .map_err(|error| error.to_string())?
3764 .0;
3765 serde_json::from_str(&projected)
3766 .map_err(|error| format!("invalid projected desired-peer payload: {error}"))?
3767 } else {
3768 original_peers
3769 };
3770 let (root_revision, aggregate) = {
3771 let mut registry = state.capability_registry.lock().await;
3772 let registration = registry
3773 .registrations
3774 .get_mut(&key)
3775 .ok_or_else(|| format!("native capability {key} is not registered"))?;
3776 if registration.native_source.is_some() {
3777 return Err("desired peers for this capability are owned by the native gateway".into());
3778 }
3779 if revision <= registration.desired_revision {
3780 return Ok(false);
3781 }
3782 registration.desired_revision = revision;
3783 registration.desired_peers = peers;
3784 registry.aggregate_desired_peers()?
3785 };
3786 let accepted = state
3787 .client()
3788 .submit_external_desired_peers(root_revision, &aggregate)
3789 .await
3790 .map_err(|error| error.to_string())?;
3791 if accepted {
3792 replay_current_connection_states(&state).await;
3796 }
3797 Ok(accepted)
3798}
3799
3800#[tauri::command]
3801async fn stop_rtc_auto_connect(state: tauri::State<'_, OpenRtcTauriState>) -> Result<(), String> {
3802 state.client().stop_auto_connect();
3803 state.client().stop_external_auto_connect().await;
3804 if let Some(active) = state.managed_session.lock().await.as_mut() {
3805 active.result.auto_connect_started = false;
3806 }
3807 Ok(())
3808}
3809
3810#[tauri::command]
3811async fn connect_to_device(
3812 state: tauri::State<'_, OpenRtcTauriState>,
3813 device_id: Option<String>,
3814 endpoint_ticket: String,
3815 timeout_ms: Option<u64>,
3816) -> Result<openrtc::client::ManagedConnectResult, String> {
3817 let client = state.client();
3818 let connect = client.connect_device(device_id.as_deref(), &endpoint_ticket);
3819 match timeout_ms {
3820 Some(timeout_ms) => {
3821 tokio::time::timeout(std::time::Duration::from_millis(timeout_ms.max(1)), connect)
3822 .await
3823 .map_err(|_| format!("connect_to_device timeout after {timeout_ms}ms"))?
3824 .map_err(|error| error.to_string())
3825 }
3826 None => connect.await.map_err(|error| error.to_string()),
3827 }
3828}
3829
3830#[tauri::command]
3831async fn connect_to_known_device_with_token(
3832 state: tauri::State<'_, OpenRtcTauriState>,
3833 device_id: String,
3834 token: String,
3835 scope: String,
3836 max_connections: u32,
3837 expires_at_ms: Option<u64>,
3838 timeout_ms: Option<u64>,
3839) -> Result<openrtc::client::ManagedConnectResult, String> {
3840 let client = state.client();
3841 let connect = client.connect_known_device_with_token(
3842 &device_id,
3843 &token,
3844 &scope,
3845 max_connections,
3846 expires_at_ms,
3847 timeout_ms.map(|value| value.min(10_000)),
3848 );
3849 match timeout_ms {
3850 Some(timeout_ms) => {
3851 tokio::time::timeout(std::time::Duration::from_millis(timeout_ms.max(1)), connect)
3852 .await
3853 .map_err(|_| {
3854 format!("connect_to_known_device_with_token timeout after {timeout_ms}ms")
3855 })?
3856 .map_err(|error| error.to_string())
3857 }
3858 None => connect.await.map_err(|error| error.to_string()),
3859 }
3860}
3861
3862#[tauri::command]
3863async fn observe_known_device_endpoint(
3864 state: tauri::State<'_, OpenRtcTauriState>,
3865 device_id: String,
3866 endpoint_ticket: String,
3867) -> Result<(), String> {
3868 state
3869 .client()
3870 .observe_known_device_endpoint(&device_id, &endpoint_ticket)
3871 .await
3872 .map_err(|error| error.to_string())
3873}
3874
3875#[tauri::command]
3876async fn disconnect_device(
3877 state: tauri::State<'_, OpenRtcTauriState>,
3878 device_id: String,
3879 node_id_hint: Option<String>,
3880) -> Result<(), String> {
3881 state
3882 .client()
3883 .disconnect_device(&device_id, node_id_hint.as_deref())
3884 .await;
3885 Ok(())
3886}
3887
3888#[tauri::command]
3889async fn set_auto_connect_excluded(
3890 state: tauri::State<'_, OpenRtcTauriState>,
3891 device_id: String,
3892 excluded: bool,
3893) -> Result<(), String> {
3894 if excluded {
3895 state.client().exclude_peer_and_publish(&device_id).await;
3896 } else {
3897 state.client().unexclude_peer_and_publish(&device_id).await;
3898 }
3899 Ok(())
3900}
3901
3902#[tauri::command]
3903async fn set_rtc_external_auto_connect_excluded(
3904 state: tauri::State<'_, OpenRtcTauriState>,
3905 device_id: String,
3906 excluded: bool,
3907) -> Result<(), String> {
3908 let client = state.client();
3909 client.set_auto_connect_excluded(&device_id, excluded);
3910 client.wake_native_external_auto_connect().await;
3911 let devices = state
3912 .native_identity
3913 .lock()
3914 .await
3915 .as_ref()
3916 .and_then(|owner| owner.devices.clone());
3917 if let (Some(devices), Some(node_id)) = (devices, client.current_node_id().await) {
3918 devices
3919 .signaling()
3920 .set_excluded_peers(
3921 &devices.principal_id,
3922 &node_id,
3923 &client.current_excluded_peers_snapshot(),
3924 )
3925 .await
3926 .map_err(|error| error.to_string())?;
3927 }
3928 Ok(())
3929}
3930
3931#[tauri::command]
3932async fn resolve_rtc_peer_connection_records(
3933 state: tauri::State<'_, OpenRtcTauriState>,
3934 id: String,
3935) -> Result<Vec<openrtc::connection_manager::ConnectionRecord>, String> {
3936 Ok(state.client().resolve_peer_connection_records(&id).await)
3937}
3938
3939#[tauri::command]
3940async fn resolve_rtc_peer_identity(
3941 state: tauri::State<'_, OpenRtcTauriState>,
3942 id: String,
3943) -> Result<Option<openrtc::connection_manager::PeerSnapshot>, String> {
3944 Ok(state.client().peer_snapshot(&id).await)
3945}
3946
3947#[tauri::command]
3948async fn get_rtc_peer_session(
3949 state: tauri::State<'_, OpenRtcTauriState>,
3950 id: String,
3951) -> Result<Option<openrtc::client::PeerSessionSnapshot>, String> {
3952 Ok(state.client().peer_session(&id).await)
3953}
3954
3955#[tauri::command]
3956async fn list_rtc_peer_sessions(
3957 state: tauri::State<'_, OpenRtcTauriState>,
3958) -> Result<Vec<openrtc::client::PeerSessionSnapshot>, String> {
3959 Ok(state.client().peer_sessions().await)
3960}
3961
3962#[tauri::command]
3963async fn list_rtc_managed_connections(
3964 state: tauri::State<'_, OpenRtcTauriState>,
3965) -> Result<Vec<openrtc::connection_manager::ConnectionRecord>, String> {
3966 Ok(state.client().list_managed_connections().await)
3967}
3968
3969#[derive(Debug, Clone, Serialize)]
3970#[serde(rename_all = "camelCase")]
3971struct NotifyRtcNetworkChangeResult {
3972 retired_stale_connections: usize,
3973}
3974
3975#[tauri::command]
3976async fn notify_rtc_network_change(
3977 state: tauri::State<'_, OpenRtcTauriState>,
3978 availability: Option<String>,
3979) -> Result<NotifyRtcNetworkChangeResult, String> {
3980 if let Some(observation) = availability {
3983 if !matches!(
3984 observation.as_str(),
3985 "background" | "foreground" | "network-change"
3986 ) {
3987 return Err("invalid transport availability observation".to_string());
3988 }
3989 #[cfg(feature = "transport-webrtc")]
3990 state
3991 .client()
3992 .observe_webrtc_availability(&observation)
3993 .await;
3994 return Ok(NotifyRtcNetworkChangeResult {
3995 retired_stale_connections: 0,
3996 });
3997 }
3998 let retired_stale_connections = state
3999 .client()
4000 .notify_network_change()
4001 .await
4002 .map_err(|error| error.to_string())?;
4003 Ok(NotifyRtcNetworkChangeResult {
4004 retired_stale_connections,
4005 })
4006}
4007
4008#[tauri::command]
4009async fn set_rtc_transport_priority(
4010 state: tauri::State<'_, OpenRtcTauriState>,
4011 priority: Vec<openrtc::route_policy::KnownRoute>,
4012) -> Result<(), String> {
4013 let client = state.client();
4014 let mut transport_config = client.transport_config().await;
4015 transport_config.route_priority = priority;
4016 client
4017 .update_transport_config(transport_config)
4018 .await
4019 .map_err(|error| error.to_string())
4020}
4021
4022#[tauri::command]
4023async fn wait_for_rtc_settled_peer(
4024 state: tauri::State<'_, OpenRtcTauriState>,
4025 id: String,
4026 timeout_ms: Option<u64>,
4027) -> Result<Option<openrtc::client::PeerSessionSnapshot>, String> {
4028 Ok(state.client().wait_for_peer(&id, timeout_ms).await)
4029}
4030
4031#[tauri::command]
4032async fn wait_for_rtc_settled_scope(
4033 state: tauri::State<'_, OpenRtcTauriState>,
4034 scope: String,
4035 timeout_ms: Option<u64>,
4036) -> Result<Option<openrtc::client::PeerSessionSnapshot>, String> {
4037 Ok(state.client().wait_for_settled_scope(&scope, timeout_ms).await)
4038}
4039
4040#[tauri::command]
4041async fn list_rtc_connection_states(
4042 state: tauri::State<'_, OpenRtcTauriState>,
4043 capability_key: String,
4044) -> Result<Vec<openrtc::client::StateSnapshot>, String> {
4045 let states = state.client().connection_states().await;
4046 let capability_key = required_native_capability_part(capability_key, "capability key")?;
4047 let registry = state.capability_registry.lock().await;
4048 Ok(states
4049 .into_iter()
4050 .filter(|snapshot| {
4051 registry
4052 .capability_keys_for_state(snapshot)
4053 .iter()
4054 .any(|key| key == &capability_key)
4055 })
4056 .collect())
4057}
4058
4059#[tauri::command]
4060async fn get_rtc_connection_state(
4061 state: tauri::State<'_, OpenRtcTauriState>,
4062 connection_id: String,
4063 capability_key: String,
4064) -> Result<Option<openrtc::client::StateSnapshot>, String> {
4065 let snapshot = state.client().connection_state(&connection_id).await;
4066 let Some(snapshot) = snapshot else {
4067 return Ok(None);
4068 };
4069 let capability_key = required_native_capability_part(capability_key, "capability key")?;
4070 let included = state
4071 .capability_registry
4072 .lock()
4073 .await
4074 .capability_keys_for_state(&snapshot)
4075 .iter()
4076 .any(|key| key == &capability_key);
4077 Ok(included.then_some(snapshot))
4078}
4079
4080#[tauri::command]
4081async fn stop_rtc_subscription(
4082 state: tauri::State<'_, OpenRtcTauriState>,
4083 request_id: String,
4084) -> Result<(), String> {
4085 stop_subscription_by_id(&state, &request_id).await;
4086 Ok(())
4087}
4088
4089async fn stop_subscription_by_id(state: &OpenRtcTauriState, request_id: &str) {
4090 if let Some(handle) = state.subscriptions.lock().await.remove(request_id) {
4091 handle.abort();
4092 }
4093 let mut projected = state.native_projection_subscription.lock().await;
4094 if projected
4095 .as_ref()
4096 .is_some_and(|subscription| subscription.request_id == request_id)
4097 {
4098 *projected = None;
4099 }
4100}
4101
4102#[tauri::command]
4109async fn start_native_projection<R: Runtime>(
4110 webview: Webview<R>,
4111 state: tauri::State<'_, OpenRtcTauriState>,
4112 request_id: Option<String>,
4113 channel: Channel<NativeProjectionEvent>,
4114) -> Result<String, String> {
4115 let request_id = request_id
4116 .map(|value| value.trim().to_string())
4117 .filter(|value| !value.is_empty())
4118 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
4119 let mut subscription = state.native_projection_subscription.lock().await;
4120 let request_id = install_native_projection(
4121 &mut subscription,
4122 webview.label().to_string(),
4123 request_id,
4124 channel,
4125 )?;
4126 drop(subscription);
4127 start_projected_stream_intake(webview.app_handle().clone(), state.inner()).await?;
4128 Ok(request_id)
4129}
4130
4131async fn start_projected_stream_intake<R: Runtime>(
4132 app: tauri::AppHandle<R>,
4133 state: &OpenRtcTauriState,
4134) -> Result<(), String> {
4135 if !state.owns_projected_stream_intake { return Ok(()); }
4136 let mut owner = state.projected_stream_intake.lock().await;
4137 if owner.is_some() { return Ok(()); }
4138 let client = state.client();
4139 ensure_iroh_node(client.clone()).await?;
4140 let incoming = client.incoming_streams().await.map_err(|error| error.to_string())?;
4141 *owner = Some(tokio::spawn(async move {
4142 let mut tasks = tokio::task::JoinSet::new();
4143 loop {
4144 let stream = tokio::select! {
4147 _ = tasks.join_next(), if !tasks.is_empty() => continue,
4148 stream = incoming.recv(), if tasks.len() < 32 => match stream {
4149 Ok(stream) => stream,
4150 Err(_) => break,
4151 },
4152 };
4153 let client = client.clone();
4154 let app = app.clone();
4155 tasks.spawn(async move {
4156 let result = async {
4157 let openrtc::native_node::IncomingStreamType::Bi(send, recv) = stream.stream else {
4158 return Ok(());
4159 };
4160 if !client.is_current_transport_stable_id(stream.endpoint_id, stream.transport_stable_id).await {
4161 return Err("retired incoming application transport".to_string());
4162 }
4163 let connection_id = client.peer_session(&stream.endpoint_id.to_string()).await
4164 .and_then(|snapshot| snapshot.active_connection_id)
4165 .ok_or_else(|| "incoming application session unavailable".to_string())?;
4166 if !matches!(client.session_admission(&connection_id), openrtc::session_token::SessionAdmission::Accepted { .. }) {
4167 return Err("incoming application session is not admitted".to_string());
4168 }
4169 let (send, recv) = client.secure_connection_bi_with_prefix(
4170 &connection_id, send, recv, &stream.recv_prefix,
4171 ).map_err(|error| error.to_string())?;
4172 let _ = tokio::time::timeout(std::time::Duration::from_secs(10),
4175 handoff_projected_peer_bi_stream(app, Some(connection_id),
4176 stream.endpoint_id.to_string(), stream.transport_stable_id, send, recv),
4177 ).await.map_err(|_| "incoming channel classification timed out".to_string())??;
4178 Ok::<(), String>(())
4179 }.await;
4180 if let Err(error) = result {
4181 eprintln!("[openrtc-tauri][stream-intake] {error}");
4182 }
4183 });
4184 }
4185 }));
4186 Ok(())
4187}
4188
4189fn install_native_projection(
4191 subscription: &mut Option<NativeProjectionSubscription>,
4192 webview_label: String,
4193 request_id: String,
4194 channel: Channel<NativeProjectionEvent>,
4195) -> Result<String, String> {
4196 if let Some(current) = subscription.as_ref() {
4197 if current.webview_label == webview_label {
4198 *subscription = Some(NativeProjectionSubscription {
4202 webview_label,
4203 request_id: request_id.clone(),
4204 channel,
4205 });
4206 return Ok(request_id);
4207 }
4208 return Err("OpenRTC native projection subscription already has a process owner".into());
4211 }
4212 *subscription = Some(NativeProjectionSubscription {
4213 webview_label,
4214 request_id: request_id.clone(),
4215 channel,
4216 });
4217 Ok(request_id)
4218}
4219
4220#[tauri::command]
4221async fn get_projected_stream_diagnostics(
4222 state: tauri::State<'_, OpenRtcTauriState>,
4223) -> Result<ProjectedStreamDiagnostics, String> {
4224 Ok(ProjectedStreamDiagnostics {
4225 subscription_active: state.native_projection_subscription.lock().await.is_some(),
4226 offered: state.projected_stream_offered.load(Ordering::Relaxed),
4227 decoded: state.projected_stream_decoded.load(Ordering::Relaxed),
4228 projected: state.projected_stream_projected.load(Ordering::Relaxed),
4229 unhandled_non_channel: state
4230 .projected_stream_unhandled_non_channel
4231 .load(Ordering::Relaxed),
4232 unhandled_other_channel: state
4233 .projected_stream_unhandled_other_channel
4234 .load(Ordering::Relaxed),
4235 unhandled_no_subscription: state
4236 .projected_stream_unhandled_no_subscription
4237 .load(Ordering::Relaxed),
4238 failures: state.projected_stream_failures.load(Ordering::Relaxed),
4239 last_transport_stable_id: state
4240 .projected_stream_last_transport_stable_id
4241 .load(Ordering::Relaxed),
4242 validation_checks: state
4243 .projected_stream_validation_checks
4244 .load(Ordering::Relaxed),
4245 validation_rejections: state
4246 .projected_stream_validation_rejections
4247 .load(Ordering::Relaxed),
4248 last_validated_transport_stable_id: state
4249 .projected_stream_last_validated_transport_stable_id
4250 .load(Ordering::Relaxed),
4251 authorized: state.projected_stream_authorized.load(Ordering::Relaxed),
4252 unauthorized: state.projected_stream_unauthorized.load(Ordering::Relaxed),
4253 last_channel: state.projected_stream_last_channel.lock().await.clone(),
4254 last_protocol: state.projected_stream_last_protocol.lock().await.clone(),
4255 last_connection_id: state
4256 .projected_stream_last_connection_id
4257 .lock()
4258 .await
4259 .clone(),
4260 last_remote_node_id: state
4261 .projected_stream_last_remote_node_id
4262 .lock()
4263 .await
4264 .clone(),
4265 })
4266}
4267
4268#[tauri::command]
4269async fn is_current_transport_stable_id(
4270 state: tauri::State<'_, OpenRtcTauriState>,
4271 endpoint_id: String,
4272 transport_stable_id: u64,
4273) -> Result<bool, String> {
4274 state
4275 .projected_stream_validation_checks
4276 .fetch_add(1, Ordering::Relaxed);
4277 state
4278 .projected_stream_last_validated_transport_stable_id
4279 .store(transport_stable_id, Ordering::Relaxed);
4280 let is_current = state
4281 .client()
4282 .is_current_transport_stable_id_str(&endpoint_id, transport_stable_id)
4283 .await
4284 .map_err(|error| error.to_string())?;
4285 if !is_current {
4286 state
4287 .projected_stream_validation_rejections
4288 .fetch_add(1, Ordering::Relaxed);
4289 }
4290 if native_stream_trace_enabled() {
4291 eprintln!(
4292 "[openrtc-tauri][incoming-application-stream] remote_node_id={} transport_stable_id={} current={} phase=validate",
4293 endpoint_id, transport_stable_id, is_current
4294 );
4295 }
4296 Ok(is_current)
4297}
4298
4299#[tauri::command]
4300async fn send_peer_message(
4301 state: tauri::State<'_, OpenRtcTauriState>,
4302 request: Request<'_>,
4303) -> Result<(), String> {
4304 let id = required_request_header(&request, PEER_ID_HEADER)?;
4305 let data = request_body_bytes(&request)?;
4306 state
4307 .client()
4308 .send_peer(&id, &data)
4309 .await
4310 .map_err(|error| error.to_string())
4311}
4312
4313#[tauri::command]
4314async fn encode_sparse_fanout_message(
4315 state: tauri::State<'_, OpenRtcTauriState>,
4316 request: Request<'_>,
4317) -> Result<Response, String> {
4318 let capability = required_request_header(&request, FANOUT_CAPABILITY_HEADER)?;
4319 let app_tag = required_request_header(&request, FANOUT_APP_TAG_HEADER)?;
4320 let payload = request_body_bytes(&request)?;
4321 let signing_request = state
4322 .client()
4323 .prepare_sparse_fanout_message(&capability, &payload)
4324 .await
4325 .map_err(|error| error.to_string())?;
4326 let signature = state.sign_device_message(&app_tag, &signing_request.signing_input)?;
4327 state
4328 .client()
4329 .finalize_sparse_fanout_message(&signing_request.request_id, &signature)
4330 .await
4331 .map(Response::new)
4332 .map_err(|error| error.to_string())
4333}
4334
4335#[tauri::command]
4336async fn accept_sparse_fanout_message(
4337 state: tauri::State<'_, OpenRtcTauriState>,
4338 request: Request<'_>,
4339) -> Result<serde_json::Value, String> {
4340 let capability = required_request_header(&request, FANOUT_CAPABILITY_HEADER)?;
4341 let source_peer = required_request_header(&request, FANOUT_SOURCE_PEER_HEADER)?;
4342 let encoded = request_body_bytes(&request)?;
4343 serde_json::to_value(
4344 state
4345 .client()
4346 .accept_sparse_fanout_message(&capability, &source_peer, &encoded)
4347 .await
4348 .map_err(|error| error.to_string())?,
4349 )
4350 .map_err(|error| error.to_string())
4351}
4352
4353#[tauri::command]
4354async fn sparse_fanout_diagnostics(
4355 state: tauri::State<'_, OpenRtcTauriState>,
4356 capability_key: String,
4357) -> Result<openrtc::sparse_fanout::SparseFanoutDiagnostics, String> {
4358 Ok(state
4359 .client()
4360 .sparse_fanout_diagnostics(&required_native_capability_part(
4361 capability_key,
4362 "capability key",
4363 )?)
4364 .await)
4365}
4366
4367#[derive(Debug, Serialize)]
4368#[serde(rename_all = "camelCase")]
4369struct NativeBroadcastPublisherDescriptor {
4370 handle: String,
4371 verification_key: Vec<u8>,
4372}
4373
4374#[derive(Debug, Serialize)]
4375#[serde(rename_all = "camelCase")]
4376struct NativeBroadcastSessionDescriptor {
4377 handle_id: String,
4378 id: String,
4379 role: openrtc::broadcast::BroadcastRole,
4380 state: openrtc::broadcast::BroadcastState,
4381 source_slots: Vec<String>,
4382 budget: openrtc::broadcast::BroadcastBudget,
4383}
4384
4385#[tauri::command]
4388async fn prepare_openrtc_broadcast_publisher(
4389 state: tauri::State<'_, OpenRtcTauriState>,
4390) -> Result<NativeBroadcastPublisherDescriptor, String> {
4391 let signer = openrtc::broadcast::BroadcastPublisherSigner::generate()
4392 .map_err(|error| error.to_string())?;
4393 let verification_key = signer.verifying_key().to_vec();
4394 let handle = uuid::Uuid::new_v4().simple().to_string();
4395 state
4396 .broadcast_signers
4397 .lock()
4398 .await
4399 .insert(handle.clone(), signer);
4400 Ok(NativeBroadcastPublisherDescriptor {
4401 handle,
4402 verification_key,
4403 })
4404}
4405
4406#[tauri::command]
4407async fn release_openrtc_broadcast_publisher(
4408 state: tauri::State<'_, OpenRtcTauriState>,
4409 handle: String,
4410) -> Result<bool, String> {
4411 Ok(state
4412 .broadcast_signers
4413 .lock()
4414 .await
4415 .remove(&handle)
4416 .is_some())
4417}
4418
4419async fn drive_native_broadcast_actions(
4420 session: &openrtc::broadcast::BroadcastSession,
4421 adapter: &dyn NativeBroadcastAdapter,
4422) -> Result<(), String> {
4423 loop {
4424 let actions = session.take_actions(openrtc::broadcast::MAX_BROADCAST_ACTIONS);
4425 if actions.is_empty() {
4426 return Ok(());
4427 }
4428 for action in actions {
4429 if let Some(observation) = adapter.apply(session.clone(), action).await? {
4430 session
4431 .observe(observation)
4432 .map_err(|error| error.to_string())?;
4433 }
4434 }
4435 }
4436}
4437
4438async fn native_broadcast_driver(
4439 state: &OpenRtcTauriState,
4440 handle_id: &str,
4441) -> Result<Arc<Mutex<()>>, String> {
4442 state
4443 .broadcast_drivers
4444 .lock()
4445 .await
4446 .get(handle_id)
4447 .cloned()
4448 .ok_or_else(|| "broadcast session is unavailable".to_string())
4449}
4450
4451#[tauri::command]
4452async fn open_openrtc_broadcast(
4453 state: tauri::State<'_, OpenRtcTauriState>,
4454 grant_token: String,
4455 issuer_public_key: Option<Vec<u8>>,
4456 publisher_signer_handle: Option<String>,
4457 now_ms: u64,
4458) -> Result<NativeBroadcastSessionDescriptor, String> {
4459 #[cfg(feature = "native-broadcast-moq")]
4460 let publication_verifying_key = if let Some(handle) = publisher_signer_handle.as_deref() {
4461 Some(
4462 state
4463 .broadcast_signers
4464 .lock()
4465 .await
4466 .get(handle)
4467 .ok_or_else(|| "broadcast publisher signer handle is invalid".to_string())?
4468 .verifying_key(),
4469 )
4470 } else {
4471 None
4472 };
4473 #[cfg(feature = "native-broadcast-moq")]
4474 let resolved_access = if state.native_broadcast_moq_adapter.is_some() {
4475 Some(
4476 state
4477 .resolve_native_broadcast_access(&grant_token, publication_verifying_key.as_ref())
4478 .await?,
4479 )
4480 } else {
4481 None
4482 };
4483 #[cfg(feature = "native-broadcast-moq")]
4484 let issuer_public_key = match (issuer_public_key, resolved_access.as_ref()) {
4485 (Some(provided), Some(resolved)) if provided.as_slice() != resolved.issuer_public_key => {
4486 return Err("broadcast issuer key mismatch".to_string());
4487 }
4488 (Some(provided), _) => provided,
4489 (None, Some(resolved)) => resolved.issuer_public_key.to_vec(),
4490 (None, None) => return Err("broadcast issuer key is required".to_string()),
4491 };
4492 #[cfg(not(feature = "native-broadcast-moq"))]
4493 let issuer_public_key =
4494 issuer_public_key.ok_or_else(|| "broadcast issuer key is required".to_string())?;
4495 let issuer_bytes: [u8; 32] = issuer_public_key
4496 .try_into()
4497 .map_err(|_| "broadcast issuer key must be 32 bytes".to_string())?;
4498 let issuer = VerifyingKey::from_bytes(&issuer_bytes)
4499 .map_err(|_| "broadcast issuer key is invalid".to_string())?;
4500 let broadcasts = state.client().broadcasts();
4501 let challenge = broadcasts
4502 .prepare_grant_verification(&grant_token, &issuer, now_ms)
4503 .map_err(|error| error.to_string())?;
4504 let device_signer = state.native_device_key_signer.as_deref().ok_or_else(|| {
4505 "native managed broadcast requires the host device-key signer".to_string()
4506 })?;
4507 let binding_signature = device_signer
4508 .sign(state.client().app_tag(), &challenge.signing_bytes)
4509 .map_err(|error| format!("sign broadcast installation challenge: {error}"))?;
4510 let grant = broadcasts
4511 .complete_grant_verification(
4512 &grant_token,
4513 &issuer,
4514 &challenge.handle,
4515 &binding_signature,
4516 now_ms,
4517 )
4518 .map_err(|error| error.to_string())?;
4519 let grant_generation = grant.grant_generation();
4520 let session = if let Some(handle) = publisher_signer_handle.as_deref() {
4521 let signers = state.broadcast_signers.lock().await;
4522 let signer = signers
4523 .get(handle)
4524 .ok_or_else(|| "broadcast publisher signer handle is invalid".to_string())?;
4525 state
4526 .client()
4527 .broadcasts()
4528 .open_publisher(grant, signer, now_ms)
4529 } else {
4530 state.client().broadcasts().open(grant, now_ms)
4531 }
4532 .map_err(|error| error.to_string())?;
4533 let handle_id = format!("{}:{grant_generation}", session.id());
4534 #[cfg(feature = "native-broadcast-moq")]
4535 if let (Some(adapter), Some(access)) = (
4536 state.native_broadcast_moq_adapter.as_ref(),
4537 resolved_access.map(|resolved| resolved.relay),
4538 ) {
4539 adapter
4540 .authorize(session.id(), grant_generation, access)
4541 .await;
4542 }
4543 let adapter = state.native_broadcast_adapter.as_deref().ok_or_else(|| {
4544 session.close();
4545 "native managed broadcast adapter is unavailable".to_string()
4546 })?;
4547 if let Err(error) = drive_native_broadcast_actions(&session, adapter).await {
4548 session.close();
4549 #[cfg(feature = "native-broadcast-moq")]
4550 if let Some(adapter) = state.native_broadcast_moq_adapter.as_ref() {
4551 adapter
4552 .forget_authorization(&session.id(), grant_generation)
4553 .await;
4554 }
4555 return Err(error);
4556 }
4557 let descriptor = NativeBroadcastSessionDescriptor {
4558 handle_id: handle_id.clone(),
4559 id: session.id(),
4560 role: session.role(),
4561 state: session.state(),
4562 source_slots: session.source_slots(),
4563 budget: session.budget(),
4564 };
4565 state
4566 .broadcast_sessions
4567 .lock()
4568 .await
4569 .insert(handle_id.clone(), session);
4570 state
4571 .broadcast_drivers
4572 .lock()
4573 .await
4574 .insert(handle_id, Arc::new(Mutex::new(())));
4575 Ok(descriptor)
4576}
4577
4578#[tauri::command]
4579#[allow(clippy::too_many_arguments)]
4580async fn begin_openrtc_broadcast_publication(
4581 state: tauri::State<'_, OpenRtcTauriState>,
4582 handle_id: String,
4583 source_slot: String,
4584 publication_id: Option<String>,
4585 kind: String,
4586 codec: String,
4587 clock_rate: u32,
4588 coded_width: Option<u32>,
4589 coded_height: Option<u32>,
4590 channels: Option<u16>,
4591) -> Result<PortableMediaPublicationStart, String> {
4592 let driver = native_broadcast_driver(&state, &handle_id).await?;
4593 let _driver = driver.lock().await;
4594 let publication_id = match publication_id.as_deref().map(str::trim) {
4595 Some(value) if !value.is_empty() => parse_media_publication_id(value)?,
4596 _ => openrtc::media::PublicationId::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
4597 };
4598 let kind: openrtc::media::MediaKind = kind
4599 .parse()
4600 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?;
4601 if !matches!(
4602 kind,
4603 openrtc::media::MediaKind::Audio | openrtc::media::MediaKind::Video
4604 ) {
4605 return Err("broadcast publications must be audio or video".to_string());
4606 }
4607 let publication = openrtc::media::MediaPublicationConfig {
4608 publication_id,
4609 media_generation: 0,
4610 kind,
4611 codec: codec
4612 .parse()
4613 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?,
4614 clock_rate,
4615 coded_width,
4616 coded_height,
4617 channels,
4618 };
4619 let session = state
4620 .broadcast_sessions
4621 .lock()
4622 .await
4623 .get(&handle_id)
4624 .cloned()
4625 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4626 let publication = session
4627 .begin_publication(&source_slot, publication)
4628 .map_err(|error| error.to_string())?;
4629 let adapter = state
4630 .native_broadcast_adapter
4631 .as_deref()
4632 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4633 drive_native_broadcast_actions(&session, adapter).await?;
4634 Ok(PortableMediaPublicationStart {
4635 publication_id: publication.publication_id.to_string(),
4636 media_generation: publication.media_generation,
4637 control: Vec::new(),
4639 })
4640}
4641
4642#[tauri::command]
4643#[allow(clippy::too_many_arguments)]
4644async fn publish_openrtc_broadcast_sample(
4645 state: tauri::State<'_, OpenRtcTauriState>,
4646 request: Request<'_>,
4647) -> Result<(), String> {
4648 let handle_id = required_request_header(&request, BROADCAST_HANDLE_ID_HEADER)?;
4649 let publication_id = required_request_header(&request, MEDIA_PUBLICATION_ID_HEADER)?;
4650 let timestamp_us = parsed_request_header(&request, MEDIA_TIMESTAMP_US_HEADER)?;
4651 let duration_us = parsed_request_header(&request, MEDIA_DURATION_US_HEADER)?;
4652 let keyframe = parsed_request_header(&request, MEDIA_KEYFRAME_HEADER)?;
4653 let discardable = parsed_request_header(&request, MEDIA_DISCARDABLE_HEADER)?;
4654 let payload = request_body_bytes(&request)?;
4655 let driver = native_broadcast_driver(&state, &handle_id).await?;
4656 let _driver = driver.lock().await;
4657 openrtc::media::ensure_javascript_safe_integer(timestamp_us, "timestamp")
4658 .map_err(|error| error.to_string())?;
4659 let session = state
4660 .broadcast_sessions
4661 .lock()
4662 .await
4663 .get(&handle_id)
4664 .cloned()
4665 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4666 session
4667 .publish(
4668 parse_media_publication_id(&publication_id)?,
4669 openrtc::media::EncodedMediaSample {
4670 timestamp_us,
4671 duration_us,
4672 keyframe,
4673 discardable,
4674 payload,
4675 },
4676 )
4677 .map_err(|error| error.to_string())?;
4678 let adapter = state
4679 .native_broadcast_adapter
4680 .as_deref()
4681 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4682 drive_native_broadcast_actions(&session, adapter).await
4683}
4684
4685#[tauri::command]
4686async fn pause_openrtc_broadcast_publication(
4687 state: tauri::State<'_, OpenRtcTauriState>,
4688 handle_id: String,
4689 publication_id: String,
4690) -> Result<(), String> {
4691 let driver = native_broadcast_driver(&state, &handle_id).await?;
4692 let _driver = driver.lock().await;
4693 let session = state
4694 .broadcast_sessions
4695 .lock()
4696 .await
4697 .get(&handle_id)
4698 .cloned()
4699 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4700 session
4701 .pause_publication(parse_media_publication_id(&publication_id)?)
4702 .map_err(|error| error.to_string())?;
4703 let adapter = state
4704 .native_broadcast_adapter
4705 .as_deref()
4706 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4707 drive_native_broadcast_actions(&session, adapter).await
4708}
4709
4710#[tauri::command]
4711async fn retire_openrtc_broadcast_publication(
4712 state: tauri::State<'_, OpenRtcTauriState>,
4713 handle_id: String,
4714 publication_id: String,
4715) -> Result<(), String> {
4716 let driver = native_broadcast_driver(&state, &handle_id).await?;
4717 let _driver = driver.lock().await;
4718 let session = state
4719 .broadcast_sessions
4720 .lock()
4721 .await
4722 .get(&handle_id)
4723 .cloned()
4724 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4725 session
4726 .retire_publication(parse_media_publication_id(&publication_id)?)
4727 .map_err(|error| error.to_string())?;
4728 let adapter = state
4729 .native_broadcast_adapter
4730 .as_deref()
4731 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4732 drive_native_broadcast_actions(&session, adapter).await
4733}
4734
4735#[tauri::command]
4736async fn retire_openrtc_broadcast_receiver(
4737 state: tauri::State<'_, OpenRtcTauriState>,
4738 handle_id: String,
4739 publication_id: String,
4740 media_generation: u32,
4741) -> Result<(), String> {
4742 let driver = native_broadcast_driver(&state, &handle_id).await?;
4743 let _driver = driver.lock().await;
4744 let session = state
4745 .broadcast_sessions
4746 .lock()
4747 .await
4748 .get(&handle_id)
4749 .cloned()
4750 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4751 session
4752 .retire_receiver(
4753 parse_media_publication_id(&publication_id)?,
4754 media_generation,
4755 )
4756 .map_err(|error| error.to_string())
4757}
4758
4759struct NativeBroadcastReceivedObject {
4760 source_slot: Option<String>,
4761 object: Vec<u8>,
4762}
4763
4764#[tauri::command]
4768async fn receive_openrtc_broadcast_media(
4769 state: tauri::State<'_, OpenRtcTauriState>,
4770 handle_id: String,
4771 max: usize,
4772) -> Result<Vec<openrtc::broadcast::AcceptedBroadcastMedia>, String> {
4773 let driver = native_broadcast_driver(&state, &handle_id).await?;
4774 let _driver = driver.lock().await;
4775 let session = state
4776 .broadcast_sessions
4777 .lock()
4778 .await
4779 .get(&handle_id)
4780 .cloned()
4781 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4782 let adapter = state
4783 .native_broadcast_adapter
4784 .as_deref()
4785 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4786 drive_native_broadcast_actions(&session, adapter).await?;
4787 let objects: Vec<NativeBroadcastReceivedObject>;
4788 #[cfg(feature = "native-broadcast-moq")]
4789 {
4790 objects = if let Some(native_moq) = state.native_broadcast_moq_adapter.as_ref() {
4791 native_moq
4792 .take_inbound_objects_with_source(&session, max.min(64))
4793 .await?
4794 .into_iter()
4795 .map(|item| NativeBroadcastReceivedObject {
4796 source_slot: Some(item.source_slot),
4797 object: item.object,
4798 })
4799 .collect()
4800 } else {
4801 adapter
4802 .take_inbound_objects(session.clone(), max.min(64))
4803 .await?
4804 .into_iter()
4805 .map(|object| NativeBroadcastReceivedObject {
4806 source_slot: None,
4807 object,
4808 })
4809 .collect()
4810 };
4811 }
4812 #[cfg(not(feature = "native-broadcast-moq"))]
4813 {
4814 objects = adapter
4815 .take_inbound_objects(session.clone(), max.min(64))
4816 .await?
4817 .into_iter()
4818 .map(|object| NativeBroadcastReceivedObject {
4819 source_slot: None,
4820 object,
4821 })
4822 .collect();
4823 }
4824 let mut accepted = Vec::with_capacity(objects.len());
4825 let mut delivered_bytes = 0_u64;
4826 for received in objects {
4827 let object_len = received.object.len() as u64;
4828 let media = if let Some(source_slot) = received.source_slot.as_deref() {
4829 session
4830 .accept_media_for_source(source_slot, &received.object)
4831 .map_err(|error| error.to_string())?
4832 } else {
4833 session
4834 .accept_media(&received.object)
4835 .map_err(|error| error.to_string())?
4836 };
4837 let Some(media) = media else { continue };
4838 delivered_bytes = delivered_bytes.saturating_add(object_len);
4839 if let Some(encoded) = media.media.as_deref() {
4840 let chunk = openrtc::media::EncodedMediaChunk::decode(encoded)
4841 .map_err(|error| error.to_string())?;
4842 openrtc::media::ensure_javascript_safe_integer(chunk.sequence, "sequence")
4843 .and_then(|_| {
4844 openrtc::media::ensure_javascript_safe_integer(chunk.timestamp_us, "timestamp")
4845 })
4846 .map_err(|error| error.to_string())?;
4847 }
4848 accepted.push(media);
4849 }
4850 #[cfg(feature = "native-broadcast-moq")]
4851 if let Some(native_moq) = state.native_broadcast_moq_adapter.as_ref() {
4852 let usage = native_moq
4853 .record_delivered_bytes(&session, delivered_bytes)
4854 .await;
4855 let settlement = drive_native_broadcast_actions(&session, adapter).await;
4856 usage?;
4857 settlement?;
4858 }
4859 Ok(accepted)
4860}
4861
4862#[tauri::command]
4863async fn get_openrtc_broadcast_stats(
4864 state: tauri::State<'_, OpenRtcTauriState>,
4865 handle_id: String,
4866) -> Result<openrtc::broadcast::BroadcastStats, String> {
4867 let session = state
4868 .broadcast_sessions
4869 .lock()
4870 .await
4871 .get(&handle_id)
4872 .cloned()
4873 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4874 Ok(session.stats())
4875}
4876
4877#[tauri::command]
4878async fn revoke_openrtc_broadcast(
4879 state: tauri::State<'_, OpenRtcTauriState>,
4880 handle_id: String,
4881 grant_generation: u64,
4882) -> Result<(), String> {
4883 let driver = native_broadcast_driver(&state, &handle_id).await?;
4884 let _driver = driver.lock().await;
4885 let session = state
4886 .broadcast_sessions
4887 .lock()
4888 .await
4889 .get(&handle_id)
4890 .cloned()
4891 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4892 session
4893 .revoke(grant_generation)
4894 .map_err(|error| error.to_string())?;
4895 let adapter = state
4896 .native_broadcast_adapter
4897 .as_deref()
4898 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4899 drive_native_broadcast_actions(&session, adapter).await
4900}
4901
4902#[tauri::command]
4903async fn close_openrtc_broadcast(
4904 state: tauri::State<'_, OpenRtcTauriState>,
4905 handle_id: String,
4906) -> Result<(), String> {
4907 let driver = native_broadcast_driver(&state, &handle_id).await?;
4908 let _driver = driver.lock().await;
4909 let session = state
4910 .broadcast_sessions
4911 .lock()
4912 .await
4913 .remove(&handle_id)
4914 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4915 session.close();
4916 let adapter = state
4917 .native_broadcast_adapter
4918 .as_deref()
4919 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4920 let result = drive_native_broadcast_actions(&session, adapter).await;
4921 state.broadcast_drivers.lock().await.remove(&handle_id);
4922 result
4923}
4924
4925#[tauri::command]
4926async fn record_sparse_fanout_forward_queue_drop(
4927 state: tauri::State<'_, OpenRtcTauriState>,
4928 capability_key: String,
4929 count: u64,
4930) -> Result<(), String> {
4931 state
4932 .client()
4933 .record_sparse_fanout_forward_queue_drop(
4934 &required_native_capability_part(capability_key, "capability key")?,
4935 count,
4936 )
4937 .await
4938 .map_err(|error| error.to_string())
4939}
4940
4941#[tauri::command]
4942async fn is_peer_connected(
4943 state: tauri::State<'_, OpenRtcTauriState>,
4944 node_id: String,
4945) -> Result<bool, String> {
4946 state
4947 .client()
4948 .is_connected_str(&node_id)
4949 .await
4950 .map_err(|error| error.to_string())
4951}
4952
4953#[derive(Debug, Serialize)]
4954#[serde(rename_all = "camelCase")]
4955struct PortableMediaPublicationStart {
4956 publication_id: String,
4957 media_generation: u32,
4958 control: Vec<u8>,
4959}
4960
4961fn media_kind_name(kind: openrtc::media::MediaKind) -> &'static str {
4962 match kind {
4963 openrtc::media::MediaKind::Audio => "audio",
4964 openrtc::media::MediaKind::Video => "video",
4965 openrtc::media::MediaKind::Screen => "screen",
4966 openrtc::media::MediaKind::Data => "data",
4967 }
4968}
4969
4970fn media_codec_name(codec: openrtc::media::MediaCodec) -> &'static str {
4971 match codec {
4972 openrtc::media::MediaCodec::Opus => "opus",
4973 openrtc::media::MediaCodec::H264 => "h264",
4974 openrtc::media::MediaCodec::Vp8 => "vp8",
4975 openrtc::media::MediaCodec::Vp9 => "vp9",
4976 openrtc::media::MediaCodec::Av1 => "av1",
4977 openrtc::media::MediaCodec::Pcm => "pcm",
4978 openrtc::media::MediaCodec::Opaque => "opaque",
4979 }
4980}
4981
4982fn parse_media_publication_id(value: &str) -> Result<openrtc::media::PublicationId, String> {
4983 value
4984 .trim()
4985 .parse()
4986 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())
4987}
4988
4989#[tauri::command]
4990#[allow(clippy::too_many_arguments)]
4991async fn begin_openrtc_media_publication(
4992 state: tauri::State<'_, OpenRtcTauriState>,
4993 publication_id: Option<String>,
4994 kind: String,
4995 codec: String,
4996 clock_rate: u32,
4997 coded_width: Option<u32>,
4998 coded_height: Option<u32>,
4999 channels: Option<u16>,
5000) -> Result<PortableMediaPublicationStart, String> {
5001 let publication_id = match publication_id.as_deref().map(str::trim) {
5002 Some(value) if !value.is_empty() => parse_media_publication_id(value)?,
5003 _ => openrtc::media::PublicationId::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
5004 };
5005 let kind: openrtc::media::MediaKind = kind
5006 .parse()
5007 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?;
5008 if !matches!(
5009 kind,
5010 openrtc::media::MediaKind::Audio | openrtc::media::MediaKind::Video
5011 ) {
5012 return Err("browser media publications must be audio or video".to_string());
5013 }
5014 let codec = codec
5015 .parse()
5016 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?;
5017 let publication = openrtc::media::MediaPublicationConfig {
5018 publication_id,
5019 media_generation: 0,
5020 kind,
5021 codec,
5022 clock_rate,
5023 coded_width,
5024 coded_height,
5025 channels,
5026 };
5027 let (publication, control) = state
5028 .portable_media
5029 .lock()
5030 .await
5031 .begin_publication(publication)
5032 .map_err(|error| error.to_string())?;
5033 Ok(PortableMediaPublicationStart {
5034 publication_id: publication.publication_id.to_string(),
5035 media_generation: publication.media_generation,
5036 control,
5037 })
5038}
5039
5040#[tauri::command]
5041#[allow(clippy::too_many_arguments)]
5042async fn encode_openrtc_media_sample(
5043 state: tauri::State<'_, OpenRtcTauriState>,
5044 request: Request<'_>,
5045) -> Result<Response, String> {
5046 let publication_id = required_request_header(&request, MEDIA_PUBLICATION_ID_HEADER)?;
5047 let timestamp_us = parsed_request_header(&request, MEDIA_TIMESTAMP_US_HEADER)?;
5048 openrtc::media::ensure_javascript_safe_integer(timestamp_us, "timestamp")
5049 .map_err(|error| error.to_string())?;
5050 let duration_us = parsed_request_header(&request, MEDIA_DURATION_US_HEADER)?;
5051 let keyframe = parsed_request_header(&request, MEDIA_KEYFRAME_HEADER)?;
5052 let discardable = parsed_request_header(&request, MEDIA_DISCARDABLE_HEADER)?;
5053 let payload = request_body_bytes(&request)?;
5054 let encoded = state
5055 .portable_media
5056 .lock()
5057 .await
5058 .encode_sample(
5059 parse_media_publication_id(&publication_id)?,
5060 openrtc::media::EncodedMediaSample {
5061 timestamp_us,
5062 duration_us,
5063 keyframe,
5064 discardable,
5065 payload,
5066 },
5067 )
5068 .map_err(|error| error.to_string())?;
5069 Ok(Response::new(encoded))
5070}
5071
5072#[tauri::command]
5073async fn pause_openrtc_media_publication(
5074 state: tauri::State<'_, OpenRtcTauriState>,
5075 publication_id: String,
5076) -> Result<(), String> {
5077 state
5078 .portable_media
5079 .lock()
5080 .await
5081 .pause_publication(parse_media_publication_id(&publication_id)?)
5082 .map_err(|error| error.to_string())
5083}
5084
5085#[tauri::command]
5086async fn retire_openrtc_media_publication(
5087 state: tauri::State<'_, OpenRtcTauriState>,
5088 publication_id: String,
5089) -> Result<(), String> {
5090 state
5091 .portable_media
5092 .lock()
5093 .await
5094 .retire_publication(parse_media_publication_id(&publication_id)?);
5095 Ok(())
5096}
5097
5098#[tauri::command]
5099async fn retire_openrtc_media_receiver(
5100 state: tauri::State<'_, OpenRtcTauriState>,
5101 publication_id: String,
5102 media_generation: u32,
5103) -> Result<(), String> {
5104 state.portable_media.lock().await.retire_receiver(
5105 parse_media_publication_id(&publication_id)?,
5106 media_generation,
5107 );
5108 Ok(())
5109}
5110
5111#[tauri::command]
5112async fn decode_openrtc_media_chunk(
5113 state: tauri::State<'_, OpenRtcTauriState>,
5114 request: Request<'_>,
5115) -> Result<Response, String> {
5116 let encoded = request_body_bytes(&request)?;
5117 let chunk = state
5118 .portable_media
5119 .lock()
5120 .await
5121 .decode_chunk(&encoded)
5122 .map_err(|error| error.to_string())?;
5123 openrtc::media::ensure_javascript_safe_integer(chunk.sequence, "sequence")
5124 .and_then(|_| {
5125 openrtc::media::ensure_javascript_safe_integer(chunk.timestamp_us, "timestamp")
5126 })
5127 .map_err(|error| error.to_string())?;
5128 let metadata = serde_json::to_vec(&serde_json::json!({
5129 "publicationId": chunk.publication_id.to_string(),
5130 "mediaGeneration": chunk.media_generation,
5131 "sequence": chunk.sequence,
5132 "timestampUs": chunk.timestamp_us,
5133 "durationUs": chunk.duration_us,
5134 "kind": media_kind_name(chunk.kind),
5135 "codec": media_codec_name(chunk.codec),
5136 "keyframe": chunk.keyframe,
5137 "discardable": chunk.discardable,
5138 }))
5139 .map_err(|error| format!("encode media IPC metadata failed: {error}"))?;
5140 let metadata_len =
5141 u32::try_from(metadata.len()).map_err(|_| "media IPC metadata is too large".to_string())?;
5142 let mut response = Vec::with_capacity(4 + metadata.len() + chunk.payload.len());
5143 response.extend_from_slice(&metadata_len.to_be_bytes());
5144 response.extend_from_slice(&metadata);
5145 response.extend_from_slice(&chunk.payload);
5146 Ok(Response::new(response))
5147}
5148
5149#[tauri::command]
5150async fn decode_openrtc_media_control(
5151 state: tauri::State<'_, OpenRtcTauriState>,
5152 request: Request<'_>,
5153) -> Result<serde_json::Value, String> {
5154 let encoded = request_body_bytes(&request)?;
5155 let control = state
5156 .portable_media
5157 .lock()
5158 .await
5159 .decode_control(&encoded)
5160 .map_err(|error| error.to_string())?;
5161 Ok(match control {
5162 openrtc::media::MediaControlFrame::Publish {
5163 publication_id,
5164 media_generation,
5165 kind,
5166 codec,
5167 clock_rate,
5168 coded_width,
5169 coded_height,
5170 channels,
5171 } => serde_json::json!({
5172 "type": "publish",
5173 "publicationId": publication_id.to_string(),
5174 "mediaGeneration": media_generation,
5175 "kind": media_kind_name(kind),
5176 "codec": media_codec_name(codec),
5177 "clockRate": clock_rate,
5178 "codedWidth": coded_width,
5179 "codedHeight": coded_height,
5180 "channels": channels,
5181 }),
5182 openrtc::media::MediaControlFrame::SetEnabled {
5183 publication_id,
5184 media_generation,
5185 enabled,
5186 } => serde_json::json!({
5187 "type": "set-enabled",
5188 "publicationId": publication_id.to_string(),
5189 "mediaGeneration": media_generation,
5190 "enabled": enabled,
5191 }),
5192 openrtc::media::MediaControlFrame::RequestKeyframe {
5193 publication_id,
5194 media_generation,
5195 } => serde_json::json!({
5196 "type": "request-keyframe",
5197 "publicationId": publication_id.to_string(),
5198 "mediaGeneration": media_generation,
5199 }),
5200 openrtc::media::MediaControlFrame::Stop {
5201 publication_id,
5202 media_generation,
5203 reason,
5204 } => serde_json::json!({
5205 "type": "stop",
5206 "publicationId": publication_id.to_string(),
5207 "mediaGeneration": media_generation,
5208 "reason": reason,
5209 }),
5210 })
5211}
5212
5213#[tauri::command]
5214async fn open_peer_bi_stream<R: Runtime>(
5215 app: tauri::AppHandle<R>,
5216 state: tauri::State<'_, OpenRtcTauriState>,
5217 peer_id: String,
5218 timeout_ms: Option<u64>,
5219) -> Result<OpenBiResult, String> {
5220 open_peer_bi_with(app, state, peer_id, timeout_ms, |client, peer_id, timeout_ms| async move {
5221 client.open_peer_bi(&peer_id, timeout_ms).await
5222 })
5223 .await
5224}
5225
5226#[tauri::command]
5227async fn open_peer_bi_transport_only_stream<R: Runtime>(
5228 app: tauri::AppHandle<R>,
5229 state: tauri::State<'_, OpenRtcTauriState>,
5230 peer_id: String,
5231 timeout_ms: Option<u64>,
5232) -> Result<OpenBiResult, String> {
5233 open_peer_bi_with(
5234 app,
5235 state,
5236 peer_id,
5237 timeout_ms,
5238 |client, peer_id, timeout_ms| async move {
5239 client
5240 .open_peer_bi_transport_only(&peer_id, timeout_ms)
5241 .await
5242 .map(|(connection_id, remote_node_id, send, recv)| {
5243 (
5244 connection_id,
5245 remote_node_id,
5246 PeerSendStream::plain(send),
5247 PeerRecvStream::plain(recv),
5248 )
5249 })
5250 },
5251 )
5252 .await
5253}
5254
5255#[tauri::command]
5256async fn open_peer_uni_stream(
5257 state: tauri::State<'_, OpenRtcTauriState>,
5258 peer_id: String,
5259 timeout_ms: Option<u64>,
5260) -> Result<OpenUniResult, String> {
5261 let peer_id = peer_id.trim().to_string();
5262 if peer_id.is_empty() {
5263 return Err("peerId is required".to_string());
5264 }
5265 let (connection_id, remote_node_id, send) = state
5266 .client()
5267 .open_peer_uni(&peer_id, timeout_ms)
5268 .await
5269 .map_err(|error| format!("open peer uni stream failed: {error}"))?;
5270 let stream_id = uuid::Uuid::new_v4().to_string();
5271 state
5272 .peer_uni_streams
5273 .lock()
5274 .await
5275 .insert(stream_id.clone(), Arc::new(Mutex::new(Some(send))));
5276 Ok(OpenUniResult {
5277 stream_id,
5278 connection_id,
5279 remote_node_id,
5280 })
5281}
5282
5283#[tauri::command]
5284async fn write_peer_bi_stream(
5285 state: tauri::State<'_, OpenRtcTauriState>,
5286 request: Request<'_>,
5287) -> Result<(), String> {
5288 let stream_id = required_request_header(&request, PEER_BI_STREAM_ID_HEADER)?;
5289 let bytes = request_body_bytes(&request)?;
5290
5291 let send = {
5292 let streams = state.peer_bi_streams.lock().await;
5293 streams
5294 .get(&stream_id)
5295 .map(|handle| handle.send.clone())
5296 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?
5297 };
5298 let result = send
5299 .lock()
5300 .await
5301 .as_mut()
5302 .ok_or_else(|| format!("peer bi stream already closed: {stream_id}"))?
5303 .write_all(&bytes)
5304 .await
5305 .map_err(|error| format!("write peer bi stream failed: {error}"));
5306 result
5307}
5308
5309#[tauri::command]
5312async fn finish_peer_bi_stream_send(
5313 state: tauri::State<'_, OpenRtcTauriState>,
5314 stream_id: String,
5315) -> Result<(), String> {
5316 let stream_id = stream_id.trim().to_string();
5317 if stream_id.is_empty() {
5318 return Err("streamId is required".to_string());
5319 }
5320 let send = {
5321 let streams = state.peer_bi_streams.lock().await;
5322 streams
5323 .get(&stream_id)
5324 .map(|handle| handle.send.clone())
5325 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?
5326 };
5327 if let Some(send) = send.lock().await.take() {
5328 send.finish()
5329 .map_err(|error| format!("finish peer bi stream send failed: {error}"))?;
5330 }
5331 Ok(())
5332}
5333
5334#[tauri::command]
5335async fn start_peer_bi_stream_read<R: Runtime>(
5336 _app: tauri::AppHandle<R>,
5337 state: tauri::State<'_, OpenRtcTauriState>,
5338 stream_id: String,
5339 channel: Channel<Response>,
5340) -> Result<(), String> {
5341 let stream_id = stream_id.trim().to_string();
5342 if stream_id.is_empty() {
5343 return Err("streamId is required".to_string());
5344 }
5345
5346 let mut streams = state.peer_bi_streams.lock().await;
5347 let handle = streams
5348 .get_mut(&stream_id)
5349 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?;
5350 if handle.read_task.is_some() {
5351 return Ok(());
5352 }
5353 let recv = handle
5354 .recv
5355 .take()
5356 .ok_or_else(|| format!("peer bi stream reader already consumed: {stream_id}"))?;
5357 handle.read_task = Some(spawn_peer_bi_stream_reader(channel, stream_id, recv));
5358 Ok(())
5359}
5360
5361#[tauri::command]
5367async fn cancel_peer_bi_stream_read(
5368 state: tauri::State<'_, OpenRtcTauriState>,
5369 stream_id: String,
5370) -> Result<(), String> {
5371 let stream_id = stream_id.trim().to_string();
5372 if stream_id.is_empty() {
5373 return Err("streamId is required".to_string());
5374 }
5375
5376 let mut streams = state.peer_bi_streams.lock().await;
5377 let handle = streams
5378 .get_mut(&stream_id)
5379 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?;
5380 handle.recv.take();
5381 if let Some(read_task) = handle.read_task.take() {
5382 read_task.abort();
5383 }
5384 Ok(())
5385}
5386
5387#[tauri::command]
5388async fn close_peer_bi_stream(
5389 state: tauri::State<'_, OpenRtcTauriState>,
5390 stream_id: String,
5391) -> Result<(), String> {
5392 let stream_id = stream_id.trim().to_string();
5393 if stream_id.is_empty() {
5394 return Err("streamId is required".to_string());
5395 }
5396
5397 let handle = {
5398 let mut streams = state.peer_bi_streams.lock().await;
5399 streams.remove(&stream_id)
5400 };
5401 if let Some(handle) = handle {
5402 let mut send = handle.send.lock().await;
5405 if let Some(send) = send.take() {
5406 let _ = send.finish();
5407 }
5408 drop(send);
5409 if let Some(read_task) = handle.read_task {
5410 read_task.abort();
5411 }
5412 }
5413 Ok(())
5414}
5415
5416#[tauri::command]
5417async fn write_peer_uni_stream(
5418 state: tauri::State<'_, OpenRtcTauriState>,
5419 request: Request<'_>,
5420) -> Result<(), String> {
5421 let stream_id = required_request_header(&request, PEER_BI_STREAM_ID_HEADER)?;
5422 let bytes = request_body_bytes(&request)?;
5423 let send = {
5424 let streams = state.peer_uni_streams.lock().await;
5425 streams
5426 .get(&stream_id)
5427 .cloned()
5428 .ok_or_else(|| format!("peer uni stream not found: {stream_id}"))?
5429 };
5430 let result = send
5431 .lock()
5432 .await
5433 .as_mut()
5434 .ok_or_else(|| format!("peer uni stream already closed: {stream_id}"))?
5435 .write_all(&bytes)
5436 .await
5437 .map_err(|error| format!("write peer uni stream failed: {error}"));
5438 result
5439}
5440
5441#[tauri::command]
5442async fn close_peer_uni_stream(
5443 state: tauri::State<'_, OpenRtcTauriState>,
5444 stream_id: String,
5445) -> Result<(), String> {
5446 let stream_id = stream_id.trim().to_string();
5447 if stream_id.is_empty() {
5448 return Err("streamId is required".to_string());
5449 }
5450 let send = state.peer_uni_streams.lock().await.remove(&stream_id);
5451 if let Some(send) = send {
5452 if let Some(send) = send.lock().await.take() {
5453 let _ = send.finish();
5454 }
5455 }
5456 Ok(())
5457}
5458
5459#[cfg(test)]
5460mod tests {
5461 mod stream_projection;
5462
5463 use super::*;
5464 use std::collections::HashMap as StdHashMap;
5465 use std::sync::atomic::{AtomicUsize, Ordering};
5466 use std::sync::Mutex as StdMutex;
5467 use tokio::sync::oneshot;
5468
5469 #[test]
5470 fn initial_auto_connect_exclusions_are_validated_and_deduplicated() {
5471 assert_eq!(
5472 normalize_initial_auto_connect_exclusions(vec![
5473 " device-b ".into(),
5474 "device-a".into(),
5475 "device-b".into(),
5476 ])
5477 .unwrap(),
5478 vec!["device-a", "device-b"],
5479 );
5480 assert!(normalize_initial_auto_connect_exclusions(vec!["".into()]).is_err());
5481 assert!(normalize_initial_auto_connect_exclusions(vec!["x".into(); 251]).is_err());
5482 }
5483
5484 #[test]
5485 fn native_startup_denials_keep_structured_metadata_and_legacy_strings() {
5486 for code in openrtc::service_errors::SERVICE_ERROR_CODES {
5487 let error = NativeCommandError::Service {
5488 message: "OpenRTC service request denied",
5489 details: openrtc::service_errors::ServiceError::from_value(&serde_json::json!({
5490 "code": code, "retryable": false, "scope": "app", "operation": "gateway.grant.issue",
5491 "requestId": "startup-1", "retryAfterMs": 1200, "resetAt": 1800000000000_u64,
5492 "providerCost": 99,
5493 })).unwrap(),
5494 };
5495 let value = serde_json::to_value(error).unwrap();
5496 assert_eq!(value["code"], *code);
5497 assert_eq!(value["retryable"], false);
5498 assert_eq!(value["scope"], "app");
5499 assert_eq!(value["requestId"], "startup-1");
5500 assert_eq!(value["retryAfterMs"], 1200);
5501 assert_eq!(value["resetAt"], 1800000000000_u64);
5502 assert_eq!(value["message"], "OpenRTC service request denied");
5503 assert!(value.get("providerCost").is_none());
5504 }
5505 let legacy = NativeCommandError::from_runtime(anyhow::anyhow!("host unavailable"));
5506 assert_eq!(serde_json::to_value(legacy).unwrap(), "host unavailable");
5507 }
5508
5509 #[cfg(feature = "testing-endpoints")]
5510 #[tokio::test]
5511 async fn real_http_denial_reaches_native_command_encoder() {
5512 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5513 struct DenialSigner;
5514 impl openrtc::native::DeviceSigner for DenialSigner {
5515 fn public_jwk(&self, _: &str) -> anyhow::Result<serde_json::Value> {
5516 Ok(serde_json::json!({ "kty": "OKP", "crv": "Ed25519",
5517 "x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }))
5518 }
5519 fn sign(&self, _: &str, _: &[u8]) -> anyhow::Result<Vec<u8>> {
5520 Ok(vec![0; 64])
5522 }
5523 }
5524 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
5525 let address = listener.local_addr().unwrap();
5526 let server = tokio::spawn(async move {
5527 let (mut stream, _) = tokio::time::timeout(std::time::Duration::from_secs(5), listener.accept())
5528 .await.unwrap().unwrap();
5529 let mut bytes = Vec::new();
5530 let mut buffer = [0_u8; 4096];
5531 loop {
5532 let read = tokio::time::timeout(std::time::Duration::from_secs(5), stream.read(&mut buffer))
5533 .await.unwrap().unwrap();
5534 assert!(read > 0);
5535 bytes.extend_from_slice(&buffer[..read]);
5536 assert!(bytes.len() < 64 * 1024);
5537 if let Some(end) = bytes.windows(4).position(|part| part == b"\r\n\r\n") {
5538 let length = String::from_utf8_lossy(&bytes[..end]).lines().find_map(|line| {
5539 let (name, value) = line.split_once(':')?;
5540 name.eq_ignore_ascii_case("content-length").then(|| value.trim().parse::<usize>().unwrap())
5541 }).unwrap();
5542 assert!(length < 32 * 1024);
5543 if bytes.len() >= end + 4 + length { break; }
5544 }
5545 }
5546 assert!(String::from_utf8_lossy(&bytes).starts_with("POST /v2/capabilities "));
5547 let body = r#"{"error":"private provider response","code":"app-budget-exhausted","retryable":false,"scope":"app","operation":"capability.issue","requestId":"local-denial","retryAfterMs":1200,"providerCost":99}"#;
5548 stream.write_all(format!(
5549 "HTTP/1.1 429 Too Many Requests\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", body.len(), body,
5550 ).as_bytes()).await.unwrap();
5551 });
5552 let control = openrtc::native::ControlPlane::anonymous(
5553 "pk_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
5554 Arc::new(DenialSigner),
5555 ).unwrap().with_testing_endpoints(format!("http://{address}"), "http://127.0.0.1:1").unwrap();
5556 let result = tokio::time::timeout(std::time::Duration::from_secs(5),
5557 control.join_space("local-room", "desktop", openrtc::native::CapabilityOptions::default()),
5558 ).await.expect("local denial must settle without retry");
5559 let error = match result { Err(error) => error, Ok(_) => panic!("denied capability admitted") };
5560 server.await.unwrap();
5561 let value = serde_json::to_value(NativeCommandError::from_runtime(error.context("private host context"))).unwrap();
5562 assert_eq!(value["code"], "app-budget-exhausted");
5563 assert_eq!(value["retryable"], false);
5564 assert_eq!(value["operation"], "capability.issue");
5565 assert_eq!(value["requestId"], "local-denial");
5566 assert_eq!(value["retryAfterMs"], 1200);
5567 assert_eq!(value["message"], "OpenRTC service request denied");
5568 assert!(!value.to_string().contains("private"));
5569 assert!(value.get("providerCost").is_none());
5570 }
5571
5572 #[test]
5573 fn native_projection_rejects_competing_owner_without_replacing_channel() {
5574 let mut slot = None;
5575 let first = Channel::new(|_| Ok(()));
5576 let first_id = first.id();
5577 install_native_projection(&mut slot, "main".into(), "first".into(), first).unwrap();
5578 let error = install_native_projection(
5579 &mut slot,
5580 "settings".into(),
5581 "second".into(),
5582 Channel::new(|_| Ok(())),
5583 )
5584 .unwrap_err();
5585 assert!(error.contains("already has a process owner"));
5586 assert_eq!(slot.as_ref().unwrap().request_id, "first");
5587 assert_eq!(slot.as_ref().unwrap().channel.id(), first_id);
5588
5589 let refreshed = Channel::new(|_| Ok(()));
5590 let refreshed_id = refreshed.id();
5591 install_native_projection(&mut slot, "main".into(), "first".into(), refreshed).unwrap();
5592 assert_eq!(slot.as_ref().unwrap().channel.id(), refreshed_id);
5593 slot = None;
5594 install_native_projection(
5595 &mut slot,
5596 "settings".into(),
5597 "second".into(),
5598 Channel::new(|_| Ok(())),
5599 )
5600 .unwrap();
5601 assert_eq!(slot.as_ref().unwrap().request_id, "second");
5602 assert!(install_native_projection(
5603 &mut slot,
5604 "main".into(),
5605 "first".into(),
5606 Channel::new(|_| Ok(())),
5607 )
5608 .is_err());
5609 assert_eq!(slot.as_ref().unwrap().request_id, "second");
5610 }
5611
5612 #[test]
5613 fn native_projection_same_webview_reclaims_after_reload() {
5614 let mut slot = None;
5615 install_native_projection(
5616 &mut slot,
5617 "main".into(),
5618 "before-reload".into(),
5619 Channel::new(|_| Ok(())),
5620 )
5621 .unwrap();
5622
5623 install_native_projection(
5624 &mut slot,
5625 "main".into(),
5626 "after-reload".into(),
5627 Channel::new(|_| Ok(())),
5628 )
5629 .unwrap();
5630
5631 assert_eq!(slot.as_ref().unwrap().webview_label, "main");
5632 assert_eq!(slot.as_ref().unwrap().request_id, "after-reload");
5633 }
5634
5635 #[tokio::test]
5636 async fn native_service_errors_reject_retired_identity_and_wrong_avenue() {
5637 let registry = Arc::new(Mutex::new(NativeCapabilityRegistry::default()));
5638 {
5639 let mut state = registry.lock().await;
5640 state.register("devices:p".into(), "devices".into(), "p".into(), false).unwrap();
5641 state.registrations.get_mut("devices:p").unwrap().native_source =
5642 Some(NativeRosterSource::Active("identity-new".into()));
5643 }
5644 let received = Arc::new(StdMutex::new(Vec::<serde_json::Value>::new()));
5645 let sink = received.clone();
5646 let channel = Channel::new(move |body| {
5647 if let tauri::ipc::InvokeResponseBody::Json(json) = body {
5648 sink.lock().unwrap().push(serde_json::from_str(&json).unwrap());
5649 }
5650 Ok(())
5651 });
5652 let projection = Arc::new(Mutex::new(Some(NativeProjectionSubscription {
5653 webview_label: "main".into(), request_id: "current-request".into(), channel,
5654 })));
5655 let mut observation = openrtc::native::NativeServiceErrorObservation {
5656 avenue: serde_json::from_value(serde_json::json!({ "kind": "user", "id": "p" })).unwrap(),
5657 runtime_instance_id: "runtime:1".into(),
5658 service_error: openrtc::service_errors::ServiceError::from_value(&serde_json::json!({
5659 "code": "app-budget-exhausted", "retryable": false, "scope": "app", "providerCost": 99,
5660 })).unwrap(),
5661 };
5662 assert!(!forward_native_service_error(®istry, &projection, "identity-old", "devices:p", &observation).await);
5663 observation.avenue.id = "another-principal".into();
5664 assert!(!forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5665 observation.avenue.id = "p".into();
5666 assert!(forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5667 let events = received.lock().unwrap();
5668 assert_eq!(events.len(), 1);
5669 assert_eq!(events[0]["kind"], "service-error");
5670 assert_eq!(events[0]["requestId"], "current-request");
5671 assert_eq!(events[0]["capabilityKeys"], serde_json::json!(["devices:p"]));
5672 assert_eq!(events[0]["payload"]["identityId"], "identity-new");
5673 assert_eq!(events[0]["payload"]["serviceError"]["code"], "app-budget-exhausted");
5674 assert!(events[0]["payload"]["serviceError"].get("providerCost").is_none());
5675 drop(events);
5676 registry.lock().await.registrations.get_mut("devices:p").unwrap().native_source =
5677 Some(NativeRosterSource::Retired);
5678 assert!(!forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5679 registry.lock().await.registrations.remove("devices:p");
5680 assert!(!forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5681 assert_eq!(received.lock().unwrap().len(), 1);
5682 }
5683
5684 #[tokio::test]
5685 async fn active_native_roster_is_replayed_for_the_same_capability_after_reload() {
5686 let (_keep_alive, wait) = oneshot::channel::<()>();
5687 let roster = NativeDeviceRoster {
5688 capability_key: "devices:user-1".into(),
5689 task: Some(tokio::spawn(async move {
5690 let _ = wait.await;
5691 })),
5692 };
5693
5694 assert!(native_roster_is_active_for_capability(Some(&roster), "devices:user-1",).unwrap());
5695 assert!(
5696 native_roster_is_active_for_capability(Some(&roster), "devices:user-2",)
5697 .unwrap_err()
5698 .contains("already have a capability owner")
5699 );
5700 }
5701
5702 struct FakeTransportInstaller {
5703 installs: Arc<AtomicUsize>,
5704 }
5705
5706 #[derive(Default)]
5707 struct FakeDeviceKeySigner {
5708 records: StdMutex<StdHashMap<String, String>>,
5709 }
5710
5711 impl DeviceKeySigner for FakeDeviceKeySigner {
5712 fn public_jwk(&self, _app_tag: &str) -> Result<serde_json::Value, String> {
5713 Ok(serde_json::json!({
5714 "kty": "OKP",
5715 "crv": "Ed25519",
5716 "x": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5717 }))
5718 }
5719
5720 fn sign(&self, _app_tag: &str, challenge: &[u8]) -> Result<Vec<u8>, String> {
5721 assert_eq!(challenge, b"openrtc:v2:test");
5722 Ok(vec![7; 64])
5723 }
5724
5725 fn delete(&self, _app_tag: &str) -> Result<(), String> {
5726 Ok(())
5727 }
5728
5729 fn read_secure_record(&self, app_tag: &str, key: &str) -> Result<Option<String>, String> {
5730 Ok(self
5731 .records
5732 .lock()
5733 .unwrap()
5734 .get(&format!("{app_tag}:{key}"))
5735 .cloned())
5736 }
5737
5738 fn write_secure_record(&self, app_tag: &str, key: &str, value: &str) -> Result<(), String> {
5739 self.records
5740 .lock()
5741 .unwrap()
5742 .insert(format!("{app_tag}:{key}"), value.to_string());
5743 Ok(())
5744 }
5745
5746 fn delete_secure_record(&self, app_tag: &str, key: &str) -> Result<(), String> {
5747 self.records
5748 .lock()
5749 .unwrap()
5750 .remove(&format!("{app_tag}:{key}"));
5751 Ok(())
5752 }
5753 }
5754
5755 impl TransportInstaller for FakeTransportInstaller {
5756 fn id(&self) -> &'static str {
5757 "ble"
5758 }
5759
5760 fn is_requested(&self, config: &openrtc::client::TransportConfig) -> bool {
5761 config.ble.as_ref().is_some_and(|ble| ble.enabled)
5762 }
5763
5764 fn install(
5765 &self,
5766 _client: Arc<openrtc::client::Client>,
5767 _config: openrtc::client::TransportConfig,
5768 _context: InstallContext,
5769 ) -> InstallFuture {
5770 self.installs.fetch_add(1, Ordering::SeqCst);
5771 Box::pin(async { Ok(Box::new(()) as Box<dyn std::any::Any + Send + Sync>) })
5772 }
5773 }
5774
5775 #[derive(Debug)]
5776 struct UnavailableTransportInstaller;
5777
5778 impl TransportInstaller for UnavailableTransportInstaller {
5779 fn id(&self) -> &'static str {
5780 "ble"
5781 }
5782
5783 fn is_requested(&self, config: &openrtc::client::TransportConfig) -> bool {
5784 config.ble.as_ref().is_some_and(|ble| ble.enabled)
5785 }
5786
5787 fn install(
5788 &self,
5789 _client: Arc<openrtc::client::Client>,
5790 _config: openrtc::client::TransportConfig,
5791 _context: InstallContext,
5792 ) -> InstallFuture {
5793 Box::pin(async { Err("Bluetooth hardware unavailable".to_string()) })
5794 }
5795 }
5796
5797 fn requested_ble_config() -> openrtc::client::TransportConfig {
5798 openrtc::client::TransportConfig {
5799 ble: Some(openrtc::client::BleConfig {
5800 enabled: true,
5801 ..openrtc::client::BleConfig::default()
5802 }),
5803 ..openrtc::client::TransportConfig::default()
5804 }
5805 }
5806
5807 fn test_config() -> OpenRtcTauriConfig {
5808 OpenRtcTauriConfig {
5809 api_key: format!("pk_test_{}", "a".repeat(40)),
5810 ..OpenRtcTauriConfig::default()
5811 }
5812 }
5813
5814 #[test]
5815 fn native_room_architecture_requires_fixed_intent_until_rust_owns_the_gateway_handle() {
5816 let mut registry = NativeCapabilityRegistry::default();
5817 registry
5818 .register_with_architecture(
5819 "room:adaptive".to_string(),
5820 "room".to_string(),
5821 "adaptive".to_string(),
5822 true,
5823 Some(openrtc::native::RoomArchitectureMode::Auto),
5824 )
5825 .expect("room registration");
5826 assert!(
5827 !registry.registrations["room:adaptive"].uses_sparse_fanout(),
5828 "caller-supplied auto state cannot promote sparse fanout",
5829 );
5830 registry
5831 .register_with_architecture(
5832 "room:fixed-sparse".to_string(),
5833 "room".to_string(),
5834 "fixed-sparse".to_string(),
5835 false,
5836 Some(openrtc::native::RoomArchitectureMode::Sparse),
5837 )
5838 .expect("fixed sparse room registration");
5839 assert!(registry.registrations["room:fixed-sparse"].uses_sparse_fanout());
5840 }
5841
5842 #[test]
5843 fn public_devices_capability_owns_the_matching_native_user_avenue() {
5844 let registration = NativeCapabilityRegistration {
5845 avenue_kind: "devices".to_string(),
5846 avenue_id: "principal-1".to_string(),
5847 sparse_fanout: false,
5848 requested_architecture: None,
5849 desired_revision: 0,
5850 desired_peers: Vec::new(),
5851 native_source: None,
5852 transfer_namespace: uuid::Uuid::new_v4().to_string(),
5853 transfer_demands: HashMap::new(),
5854 };
5855
5856 assert!(registration.owns_native_devices("principal-1"));
5857 assert!(!registration.owns_native_devices("principal-2"));
5858
5859 let mut wrong_kind = registration.clone();
5860 wrong_kind.avenue_kind = "user".to_string();
5861 assert!(!wrong_kind.owns_native_devices("principal-1"));
5862 }
5863
5864 #[test]
5865 fn native_room_architecture_rejects_non_room_and_respects_fixed_mesh_intent() {
5866 let mut registry = NativeCapabilityRegistry::default();
5867 assert!(registry
5868 .register_with_architecture(
5869 "space:not-room".to_string(),
5870 "space".to_string(),
5871 "not-room".to_string(),
5872 false,
5873 Some(openrtc::native::RoomArchitectureMode::Auto),
5874 )
5875 .is_err());
5876 registry
5877 .register_with_architecture(
5878 "room:fixed".to_string(),
5879 "room".to_string(),
5880 "fixed".to_string(),
5881 false,
5882 Some(openrtc::native::RoomArchitectureMode::Mesh),
5883 )
5884 .expect("fixed room registration");
5885 assert!(!registry.registrations["room:fixed"].uses_sparse_fanout());
5886 }
5887
5888 #[test]
5889 fn native_capability_registry_isolates_duplicate_peer_channels() {
5890 let mut registry = NativeCapabilityRegistry::default();
5891 registry
5892 .register(
5893 "devices:user-1".to_string(),
5894 "devices".to_string(),
5895 "user-1".to_string(),
5896 false,
5897 )
5898 .expect("devices registration");
5899 registry
5900 .register(
5901 "space:room-1".to_string(),
5902 "space".to_string(),
5903 "room-1".to_string(),
5904 false,
5905 )
5906 .expect("space registration");
5907 registry
5908 .registrations
5909 .get_mut("devices:user-1")
5910 .unwrap()
5911 .desired_peers = vec![serde_json::json!({
5912 "deviceId": "shared-peer",
5913 "nodeId": "device-route"
5914 })];
5915 registry
5916 .registrations
5917 .get_mut("space:room-1")
5918 .unwrap()
5919 .desired_peers = vec![serde_json::json!({
5920 "deviceId": "shared-peer",
5921 "nodeId": "space-route"
5922 })];
5923
5924 assert_eq!(
5925 registry.capability_keys_for_identity(
5926 Some("shared-peer"),
5927 Some("shared-peer"),
5928 None,
5929 None,
5930 ),
5931 vec!["devices:user-1".to_string(), "space:room-1".to_string()]
5932 );
5933
5934 let explicit_channel = openrtc::stream_metadata::ChannelMetadata {
5935 channel_id: openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID.to_string(),
5936 metadata: Some(serde_json::Map::from_iter([(
5937 "openrtcCapability".to_string(),
5938 serde_json::json!("space:room-1"),
5939 )])),
5940 };
5941 assert_eq!(
5942 registry.projected_stream_capability(&explicit_channel, &[]),
5943 Some("space:room-1".to_string())
5944 );
5945
5946 let unscoped_channel = openrtc::stream_metadata::ChannelMetadata {
5947 channel_id: openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID.to_string(),
5948 metadata: None,
5949 };
5950 assert_eq!(
5951 registry.projected_stream_capability(&unscoped_channel, &[]),
5952 None
5953 );
5954 }
5955
5956 #[test]
5957 fn native_capability_registry_routes_state_by_authenticated_scope() {
5958 let mut registry = NativeCapabilityRegistry::default();
5959 registry
5960 .register(
5961 "ticket:share-1".to_string(),
5962 "ticket".to_string(),
5963 "share-1".to_string(),
5964 false,
5965 )
5966 .expect("ticket registration");
5967 registry
5968 .register(
5969 "space:other".to_string(),
5970 "space".to_string(),
5971 "other".to_string(),
5972 false,
5973 )
5974 .expect("unrelated registration");
5975
5976 let snapshot = openrtc::client::StateSnapshot {
5977 connection_id: "mesh-connection".to_string(),
5978 device_id: Some("mesh-peer".to_string()),
5979 device_id_hint: None,
5980 remote_node_id: Some("mesh-node".to_string()),
5981 state: "connected".to_string(),
5982 transport_state: "connected".to_string(),
5983 protocol_state: "routable".to_string(),
5984 routable: true,
5985 logical_session_terminal: false,
5986 readiness_state: openrtc::client::ReadinessState::Routable,
5987 readiness_reason: "settled".to_string(),
5988 transport_generation: 1,
5989 route_generation: 1,
5990 active_transport_stable_id: Some(1),
5991 active_transport: "iroh".to_string(),
5992 parallel_transport: None,
5993 latency_ms: None,
5994 carrier: None,
5995 metrics_sampled_at_ms: None,
5996 moq_object_quantities: None,
5997 replacement_in_progress: false,
5998 last_lifecycle_transition_at_ms: 1,
5999 transition_count: 1,
6000 connecting_transition_count: 1,
6001 replacement_count: 0,
6002 retire_count: 0,
6003 last_disconnect_reason: None,
6004 last_reconnect_reason: None,
6005 scopes: vec![
6006 " unregistered:scope ".to_string(),
6007 " ticket:share-1 ".to_string(),
6008 "ticket:share-1".to_string(),
6009 ],
6010 error: None,
6011 created_at: 1,
6012 updated_at: 1,
6013 };
6014
6015 assert_eq!(
6016 registry.capability_keys_for_state(&snapshot),
6017 vec!["ticket:share-1".to_string()],
6018 "the registered authenticated scope routes the state without a desired-peer roster",
6019 );
6020 }
6021
6022 #[test]
6023 fn native_capability_registry_aggregates_without_duplicating_root_peers() {
6024 let mut registry = NativeCapabilityRegistry::default();
6025 for (key, kind, id) in [
6026 ("devices:user-1", "devices", "user-1"),
6027 ("space:room-1", "space", "room-1"),
6028 ] {
6029 registry
6030 .register(key.to_string(), kind.to_string(), id.to_string(), false)
6031 .expect("capability registration");
6032 registry.registrations.get_mut(key).unwrap().desired_peers =
6033 vec![serde_json::json!({"deviceId": "shared-peer", "ticket": "ticket-a"})];
6034 }
6035
6036 let (first_revision, peers_json) = registry.aggregate_desired_peers().unwrap();
6037 let peers: Vec<serde_json::Value> = serde_json::from_str(&peers_json).unwrap();
6038 assert_eq!(first_revision, 1);
6039 assert_eq!(peers.len(), 1, "one physical peer must be dialed once");
6040
6041 registry
6042 .register(
6043 "devices:user-1".to_string(),
6044 "devices".to_string(),
6045 "user-1".to_string(),
6046 false,
6047 )
6048 .expect("exact registration is idempotent");
6049 assert!(registry
6050 .register(
6051 "devices:user-1".to_string(),
6052 "space".to_string(),
6053 "room-1".to_string(),
6054 false,
6055 )
6056 .is_err());
6057 }
6058
6059 #[test]
6060 fn ticket_share_stream_projection_follows_rust_admission_scope() {
6061 let mut registry = NativeCapabilityRegistry::default();
6062 registry.register(
6063 "ticket:share_host_123".to_string(),
6064 "ticket".to_string(),
6065 "share_host_123".to_string(),
6066 false,
6067 ).unwrap();
6068 let admitted = openrtc::session_token::GrantScope("ticket:share_host_123".to_string());
6069 let wrong_scope = openrtc::session_token::GrantScope("ticket:other".to_string());
6070 let retired_scope = openrtc::session_token::GrantScope("share:share_host_123".to_string());
6071 let channel = openrtc::stream_metadata::ChannelMetadata {
6072 channel_id: "share/explicit-control".to_string(),
6073 metadata: Some(serde_json::Map::from_iter([(
6074 "openrtcCapability".to_string(),
6075 serde_json::json!("ticket:share_host_123"),
6076 )])),
6077 };
6078 assert_eq!(
6079 registry.projected_stream_capability(&channel, std::slice::from_ref(&admitted.0)),
6080 Some("ticket:share_host_123".to_string())
6081 );
6082 assert_eq!(
6083 registry.projected_stream_capability(&channel, std::slice::from_ref(&wrong_scope.0)),
6084 None
6085 );
6086 assert_eq!(registry.projected_stream_capability(&channel, &[]), None);
6087 assert_eq!(
6088 registry.projected_stream_capability(&channel, std::slice::from_ref(&retired_scope.0)),
6089 None
6090 );
6091 let mut file_channel = channel.clone();
6092 file_channel.channel_id = "share/explicit-file".to_string();
6093 assert_eq!(
6094 registry.projected_stream_capability(&file_channel, std::slice::from_ref(&admitted.0)),
6095 Some("ticket:share_host_123".to_string())
6096 );
6097 let mut untagged = channel.clone();
6098 untagged.metadata = None;
6099 assert_eq!(
6100 registry.projected_stream_capability(&untagged, std::slice::from_ref(&admitted.0)),
6101 Some("ticket:share_host_123".to_string())
6102 );
6103 let mut wrong_owner = channel;
6104 wrong_owner.metadata = Some(serde_json::Map::from_iter([(
6105 "openrtcCapability".to_string(),
6106 serde_json::json!("devices:user-1"),
6107 )]));
6108 assert_eq!(
6109 registry.projected_stream_capability(&wrong_owner, std::slice::from_ref(&admitted.0)),
6110 None
6111 );
6112 registry.registrations.clear();
6113 assert_eq!(
6114 registry.projected_stream_capability(&file_channel, std::slice::from_ref(&admitted.0)),
6115 None
6116 );
6117 }
6118
6119 #[test]
6120 fn native_capability_registry_merges_sparse_roles_deterministically() {
6121 fn aggregate(reverse_registration_order: bool) -> (u64, Vec<serde_json::Value>) {
6122 let mut registry = NativeCapabilityRegistry::default();
6123 let registrations = if reverse_registration_order {
6124 [
6125 ("space:backup", "space", "backup"),
6126 ("room:active", "room", "active"),
6127 ]
6128 } else {
6129 [
6130 ("room:active", "room", "active"),
6131 ("space:backup", "space", "backup"),
6132 ]
6133 };
6134 for (key, kind, id) in registrations {
6135 registry
6136 .register(key.to_string(), kind.to_string(), id.to_string(), false)
6137 .unwrap();
6138 }
6139 registry
6140 .registrations
6141 .get_mut("room:active")
6142 .unwrap()
6143 .desired_peers = vec![serde_json::json!({
6144 "deviceId": "shared-peer",
6145 "ticket": "active-ticket",
6146 "topologyRole": "active",
6147 "topologyRevision": 3,
6148 })];
6149 registry
6150 .registrations
6151 .get_mut("space:backup")
6152 .unwrap()
6153 .desired_peers = vec![serde_json::json!({
6154 "deviceId": "shared-peer",
6155 "ticket": "backup-ticket",
6156 "topologyRole": "backup",
6157 "topologyRevision": 100,
6158 })];
6159 let (revision, peers_json) = registry.aggregate_desired_peers().unwrap();
6160 (revision, serde_json::from_str(&peers_json).unwrap())
6161 }
6162
6163 let forward = aggregate(false);
6164 let reverse = aggregate(true);
6165 assert_eq!(
6166 forward, reverse,
6167 "registration order is not lifecycle authority"
6168 );
6169 assert_eq!(forward.0, 1);
6170 assert_eq!(forward.1.len(), 1);
6171 assert_eq!(forward.1[0]["ticket"], "active-ticket");
6172 assert_eq!(forward.1[0]["topologyRole"], "active");
6173 assert_eq!(forward.1[0]["topologyRevision"], 1);
6174 }
6175
6176 #[test]
6177 fn native_capability_registry_preserves_scoped_automatic_intent_with_observed_reference() {
6178 let mut registry = NativeCapabilityRegistry::default();
6179 for (key, kind) in [("room:a", "room"), ("space:b", "space"), ("devices", "devices")] {
6180 registry
6181 .register(key.to_string(), kind.to_string(), key.to_string(), false)
6182 .unwrap();
6183 }
6184 registry
6185 .registrations
6186 .get_mut("room:a")
6187 .unwrap()
6188 .desired_peers = vec![serde_json::json!({
6189 "deviceId": "shared-peer",
6190 "topologyRole": "backup",
6191 "topologyRevision": 41,
6192 })];
6193 registry
6194 .registrations
6195 .get_mut("space:b")
6196 .unwrap()
6197 .desired_peers = vec![serde_json::json!({
6198 "deviceId": "shared-peer",
6199 "nodeId": "shared-node",
6200 "ticket": "space-ticket",
6201 "topologyRole": "active",
6202 "topologyRevision": 7,
6203 })];
6204
6205 registry.registrations.get_mut("devices").unwrap().desired_peers = vec![serde_json::json!({
6206 "deviceId":"shared-peer", "nodeId":"owned-node", "ticket":"owned-ticket", "connectionIntent":"observe",
6207 })];
6208 let (_, both_json) = registry.aggregate_desired_peers().unwrap();
6209 let both: Vec<serde_json::Value> = serde_json::from_str(&both_json).unwrap();
6210 assert_eq!(both.len(), 1);
6211 assert_eq!(both[0]["topologyRole"], "active");
6212 assert_eq!(both[0]["ticket"], "space-ticket");
6213 assert_eq!(
6214 registry.registrations["room:a"].desired_peers[0]["topologyRevision"], 41,
6215 "root projection must not rewrite avenue-local lease state",
6216 );
6217
6218 registry
6219 .registrations
6220 .get_mut("space:b")
6221 .unwrap()
6222 .desired_peers
6223 .clear();
6224 let (_, room_only_json) = registry.aggregate_desired_peers().unwrap();
6225 let room_only: Vec<serde_json::Value> = serde_json::from_str(&room_only_json).unwrap();
6226 assert_eq!(
6227 room_only.len(),
6228 1,
6229 "one remaining capability keeps the leg desired"
6230 );
6231 assert_eq!(room_only[0]["topologyRole"], "backup");
6232
6233 registry
6234 .registrations
6235 .get_mut("room:a")
6236 .unwrap()
6237 .desired_peers
6238 .clear();
6239 let (_, empty_json) = registry.aggregate_desired_peers().unwrap();
6240 let empty: Vec<serde_json::Value> = serde_json::from_str(&empty_json).unwrap();
6241 assert_eq!(empty.len(), 1, "observing membership remains after scoped automatic references withdraw");
6242 assert_eq!(empty[0]["connectionIntent"], "observe");
6243 assert_eq!(empty[0]["ticket"], "owned-ticket");
6244 registry.registrations.get_mut("devices").unwrap().desired_peers.clear();
6245 let (_, empty_json) = registry.aggregate_desired_peers().unwrap();
6246 assert_eq!(empty_json, "[]");
6247 }
6248
6249 #[test]
6250 fn native_ticket_projection_requires_the_exact_live_scope() {
6251 let mut registry = NativeCapabilityRegistry::default();
6252 for id in ["a", "b"] {
6253 registry
6254 .register(format!("ticket:{id}"), "ticket".into(), id.into(), false)
6255 .unwrap();
6256 }
6257 let both = vec!["ticket:a".to_string(), "ticket:b".to_string()];
6258 let only_a = vec!["ticket:a".to_string()];
6259 let event = openrtc::client::NativePeerDataEvent {
6260 connection_id: "same-peer".into(),
6261 remote_node_id: None,
6262 transport: "iroh".into(),
6263 transport_stable_id: 1,
6264 transport_generation: 1,
6265 route_generation: 1,
6266 payload: serde_json::to_vec(&serde_json::json!({
6267 "capability": "ticket:b", "kind": "raw", "payload": [1]
6268 }))
6269 .unwrap(),
6270 };
6271 assert_eq!(
6272 registry.capability_keys_for_peer_data(&event, &both),
6273 ["ticket:b"]
6274 );
6275 assert!(registry
6276 .capability_keys_for_peer_data(&event, &only_a)
6277 .is_empty());
6278 assert!(registry
6279 .capability_keys_for_peer_data(&event, &[])
6280 .is_empty());
6281 for id in ["a", "b"] {
6284 let key = format!("ticket:{id}");
6285 let channel = openrtc::stream_metadata::ChannelMetadata {
6286 channel_id: "share/explicit-control".into(),
6287 metadata: Some(serde_json::Map::from_iter([(
6288 "openrtcCapability".into(),
6289 serde_json::json!(key),
6290 )])),
6291 };
6292 assert_eq!(
6293 registry.projected_stream_capability(&channel, &both),
6294 Some(key.clone())
6295 );
6296 assert_eq!(
6297 registry.projected_stream_capability(&channel, &only_a),
6298 (id == "a").then_some(key)
6299 );
6300 }
6301 let untagged = openrtc::stream_metadata::ChannelMetadata {
6302 channel_id: "share/explicit-control".into(),
6303 metadata: None,
6304 };
6305 assert!(registry
6306 .projected_stream_capability(&untagged, &both)
6307 .is_none());
6308 assert_eq!(
6309 registry.projected_stream_capability(&untagged, &only_a),
6310 Some("ticket:a".into())
6311 );
6312 }
6313
6314 #[test]
6315 fn native_capability_registry_scopes_peer_data_and_rejects_unscoped_streams() {
6316 let mut registry = NativeCapabilityRegistry::default();
6317 registry
6318 .register(
6319 "devices:user-1".to_string(),
6320 "devices".to_string(),
6321 "user-1".to_string(),
6322 false,
6323 )
6324 .unwrap();
6325 registry
6326 .registrations
6327 .get_mut("devices:user-1")
6328 .unwrap()
6329 .desired_peers = vec![serde_json::json!({"deviceId": "peer-1"})];
6330
6331 let explicit = openrtc::client::NativePeerDataEvent {
6332 connection_id: "peer-1".to_string(),
6333 remote_node_id: None,
6334 transport: "webrtc".to_string(),
6335 transport_stable_id: 1,
6336 transport_generation: 1,
6337 route_generation: 1,
6338 payload: serde_json::to_vec(&serde_json::json!({
6339 "capability": "devices:user-1",
6340 "kind": "raw",
6341 "payload": [1, 2, 3]
6342 }))
6343 .unwrap(),
6344 };
6345 assert_eq!(
6346 registry.capability_keys_for_peer_data(&explicit, &[]),
6347 vec!["devices:user-1".to_string()]
6348 );
6349
6350 let unknown = openrtc::client::NativePeerDataEvent {
6351 payload: serde_json::to_vec(&serde_json::json!({
6352 "capability": "space:unknown",
6353 "kind": "raw"
6354 }))
6355 .unwrap(),
6356 ..explicit.clone()
6357 };
6358 assert!(registry
6359 .capability_keys_for_peer_data(&unknown, &[])
6360 .is_empty());
6361
6362 registry
6363 .register(
6364 "space:shared".to_string(),
6365 "space".to_string(),
6366 "shared".to_string(),
6367 false,
6368 )
6369 .unwrap();
6370 registry
6371 .registrations
6372 .get_mut("space:shared")
6373 .unwrap()
6374 .desired_peers = vec![serde_json::json!({"deviceId": "peer-1"})];
6375 let unscoped = openrtc::client::NativePeerDataEvent {
6376 payload: serde_json::to_vec(&serde_json::json!({
6377 "kind": "raw",
6378 "payload": [1, 2, 3]
6379 }))
6380 .unwrap(),
6381 ..explicit.clone()
6382 };
6383 assert!(
6384 registry
6385 .capability_keys_for_peer_data(&unscoped, &[])
6386 .is_empty(),
6387 "a shared physical peer cannot fan unscoped data into two avenues",
6388 );
6389
6390 let unscoped_channel = openrtc::stream_metadata::ChannelMetadata {
6391 channel_id: openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID.to_string(),
6392 metadata: None,
6393 };
6394 assert_eq!(
6395 registry.projected_stream_capability(&unscoped_channel, &[]),
6396 None,
6397 "stream ownership must never be inferred from capability count"
6398 );
6399 }
6400
6401 #[test]
6402 fn native_device_proof_uses_host_signer_without_exporting_private_key() {
6403 let state = OpenRtcTauriState::new(test_config())
6404 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6405 assert_eq!(
6406 state
6407 .native_device_key_signer
6408 .as_deref()
6409 .unwrap()
6410 .offline_assurance("app_native_test"),
6411 openrtc::offline::OfflineAssurance::Software
6412 );
6413 let public = state
6414 .device_public_key("app_native_test")
6415 .expect("public key");
6416 assert_eq!(public["kty"], "OKP");
6417 assert_eq!(public["crv"], "Ed25519");
6418 assert!(public.get("d").is_none());
6419 let signature = state
6420 .sign_device_proof("app_native_test", "openrtc:v2:test")
6421 .expect("signature");
6422 assert_eq!(
6423 base64::engine::general_purpose::URL_SAFE_NO_PAD
6424 .decode(signature)
6425 .expect("base64"),
6426 vec![7; 64]
6427 );
6428 }
6429
6430 #[tokio::test]
6431 async fn native_identity_reload_preserves_owner_and_principal_change_retires_it() {
6432 use openrtc::native::AssertionProvider;
6433 let state = OpenRtcTauriState::new(test_config())
6434 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6435 let first = state
6436 .configure_native_identity("main", "first-user".into(), |_| Ok(()))
6437 .await
6438 .unwrap();
6439 let provider = state
6440 .native_identity
6441 .lock()
6442 .await
6443 .as_ref()
6444 .unwrap()
6445 .assertions
6446 .clone();
6447 let repeated = state
6448 .configure_native_identity("main", "first-user".into(), |_| Ok(()))
6449 .await
6450 .unwrap();
6451 assert_eq!(first, repeated);
6452 assert!(Arc::ptr_eq(
6453 &provider,
6454 &state
6455 .native_identity
6456 .lock()
6457 .await
6458 .as_ref()
6459 .unwrap()
6460 .assertions
6461 ));
6462 assert_eq!(provider.identity_epoch(), 0);
6463 assert!(state
6464 .configure_native_identity("other-window", "second-user".into(), |_| Ok(()))
6465 .await
6466 .is_err());
6467 assert_eq!(
6468 provider.identity_epoch(),
6469 0,
6470 "competing window cannot retire the owner"
6471 );
6472 let second = state
6473 .configure_native_identity("main", "second-user".into(), |_| Ok(()))
6474 .await
6475 .unwrap();
6476 assert_ne!(first, second);
6477 assert_eq!(provider.identity_epoch(), 1);
6478 assert!(provider.session_key().unwrap().is_none());
6479 let owner = state.native_identity.lock().await;
6480 assert_eq!(
6481 owner
6482 .as_ref()
6483 .unwrap()
6484 .assertions
6485 .session_key()
6486 .unwrap()
6487 .as_deref(),
6488 Some("second-user")
6489 );
6490 let next_provider = owner.as_ref().unwrap().assertions.clone();
6491 drop(owner);
6492 assert!(!state.clear_native_identity("main", &first).await.unwrap());
6493 assert!(state
6494 .clear_native_identity("other-window", &second)
6495 .await
6496 .is_err());
6497 assert_eq!(next_provider.identity_epoch(), 0);
6498 assert!(state.clear_native_identity("main", &second).await.unwrap());
6499 assert_eq!(next_provider.identity_epoch(), 1);
6500 assert!(state.native_identity.lock().await.is_none());
6501 }
6502
6503 #[tokio::test]
6504 async fn native_roster_feeds_root_and_fences_replaced_source() {
6505 let state = OpenRtcTauriState::new(test_config())
6506 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6507 let old_source = state
6508 .configure_native_identity("main", "first-user".into(), |_| Ok(()))
6509 .await
6510 .unwrap();
6511 let client = state.client();
6512 client
6513 .start_external_auto_connect(
6514 format!("{}:native-root", client.app_tag()),
6515 "local".into(),
6516 )
6517 .await
6518 .unwrap();
6519 {
6520 let mut registry = state.capability_registry.lock().await;
6521 registry
6522 .register("devices".into(), "user".into(), "principal".into(), false)
6523 .unwrap();
6524 registry
6525 .register("space".into(), "space".into(), "shared-space".into(), false)
6526 .unwrap();
6527 registry
6528 .registrations
6529 .get_mut("devices")
6530 .unwrap()
6531 .native_source = Some(NativeRosterSource::Active(old_source.clone()));
6532 registry
6533 .registrations
6534 .get_mut("space")
6535 .unwrap()
6536 .desired_peers = vec![serde_json::json!({
6537 "deviceId":"space-peer", "online":false,
6538 })];
6539 }
6540 let devices = ["local", "known-offline"]
6541 .into_iter()
6542 .map(|id| {
6543 serde_json::from_value::<openrtc::signaling::Device>(serde_json::json!({
6544 "deviceId":id, "deviceName":"Fixture", "online":false,
6545 }))
6546 .unwrap()
6547 })
6548 .collect::<Vec<_>>();
6549 assert!(submit_native_device_snapshot(
6550 &client,
6551 &state.capability_registry,
6552 &old_source,
6553 "devices",
6554 "local",
6555 &devices,
6556 true
6557 )
6558 .await
6559 .unwrap());
6560 {
6561 let mut registry = state.capability_registry.lock().await;
6562 let peers = ®istry.registrations["devices"].desired_peers;
6563 assert_eq!(peers.len(), 1);
6564 assert_eq!(peers[0]["deviceId"], "known-offline");
6565 assert_eq!(
6566 peers[0]["online"], false,
6567 "offline inventory remains a typed actor input"
6568 );
6569 let (_, aggregate) = registry.aggregate_desired_peers().unwrap();
6570 let aggregate: Vec<serde_json::Value> = serde_json::from_str(&aggregate).unwrap();
6571 assert_eq!(
6572 aggregate.len(),
6573 2,
6574 "native user roster must preserve another avenue"
6575 );
6576 }
6577 let new_source = state
6578 .configure_native_identity("main", "second-user".into(), |_| Ok(()))
6579 .await
6580 .unwrap();
6581 {
6582 let mut registry = state.capability_registry.lock().await;
6583 assert!(matches!(
6584 registry.registrations["devices"].native_source,
6585 Some(NativeRosterSource::Retired)
6586 ));
6587 assert!(registry.registrations["devices"].desired_peers.is_empty());
6588 registry
6589 .registrations
6590 .get_mut("devices")
6591 .unwrap()
6592 .native_source = Some(NativeRosterSource::Active(new_source.clone()));
6593 }
6594 assert!(!submit_native_device_snapshot(
6595 &client,
6596 &state.capability_registry,
6597 &old_source,
6598 "devices",
6599 "local",
6600 &[],
6601 true
6602 )
6603 .await
6604 .unwrap());
6605 let mut fleet_devices = devices.clone();
6606 for index in 0..499 {
6607 fleet_devices.push(serde_json::from_value::<openrtc::signaling::Device>(serde_json::json!({
6608 "deviceId":format!("fleet-{index}"), "deviceName":"Observed fixture", "online":false,
6609 })).unwrap());
6610 }
6611 assert!(submit_native_device_snapshot(
6612 &client,
6613 &state.capability_registry,
6614 &new_source,
6615 "devices",
6616 "local",
6617 &fleet_devices,
6618 false
6619 )
6620 .await
6621 .unwrap());
6622 {
6623 let registry = state.capability_registry.lock().await;
6624 assert_eq!(registry.registrations["devices"].desired_peers.len(), 500);
6625 assert!(registry.registrations["devices"].desired_peers.iter().all(|peer| peer["connectionIntent"] == "observe"));
6626 assert_eq!(registry.registrations["space"].desired_peers.len(), 1);
6627 }
6628 state
6629 .clear_native_identity("main", &new_source)
6630 .await
6631 .unwrap();
6632 assert!(!submit_native_device_snapshot(
6633 &client,
6634 &state.capability_registry,
6635 &new_source,
6636 "devices",
6637 "local",
6638 &devices,
6639 true
6640 )
6641 .await
6642 .unwrap());
6643 client.stop_external_auto_connect().await;
6644 }
6645
6646 #[tokio::test]
6647 async fn native_identity_retirement_after_actor_stop_allows_new_sign_in() {
6648 let state = OpenRtcTauriState::new(test_config())
6649 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6650 let old_id = state.configure_native_identity("main", "old-user".into(), |_| Ok(()))
6651 .await.unwrap();
6652 {
6653 let mut registry = state.capability_registry.lock().await;
6654 registry.register("devices:old-user".into(), "devices".into(), "old-user".into(), false).unwrap();
6655 let registration = registry.registrations.get_mut("devices:old-user").unwrap();
6656 registration.native_source = Some(NativeRosterSource::Active(old_id.clone()));
6657 registration.desired_peers = vec![serde_json::json!({"deviceId":"old-peer", "online":false})];
6658 }
6659 state.client().stop_external_auto_connect().await;
6660 let next_id = state.configure_native_identity("main", "new-user".into(), |_| Ok(()))
6661 .await.expect("retirement must be idempotent after the actor has stopped");
6662 assert_ne!(old_id, next_id);
6663 let registry = state.capability_registry.lock().await;
6664 let old = ®istry.registrations["devices:old-user"];
6665 assert!(matches!(old.native_source, Some(NativeRosterSource::Retired)));
6666 assert!(old.desired_peers.is_empty());
6667 assert!(state.client().submit_external_desired_peers(999, "[]").await.is_err(),
6668 "cleanup must not restart the actor or make startup submission permissive");
6669 }
6670
6671 #[test]
6672 fn offline_support_reports_signer_and_compiled_lan_truth() {
6673 let unavailable = OpenRtcTauriState::new(test_config()).offline_runtime_support();
6674 assert!(!unavailable.provisioning);
6675 assert_eq!(unavailable.local_mesh, cfg!(feature = "transport-lan"));
6676 assert_eq!(
6677 unavailable.reason,
6678 Some("native host device signer is unavailable")
6679 );
6680
6681 let available = OpenRtcTauriState::new(test_config())
6682 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()))
6683 .offline_runtime_support();
6684 assert!(available.provisioning);
6685 assert_eq!(available.local_mesh, cfg!(feature = "transport-lan"));
6686 assert_eq!(
6687 available.reason,
6688 (!cfg!(feature = "transport-lan"))
6689 .then_some("native host was built without transport-lan"),
6690 );
6691 }
6692
6693 #[tokio::test]
6694 async fn public_runtime_status_uses_installed_host_facts() {
6695 use openrtc::client::CapabilityMaturity;
6696
6697 let unavailable_state = OpenRtcTauriState::new(test_config());
6698 let unavailable = unavailable_state
6699 .project_public_runtime_status(unavailable_state.client().runtime_status().await);
6700 assert_eq!(
6701 unavailable.product_maturity.offline_edge,
6702 CapabilityMaturity::Unavailable
6703 );
6704 assert_eq!(
6705 unavailable.product_maturity.broadcast,
6706 CapabilityMaturity::Unavailable
6707 );
6708 let serialized = serde_json::to_value(&unavailable).expect("serialized runtime status");
6709 assert_eq!(serialized["productMaturity"]["offlineEdge"], "unavailable");
6710 assert_eq!(serialized["productMaturity"]["broadcast"], "unavailable");
6711
6712 let installed_state = OpenRtcTauriState::new(test_config())
6713 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6714 let installed = installed_state
6715 .project_public_runtime_status(installed_state.client().runtime_status().await);
6716 assert_eq!(
6717 installed.product_maturity.offline_edge,
6718 if cfg!(feature = "transport-lan") {
6719 CapabilityMaturity::Preview
6720 } else {
6721 CapabilityMaturity::SupportOnly
6722 }
6723 );
6724 assert_eq!(
6725 installed.product_maturity.broadcast,
6726 if cfg!(feature = "native-broadcast-moq") {
6727 CapabilityMaturity::Preview
6728 } else {
6729 CapabilityMaturity::Unavailable
6730 }
6731 );
6732 }
6733
6734 #[test]
6735 fn native_certificate_records_round_trip_through_host_secure_storage() {
6736 let state = OpenRtcTauriState::new(test_config())
6737 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6738 let app_tag = "app_native_test";
6739 let key = "openrtc:v2:device-session:abc:device-1";
6740 assert_eq!(state.read_secure_record(app_tag, key).unwrap(), None);
6741 state
6742 .write_secure_record(app_tag, key, "{\"token\":\"bound\"}")
6743 .unwrap();
6744 assert_eq!(
6745 state.read_secure_record(app_tag, key).unwrap().as_deref(),
6746 Some("{\"token\":\"bound\"}")
6747 );
6748 state.delete_secure_record(app_tag, key).unwrap();
6749 assert_eq!(state.read_secure_record(app_tag, key).unwrap(), None);
6750 }
6751
6752 #[test]
6753 fn native_device_proof_fails_closed_without_secure_host_signer() {
6754 let state = OpenRtcTauriState::new(test_config());
6755 assert!(state
6756 .device_public_key("app_native_test")
6757 .expect_err("missing signer must fail")
6758 .contains("secure-storage signer"));
6759 }
6760
6761 fn native_transport_install_context() -> InstallContext {
6762 InstallContext {
6763 data_dir: std::env::temp_dir().join("openrtc-tauri-native-transport-tests"),
6764 }
6765 }
6766
6767 fn managed_session_test_result(device_id: &str) -> StartSessionResult {
6768 StartSessionResult {
6769 local_node_id: format!("node-{device_id}"),
6770 ticket_scope: Some("user-device".to_string()),
6771 ticket: None,
6772 presence_started: true,
6773 auto_connect_started: true,
6774 local_device: openrtc::native_device::NativeDeviceIdentity {
6775 device_id: device_id.to_string(),
6776 device_name: "Test Device".to_string(),
6777 created_at_ms: 1,
6778 updated_at_ms: 1,
6779 name_source: None,
6780 system_info: None,
6781 },
6782 }
6783 }
6784
6785 async fn simulate_managed_session_start(
6786 state: Arc<OpenRtcTauriState>,
6787 device_id: String,
6788 pause: Option<(oneshot::Sender<()>, oneshot::Receiver<()>)>,
6789 starts: Arc<AtomicUsize>,
6790 stopped_owners: Arc<Mutex<Vec<usize>>>,
6791 ) -> SessionDisposition {
6792 let _start_guard = state.managed_session_start_guard.lock().await;
6793 let client = state.client();
6794 let key = format!("{}:{device_id}", client.app_tag());
6795 let (disposition, active_epoch) = {
6796 let active = state.managed_session.lock().await;
6797 let disposition = managed_session_disposition(
6798 active.as_ref().map(|record| record.key.as_str()),
6799 active
6800 .as_ref()
6801 .is_some_and(|record| Arc::ptr_eq(&record.owner_client, &client)),
6802 active
6803 .as_ref()
6804 .is_some_and(|record| record.result.presence_started),
6805 active
6806 .as_ref()
6807 .is_some_and(|record| record.result.auto_connect_started),
6808 &key,
6809 true,
6810 true,
6811 );
6812 (
6813 disposition,
6814 active.as_ref().map(|record| record.owner_epoch),
6815 )
6816 };
6817
6818 if disposition == SessionDisposition::Reuse {
6819 return disposition;
6820 }
6821
6822 if disposition == SessionDisposition::Replace {
6823 let previous = state
6824 .managed_session
6825 .lock()
6826 .await
6827 .take()
6828 .expect("replacement must have a previous owner");
6829 stopped_owners
6830 .lock()
6831 .await
6832 .push(Arc::as_ptr(&previous.owner_client) as usize);
6833 }
6834
6835 starts.fetch_add(1, Ordering::SeqCst);
6836 if let Some((entered, release)) = pause {
6837 entered.send(()).expect("start observer must be waiting");
6838 release.await.expect("start release must be sent");
6839 }
6840
6841 let owner_epoch = match disposition {
6842 SessionDisposition::Refresh => {
6843 active_epoch.expect("refresh must preserve the active owner epoch")
6844 }
6845 SessionDisposition::Start | SessionDisposition::Replace => {
6846 state.allocate_managed_session_owner_epoch()
6847 }
6848 SessionDisposition::Reuse => unreachable!("reuse returned before startup"),
6849 };
6850 let result = managed_session_test_result(&device_id);
6851 *state.managed_session.lock().await = Some(ManagedSessionRecord {
6852 key,
6853 owner_client: client,
6854 owner_epoch,
6855 result,
6856 });
6857 disposition
6858 }
6859
6860 #[tokio::test]
6861 async fn concurrent_same_key_start_reuses_one_owner_epoch() {
6862 let state = Arc::new(OpenRtcTauriState::new(test_config()));
6863 let starts = Arc::new(AtomicUsize::new(0));
6864 let stopped_owners = Arc::new(Mutex::new(Vec::new()));
6865 let (first_entered_tx, first_entered_rx) = oneshot::channel();
6866 let (first_release_tx, first_release_rx) = oneshot::channel();
6867
6868 let first = tokio::spawn(simulate_managed_session_start(
6869 state.clone(),
6870 "same-device".to_string(),
6871 Some((first_entered_tx, first_release_rx)),
6872 starts.clone(),
6873 stopped_owners.clone(),
6874 ));
6875 first_entered_rx.await.expect("first start must pause");
6876
6877 let (second_attempting_tx, second_attempting_rx) = oneshot::channel();
6878 let second_state = state.clone();
6879 let second_starts = starts.clone();
6880 let second_stopped_owners = stopped_owners.clone();
6881 let second = tokio::spawn(async move {
6882 second_attempting_tx.send(()).unwrap();
6883 simulate_managed_session_start(
6884 second_state,
6885 "same-device".to_string(),
6886 None,
6887 second_starts,
6888 second_stopped_owners,
6889 )
6890 .await
6891 });
6892 second_attempting_rx.await.unwrap();
6893 tokio::task::yield_now().await;
6894 assert_eq!(starts.load(Ordering::SeqCst), 1);
6895
6896 first_release_tx.send(()).unwrap();
6897 assert_eq!(
6898 first.await.expect("first start task"),
6899 SessionDisposition::Start
6900 );
6901 assert_eq!(
6902 second.await.expect("second start task"),
6903 SessionDisposition::Reuse
6904 );
6905 assert_eq!(starts.load(Ordering::SeqCst), 1);
6906
6907 let active = state
6908 .managed_session
6909 .lock()
6910 .await
6911 .clone()
6912 .expect("active owner");
6913 assert_eq!(active.owner_epoch, 1);
6914 assert!(Arc::ptr_eq(&active.owner_client, &state.client()));
6915 assert!(stopped_owners.lock().await.is_empty());
6916 }
6917
6918 #[tokio::test]
6919 async fn concurrent_different_key_start_replaces_prior_owner_without_late_overwrite() {
6920 let state = Arc::new(OpenRtcTauriState::new(test_config()));
6921 let starts = Arc::new(AtomicUsize::new(0));
6922 let stopped_owners = Arc::new(Mutex::new(Vec::new()));
6923 let (first_entered_tx, first_entered_rx) = oneshot::channel();
6924 let (first_release_tx, first_release_rx) = oneshot::channel();
6925
6926 let first = tokio::spawn(simulate_managed_session_start(
6927 state.clone(),
6928 "first-device".to_string(),
6929 Some((first_entered_tx, first_release_rx)),
6930 starts.clone(),
6931 stopped_owners.clone(),
6932 ));
6933 first_entered_rx.await.expect("first start must pause");
6934 let first_owner = state.client();
6935
6936 let (second_attempting_tx, second_attempting_rx) = oneshot::channel();
6937 let second_state = state.clone();
6938 let second_starts = starts.clone();
6939 let second_stopped_owners = stopped_owners.clone();
6940 let second = tokio::spawn(async move {
6941 second_attempting_tx.send(()).unwrap();
6942 simulate_managed_session_start(
6943 second_state,
6944 "second-device".to_string(),
6945 None,
6946 second_starts,
6947 second_stopped_owners,
6948 )
6949 .await
6950 });
6951 second_attempting_rx.await.unwrap();
6952 tokio::task::yield_now().await;
6953 assert!(Arc::ptr_eq(&first_owner, &state.client()));
6954
6955 first_release_tx.send(()).unwrap();
6956 assert_eq!(
6957 first.await.expect("first start task"),
6958 SessionDisposition::Start
6959 );
6960 assert_eq!(
6961 second.await.expect("replacement start task"),
6962 SessionDisposition::Replace
6963 );
6964 assert_eq!(starts.load(Ordering::SeqCst), 2);
6965
6966 let active = state
6967 .managed_session
6968 .lock()
6969 .await
6970 .clone()
6971 .expect("active owner");
6972 assert_eq!(active.owner_epoch, 2);
6973 assert!(active.key.contains("second-device"));
6974 assert!(Arc::ptr_eq(&active.owner_client, &state.client()));
6975 assert_eq!(
6976 stopped_owners.lock().await.as_slice(),
6977 [Arc::as_ptr(&first_owner) as usize]
6978 );
6979 assert!(Arc::ptr_eq(&active.owner_client, &first_owner));
6980 }
6981
6982 #[test]
6983 fn derives_app_tag_from_api_key_by_default() {
6984 let config = OpenRtcTauriConfig {
6985 api_key: format!("pk_test_{}", "b".repeat(24) + "1234567890abcdef"),
6986 ..OpenRtcTauriConfig::default()
6987 };
6988
6989 assert_eq!(config.app_tag().unwrap(), "app_1234567890abcdef");
6990 }
6991
6992 #[test]
6993 fn native_testing_endpoint_pair_is_owned_by_the_plugin_state() {
6994 let state = OpenRtcTauriState::new(test_config())
6995 .with_testing_endpoints("http://127.0.0.1:5004", "http://127.0.0.1:8787");
6996
6997 assert_eq!(
6998 state.testing_endpoints,
6999 Some((
7000 "http://127.0.0.1:5004".to_string(),
7001 "http://127.0.0.1:8787".to_string(),
7002 )),
7003 );
7004 }
7005
7006 #[tokio::test]
7007 async fn requested_native_transport_without_installer_keeps_base_route_available() {
7008 let state = OpenRtcTauriState::new(test_config());
7009 let client = state.client();
7010 ensure_requested_native_transports(
7011 &state,
7012 &client,
7013 Some(&requested_ble_config()),
7014 &native_transport_install_context(),
7015 )
7016 .await
7017 .expect("an unavailable optional transport must not fail base Iroh startup");
7018
7019 assert!(state.installed_native_transports.lock().await.is_empty());
7020 }
7021
7022 #[tokio::test]
7023 async fn native_transport_installer_is_idempotent_for_one_client() {
7024 let installs = Arc::new(AtomicUsize::new(0));
7025 let state = OpenRtcTauriState::new(test_config()).with_native_transport_installer(
7026 Arc::new(FakeTransportInstaller {
7027 installs: installs.clone(),
7028 }),
7029 );
7030 let client = state.client();
7031 let config = requested_ble_config();
7032
7033 ensure_requested_native_transports(
7034 &state,
7035 &client,
7036 Some(&config),
7037 &native_transport_install_context(),
7038 )
7039 .await
7040 .expect("first install");
7041 ensure_requested_native_transports(
7042 &state,
7043 &client,
7044 Some(&config),
7045 &native_transport_install_context(),
7046 )
7047 .await
7048 .expect("idempotent install");
7049
7050 assert_eq!(installs.load(Ordering::SeqCst), 1);
7051 }
7052
7053 #[tokio::test]
7054 async fn unavailable_native_transport_keeps_base_route_available() {
7055 let state = OpenRtcTauriState::new(test_config())
7056 .with_native_transport_installer(Arc::new(UnavailableTransportInstaller));
7057 let client = state.client();
7058
7059 ensure_requested_native_transports(
7060 &state,
7061 &client,
7062 Some(&requested_ble_config()),
7063 &native_transport_install_context(),
7064 )
7065 .await
7066 .expect("optional transport installation failure must not fail base Iroh startup");
7067
7068 assert!(state.installed_native_transports.lock().await.is_empty());
7069 }
7070
7071 #[test]
7072 fn invalid_or_missing_api_key_fails_closed() {
7073 assert!(OpenRtcTauriConfig::default().validated_api_key().is_err());
7074 let mut config = OpenRtcTauriConfig::default();
7075 config.api_key = "pk_test_short".to_string();
7076 assert!(config.validated_api_key().is_err());
7077 }
7078
7079 #[cfg(feature = "native-broadcast-moq")]
7080 #[test]
7081 fn draft16_broadcast_feature_installs_one_private_native_adapter() {
7082 let state = OpenRtcTauriState::new(test_config());
7083 assert!(state.native_broadcast_adapter.is_some());
7084 assert!(state.native_broadcast_moq_adapter.is_some());
7085 }
7086
7087 #[test]
7088 fn from_env_uses_api_key_and_ignores_legacy_namespace_selectors() {
7089 let previous_api_key = std::env::var("VITE_OPENRTC_API_KEY").ok();
7090 let previous_project = std::env::var("VITE_OPENRTC_PROJECT_ID").ok();
7091 let previous_app_tag = std::env::var("VITE_PLUTO_OPENRTC_APP_TAG").ok();
7092 let api_key = format!("pk_test_{}", "c".repeat(40));
7093 std::env::set_var("VITE_OPENRTC_API_KEY", &api_key);
7094 std::env::set_var("VITE_OPENRTC_PROJECT_ID", "pluto-rtc-prod");
7095 std::env::set_var("VITE_PLUTO_OPENRTC_APP_TAG", "app_from_vite_env");
7096
7097 let config = OpenRtcTauriConfig::from_env();
7098
7099 assert_eq!(config.api_key, api_key);
7100 assert_eq!(
7101 config.app_tag().unwrap(),
7102 openrtc::app_tag_from_api_key(&api_key)
7103 );
7104
7105 match previous_api_key {
7106 Some(value) => std::env::set_var("VITE_OPENRTC_API_KEY", value),
7107 None => std::env::remove_var("VITE_OPENRTC_API_KEY"),
7108 }
7109 match previous_project {
7110 Some(value) => std::env::set_var("VITE_OPENRTC_PROJECT_ID", value),
7111 None => std::env::remove_var("VITE_OPENRTC_PROJECT_ID"),
7112 }
7113 match previous_app_tag {
7114 Some(value) => std::env::set_var("VITE_PLUTO_OPENRTC_APP_TAG", value),
7115 None => std::env::remove_var("VITE_PLUTO_OPENRTC_APP_TAG"),
7116 }
7117 }
7118
7119 #[test]
7120 fn native_state_has_one_constructor_owned_app_identity() {
7121 let config = test_config();
7122 let expected = openrtc::app_tag_from_api_key(&config.api_key);
7123 let state = OpenRtcTauriState::new(config);
7124 let first = state.client();
7125 let second = state.client();
7126
7127 assert_eq!(first.app_tag(), expected);
7128 assert!(Arc::ptr_eq(&first, &second));
7129 }
7130
7131 #[test]
7132 fn managed_session_uses_persisted_native_device_id() {
7133 let identity = openrtc::native_device::NativeDeviceIdentity {
7134 device_id: "persisted-native-device".to_string(),
7135 device_name: "Mac".to_string(),
7136 created_at_ms: 1,
7137 updated_at_ms: 1,
7138 name_source: None,
7139 system_info: None,
7140 };
7141
7142 assert_eq!(
7143 managed_session_device_id(Some(" desktop-e2e-native "), &identity),
7144 "persisted-native-device"
7145 );
7146 assert_eq!(
7147 managed_session_device_id(None, &identity),
7148 "persisted-native-device"
7149 );
7150 }
7151
7152 #[test]
7153 fn repeated_managed_session_triggers_converge_on_one_owner() {
7154 let key = "app:persisted-native-device";
7155 let cases = [
7156 ("react-remount", true, true, true, true),
7157 ("hmr", true, true, true, true),
7158 ("auth-refresh", true, true, true, true),
7159 ("resume", true, true, true, true),
7160 ("alias-change", true, true, true, true),
7161 ];
7162 for (label, active_presence, active_auto, requested_presence, requested_auto) in cases {
7163 assert_eq!(
7164 managed_session_disposition(
7165 Some(key),
7166 true,
7167 active_presence,
7168 active_auto,
7169 key,
7170 requested_presence,
7171 requested_auto,
7172 ),
7173 SessionDisposition::Reuse,
7174 "{label} must reuse the authoritative tuple"
7175 );
7176 }
7177
7178 assert_eq!(
7179 managed_session_disposition(Some(key), true, false, true, key, true, true),
7180 SessionDisposition::Refresh,
7181 "failed presence startup must retry idempotently"
7182 );
7183 assert_eq!(
7184 managed_session_disposition(
7185 Some(key),
7186 true,
7187 true,
7188 true,
7189 "app:other-native-device",
7190 true,
7191 true,
7192 ),
7193 SessionDisposition::Replace,
7194 "a physical native device change must replace the previous lifecycle owner"
7195 );
7196 assert_eq!(
7197 managed_session_disposition(Some(key), false, true, true, key, true, true),
7198 SessionDisposition::Replace,
7199 "the same tuple on a different client epoch must replace the previous owner"
7200 );
7201 }
7202
7203 #[test]
7204 fn user_device_revocation_invalidates_the_cached_managed_ticket() {
7205 assert!(revokes_managed_session("user-device"));
7206 assert!(revokes_managed_session(" user-device "));
7207 assert!(!revokes_managed_session("share:example"));
7208 assert!(!revokes_managed_session(""));
7209 }
7210
7211 #[test]
7212 fn managed_session_metadata_uses_authoritative_device_id() {
7213 let raw = serde_json::json!({
7214 "deviceId": "persisted-native-device",
7215 "assistantDevice": {
7216 "deviceId": "desktop-e2e-native"
7217 }
7218 })
7219 .to_string();
7220
7221 let metadata =
7222 metadata_with_authoritative_device_id(Some(raw), "desktop-e2e-native").unwrap();
7223 let parsed: serde_json::Value = serde_json::from_str(&metadata).unwrap();
7224
7225 assert_eq!(parsed["deviceId"], "desktop-e2e-native");
7226 assert_eq!(parsed["assistantDevice"]["deviceId"], "desktop-e2e-native");
7227 }
7228}