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