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