1use crate::app::{Envelope, EnvelopeResponse, OperationRegistry};
2use crate::capability::{NegotiationRequest, NegotiationResponse};
3use crate::domain::{
4 AttachmentDownloadChunk, AttachmentDownloadChunkRequest, AttachmentId, AttachmentListRequest,
5 AttachmentListResult, AttachmentMeta, AttachmentStoreRequest, AttachmentUploadChunkAck,
6 AttachmentUploadChunkRequest, AttachmentUploadCommitRequest, AttachmentUploadSession,
7 AttachmentUploadStartRequest, ContactListRequest, ContactListResult, ContactRecord,
8 ContactUpdateRequest, IdentityAnnounceRequest, IdentityAnnounceResult,
9 IdentityBootstrapRequest, IdentityBundle, IdentityCreateRequest, IdentityImportRequest,
10 IdentityRef, IdentityResolveRequest, MarkerCreateRequest, MarkerDeleteRequest,
11 MarkerListRequest, MarkerListResult, MarkerRecord, MarkerUpdatePositionRequest,
12 PaperDecodeResult, PaperMessageEnvelope, PeerConnectionRequest, PeerConnectionResult,
13 PresenceListRequest, PresenceListResult, RemoteCommandRequest, RemoteCommandResponse,
14 RemoteCommandSession, RemoteCommandSessionListRequest, RemoteCommandSessionListResult,
15 RouterStats, RouterStoragePolicy, RouterStoragePolicyPatch, TelemetryPoint, TelemetryQuery,
16 TopicCreateRequest, TopicId, TopicListRequest, TopicListResult, TopicPublishRequest,
17 TopicRecord, TopicSubscriptionRequest, VoiceSessionId, VoiceSessionOpenRequest,
18 VoiceSessionState, VoiceSessionUpdateRequest,
19};
20use crate::error::{code, ErrorCategory, SdkError};
21use crate::event::{EventBatch, EventCursor};
22#[cfg(feature = "sdk-async")]
23use crate::event::{EventSubscription, SdkEvent, SubscriptionStart};
24use crate::types::{
25 Ack, CancelResult, ConfigPatch, DeliverySnapshot, MessageId, RuntimeSnapshot, SendRequest,
26 ShutdownMode, TickBudget, TickResult,
27};
28use serde::{Deserialize, Serialize};
29#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
30use serde_json::Map as JsonMap;
31#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
32use serde_json::Value as JsonValue;
33#[cfg(feature = "sdk-async")]
34use std::future::Future;
35#[cfg(feature = "sdk-async")]
36use std::pin::Pin;
37#[cfg(feature = "sdk-async")]
38use tokio_stream::Stream;
39
40const CAP_KEY_MANAGEMENT: &str = "sdk.capability.key_management";
41#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
42const LXMF_RAW_FIELDS_KEY: &str = "_lxmf_fields_msgpack_b64";
43
44#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
45fn lxmf_wire_fields_from_payload(payload: JsonValue) -> JsonValue {
46 let JsonValue::Object(mut map) = payload else {
47 return JsonValue::Null;
48 };
49
50 if let Some(JsonValue::Object(fields)) = map.remove("fields") {
51 return non_empty_fields(fields);
52 }
53
54 let fields = map
55 .into_iter()
56 .filter(|(key, _)| !is_reserved_payload_field_key(key))
57 .collect::<JsonMap<String, JsonValue>>();
58 non_empty_fields(fields)
59}
60
61#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
62fn is_reserved_payload_field_key(key: &str) -> bool {
63 key != LXMF_RAW_FIELDS_KEY
64 && matches!(
65 key,
66 "title" | "content" | "body" | "payload" | "_lxmf" | "_sdk" | "_fields_raw"
67 )
68}
69
70#[cfg(any(feature = "rpc-backend", feature = "zmq-pipeline-backend"))]
71fn non_empty_fields(fields: JsonMap<String, JsonValue>) -> JsonValue {
72 if fields.is_empty() {
73 JsonValue::Null
74 } else {
75 JsonValue::Object(fields)
76 }
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "snake_case")]
81pub enum KeyProviderClass {
82 InMemory,
83 File,
84 OsKeystore,
85 Hsm,
86 Custom(String),
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
90#[serde(rename_all = "snake_case")]
91pub enum SdkKeyPurpose {
92 IdentitySigning,
93 TransportDh,
94 SharedSecret,
95 Custom(String),
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
99pub struct SdkStoredKey {
100 pub key_id: String,
101 pub purpose: SdkKeyPurpose,
102 pub material: Vec<u8>,
103}
104
105pub trait SdkBackend: Send + Sync {
106 fn negotiate(&self, req: NegotiationRequest) -> Result<NegotiationResponse, SdkError>;
107
108 fn send(&self, req: SendRequest) -> Result<MessageId, SdkError>;
109
110 fn cancel(&self, id: MessageId) -> Result<CancelResult, SdkError>;
111
112 fn status(&self, id: MessageId) -> Result<Option<DeliverySnapshot>, SdkError>;
113
114 fn configure(&self, expected_revision: u64, patch: ConfigPatch) -> Result<Ack, SdkError>;
115
116 fn poll_events(&self, cursor: Option<EventCursor>, max: usize) -> Result<EventBatch, SdkError>;
117
118 fn snapshot(&self) -> Result<RuntimeSnapshot, SdkError>;
119
120 fn shutdown(&self, mode: ShutdownMode) -> Result<Ack, SdkError>;
121
122 fn router_stats(&self) -> Result<RouterStats, SdkError> {
123 Err(SdkError::capability_disabled("sdk.capability.router_management"))
124 }
125
126 fn router_storage_policy(&self) -> Result<RouterStoragePolicy, SdkError> {
127 Err(SdkError::capability_disabled("sdk.capability.router_management"))
128 }
129
130 fn set_router_storage_policy(
131 &self,
132 _patch: RouterStoragePolicyPatch,
133 ) -> Result<RouterStoragePolicy, SdkError> {
134 Err(SdkError::capability_disabled("sdk.capability.router_management"))
135 }
136
137 fn tick(&self, _budget: TickBudget) -> Result<TickResult, SdkError> {
138 Err(SdkError::new(
139 code::CAPABILITY_DISABLED,
140 ErrorCategory::Capability,
141 "backend does not support manual ticking",
142 ))
143 }
144
145 fn topic_create(&self, _req: TopicCreateRequest) -> Result<TopicRecord, SdkError> {
146 Err(SdkError::capability_disabled("sdk.capability.topics"))
147 }
148
149 fn topic_get(&self, _topic_id: TopicId) -> Result<Option<TopicRecord>, SdkError> {
150 Err(SdkError::capability_disabled("sdk.capability.topics"))
151 }
152
153 fn topic_list(&self, _req: TopicListRequest) -> Result<TopicListResult, SdkError> {
154 Err(SdkError::capability_disabled("sdk.capability.topics"))
155 }
156
157 fn topic_subscribe(&self, _req: TopicSubscriptionRequest) -> Result<Ack, SdkError> {
158 Err(SdkError::capability_disabled("sdk.capability.topic_subscriptions"))
159 }
160
161 fn topic_unsubscribe(&self, _topic_id: TopicId) -> Result<Ack, SdkError> {
162 Err(SdkError::capability_disabled("sdk.capability.topic_subscriptions"))
163 }
164
165 fn topic_publish(&self, _req: TopicPublishRequest) -> Result<Ack, SdkError> {
166 Err(SdkError::capability_disabled("sdk.capability.topic_fanout"))
167 }
168
169 fn telemetry_query(&self, _query: TelemetryQuery) -> Result<Vec<TelemetryPoint>, SdkError> {
170 Err(SdkError::capability_disabled("sdk.capability.telemetry_query"))
171 }
172
173 fn telemetry_subscribe(&self, _query: TelemetryQuery) -> Result<Ack, SdkError> {
174 Err(SdkError::capability_disabled("sdk.capability.telemetry_stream"))
175 }
176
177 fn attachment_store(&self, _req: AttachmentStoreRequest) -> Result<AttachmentMeta, SdkError> {
178 Err(SdkError::capability_disabled("sdk.capability.attachments"))
179 }
180
181 fn attachment_get(
182 &self,
183 _attachment_id: AttachmentId,
184 ) -> Result<Option<AttachmentMeta>, SdkError> {
185 Err(SdkError::capability_disabled("sdk.capability.attachments"))
186 }
187
188 fn attachment_list(
189 &self,
190 _req: AttachmentListRequest,
191 ) -> Result<AttachmentListResult, SdkError> {
192 Err(SdkError::capability_disabled("sdk.capability.attachments"))
193 }
194
195 fn attachment_delete(&self, _attachment_id: AttachmentId) -> Result<Ack, SdkError> {
196 Err(SdkError::capability_disabled("sdk.capability.attachment_delete"))
197 }
198
199 fn attachment_download(&self, _attachment_id: AttachmentId) -> Result<Ack, SdkError> {
200 Err(SdkError::capability_disabled("sdk.capability.attachments"))
201 }
202
203 fn attachment_upload_start(
204 &self,
205 _req: AttachmentUploadStartRequest,
206 ) -> Result<AttachmentUploadSession, SdkError> {
207 Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
208 }
209
210 fn attachment_upload_chunk(
211 &self,
212 _req: AttachmentUploadChunkRequest,
213 ) -> Result<AttachmentUploadChunkAck, SdkError> {
214 Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
215 }
216
217 fn attachment_upload_commit(
218 &self,
219 _req: AttachmentUploadCommitRequest,
220 ) -> Result<AttachmentMeta, SdkError> {
221 Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
222 }
223
224 fn attachment_download_chunk(
225 &self,
226 _req: AttachmentDownloadChunkRequest,
227 ) -> Result<AttachmentDownloadChunk, SdkError> {
228 Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
229 }
230
231 fn attachment_associate_topic(
232 &self,
233 _attachment_id: AttachmentId,
234 _topic_id: TopicId,
235 ) -> Result<Ack, SdkError> {
236 Err(SdkError::capability_disabled("sdk.capability.attachments"))
237 }
238
239 fn marker_create(&self, _req: MarkerCreateRequest) -> Result<MarkerRecord, SdkError> {
240 Err(SdkError::capability_disabled("sdk.capability.markers"))
241 }
242
243 fn marker_list(&self, _req: MarkerListRequest) -> Result<MarkerListResult, SdkError> {
244 Err(SdkError::capability_disabled("sdk.capability.markers"))
245 }
246
247 fn marker_update_position(
248 &self,
249 _req: MarkerUpdatePositionRequest,
250 ) -> Result<MarkerRecord, SdkError> {
251 Err(SdkError::capability_disabled("sdk.capability.markers"))
252 }
253
254 fn marker_delete(&self, _req: MarkerDeleteRequest) -> Result<Ack, SdkError> {
255 Err(SdkError::capability_disabled("sdk.capability.markers"))
256 }
257
258 fn identity_list(&self) -> Result<Vec<IdentityBundle>, SdkError> {
259 Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
260 }
261
262 fn identity_create(&self, _req: IdentityCreateRequest) -> Result<IdentityBundle, SdkError> {
263 Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
264 }
265
266 fn identity_announce_now(&self) -> Result<Ack, SdkError> {
267 Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
268 }
269
270 fn identity_announce(
271 &self,
272 _req: IdentityAnnounceRequest,
273 ) -> Result<IdentityAnnounceResult, SdkError> {
274 Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
275 }
276
277 fn identity_presence_list(
278 &self,
279 _req: PresenceListRequest,
280 ) -> Result<PresenceListResult, SdkError> {
281 Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
282 }
283
284 fn identity_activate(&self, _identity: IdentityRef) -> Result<Ack, SdkError> {
285 Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
286 }
287
288 fn identity_import(&self, _req: IdentityImportRequest) -> Result<IdentityBundle, SdkError> {
289 Err(SdkError::capability_disabled("sdk.capability.identity_import_export"))
290 }
291
292 fn identity_export(&self, _identity: IdentityRef) -> Result<IdentityImportRequest, SdkError> {
293 Err(SdkError::capability_disabled("sdk.capability.identity_import_export"))
294 }
295
296 fn identity_resolve(
297 &self,
298 _req: IdentityResolveRequest,
299 ) -> Result<Option<IdentityRef>, SdkError> {
300 Err(SdkError::capability_disabled("sdk.capability.identity_hash_resolution"))
301 }
302
303 fn identity_contact_update(
304 &self,
305 _req: ContactUpdateRequest,
306 ) -> Result<ContactRecord, SdkError> {
307 Err(SdkError::capability_disabled("sdk.capability.contact_management"))
308 }
309
310 fn identity_contact_list(
311 &self,
312 _req: ContactListRequest,
313 ) -> Result<ContactListResult, SdkError> {
314 Err(SdkError::capability_disabled("sdk.capability.contact_management"))
315 }
316
317 fn identity_bootstrap(
318 &self,
319 _req: IdentityBootstrapRequest,
320 ) -> Result<ContactRecord, SdkError> {
321 Err(SdkError::capability_disabled("sdk.capability.contact_management"))
322 }
323
324 fn peer_connect(&self, _req: PeerConnectionRequest) -> Result<PeerConnectionResult, SdkError> {
325 Err(SdkError::capability_disabled("sdk.capability.peer_lifecycle"))
326 }
327
328 fn peer_disconnect(
329 &self,
330 _req: PeerConnectionRequest,
331 ) -> Result<PeerConnectionResult, SdkError> {
332 Err(SdkError::capability_disabled("sdk.capability.peer_lifecycle"))
333 }
334
335 fn peer_reconnect(
336 &self,
337 _req: PeerConnectionRequest,
338 ) -> Result<PeerConnectionResult, SdkError> {
339 Err(SdkError::capability_disabled("sdk.capability.peer_lifecycle"))
340 }
341
342 fn paper_encode(&self, _message_id: MessageId) -> Result<PaperMessageEnvelope, SdkError> {
343 Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
344 }
345
346 fn paper_decode(&self, _envelope: PaperMessageEnvelope) -> Result<Ack, SdkError> {
347 Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
348 }
349
350 fn paper_decode_with_metadata(
351 &self,
352 _envelope: PaperMessageEnvelope,
353 ) -> Result<PaperDecodeResult, SdkError> {
354 Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
355 }
356
357 fn command_invoke(
358 &self,
359 _req: RemoteCommandRequest,
360 ) -> Result<RemoteCommandResponse, SdkError> {
361 Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
362 }
363
364 fn command_reply(
365 &self,
366 _correlation_id: String,
367 _reply: RemoteCommandResponse,
368 ) -> Result<Ack, SdkError> {
369 Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
370 }
371
372 fn command_session_get(
373 &self,
374 _correlation_id: String,
375 ) -> Result<Option<RemoteCommandSession>, SdkError> {
376 Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
377 }
378
379 fn command_session_list(
380 &self,
381 _req: RemoteCommandSessionListRequest,
382 ) -> Result<RemoteCommandSessionListResult, SdkError> {
383 Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
384 }
385
386 fn voice_session_open(
387 &self,
388 _req: VoiceSessionOpenRequest,
389 ) -> Result<VoiceSessionId, SdkError> {
390 Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
391 }
392
393 fn voice_session_update(
394 &self,
395 _req: VoiceSessionUpdateRequest,
396 ) -> Result<VoiceSessionState, SdkError> {
397 Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
398 }
399
400 fn voice_session_close(&self, _session_id: VoiceSessionId) -> Result<Ack, SdkError> {
401 Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
402 }
403
404 fn operation_registry(&self) -> Result<OperationRegistry, SdkError> {
405 Err(SdkError::capability_disabled("sdk.capability.operation_registry"))
406 }
407
408 fn envelope_execute(&self, _envelope: Envelope) -> Result<EnvelopeResponse, SdkError> {
409 Err(SdkError::capability_disabled("sdk.capability.operation_registry"))
410 }
411}
412
413pub trait SdkBackendKeyManagement: SdkBackend {
414 fn key_provider_class(&self) -> Result<KeyProviderClass, SdkError> {
415 Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
416 }
417
418 fn key_get(&self, _key_id: &str) -> Result<Option<SdkStoredKey>, SdkError> {
419 Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
420 }
421
422 fn key_put(&self, _key: SdkStoredKey) -> Result<Ack, SdkError> {
423 Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
424 }
425
426 fn key_delete(&self, _key_id: &str) -> Result<Ack, SdkError> {
427 Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
428 }
429
430 fn key_list_ids(&self) -> Result<Vec<String>, SdkError> {
431 Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
432 }
433}
434
435#[cfg(feature = "sdk-async")]
436pub type SdkEventStream = Pin<Box<dyn Stream<Item = Result<SdkEvent, SdkError>> + Send>>;
437
438#[cfg(feature = "sdk-async")]
439pub type SdkBoxFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, SdkError>> + Send + 'a>>;
440
441#[cfg(feature = "sdk-async")]
442pub trait SdkBackendAsyncOps: SdkBackend {
443 fn negotiate_async(&self, req: NegotiationRequest) -> SdkBoxFuture<'_, NegotiationResponse>;
444
445 fn send_async(&self, req: SendRequest) -> SdkBoxFuture<'_, MessageId>;
446
447 fn cancel_async(&self, id: MessageId) -> SdkBoxFuture<'_, CancelResult> {
448 Box::pin(async move { self.cancel(id) })
449 }
450
451 fn status_async(&self, id: MessageId) -> SdkBoxFuture<'_, Option<DeliverySnapshot>>;
452
453 fn configure_async(&self, expected_revision: u64, patch: ConfigPatch) -> SdkBoxFuture<'_, Ack> {
454 Box::pin(async move { self.configure(expected_revision, patch) })
455 }
456
457 fn poll_events_async(
458 &self,
459 cursor: Option<EventCursor>,
460 max: usize,
461 ) -> SdkBoxFuture<'_, EventBatch> {
462 Box::pin(async move { self.poll_events(cursor, max) })
463 }
464
465 fn snapshot_async(&self) -> SdkBoxFuture<'_, RuntimeSnapshot>;
466
467 fn shutdown_async(&self, mode: ShutdownMode) -> SdkBoxFuture<'_, Ack>;
468}
469
470#[cfg(feature = "sdk-async")]
471pub trait SdkBackendAsyncEvents: SdkBackend {
472 fn subscribe_events(&self, start: SubscriptionStart) -> Result<EventSubscription, SdkError>;
473
474 fn open_event_stream(
475 &self,
476 _subscription: &EventSubscription,
477 ) -> Result<Option<SdkEventStream>, SdkError> {
478 Ok(None)
479 }
480}
481
482#[cfg(not(feature = "sdk-async"))]
483pub trait SdkBackendAsyncEvents: SdkBackend {}
484
485pub mod mobile_ble;
486
487#[cfg(all(feature = "rpc-backend", feature = "std"))]
488pub mod rpc;
489
490#[cfg(all(feature = "zmq-pipeline-backend", feature = "std"))]
491pub mod zmq_pipeline;
492
493#[cfg(test)]
494mod tests {
495 include!("backend_tests.rs");
496}