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