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