Skip to main content

ndr_ffi/
lib.rs

1//! UniFFI bindings for nostr-double-ratchet
2//!
3//! This crate provides FFI-friendly wrappers around the core nostr-double-ratchet
4//! library for use in iOS and Android applications via UniFFI.
5
6use std::sync::{Arc, Mutex};
7
8use nostr_double_ratchet::{
9    AppKeys, CreateGroupOptions, DeviceEntry, FileStorageAdapter, GroupData, GroupDecryptedEvent,
10    GroupSendEvent, InMemoryStorage, Invite, NdrRuntime, Session, SessionManagerEvent,
11    SessionState, StorageAdapter,
12};
13
14mod error;
15pub use error::NdrError;
16
17/// Returns the version of the ndr-ffi crate.
18#[uniffi::export]
19pub fn version() -> String {
20    env!("CARGO_PKG_VERSION").to_string()
21}
22
23/// FFI-friendly keypair with hex-encoded keys.
24#[derive(uniffi::Record)]
25pub struct FfiKeyPair {
26    pub public_key_hex: String,
27    pub private_key_hex: String,
28}
29
30/// Result of accepting an invite.
31#[derive(uniffi::Record)]
32pub struct InviteAcceptResult {
33    pub session: Arc<SessionHandle>,
34    pub response_event_json: String,
35}
36
37/// Result of sending a message.
38#[derive(uniffi::Record)]
39pub struct SendResult {
40    pub outer_event_json: String,
41    pub inner_event_json: String,
42}
43
44/// Result of decrypting a message.
45#[derive(uniffi::Record)]
46pub struct DecryptResult {
47    pub plaintext: String,
48    pub inner_event_json: String,
49}
50
51/// Event emitted by SessionManager for external publish/subscribe handling.
52#[derive(uniffi::Record)]
53pub struct PubSubEvent {
54    pub kind: String,
55    pub subid: Option<String>,
56    pub filter_json: Option<String>,
57    pub event_json: Option<String>,
58    pub sender_pubkey_hex: Option<String>,
59    pub content: Option<String>,
60    pub event_id: Option<String>,
61}
62
63/// Result of accepting an invite through SessionManager.
64#[derive(uniffi::Record)]
65pub struct SessionManagerAcceptInviteResult {
66    pub owner_pubkey_hex: String,
67    pub inviter_device_pubkey_hex: String,
68    pub device_id: String,
69    pub created_new_session: bool,
70}
71
72/// FFI-friendly group metadata payload.
73#[derive(uniffi::Record)]
74pub struct FfiGroupData {
75    pub id: String,
76    pub name: String,
77    pub description: Option<String>,
78    pub picture: Option<String>,
79    pub members: Vec<String>,
80    pub admins: Vec<String>,
81    pub created_at_ms: u64,
82    pub secret: Option<String>,
83    pub accepted: Option<bool>,
84}
85
86/// Result of sending a group event through GroupManager.
87#[derive(uniffi::Record)]
88pub struct GroupSendResult {
89    pub outer_event_json: String,
90    pub inner_event_json: String,
91    pub outer_event_id: String,
92    pub inner_event_id: String,
93}
94
95/// Metadata fanout summary for group creation.
96#[derive(uniffi::Record)]
97pub struct GroupCreateFanout {
98    pub enabled: bool,
99    pub attempted: u64,
100    pub succeeded: Vec<String>,
101    pub failed: Vec<String>,
102}
103
104/// Result of creating a group through GroupManager.
105#[derive(uniffi::Record)]
106pub struct GroupCreateResult {
107    pub group: FfiGroupData,
108    pub metadata_rumor_json: Option<String>,
109    pub fanout: GroupCreateFanout,
110}
111
112/// Decrypted group event returned by GroupManager.
113#[derive(uniffi::Record)]
114pub struct GroupDecryptedResult {
115    pub group_id: String,
116    pub sender_event_pubkey_hex: String,
117    pub sender_device_pubkey_hex: String,
118    pub sender_owner_pubkey_hex: Option<String>,
119    pub outer_event_id: String,
120    pub outer_created_at: u64,
121    pub key_id: u32,
122    pub message_number: u32,
123    pub inner_event_json: String,
124    pub inner_event_id: String,
125}
126
127/// Shared outer-subscription sync plan for group sender-event authors.
128#[derive(uniffi::Record)]
129pub struct GroupOuterSubscriptionPlanResult {
130    pub authors: Vec<String>,
131    pub added_authors: Vec<String>,
132}
133
134/// Session-state snapshot used to inspect message-push routing without reading storage files.
135#[derive(uniffi::Record)]
136pub struct MessagePushSessionStateResult {
137    pub state_json: String,
138    pub tracked_sender_pubkeys: Vec<String>,
139    pub has_receiving_capability: bool,
140}
141
142/// Generate a new keypair.
143#[uniffi::export]
144pub fn generate_keypair() -> FfiKeyPair {
145    let keys = nostr::Keys::generate();
146    FfiKeyPair {
147        public_key_hex: keys.public_key().to_hex(),
148        private_key_hex: keys.secret_key().to_secret_hex(),
149    }
150}
151
152/// Derive a public key from a hex-encoded private key.
153#[uniffi::export]
154pub fn derive_public_key(privkey_hex: String) -> Result<String, NdrError> {
155    let privkey = parse_private_key(&privkey_hex)?;
156    let secret_key =
157        nostr::SecretKey::from_slice(&privkey).map_err(|e| NdrError::InvalidKey(e.to_string()))?;
158    Ok(nostr::Keys::new(secret_key).public_key().to_hex())
159}
160
161/// FFI-friendly device entry for AppKeys.
162#[derive(uniffi::Record)]
163pub struct FfiDeviceEntry {
164    pub identity_pubkey_hex: String,
165    pub created_at: u64,
166    pub device_label: Option<String>,
167    pub client_label: Option<String>,
168}
169
170fn ffi_device_entry_from_app_keys(app_keys: &AppKeys, device: DeviceEntry) -> FfiDeviceEntry {
171    let labels = app_keys.get_device_labels(&device.identity_pubkey);
172    FfiDeviceEntry {
173        identity_pubkey_hex: hex::encode(device.identity_pubkey.to_bytes()),
174        created_at: device.created_at,
175        device_label: labels.and_then(|label| label.device_label.clone()),
176        client_label: labels.and_then(|label| label.client_label.clone()),
177    }
178}
179
180fn owner_keys_from_privkey_hex(owner_privkey_hex: &str) -> Result<nostr::Keys, NdrError> {
181    let owner_privkey = parse_private_key(owner_privkey_hex)?;
182    let owner_sk = nostr::SecretKey::from_slice(&owner_privkey)
183        .map_err(|e| NdrError::InvalidKey(e.to_string()))?;
184    Ok(nostr::Keys::new(owner_sk))
185}
186
187fn parse_app_keys_for_owner(
188    event: &nostr::Event,
189    owner_privkey_hex: Option<&str>,
190) -> Result<AppKeys, NdrError> {
191    match owner_privkey_hex {
192        Some(privkey_hex) => {
193            let owner_keys = owner_keys_from_privkey_hex(privkey_hex)?;
194            Ok(AppKeys::from_event_with_labels(event, &owner_keys)?)
195        }
196        None => Ok(AppKeys::from_event(event)?),
197    }
198}
199
200/// Create a signed AppKeys event JSON for publishing to relays.
201#[uniffi::export]
202pub fn create_signed_app_keys_event(
203    owner_pubkey_hex: String,
204    owner_privkey_hex: String,
205    devices: Vec<FfiDeviceEntry>,
206) -> Result<String, NdrError> {
207    let owner_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&owner_pubkey_hex)?;
208    let owner_keys = owner_keys_from_privkey_hex(&owner_privkey_hex)?;
209    if owner_keys.public_key() != owner_pubkey {
210        return Err(NdrError::InvalidKey(
211            "owner pubkey does not match owner private key".to_string(),
212        ));
213    }
214
215    let entries = devices
216        .iter()
217        .filter_map(|d| {
218            let pk = nostr_double_ratchet::utils::pubkey_from_hex(&d.identity_pubkey_hex).ok()?;
219            Some(DeviceEntry::new(pk, d.created_at))
220        })
221        .collect::<Vec<_>>();
222
223    let mut app_keys = AppKeys::new(entries);
224    for device in devices {
225        let Ok(identity_pubkey) =
226            nostr_double_ratchet::utils::pubkey_from_hex(&device.identity_pubkey_hex)
227        else {
228            continue;
229        };
230
231        if device.device_label.is_none() && device.client_label.is_none() {
232            continue;
233        }
234
235        app_keys.set_device_labels(
236            identity_pubkey,
237            device.device_label,
238            device.client_label,
239            None,
240        );
241    }
242    let unsigned = app_keys.get_encrypted_event(&owner_keys)?;
243    let signed = unsigned
244        .sign_with_keys(&owner_keys)
245        .map_err(|e| NdrError::Serialization(e.to_string()))?;
246    Ok(serde_json::to_string(&signed)?)
247}
248
249/// Parse an AppKeys event JSON and return the contained device entries.
250#[uniffi::export]
251pub fn parse_app_keys_event(
252    event_json: String,
253    owner_privkey_hex: Option<String>,
254) -> Result<Vec<FfiDeviceEntry>, NdrError> {
255    let event: nostr::Event = serde_json::from_str(&event_json)?;
256    let app_keys = parse_app_keys_for_owner(&event, owner_privkey_hex.as_deref())?;
257    Ok(app_keys
258        .get_all_devices()
259        .into_iter()
260        .map(|d| ffi_device_entry_from_app_keys(&app_keys, d))
261        .collect())
262}
263
264/// Resolve the latest authorized device list from a set of AppKeys event JSON strings.
265#[uniffi::export]
266pub fn resolve_latest_app_keys_devices(
267    event_jsons: Vec<String>,
268    owner_privkey_hex: Option<String>,
269) -> Result<Vec<FfiDeviceEntry>, NdrError> {
270    let events = event_jsons
271        .iter()
272        .filter_map(|event_json| serde_json::from_str::<nostr::Event>(event_json).ok())
273        .collect::<Vec<_>>();
274
275    let mut latest: Option<nostr_double_ratchet::AppKeysSnapshot> = None;
276
277    for event in events.iter() {
278        if !nostr_double_ratchet::is_app_keys_event(event) {
279            continue;
280        }
281
282        let Ok(app_keys) = parse_app_keys_for_owner(event, owner_privkey_hex.as_deref()) else {
283            continue;
284        };
285
286        latest = Some(match latest.as_ref() {
287            Some(current) => nostr_double_ratchet::apply_app_keys_snapshot(
288                Some(&current.app_keys),
289                current.created_at,
290                &app_keys,
291                event.created_at.as_secs(),
292            ),
293            None => nostr_double_ratchet::AppKeysSnapshot {
294                decision: nostr_double_ratchet::AppKeysSnapshotDecision::Advanced,
295                app_keys,
296                created_at: event.created_at.as_secs(),
297            },
298        });
299    }
300
301    let Some(snapshot) = latest else {
302        return Ok(Vec::new());
303    };
304
305    Ok(snapshot
306        .app_keys
307        .get_all_devices()
308        .into_iter()
309        .map(|d| ffi_device_entry_from_app_keys(&snapshot.app_keys, d))
310        .collect())
311}
312
313/// Resolve conversation routing candidates for a decrypted rumor.
314#[uniffi::export]
315pub fn resolve_conversation_candidate_pubkeys(
316    owner_pubkey_hex: String,
317    rumor_pubkey_hex: String,
318    rumor_tags: Vec<Vec<String>>,
319    sender_pubkey_hex: String,
320) -> Vec<String> {
321    nostr_double_ratchet::resolve_conversation_candidate_pubkeys(
322        &owner_pubkey_hex,
323        &rumor_pubkey_hex,
324        &rumor_tags,
325        &sender_pubkey_hex,
326    )
327}
328
329/// FFI wrapper for Invite.
330#[derive(uniffi::Object)]
331pub struct InviteHandle {
332    inner: Mutex<Invite>,
333}
334
335#[uniffi::export]
336impl InviteHandle {
337    /// Create a new invite.
338    #[uniffi::constructor]
339    pub fn create_new(
340        inviter_pubkey_hex: String,
341        device_id: Option<String>,
342        max_uses: Option<u32>,
343    ) -> Result<Arc<Self>, NdrError> {
344        let inviter = nostr_double_ratchet::utils::pubkey_from_hex(&inviter_pubkey_hex)?;
345        let invite = Invite::create_new(inviter, device_id, max_uses.map(|n| n as usize))?;
346        Ok(Arc::new(Self {
347            inner: Mutex::new(invite),
348        }))
349    }
350
351    /// Parse an invite from a URL.
352    #[uniffi::constructor]
353    pub fn from_url(url: String) -> Result<Arc<Self>, NdrError> {
354        let invite = Invite::from_url(&url)?;
355        Ok(Arc::new(Self {
356            inner: Mutex::new(invite),
357        }))
358    }
359
360    /// Parse an invite from a Nostr event JSON.
361    #[uniffi::constructor]
362    pub fn from_event_json(event_json: String) -> Result<Arc<Self>, NdrError> {
363        let event: nostr::Event = serde_json::from_str(&event_json)?;
364        let invite = Invite::from_event(&event)?;
365        Ok(Arc::new(Self {
366            inner: Mutex::new(invite),
367        }))
368    }
369
370    /// Deserialize an invite from JSON.
371    #[uniffi::constructor]
372    pub fn deserialize(json: String) -> Result<Arc<Self>, NdrError> {
373        let invite = Invite::deserialize(&json)?;
374        Ok(Arc::new(Self {
375            inner: Mutex::new(invite),
376        }))
377    }
378
379    /// Convert the invite to a shareable URL.
380    pub fn to_url(&self, root: String) -> Result<String, NdrError> {
381        let invite = self.inner.lock().unwrap();
382        Ok(invite.get_url(&root)?)
383    }
384
385    /// Convert the invite to a Nostr event JSON.
386    pub fn to_event_json(&self) -> Result<String, NdrError> {
387        let invite = self.inner.lock().unwrap();
388        let event = invite.get_event()?;
389        Ok(serde_json::to_string(&event)?)
390    }
391
392    /// Serialize the invite to JSON for persistence.
393    pub fn serialize(&self) -> Result<String, NdrError> {
394        let invite = self.inner.lock().unwrap();
395        Ok(invite.serialize()?)
396    }
397
398    /// Accept the invite and create a session.
399    pub fn accept(
400        &self,
401        invitee_pubkey_hex: String,
402        invitee_privkey_hex: String,
403        device_id: Option<String>,
404    ) -> Result<InviteAcceptResult, NdrError> {
405        let invite = self.inner.lock().unwrap();
406        let invitee_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&invitee_pubkey_hex)?;
407        let invitee_privkey = parse_private_key(&invitee_privkey_hex)?;
408
409        let (session, response_event) =
410            invite.accept(invitee_pubkey, invitee_privkey, device_id)?;
411        let response_event_json = serde_json::to_string(&response_event)?;
412
413        Ok(InviteAcceptResult {
414            session: Arc::new(SessionHandle {
415                inner: Mutex::new(session),
416            }),
417            response_event_json,
418        })
419    }
420
421    /// Accept the invite as an owner and include the owner pubkey in the response payload.
422    pub fn accept_with_owner(
423        &self,
424        invitee_pubkey_hex: String,
425        invitee_privkey_hex: String,
426        device_id: Option<String>,
427        owner_pubkey_hex: Option<String>,
428    ) -> Result<InviteAcceptResult, NdrError> {
429        let invite = self.inner.lock().unwrap();
430        let invitee_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&invitee_pubkey_hex)?;
431        let invitee_privkey = parse_private_key(&invitee_privkey_hex)?;
432        let owner_pubkey = match owner_pubkey_hex {
433            Some(h) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&h)?),
434            None => None,
435        };
436
437        let (session, response_event) =
438            invite.accept_with_owner(invitee_pubkey, invitee_privkey, device_id, owner_pubkey)?;
439        let response_event_json = serde_json::to_string(&response_event)?;
440
441        Ok(InviteAcceptResult {
442            session: Arc::new(SessionHandle {
443                inner: Mutex::new(session),
444            }),
445            response_event_json,
446        })
447    }
448
449    /// Update the invite purpose (e.g. \"link\").
450    pub fn set_purpose(&self, purpose: Option<String>) {
451        let mut invite = self.inner.lock().unwrap();
452        invite.purpose = purpose;
453    }
454
455    /// Update the owner pubkey embedded in invite URLs.
456    pub fn set_owner_pubkey_hex(&self, owner_pubkey_hex: Option<String>) -> Result<(), NdrError> {
457        let mut invite = self.inner.lock().unwrap();
458        invite.owner_public_key = match owner_pubkey_hex {
459            Some(h) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&h)?),
460            None => None,
461        };
462        Ok(())
463    }
464
465    /// Process an invite response event and create a session (inviter side).
466    ///
467    /// Returns `None` if the event is not a valid response for this invite.
468    pub fn process_response(
469        &self,
470        event_json: String,
471        inviter_privkey_hex: String,
472    ) -> Result<Option<InviteProcessResult>, NdrError> {
473        let invite = self.inner.lock().unwrap();
474        let event: nostr::Event = serde_json::from_str(&event_json)?;
475        let inviter_privkey = parse_private_key(&inviter_privkey_hex)?;
476
477        let response = invite.process_invite_response(&event, inviter_privkey)?;
478        let Some(response) = response else {
479            return Ok(None);
480        };
481
482        Ok(Some(InviteProcessResult {
483            session: Arc::new(SessionHandle {
484                inner: Mutex::new(response.session),
485            }),
486            invitee_pubkey_hex: response.invitee_identity.to_hex(),
487            device_id: response.device_id,
488            owner_pubkey_hex: response.owner_public_key.map(|pk| pk.to_hex()),
489        }))
490    }
491
492    /// Get the inviter's public key as hex.
493    pub fn get_inviter_pubkey_hex(&self) -> String {
494        let invite = self.inner.lock().unwrap();
495        invite.inviter.to_hex()
496    }
497
498    /// Get the shared secret as hex.
499    pub fn get_shared_secret_hex(&self) -> String {
500        let invite = self.inner.lock().unwrap();
501        hex::encode(invite.shared_secret)
502    }
503}
504
505/// FFI wrapper for Session.
506#[derive(uniffi::Object)]
507pub struct SessionHandle {
508    inner: Mutex<Session>,
509}
510
511#[uniffi::export]
512impl SessionHandle {
513    /// Initialize a new session.
514    #[uniffi::constructor]
515    pub fn init(
516        their_ephemeral_pubkey_hex: String,
517        our_ephemeral_privkey_hex: String,
518        is_initiator: bool,
519        shared_secret_hex: String,
520        name: Option<String>,
521    ) -> Result<Arc<Self>, NdrError> {
522        let their_pubkey =
523            nostr_double_ratchet::utils::pubkey_from_hex(&their_ephemeral_pubkey_hex)?;
524        let our_privkey = parse_private_key(&our_ephemeral_privkey_hex)?;
525        let shared_secret = parse_secret(&shared_secret_hex)?;
526
527        let session = Session::init(their_pubkey, our_privkey, is_initiator, shared_secret, name)?;
528
529        Ok(Arc::new(Self {
530            inner: Mutex::new(session),
531        }))
532    }
533
534    /// Restore a session from serialized state JSON.
535    #[uniffi::constructor]
536    pub fn from_state_json(state_json: String) -> Result<Arc<Self>, NdrError> {
537        let state: SessionState =
538            nostr_double_ratchet::utils::deserialize_session_state(&state_json)?;
539        let session = Session::new(state, "restored".to_string());
540
541        Ok(Arc::new(Self {
542            inner: Mutex::new(session),
543        }))
544    }
545
546    /// Serialize the session state to JSON.
547    pub fn state_json(&self) -> Result<String, NdrError> {
548        let session = self.inner.lock().unwrap();
549        Ok(nostr_double_ratchet::utils::serialize_session_state(
550            &session.state,
551        )?)
552    }
553
554    /// Check if the session is ready to send messages.
555    pub fn can_send(&self) -> bool {
556        let session = self.inner.lock().unwrap();
557        session.can_send()
558    }
559
560    /// Send a text message.
561    pub fn send_text(&self, text: String) -> Result<SendResult, NdrError> {
562        let mut session = self.inner.lock().unwrap();
563        let inner_event = nostr_double_ratchet::build_text_rumor(
564            nostr::Keys::generate().public_key(),
565            text,
566            vec![],
567        )?;
568        let outer_event = session.send_event(inner_event.clone())?;
569        let inner_event_json = serde_json::to_string(&inner_event)?;
570
571        Ok(SendResult {
572            outer_event_json: serde_json::to_string(&outer_event)?,
573            inner_event_json,
574        })
575    }
576
577    /// Decrypt a received event.
578    pub fn decrypt_event(&self, outer_event_json: String) -> Result<DecryptResult, NdrError> {
579        let mut session = self.inner.lock().unwrap();
580        let event: nostr::Event = serde_json::from_str(&outer_event_json)?;
581
582        let plaintext = session.receive(&event)?.unwrap_or_default();
583
584        // Try to parse inner event if it's JSON
585        let inner_event_json = if plaintext.starts_with('{') {
586            plaintext.clone()
587        } else {
588            // Wrap plain text in a simple structure
589            serde_json::json!({
590                "content": plaintext
591            })
592            .to_string()
593        };
594
595        Ok(DecryptResult {
596            plaintext,
597            inner_event_json,
598        })
599    }
600
601    /// Check if an event is a double-ratchet message.
602    pub fn is_dr_message(&self, event_json: String) -> bool {
603        if let Ok(event) = serde_json::from_str::<nostr::Event>(&event_json) {
604            event.kind == nostr::Kind::Custom(nostr_double_ratchet::MESSAGE_EVENT_KIND as u16)
605        } else {
606            false
607        }
608    }
609}
610
611/// FFI wrapper for SessionManager.
612#[derive(uniffi::Object)]
613pub struct SessionManagerHandle {
614    runtime: NdrRuntime,
615}
616
617#[uniffi::export]
618impl SessionManagerHandle {
619    /// Create a new session manager with an internal event queue.
620    #[uniffi::constructor]
621    pub fn new(
622        our_pubkey_hex: String,
623        our_identity_privkey_hex: String,
624        device_id: String,
625        owner_pubkey_hex: Option<String>,
626    ) -> Result<Arc<Self>, NdrError> {
627        let our_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&our_pubkey_hex)?;
628        let our_identity_key = parse_private_key(&our_identity_privkey_hex)?;
629        let owner_pubkey = match owner_pubkey_hex {
630            Some(h) => nostr_double_ratchet::utils::pubkey_from_hex(&h)?,
631            None => our_pubkey,
632        };
633
634        let storage: Arc<dyn StorageAdapter> = Arc::new(InMemoryStorage::new());
635        let runtime = NdrRuntime::new(
636            our_pubkey,
637            our_identity_key,
638            device_id,
639            owner_pubkey,
640            Some(storage),
641            None,
642        );
643
644        Ok(Arc::new(Self { runtime }))
645    }
646
647    /// Create a new session manager with file-backed storage.
648    #[uniffi::constructor]
649    pub fn new_with_storage_path(
650        our_pubkey_hex: String,
651        our_identity_privkey_hex: String,
652        device_id: String,
653        storage_path: String,
654        owner_pubkey_hex: Option<String>,
655    ) -> Result<Arc<Self>, NdrError> {
656        let our_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&our_pubkey_hex)?;
657        let our_identity_key = parse_private_key(&our_identity_privkey_hex)?;
658        let owner_pubkey = match owner_pubkey_hex {
659            Some(h) => nostr_double_ratchet::utils::pubkey_from_hex(&h)?,
660            None => our_pubkey,
661        };
662
663        let storage = FileStorageAdapter::new(std::path::PathBuf::from(storage_path))
664            .map_err(NdrError::from)?;
665        let storage: Arc<dyn StorageAdapter> = Arc::new(storage);
666
667        let runtime = NdrRuntime::new(
668            our_pubkey,
669            our_identity_key,
670            device_id,
671            owner_pubkey,
672            Some(storage),
673            None,
674        );
675
676        Ok(Arc::new(Self { runtime }))
677    }
678
679    /// Initialize the session manager (loads state, creates device invite, subscribes).
680    pub fn init(&self) -> Result<(), NdrError> {
681        self.runtime.init()?;
682        Ok(())
683    }
684
685    /// Subscribe to a user's AppKeys/device-invite streams and converge sessions.
686    pub fn setup_user(&self, user_pubkey_hex: String) -> Result<(), NdrError> {
687        let user_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&user_pubkey_hex)?;
688        self.runtime.setup_user(user_pubkey)?;
689        Ok(())
690    }
691
692    /// Accept an invite URL using SessionManager's owner-aware routing/auth checks.
693    ///
694    /// This flow also emits the signed invite response via SessionManager pubsub events,
695    /// so hosts should continue draining and publishing `publish_signed` events.
696    pub fn accept_invite_from_url(
697        &self,
698        invite_url: String,
699        owner_pubkey_hint_hex: Option<String>,
700    ) -> Result<SessionManagerAcceptInviteResult, NdrError> {
701        let invite = Invite::from_url(&invite_url)?;
702        let owner_pubkey_hint = owner_pubkey_hint_hex
703            .as_deref()
704            .map(nostr_double_ratchet::utils::pubkey_from_hex)
705            .transpose()?;
706
707        let accepted = self.runtime.accept_invite(&invite, owner_pubkey_hint)?;
708
709        Ok(SessionManagerAcceptInviteResult {
710            owner_pubkey_hex: accepted.owner_pubkey.to_hex(),
711            inviter_device_pubkey_hex: accepted.inviter_device_pubkey.to_hex(),
712            device_id: accepted.device_id,
713            created_new_session: accepted.created_new_session,
714        })
715    }
716
717    /// Accept an invite event JSON using SessionManager's owner-aware routing/auth checks.
718    pub fn accept_invite_from_event_json(
719        &self,
720        event_json: String,
721        owner_pubkey_hint_hex: Option<String>,
722    ) -> Result<SessionManagerAcceptInviteResult, NdrError> {
723        let event: nostr::Event = serde_json::from_str(&event_json)?;
724        let invite = Invite::from_event(&event)?;
725        let owner_pubkey_hint = owner_pubkey_hint_hex
726            .as_deref()
727            .map(nostr_double_ratchet::utils::pubkey_from_hex)
728            .transpose()?;
729
730        let accepted = self.runtime.accept_invite(&invite, owner_pubkey_hint)?;
731
732        Ok(SessionManagerAcceptInviteResult {
733            owner_pubkey_hex: accepted.owner_pubkey.to_hex(),
734            inviter_device_pubkey_hex: accepted.inviter_device_pubkey.to_hex(),
735            device_id: accepted.device_id,
736            created_new_session: accepted.created_new_session,
737        })
738    }
739
740    /// Send a text message to a recipient.
741    pub fn send_text(
742        &self,
743        recipient_pubkey_hex: String,
744        text: String,
745        expires_at_seconds: Option<u64>,
746    ) -> Result<Vec<String>, NdrError> {
747        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
748        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
749            expires_at: Some(expires_at),
750            ttl_seconds: None,
751        });
752        Ok(self.runtime.send_text(recipient, text, options)?)
753    }
754
755    /// Send a text message and return both the stable inner (rumor) id and the
756    /// list of outer message event ids that were published.
757    pub fn send_text_with_inner_id(
758        &self,
759        recipient_pubkey_hex: String,
760        text: String,
761        expires_at_seconds: Option<u64>,
762    ) -> Result<SendTextResult, NdrError> {
763        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
764        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
765            expires_at: Some(expires_at),
766            ttl_seconds: None,
767        });
768        let (inner_id, outer_event_ids) = self
769            .runtime
770            .send_text_with_inner_id(recipient, text, options)?;
771        Ok(SendTextResult {
772            inner_id,
773            outer_event_ids,
774        })
775    }
776
777    /// Send an arbitrary inner rumor event to a recipient, returning stable inner id + outer ids.
778    ///
779    /// This is used for group chats where we need custom kinds/tags (e.g. group metadata kind 40,
780    /// group-tagged chat messages kind 14, reactions kind 7, typing kind 25).
781    ///
782    /// The caller controls the inner rumor tags via `tags_json` (JSON array of string arrays).
783    /// For group fan-out, do NOT include recipient-specific tags like `["p", <recipient>]` so
784    /// the inner rumor id stays stable across all recipients.
785    pub fn send_event_with_inner_id(
786        &self,
787        recipient_pubkey_hex: String,
788        kind: u32,
789        content: String,
790        tags_json: String,
791        created_at_seconds: Option<u64>,
792    ) -> Result<SendTextResult, NdrError> {
793        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
794
795        // Parse tags from JSON (array of string arrays).
796        let tags_vec: Vec<Vec<String>> = if tags_json.trim().is_empty() {
797            Vec::new()
798        } else {
799            serde_json::from_str(&tags_json)?
800        };
801
802        // Try to reuse an explicit ms tag as the created_at source when the caller didn't
803        // provide created_at_seconds (helps keep ids stable across repeated fan-out calls).
804        let mut ms_value: Option<u64> = None;
805        for t in tags_vec.iter() {
806            if t.first().map(|s| s.as_str()) != Some("ms") {
807                continue;
808            }
809            if let Some(v) = t.get(1) {
810                ms_value = v.parse::<u64>().ok();
811                break;
812            }
813        }
814
815        let mut tags: Vec<nostr::Tag> = Vec::with_capacity(tags_vec.len() + 1);
816        for t in tags_vec {
817            tags.push(nostr::Tag::parse(&t).map_err(|e| NdrError::InvalidEvent(e.to_string()))?);
818        }
819
820        let owner_pubkey = self.runtime.get_owner_pubkey();
821        let event = nostr_double_ratchet::build_inner_event(
822            owner_pubkey,
823            kind,
824            content,
825            tags,
826            nostr_double_ratchet::InnerEventBuildOptions {
827                created_at_seconds,
828                ms: ms_value,
829                ensure_ms_tag: true,
830            },
831        )?;
832        let inner_id = event
833            .id
834            .as_ref()
835            .map(|id| id.to_string())
836            .unwrap_or_default();
837
838        let outer_event_ids = self.runtime.send_event(recipient, event)?;
839
840        Ok(SendTextResult {
841            inner_id,
842            outer_event_ids,
843        })
844    }
845
846    /// Send an already-built rumor JSON to a recipient without rebuilding it.
847    ///
848    /// This preserves the original rumor pubkey, timestamp, tags, and id.
849    pub fn send_rumor_json(
850        &self,
851        recipient_pubkey_hex: String,
852        rumor_json: String,
853    ) -> Result<SendTextResult, NdrError> {
854        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
855        let event: nostr::UnsignedEvent = serde_json::from_str(&rumor_json)?;
856        let inner_id = unsigned_event_id_string(&event);
857        let outer_event_ids = self.runtime.send_event(recipient, event)?;
858
859        Ok(SendTextResult {
860            inner_id,
861            outer_event_ids,
862        })
863    }
864
865    /// Upsert group metadata into the embedded GroupManager.
866    pub fn group_upsert(&self, group: FfiGroupData) -> Result<(), NdrError> {
867        self.runtime.with_group_context(|_, group_manager, _| {
868            group_manager.upsert_group(ffi_group_data_to_group_data(group))
869        })?;
870        Ok(())
871    }
872
873    /// Create a group through the embedded GroupManager, with optional metadata fanout.
874    pub fn group_create(
875        &self,
876        name: String,
877        member_owner_pubkeys: Vec<String>,
878        fanout_metadata: Option<bool>,
879        now_ms: Option<u64>,
880    ) -> Result<GroupCreateResult, NdrError> {
881        let member_refs: Vec<&str> = member_owner_pubkeys.iter().map(String::as_str).collect();
882        let should_fanout = fanout_metadata.unwrap_or(true);
883
884        self.runtime
885            .with_group_context(|session_manager, group_manager, _| {
886                let mut send_pairwise = |recipient_owner: nostr::PublicKey,
887                                         rumor: &nostr::UnsignedEvent|
888                 -> nostr_double_ratchet::Result<()> {
889                    session_manager.send_event(recipient_owner, rumor.clone())?;
890                    Ok(())
891                };
892
893                let mut opts = CreateGroupOptions {
894                    send_pairwise: None,
895                    fanout_metadata: should_fanout,
896                    now_ms,
897                };
898                if should_fanout {
899                    opts.send_pairwise = Some(&mut send_pairwise);
900                }
901
902                let created = group_manager.create_group(&name, &member_refs, opts)?;
903                let metadata_rumor_json = created
904                    .metadata_rumor
905                    .as_ref()
906                    .map(serde_json::to_string)
907                    .transpose()?;
908
909                Ok(GroupCreateResult {
910                    group: group_data_to_ffi_group_data(created.group),
911                    metadata_rumor_json,
912                    fanout: GroupCreateFanout {
913                        enabled: created.fanout.enabled,
914                        attempted: created.fanout.attempted as u64,
915                        succeeded: created.fanout.succeeded,
916                        failed: created.fanout.failed,
917                    },
918                })
919            })
920    }
921
922    /// Remove a group from the embedded GroupManager.
923    pub fn group_remove(&self, group_id: String) {
924        self.runtime
925            .with_group_context(|_, group_manager, _| group_manager.remove_group(&group_id));
926    }
927
928    /// Return known sender-event pubkeys used for one-to-many group transport.
929    pub fn group_known_sender_event_pubkeys(&self) -> Vec<String> {
930        self.runtime
931            .group_known_sender_event_pubkeys()
932            .into_iter()
933            .map(|pk| pk.to_hex())
934            .collect()
935    }
936
937    /// Return the current group outer authors and which ones were newly added
938    /// since the last sync plan request for this handle.
939    pub fn group_outer_subscription_plan(&self) -> GroupOuterSubscriptionPlanResult {
940        let plan = self.runtime.group_outer_subscription_plan();
941        GroupOuterSubscriptionPlanResult {
942            authors: plan.authors.into_iter().map(|pk| pk.to_hex()).collect(),
943            added_authors: plan
944                .added_authors
945                .into_iter()
946                .map(|pk| pk.to_hex())
947                .collect(),
948        }
949    }
950
951    /// Send a group event through GroupManager.
952    ///
953    /// Pairwise sender-key distribution rumors are sent through SessionManager sessions.
954    /// The encrypted one-to-many outer event is emitted via the SessionManager pubsub queue.
955    pub fn group_send_event(
956        &self,
957        group_id: String,
958        kind: u32,
959        content: String,
960        tags_json: String,
961        now_ms: Option<u64>,
962    ) -> Result<GroupSendResult, NdrError> {
963        let tags: Vec<Vec<String>> = if tags_json.trim().is_empty() {
964            Vec::new()
965        } else {
966            serde_json::from_str(&tags_json)?
967        };
968
969        self.runtime
970            .with_group_context(|session_manager, group_manager, event_tx| {
971                let mut send_pairwise = |recipient_owner: nostr::PublicKey,
972                                         rumor: &nostr::UnsignedEvent|
973                 -> nostr_double_ratchet::Result<()> {
974                    session_manager.send_event(recipient_owner, rumor.clone())?;
975                    Ok(())
976                };
977
978                let mut publish_outer = |outer: &nostr::Event| -> nostr_double_ratchet::Result<()> {
979                    event_tx
980                        .send(SessionManagerEvent::PublishSigned(outer.clone()))
981                        .map_err(|e| nostr_double_ratchet::Error::Storage(e.to_string()))?;
982                    Ok(())
983                };
984
985                let result = group_manager.send_event(
986                    &group_id,
987                    GroupSendEvent {
988                        kind,
989                        content,
990                        tags,
991                    },
992                    &mut send_pairwise,
993                    &mut publish_outer,
994                    now_ms,
995                )?;
996
997                Ok(GroupSendResult {
998                    outer_event_json: serde_json::to_string(&result.outer)?,
999                    inner_event_json: serde_json::to_string(&result.inner)?,
1000                    outer_event_id: result.outer.id.to_string(),
1001                    inner_event_id: unsigned_event_id_string(&result.inner),
1002                })
1003            })
1004    }
1005
1006    /// Handle a decrypted pairwise session rumor that may carry sender-key distribution.
1007    pub fn group_handle_incoming_session_event(
1008        &self,
1009        event_json: String,
1010        from_owner_pubkey_hex: String,
1011        from_sender_device_pubkey_hex: Option<String>,
1012    ) -> Result<Vec<GroupDecryptedResult>, NdrError> {
1013        let event: nostr::UnsignedEvent = serde_json::from_str(&event_json)?;
1014        let from_owner_pubkey =
1015            nostr_double_ratchet::utils::pubkey_from_hex(&from_owner_pubkey_hex)?;
1016
1017        let from_sender_device_pubkey = match from_sender_device_pubkey_hex {
1018            Some(hex) => Some(nostr_double_ratchet::utils::pubkey_from_hex(&hex)?),
1019            None => Some(event.pubkey),
1020        };
1021
1022        let decrypted = self.runtime.group_handle_incoming_session_event(
1023            &event,
1024            from_owner_pubkey,
1025            from_sender_device_pubkey,
1026        );
1027        Ok(decrypted
1028            .into_iter()
1029            .map(group_decrypted_to_result)
1030            .collect())
1031    }
1032
1033    /// Handle an incoming relay event that may be an encrypted one-to-many group outer event.
1034    pub fn group_handle_outer_event(
1035        &self,
1036        event_json: String,
1037    ) -> Result<Option<GroupDecryptedResult>, NdrError> {
1038        let event: nostr::Event = serde_json::from_str(&event_json)?;
1039        Ok(self
1040            .runtime
1041            .group_handle_outer_event(&event)
1042            .map(group_decrypted_to_result))
1043    }
1044
1045    /// Send a delivery/read receipt for messages.
1046    pub fn send_receipt(
1047        &self,
1048        recipient_pubkey_hex: String,
1049        receipt_type: String,
1050        message_ids: Vec<String>,
1051        expires_at_seconds: Option<u64>,
1052    ) -> Result<Vec<String>, NdrError> {
1053        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
1054        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
1055            expires_at: Some(expires_at),
1056            ttl_seconds: None,
1057        });
1058        Ok(self
1059            .runtime
1060            .send_receipt(recipient, &receipt_type, message_ids, options)?)
1061    }
1062
1063    /// Send a typing indicator.
1064    pub fn send_typing(
1065        &self,
1066        recipient_pubkey_hex: String,
1067        expires_at_seconds: Option<u64>,
1068    ) -> Result<Vec<String>, NdrError> {
1069        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
1070        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
1071            expires_at: Some(expires_at),
1072            ttl_seconds: None,
1073        });
1074        Ok(self.runtime.send_typing(recipient, options)?)
1075    }
1076
1077    /// Send an emoji reaction (kind 7) to a specific message id.
1078    pub fn send_reaction(
1079        &self,
1080        recipient_pubkey_hex: String,
1081        message_id: String,
1082        emoji: String,
1083        expires_at_seconds: Option<u64>,
1084    ) -> Result<Vec<String>, NdrError> {
1085        let recipient = nostr_double_ratchet::utils::pubkey_from_hex(&recipient_pubkey_hex)?;
1086        let options = expires_at_seconds.map(|expires_at| nostr_double_ratchet::SendOptions {
1087            expires_at: Some(expires_at),
1088            ttl_seconds: None,
1089        });
1090        Ok(self
1091            .runtime
1092            .send_reaction(recipient, message_id, emoji, options)?)
1093    }
1094
1095    /// Import a session state for a peer.
1096    pub fn import_session_state(
1097        &self,
1098        peer_pubkey_hex: String,
1099        state_json: String,
1100        device_id: Option<String>,
1101    ) -> Result<(), NdrError> {
1102        let peer_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&peer_pubkey_hex)?;
1103        let state: SessionState =
1104            nostr_double_ratchet::utils::deserialize_session_state(&state_json)?;
1105        self.runtime
1106            .import_session_state(peer_pubkey, device_id, state)?;
1107        Ok(())
1108    }
1109
1110    /// Export the active session state for a peer.
1111    pub fn get_active_session_state(
1112        &self,
1113        peer_pubkey_hex: String,
1114    ) -> Result<Option<String>, NdrError> {
1115        let peer_pubkey = nostr_double_ratchet::utils::pubkey_from_hex(&peer_pubkey_hex)?;
1116        if let Some(state) = self.runtime.export_active_session_state(peer_pubkey)? {
1117            Ok(Some(nostr_double_ratchet::utils::serialize_session_state(
1118                &state,
1119            )?))
1120        } else {
1121            Ok(None)
1122        }
1123    }
1124
1125    /// List peer owner pubkeys known from loaded state or persisted storage.
1126    pub fn known_peer_owner_pubkeys(&self) -> Vec<String> {
1127        self.runtime
1128            .known_peer_owner_pubkeys()
1129            .into_iter()
1130            .map(|pubkey| pubkey.to_hex())
1131            .collect()
1132    }
1133
1134    /// Return the persisted user-record snapshot JSON for a peer owner, if present.
1135    pub fn get_stored_user_record_json(
1136        &self,
1137        peer_owner_pubkey_hex: String,
1138    ) -> Result<Option<String>, NdrError> {
1139        let peer_owner_pubkey =
1140            nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
1141        Ok(self
1142            .runtime
1143            .get_stored_user_record_json(peer_owner_pubkey)?)
1144    }
1145
1146    /// Return the tracked message-push author pubkeys for a peer owner.
1147    pub fn get_message_push_author_pubkeys(
1148        &self,
1149        peer_owner_pubkey_hex: String,
1150    ) -> Result<Vec<String>, NdrError> {
1151        let peer_owner_pubkey =
1152            nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
1153        Ok(self
1154            .runtime
1155            .get_message_push_author_pubkeys(peer_owner_pubkey)
1156            .into_iter()
1157            .map(|pubkey| pubkey.to_hex())
1158            .collect())
1159    }
1160
1161    /// Return pairwise session snapshots used for message-push routing for a peer owner.
1162    pub fn get_message_push_session_states(
1163        &self,
1164        peer_owner_pubkey_hex: String,
1165    ) -> Result<Vec<MessagePushSessionStateResult>, NdrError> {
1166        let peer_owner_pubkey =
1167            nostr_double_ratchet::utils::pubkey_from_hex(&peer_owner_pubkey_hex)?;
1168        self.runtime
1169            .get_message_push_session_states(peer_owner_pubkey)
1170            .into_iter()
1171            .map(|snapshot| {
1172                Ok(MessagePushSessionStateResult {
1173                    state_json: nostr_double_ratchet::utils::serialize_session_state(
1174                        &snapshot.state,
1175                    )?,
1176                    tracked_sender_pubkeys: snapshot
1177                        .tracked_sender_pubkeys
1178                        .into_iter()
1179                        .map(|pubkey| pubkey.to_hex())
1180                        .collect(),
1181                    has_receiving_capability: snapshot.has_receiving_capability,
1182                })
1183            })
1184            .collect()
1185    }
1186
1187    /// Process a received Nostr event JSON.
1188    pub fn process_event(&self, event_json: String) -> Result<(), NdrError> {
1189        let event: nostr::Event = serde_json::from_str(&event_json)?;
1190        self.runtime.process_received_event(event);
1191        Ok(())
1192    }
1193
1194    /// Drain pending pubsub events from the internal queue.
1195    pub fn drain_events(&self) -> Result<Vec<PubSubEvent>, NdrError> {
1196        let mut events = Vec::new();
1197
1198        for event in self.runtime.drain_events() {
1199            let pubsub_event = match event {
1200                SessionManagerEvent::Publish(unsigned) => PubSubEvent {
1201                    kind: "publish".to_string(),
1202                    subid: None,
1203                    filter_json: None,
1204                    event_json: Some(serde_json::to_string(&unsigned)?),
1205                    sender_pubkey_hex: None,
1206                    content: None,
1207                    event_id: None,
1208                },
1209                SessionManagerEvent::PublishSigned(signed) => PubSubEvent {
1210                    kind: "publish_signed".to_string(),
1211                    subid: None,
1212                    filter_json: None,
1213                    event_json: Some(serde_json::to_string(&signed)?),
1214                    sender_pubkey_hex: None,
1215                    content: None,
1216                    event_id: None,
1217                },
1218                SessionManagerEvent::PublishSignedForInnerEvent {
1219                    event,
1220                    inner_event_id,
1221                    ..
1222                } => PubSubEvent {
1223                    kind: "publish_signed".to_string(),
1224                    subid: None,
1225                    filter_json: None,
1226                    event_json: Some(serde_json::to_string(&event)?),
1227                    sender_pubkey_hex: None,
1228                    content: None,
1229                    event_id: inner_event_id,
1230                },
1231                SessionManagerEvent::Subscribe { subid, filter_json } => PubSubEvent {
1232                    kind: "subscribe".to_string(),
1233                    subid: Some(subid),
1234                    filter_json: Some(filter_json),
1235                    event_json: None,
1236                    sender_pubkey_hex: None,
1237                    content: None,
1238                    event_id: None,
1239                },
1240                SessionManagerEvent::Unsubscribe(subid) => PubSubEvent {
1241                    kind: "unsubscribe".to_string(),
1242                    subid: Some(subid),
1243                    filter_json: None,
1244                    event_json: None,
1245                    sender_pubkey_hex: None,
1246                    content: None,
1247                    event_id: None,
1248                },
1249                SessionManagerEvent::DecryptedMessage {
1250                    sender,
1251                    content,
1252                    event_id,
1253                    ..
1254                } => PubSubEvent {
1255                    kind: "decrypted_message".to_string(),
1256                    subid: None,
1257                    filter_json: None,
1258                    event_json: None,
1259                    sender_pubkey_hex: Some(sender.to_hex()),
1260                    content: Some(content),
1261                    event_id,
1262                },
1263                SessionManagerEvent::ReceivedEvent(event) => PubSubEvent {
1264                    kind: "received_event".to_string(),
1265                    subid: None,
1266                    filter_json: None,
1267                    event_json: Some(serde_json::to_string(&event)?),
1268                    sender_pubkey_hex: None,
1269                    content: None,
1270                    event_id: None,
1271                },
1272            };
1273            events.push(pubsub_event);
1274        }
1275
1276        Ok(events)
1277    }
1278
1279    /// Get our device id.
1280    pub fn get_device_id(&self) -> String {
1281        self.runtime.get_device_id().to_string()
1282    }
1283
1284    /// Get our public key as hex.
1285    pub fn get_our_pubkey_hex(&self) -> String {
1286        self.runtime.get_our_pubkey().to_hex()
1287    }
1288
1289    /// Get owner public key as hex.
1290    pub fn get_owner_pubkey_hex(&self) -> String {
1291        self.runtime.get_owner_pubkey().to_hex()
1292    }
1293
1294    /// Get total active sessions.
1295    pub fn get_total_sessions(&self) -> u64 {
1296        self.runtime.get_total_sessions() as u64
1297    }
1298}
1299
1300#[cfg(test)]
1301mod architecture_tests {
1302    #[test]
1303    fn ffi_handle_does_not_reach_into_session_manager() {
1304        let source = include_str!("lib.rs");
1305        let banned = concat!(".session_", "manager()");
1306        assert!(
1307            !source.contains(banned),
1308            "FFI should use NdrRuntime APIs instead of direct SessionManager access"
1309        );
1310    }
1311}
1312
1313/// Parse a hex-encoded private key.
1314fn parse_private_key(hex_str: &str) -> Result<[u8; 32], NdrError> {
1315    let bytes = hex::decode(hex_str).map_err(|_| NdrError::InvalidKey("Invalid hex".into()))?;
1316    if bytes.len() != 32 {
1317        return Err(NdrError::InvalidKey("Private key must be 32 bytes".into()));
1318    }
1319    let mut arr = [0u8; 32];
1320    arr.copy_from_slice(&bytes);
1321    Ok(arr)
1322}
1323
1324/// Parse a hex-encoded secret.
1325fn parse_secret(hex_str: &str) -> Result<[u8; 32], NdrError> {
1326    let bytes = hex::decode(hex_str).map_err(|_| NdrError::Serialization("Invalid hex".into()))?;
1327    if bytes.len() != 32 {
1328        return Err(NdrError::Serialization("Secret must be 32 bytes".into()));
1329    }
1330    let mut arr = [0u8; 32];
1331    arr.copy_from_slice(&bytes);
1332    Ok(arr)
1333}
1334
1335fn ffi_group_data_to_group_data(group: FfiGroupData) -> GroupData {
1336    GroupData {
1337        id: group.id,
1338        name: group.name,
1339        description: group.description,
1340        picture: group.picture,
1341        members: group.members,
1342        admins: group.admins,
1343        created_at: group.created_at_ms,
1344        secret: group.secret,
1345        accepted: group.accepted,
1346    }
1347}
1348
1349fn group_data_to_ffi_group_data(group: GroupData) -> FfiGroupData {
1350    FfiGroupData {
1351        id: group.id,
1352        name: group.name,
1353        description: group.description,
1354        picture: group.picture,
1355        members: group.members,
1356        admins: group.admins,
1357        created_at_ms: group.created_at,
1358        secret: group.secret,
1359        accepted: group.accepted,
1360    }
1361}
1362
1363fn unsigned_event_id_string(event: &nostr::UnsignedEvent) -> String {
1364    event
1365        .id
1366        .as_ref()
1367        .map(std::string::ToString::to_string)
1368        .unwrap_or_default()
1369}
1370
1371fn group_decrypted_to_result(event: GroupDecryptedEvent) -> GroupDecryptedResult {
1372    GroupDecryptedResult {
1373        group_id: event.group_id,
1374        sender_event_pubkey_hex: event.sender_event_pubkey.to_hex(),
1375        sender_device_pubkey_hex: event.sender_device_pubkey.to_hex(),
1376        sender_owner_pubkey_hex: event.sender_owner_pubkey.map(|pk| pk.to_hex()),
1377        outer_event_id: event.outer_event_id,
1378        outer_created_at: event.outer_created_at,
1379        key_id: event.key_id,
1380        message_number: event.message_number,
1381        inner_event_json: serde_json::to_string(&event.inner).unwrap_or_default(),
1382        inner_event_id: unsigned_event_id_string(&event.inner),
1383    }
1384}
1385
1386uniffi::setup_scaffolding!();
1387
1388/// Result of processing an invite response.
1389#[derive(uniffi::Record)]
1390pub struct InviteProcessResult {
1391    pub session: Arc<SessionHandle>,
1392    pub invitee_pubkey_hex: String,
1393    pub device_id: Option<String>,
1394    pub owner_pubkey_hex: Option<String>,
1395}
1396
1397/// Result of sending a text message including stable inner id.
1398#[derive(uniffi::Record)]
1399pub struct SendTextResult {
1400    pub inner_id: String,
1401    pub outer_event_ids: Vec<String>,
1402}
1403
1404#[cfg(test)]
1405mod tests {
1406    use super::*;
1407
1408    #[test]
1409    fn test_version() {
1410        let v = version();
1411        assert!(!v.is_empty());
1412        assert!(v.contains('.'));
1413    }
1414
1415    #[test]
1416    fn test_keypair_generate_formats_hex() {
1417        let kp = generate_keypair();
1418        assert_eq!(kp.public_key_hex.len(), 64);
1419        assert_eq!(kp.private_key_hex.len(), 64);
1420        // Verify they're valid hex
1421        assert!(hex::decode(&kp.public_key_hex).is_ok());
1422        assert!(hex::decode(&kp.private_key_hex).is_ok());
1423    }
1424
1425    #[test]
1426    fn test_derive_public_key_matches_generate() {
1427        let kp = generate_keypair();
1428        let pubkey = derive_public_key(kp.private_key_hex.clone()).unwrap();
1429        assert_eq!(pubkey, kp.public_key_hex);
1430    }
1431
1432    #[test]
1433    fn test_invite_url_roundtrip() {
1434        let kp = generate_keypair();
1435        let invite = InviteHandle::create_new(kp.public_key_hex.clone(), None, None).unwrap();
1436
1437        let url = invite.to_url("https://example.com".to_string()).unwrap();
1438        assert!(url.starts_with("https://example.com"));
1439
1440        let restored = InviteHandle::from_url(url).unwrap();
1441        assert_eq!(
1442            invite.get_inviter_pubkey_hex(),
1443            restored.get_inviter_pubkey_hex()
1444        );
1445        assert_eq!(
1446            invite.get_shared_secret_hex(),
1447            restored.get_shared_secret_hex()
1448        );
1449    }
1450
1451    #[test]
1452    fn test_invite_serialize_roundtrip() {
1453        let kp = generate_keypair();
1454        let invite =
1455            InviteHandle::create_new(kp.public_key_hex.clone(), Some("device1".into()), Some(5))
1456                .unwrap();
1457
1458        let json = invite.serialize().unwrap();
1459        let restored = InviteHandle::deserialize(json).unwrap();
1460
1461        assert_eq!(
1462            invite.get_inviter_pubkey_hex(),
1463            restored.get_inviter_pubkey_hex()
1464        );
1465        assert_eq!(
1466            invite.get_shared_secret_hex(),
1467            restored.get_shared_secret_hex()
1468        );
1469    }
1470
1471    #[test]
1472    fn test_invite_accept_returns_session_and_event() {
1473        let inviter_kp = generate_keypair();
1474        let invitee_kp = generate_keypair();
1475
1476        let invite =
1477            InviteHandle::create_new(inviter_kp.public_key_hex.clone(), None, None).unwrap();
1478        let url = invite.to_url("https://example.com".to_string()).unwrap();
1479
1480        let invite_copy = InviteHandle::from_url(url).unwrap();
1481        let result = invite_copy
1482            .accept(
1483                invitee_kp.public_key_hex.clone(),
1484                invitee_kp.private_key_hex.clone(),
1485                None,
1486            )
1487            .unwrap();
1488
1489        assert!(!result.response_event_json.is_empty());
1490        assert!(result.session.can_send());
1491    }
1492
1493    #[test]
1494    fn test_invite_process_response_yields_working_session_pair() {
1495        let alice_kp = generate_keypair();
1496        let bob_kp = generate_keypair();
1497
1498        let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
1499        let accept = invite
1500            .accept(
1501                bob_kp.public_key_hex.clone(),
1502                bob_kp.private_key_hex.clone(),
1503                None,
1504            )
1505            .unwrap();
1506
1507        let processed = invite
1508            .process_response(
1509                accept.response_event_json.clone(),
1510                alice_kp.private_key_hex.clone(),
1511            )
1512            .unwrap()
1513            .unwrap();
1514
1515        // Bob sends first (initiator), Alice receives first (non-initiator)
1516        let bob_send = accept.session.send_text("hi".to_string()).unwrap();
1517        let alice_decrypt = processed
1518            .session
1519            .decrypt_event(bob_send.outer_event_json.clone())
1520            .unwrap();
1521        assert!(alice_decrypt.plaintext.contains("hi"));
1522
1523        // After receiving, Alice should be able to send.
1524        assert!(processed.session.can_send());
1525
1526        let alice_reply = processed.session.send_text("ok".to_string()).unwrap();
1527        let bob_decrypt = accept
1528            .session
1529            .decrypt_event(alice_reply.outer_event_json)
1530            .unwrap();
1531        assert!(bob_decrypt.plaintext.contains("ok"));
1532    }
1533
1534    #[test]
1535    fn test_session_send_receive() {
1536        // Setup: create invite and accept it to get two linked sessions
1537        let alice_kp = generate_keypair();
1538        let bob_kp = generate_keypair();
1539
1540        // Alice creates invite
1541        let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
1542        let invite_json = invite.serialize().unwrap();
1543
1544        // Bob accepts invite
1545        let bob_invite = InviteHandle::deserialize(invite_json).unwrap();
1546        let accept_result = bob_invite
1547            .accept(
1548                bob_kp.public_key_hex.clone(),
1549                bob_kp.private_key_hex.clone(),
1550                None,
1551            )
1552            .unwrap();
1553
1554        let bob_session = accept_result.session;
1555
1556        // Alice processes the response to create her session
1557        // For this test, we use the session's shared state to verify
1558        assert!(bob_session.can_send());
1559
1560        // Bob sends a message
1561        let send_result = bob_session.send_text("Hello Alice!".to_string()).unwrap();
1562        assert!(!send_result.outer_event_json.is_empty());
1563    }
1564
1565    #[test]
1566    fn test_session_state_roundtrip() {
1567        let alice_kp = generate_keypair();
1568        let bob_kp = generate_keypair();
1569
1570        let invite = InviteHandle::create_new(alice_kp.public_key_hex.clone(), None, None).unwrap();
1571        let invite_json = invite.serialize().unwrap();
1572
1573        let bob_invite = InviteHandle::deserialize(invite_json).unwrap();
1574        let accept_result = bob_invite
1575            .accept(
1576                bob_kp.public_key_hex.clone(),
1577                bob_kp.private_key_hex.clone(),
1578                None,
1579            )
1580            .unwrap();
1581
1582        let session = accept_result.session;
1583        let state_json = session.state_json().unwrap();
1584
1585        // Restore session from state
1586        let restored = SessionHandle::from_state_json(state_json).unwrap();
1587        assert_eq!(session.can_send(), restored.can_send());
1588    }
1589
1590    #[test]
1591    fn test_group_send_event_tracks_sender_event_pubkey() {
1592        let kp = generate_keypair();
1593        let manager = SessionManagerHandle::new(
1594            kp.public_key_hex.clone(),
1595            kp.private_key_hex.clone(),
1596            kp.public_key_hex.clone(),
1597            None,
1598        )
1599        .unwrap();
1600        manager.init().unwrap();
1601
1602        manager
1603            .group_upsert(FfiGroupData {
1604                id: "group-ffi-test".to_string(),
1605                name: "ffi".to_string(),
1606                description: None,
1607                picture: None,
1608                members: vec![kp.public_key_hex.clone()],
1609                admins: vec![kp.public_key_hex.clone()],
1610                created_at_ms: 1_700_000_000_000,
1611                secret: None,
1612                accepted: Some(true),
1613            })
1614            .unwrap();
1615
1616        assert!(manager.group_known_sender_event_pubkeys().is_empty());
1617
1618        let send = manager
1619            .group_send_event(
1620                "group-ffi-test".to_string(),
1621                14,
1622                "hello".to_string(),
1623                "[]".to_string(),
1624                Some(1_700_000_000_000),
1625            )
1626            .unwrap();
1627        assert!(!send.outer_event_json.is_empty());
1628        assert!(!send.inner_event_json.is_empty());
1629        assert!(!send.outer_event_id.is_empty());
1630        assert!(!send.inner_event_id.is_empty());
1631
1632        let sender_event_pubkeys = manager.group_known_sender_event_pubkeys();
1633        assert_eq!(
1634            sender_event_pubkeys.len(),
1635            0,
1636            "local sender-event pubkeys should be filtered from subscription lists"
1637        );
1638    }
1639
1640    #[test]
1641    fn test_group_create_returns_group_and_metadata_rumor() {
1642        let kp = generate_keypair();
1643        let manager = SessionManagerHandle::new(
1644            kp.public_key_hex.clone(),
1645            kp.private_key_hex.clone(),
1646            kp.public_key_hex.clone(),
1647            None,
1648        )
1649        .unwrap();
1650        manager.init().unwrap();
1651
1652        let created = manager
1653            .group_create(
1654                "ffi-created".to_string(),
1655                vec![kp.public_key_hex.clone()],
1656                Some(true),
1657                Some(1_700_000_123_000),
1658            )
1659            .unwrap();
1660
1661        assert_eq!(created.group.name, "ffi-created");
1662        assert!(created.group.members.contains(&kp.public_key_hex));
1663        assert!(created.metadata_rumor_json.is_some());
1664        assert!(created.fanout.enabled);
1665    }
1666
1667    #[test]
1668    fn test_send_rumor_json_preserves_inner_id() {
1669        let alice = generate_keypair();
1670        let bob = generate_keypair();
1671        let sender_device = generate_keypair();
1672
1673        let manager = SessionManagerHandle::new(
1674            alice.public_key_hex.clone(),
1675            alice.private_key_hex.clone(),
1676            alice.public_key_hex.clone(),
1677            None,
1678        )
1679        .unwrap();
1680        manager.init().unwrap();
1681
1682        let our_next = nostr::Keys::generate();
1683        let state = SessionState {
1684            root_key: [7u8; 32],
1685            their_current_nostr_public_key: Some(
1686                nostr_double_ratchet::utils::pubkey_from_hex(&bob.public_key_hex).unwrap(),
1687            ),
1688            their_next_nostr_public_key: None,
1689            our_current_nostr_key: None,
1690            our_next_nostr_key: nostr_double_ratchet::SerializableKeyPair {
1691                public_key: our_next.public_key(),
1692                private_key: our_next.secret_key().secret_bytes(),
1693            },
1694            receiving_chain_key: None,
1695            sending_chain_key: Some([9u8; 32]),
1696            sending_chain_message_number: 0,
1697            receiving_chain_message_number: 0,
1698            previous_sending_chain_message_count: 0,
1699            skipped_keys: std::collections::HashMap::new(),
1700        };
1701
1702        manager
1703            .import_session_state(
1704                bob.public_key_hex.clone(),
1705                nostr_double_ratchet::utils::serialize_session_state(&state).unwrap(),
1706                Some("bob-device".to_string()),
1707            )
1708            .unwrap();
1709
1710        let rumor = nostr::EventBuilder::new(nostr::Kind::Custom(14), "raw rumor")
1711            .tags(vec![
1712                nostr::Tag::parse(&["l".to_string(), "group-ffi-test".to_string()]).unwrap(),
1713                nostr::Tag::parse(&["ms".to_string(), "1700000000000".to_string()]).unwrap(),
1714            ])
1715            .custom_created_at(nostr::Timestamp::from(1_700_000_000))
1716            .build(
1717                nostr_double_ratchet::utils::pubkey_from_hex(&sender_device.public_key_hex)
1718                    .unwrap(),
1719            );
1720        let rumor_json = serde_json::to_string(&rumor).unwrap();
1721
1722        let send = manager
1723            .send_rumor_json(bob.public_key_hex.clone(), rumor_json)
1724            .unwrap();
1725
1726        assert_eq!(
1727            send.inner_id,
1728            rumor.id.as_ref().map(ToString::to_string).unwrap()
1729        );
1730    }
1731
1732    #[test]
1733    fn test_is_dr_message() {
1734        let kp = generate_keypair();
1735        let invite = InviteHandle::create_new(kp.public_key_hex.clone(), None, None).unwrap();
1736
1737        let bob_kp = generate_keypair();
1738        let accept_result = invite
1739            .accept(
1740                bob_kp.public_key_hex.clone(),
1741                bob_kp.private_key_hex.clone(),
1742                None,
1743            )
1744            .unwrap();
1745
1746        let session = accept_result.session;
1747        let send_result = session.send_text("test".to_string()).unwrap();
1748
1749        // DR message should return true
1750        assert!(session.is_dr_message(send_result.outer_event_json));
1751
1752        // Non-DR message should return false
1753        let non_dr_event = serde_json::json!({
1754            "id": "0000000000000000000000000000000000000000000000000000000000000000",
1755            "pubkey": "0000000000000000000000000000000000000000000000000000000000000000",
1756            "created_at": 0,
1757            "kind": 1,
1758            "tags": [],
1759            "content": "test",
1760            "sig": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
1761        });
1762        assert!(!session.is_dr_message(non_dr_event.to_string()));
1763    }
1764
1765    #[test]
1766    fn test_app_keys_labels_roundtrip() {
1767        let owner = generate_keypair();
1768        let laptop = generate_keypair();
1769        let phone = generate_keypair();
1770
1771        let event_json = create_signed_app_keys_event(
1772            owner.public_key_hex.clone(),
1773            owner.private_key_hex.clone(),
1774            vec![
1775                FfiDeviceEntry {
1776                    identity_pubkey_hex: laptop.public_key_hex.clone(),
1777                    created_at: 1_700_000_000,
1778                    device_label: Some("Sirius MacBook".to_string()),
1779                    client_label: Some("Iris Chat Desktop".to_string()),
1780                },
1781                FfiDeviceEntry {
1782                    identity_pubkey_hex: phone.public_key_hex.clone(),
1783                    created_at: 1_700_000_100,
1784                    device_label: Some("Linked device".to_string()),
1785                    client_label: Some("Iris Chat Mobile".to_string()),
1786                },
1787            ],
1788        )
1789        .unwrap();
1790
1791        let parsed =
1792            parse_app_keys_event(event_json.clone(), Some(owner.private_key_hex.clone())).unwrap();
1793        let resolved =
1794            resolve_latest_app_keys_devices(vec![event_json], Some(owner.private_key_hex)).unwrap();
1795
1796        for devices in [parsed, resolved] {
1797            assert_eq!(devices.len(), 2);
1798
1799            let laptop_entry = devices
1800                .iter()
1801                .find(|entry| entry.identity_pubkey_hex == laptop.public_key_hex)
1802                .unwrap();
1803            assert_eq!(laptop_entry.device_label.as_deref(), Some("Sirius MacBook"));
1804            assert_eq!(
1805                laptop_entry.client_label.as_deref(),
1806                Some("Iris Chat Desktop")
1807            );
1808
1809            let phone_entry = devices
1810                .iter()
1811                .find(|entry| entry.identity_pubkey_hex == phone.public_key_hex)
1812                .unwrap();
1813            assert_eq!(phone_entry.device_label.as_deref(), Some("Linked device"));
1814            assert_eq!(
1815                phone_entry.client_label.as_deref(),
1816                Some("Iris Chat Mobile")
1817            );
1818        }
1819    }
1820}