Skip to main content

openrtc_tauri_plugin/
lib.rs

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