1pub mod app_keys;
2pub mod device_link;
3pub mod direct_message_subscriptions;
4pub mod error;
5pub mod group;
6pub mod group_manager;
7pub mod group_wire;
8pub mod ids;
9pub mod invite;
10pub mod message_builders;
11pub mod message_origin;
12pub mod multi_device;
13pub mod one_to_many;
14pub mod protocol_types;
15pub mod roster;
16pub mod roster_editor;
17pub mod sender_key;
18pub mod session;
19pub mod session_manager;
20pub mod shared_channel;
21pub mod utils;
22pub mod wire;
23
24pub use app_keys::{
25 build_app_keys_device_authorization_filter,
26 encrypted_device_label_payloads_from_app_keys_event, is_app_keys_event,
27 resolve_app_keys_owner_for_device, AppKeys, DeviceEntry, DeviceLabels,
28 APP_KEYS_ENCRYPTED_DEVICE_LABELS_FACT, APP_KEYS_ENCRYPTED_DEVICE_LABELS_SCHEMA,
29 APP_KEYS_FACT_TYPE, APP_KEYS_OWNER_PUBKEY_FACT, APP_KEYS_SCHEMA, APP_KEYS_SNAPSHOT_KIND,
30};
31pub use device_link::{
32 deterministic_link_invite_for_device, deterministic_link_invite_for_device_link_request,
33 encode_compact_device_link_request, parse_compact_device_link_request, DeviceLinkRequest,
34};
35pub use direct_message_subscriptions::{
36 app_keys_subscription_authors, build_app_keys_backfill_filter,
37 build_direct_message_backfill_filter, build_invite_backfill_filter,
38 build_invite_response_backfill_filter, build_protocol_discovery_filters,
39 build_runtime_backfill_filters, direct_message_subscription_authors,
40 invite_response_subscription_recipients, DirectMessageSubscriptionTracker,
41 RuntimeSubscriptionRegistration, RuntimeSubscriptionTracker,
42};
43pub use error::{DomainError, Error, Result};
44pub use group::*;
45pub use group_manager::*;
46pub use group_wire::{
47 build_group_roster_fact_filter, group_roster_unsigned_event, is_group_roster_fact_event,
48 parse_group_roster_fact_event, project_group_roster_fact_events, GroupEventManager,
49 GroupRosterFact, JsonGroupPayloadCodecV1, GROUP_ROSTER_FACT_KIND, GROUP_ROSTER_FACT_SCHEMA,
50 GROUP_ROSTER_FACT_TYPE,
51};
52pub use ids::{DevicePubkey, OwnerPubkey, UnixSeconds};
53pub use invite::{Invite, InviteResponse, InviteResponseEnvelope, OwnerClaimVerifier};
54pub use message_builders::*;
55pub use message_origin::{classify_message_origin, MessageOrigin};
56pub use multi_device::{
57 apply_app_keys_snapshot, apply_app_keys_snapshot_with_required_device,
58 evaluate_device_registration_state, resolve_conversation_candidate_pubkeys,
59 resolve_invite_owner_routing, resolve_rumor_peer_pubkey, select_latest_app_keys_from_events,
60 should_require_relay_registration_confirmation, AppKeysSnapshot, AppKeysSnapshotDecision,
61 DeviceRegistrationState, InviteOwnerRoutingResolution,
62};
63pub use one_to_many::*;
64pub use protocol_types::{ProtocolContext, MAX_SKIP};
65pub use roster::{AuthorizedDevice, DeviceRoster, RosterSnapshotDecision};
66pub use roster_editor::RosterEditor;
67pub use sender_key::*;
68pub use session::{
69 Header, MessageEnvelope, ReceiveOutcome, ReceivePlan, SendOutcome, SendPlan,
70 SerializableKeyPair, Session, SessionState, SkippedKeysEntry,
71};
72pub use session_manager::{
73 Delivery, DeviceRecordSnapshot, PreparedSend, ProcessedInviteResponse, PruneReport,
74 ReceivedMessage, RelayGap, SessionManager, SessionManagerSnapshot, UserRecordSnapshot,
75};
76pub use shared_channel::SharedChannel;
77pub use wire::{
78 group_sender_key_message_event, invite_response_event, invite_unsigned_event, invite_url,
79 message_event, parse_group_sender_key_message_event,
80 parse_group_sender_key_message_event_unchecked, parse_invite_event,
81 parse_invite_response_event, parse_invite_url, parse_message_event, parse_roster_event,
82 roster_unsigned_event, DecodedRosterEvent, GROUP_SENDER_KEY_MESSAGE_KIND, INVITE_EVENT_KIND,
83 INVITE_LIST_LABEL, INVITE_RESPONSE_KIND, MESSAGE_EVENT_KIND, ROSTER_EVENT_KIND,
84};
85
86pub(crate) use ids::owner_pubkey_from_device_pubkey;
87pub(crate) use utils::{
88 device_pubkey_from_secret_bytes, kdf, random_secret_key_bytes, secret_key_from_bytes,
89};
90
91impl OwnerClaimVerifier for AppKeys {
92 fn has_device(&self, _device_pubkey: DevicePubkey, device_identity: nostr::PublicKey) -> bool {
93 self.get_device(&device_identity).is_some()
94 }
95}
96
97pub const APP_KEYS_EVENT_KIND: u32 = APP_KEYS_SNAPSHOT_KIND;
98pub const CHAT_MESSAGE_KIND: u32 = 14;
99pub const CHAT_SETTINGS_KIND: u32 = 10448;
100pub const REACTION_KIND: u32 = 7;
101pub const RECEIPT_KIND: u32 = 15;
102pub const TYPING_KIND: u32 = 25;
103pub const SHARED_CHANNEL_KIND: u32 = 4;
104pub const EXPIRATION_TAG: &str = "expiration";
105
106#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct SendOptions {
109 pub expires_at: Option<u64>,
110 pub ttl_seconds: Option<u64>,
111}
112
113#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
114#[serde(rename_all = "camelCase")]
115pub struct ChatSettingsPayloadV1 {
116 #[serde(rename = "type")]
117 pub typ: String,
118 pub v: u32,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub message_ttl_seconds: Option<u64>,
121}
122
123pub fn process_invite_response_event(
124 invite: &Invite,
125 event: &nostr::Event,
126 inviter_private_key: [u8; 32],
127) -> Result<Option<InviteResponse>> {
128 let envelope = parse_invite_response_event(event)
129 .map_err(|error| Error::InvalidEvent(error.to_string()))?;
130 let mut rng = rand::rngs::OsRng;
131 let mut ctx = ProtocolContext::new(UnixSeconds(event.created_at.as_secs()), &mut rng);
132 let mut invite = invite.clone();
133 invite
134 .process_response(&mut ctx, &envelope, inviter_private_key)
135 .map(Some)
136}
137
138pub trait SessionNostrExt {
139 fn send_event(&mut self, event: nostr::UnsignedEvent) -> Result<nostr::Event>;
140 fn receive(&mut self, event: &nostr::Event) -> Result<Option<String>>;
141}
142
143impl SessionNostrExt for Session {
144 fn send_event(&mut self, mut event: nostr::UnsignedEvent) -> Result<nostr::Event> {
145 event.ensure_id();
146 let payload = serde_json::to_vec(&event)?;
147 let now = UnixSeconds(
148 std::time::SystemTime::now()
149 .duration_since(std::time::UNIX_EPOCH)
150 .unwrap()
151 .as_secs(),
152 );
153 let plan = self.plan_send(&payload, now)?;
154 let envelope = self.apply_send(plan).envelope;
155 message_event(&envelope).map_err(|error| Error::InvalidEvent(error.to_string()))
156 }
157
158 fn receive(&mut self, event: &nostr::Event) -> Result<Option<String>> {
159 let envelope =
160 parse_message_event(event).map_err(|error| Error::InvalidEvent(error.to_string()))?;
161 if !self.matches_sender(envelope.sender) {
162 return Ok(None);
163 }
164 let mut rng = rand::rngs::OsRng;
165 let mut ctx = ProtocolContext::new(UnixSeconds(event.created_at.as_secs()), &mut rng);
166 let plan = self.plan_receive(&mut ctx, &envelope)?;
167 let outcome = self.apply_receive(plan);
168 let plaintext = String::from_utf8(outcome.payload)
169 .map_err(|error| Error::Decryption(error.to_string()))?;
170 Ok(Some(plaintext))
171 }
172}
173
174pub trait InviteNostrExt {
175 fn get_url(&self, root: &str) -> Result<String>;
176 fn get_event(&self) -> Result<nostr::UnsignedEvent>;
177 fn from_url(url: &str) -> Result<Invite>;
178 fn from_event(event: &nostr::Event) -> Result<Invite>;
179 fn process_invite_response(
180 &self,
181 event: &nostr::Event,
182 inviter_private_key: [u8; 32],
183 ) -> Result<Option<InviteResponse>>;
184}
185
186impl InviteNostrExt for Invite {
187 fn get_url(&self, root: &str) -> Result<String> {
188 invite_url(self, root).map_err(|error| Error::InvalidEvent(error.to_string()))
189 }
190
191 fn get_event(&self) -> Result<nostr::UnsignedEvent> {
192 if self.device_id.is_none() {
193 return Err(Error::DeviceIdRequired);
194 }
195 invite_unsigned_event(self).map_err(|error| Error::InvalidEvent(error.to_string()))
196 }
197
198 fn from_url(url: &str) -> Result<Invite> {
199 parse_invite_url(url).map_err(|error| Error::InvalidEvent(error.to_string()))
200 }
201
202 fn from_event(event: &nostr::Event) -> Result<Invite> {
203 parse_invite_event(event).map_err(|error| Error::InvalidEvent(error.to_string()))
204 }
205
206 fn process_invite_response(
207 &self,
208 event: &nostr::Event,
209 inviter_private_key: [u8; 32],
210 ) -> Result<Option<InviteResponse>> {
211 process_invite_response_event(self, event, inviter_private_key)
212 }
213}
214
215#[cfg(test)]
216mod architecture_tests {
217 #[test]
218 fn core_modules_do_not_manage_runtime_or_wire() {
219 let sources = [
220 include_str!("group.rs"),
221 include_str!("group_manager.rs"),
222 include_str!("invite.rs"),
223 include_str!("roster.rs"),
224 include_str!("roster_editor.rs"),
225 include_str!("sender_key.rs"),
226 include_str!("session.rs"),
227 include_str!("session_manager.rs"),
228 ];
229 for banned in [
230 "NdrRuntime",
231 "SessionManagerEvent",
232 "StorageAdapter",
233 "crossbeam",
234 "tokio",
235 "EventBuilder",
236 "Filter",
237 "crate::wire",
238 "wire::",
239 "pairwise_codec",
240 "GroupWireEnvelopeV1",
241 "GroupPairwisePayloadV1",
242 "GroupSenderKeyPlaintextV1",
243 "wire_format_version",
244 ] {
245 for source in sources {
246 assert!(
247 !source.contains(banned),
248 "core should stay deterministic and wire/runtime-free; found `{banned}`"
249 );
250 }
251 }
252 }
253}