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 presence_loop_active: Mutex<bool>,
652 subscriptions: Mutex<HashMap<String, tokio::task::JoinHandle<()>>>,
653 native_projection_subscription: Arc<Mutex<Option<NativeProjectionSubscription>>>,
654 projected_stream_offered: AtomicU64,
655 projected_stream_decoded: AtomicU64,
656 projected_stream_projected: AtomicU64,
657 projected_stream_unhandled_non_channel: AtomicU64,
658 projected_stream_unhandled_other_channel: AtomicU64,
659 projected_stream_unhandled_no_subscription: AtomicU64,
660 projected_stream_failures: AtomicU64,
661 projected_stream_last_transport_stable_id: AtomicU64,
662 projected_stream_validation_checks: AtomicU64,
663 projected_stream_validation_rejections: AtomicU64,
664 projected_stream_last_validated_transport_stable_id: AtomicU64,
665 projected_stream_authorized: AtomicU64,
666 projected_stream_unauthorized: AtomicU64,
667 projected_stream_last_channel: Mutex<Option<String>>,
668 projected_stream_last_protocol: Mutex<Option<String>>,
669 projected_stream_last_connection_id: Mutex<Option<String>>,
670 projected_stream_last_remote_node_id: Mutex<Option<String>>,
671 peer_bi_streams: Mutex<HashMap<String, PeerBiStreamHandle>>,
672 peer_uni_streams: Mutex<HashMap<String, Arc<Mutex<Option<PeerSendStream>>>>>,
673 ticket_meshes: Mutex<HashMap<String, Arc<openrtc::ticket_mesh::TicketMesh>>>,
674 portable_media: Mutex<openrtc::media::PortableMediaSession>,
675 broadcast_sessions: Mutex<HashMap<String, openrtc::broadcast::BroadcastSession>>,
676 broadcast_signers: Mutex<HashMap<String, openrtc::broadcast::BroadcastPublisherSigner>>,
677 broadcast_drivers: Mutex<HashMap<String, Arc<Mutex<()>>>>,
678 managed_session_start_guard: Mutex<()>,
679 managed_session_next_owner_epoch: AtomicU64,
680 managed_session: Mutex<Option<ManagedSessionRecord>>,
681 capability_registry: Arc<Mutex<NativeCapabilityRegistry>>,
682 native_transport_installers: Vec<Arc<dyn TransportInstaller>>,
683 installed_native_transports: Mutex<HashMap<&'static str, InstalledNativeTransport>>,
684 native_device_key_signer: Option<Arc<dyn DeviceKeySigner>>,
685 native_broadcast_adapter: Option<Arc<dyn NativeBroadcastAdapter>>,
686 testing_endpoints: Option<(String, String)>,
687 #[cfg(feature = "managed-group-encryption")]
688 managed_groups: Mutex<HashMap<String, openrtc::native::NativeManagedGroupController>>,
689 #[cfg(feature = "native-broadcast-moq")]
690 native_broadcast_moq_adapter: Option<Arc<native_broadcast_moq::NativeBroadcastMoqAdapter>>,
691}
692
693#[derive(Debug, Clone, serde::Serialize)]
694#[serde(rename_all = "camelCase")]
695struct NativeRuntimeStatusResponse {
696 #[serde(flatten)]
697 status: openrtc::client::RuntimeStatus,
698 product_maturity: openrtc::client::ProductCapabilityMaturity,
699}
700
701impl OpenRtcTauriState {
702 pub fn new(config: OpenRtcTauriConfig) -> Self {
703 openrtc::ensure_rustls();
704
705 let token_relay = Arc::new(TokenRelayState::default());
706 let client = build_client(&config, &token_relay)
707 .expect("OpenRTC Tauri 2.0 requires a valid public API key");
708 let data_dir = config.data_dir.clone();
709 #[cfg(feature = "native-broadcast-moq")]
710 let native_broadcast_moq_adapter =
711 Arc::new(native_broadcast_moq::NativeBroadcastMoqAdapter::new());
712
713 Self {
714 client: RwLock::new(client),
715 config,
716 token_relay,
717 native_identity: Mutex::new(None),
718 data_dir,
719 connection_state_forwarder: std::sync::Mutex::new(None),
720 peer_data_forwarder: std::sync::Mutex::new(None),
721 presence_loop_active: Mutex::new(false),
722 subscriptions: Mutex::new(HashMap::new()),
723 native_projection_subscription: Arc::new(Mutex::new(None)),
724 projected_stream_offered: AtomicU64::new(0),
725 projected_stream_decoded: AtomicU64::new(0),
726 projected_stream_projected: AtomicU64::new(0),
727 projected_stream_unhandled_non_channel: AtomicU64::new(0),
728 projected_stream_unhandled_other_channel: AtomicU64::new(0),
729 projected_stream_unhandled_no_subscription: AtomicU64::new(0),
730 projected_stream_failures: AtomicU64::new(0),
731 projected_stream_last_transport_stable_id: AtomicU64::new(0),
732 projected_stream_validation_checks: AtomicU64::new(0),
733 projected_stream_validation_rejections: AtomicU64::new(0),
734 projected_stream_last_validated_transport_stable_id: AtomicU64::new(0),
735 projected_stream_authorized: AtomicU64::new(0),
736 projected_stream_unauthorized: AtomicU64::new(0),
737 projected_stream_last_channel: Mutex::new(None),
738 projected_stream_last_protocol: Mutex::new(None),
739 projected_stream_last_connection_id: Mutex::new(None),
740 projected_stream_last_remote_node_id: Mutex::new(None),
741 peer_bi_streams: Mutex::new(HashMap::new()),
742 peer_uni_streams: Mutex::new(HashMap::new()),
743 ticket_meshes: Mutex::new(HashMap::new()),
744 portable_media: Mutex::new(openrtc::media::PortableMediaSession::default()),
745 broadcast_sessions: Mutex::new(HashMap::new()),
746 broadcast_signers: Mutex::new(HashMap::new()),
747 broadcast_drivers: Mutex::new(HashMap::new()),
748 managed_session_start_guard: Mutex::new(()),
749 managed_session_next_owner_epoch: AtomicU64::new(0),
750 managed_session: Mutex::new(None),
751 capability_registry: Arc::new(Mutex::new(NativeCapabilityRegistry::default())),
752 native_transport_installers: Vec::new(),
753 installed_native_transports: Mutex::new(HashMap::new()),
754 native_device_key_signer: None,
755 testing_endpoints: None,
756 #[cfg(feature = "managed-group-encryption")]
757 managed_groups: Mutex::new(HashMap::new()),
758 native_broadcast_adapter: {
759 #[cfg(feature = "native-broadcast-moq")]
760 {
761 Some(native_broadcast_moq_adapter.clone())
762 }
763 #[cfg(not(feature = "native-broadcast-moq"))]
764 {
765 None
766 }
767 },
768 #[cfg(feature = "native-broadcast-moq")]
769 native_broadcast_moq_adapter: Some(native_broadcast_moq_adapter),
770 }
771 }
772
773 pub fn with_native_transport_installer(
774 mut self,
775 installer: Arc<dyn TransportInstaller>,
776 ) -> Self {
777 self.native_transport_installers.push(installer);
778 self
779 }
780
781 pub fn with_native_device_key_signer(mut self, signer: Arc<dyn DeviceKeySigner>) -> Self {
782 self.native_device_key_signer = Some(signer);
783 self
784 }
785
786 pub fn with_testing_endpoints(
790 mut self,
791 control_plane: impl Into<String>,
792 gateway: impl Into<String>,
793 ) -> Self {
794 self.testing_endpoints = Some((control_plane.into(), gateway.into()));
795 self
796 }
797
798 pub fn native_control_plane(
802 &self,
803 assertions: Arc<dyn openrtc::native::AssertionProvider>,
804 ) -> Result<openrtc::native::ControlPlane, String> {
805 let signer = self.native_device_key_signer.clone().ok_or_else(|| {
806 "native control plane requires a host secure-storage signer".to_string()
807 })?;
808 let storage = Arc::new(native_identity::HostIdentityStorage(signer));
809 let control_plane = openrtc::native::ControlPlane::new(
810 self.config.validated_api_key()?,
811 assertions,
812 storage.clone(),
813 storage,
814 )
815 .map_err(|error| error.to_string())?;
816 let control_plane = if let Some((control_plane_endpoint, gateway_endpoint)) =
817 self.testing_endpoints.as_ref()
818 {
819 control_plane
820 .with_testing_endpoints(control_plane_endpoint, gateway_endpoint)
821 .map_err(|error| error.to_string())?
822 } else {
823 control_plane
824 };
825 Ok(control_plane)
826 }
827
828 fn offline_runtime_support(&self) -> OfflineRuntimeSupport {
829 let provisioning = self.native_device_key_signer.is_some();
830 OfflineRuntimeSupport {
831 provisioning,
832 local_mesh: cfg!(feature = "transport-lan"),
833 cloud_required: false,
834 reason: if !provisioning {
835 Some("native host device signer is unavailable")
836 } else {
837 (!cfg!(feature = "transport-lan"))
838 .then_some("native host was built without transport-lan")
839 },
840 }
841 }
842
843 fn project_public_runtime_status(
844 &self,
845 status: openrtc::client::RuntimeStatus,
846 ) -> NativeRuntimeStatusResponse {
847 use openrtc::client::CapabilityMaturity;
848
849 let mut product_maturity = self.client().product_capability_maturity();
850 product_maturity.offline_edge = if self.native_device_key_signer.is_none() {
851 CapabilityMaturity::Unavailable
852 } else if cfg!(feature = "transport-lan") {
853 CapabilityMaturity::Preview
854 } else {
855 CapabilityMaturity::SupportOnly
856 };
857
858 product_maturity.broadcast =
859 if self.native_device_key_signer.is_some() && self.native_broadcast_adapter.is_some() {
860 CapabilityMaturity::Preview
861 } else {
862 CapabilityMaturity::Unavailable
863 };
864 NativeRuntimeStatusResponse {
865 status,
866 product_maturity,
867 }
868 }
869
870 pub fn with_native_broadcast_adapter(
871 mut self,
872 adapter: Arc<dyn NativeBroadcastAdapter>,
873 ) -> Self {
874 self.native_broadcast_adapter = Some(adapter);
875 #[cfg(feature = "native-broadcast-moq")]
876 {
877 self.native_broadcast_moq_adapter = None;
878 }
879 self
880 }
881
882 #[cfg(feature = "native-broadcast-moq")]
883 async fn resolve_native_broadcast_access(
884 &self,
885 grant: &str,
886 publication_verifying_key: Option<&[u8; 32]>,
887 ) -> Result<native_broadcast_moq::ResolvedNativeBroadcastAccess, String> {
888 let api_key = self.config.validated_api_key()?;
889 let app_tag = self.client().app_tag().to_string();
890 let nonce = uuid::Uuid::new_v4().simple().to_string();
891 let issued_at = std::time::SystemTime::now()
892 .duration_since(std::time::UNIX_EPOCH)
893 .unwrap_or_default()
894 .as_secs();
895 let (challenge, publication_key) = native_broadcast_moq::broadcast_access_challenge(
896 api_key,
897 grant,
898 publication_verifying_key,
899 &nonce,
900 issued_at,
901 );
902 let device_proof = native_broadcast_moq::NativeBroadcastDeviceProof {
903 public_key_jwk: self.device_public_key(&app_tag)?,
904 signature: self.sign_device_proof(&app_tag, &challenge)?,
905 nonce,
906 issued_at,
907 };
908 native_broadcast_moq::resolve_broadcast_access(
909 openrtc::native::OPENRTC_PRODUCTION_CONTROL_PLANE,
910 api_key,
911 grant,
912 publication_key.as_deref(),
913 device_proof,
914 )
915 .await
916 }
917
918 fn device_public_key(&self, app_tag: &str) -> Result<serde_json::Value, String> {
919 let app_tag = required_app_tag(app_tag)?;
920 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
921 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
922 })?;
923 let value = signer.public_jwk(app_tag)?;
924 validate_public_device_jwk(&value)?;
925 Ok(value)
926 }
927
928 fn sign_device_proof(&self, app_tag: &str, challenge: &str) -> Result<String, String> {
929 let app_tag = required_app_tag(app_tag)?;
930 if challenge.is_empty() || challenge.len() > 2_048 {
931 return Err("OpenRTC device challenge is invalid".to_string());
932 }
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 signature = signer.sign(app_tag, challenge.as_bytes())?;
937 if signature.len() != 64 {
938 return Err("OpenRTC device signer returned an invalid Ed25519 signature".to_string());
939 }
940 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature))
941 }
942
943 fn sign_device_message(&self, app_tag: &str, message: &[u8]) -> Result<String, String> {
944 let app_tag = required_app_tag(app_tag)?;
945 if message.is_empty() || message.len() > 96 * 1024 {
946 return Err("OpenRTC device message is invalid".to_string());
947 }
948 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
949 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
950 })?;
951 let signature = signer.sign(app_tag, message)?;
952 if signature.len() != 64 {
953 return Err("OpenRTC device signer returned an invalid Ed25519 signature".to_string());
954 }
955 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(signature))
956 }
957
958 fn delete_device_key(&self, app_tag: &str) -> Result<(), String> {
959 let app_tag = required_app_tag(app_tag)?;
960 let signer = self.native_device_key_signer.as_ref().ok_or_else(|| {
961 "OpenRTC 2.0 native device proof requires a host secure-storage signer".to_string()
962 })?;
963 signer.delete(app_tag)
964 }
965
966 fn read_secure_record(&self, app_tag: &str, key: &str) -> Result<Option<String>, String> {
967 let app_tag = required_app_tag(app_tag)?;
968 let key = required_secure_record_key(key)?;
969 self.native_device_key_signer
970 .as_ref()
971 .ok_or_else(|| {
972 "OpenRTC 2.0 native certificate persistence requires a host secure store"
973 .to_string()
974 })?
975 .read_secure_record(app_tag, key)
976 }
977
978 fn write_secure_record(&self, app_tag: &str, key: &str, value: &str) -> Result<(), String> {
979 let app_tag = required_app_tag(app_tag)?;
980 let key = required_secure_record_key(key)?;
981 if value.is_empty() || value.len() > 16 * 1024 {
982 return Err("OpenRTC secure record is invalid".to_string());
983 }
984 self.native_device_key_signer
985 .as_ref()
986 .ok_or_else(|| {
987 "OpenRTC 2.0 native certificate persistence requires a host secure store"
988 .to_string()
989 })?
990 .write_secure_record(app_tag, key, value)
991 }
992
993 fn delete_secure_record(&self, app_tag: &str, key: &str) -> Result<(), String> {
994 let app_tag = required_app_tag(app_tag)?;
995 let key = required_secure_record_key(key)?;
996 self.native_device_key_signer
997 .as_ref()
998 .ok_or_else(|| {
999 "OpenRTC 2.0 native certificate persistence requires a host secure store"
1000 .to_string()
1001 })?
1002 .delete_secure_record(app_tag, key)
1003 }
1004
1005 pub fn client(&self) -> Arc<openrtc::client::Client> {
1006 self.client
1007 .read()
1008 .map(|guard| guard.clone())
1009 .expect("OpenRTC Tauri client state is poisoned")
1010 }
1011
1012 pub fn set_identity_credential(&self, identity_credential: Option<String>) {
1013 self.token_relay.set(identity_credential);
1014 }
1015
1016 fn allocate_managed_session_owner_epoch(&self) -> u64 {
1017 self.managed_session_next_owner_epoch
1018 .fetch_add(1, Ordering::SeqCst)
1019 + 1
1020 }
1021
1022 fn replace_connection_state_forwarder(&self, client: Arc<openrtc::client::Client>) {
1023 let capability_registry = self.capability_registry.clone();
1024 let projection_subscription = self.native_projection_subscription.clone();
1025 let next = tauri::async_runtime::spawn(async move {
1026 forward_connection_state_events(client, capability_registry, projection_subscription)
1027 .await;
1028 });
1029 if let Ok(mut guard) = self.connection_state_forwarder.lock() {
1030 if let Some(previous) = guard.replace(next) {
1031 previous.abort();
1032 }
1033 }
1034 }
1035
1036 fn replace_peer_data_forwarder(&self, client: Arc<openrtc::client::Client>) {
1037 let capability_registry = self.capability_registry.clone();
1038 let projection_subscription = self.native_projection_subscription.clone();
1039 let next = tauri::async_runtime::spawn(async move {
1040 forward_peer_data_events(client, capability_registry, projection_subscription).await;
1041 });
1042 if let Ok(mut guard) = self.peer_data_forwarder.lock() {
1043 if let Some(previous) = guard.replace(next) {
1044 previous.abort();
1045 }
1046 }
1047 }
1048}
1049
1050fn build_client(
1051 config: &OpenRtcTauriConfig,
1052 token_relay: &Arc<TokenRelayState>,
1053) -> Result<Arc<openrtc::client::Client>, String> {
1054 let mut builder = openrtc::client::Client::builder(
1055 config.validated_api_key()?.to_string(),
1056 token_relay.token_provider(),
1057 )
1058 .map_err(|error| error.to_string())?;
1059 if let Some(transport_config) = config.transport_config.clone() {
1060 builder = builder.transport_config(transport_config);
1061 }
1062 Ok(Arc::new(builder.build()))
1063}
1064
1065fn requested_local_device_id(value: Option<&str>) -> Option<String> {
1066 value
1067 .map(str::trim)
1068 .filter(|value| !value.is_empty())
1069 .map(ToOwned::to_owned)
1070}
1071
1072fn managed_session_device_id(
1073 _requested: Option<&str>,
1074 native_identity: &openrtc::native_device::NativeDeviceIdentity,
1075) -> String {
1076 native_identity.device_id.clone()
1080}
1081
1082#[cfg(test)]
1083fn metadata_with_authoritative_device_id(
1084 metadata: Option<String>,
1085 device_id: &str,
1086) -> Option<String> {
1087 let device_id = device_id.trim();
1088 if device_id.is_empty() {
1089 return metadata;
1090 }
1091
1092 let Some(raw_metadata) = metadata else {
1093 return Some(serde_json::json!({ "deviceId": device_id }).to_string());
1094 };
1095
1096 match serde_json::from_str::<serde_json::Value>(&raw_metadata) {
1097 Ok(serde_json::Value::Object(mut map)) => {
1098 map.insert(
1099 "deviceId".to_string(),
1100 serde_json::Value::String(device_id.to_string()),
1101 );
1102 Some(serde_json::Value::Object(map).to_string())
1103 }
1104 _ => Some(
1105 serde_json::json!({
1106 "deviceId": device_id,
1107 "metadata": raw_metadata,
1108 })
1109 .to_string(),
1110 ),
1111 }
1112}
1113
1114struct PeerBiStreamHandle {
1115 send: Arc<Mutex<Option<PeerSendStream>>>,
1116 recv: Option<PeerRecvStream>,
1117 read_task: Option<tokio::task::JoinHandle<()>>,
1118}
1119
1120struct NativeProjectionSubscription {
1121 webview_label: String,
1122 request_id: String,
1123 channel: Channel<NativeProjectionEvent>,
1124}
1125
1126#[derive(Serialize, Clone)]
1127#[serde(rename_all = "camelCase")]
1128struct NativeProjectionEvent {
1129 request_id: String,
1130 kind: &'static str,
1131 capability_keys: Vec<String>,
1132 payload: serde_json::Value,
1133}
1134
1135#[derive(Serialize)]
1136#[serde(rename_all = "camelCase")]
1137pub struct OpenBiResult {
1138 stream_id: String,
1139 connection_id: Option<String>,
1140 remote_node_id: String,
1141}
1142
1143#[derive(Serialize)]
1144#[serde(rename_all = "camelCase")]
1145pub struct OpenUniResult {
1146 stream_id: String,
1147 connection_id: Option<String>,
1148 remote_node_id: String,
1149}
1150
1151#[derive(Serialize, Clone)]
1152#[serde(rename_all = "camelCase")]
1153struct IncomingPeerBiStreamEvent {
1154 request_id: String,
1155 stream_id: String,
1156 connection_id: Option<String>,
1157 remote_node_id: String,
1158 transport_stable_id: u64,
1159 channel: Option<openrtc::stream_metadata::ChannelMetadata>,
1160 application_authorized: bool,
1161 capability_key: String,
1162}
1163
1164#[derive(Debug, Clone, Serialize)]
1165#[serde(rename_all = "camelCase")]
1166struct ProjectedStreamDiagnostics {
1167 subscription_active: bool,
1168 offered: u64,
1169 decoded: u64,
1170 projected: u64,
1171 unhandled_non_channel: u64,
1172 unhandled_other_channel: u64,
1173 unhandled_no_subscription: u64,
1174 failures: u64,
1175 last_transport_stable_id: u64,
1176 validation_checks: u64,
1177 validation_rejections: u64,
1178 last_validated_transport_stable_id: u64,
1179 authorized: u64,
1180 unauthorized: u64,
1181 last_channel: Option<String>,
1182 last_protocol: Option<String>,
1183 last_connection_id: Option<String>,
1184 last_remote_node_id: Option<String>,
1185}
1186
1187pub enum ProjectedPeerBiHandoff {
1192 Handled,
1193 Unhandled {
1194 send: PeerSendStream,
1195 recv: PeerRecvStream,
1196 },
1197}
1198
1199#[derive(Serialize, Clone)]
1200#[serde(rename_all = "camelCase")]
1201struct PeerBiStreamClosedEvent {
1202 r#type: &'static str,
1203 error: Option<String>,
1204}
1205
1206#[derive(Debug, Clone, Serialize)]
1207#[serde(rename_all = "camelCase")]
1208pub struct StartSessionResult {
1209 local_node_id: String,
1210 ticket_scope: Option<String>,
1211 ticket: Option<String>,
1212 presence_started: bool,
1213 auto_connect_started: bool,
1214 local_device: openrtc::native_device::NativeDeviceIdentity,
1215}
1216
1217#[derive(Clone)]
1218struct ManagedSessionRecord {
1219 key: String,
1220 owner_client: Arc<openrtc::client::Client>,
1221 owner_epoch: u64,
1222 result: StartSessionResult,
1223}
1224
1225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1226enum SessionDisposition {
1227 Start,
1228 Reuse,
1229 Refresh,
1230 Replace,
1231}
1232
1233fn managed_session_disposition(
1234 active_key: Option<&str>,
1235 active_client_matches: bool,
1236 active_presence_started: bool,
1237 active_auto_connect_started: bool,
1238 requested_key: &str,
1239 requested_presence: bool,
1240 requested_auto_connect: bool,
1241) -> SessionDisposition {
1242 let Some(active_key) = active_key else {
1243 return SessionDisposition::Start;
1244 };
1245 if !active_client_matches || active_key != requested_key {
1246 return SessionDisposition::Replace;
1247 }
1248 if (!requested_presence || active_presence_started)
1249 && (!requested_auto_connect || active_auto_connect_started)
1250 {
1251 SessionDisposition::Reuse
1252 } else {
1253 SessionDisposition::Refresh
1254 }
1255}
1256
1257fn revokes_managed_session(scope: &str) -> bool {
1258 scope.trim() == "user-device"
1259}
1260
1261pub fn init<R: Runtime>(config: OpenRtcTauriConfig) -> tauri::plugin::TauriPlugin<R> {
1262 init_with_state(OpenRtcTauriState::new(config))
1263}
1264
1265pub fn init_with_state<R: Runtime>(state: OpenRtcTauriState) -> tauri::plugin::TauriPlugin<R> {
1266 tauri::plugin::Builder::new(PLUGIN_NAME)
1267 .setup(move |app, _api| {
1268 let client = state.client();
1269 let transport_config = state.config.transport_config.clone();
1270 let install_context = InstallContext {
1275 data_dir: app_data_dir(app, &state).map_err(std::io::Error::other)?,
1276 };
1277 tauri::async_runtime::block_on(ensure_requested_native_transports(
1278 &state,
1279 &client,
1280 transport_config.as_ref(),
1281 &install_context,
1282 ))
1283 .map_err(std::io::Error::other)?;
1284 state.replace_connection_state_forwarder(client.clone());
1285 state.replace_peer_data_forwarder(client);
1286 app.manage(state);
1287 Ok(())
1288 })
1289 .invoke_handler(tauri::generate_handler![
1290 openrtc_configure_native_identity,
1291 openrtc_complete_native_assertion,
1292 openrtc_clear_native_identity,
1293 openrtc_start_native_devices,
1294 openrtc_publish_native_devices,
1295 openrtc_list_native_devices,
1296 openrtc_set_identity_credential,
1297 openrtc_device_public_key,
1298 openrtc_sign_device_proof,
1299 openrtc_delete_device_key,
1300 openrtc_read_secure_record,
1301 openrtc_write_secure_record,
1302 openrtc_delete_secure_record,
1303 openrtc_offline_runtime_support,
1304 openrtc_create_offline_enrollment_request,
1305 openrtc_verify_offline_enrollment_request,
1306 rtc_native_status,
1307 get_rtc_local_device_info,
1308 update_rtc_local_device_name,
1309 get_iroh_node_id,
1310 start_iroh_node,
1311 get_iroh_endpoint_ticket,
1312 register_session_token,
1313 get_endpoint_ticket_with_token,
1314 issue_ticket_invite,
1315 connect_ticket_invite,
1316 issue_ticket_mesh,
1317 join_ticket_mesh,
1318 ticket_mesh_peers,
1319 ticket_mesh_issuer_node,
1320 close_ticket_mesh,
1321 validate_session_token,
1322 revoke_session_tokens_by_scope,
1323 stop_rtc_presence_loop,
1324 register_rtc_capability,
1325 openrtc_init_managed_room_group,
1326 openrtc_handle_managed_room_prepare_page,
1327 openrtc_handle_managed_room_artifact_chunk,
1328 openrtc_seal_managed_room_payload,
1329 openrtc_open_managed_room_payload,
1330 openrtc_forget_managed_room_group,
1331 unregister_rtc_capability,
1332 start_rtc_managed_session,
1333 start_rtc_external_auto_connect,
1334 submit_rtc_desired_peers,
1335 stop_rtc_auto_connect,
1336 notify_rtc_network_change,
1337 set_rtc_transport_priority,
1338 connect_to_device,
1339 connect_to_known_device_with_token,
1340 observe_known_device_endpoint,
1341 disconnect_device,
1342 set_auto_connect_excluded,
1343 set_rtc_external_auto_connect_excluded,
1344 resolve_rtc_peer_connection_records,
1345 resolve_rtc_peer_identity,
1346 get_rtc_peer_session,
1347 list_rtc_peer_sessions,
1348 list_rtc_managed_connections,
1349 wait_for_rtc_settled_peer,
1350 wait_for_rtc_settled_scope,
1351 list_rtc_connection_states,
1352 get_rtc_connection_state,
1353 stop_rtc_subscription,
1354 start_native_projection,
1355 get_projected_stream_diagnostics,
1356 is_current_transport_stable_id,
1357 send_peer_message,
1358 encode_sparse_fanout_message,
1359 accept_sparse_fanout_message,
1360 sparse_fanout_diagnostics,
1361 prepare_openrtc_broadcast_publisher,
1362 release_openrtc_broadcast_publisher,
1363 open_openrtc_broadcast,
1364 begin_openrtc_broadcast_publication,
1365 publish_openrtc_broadcast_sample,
1366 pause_openrtc_broadcast_publication,
1367 retire_openrtc_broadcast_publication,
1368 retire_openrtc_broadcast_receiver,
1369 receive_openrtc_broadcast_media,
1370 get_openrtc_broadcast_stats,
1371 revoke_openrtc_broadcast,
1372 close_openrtc_broadcast,
1373 record_sparse_fanout_forward_queue_drop,
1374 is_peer_connected,
1375 open_peer_bi_stream,
1376 open_peer_bi_transport_only_stream,
1377 open_peer_uni_stream,
1378 write_peer_bi_stream,
1379 finish_peer_bi_stream_send,
1380 start_peer_bi_stream_read,
1381 cancel_peer_bi_stream_read,
1382 close_peer_bi_stream,
1383 write_peer_uni_stream,
1384 close_peer_uni_stream,
1385 begin_openrtc_media_publication,
1386 encode_openrtc_media_sample,
1387 pause_openrtc_media_publication,
1388 retire_openrtc_media_publication,
1389 retire_openrtc_media_receiver,
1390 decode_openrtc_media_chunk,
1391 decode_openrtc_media_control,
1392 ])
1393 .build()
1394}
1395
1396pub fn init_from_env<R: Runtime>() -> tauri::plugin::TauriPlugin<R> {
1397 init(OpenRtcTauriConfig::from_env())
1398}
1399
1400async fn send_native_projection<T: Serialize>(
1401 subscription: &Arc<Mutex<Option<NativeProjectionSubscription>>>,
1402 kind: &'static str,
1403 capability_keys: Vec<String>,
1404 payload: &T,
1405) {
1406 if capability_keys.is_empty() {
1407 return;
1408 }
1409 let Ok(payload) = serde_json::to_value(payload) else {
1410 return;
1411 };
1412 let target = subscription.lock().await.as_ref().map(|subscription| {
1413 (
1414 subscription.request_id.clone(),
1415 subscription.channel.clone(),
1416 )
1417 });
1418 let Some((request_id, channel)) = target else {
1419 return;
1420 };
1421 let _ = channel.send(NativeProjectionEvent {
1422 request_id,
1423 kind,
1424 capability_keys,
1425 payload,
1426 });
1427}
1428
1429async fn forward_native_service_error(
1430 registry: &Arc<Mutex<NativeCapabilityRegistry>>,
1431 projection: &Arc<Mutex<Option<NativeProjectionSubscription>>>,
1432 identity_id: &str,
1433 capability_key: &str,
1434 observation: &openrtc::native::NativeServiceErrorObservation,
1435) -> bool {
1436 let registry = registry.lock().await;
1439 let current = registry.registrations.get(capability_key).is_some_and(|entry| {
1440 matches!(&entry.native_source, Some(NativeRosterSource::Active(id)) if id == identity_id)
1441 && observation.avenue.kind == "user"
1442 && entry.owns_native_devices(&observation.avenue.id)
1443 });
1444 if !current { return false; }
1445 send_native_projection(projection, "service-error", vec![capability_key.to_owned()],
1446 &serde_json::json!({
1447 "identityId": identity_id,
1448 "avenue": observation.avenue,
1449 "runtimeInstanceId": observation.runtime_instance_id,
1450 "serviceError": observation.service_error,
1451 })).await;
1452 true
1453}
1454
1455async fn forward_connection_state_events(
1456 client: Arc<openrtc::client::Client>,
1457 capability_registry: Arc<Mutex<NativeCapabilityRegistry>>,
1458 projection_subscription: Arc<Mutex<Option<NativeProjectionSubscription>>>,
1459) {
1460 let mut rx = client.connection_state_updates();
1461 while let Ok(snapshot) = rx.recv().await {
1462 let keys = capability_registry
1463 .lock()
1464 .await
1465 .capability_keys_for_state(&snapshot);
1466 send_native_projection(
1467 &projection_subscription,
1468 "connection-state",
1469 keys,
1470 &snapshot,
1471 )
1472 .await;
1473 }
1474}
1475
1476async fn forward_peer_data_events(
1477 client: Arc<openrtc::client::Client>,
1478 capability_registry: Arc<Mutex<NativeCapabilityRegistry>>,
1479 projection_subscription: Arc<Mutex<Option<NativeProjectionSubscription>>>,
1480) {
1481 let mut rx = client.subscribe_native_peer_data();
1482 while let Ok(event) = rx.recv().await {
1483 let keys = capability_registry
1484 .lock()
1485 .await
1486 .capability_keys_for_peer_data(
1487 &event,
1488 &client.session_admitted_scopes(&event.connection_id),
1489 );
1490 send_native_projection(&projection_subscription, "peer-data", keys, &event).await;
1491 }
1492}
1493
1494async fn replay_current_connection_states(state: &OpenRtcTauriState) {
1495 let snapshots = state.client().connection_states().await;
1496 let registry = state.capability_registry.lock().await;
1497 for snapshot in snapshots {
1498 let keys = registry.capability_keys_for_state(&snapshot);
1499 send_native_projection(
1500 &state.native_projection_subscription,
1501 "connection-state",
1502 keys,
1503 &snapshot,
1504 )
1505 .await;
1506 }
1507}
1508
1509fn app_data_dir<R: Runtime>(
1510 app: &tauri::AppHandle<R>,
1511 state: &OpenRtcTauriState,
1512) -> Result<PathBuf, String> {
1513 state
1514 .data_dir
1515 .clone()
1516 .or_else(|| app.path().app_data_dir().ok())
1517 .ok_or_else(|| "failed to resolve OpenRTC app data directory".to_string())
1518}
1519
1520async fn ensure_iroh_node(client: Arc<openrtc::client::Client>) -> Result<String, String> {
1521 if let Some(node_id) = client.current_node_id().await {
1522 return Ok(node_id);
1523 }
1524 client
1525 .init_iroh(None, Vec::new())
1526 .await
1527 .map_err(|error| format!("failed to initialize OpenRTC Iroh node: {error}"))
1528}
1529
1530async fn ensure_requested_native_transports(
1531 state: &OpenRtcTauriState,
1532 client: &Arc<openrtc::client::Client>,
1533 transports: Option<&openrtc::client::TransportConfig>,
1534 context: &InstallContext,
1535) -> Result<(), String> {
1536 let Some(transports) = transports else {
1537 return Ok(());
1538 };
1539 let ble_requested = transports.ble.as_ref().is_some_and(|config| config.enabled);
1540 if ble_requested
1541 && !state
1542 .native_transport_installers
1543 .iter()
1544 .any(|installer| installer.is_requested(transports))
1545 {
1546 eprintln!(
1547 "[openrtc-tauri][transport] BLE requested but unavailable: this host did not register a BLE transport installer; continuing on the Iroh base route"
1548 );
1549 return Ok(());
1550 }
1551 let client_ptr = Arc::as_ptr(client) as usize;
1552 for installer in &state.native_transport_installers {
1553 if !installer.is_requested(transports) {
1554 continue;
1555 }
1556 let already_installed = state
1557 .installed_native_transports
1558 .lock()
1559 .await
1560 .get(installer.id())
1561 .is_some_and(|installed| installed.client_ptr == client_ptr);
1562 if already_installed {
1563 continue;
1564 }
1565 if client.current_node_id().await.is_some() {
1566 eprintln!(
1567 "[openrtc-tauri][transport] {} requested after native node startup; continuing on the Iroh base route",
1568 installer.id()
1569 );
1570 continue;
1571 }
1572 let runtime = match installer
1573 .install(client.clone(), transports.clone(), context.clone())
1574 .await
1575 {
1576 Ok(runtime) => runtime,
1577 Err(error) => {
1578 eprintln!(
1579 "[openrtc-tauri][transport] {} unavailable: {}; continuing on the Iroh base route",
1580 installer.id(),
1581 error
1582 );
1583 continue;
1584 }
1585 };
1586 state.installed_native_transports.lock().await.insert(
1587 installer.id(),
1588 InstalledNativeTransport {
1589 client_ptr,
1590 _runtime: runtime,
1591 },
1592 );
1593 }
1594 Ok(())
1595}
1596
1597async fn register_peer_bi_stream(
1598 state: &OpenRtcTauriState,
1599 connection_id: Option<String>,
1600 remote_node_id: String,
1601 send: PeerSendStream,
1602 recv: PeerRecvStream,
1603) -> OpenBiResult {
1604 let stream_id = uuid::Uuid::new_v4().to_string();
1605 let send = Arc::new(Mutex::new(Some(send)));
1606
1607 state.peer_bi_streams.lock().await.insert(
1608 stream_id.clone(),
1609 PeerBiStreamHandle {
1610 send,
1611 recv: Some(recv),
1614 read_task: None,
1615 },
1616 );
1617
1618 OpenBiResult {
1619 stream_id,
1620 connection_id,
1621 remote_node_id,
1622 }
1623}
1624
1625async fn read_peer_exact(recv: &mut PeerRecvStream, buffer: &mut [u8]) -> std::io::Result<()> {
1626 let mut offset = 0;
1627 while offset < buffer.len() {
1628 let read = recv.read(&mut buffer[offset..]).await?;
1629 if read == 0 {
1630 return Err(std::io::Error::new(
1631 std::io::ErrorKind::UnexpectedEof,
1632 "peer stream ended during channel classification",
1633 ));
1634 }
1635 offset += read;
1636 }
1637 Ok(())
1638}
1639
1640fn is_openrtc_projected_channel(channel: &openrtc::stream_metadata::ChannelMetadata) -> bool {
1641 channel.channel_id == openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID
1642 || channel.channel_id.starts_with("share/")
1643}
1644
1645pub async fn handoff_projected_peer_bi_stream<R: Runtime>(
1653 app: tauri::AppHandle<R>,
1654 connection_id: Option<String>,
1655 remote_node_id: String,
1656 transport_stable_id: u64,
1657 send: PeerSendStream,
1658 mut recv: PeerRecvStream,
1659) -> Result<ProjectedPeerBiHandoff, String> {
1660 let state = app.state::<OpenRtcTauriState>();
1661 state
1662 .projected_stream_offered
1663 .fetch_add(1, Ordering::Relaxed);
1664 state
1665 .projected_stream_last_transport_stable_id
1666 .store(transport_stable_id, Ordering::Relaxed);
1667 *state.projected_stream_last_connection_id.lock().await = connection_id.clone();
1668 *state.projected_stream_last_remote_node_id.lock().await = Some(remote_node_id.clone());
1669 let mut envelope = Vec::new();
1670 let channel = loop {
1671 match openrtc::stream_metadata::decode_prefix(&envelope) {
1672 openrtc::stream_metadata::DecodeDecision::NotMatched => {
1673 state
1674 .projected_stream_unhandled_non_channel
1675 .fetch_add(1, Ordering::Relaxed);
1676 return Ok(ProjectedPeerBiHandoff::Unhandled {
1677 send,
1678 recv: recv.with_plaintext_prefix(envelope),
1679 });
1680 }
1681 openrtc::stream_metadata::DecodeDecision::Decoded(prefix) => break prefix.channel,
1682 openrtc::stream_metadata::DecodeDecision::NeedMore(required) => {
1683 let missing = required.saturating_sub(envelope.len());
1684 if missing == 0 {
1685 return Err("channel-envelope decoder made no progress".to_string());
1686 }
1687 let mut bytes = vec![0u8; missing];
1688 if let Err(error) = read_peer_exact(&mut recv, &mut bytes).await {
1689 state
1690 .projected_stream_failures
1691 .fetch_add(1, Ordering::Relaxed);
1692 return Err(format!("read projected channel envelope: {error}"));
1693 }
1694 envelope.extend_from_slice(&bytes);
1695 }
1696 }
1697 };
1698 state
1699 .projected_stream_decoded
1700 .fetch_add(1, Ordering::Relaxed);
1701 *state.projected_stream_last_channel.lock().await = Some(channel.channel_id.clone());
1702 *state.projected_stream_last_protocol.lock().await = channel
1703 .metadata
1704 .as_ref()
1705 .and_then(|metadata| metadata.get("protocol"))
1706 .and_then(serde_json::Value::as_str)
1707 .map(str::to_string);
1708
1709 if !is_openrtc_projected_channel(&channel) {
1710 state
1711 .projected_stream_unhandled_other_channel
1712 .fetch_add(1, Ordering::Relaxed);
1713 return Ok(ProjectedPeerBiHandoff::Unhandled {
1714 send,
1715 recv: recv.with_plaintext_prefix(envelope),
1716 });
1717 }
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 install_native_projection(
4017 &mut subscription,
4018 webview.label().to_string(),
4019 request_id,
4020 channel,
4021 )
4022}
4023
4024fn install_native_projection(
4026 subscription: &mut Option<NativeProjectionSubscription>,
4027 webview_label: String,
4028 request_id: String,
4029 channel: Channel<NativeProjectionEvent>,
4030) -> Result<String, String> {
4031 if let Some(current) = subscription.as_ref() {
4032 if current.webview_label == webview_label {
4033 *subscription = Some(NativeProjectionSubscription {
4037 webview_label,
4038 request_id: request_id.clone(),
4039 channel,
4040 });
4041 return Ok(request_id);
4042 }
4043 return Err("OpenRTC native projection subscription already has a process owner".into());
4046 }
4047 *subscription = Some(NativeProjectionSubscription {
4048 webview_label,
4049 request_id: request_id.clone(),
4050 channel,
4051 });
4052 Ok(request_id)
4053}
4054
4055#[tauri::command]
4056async fn get_projected_stream_diagnostics(
4057 state: tauri::State<'_, OpenRtcTauriState>,
4058) -> Result<ProjectedStreamDiagnostics, String> {
4059 Ok(ProjectedStreamDiagnostics {
4060 subscription_active: state.native_projection_subscription.lock().await.is_some(),
4061 offered: state.projected_stream_offered.load(Ordering::Relaxed),
4062 decoded: state.projected_stream_decoded.load(Ordering::Relaxed),
4063 projected: state.projected_stream_projected.load(Ordering::Relaxed),
4064 unhandled_non_channel: state
4065 .projected_stream_unhandled_non_channel
4066 .load(Ordering::Relaxed),
4067 unhandled_other_channel: state
4068 .projected_stream_unhandled_other_channel
4069 .load(Ordering::Relaxed),
4070 unhandled_no_subscription: state
4071 .projected_stream_unhandled_no_subscription
4072 .load(Ordering::Relaxed),
4073 failures: state.projected_stream_failures.load(Ordering::Relaxed),
4074 last_transport_stable_id: state
4075 .projected_stream_last_transport_stable_id
4076 .load(Ordering::Relaxed),
4077 validation_checks: state
4078 .projected_stream_validation_checks
4079 .load(Ordering::Relaxed),
4080 validation_rejections: state
4081 .projected_stream_validation_rejections
4082 .load(Ordering::Relaxed),
4083 last_validated_transport_stable_id: state
4084 .projected_stream_last_validated_transport_stable_id
4085 .load(Ordering::Relaxed),
4086 authorized: state.projected_stream_authorized.load(Ordering::Relaxed),
4087 unauthorized: state.projected_stream_unauthorized.load(Ordering::Relaxed),
4088 last_channel: state.projected_stream_last_channel.lock().await.clone(),
4089 last_protocol: state.projected_stream_last_protocol.lock().await.clone(),
4090 last_connection_id: state
4091 .projected_stream_last_connection_id
4092 .lock()
4093 .await
4094 .clone(),
4095 last_remote_node_id: state
4096 .projected_stream_last_remote_node_id
4097 .lock()
4098 .await
4099 .clone(),
4100 })
4101}
4102
4103#[tauri::command]
4104async fn is_current_transport_stable_id(
4105 state: tauri::State<'_, OpenRtcTauriState>,
4106 endpoint_id: String,
4107 transport_stable_id: u64,
4108) -> Result<bool, String> {
4109 state
4110 .projected_stream_validation_checks
4111 .fetch_add(1, Ordering::Relaxed);
4112 state
4113 .projected_stream_last_validated_transport_stable_id
4114 .store(transport_stable_id, Ordering::Relaxed);
4115 let is_current = state
4116 .client()
4117 .is_current_transport_stable_id_str(&endpoint_id, transport_stable_id)
4118 .await
4119 .map_err(|error| error.to_string())?;
4120 if !is_current {
4121 state
4122 .projected_stream_validation_rejections
4123 .fetch_add(1, Ordering::Relaxed);
4124 }
4125 if native_stream_trace_enabled() {
4126 eprintln!(
4127 "[openrtc-tauri][incoming-application-stream] remote_node_id={} transport_stable_id={} current={} phase=validate",
4128 endpoint_id, transport_stable_id, is_current
4129 );
4130 }
4131 Ok(is_current)
4132}
4133
4134#[tauri::command]
4135async fn send_peer_message(
4136 state: tauri::State<'_, OpenRtcTauriState>,
4137 request: Request<'_>,
4138) -> Result<(), String> {
4139 let id = required_request_header(&request, PEER_ID_HEADER)?;
4140 let data = request_body_bytes(&request)?;
4141 state
4142 .client()
4143 .send_peer(&id, &data)
4144 .await
4145 .map_err(|error| error.to_string())
4146}
4147
4148#[tauri::command]
4149async fn encode_sparse_fanout_message(
4150 state: tauri::State<'_, OpenRtcTauriState>,
4151 request: Request<'_>,
4152) -> Result<Response, String> {
4153 let capability = required_request_header(&request, FANOUT_CAPABILITY_HEADER)?;
4154 let app_tag = required_request_header(&request, FANOUT_APP_TAG_HEADER)?;
4155 let payload = request_body_bytes(&request)?;
4156 let signing_request = state
4157 .client()
4158 .prepare_sparse_fanout_message(&capability, &payload)
4159 .await
4160 .map_err(|error| error.to_string())?;
4161 let signature = state.sign_device_message(&app_tag, &signing_request.signing_input)?;
4162 state
4163 .client()
4164 .finalize_sparse_fanout_message(&signing_request.request_id, &signature)
4165 .await
4166 .map(Response::new)
4167 .map_err(|error| error.to_string())
4168}
4169
4170#[tauri::command]
4171async fn accept_sparse_fanout_message(
4172 state: tauri::State<'_, OpenRtcTauriState>,
4173 request: Request<'_>,
4174) -> Result<serde_json::Value, String> {
4175 let capability = required_request_header(&request, FANOUT_CAPABILITY_HEADER)?;
4176 let source_peer = required_request_header(&request, FANOUT_SOURCE_PEER_HEADER)?;
4177 let encoded = request_body_bytes(&request)?;
4178 serde_json::to_value(
4179 state
4180 .client()
4181 .accept_sparse_fanout_message(&capability, &source_peer, &encoded)
4182 .await
4183 .map_err(|error| error.to_string())?,
4184 )
4185 .map_err(|error| error.to_string())
4186}
4187
4188#[tauri::command]
4189async fn sparse_fanout_diagnostics(
4190 state: tauri::State<'_, OpenRtcTauriState>,
4191 capability_key: String,
4192) -> Result<openrtc::sparse_fanout::SparseFanoutDiagnostics, String> {
4193 Ok(state
4194 .client()
4195 .sparse_fanout_diagnostics(&required_native_capability_part(
4196 capability_key,
4197 "capability key",
4198 )?)
4199 .await)
4200}
4201
4202#[derive(Debug, Serialize)]
4203#[serde(rename_all = "camelCase")]
4204struct NativeBroadcastPublisherDescriptor {
4205 handle: String,
4206 verification_key: Vec<u8>,
4207}
4208
4209#[derive(Debug, Serialize)]
4210#[serde(rename_all = "camelCase")]
4211struct NativeBroadcastSessionDescriptor {
4212 handle_id: String,
4213 id: String,
4214 role: openrtc::broadcast::BroadcastRole,
4215 state: openrtc::broadcast::BroadcastState,
4216 source_slots: Vec<String>,
4217 budget: openrtc::broadcast::BroadcastBudget,
4218}
4219
4220#[tauri::command]
4223async fn prepare_openrtc_broadcast_publisher(
4224 state: tauri::State<'_, OpenRtcTauriState>,
4225) -> Result<NativeBroadcastPublisherDescriptor, String> {
4226 let signer = openrtc::broadcast::BroadcastPublisherSigner::generate()
4227 .map_err(|error| error.to_string())?;
4228 let verification_key = signer.verifying_key().to_vec();
4229 let handle = uuid::Uuid::new_v4().simple().to_string();
4230 state
4231 .broadcast_signers
4232 .lock()
4233 .await
4234 .insert(handle.clone(), signer);
4235 Ok(NativeBroadcastPublisherDescriptor {
4236 handle,
4237 verification_key,
4238 })
4239}
4240
4241#[tauri::command]
4242async fn release_openrtc_broadcast_publisher(
4243 state: tauri::State<'_, OpenRtcTauriState>,
4244 handle: String,
4245) -> Result<bool, String> {
4246 Ok(state
4247 .broadcast_signers
4248 .lock()
4249 .await
4250 .remove(&handle)
4251 .is_some())
4252}
4253
4254async fn drive_native_broadcast_actions(
4255 session: &openrtc::broadcast::BroadcastSession,
4256 adapter: &dyn NativeBroadcastAdapter,
4257) -> Result<(), String> {
4258 loop {
4259 let actions = session.take_actions(openrtc::broadcast::MAX_BROADCAST_ACTIONS);
4260 if actions.is_empty() {
4261 return Ok(());
4262 }
4263 for action in actions {
4264 if let Some(observation) = adapter.apply(session.clone(), action).await? {
4265 session
4266 .observe(observation)
4267 .map_err(|error| error.to_string())?;
4268 }
4269 }
4270 }
4271}
4272
4273async fn native_broadcast_driver(
4274 state: &OpenRtcTauriState,
4275 handle_id: &str,
4276) -> Result<Arc<Mutex<()>>, String> {
4277 state
4278 .broadcast_drivers
4279 .lock()
4280 .await
4281 .get(handle_id)
4282 .cloned()
4283 .ok_or_else(|| "broadcast session is unavailable".to_string())
4284}
4285
4286#[tauri::command]
4287async fn open_openrtc_broadcast(
4288 state: tauri::State<'_, OpenRtcTauriState>,
4289 grant_token: String,
4290 issuer_public_key: Option<Vec<u8>>,
4291 publisher_signer_handle: Option<String>,
4292 now_ms: u64,
4293) -> Result<NativeBroadcastSessionDescriptor, String> {
4294 #[cfg(feature = "native-broadcast-moq")]
4295 let publication_verifying_key = if let Some(handle) = publisher_signer_handle.as_deref() {
4296 Some(
4297 state
4298 .broadcast_signers
4299 .lock()
4300 .await
4301 .get(handle)
4302 .ok_or_else(|| "broadcast publisher signer handle is invalid".to_string())?
4303 .verifying_key(),
4304 )
4305 } else {
4306 None
4307 };
4308 #[cfg(feature = "native-broadcast-moq")]
4309 let resolved_access = if state.native_broadcast_moq_adapter.is_some() {
4310 Some(
4311 state
4312 .resolve_native_broadcast_access(&grant_token, publication_verifying_key.as_ref())
4313 .await?,
4314 )
4315 } else {
4316 None
4317 };
4318 #[cfg(feature = "native-broadcast-moq")]
4319 let issuer_public_key = match (issuer_public_key, resolved_access.as_ref()) {
4320 (Some(provided), Some(resolved)) if provided.as_slice() != resolved.issuer_public_key => {
4321 return Err("broadcast issuer key mismatch".to_string());
4322 }
4323 (Some(provided), _) => provided,
4324 (None, Some(resolved)) => resolved.issuer_public_key.to_vec(),
4325 (None, None) => return Err("broadcast issuer key is required".to_string()),
4326 };
4327 #[cfg(not(feature = "native-broadcast-moq"))]
4328 let issuer_public_key =
4329 issuer_public_key.ok_or_else(|| "broadcast issuer key is required".to_string())?;
4330 let issuer_bytes: [u8; 32] = issuer_public_key
4331 .try_into()
4332 .map_err(|_| "broadcast issuer key must be 32 bytes".to_string())?;
4333 let issuer = VerifyingKey::from_bytes(&issuer_bytes)
4334 .map_err(|_| "broadcast issuer key is invalid".to_string())?;
4335 let broadcasts = state.client().broadcasts();
4336 let challenge = broadcasts
4337 .prepare_grant_verification(&grant_token, &issuer, now_ms)
4338 .map_err(|error| error.to_string())?;
4339 let device_signer = state.native_device_key_signer.as_deref().ok_or_else(|| {
4340 "native managed broadcast requires the host device-key signer".to_string()
4341 })?;
4342 let binding_signature = device_signer
4343 .sign(state.client().app_tag(), &challenge.signing_bytes)
4344 .map_err(|error| format!("sign broadcast installation challenge: {error}"))?;
4345 let grant = broadcasts
4346 .complete_grant_verification(
4347 &grant_token,
4348 &issuer,
4349 &challenge.handle,
4350 &binding_signature,
4351 now_ms,
4352 )
4353 .map_err(|error| error.to_string())?;
4354 let grant_generation = grant.grant_generation();
4355 let session = if let Some(handle) = publisher_signer_handle.as_deref() {
4356 let signers = state.broadcast_signers.lock().await;
4357 let signer = signers
4358 .get(handle)
4359 .ok_or_else(|| "broadcast publisher signer handle is invalid".to_string())?;
4360 state
4361 .client()
4362 .broadcasts()
4363 .open_publisher(grant, signer, now_ms)
4364 } else {
4365 state.client().broadcasts().open(grant, now_ms)
4366 }
4367 .map_err(|error| error.to_string())?;
4368 let handle_id = format!("{}:{grant_generation}", session.id());
4369 #[cfg(feature = "native-broadcast-moq")]
4370 if let (Some(adapter), Some(access)) = (
4371 state.native_broadcast_moq_adapter.as_ref(),
4372 resolved_access.map(|resolved| resolved.relay),
4373 ) {
4374 adapter
4375 .authorize(session.id(), grant_generation, access)
4376 .await;
4377 }
4378 let adapter = state.native_broadcast_adapter.as_deref().ok_or_else(|| {
4379 session.close();
4380 "native managed broadcast adapter is unavailable".to_string()
4381 })?;
4382 if let Err(error) = drive_native_broadcast_actions(&session, adapter).await {
4383 session.close();
4384 #[cfg(feature = "native-broadcast-moq")]
4385 if let Some(adapter) = state.native_broadcast_moq_adapter.as_ref() {
4386 adapter
4387 .forget_authorization(&session.id(), grant_generation)
4388 .await;
4389 }
4390 return Err(error);
4391 }
4392 let descriptor = NativeBroadcastSessionDescriptor {
4393 handle_id: handle_id.clone(),
4394 id: session.id(),
4395 role: session.role(),
4396 state: session.state(),
4397 source_slots: session.source_slots(),
4398 budget: session.budget(),
4399 };
4400 state
4401 .broadcast_sessions
4402 .lock()
4403 .await
4404 .insert(handle_id.clone(), session);
4405 state
4406 .broadcast_drivers
4407 .lock()
4408 .await
4409 .insert(handle_id, Arc::new(Mutex::new(())));
4410 Ok(descriptor)
4411}
4412
4413#[tauri::command]
4414#[allow(clippy::too_many_arguments)]
4415async fn begin_openrtc_broadcast_publication(
4416 state: tauri::State<'_, OpenRtcTauriState>,
4417 handle_id: String,
4418 source_slot: String,
4419 publication_id: Option<String>,
4420 kind: String,
4421 codec: String,
4422 clock_rate: u32,
4423 coded_width: Option<u32>,
4424 coded_height: Option<u32>,
4425 channels: Option<u16>,
4426) -> Result<PortableMediaPublicationStart, String> {
4427 let driver = native_broadcast_driver(&state, &handle_id).await?;
4428 let _driver = driver.lock().await;
4429 let publication_id = match publication_id.as_deref().map(str::trim) {
4430 Some(value) if !value.is_empty() => parse_media_publication_id(value)?,
4431 _ => openrtc::media::PublicationId::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
4432 };
4433 let kind: openrtc::media::MediaKind = kind
4434 .parse()
4435 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?;
4436 if !matches!(
4437 kind,
4438 openrtc::media::MediaKind::Audio | openrtc::media::MediaKind::Video
4439 ) {
4440 return Err("broadcast publications must be audio or video".to_string());
4441 }
4442 let publication = openrtc::media::MediaPublicationConfig {
4443 publication_id,
4444 media_generation: 0,
4445 kind,
4446 codec: codec
4447 .parse()
4448 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?,
4449 clock_rate,
4450 coded_width,
4451 coded_height,
4452 channels,
4453 };
4454 let session = state
4455 .broadcast_sessions
4456 .lock()
4457 .await
4458 .get(&handle_id)
4459 .cloned()
4460 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4461 let publication = session
4462 .begin_publication(&source_slot, publication)
4463 .map_err(|error| error.to_string())?;
4464 let adapter = state
4465 .native_broadcast_adapter
4466 .as_deref()
4467 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4468 drive_native_broadcast_actions(&session, adapter).await?;
4469 Ok(PortableMediaPublicationStart {
4470 publication_id: publication.publication_id.to_string(),
4471 media_generation: publication.media_generation,
4472 control: Vec::new(),
4474 })
4475}
4476
4477#[tauri::command]
4478#[allow(clippy::too_many_arguments)]
4479async fn publish_openrtc_broadcast_sample(
4480 state: tauri::State<'_, OpenRtcTauriState>,
4481 request: Request<'_>,
4482) -> Result<(), String> {
4483 let handle_id = required_request_header(&request, BROADCAST_HANDLE_ID_HEADER)?;
4484 let publication_id = required_request_header(&request, MEDIA_PUBLICATION_ID_HEADER)?;
4485 let timestamp_us = parsed_request_header(&request, MEDIA_TIMESTAMP_US_HEADER)?;
4486 let duration_us = parsed_request_header(&request, MEDIA_DURATION_US_HEADER)?;
4487 let keyframe = parsed_request_header(&request, MEDIA_KEYFRAME_HEADER)?;
4488 let discardable = parsed_request_header(&request, MEDIA_DISCARDABLE_HEADER)?;
4489 let payload = request_body_bytes(&request)?;
4490 let driver = native_broadcast_driver(&state, &handle_id).await?;
4491 let _driver = driver.lock().await;
4492 openrtc::media::ensure_javascript_safe_integer(timestamp_us, "timestamp")
4493 .map_err(|error| error.to_string())?;
4494 let session = state
4495 .broadcast_sessions
4496 .lock()
4497 .await
4498 .get(&handle_id)
4499 .cloned()
4500 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4501 session
4502 .publish(
4503 parse_media_publication_id(&publication_id)?,
4504 openrtc::media::EncodedMediaSample {
4505 timestamp_us,
4506 duration_us,
4507 keyframe,
4508 discardable,
4509 payload,
4510 },
4511 )
4512 .map_err(|error| error.to_string())?;
4513 let adapter = state
4514 .native_broadcast_adapter
4515 .as_deref()
4516 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4517 drive_native_broadcast_actions(&session, adapter).await
4518}
4519
4520#[tauri::command]
4521async fn pause_openrtc_broadcast_publication(
4522 state: tauri::State<'_, OpenRtcTauriState>,
4523 handle_id: String,
4524 publication_id: String,
4525) -> Result<(), String> {
4526 let driver = native_broadcast_driver(&state, &handle_id).await?;
4527 let _driver = driver.lock().await;
4528 let session = state
4529 .broadcast_sessions
4530 .lock()
4531 .await
4532 .get(&handle_id)
4533 .cloned()
4534 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4535 session
4536 .pause_publication(parse_media_publication_id(&publication_id)?)
4537 .map_err(|error| error.to_string())?;
4538 let adapter = state
4539 .native_broadcast_adapter
4540 .as_deref()
4541 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4542 drive_native_broadcast_actions(&session, adapter).await
4543}
4544
4545#[tauri::command]
4546async fn retire_openrtc_broadcast_publication(
4547 state: tauri::State<'_, OpenRtcTauriState>,
4548 handle_id: String,
4549 publication_id: String,
4550) -> Result<(), String> {
4551 let driver = native_broadcast_driver(&state, &handle_id).await?;
4552 let _driver = driver.lock().await;
4553 let session = state
4554 .broadcast_sessions
4555 .lock()
4556 .await
4557 .get(&handle_id)
4558 .cloned()
4559 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4560 session
4561 .retire_publication(parse_media_publication_id(&publication_id)?)
4562 .map_err(|error| error.to_string())?;
4563 let adapter = state
4564 .native_broadcast_adapter
4565 .as_deref()
4566 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4567 drive_native_broadcast_actions(&session, adapter).await
4568}
4569
4570#[tauri::command]
4571async fn retire_openrtc_broadcast_receiver(
4572 state: tauri::State<'_, OpenRtcTauriState>,
4573 handle_id: String,
4574 publication_id: String,
4575 media_generation: u32,
4576) -> Result<(), String> {
4577 let driver = native_broadcast_driver(&state, &handle_id).await?;
4578 let _driver = driver.lock().await;
4579 let session = state
4580 .broadcast_sessions
4581 .lock()
4582 .await
4583 .get(&handle_id)
4584 .cloned()
4585 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4586 session
4587 .retire_receiver(
4588 parse_media_publication_id(&publication_id)?,
4589 media_generation,
4590 )
4591 .map_err(|error| error.to_string())
4592}
4593
4594struct NativeBroadcastReceivedObject {
4595 source_slot: Option<String>,
4596 object: Vec<u8>,
4597}
4598
4599#[tauri::command]
4603async fn receive_openrtc_broadcast_media(
4604 state: tauri::State<'_, OpenRtcTauriState>,
4605 handle_id: String,
4606 max: usize,
4607) -> Result<Vec<openrtc::broadcast::AcceptedBroadcastMedia>, String> {
4608 let driver = native_broadcast_driver(&state, &handle_id).await?;
4609 let _driver = driver.lock().await;
4610 let session = state
4611 .broadcast_sessions
4612 .lock()
4613 .await
4614 .get(&handle_id)
4615 .cloned()
4616 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4617 let adapter = state
4618 .native_broadcast_adapter
4619 .as_deref()
4620 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4621 drive_native_broadcast_actions(&session, adapter).await?;
4622 let objects: Vec<NativeBroadcastReceivedObject>;
4623 #[cfg(feature = "native-broadcast-moq")]
4624 {
4625 objects = if let Some(native_moq) = state.native_broadcast_moq_adapter.as_ref() {
4626 native_moq
4627 .take_inbound_objects_with_source(&session, max.min(64))
4628 .await?
4629 .into_iter()
4630 .map(|item| NativeBroadcastReceivedObject {
4631 source_slot: Some(item.source_slot),
4632 object: item.object,
4633 })
4634 .collect()
4635 } else {
4636 adapter
4637 .take_inbound_objects(session.clone(), max.min(64))
4638 .await?
4639 .into_iter()
4640 .map(|object| NativeBroadcastReceivedObject {
4641 source_slot: None,
4642 object,
4643 })
4644 .collect()
4645 };
4646 }
4647 #[cfg(not(feature = "native-broadcast-moq"))]
4648 {
4649 objects = adapter
4650 .take_inbound_objects(session.clone(), max.min(64))
4651 .await?
4652 .into_iter()
4653 .map(|object| NativeBroadcastReceivedObject {
4654 source_slot: None,
4655 object,
4656 })
4657 .collect();
4658 }
4659 let mut accepted = Vec::with_capacity(objects.len());
4660 let mut delivered_bytes = 0_u64;
4661 for received in objects {
4662 let object_len = received.object.len() as u64;
4663 let media = if let Some(source_slot) = received.source_slot.as_deref() {
4664 session
4665 .accept_media_for_source(source_slot, &received.object)
4666 .map_err(|error| error.to_string())?
4667 } else {
4668 session
4669 .accept_media(&received.object)
4670 .map_err(|error| error.to_string())?
4671 };
4672 let Some(media) = media else { continue };
4673 delivered_bytes = delivered_bytes.saturating_add(object_len);
4674 if let Some(encoded) = media.media.as_deref() {
4675 let chunk = openrtc::media::EncodedMediaChunk::decode(encoded)
4676 .map_err(|error| error.to_string())?;
4677 openrtc::media::ensure_javascript_safe_integer(chunk.sequence, "sequence")
4678 .and_then(|_| {
4679 openrtc::media::ensure_javascript_safe_integer(chunk.timestamp_us, "timestamp")
4680 })
4681 .map_err(|error| error.to_string())?;
4682 }
4683 accepted.push(media);
4684 }
4685 #[cfg(feature = "native-broadcast-moq")]
4686 if let Some(native_moq) = state.native_broadcast_moq_adapter.as_ref() {
4687 let usage = native_moq
4688 .record_delivered_bytes(&session, delivered_bytes)
4689 .await;
4690 let settlement = drive_native_broadcast_actions(&session, adapter).await;
4691 usage?;
4692 settlement?;
4693 }
4694 Ok(accepted)
4695}
4696
4697#[tauri::command]
4698async fn get_openrtc_broadcast_stats(
4699 state: tauri::State<'_, OpenRtcTauriState>,
4700 handle_id: String,
4701) -> Result<openrtc::broadcast::BroadcastStats, String> {
4702 let session = state
4703 .broadcast_sessions
4704 .lock()
4705 .await
4706 .get(&handle_id)
4707 .cloned()
4708 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4709 Ok(session.stats())
4710}
4711
4712#[tauri::command]
4713async fn revoke_openrtc_broadcast(
4714 state: tauri::State<'_, OpenRtcTauriState>,
4715 handle_id: String,
4716 grant_generation: u64,
4717) -> Result<(), String> {
4718 let driver = native_broadcast_driver(&state, &handle_id).await?;
4719 let _driver = driver.lock().await;
4720 let session = state
4721 .broadcast_sessions
4722 .lock()
4723 .await
4724 .get(&handle_id)
4725 .cloned()
4726 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4727 session
4728 .revoke(grant_generation)
4729 .map_err(|error| error.to_string())?;
4730 let adapter = state
4731 .native_broadcast_adapter
4732 .as_deref()
4733 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4734 drive_native_broadcast_actions(&session, adapter).await
4735}
4736
4737#[tauri::command]
4738async fn close_openrtc_broadcast(
4739 state: tauri::State<'_, OpenRtcTauriState>,
4740 handle_id: String,
4741) -> Result<(), String> {
4742 let driver = native_broadcast_driver(&state, &handle_id).await?;
4743 let _driver = driver.lock().await;
4744 let session = state
4745 .broadcast_sessions
4746 .lock()
4747 .await
4748 .remove(&handle_id)
4749 .ok_or_else(|| "broadcast session is unavailable".to_string())?;
4750 session.close();
4751 let adapter = state
4752 .native_broadcast_adapter
4753 .as_deref()
4754 .ok_or_else(|| "native managed broadcast adapter is unavailable".to_string())?;
4755 let result = drive_native_broadcast_actions(&session, adapter).await;
4756 state.broadcast_drivers.lock().await.remove(&handle_id);
4757 result
4758}
4759
4760#[tauri::command]
4761async fn record_sparse_fanout_forward_queue_drop(
4762 state: tauri::State<'_, OpenRtcTauriState>,
4763 capability_key: String,
4764 count: u64,
4765) -> Result<(), String> {
4766 state
4767 .client()
4768 .record_sparse_fanout_forward_queue_drop(
4769 &required_native_capability_part(capability_key, "capability key")?,
4770 count,
4771 )
4772 .await
4773 .map_err(|error| error.to_string())
4774}
4775
4776#[tauri::command]
4777async fn is_peer_connected(
4778 state: tauri::State<'_, OpenRtcTauriState>,
4779 node_id: String,
4780) -> Result<bool, String> {
4781 state
4782 .client()
4783 .is_connected_str(&node_id)
4784 .await
4785 .map_err(|error| error.to_string())
4786}
4787
4788#[derive(Debug, Serialize)]
4789#[serde(rename_all = "camelCase")]
4790struct PortableMediaPublicationStart {
4791 publication_id: String,
4792 media_generation: u32,
4793 control: Vec<u8>,
4794}
4795
4796fn media_kind_name(kind: openrtc::media::MediaKind) -> &'static str {
4797 match kind {
4798 openrtc::media::MediaKind::Audio => "audio",
4799 openrtc::media::MediaKind::Video => "video",
4800 openrtc::media::MediaKind::Screen => "screen",
4801 openrtc::media::MediaKind::Data => "data",
4802 }
4803}
4804
4805fn media_codec_name(codec: openrtc::media::MediaCodec) -> &'static str {
4806 match codec {
4807 openrtc::media::MediaCodec::Opus => "opus",
4808 openrtc::media::MediaCodec::H264 => "h264",
4809 openrtc::media::MediaCodec::Vp8 => "vp8",
4810 openrtc::media::MediaCodec::Vp9 => "vp9",
4811 openrtc::media::MediaCodec::Av1 => "av1",
4812 openrtc::media::MediaCodec::Pcm => "pcm",
4813 openrtc::media::MediaCodec::Opaque => "opaque",
4814 }
4815}
4816
4817fn parse_media_publication_id(value: &str) -> Result<openrtc::media::PublicationId, String> {
4818 value
4819 .trim()
4820 .parse()
4821 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())
4822}
4823
4824#[tauri::command]
4825#[allow(clippy::too_many_arguments)]
4826async fn begin_openrtc_media_publication(
4827 state: tauri::State<'_, OpenRtcTauriState>,
4828 publication_id: Option<String>,
4829 kind: String,
4830 codec: String,
4831 clock_rate: u32,
4832 coded_width: Option<u32>,
4833 coded_height: Option<u32>,
4834 channels: Option<u16>,
4835) -> Result<PortableMediaPublicationStart, String> {
4836 let publication_id = match publication_id.as_deref().map(str::trim) {
4837 Some(value) if !value.is_empty() => parse_media_publication_id(value)?,
4838 _ => openrtc::media::PublicationId::from_bytes(*uuid::Uuid::new_v4().as_bytes()),
4839 };
4840 let kind: openrtc::media::MediaKind = kind
4841 .parse()
4842 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?;
4843 if !matches!(
4844 kind,
4845 openrtc::media::MediaKind::Audio | openrtc::media::MediaKind::Video
4846 ) {
4847 return Err("browser media publications must be audio or video".to_string());
4848 }
4849 let codec = codec
4850 .parse()
4851 .map_err(|error: openrtc::media::MediaProtocolError| error.to_string())?;
4852 let publication = openrtc::media::MediaPublicationConfig {
4853 publication_id,
4854 media_generation: 0,
4855 kind,
4856 codec,
4857 clock_rate,
4858 coded_width,
4859 coded_height,
4860 channels,
4861 };
4862 let (publication, control) = state
4863 .portable_media
4864 .lock()
4865 .await
4866 .begin_publication(publication)
4867 .map_err(|error| error.to_string())?;
4868 Ok(PortableMediaPublicationStart {
4869 publication_id: publication.publication_id.to_string(),
4870 media_generation: publication.media_generation,
4871 control,
4872 })
4873}
4874
4875#[tauri::command]
4876#[allow(clippy::too_many_arguments)]
4877async fn encode_openrtc_media_sample(
4878 state: tauri::State<'_, OpenRtcTauriState>,
4879 request: Request<'_>,
4880) -> Result<Response, String> {
4881 let publication_id = required_request_header(&request, MEDIA_PUBLICATION_ID_HEADER)?;
4882 let timestamp_us = parsed_request_header(&request, MEDIA_TIMESTAMP_US_HEADER)?;
4883 openrtc::media::ensure_javascript_safe_integer(timestamp_us, "timestamp")
4884 .map_err(|error| error.to_string())?;
4885 let duration_us = parsed_request_header(&request, MEDIA_DURATION_US_HEADER)?;
4886 let keyframe = parsed_request_header(&request, MEDIA_KEYFRAME_HEADER)?;
4887 let discardable = parsed_request_header(&request, MEDIA_DISCARDABLE_HEADER)?;
4888 let payload = request_body_bytes(&request)?;
4889 let encoded = state
4890 .portable_media
4891 .lock()
4892 .await
4893 .encode_sample(
4894 parse_media_publication_id(&publication_id)?,
4895 openrtc::media::EncodedMediaSample {
4896 timestamp_us,
4897 duration_us,
4898 keyframe,
4899 discardable,
4900 payload,
4901 },
4902 )
4903 .map_err(|error| error.to_string())?;
4904 Ok(Response::new(encoded))
4905}
4906
4907#[tauri::command]
4908async fn pause_openrtc_media_publication(
4909 state: tauri::State<'_, OpenRtcTauriState>,
4910 publication_id: String,
4911) -> Result<(), String> {
4912 state
4913 .portable_media
4914 .lock()
4915 .await
4916 .pause_publication(parse_media_publication_id(&publication_id)?)
4917 .map_err(|error| error.to_string())
4918}
4919
4920#[tauri::command]
4921async fn retire_openrtc_media_publication(
4922 state: tauri::State<'_, OpenRtcTauriState>,
4923 publication_id: String,
4924) -> Result<(), String> {
4925 state
4926 .portable_media
4927 .lock()
4928 .await
4929 .retire_publication(parse_media_publication_id(&publication_id)?);
4930 Ok(())
4931}
4932
4933#[tauri::command]
4934async fn retire_openrtc_media_receiver(
4935 state: tauri::State<'_, OpenRtcTauriState>,
4936 publication_id: String,
4937 media_generation: u32,
4938) -> Result<(), String> {
4939 state.portable_media.lock().await.retire_receiver(
4940 parse_media_publication_id(&publication_id)?,
4941 media_generation,
4942 );
4943 Ok(())
4944}
4945
4946#[tauri::command]
4947async fn decode_openrtc_media_chunk(
4948 state: tauri::State<'_, OpenRtcTauriState>,
4949 request: Request<'_>,
4950) -> Result<Response, String> {
4951 let encoded = request_body_bytes(&request)?;
4952 let chunk = state
4953 .portable_media
4954 .lock()
4955 .await
4956 .decode_chunk(&encoded)
4957 .map_err(|error| error.to_string())?;
4958 openrtc::media::ensure_javascript_safe_integer(chunk.sequence, "sequence")
4959 .and_then(|_| {
4960 openrtc::media::ensure_javascript_safe_integer(chunk.timestamp_us, "timestamp")
4961 })
4962 .map_err(|error| error.to_string())?;
4963 let metadata = serde_json::to_vec(&serde_json::json!({
4964 "publicationId": chunk.publication_id.to_string(),
4965 "mediaGeneration": chunk.media_generation,
4966 "sequence": chunk.sequence,
4967 "timestampUs": chunk.timestamp_us,
4968 "durationUs": chunk.duration_us,
4969 "kind": media_kind_name(chunk.kind),
4970 "codec": media_codec_name(chunk.codec),
4971 "keyframe": chunk.keyframe,
4972 "discardable": chunk.discardable,
4973 }))
4974 .map_err(|error| format!("encode media IPC metadata failed: {error}"))?;
4975 let metadata_len =
4976 u32::try_from(metadata.len()).map_err(|_| "media IPC metadata is too large".to_string())?;
4977 let mut response = Vec::with_capacity(4 + metadata.len() + chunk.payload.len());
4978 response.extend_from_slice(&metadata_len.to_be_bytes());
4979 response.extend_from_slice(&metadata);
4980 response.extend_from_slice(&chunk.payload);
4981 Ok(Response::new(response))
4982}
4983
4984#[tauri::command]
4985async fn decode_openrtc_media_control(
4986 state: tauri::State<'_, OpenRtcTauriState>,
4987 request: Request<'_>,
4988) -> Result<serde_json::Value, String> {
4989 let encoded = request_body_bytes(&request)?;
4990 let control = state
4991 .portable_media
4992 .lock()
4993 .await
4994 .decode_control(&encoded)
4995 .map_err(|error| error.to_string())?;
4996 Ok(match control {
4997 openrtc::media::MediaControlFrame::Publish {
4998 publication_id,
4999 media_generation,
5000 kind,
5001 codec,
5002 clock_rate,
5003 coded_width,
5004 coded_height,
5005 channels,
5006 } => serde_json::json!({
5007 "type": "publish",
5008 "publicationId": publication_id.to_string(),
5009 "mediaGeneration": media_generation,
5010 "kind": media_kind_name(kind),
5011 "codec": media_codec_name(codec),
5012 "clockRate": clock_rate,
5013 "codedWidth": coded_width,
5014 "codedHeight": coded_height,
5015 "channels": channels,
5016 }),
5017 openrtc::media::MediaControlFrame::SetEnabled {
5018 publication_id,
5019 media_generation,
5020 enabled,
5021 } => serde_json::json!({
5022 "type": "set-enabled",
5023 "publicationId": publication_id.to_string(),
5024 "mediaGeneration": media_generation,
5025 "enabled": enabled,
5026 }),
5027 openrtc::media::MediaControlFrame::RequestKeyframe {
5028 publication_id,
5029 media_generation,
5030 } => serde_json::json!({
5031 "type": "request-keyframe",
5032 "publicationId": publication_id.to_string(),
5033 "mediaGeneration": media_generation,
5034 }),
5035 openrtc::media::MediaControlFrame::Stop {
5036 publication_id,
5037 media_generation,
5038 reason,
5039 } => serde_json::json!({
5040 "type": "stop",
5041 "publicationId": publication_id.to_string(),
5042 "mediaGeneration": media_generation,
5043 "reason": reason,
5044 }),
5045 })
5046}
5047
5048#[tauri::command]
5049async fn open_peer_bi_stream<R: Runtime>(
5050 app: tauri::AppHandle<R>,
5051 state: tauri::State<'_, OpenRtcTauriState>,
5052 peer_id: String,
5053 timeout_ms: Option<u64>,
5054) -> Result<OpenBiResult, String> {
5055 open_peer_bi_with(app, state, peer_id, timeout_ms, |client, peer_id, timeout_ms| async move {
5056 client.open_peer_bi(&peer_id, timeout_ms).await
5057 })
5058 .await
5059}
5060
5061#[tauri::command]
5062async fn open_peer_bi_transport_only_stream<R: Runtime>(
5063 app: tauri::AppHandle<R>,
5064 state: tauri::State<'_, OpenRtcTauriState>,
5065 peer_id: String,
5066 timeout_ms: Option<u64>,
5067) -> Result<OpenBiResult, String> {
5068 open_peer_bi_with(
5069 app,
5070 state,
5071 peer_id,
5072 timeout_ms,
5073 |client, peer_id, timeout_ms| async move {
5074 client
5075 .open_peer_bi_transport_only(&peer_id, timeout_ms)
5076 .await
5077 .map(|(connection_id, remote_node_id, send, recv)| {
5078 (
5079 connection_id,
5080 remote_node_id,
5081 PeerSendStream::plain(send),
5082 PeerRecvStream::plain(recv),
5083 )
5084 })
5085 },
5086 )
5087 .await
5088}
5089
5090#[tauri::command]
5091async fn open_peer_uni_stream(
5092 state: tauri::State<'_, OpenRtcTauriState>,
5093 peer_id: String,
5094 timeout_ms: Option<u64>,
5095) -> Result<OpenUniResult, String> {
5096 let peer_id = peer_id.trim().to_string();
5097 if peer_id.is_empty() {
5098 return Err("peerId is required".to_string());
5099 }
5100 let (connection_id, remote_node_id, send) = state
5101 .client()
5102 .open_peer_uni(&peer_id, timeout_ms)
5103 .await
5104 .map_err(|error| format!("open peer uni stream failed: {error}"))?;
5105 let stream_id = uuid::Uuid::new_v4().to_string();
5106 state
5107 .peer_uni_streams
5108 .lock()
5109 .await
5110 .insert(stream_id.clone(), Arc::new(Mutex::new(Some(send))));
5111 Ok(OpenUniResult {
5112 stream_id,
5113 connection_id,
5114 remote_node_id,
5115 })
5116}
5117
5118#[tauri::command]
5119async fn write_peer_bi_stream(
5120 state: tauri::State<'_, OpenRtcTauriState>,
5121 request: Request<'_>,
5122) -> Result<(), String> {
5123 let stream_id = required_request_header(&request, PEER_BI_STREAM_ID_HEADER)?;
5124 let bytes = request_body_bytes(&request)?;
5125
5126 let send = {
5127 let streams = state.peer_bi_streams.lock().await;
5128 streams
5129 .get(&stream_id)
5130 .map(|handle| handle.send.clone())
5131 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?
5132 };
5133 let result = send
5134 .lock()
5135 .await
5136 .as_mut()
5137 .ok_or_else(|| format!("peer bi stream already closed: {stream_id}"))?
5138 .write_all(&bytes)
5139 .await
5140 .map_err(|error| format!("write peer bi stream failed: {error}"));
5141 result
5142}
5143
5144#[tauri::command]
5147async fn finish_peer_bi_stream_send(
5148 state: tauri::State<'_, OpenRtcTauriState>,
5149 stream_id: String,
5150) -> Result<(), String> {
5151 let stream_id = stream_id.trim().to_string();
5152 if stream_id.is_empty() {
5153 return Err("streamId is required".to_string());
5154 }
5155 let send = {
5156 let streams = state.peer_bi_streams.lock().await;
5157 streams
5158 .get(&stream_id)
5159 .map(|handle| handle.send.clone())
5160 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?
5161 };
5162 if let Some(send) = send.lock().await.take() {
5163 send.finish()
5164 .map_err(|error| format!("finish peer bi stream send failed: {error}"))?;
5165 }
5166 Ok(())
5167}
5168
5169#[tauri::command]
5170async fn start_peer_bi_stream_read<R: Runtime>(
5171 _app: tauri::AppHandle<R>,
5172 state: tauri::State<'_, OpenRtcTauriState>,
5173 stream_id: String,
5174 channel: Channel<Response>,
5175) -> Result<(), String> {
5176 let stream_id = stream_id.trim().to_string();
5177 if stream_id.is_empty() {
5178 return Err("streamId is required".to_string());
5179 }
5180
5181 let mut streams = state.peer_bi_streams.lock().await;
5182 let handle = streams
5183 .get_mut(&stream_id)
5184 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?;
5185 if handle.read_task.is_some() {
5186 return Ok(());
5187 }
5188 let recv = handle
5189 .recv
5190 .take()
5191 .ok_or_else(|| format!("peer bi stream reader already consumed: {stream_id}"))?;
5192 handle.read_task = Some(spawn_peer_bi_stream_reader(channel, stream_id, recv));
5193 Ok(())
5194}
5195
5196#[tauri::command]
5202async fn cancel_peer_bi_stream_read(
5203 state: tauri::State<'_, OpenRtcTauriState>,
5204 stream_id: String,
5205) -> Result<(), String> {
5206 let stream_id = stream_id.trim().to_string();
5207 if stream_id.is_empty() {
5208 return Err("streamId is required".to_string());
5209 }
5210
5211 let mut streams = state.peer_bi_streams.lock().await;
5212 let handle = streams
5213 .get_mut(&stream_id)
5214 .ok_or_else(|| format!("peer bi stream not found: {stream_id}"))?;
5215 handle.recv.take();
5216 if let Some(read_task) = handle.read_task.take() {
5217 read_task.abort();
5218 }
5219 Ok(())
5220}
5221
5222#[tauri::command]
5223async fn close_peer_bi_stream(
5224 state: tauri::State<'_, OpenRtcTauriState>,
5225 stream_id: String,
5226) -> Result<(), String> {
5227 let stream_id = stream_id.trim().to_string();
5228 if stream_id.is_empty() {
5229 return Err("streamId is required".to_string());
5230 }
5231
5232 let handle = {
5233 let mut streams = state.peer_bi_streams.lock().await;
5234 streams.remove(&stream_id)
5235 };
5236 if let Some(handle) = handle {
5237 let mut send = handle.send.lock().await;
5240 if let Some(send) = send.take() {
5241 let _ = send.finish();
5242 }
5243 drop(send);
5244 if let Some(read_task) = handle.read_task {
5245 read_task.abort();
5246 }
5247 }
5248 Ok(())
5249}
5250
5251#[tauri::command]
5252async fn write_peer_uni_stream(
5253 state: tauri::State<'_, OpenRtcTauriState>,
5254 request: Request<'_>,
5255) -> Result<(), String> {
5256 let stream_id = required_request_header(&request, PEER_BI_STREAM_ID_HEADER)?;
5257 let bytes = request_body_bytes(&request)?;
5258 let send = {
5259 let streams = state.peer_uni_streams.lock().await;
5260 streams
5261 .get(&stream_id)
5262 .cloned()
5263 .ok_or_else(|| format!("peer uni stream not found: {stream_id}"))?
5264 };
5265 let result = send
5266 .lock()
5267 .await
5268 .as_mut()
5269 .ok_or_else(|| format!("peer uni stream already closed: {stream_id}"))?
5270 .write_all(&bytes)
5271 .await
5272 .map_err(|error| format!("write peer uni stream failed: {error}"));
5273 result
5274}
5275
5276#[tauri::command]
5277async fn close_peer_uni_stream(
5278 state: tauri::State<'_, OpenRtcTauriState>,
5279 stream_id: String,
5280) -> Result<(), String> {
5281 let stream_id = stream_id.trim().to_string();
5282 if stream_id.is_empty() {
5283 return Err("streamId is required".to_string());
5284 }
5285 let send = state.peer_uni_streams.lock().await.remove(&stream_id);
5286 if let Some(send) = send {
5287 if let Some(send) = send.lock().await.take() {
5288 let _ = send.finish();
5289 }
5290 }
5291 Ok(())
5292}
5293
5294#[cfg(test)]
5295mod tests {
5296 use super::*;
5297 use std::collections::HashMap as StdHashMap;
5298 use std::sync::atomic::{AtomicUsize, Ordering};
5299 use std::sync::Mutex as StdMutex;
5300 use tokio::sync::oneshot;
5301
5302 #[test]
5303 fn initial_auto_connect_exclusions_are_validated_and_deduplicated() {
5304 assert_eq!(
5305 normalize_initial_auto_connect_exclusions(vec![
5306 " device-b ".into(),
5307 "device-a".into(),
5308 "device-b".into(),
5309 ])
5310 .unwrap(),
5311 vec!["device-a", "device-b"],
5312 );
5313 assert!(normalize_initial_auto_connect_exclusions(vec!["".into()]).is_err());
5314 assert!(normalize_initial_auto_connect_exclusions(vec!["x".into(); 251]).is_err());
5315 }
5316
5317 #[test]
5318 fn native_startup_denials_keep_structured_metadata_and_legacy_strings() {
5319 for code in openrtc::service_errors::SERVICE_ERROR_CODES {
5320 let error = NativeCommandError::Service {
5321 message: "OpenRTC service request denied",
5322 details: openrtc::service_errors::ServiceError::from_value(&serde_json::json!({
5323 "code": code, "retryable": false, "scope": "app", "operation": "gateway.grant.issue",
5324 "requestId": "startup-1", "retryAfterMs": 1200, "resetAt": 1800000000000_u64,
5325 "providerCost": 99,
5326 })).unwrap(),
5327 };
5328 let value = serde_json::to_value(error).unwrap();
5329 assert_eq!(value["code"], *code);
5330 assert_eq!(value["retryable"], false);
5331 assert_eq!(value["scope"], "app");
5332 assert_eq!(value["requestId"], "startup-1");
5333 assert_eq!(value["retryAfterMs"], 1200);
5334 assert_eq!(value["resetAt"], 1800000000000_u64);
5335 assert_eq!(value["message"], "OpenRTC service request denied");
5336 assert!(value.get("providerCost").is_none());
5337 }
5338 let legacy = NativeCommandError::from_runtime(anyhow::anyhow!("host unavailable"));
5339 assert_eq!(serde_json::to_value(legacy).unwrap(), "host unavailable");
5340 }
5341
5342 #[cfg(feature = "testing-endpoints")]
5343 #[tokio::test]
5344 async fn real_http_denial_reaches_native_command_encoder() {
5345 use tokio::io::{AsyncReadExt, AsyncWriteExt};
5346 struct DenialSigner;
5347 impl openrtc::native::DeviceSigner for DenialSigner {
5348 fn public_jwk(&self, _: &str) -> anyhow::Result<serde_json::Value> {
5349 Ok(serde_json::json!({ "kty": "OKP", "crv": "Ed25519",
5350 "x": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" }))
5351 }
5352 fn sign(&self, _: &str, _: &[u8]) -> anyhow::Result<Vec<u8>> {
5353 Ok(vec![0; 64])
5355 }
5356 }
5357 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
5358 let address = listener.local_addr().unwrap();
5359 let server = tokio::spawn(async move {
5360 let (mut stream, _) = tokio::time::timeout(std::time::Duration::from_secs(5), listener.accept())
5361 .await.unwrap().unwrap();
5362 let mut bytes = Vec::new();
5363 let mut buffer = [0_u8; 4096];
5364 loop {
5365 let read = tokio::time::timeout(std::time::Duration::from_secs(5), stream.read(&mut buffer))
5366 .await.unwrap().unwrap();
5367 assert!(read > 0);
5368 bytes.extend_from_slice(&buffer[..read]);
5369 assert!(bytes.len() < 64 * 1024);
5370 if let Some(end) = bytes.windows(4).position(|part| part == b"\r\n\r\n") {
5371 let length = String::from_utf8_lossy(&bytes[..end]).lines().find_map(|line| {
5372 let (name, value) = line.split_once(':')?;
5373 name.eq_ignore_ascii_case("content-length").then(|| value.trim().parse::<usize>().unwrap())
5374 }).unwrap();
5375 assert!(length < 32 * 1024);
5376 if bytes.len() >= end + 4 + length { break; }
5377 }
5378 }
5379 assert!(String::from_utf8_lossy(&bytes).starts_with("POST /v2/capabilities "));
5380 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}"#;
5381 stream.write_all(format!(
5382 "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,
5383 ).as_bytes()).await.unwrap();
5384 });
5385 let control = openrtc::native::ControlPlane::anonymous(
5386 "pk_live_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
5387 Arc::new(DenialSigner),
5388 ).unwrap().with_testing_endpoints(format!("http://{address}"), "http://127.0.0.1:1").unwrap();
5389 let result = tokio::time::timeout(std::time::Duration::from_secs(5),
5390 control.join_space("local-room", "desktop", openrtc::native::CapabilityOptions::default()),
5391 ).await.expect("local denial must settle without retry");
5392 let error = match result { Err(error) => error, Ok(_) => panic!("denied capability admitted") };
5393 server.await.unwrap();
5394 let value = serde_json::to_value(NativeCommandError::from_runtime(error.context("private host context"))).unwrap();
5395 assert_eq!(value["code"], "app-budget-exhausted");
5396 assert_eq!(value["retryable"], false);
5397 assert_eq!(value["operation"], "capability.issue");
5398 assert_eq!(value["requestId"], "local-denial");
5399 assert_eq!(value["retryAfterMs"], 1200);
5400 assert_eq!(value["message"], "OpenRTC service request denied");
5401 assert!(!value.to_string().contains("private"));
5402 assert!(value.get("providerCost").is_none());
5403 }
5404
5405 #[test]
5406 fn native_projection_rejects_competing_owner_without_replacing_channel() {
5407 let mut slot = None;
5408 let first = Channel::new(|_| Ok(()));
5409 let first_id = first.id();
5410 install_native_projection(&mut slot, "main".into(), "first".into(), first).unwrap();
5411 let error = install_native_projection(
5412 &mut slot,
5413 "settings".into(),
5414 "second".into(),
5415 Channel::new(|_| Ok(())),
5416 )
5417 .unwrap_err();
5418 assert!(error.contains("already has a process owner"));
5419 assert_eq!(slot.as_ref().unwrap().request_id, "first");
5420 assert_eq!(slot.as_ref().unwrap().channel.id(), first_id);
5421
5422 let refreshed = Channel::new(|_| Ok(()));
5423 let refreshed_id = refreshed.id();
5424 install_native_projection(&mut slot, "main".into(), "first".into(), refreshed).unwrap();
5425 assert_eq!(slot.as_ref().unwrap().channel.id(), refreshed_id);
5426 slot = None;
5427 install_native_projection(
5428 &mut slot,
5429 "settings".into(),
5430 "second".into(),
5431 Channel::new(|_| Ok(())),
5432 )
5433 .unwrap();
5434 assert_eq!(slot.as_ref().unwrap().request_id, "second");
5435 assert!(install_native_projection(
5436 &mut slot,
5437 "main".into(),
5438 "first".into(),
5439 Channel::new(|_| Ok(())),
5440 )
5441 .is_err());
5442 assert_eq!(slot.as_ref().unwrap().request_id, "second");
5443 }
5444
5445 #[test]
5446 fn native_projection_same_webview_reclaims_after_reload() {
5447 let mut slot = None;
5448 install_native_projection(
5449 &mut slot,
5450 "main".into(),
5451 "before-reload".into(),
5452 Channel::new(|_| Ok(())),
5453 )
5454 .unwrap();
5455
5456 install_native_projection(
5457 &mut slot,
5458 "main".into(),
5459 "after-reload".into(),
5460 Channel::new(|_| Ok(())),
5461 )
5462 .unwrap();
5463
5464 assert_eq!(slot.as_ref().unwrap().webview_label, "main");
5465 assert_eq!(slot.as_ref().unwrap().request_id, "after-reload");
5466 }
5467
5468 #[tokio::test]
5469 async fn native_service_errors_reject_retired_identity_and_wrong_avenue() {
5470 let registry = Arc::new(Mutex::new(NativeCapabilityRegistry::default()));
5471 {
5472 let mut state = registry.lock().await;
5473 state.register("devices:p".into(), "devices".into(), "p".into(), false).unwrap();
5474 state.registrations.get_mut("devices:p").unwrap().native_source =
5475 Some(NativeRosterSource::Active("identity-new".into()));
5476 }
5477 let received = Arc::new(StdMutex::new(Vec::<serde_json::Value>::new()));
5478 let sink = received.clone();
5479 let channel = Channel::new(move |body| {
5480 if let tauri::ipc::InvokeResponseBody::Json(json) = body {
5481 sink.lock().unwrap().push(serde_json::from_str(&json).unwrap());
5482 }
5483 Ok(())
5484 });
5485 let projection = Arc::new(Mutex::new(Some(NativeProjectionSubscription {
5486 webview_label: "main".into(), request_id: "current-request".into(), channel,
5487 })));
5488 let mut observation = openrtc::native::NativeServiceErrorObservation {
5489 avenue: serde_json::from_value(serde_json::json!({ "kind": "user", "id": "p" })).unwrap(),
5490 runtime_instance_id: "runtime:1".into(),
5491 service_error: openrtc::service_errors::ServiceError::from_value(&serde_json::json!({
5492 "code": "app-budget-exhausted", "retryable": false, "scope": "app", "providerCost": 99,
5493 })).unwrap(),
5494 };
5495 assert!(!forward_native_service_error(®istry, &projection, "identity-old", "devices:p", &observation).await);
5496 observation.avenue.id = "another-principal".into();
5497 assert!(!forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5498 observation.avenue.id = "p".into();
5499 assert!(forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5500 let events = received.lock().unwrap();
5501 assert_eq!(events.len(), 1);
5502 assert_eq!(events[0]["kind"], "service-error");
5503 assert_eq!(events[0]["requestId"], "current-request");
5504 assert_eq!(events[0]["capabilityKeys"], serde_json::json!(["devices:p"]));
5505 assert_eq!(events[0]["payload"]["identityId"], "identity-new");
5506 assert_eq!(events[0]["payload"]["serviceError"]["code"], "app-budget-exhausted");
5507 assert!(events[0]["payload"]["serviceError"].get("providerCost").is_none());
5508 drop(events);
5509 registry.lock().await.registrations.get_mut("devices:p").unwrap().native_source =
5510 Some(NativeRosterSource::Retired);
5511 assert!(!forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5512 registry.lock().await.registrations.remove("devices:p");
5513 assert!(!forward_native_service_error(®istry, &projection, "identity-new", "devices:p", &observation).await);
5514 assert_eq!(received.lock().unwrap().len(), 1);
5515 }
5516
5517 #[tokio::test]
5518 async fn active_native_roster_is_replayed_for_the_same_capability_after_reload() {
5519 let (_keep_alive, wait) = oneshot::channel::<()>();
5520 let roster = NativeDeviceRoster {
5521 capability_key: "devices:user-1".into(),
5522 task: Some(tokio::spawn(async move {
5523 let _ = wait.await;
5524 })),
5525 };
5526
5527 assert!(native_roster_is_active_for_capability(Some(&roster), "devices:user-1",).unwrap());
5528 assert!(
5529 native_roster_is_active_for_capability(Some(&roster), "devices:user-2",)
5530 .unwrap_err()
5531 .contains("already have a capability owner")
5532 );
5533 }
5534
5535 struct FakeTransportInstaller {
5536 installs: Arc<AtomicUsize>,
5537 }
5538
5539 #[derive(Default)]
5540 struct FakeDeviceKeySigner {
5541 records: StdMutex<StdHashMap<String, String>>,
5542 }
5543
5544 impl DeviceKeySigner for FakeDeviceKeySigner {
5545 fn public_jwk(&self, _app_tag: &str) -> Result<serde_json::Value, String> {
5546 Ok(serde_json::json!({
5547 "kty": "OKP",
5548 "crv": "Ed25519",
5549 "x": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
5550 }))
5551 }
5552
5553 fn sign(&self, _app_tag: &str, challenge: &[u8]) -> Result<Vec<u8>, String> {
5554 assert_eq!(challenge, b"openrtc:v2:test");
5555 Ok(vec![7; 64])
5556 }
5557
5558 fn delete(&self, _app_tag: &str) -> Result<(), String> {
5559 Ok(())
5560 }
5561
5562 fn read_secure_record(&self, app_tag: &str, key: &str) -> Result<Option<String>, String> {
5563 Ok(self
5564 .records
5565 .lock()
5566 .unwrap()
5567 .get(&format!("{app_tag}:{key}"))
5568 .cloned())
5569 }
5570
5571 fn write_secure_record(&self, app_tag: &str, key: &str, value: &str) -> Result<(), String> {
5572 self.records
5573 .lock()
5574 .unwrap()
5575 .insert(format!("{app_tag}:{key}"), value.to_string());
5576 Ok(())
5577 }
5578
5579 fn delete_secure_record(&self, app_tag: &str, key: &str) -> Result<(), String> {
5580 self.records
5581 .lock()
5582 .unwrap()
5583 .remove(&format!("{app_tag}:{key}"));
5584 Ok(())
5585 }
5586 }
5587
5588 impl TransportInstaller for FakeTransportInstaller {
5589 fn id(&self) -> &'static str {
5590 "ble"
5591 }
5592
5593 fn is_requested(&self, config: &openrtc::client::TransportConfig) -> bool {
5594 config.ble.as_ref().is_some_and(|ble| ble.enabled)
5595 }
5596
5597 fn install(
5598 &self,
5599 _client: Arc<openrtc::client::Client>,
5600 _config: openrtc::client::TransportConfig,
5601 _context: InstallContext,
5602 ) -> InstallFuture {
5603 self.installs.fetch_add(1, Ordering::SeqCst);
5604 Box::pin(async { Ok(Box::new(()) as Box<dyn std::any::Any + Send + Sync>) })
5605 }
5606 }
5607
5608 #[derive(Debug)]
5609 struct UnavailableTransportInstaller;
5610
5611 impl TransportInstaller for UnavailableTransportInstaller {
5612 fn id(&self) -> &'static str {
5613 "ble"
5614 }
5615
5616 fn is_requested(&self, config: &openrtc::client::TransportConfig) -> bool {
5617 config.ble.as_ref().is_some_and(|ble| ble.enabled)
5618 }
5619
5620 fn install(
5621 &self,
5622 _client: Arc<openrtc::client::Client>,
5623 _config: openrtc::client::TransportConfig,
5624 _context: InstallContext,
5625 ) -> InstallFuture {
5626 Box::pin(async { Err("Bluetooth hardware unavailable".to_string()) })
5627 }
5628 }
5629
5630 fn requested_ble_config() -> openrtc::client::TransportConfig {
5631 openrtc::client::TransportConfig {
5632 ble: Some(openrtc::client::BleConfig {
5633 enabled: true,
5634 ..openrtc::client::BleConfig::default()
5635 }),
5636 ..openrtc::client::TransportConfig::default()
5637 }
5638 }
5639
5640 fn test_config() -> OpenRtcTauriConfig {
5641 OpenRtcTauriConfig {
5642 api_key: format!("pk_test_{}", "a".repeat(40)),
5643 ..OpenRtcTauriConfig::default()
5644 }
5645 }
5646
5647 #[test]
5648 fn native_room_architecture_requires_fixed_intent_until_rust_owns_the_gateway_handle() {
5649 let mut registry = NativeCapabilityRegistry::default();
5650 registry
5651 .register_with_architecture(
5652 "room:adaptive".to_string(),
5653 "room".to_string(),
5654 "adaptive".to_string(),
5655 true,
5656 Some(openrtc::native::RoomArchitectureMode::Auto),
5657 )
5658 .expect("room registration");
5659 assert!(
5660 !registry.registrations["room:adaptive"].uses_sparse_fanout(),
5661 "caller-supplied auto state cannot promote sparse fanout",
5662 );
5663 registry
5664 .register_with_architecture(
5665 "room:fixed-sparse".to_string(),
5666 "room".to_string(),
5667 "fixed-sparse".to_string(),
5668 false,
5669 Some(openrtc::native::RoomArchitectureMode::Sparse),
5670 )
5671 .expect("fixed sparse room registration");
5672 assert!(registry.registrations["room:fixed-sparse"].uses_sparse_fanout());
5673 }
5674
5675 #[test]
5676 fn public_devices_capability_owns_the_matching_native_user_avenue() {
5677 let registration = NativeCapabilityRegistration {
5678 avenue_kind: "devices".to_string(),
5679 avenue_id: "principal-1".to_string(),
5680 sparse_fanout: false,
5681 requested_architecture: None,
5682 desired_revision: 0,
5683 desired_peers: Vec::new(),
5684 native_source: None,
5685 };
5686
5687 assert!(registration.owns_native_devices("principal-1"));
5688 assert!(!registration.owns_native_devices("principal-2"));
5689
5690 let mut wrong_kind = registration.clone();
5691 wrong_kind.avenue_kind = "user".to_string();
5692 assert!(!wrong_kind.owns_native_devices("principal-1"));
5693 }
5694
5695 #[test]
5696 fn native_room_architecture_rejects_non_room_and_respects_fixed_mesh_intent() {
5697 let mut registry = NativeCapabilityRegistry::default();
5698 assert!(registry
5699 .register_with_architecture(
5700 "space:not-room".to_string(),
5701 "space".to_string(),
5702 "not-room".to_string(),
5703 false,
5704 Some(openrtc::native::RoomArchitectureMode::Auto),
5705 )
5706 .is_err());
5707 registry
5708 .register_with_architecture(
5709 "room:fixed".to_string(),
5710 "room".to_string(),
5711 "fixed".to_string(),
5712 false,
5713 Some(openrtc::native::RoomArchitectureMode::Mesh),
5714 )
5715 .expect("fixed room registration");
5716 assert!(!registry.registrations["room:fixed"].uses_sparse_fanout());
5717 }
5718
5719 #[test]
5720 fn native_capability_registry_isolates_duplicate_peer_channels() {
5721 let mut registry = NativeCapabilityRegistry::default();
5722 registry
5723 .register(
5724 "devices:user-1".to_string(),
5725 "devices".to_string(),
5726 "user-1".to_string(),
5727 false,
5728 )
5729 .expect("devices registration");
5730 registry
5731 .register(
5732 "space:room-1".to_string(),
5733 "space".to_string(),
5734 "room-1".to_string(),
5735 false,
5736 )
5737 .expect("space registration");
5738 registry
5739 .registrations
5740 .get_mut("devices:user-1")
5741 .unwrap()
5742 .desired_peers = vec![serde_json::json!({
5743 "deviceId": "shared-peer",
5744 "nodeId": "device-route"
5745 })];
5746 registry
5747 .registrations
5748 .get_mut("space:room-1")
5749 .unwrap()
5750 .desired_peers = vec![serde_json::json!({
5751 "deviceId": "shared-peer",
5752 "nodeId": "space-route"
5753 })];
5754
5755 assert_eq!(
5756 registry.capability_keys_for_identity(
5757 Some("shared-peer"),
5758 Some("shared-peer"),
5759 None,
5760 None,
5761 ),
5762 vec!["devices:user-1".to_string(), "space:room-1".to_string()]
5763 );
5764
5765 let explicit_channel = openrtc::stream_metadata::ChannelMetadata {
5766 channel_id: openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID.to_string(),
5767 metadata: Some(serde_json::Map::from_iter([(
5768 "openrtcCapability".to_string(),
5769 serde_json::json!("space:room-1"),
5770 )])),
5771 };
5772 assert_eq!(
5773 registry.projected_stream_capability(&explicit_channel, &[]),
5774 Some("space:room-1".to_string())
5775 );
5776
5777 let unscoped_channel = openrtc::stream_metadata::ChannelMetadata {
5778 channel_id: openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID.to_string(),
5779 metadata: None,
5780 };
5781 assert_eq!(
5782 registry.projected_stream_capability(&unscoped_channel, &[]),
5783 None
5784 );
5785 }
5786
5787 #[test]
5788 fn native_capability_registry_routes_state_by_authenticated_scope() {
5789 let mut registry = NativeCapabilityRegistry::default();
5790 registry
5791 .register(
5792 "ticket:share-1".to_string(),
5793 "ticket".to_string(),
5794 "share-1".to_string(),
5795 false,
5796 )
5797 .expect("ticket registration");
5798 registry
5799 .register(
5800 "space:other".to_string(),
5801 "space".to_string(),
5802 "other".to_string(),
5803 false,
5804 )
5805 .expect("unrelated registration");
5806
5807 let snapshot = openrtc::client::StateSnapshot {
5808 connection_id: "mesh-connection".to_string(),
5809 device_id: Some("mesh-peer".to_string()),
5810 device_id_hint: None,
5811 remote_node_id: Some("mesh-node".to_string()),
5812 state: "connected".to_string(),
5813 transport_state: "connected".to_string(),
5814 protocol_state: "routable".to_string(),
5815 routable: true,
5816 logical_session_terminal: false,
5817 readiness_state: openrtc::client::ReadinessState::Routable,
5818 readiness_reason: "settled".to_string(),
5819 transport_generation: 1,
5820 route_generation: 1,
5821 active_transport_stable_id: Some(1),
5822 active_transport: "iroh".to_string(),
5823 parallel_transport: None,
5824 latency_ms: None,
5825 carrier: None,
5826 metrics_sampled_at_ms: None,
5827 replacement_in_progress: false,
5828 last_lifecycle_transition_at_ms: 1,
5829 transition_count: 1,
5830 connecting_transition_count: 1,
5831 replacement_count: 0,
5832 retire_count: 0,
5833 last_disconnect_reason: None,
5834 last_reconnect_reason: None,
5835 scopes: vec![
5836 " unregistered:scope ".to_string(),
5837 " ticket:share-1 ".to_string(),
5838 "ticket:share-1".to_string(),
5839 ],
5840 error: None,
5841 created_at: 1,
5842 updated_at: 1,
5843 };
5844
5845 assert_eq!(
5846 registry.capability_keys_for_state(&snapshot),
5847 vec!["ticket:share-1".to_string()],
5848 "the registered authenticated scope routes the state without a desired-peer roster",
5849 );
5850 }
5851
5852 #[test]
5853 fn native_capability_registry_aggregates_without_duplicating_root_peers() {
5854 let mut registry = NativeCapabilityRegistry::default();
5855 for (key, kind, id) in [
5856 ("devices:user-1", "devices", "user-1"),
5857 ("space:room-1", "space", "room-1"),
5858 ] {
5859 registry
5860 .register(key.to_string(), kind.to_string(), id.to_string(), false)
5861 .expect("capability registration");
5862 registry.registrations.get_mut(key).unwrap().desired_peers =
5863 vec![serde_json::json!({"deviceId": "shared-peer", "ticket": "ticket-a"})];
5864 }
5865
5866 let (first_revision, peers_json) = registry.aggregate_desired_peers().unwrap();
5867 let peers: Vec<serde_json::Value> = serde_json::from_str(&peers_json).unwrap();
5868 assert_eq!(first_revision, 1);
5869 assert_eq!(peers.len(), 1, "one physical peer must be dialed once");
5870
5871 registry
5872 .register(
5873 "devices:user-1".to_string(),
5874 "devices".to_string(),
5875 "user-1".to_string(),
5876 false,
5877 )
5878 .expect("exact registration is idempotent");
5879 assert!(registry
5880 .register(
5881 "devices:user-1".to_string(),
5882 "space".to_string(),
5883 "room-1".to_string(),
5884 false,
5885 )
5886 .is_err());
5887 }
5888
5889 #[test]
5890 fn ticket_share_stream_projection_follows_rust_admission_scope() {
5891 let mut registry = NativeCapabilityRegistry::default();
5892 registry.register(
5893 "ticket:share_host_123".to_string(),
5894 "ticket".to_string(),
5895 "share_host_123".to_string(),
5896 false,
5897 ).unwrap();
5898 let admitted = openrtc::session_token::GrantScope("ticket:share_host_123".to_string());
5899 let wrong_scope = openrtc::session_token::GrantScope("ticket:other".to_string());
5900 let retired_scope = openrtc::session_token::GrantScope("share:share_host_123".to_string());
5901 let channel = openrtc::stream_metadata::ChannelMetadata {
5902 channel_id: "share/explicit-control".to_string(),
5903 metadata: Some(serde_json::Map::from_iter([(
5904 "openrtcCapability".to_string(),
5905 serde_json::json!("ticket:share_host_123"),
5906 )])),
5907 };
5908 assert!(is_openrtc_projected_channel(&channel));
5909 assert_eq!(
5910 registry.projected_stream_capability(&channel, std::slice::from_ref(&admitted.0)),
5911 Some("ticket:share_host_123".to_string())
5912 );
5913 assert_eq!(
5914 registry.projected_stream_capability(&channel, std::slice::from_ref(&wrong_scope.0)),
5915 None
5916 );
5917 assert_eq!(registry.projected_stream_capability(&channel, &[]), None);
5918 assert_eq!(
5919 registry.projected_stream_capability(&channel, std::slice::from_ref(&retired_scope.0)),
5920 None
5921 );
5922 let mut file_channel = channel.clone();
5923 file_channel.channel_id = "share/explicit-file".to_string();
5924 assert_eq!(
5925 registry.projected_stream_capability(&file_channel, std::slice::from_ref(&admitted.0)),
5926 Some("ticket:share_host_123".to_string())
5927 );
5928 let mut untagged = channel.clone();
5929 untagged.metadata = None;
5930 assert!(is_openrtc_projected_channel(&untagged));
5931 assert_eq!(
5932 registry.projected_stream_capability(&untagged, std::slice::from_ref(&admitted.0)),
5933 Some("ticket:share_host_123".to_string())
5934 );
5935 let mut wrong_owner = channel;
5936 wrong_owner.metadata = Some(serde_json::Map::from_iter([(
5937 "openrtcCapability".to_string(),
5938 serde_json::json!("devices:user-1"),
5939 )]));
5940 assert_eq!(
5941 registry.projected_stream_capability(&wrong_owner, std::slice::from_ref(&admitted.0)),
5942 None
5943 );
5944 registry.registrations.clear();
5945 assert_eq!(
5946 registry.projected_stream_capability(&file_channel, std::slice::from_ref(&admitted.0)),
5947 None
5948 );
5949 }
5950
5951 #[test]
5952 fn native_capability_registry_merges_sparse_roles_deterministically() {
5953 fn aggregate(reverse_registration_order: bool) -> (u64, Vec<serde_json::Value>) {
5954 let mut registry = NativeCapabilityRegistry::default();
5955 let registrations = if reverse_registration_order {
5956 [
5957 ("space:backup", "space", "backup"),
5958 ("room:active", "room", "active"),
5959 ]
5960 } else {
5961 [
5962 ("room:active", "room", "active"),
5963 ("space:backup", "space", "backup"),
5964 ]
5965 };
5966 for (key, kind, id) in registrations {
5967 registry
5968 .register(key.to_string(), kind.to_string(), id.to_string(), false)
5969 .unwrap();
5970 }
5971 registry
5972 .registrations
5973 .get_mut("room:active")
5974 .unwrap()
5975 .desired_peers = vec![serde_json::json!({
5976 "deviceId": "shared-peer",
5977 "ticket": "active-ticket",
5978 "topologyRole": "active",
5979 "topologyRevision": 3,
5980 })];
5981 registry
5982 .registrations
5983 .get_mut("space:backup")
5984 .unwrap()
5985 .desired_peers = vec![serde_json::json!({
5986 "deviceId": "shared-peer",
5987 "ticket": "backup-ticket",
5988 "topologyRole": "backup",
5989 "topologyRevision": 100,
5990 })];
5991 let (revision, peers_json) = registry.aggregate_desired_peers().unwrap();
5992 (revision, serde_json::from_str(&peers_json).unwrap())
5993 }
5994
5995 let forward = aggregate(false);
5996 let reverse = aggregate(true);
5997 assert_eq!(
5998 forward, reverse,
5999 "registration order is not lifecycle authority"
6000 );
6001 assert_eq!(forward.0, 1);
6002 assert_eq!(forward.1.len(), 1);
6003 assert_eq!(forward.1[0]["ticket"], "active-ticket");
6004 assert_eq!(forward.1[0]["topologyRole"], "active");
6005 assert_eq!(forward.1[0]["topologyRevision"], 1);
6006 }
6007
6008 #[test]
6009 fn native_capability_registry_keeps_physical_peer_until_all_references_withdraw() {
6010 let mut registry = NativeCapabilityRegistry::default();
6011 for (key, kind) in [("room:a", "room"), ("space:b", "space")] {
6012 registry
6013 .register(key.to_string(), kind.to_string(), key.to_string(), false)
6014 .unwrap();
6015 }
6016 registry
6017 .registrations
6018 .get_mut("room:a")
6019 .unwrap()
6020 .desired_peers = vec![serde_json::json!({
6021 "deviceId": "shared-peer",
6022 "topologyRole": "backup",
6023 "topologyRevision": 41,
6024 })];
6025 registry
6026 .registrations
6027 .get_mut("space:b")
6028 .unwrap()
6029 .desired_peers = vec![serde_json::json!({
6030 "deviceId": "shared-peer",
6031 "nodeId": "shared-node",
6032 "ticket": "space-ticket",
6033 "topologyRole": "active",
6034 "topologyRevision": 7,
6035 })];
6036
6037 let (_, both_json) = registry.aggregate_desired_peers().unwrap();
6038 let both: Vec<serde_json::Value> = serde_json::from_str(&both_json).unwrap();
6039 assert_eq!(both.len(), 1);
6040 assert_eq!(both[0]["topologyRole"], "active");
6041 assert_eq!(both[0]["ticket"], "space-ticket");
6042 assert_eq!(
6043 registry.registrations["room:a"].desired_peers[0]["topologyRevision"], 41,
6044 "root projection must not rewrite avenue-local lease state",
6045 );
6046
6047 registry
6048 .registrations
6049 .get_mut("space:b")
6050 .unwrap()
6051 .desired_peers
6052 .clear();
6053 let (_, room_only_json) = registry.aggregate_desired_peers().unwrap();
6054 let room_only: Vec<serde_json::Value> = serde_json::from_str(&room_only_json).unwrap();
6055 assert_eq!(
6056 room_only.len(),
6057 1,
6058 "one remaining capability keeps the leg desired"
6059 );
6060 assert_eq!(room_only[0]["topologyRole"], "backup");
6061
6062 registry
6063 .registrations
6064 .get_mut("room:a")
6065 .unwrap()
6066 .desired_peers
6067 .clear();
6068 let (_, empty_json) = registry.aggregate_desired_peers().unwrap();
6069 let empty: Vec<serde_json::Value> = serde_json::from_str(&empty_json).unwrap();
6070 assert!(
6071 empty.is_empty(),
6072 "physical desire ends only after every reference withdraws"
6073 );
6074 }
6075
6076 #[test]
6077 fn native_ticket_projection_requires_the_exact_live_scope() {
6078 let mut registry = NativeCapabilityRegistry::default();
6079 for id in ["a", "b"] {
6080 registry
6081 .register(format!("ticket:{id}"), "ticket".into(), id.into(), false)
6082 .unwrap();
6083 }
6084 let both = vec!["ticket:a".to_string(), "ticket:b".to_string()];
6085 let only_a = vec!["ticket:a".to_string()];
6086 let event = openrtc::client::NativePeerDataEvent {
6087 connection_id: "same-peer".into(),
6088 remote_node_id: None,
6089 transport: "iroh".into(),
6090 transport_stable_id: 1,
6091 transport_generation: 1,
6092 route_generation: 1,
6093 payload: serde_json::to_vec(&serde_json::json!({
6094 "capability": "ticket:b", "kind": "raw", "payload": [1]
6095 }))
6096 .unwrap(),
6097 };
6098 assert_eq!(
6099 registry.capability_keys_for_peer_data(&event, &both),
6100 ["ticket:b"]
6101 );
6102 assert!(registry
6103 .capability_keys_for_peer_data(&event, &only_a)
6104 .is_empty());
6105 assert!(registry
6106 .capability_keys_for_peer_data(&event, &[])
6107 .is_empty());
6108 for id in ["a", "b"] {
6111 let key = format!("ticket:{id}");
6112 let channel = openrtc::stream_metadata::ChannelMetadata {
6113 channel_id: "share/explicit-control".into(),
6114 metadata: Some(serde_json::Map::from_iter([(
6115 "openrtcCapability".into(),
6116 serde_json::json!(key),
6117 )])),
6118 };
6119 assert_eq!(
6120 registry.projected_stream_capability(&channel, &both),
6121 Some(key.clone())
6122 );
6123 assert_eq!(
6124 registry.projected_stream_capability(&channel, &only_a),
6125 (id == "a").then_some(key)
6126 );
6127 }
6128 let untagged = openrtc::stream_metadata::ChannelMetadata {
6129 channel_id: "share/explicit-control".into(),
6130 metadata: None,
6131 };
6132 assert!(registry
6133 .projected_stream_capability(&untagged, &both)
6134 .is_none());
6135 assert_eq!(
6136 registry.projected_stream_capability(&untagged, &only_a),
6137 Some("ticket:a".into())
6138 );
6139 }
6140
6141 #[test]
6142 fn native_capability_registry_scopes_peer_data_and_rejects_unscoped_streams() {
6143 let mut registry = NativeCapabilityRegistry::default();
6144 registry
6145 .register(
6146 "devices:user-1".to_string(),
6147 "devices".to_string(),
6148 "user-1".to_string(),
6149 false,
6150 )
6151 .unwrap();
6152 registry
6153 .registrations
6154 .get_mut("devices:user-1")
6155 .unwrap()
6156 .desired_peers = vec![serde_json::json!({"deviceId": "peer-1"})];
6157
6158 let explicit = openrtc::client::NativePeerDataEvent {
6159 connection_id: "peer-1".to_string(),
6160 remote_node_id: None,
6161 transport: "webrtc".to_string(),
6162 transport_stable_id: 1,
6163 transport_generation: 1,
6164 route_generation: 1,
6165 payload: serde_json::to_vec(&serde_json::json!({
6166 "capability": "devices:user-1",
6167 "kind": "raw",
6168 "payload": [1, 2, 3]
6169 }))
6170 .unwrap(),
6171 };
6172 assert_eq!(
6173 registry.capability_keys_for_peer_data(&explicit, &[]),
6174 vec!["devices:user-1".to_string()]
6175 );
6176
6177 let unknown = openrtc::client::NativePeerDataEvent {
6178 payload: serde_json::to_vec(&serde_json::json!({
6179 "capability": "space:unknown",
6180 "kind": "raw"
6181 }))
6182 .unwrap(),
6183 ..explicit.clone()
6184 };
6185 assert!(registry
6186 .capability_keys_for_peer_data(&unknown, &[])
6187 .is_empty());
6188
6189 registry
6190 .register(
6191 "space:shared".to_string(),
6192 "space".to_string(),
6193 "shared".to_string(),
6194 false,
6195 )
6196 .unwrap();
6197 registry
6198 .registrations
6199 .get_mut("space:shared")
6200 .unwrap()
6201 .desired_peers = vec![serde_json::json!({"deviceId": "peer-1"})];
6202 let unscoped = openrtc::client::NativePeerDataEvent {
6203 payload: serde_json::to_vec(&serde_json::json!({
6204 "kind": "raw",
6205 "payload": [1, 2, 3]
6206 }))
6207 .unwrap(),
6208 ..explicit.clone()
6209 };
6210 assert!(
6211 registry
6212 .capability_keys_for_peer_data(&unscoped, &[])
6213 .is_empty(),
6214 "a shared physical peer cannot fan unscoped data into two avenues",
6215 );
6216
6217 let unscoped_channel = openrtc::stream_metadata::ChannelMetadata {
6218 channel_id: openrtc::stream_metadata::PUBLIC_PEER_STREAM_CHANNEL_ID.to_string(),
6219 metadata: None,
6220 };
6221 assert_eq!(
6222 registry.projected_stream_capability(&unscoped_channel, &[]),
6223 None,
6224 "stream ownership must never be inferred from capability count"
6225 );
6226 }
6227
6228 #[test]
6229 fn native_device_proof_uses_host_signer_without_exporting_private_key() {
6230 let state = OpenRtcTauriState::new(test_config())
6231 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6232 assert_eq!(
6233 state
6234 .native_device_key_signer
6235 .as_deref()
6236 .unwrap()
6237 .offline_assurance("app_native_test"),
6238 openrtc::offline::OfflineAssurance::Software
6239 );
6240 let public = state
6241 .device_public_key("app_native_test")
6242 .expect("public key");
6243 assert_eq!(public["kty"], "OKP");
6244 assert_eq!(public["crv"], "Ed25519");
6245 assert!(public.get("d").is_none());
6246 let signature = state
6247 .sign_device_proof("app_native_test", "openrtc:v2:test")
6248 .expect("signature");
6249 assert_eq!(
6250 base64::engine::general_purpose::URL_SAFE_NO_PAD
6251 .decode(signature)
6252 .expect("base64"),
6253 vec![7; 64]
6254 );
6255 }
6256
6257 #[tokio::test]
6258 async fn native_identity_reload_preserves_owner_and_principal_change_retires_it() {
6259 use openrtc::native::AssertionProvider;
6260 let state = OpenRtcTauriState::new(test_config())
6261 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6262 let first = state
6263 .configure_native_identity("main", "first-user".into(), |_| Ok(()))
6264 .await
6265 .unwrap();
6266 let provider = state
6267 .native_identity
6268 .lock()
6269 .await
6270 .as_ref()
6271 .unwrap()
6272 .assertions
6273 .clone();
6274 let repeated = state
6275 .configure_native_identity("main", "first-user".into(), |_| Ok(()))
6276 .await
6277 .unwrap();
6278 assert_eq!(first, repeated);
6279 assert!(Arc::ptr_eq(
6280 &provider,
6281 &state
6282 .native_identity
6283 .lock()
6284 .await
6285 .as_ref()
6286 .unwrap()
6287 .assertions
6288 ));
6289 assert_eq!(provider.identity_epoch(), 0);
6290 assert!(state
6291 .configure_native_identity("other-window", "second-user".into(), |_| Ok(()))
6292 .await
6293 .is_err());
6294 assert_eq!(
6295 provider.identity_epoch(),
6296 0,
6297 "competing window cannot retire the owner"
6298 );
6299 let second = state
6300 .configure_native_identity("main", "second-user".into(), |_| Ok(()))
6301 .await
6302 .unwrap();
6303 assert_ne!(first, second);
6304 assert_eq!(provider.identity_epoch(), 1);
6305 assert!(provider.session_key().unwrap().is_none());
6306 let owner = state.native_identity.lock().await;
6307 assert_eq!(
6308 owner
6309 .as_ref()
6310 .unwrap()
6311 .assertions
6312 .session_key()
6313 .unwrap()
6314 .as_deref(),
6315 Some("second-user")
6316 );
6317 let next_provider = owner.as_ref().unwrap().assertions.clone();
6318 drop(owner);
6319 assert!(!state.clear_native_identity("main", &first).await.unwrap());
6320 assert!(state
6321 .clear_native_identity("other-window", &second)
6322 .await
6323 .is_err());
6324 assert_eq!(next_provider.identity_epoch(), 0);
6325 assert!(state.clear_native_identity("main", &second).await.unwrap());
6326 assert_eq!(next_provider.identity_epoch(), 1);
6327 assert!(state.native_identity.lock().await.is_none());
6328 }
6329
6330 #[tokio::test]
6331 async fn native_roster_feeds_root_and_fences_replaced_source() {
6332 let state = OpenRtcTauriState::new(test_config())
6333 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6334 let old_source = state
6335 .configure_native_identity("main", "first-user".into(), |_| Ok(()))
6336 .await
6337 .unwrap();
6338 let client = state.client();
6339 client
6340 .start_external_auto_connect(
6341 format!("{}:native-root", client.app_tag()),
6342 "local".into(),
6343 )
6344 .await
6345 .unwrap();
6346 {
6347 let mut registry = state.capability_registry.lock().await;
6348 registry
6349 .register("devices".into(), "user".into(), "principal".into(), false)
6350 .unwrap();
6351 registry
6352 .register("space".into(), "space".into(), "shared-space".into(), false)
6353 .unwrap();
6354 registry
6355 .registrations
6356 .get_mut("devices")
6357 .unwrap()
6358 .native_source = Some(NativeRosterSource::Active(old_source.clone()));
6359 registry
6360 .registrations
6361 .get_mut("space")
6362 .unwrap()
6363 .desired_peers = vec![serde_json::json!({
6364 "deviceId":"space-peer", "online":false,
6365 })];
6366 }
6367 let devices = ["local", "known-offline"]
6368 .into_iter()
6369 .map(|id| {
6370 serde_json::from_value::<openrtc::signaling::Device>(serde_json::json!({
6371 "deviceId":id, "deviceName":"Fixture", "online":false,
6372 }))
6373 .unwrap()
6374 })
6375 .collect::<Vec<_>>();
6376 assert!(submit_native_device_snapshot(
6377 &client,
6378 &state.capability_registry,
6379 &old_source,
6380 "devices",
6381 "local",
6382 &devices,
6383 true
6384 )
6385 .await
6386 .unwrap());
6387 {
6388 let mut registry = state.capability_registry.lock().await;
6389 let peers = ®istry.registrations["devices"].desired_peers;
6390 assert_eq!(peers.len(), 1);
6391 assert_eq!(peers[0]["deviceId"], "known-offline");
6392 assert_eq!(
6393 peers[0]["online"], false,
6394 "offline inventory remains a typed actor input"
6395 );
6396 let (_, aggregate) = registry.aggregate_desired_peers().unwrap();
6397 let aggregate: Vec<serde_json::Value> = serde_json::from_str(&aggregate).unwrap();
6398 assert_eq!(
6399 aggregate.len(),
6400 2,
6401 "native user roster must preserve another avenue"
6402 );
6403 }
6404 let new_source = state
6405 .configure_native_identity("main", "second-user".into(), |_| Ok(()))
6406 .await
6407 .unwrap();
6408 {
6409 let mut registry = state.capability_registry.lock().await;
6410 assert!(matches!(
6411 registry.registrations["devices"].native_source,
6412 Some(NativeRosterSource::Retired)
6413 ));
6414 assert!(registry.registrations["devices"].desired_peers.is_empty());
6415 registry
6416 .registrations
6417 .get_mut("devices")
6418 .unwrap()
6419 .native_source = Some(NativeRosterSource::Active(new_source.clone()));
6420 }
6421 assert!(!submit_native_device_snapshot(
6422 &client,
6423 &state.capability_registry,
6424 &old_source,
6425 "devices",
6426 "local",
6427 &[],
6428 true
6429 )
6430 .await
6431 .unwrap());
6432 assert!(submit_native_device_snapshot(
6433 &client,
6434 &state.capability_registry,
6435 &new_source,
6436 "devices",
6437 "local",
6438 &devices,
6439 false
6440 )
6441 .await
6442 .unwrap());
6443 {
6444 let registry = state.capability_registry.lock().await;
6445 assert!(registry.registrations["devices"].desired_peers.is_empty());
6446 assert_eq!(registry.registrations["space"].desired_peers.len(), 1);
6447 }
6448 state
6449 .clear_native_identity("main", &new_source)
6450 .await
6451 .unwrap();
6452 assert!(!submit_native_device_snapshot(
6453 &client,
6454 &state.capability_registry,
6455 &new_source,
6456 "devices",
6457 "local",
6458 &devices,
6459 true
6460 )
6461 .await
6462 .unwrap());
6463 client.stop_external_auto_connect().await;
6464 }
6465
6466 #[tokio::test]
6467 async fn native_identity_retirement_after_actor_stop_allows_new_sign_in() {
6468 let state = OpenRtcTauriState::new(test_config())
6469 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6470 let old_id = state.configure_native_identity("main", "old-user".into(), |_| Ok(()))
6471 .await.unwrap();
6472 {
6473 let mut registry = state.capability_registry.lock().await;
6474 registry.register("devices:old-user".into(), "devices".into(), "old-user".into(), false).unwrap();
6475 let registration = registry.registrations.get_mut("devices:old-user").unwrap();
6476 registration.native_source = Some(NativeRosterSource::Active(old_id.clone()));
6477 registration.desired_peers = vec![serde_json::json!({"deviceId":"old-peer", "online":false})];
6478 }
6479 state.client().stop_external_auto_connect().await;
6480 let next_id = state.configure_native_identity("main", "new-user".into(), |_| Ok(()))
6481 .await.expect("retirement must be idempotent after the actor has stopped");
6482 assert_ne!(old_id, next_id);
6483 let registry = state.capability_registry.lock().await;
6484 let old = ®istry.registrations["devices:old-user"];
6485 assert!(matches!(old.native_source, Some(NativeRosterSource::Retired)));
6486 assert!(old.desired_peers.is_empty());
6487 assert!(state.client().submit_external_desired_peers(999, "[]").await.is_err(),
6488 "cleanup must not restart the actor or make startup submission permissive");
6489 }
6490
6491 #[test]
6492 fn offline_support_reports_signer_and_compiled_lan_truth() {
6493 let unavailable = OpenRtcTauriState::new(test_config()).offline_runtime_support();
6494 assert!(!unavailable.provisioning);
6495 assert_eq!(unavailable.local_mesh, cfg!(feature = "transport-lan"));
6496 assert_eq!(
6497 unavailable.reason,
6498 Some("native host device signer is unavailable")
6499 );
6500
6501 let available = OpenRtcTauriState::new(test_config())
6502 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()))
6503 .offline_runtime_support();
6504 assert!(available.provisioning);
6505 assert_eq!(available.local_mesh, cfg!(feature = "transport-lan"));
6506 assert_eq!(
6507 available.reason,
6508 (!cfg!(feature = "transport-lan"))
6509 .then_some("native host was built without transport-lan"),
6510 );
6511 }
6512
6513 #[tokio::test]
6514 async fn public_runtime_status_uses_installed_host_facts() {
6515 use openrtc::client::CapabilityMaturity;
6516
6517 let unavailable_state = OpenRtcTauriState::new(test_config());
6518 let unavailable = unavailable_state
6519 .project_public_runtime_status(unavailable_state.client().runtime_status().await);
6520 assert_eq!(
6521 unavailable.product_maturity.offline_edge,
6522 CapabilityMaturity::Unavailable
6523 );
6524 assert_eq!(
6525 unavailable.product_maturity.broadcast,
6526 CapabilityMaturity::Unavailable
6527 );
6528 let serialized = serde_json::to_value(&unavailable).expect("serialized runtime status");
6529 assert_eq!(serialized["productMaturity"]["offlineEdge"], "unavailable");
6530 assert_eq!(serialized["productMaturity"]["broadcast"], "unavailable");
6531
6532 let installed_state = OpenRtcTauriState::new(test_config())
6533 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6534 let installed = installed_state
6535 .project_public_runtime_status(installed_state.client().runtime_status().await);
6536 assert_eq!(
6537 installed.product_maturity.offline_edge,
6538 if cfg!(feature = "transport-lan") {
6539 CapabilityMaturity::Preview
6540 } else {
6541 CapabilityMaturity::SupportOnly
6542 }
6543 );
6544 assert_eq!(
6545 installed.product_maturity.broadcast,
6546 if cfg!(feature = "native-broadcast-moq") {
6547 CapabilityMaturity::Preview
6548 } else {
6549 CapabilityMaturity::Unavailable
6550 }
6551 );
6552 }
6553
6554 #[test]
6555 fn native_certificate_records_round_trip_through_host_secure_storage() {
6556 let state = OpenRtcTauriState::new(test_config())
6557 .with_native_device_key_signer(Arc::new(FakeDeviceKeySigner::default()));
6558 let app_tag = "app_native_test";
6559 let key = "openrtc:v2:device-session:abc:device-1";
6560 assert_eq!(state.read_secure_record(app_tag, key).unwrap(), None);
6561 state
6562 .write_secure_record(app_tag, key, "{\"token\":\"bound\"}")
6563 .unwrap();
6564 assert_eq!(
6565 state.read_secure_record(app_tag, key).unwrap().as_deref(),
6566 Some("{\"token\":\"bound\"}")
6567 );
6568 state.delete_secure_record(app_tag, key).unwrap();
6569 assert_eq!(state.read_secure_record(app_tag, key).unwrap(), None);
6570 }
6571
6572 #[test]
6573 fn native_device_proof_fails_closed_without_secure_host_signer() {
6574 let state = OpenRtcTauriState::new(test_config());
6575 assert!(state
6576 .device_public_key("app_native_test")
6577 .expect_err("missing signer must fail")
6578 .contains("secure-storage signer"));
6579 }
6580
6581 fn native_transport_install_context() -> InstallContext {
6582 InstallContext {
6583 data_dir: std::env::temp_dir().join("openrtc-tauri-native-transport-tests"),
6584 }
6585 }
6586
6587 fn managed_session_test_result(device_id: &str) -> StartSessionResult {
6588 StartSessionResult {
6589 local_node_id: format!("node-{device_id}"),
6590 ticket_scope: Some("user-device".to_string()),
6591 ticket: None,
6592 presence_started: true,
6593 auto_connect_started: true,
6594 local_device: openrtc::native_device::NativeDeviceIdentity {
6595 device_id: device_id.to_string(),
6596 device_name: "Test Device".to_string(),
6597 created_at_ms: 1,
6598 updated_at_ms: 1,
6599 name_source: None,
6600 system_info: None,
6601 },
6602 }
6603 }
6604
6605 async fn simulate_managed_session_start(
6606 state: Arc<OpenRtcTauriState>,
6607 device_id: String,
6608 pause: Option<(oneshot::Sender<()>, oneshot::Receiver<()>)>,
6609 starts: Arc<AtomicUsize>,
6610 stopped_owners: Arc<Mutex<Vec<usize>>>,
6611 ) -> SessionDisposition {
6612 let _start_guard = state.managed_session_start_guard.lock().await;
6613 let client = state.client();
6614 let key = format!("{}:{device_id}", client.app_tag());
6615 let (disposition, active_epoch) = {
6616 let active = state.managed_session.lock().await;
6617 let disposition = managed_session_disposition(
6618 active.as_ref().map(|record| record.key.as_str()),
6619 active
6620 .as_ref()
6621 .is_some_and(|record| Arc::ptr_eq(&record.owner_client, &client)),
6622 active
6623 .as_ref()
6624 .is_some_and(|record| record.result.presence_started),
6625 active
6626 .as_ref()
6627 .is_some_and(|record| record.result.auto_connect_started),
6628 &key,
6629 true,
6630 true,
6631 );
6632 (
6633 disposition,
6634 active.as_ref().map(|record| record.owner_epoch),
6635 )
6636 };
6637
6638 if disposition == SessionDisposition::Reuse {
6639 return disposition;
6640 }
6641
6642 if disposition == SessionDisposition::Replace {
6643 let previous = state
6644 .managed_session
6645 .lock()
6646 .await
6647 .take()
6648 .expect("replacement must have a previous owner");
6649 stopped_owners
6650 .lock()
6651 .await
6652 .push(Arc::as_ptr(&previous.owner_client) as usize);
6653 }
6654
6655 starts.fetch_add(1, Ordering::SeqCst);
6656 if let Some((entered, release)) = pause {
6657 entered.send(()).expect("start observer must be waiting");
6658 release.await.expect("start release must be sent");
6659 }
6660
6661 let owner_epoch = match disposition {
6662 SessionDisposition::Refresh => {
6663 active_epoch.expect("refresh must preserve the active owner epoch")
6664 }
6665 SessionDisposition::Start | SessionDisposition::Replace => {
6666 state.allocate_managed_session_owner_epoch()
6667 }
6668 SessionDisposition::Reuse => unreachable!("reuse returned before startup"),
6669 };
6670 let result = managed_session_test_result(&device_id);
6671 *state.managed_session.lock().await = Some(ManagedSessionRecord {
6672 key,
6673 owner_client: client,
6674 owner_epoch,
6675 result,
6676 });
6677 disposition
6678 }
6679
6680 #[tokio::test]
6681 async fn concurrent_same_key_start_reuses_one_owner_epoch() {
6682 let state = Arc::new(OpenRtcTauriState::new(test_config()));
6683 let starts = Arc::new(AtomicUsize::new(0));
6684 let stopped_owners = Arc::new(Mutex::new(Vec::new()));
6685 let (first_entered_tx, first_entered_rx) = oneshot::channel();
6686 let (first_release_tx, first_release_rx) = oneshot::channel();
6687
6688 let first = tokio::spawn(simulate_managed_session_start(
6689 state.clone(),
6690 "same-device".to_string(),
6691 Some((first_entered_tx, first_release_rx)),
6692 starts.clone(),
6693 stopped_owners.clone(),
6694 ));
6695 first_entered_rx.await.expect("first start must pause");
6696
6697 let (second_attempting_tx, second_attempting_rx) = oneshot::channel();
6698 let second_state = state.clone();
6699 let second_starts = starts.clone();
6700 let second_stopped_owners = stopped_owners.clone();
6701 let second = tokio::spawn(async move {
6702 second_attempting_tx.send(()).unwrap();
6703 simulate_managed_session_start(
6704 second_state,
6705 "same-device".to_string(),
6706 None,
6707 second_starts,
6708 second_stopped_owners,
6709 )
6710 .await
6711 });
6712 second_attempting_rx.await.unwrap();
6713 tokio::task::yield_now().await;
6714 assert_eq!(starts.load(Ordering::SeqCst), 1);
6715
6716 first_release_tx.send(()).unwrap();
6717 assert_eq!(
6718 first.await.expect("first start task"),
6719 SessionDisposition::Start
6720 );
6721 assert_eq!(
6722 second.await.expect("second start task"),
6723 SessionDisposition::Reuse
6724 );
6725 assert_eq!(starts.load(Ordering::SeqCst), 1);
6726
6727 let active = state
6728 .managed_session
6729 .lock()
6730 .await
6731 .clone()
6732 .expect("active owner");
6733 assert_eq!(active.owner_epoch, 1);
6734 assert!(Arc::ptr_eq(&active.owner_client, &state.client()));
6735 assert!(stopped_owners.lock().await.is_empty());
6736 }
6737
6738 #[tokio::test]
6739 async fn concurrent_different_key_start_replaces_prior_owner_without_late_overwrite() {
6740 let state = Arc::new(OpenRtcTauriState::new(test_config()));
6741 let starts = Arc::new(AtomicUsize::new(0));
6742 let stopped_owners = Arc::new(Mutex::new(Vec::new()));
6743 let (first_entered_tx, first_entered_rx) = oneshot::channel();
6744 let (first_release_tx, first_release_rx) = oneshot::channel();
6745
6746 let first = tokio::spawn(simulate_managed_session_start(
6747 state.clone(),
6748 "first-device".to_string(),
6749 Some((first_entered_tx, first_release_rx)),
6750 starts.clone(),
6751 stopped_owners.clone(),
6752 ));
6753 first_entered_rx.await.expect("first start must pause");
6754 let first_owner = state.client();
6755
6756 let (second_attempting_tx, second_attempting_rx) = oneshot::channel();
6757 let second_state = state.clone();
6758 let second_starts = starts.clone();
6759 let second_stopped_owners = stopped_owners.clone();
6760 let second = tokio::spawn(async move {
6761 second_attempting_tx.send(()).unwrap();
6762 simulate_managed_session_start(
6763 second_state,
6764 "second-device".to_string(),
6765 None,
6766 second_starts,
6767 second_stopped_owners,
6768 )
6769 .await
6770 });
6771 second_attempting_rx.await.unwrap();
6772 tokio::task::yield_now().await;
6773 assert!(Arc::ptr_eq(&first_owner, &state.client()));
6774
6775 first_release_tx.send(()).unwrap();
6776 assert_eq!(
6777 first.await.expect("first start task"),
6778 SessionDisposition::Start
6779 );
6780 assert_eq!(
6781 second.await.expect("replacement start task"),
6782 SessionDisposition::Replace
6783 );
6784 assert_eq!(starts.load(Ordering::SeqCst), 2);
6785
6786 let active = state
6787 .managed_session
6788 .lock()
6789 .await
6790 .clone()
6791 .expect("active owner");
6792 assert_eq!(active.owner_epoch, 2);
6793 assert!(active.key.contains("second-device"));
6794 assert!(Arc::ptr_eq(&active.owner_client, &state.client()));
6795 assert_eq!(
6796 stopped_owners.lock().await.as_slice(),
6797 [Arc::as_ptr(&first_owner) as usize]
6798 );
6799 assert!(Arc::ptr_eq(&active.owner_client, &first_owner));
6800 }
6801
6802 #[test]
6803 fn derives_app_tag_from_api_key_by_default() {
6804 let config = OpenRtcTauriConfig {
6805 api_key: format!("pk_test_{}", "b".repeat(24) + "1234567890abcdef"),
6806 ..OpenRtcTauriConfig::default()
6807 };
6808
6809 assert_eq!(config.app_tag().unwrap(), "app_1234567890abcdef");
6810 }
6811
6812 #[test]
6813 fn native_testing_endpoint_pair_is_owned_by_the_plugin_state() {
6814 let state = OpenRtcTauriState::new(test_config())
6815 .with_testing_endpoints("http://127.0.0.1:5004", "http://127.0.0.1:8787");
6816
6817 assert_eq!(
6818 state.testing_endpoints,
6819 Some((
6820 "http://127.0.0.1:5004".to_string(),
6821 "http://127.0.0.1:8787".to_string(),
6822 )),
6823 );
6824 }
6825
6826 #[tokio::test]
6827 async fn requested_native_transport_without_installer_keeps_base_route_available() {
6828 let state = OpenRtcTauriState::new(test_config());
6829 let client = state.client();
6830 ensure_requested_native_transports(
6831 &state,
6832 &client,
6833 Some(&requested_ble_config()),
6834 &native_transport_install_context(),
6835 )
6836 .await
6837 .expect("an unavailable optional transport must not fail base Iroh startup");
6838
6839 assert!(state.installed_native_transports.lock().await.is_empty());
6840 }
6841
6842 #[tokio::test]
6843 async fn native_transport_installer_is_idempotent_for_one_client() {
6844 let installs = Arc::new(AtomicUsize::new(0));
6845 let state = OpenRtcTauriState::new(test_config()).with_native_transport_installer(
6846 Arc::new(FakeTransportInstaller {
6847 installs: installs.clone(),
6848 }),
6849 );
6850 let client = state.client();
6851 let config = requested_ble_config();
6852
6853 ensure_requested_native_transports(
6854 &state,
6855 &client,
6856 Some(&config),
6857 &native_transport_install_context(),
6858 )
6859 .await
6860 .expect("first install");
6861 ensure_requested_native_transports(
6862 &state,
6863 &client,
6864 Some(&config),
6865 &native_transport_install_context(),
6866 )
6867 .await
6868 .expect("idempotent install");
6869
6870 assert_eq!(installs.load(Ordering::SeqCst), 1);
6871 }
6872
6873 #[tokio::test]
6874 async fn unavailable_native_transport_keeps_base_route_available() {
6875 let state = OpenRtcTauriState::new(test_config())
6876 .with_native_transport_installer(Arc::new(UnavailableTransportInstaller));
6877 let client = state.client();
6878
6879 ensure_requested_native_transports(
6880 &state,
6881 &client,
6882 Some(&requested_ble_config()),
6883 &native_transport_install_context(),
6884 )
6885 .await
6886 .expect("optional transport installation failure must not fail base Iroh startup");
6887
6888 assert!(state.installed_native_transports.lock().await.is_empty());
6889 }
6890
6891 #[test]
6892 fn invalid_or_missing_api_key_fails_closed() {
6893 assert!(OpenRtcTauriConfig::default().validated_api_key().is_err());
6894 let mut config = OpenRtcTauriConfig::default();
6895 config.api_key = "pk_test_short".to_string();
6896 assert!(config.validated_api_key().is_err());
6897 }
6898
6899 #[cfg(feature = "native-broadcast-moq")]
6900 #[test]
6901 fn draft16_broadcast_feature_installs_one_private_native_adapter() {
6902 let state = OpenRtcTauriState::new(test_config());
6903 assert!(state.native_broadcast_adapter.is_some());
6904 assert!(state.native_broadcast_moq_adapter.is_some());
6905 }
6906
6907 #[test]
6908 fn from_env_uses_api_key_and_ignores_legacy_namespace_selectors() {
6909 let previous_api_key = std::env::var("VITE_OPENRTC_API_KEY").ok();
6910 let previous_project = std::env::var("VITE_OPENRTC_PROJECT_ID").ok();
6911 let previous_app_tag = std::env::var("VITE_PLUTO_OPENRTC_APP_TAG").ok();
6912 let api_key = format!("pk_test_{}", "c".repeat(40));
6913 std::env::set_var("VITE_OPENRTC_API_KEY", &api_key);
6914 std::env::set_var("VITE_OPENRTC_PROJECT_ID", "pluto-rtc-prod");
6915 std::env::set_var("VITE_PLUTO_OPENRTC_APP_TAG", "app_from_vite_env");
6916
6917 let config = OpenRtcTauriConfig::from_env();
6918
6919 assert_eq!(config.api_key, api_key);
6920 assert_eq!(
6921 config.app_tag().unwrap(),
6922 openrtc::app_tag_from_api_key(&api_key)
6923 );
6924
6925 match previous_api_key {
6926 Some(value) => std::env::set_var("VITE_OPENRTC_API_KEY", value),
6927 None => std::env::remove_var("VITE_OPENRTC_API_KEY"),
6928 }
6929 match previous_project {
6930 Some(value) => std::env::set_var("VITE_OPENRTC_PROJECT_ID", value),
6931 None => std::env::remove_var("VITE_OPENRTC_PROJECT_ID"),
6932 }
6933 match previous_app_tag {
6934 Some(value) => std::env::set_var("VITE_PLUTO_OPENRTC_APP_TAG", value),
6935 None => std::env::remove_var("VITE_PLUTO_OPENRTC_APP_TAG"),
6936 }
6937 }
6938
6939 #[test]
6940 fn native_state_has_one_constructor_owned_app_identity() {
6941 let config = test_config();
6942 let expected = openrtc::app_tag_from_api_key(&config.api_key);
6943 let state = OpenRtcTauriState::new(config);
6944 let first = state.client();
6945 let second = state.client();
6946
6947 assert_eq!(first.app_tag(), expected);
6948 assert!(Arc::ptr_eq(&first, &second));
6949 }
6950
6951 #[test]
6952 fn managed_session_uses_persisted_native_device_id() {
6953 let identity = openrtc::native_device::NativeDeviceIdentity {
6954 device_id: "persisted-native-device".to_string(),
6955 device_name: "Mac".to_string(),
6956 created_at_ms: 1,
6957 updated_at_ms: 1,
6958 name_source: None,
6959 system_info: None,
6960 };
6961
6962 assert_eq!(
6963 managed_session_device_id(Some(" desktop-e2e-native "), &identity),
6964 "persisted-native-device"
6965 );
6966 assert_eq!(
6967 managed_session_device_id(None, &identity),
6968 "persisted-native-device"
6969 );
6970 }
6971
6972 #[test]
6973 fn repeated_managed_session_triggers_converge_on_one_owner() {
6974 let key = "app:persisted-native-device";
6975 let cases = [
6976 ("react-remount", true, true, true, true),
6977 ("hmr", true, true, true, true),
6978 ("auth-refresh", true, true, true, true),
6979 ("resume", true, true, true, true),
6980 ("alias-change", true, true, true, true),
6981 ];
6982 for (label, active_presence, active_auto, requested_presence, requested_auto) in cases {
6983 assert_eq!(
6984 managed_session_disposition(
6985 Some(key),
6986 true,
6987 active_presence,
6988 active_auto,
6989 key,
6990 requested_presence,
6991 requested_auto,
6992 ),
6993 SessionDisposition::Reuse,
6994 "{label} must reuse the authoritative tuple"
6995 );
6996 }
6997
6998 assert_eq!(
6999 managed_session_disposition(Some(key), true, false, true, key, true, true),
7000 SessionDisposition::Refresh,
7001 "failed presence startup must retry idempotently"
7002 );
7003 assert_eq!(
7004 managed_session_disposition(
7005 Some(key),
7006 true,
7007 true,
7008 true,
7009 "app:other-native-device",
7010 true,
7011 true,
7012 ),
7013 SessionDisposition::Replace,
7014 "a physical native device change must replace the previous lifecycle owner"
7015 );
7016 assert_eq!(
7017 managed_session_disposition(Some(key), false, true, true, key, true, true),
7018 SessionDisposition::Replace,
7019 "the same tuple on a different client epoch must replace the previous owner"
7020 );
7021 }
7022
7023 #[test]
7024 fn user_device_revocation_invalidates_the_cached_managed_ticket() {
7025 assert!(revokes_managed_session("user-device"));
7026 assert!(revokes_managed_session(" user-device "));
7027 assert!(!revokes_managed_session("share:example"));
7028 assert!(!revokes_managed_session(""));
7029 }
7030
7031 #[test]
7032 fn managed_session_metadata_uses_authoritative_device_id() {
7033 let raw = serde_json::json!({
7034 "deviceId": "persisted-native-device",
7035 "assistantDevice": {
7036 "deviceId": "desktop-e2e-native"
7037 }
7038 })
7039 .to_string();
7040
7041 let metadata =
7042 metadata_with_authoritative_device_id(Some(raw), "desktop-e2e-native").unwrap();
7043 let parsed: serde_json::Value = serde_json::from_str(&metadata).unwrap();
7044
7045 assert_eq!(parsed["deviceId"], "desktop-e2e-native");
7046 assert_eq!(parsed["assistantDevice"]["deviceId"], "desktop-e2e-native");
7047 }
7048}