Skip to main content

lxmf_sdk/
backend.rs

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, IdentityBootstrapRequest, IdentityBundle, IdentityImportRequest,
9    IdentityRef, IdentityResolveRequest, MarkerCreateRequest, MarkerDeleteRequest,
10    MarkerListRequest, MarkerListResult, MarkerRecord, MarkerUpdatePositionRequest,
11    PaperMessageEnvelope, PresenceListRequest, PresenceListResult, RemoteCommandRequest,
12    RemoteCommandResponse, RemoteCommandSession, RemoteCommandSessionListRequest,
13    RemoteCommandSessionListResult, TelemetryPoint, TelemetryQuery, TopicCreateRequest, TopicId,
14    TopicListRequest, TopicListResult, TopicPublishRequest, TopicRecord, TopicSubscriptionRequest,
15    VoiceSessionId, VoiceSessionOpenRequest, VoiceSessionState, VoiceSessionUpdateRequest,
16};
17use crate::error::{code, ErrorCategory, SdkError};
18use crate::event::{EventBatch, EventCursor};
19#[cfg(feature = "sdk-async")]
20use crate::event::{EventSubscription, SubscriptionStart};
21use crate::types::{
22    Ack, CancelResult, ConfigPatch, DeliverySnapshot, MessageId, RuntimeSnapshot, SendRequest,
23    ShutdownMode, TickBudget, TickResult,
24};
25use serde::{Deserialize, Serialize};
26
27const CAP_KEY_MANAGEMENT: &str = "sdk.capability.key_management";
28
29#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum KeyProviderClass {
32    InMemory,
33    File,
34    OsKeystore,
35    Hsm,
36    Custom(String),
37}
38
39#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
40#[serde(rename_all = "snake_case")]
41pub enum SdkKeyPurpose {
42    IdentitySigning,
43    TransportDh,
44    SharedSecret,
45    Custom(String),
46}
47
48#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
49pub struct SdkStoredKey {
50    pub key_id: String,
51    pub purpose: SdkKeyPurpose,
52    pub material: Vec<u8>,
53}
54
55pub trait SdkBackend: Send + Sync {
56    fn negotiate(&self, req: NegotiationRequest) -> Result<NegotiationResponse, SdkError>;
57
58    fn send(&self, req: SendRequest) -> Result<MessageId, SdkError>;
59
60    fn cancel(&self, id: MessageId) -> Result<CancelResult, SdkError>;
61
62    fn status(&self, id: MessageId) -> Result<Option<DeliverySnapshot>, SdkError>;
63
64    fn configure(&self, expected_revision: u64, patch: ConfigPatch) -> Result<Ack, SdkError>;
65
66    fn poll_events(&self, cursor: Option<EventCursor>, max: usize) -> Result<EventBatch, SdkError>;
67
68    fn snapshot(&self) -> Result<RuntimeSnapshot, SdkError>;
69
70    fn shutdown(&self, mode: ShutdownMode) -> Result<Ack, SdkError>;
71
72    fn tick(&self, _budget: TickBudget) -> Result<TickResult, SdkError> {
73        Err(SdkError::new(
74            code::CAPABILITY_DISABLED,
75            ErrorCategory::Capability,
76            "backend does not support manual ticking",
77        ))
78    }
79
80    fn topic_create(&self, _req: TopicCreateRequest) -> Result<TopicRecord, SdkError> {
81        Err(SdkError::capability_disabled("sdk.capability.topics"))
82    }
83
84    fn topic_get(&self, _topic_id: TopicId) -> Result<Option<TopicRecord>, SdkError> {
85        Err(SdkError::capability_disabled("sdk.capability.topics"))
86    }
87
88    fn topic_list(&self, _req: TopicListRequest) -> Result<TopicListResult, SdkError> {
89        Err(SdkError::capability_disabled("sdk.capability.topics"))
90    }
91
92    fn topic_subscribe(&self, _req: TopicSubscriptionRequest) -> Result<Ack, SdkError> {
93        Err(SdkError::capability_disabled("sdk.capability.topic_subscriptions"))
94    }
95
96    fn topic_unsubscribe(&self, _topic_id: TopicId) -> Result<Ack, SdkError> {
97        Err(SdkError::capability_disabled("sdk.capability.topic_subscriptions"))
98    }
99
100    fn topic_publish(&self, _req: TopicPublishRequest) -> Result<Ack, SdkError> {
101        Err(SdkError::capability_disabled("sdk.capability.topic_fanout"))
102    }
103
104    fn telemetry_query(&self, _query: TelemetryQuery) -> Result<Vec<TelemetryPoint>, SdkError> {
105        Err(SdkError::capability_disabled("sdk.capability.telemetry_query"))
106    }
107
108    fn telemetry_subscribe(&self, _query: TelemetryQuery) -> Result<Ack, SdkError> {
109        Err(SdkError::capability_disabled("sdk.capability.telemetry_stream"))
110    }
111
112    fn attachment_store(&self, _req: AttachmentStoreRequest) -> Result<AttachmentMeta, SdkError> {
113        Err(SdkError::capability_disabled("sdk.capability.attachments"))
114    }
115
116    fn attachment_get(
117        &self,
118        _attachment_id: AttachmentId,
119    ) -> Result<Option<AttachmentMeta>, SdkError> {
120        Err(SdkError::capability_disabled("sdk.capability.attachments"))
121    }
122
123    fn attachment_list(
124        &self,
125        _req: AttachmentListRequest,
126    ) -> Result<AttachmentListResult, SdkError> {
127        Err(SdkError::capability_disabled("sdk.capability.attachments"))
128    }
129
130    fn attachment_delete(&self, _attachment_id: AttachmentId) -> Result<Ack, SdkError> {
131        Err(SdkError::capability_disabled("sdk.capability.attachment_delete"))
132    }
133
134    fn attachment_download(&self, _attachment_id: AttachmentId) -> Result<Ack, SdkError> {
135        Err(SdkError::capability_disabled("sdk.capability.attachments"))
136    }
137
138    fn attachment_upload_start(
139        &self,
140        _req: AttachmentUploadStartRequest,
141    ) -> Result<AttachmentUploadSession, SdkError> {
142        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
143    }
144
145    fn attachment_upload_chunk(
146        &self,
147        _req: AttachmentUploadChunkRequest,
148    ) -> Result<AttachmentUploadChunkAck, SdkError> {
149        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
150    }
151
152    fn attachment_upload_commit(
153        &self,
154        _req: AttachmentUploadCommitRequest,
155    ) -> Result<AttachmentMeta, SdkError> {
156        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
157    }
158
159    fn attachment_download_chunk(
160        &self,
161        _req: AttachmentDownloadChunkRequest,
162    ) -> Result<AttachmentDownloadChunk, SdkError> {
163        Err(SdkError::capability_disabled("sdk.capability.attachment_streaming"))
164    }
165
166    fn attachment_associate_topic(
167        &self,
168        _attachment_id: AttachmentId,
169        _topic_id: TopicId,
170    ) -> Result<Ack, SdkError> {
171        Err(SdkError::capability_disabled("sdk.capability.attachments"))
172    }
173
174    fn marker_create(&self, _req: MarkerCreateRequest) -> Result<MarkerRecord, SdkError> {
175        Err(SdkError::capability_disabled("sdk.capability.markers"))
176    }
177
178    fn marker_list(&self, _req: MarkerListRequest) -> Result<MarkerListResult, SdkError> {
179        Err(SdkError::capability_disabled("sdk.capability.markers"))
180    }
181
182    fn marker_update_position(
183        &self,
184        _req: MarkerUpdatePositionRequest,
185    ) -> Result<MarkerRecord, SdkError> {
186        Err(SdkError::capability_disabled("sdk.capability.markers"))
187    }
188
189    fn marker_delete(&self, _req: MarkerDeleteRequest) -> Result<Ack, SdkError> {
190        Err(SdkError::capability_disabled("sdk.capability.markers"))
191    }
192
193    fn identity_list(&self) -> Result<Vec<IdentityBundle>, SdkError> {
194        Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
195    }
196
197    fn identity_announce_now(&self) -> Result<Ack, SdkError> {
198        Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
199    }
200
201    fn identity_presence_list(
202        &self,
203        _req: PresenceListRequest,
204    ) -> Result<PresenceListResult, SdkError> {
205        Err(SdkError::capability_disabled("sdk.capability.identity_discovery"))
206    }
207
208    fn identity_activate(&self, _identity: IdentityRef) -> Result<Ack, SdkError> {
209        Err(SdkError::capability_disabled("sdk.capability.identity_multi"))
210    }
211
212    fn identity_import(&self, _req: IdentityImportRequest) -> Result<IdentityBundle, SdkError> {
213        Err(SdkError::capability_disabled("sdk.capability.identity_import_export"))
214    }
215
216    fn identity_export(&self, _identity: IdentityRef) -> Result<IdentityImportRequest, SdkError> {
217        Err(SdkError::capability_disabled("sdk.capability.identity_import_export"))
218    }
219
220    fn identity_resolve(
221        &self,
222        _req: IdentityResolveRequest,
223    ) -> Result<Option<IdentityRef>, SdkError> {
224        Err(SdkError::capability_disabled("sdk.capability.identity_hash_resolution"))
225    }
226
227    fn identity_contact_update(
228        &self,
229        _req: ContactUpdateRequest,
230    ) -> Result<ContactRecord, SdkError> {
231        Err(SdkError::capability_disabled("sdk.capability.contact_management"))
232    }
233
234    fn identity_contact_list(
235        &self,
236        _req: ContactListRequest,
237    ) -> Result<ContactListResult, SdkError> {
238        Err(SdkError::capability_disabled("sdk.capability.contact_management"))
239    }
240
241    fn identity_bootstrap(
242        &self,
243        _req: IdentityBootstrapRequest,
244    ) -> Result<ContactRecord, SdkError> {
245        Err(SdkError::capability_disabled("sdk.capability.contact_management"))
246    }
247
248    fn paper_encode(&self, _message_id: MessageId) -> Result<PaperMessageEnvelope, SdkError> {
249        Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
250    }
251
252    fn paper_decode(&self, _envelope: PaperMessageEnvelope) -> Result<Ack, SdkError> {
253        Err(SdkError::capability_disabled("sdk.capability.paper_messages"))
254    }
255
256    fn command_invoke(
257        &self,
258        _req: RemoteCommandRequest,
259    ) -> Result<RemoteCommandResponse, SdkError> {
260        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
261    }
262
263    fn command_reply(
264        &self,
265        _correlation_id: String,
266        _reply: RemoteCommandResponse,
267    ) -> Result<Ack, SdkError> {
268        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
269    }
270
271    fn command_session_get(
272        &self,
273        _correlation_id: String,
274    ) -> Result<Option<RemoteCommandSession>, SdkError> {
275        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
276    }
277
278    fn command_session_list(
279        &self,
280        _req: RemoteCommandSessionListRequest,
281    ) -> Result<RemoteCommandSessionListResult, SdkError> {
282        Err(SdkError::capability_disabled("sdk.capability.remote_commands"))
283    }
284
285    fn voice_session_open(
286        &self,
287        _req: VoiceSessionOpenRequest,
288    ) -> Result<VoiceSessionId, SdkError> {
289        Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
290    }
291
292    fn voice_session_update(
293        &self,
294        _req: VoiceSessionUpdateRequest,
295    ) -> Result<VoiceSessionState, SdkError> {
296        Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
297    }
298
299    fn voice_session_close(&self, _session_id: VoiceSessionId) -> Result<Ack, SdkError> {
300        Err(SdkError::capability_disabled("sdk.capability.voice_signaling"))
301    }
302
303    fn operation_registry(&self) -> Result<OperationRegistry, SdkError> {
304        Err(SdkError::capability_disabled("sdk.capability.operation_registry"))
305    }
306
307    fn envelope_execute(&self, _envelope: Envelope) -> Result<EnvelopeResponse, SdkError> {
308        Err(SdkError::capability_disabled("sdk.capability.operation_registry"))
309    }
310}
311
312pub trait SdkBackendKeyManagement: SdkBackend {
313    fn key_provider_class(&self) -> Result<KeyProviderClass, SdkError> {
314        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
315    }
316
317    fn key_get(&self, _key_id: &str) -> Result<Option<SdkStoredKey>, SdkError> {
318        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
319    }
320
321    fn key_put(&self, _key: SdkStoredKey) -> Result<Ack, SdkError> {
322        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
323    }
324
325    fn key_delete(&self, _key_id: &str) -> Result<Ack, SdkError> {
326        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
327    }
328
329    fn key_list_ids(&self) -> Result<Vec<String>, SdkError> {
330        Err(SdkError::capability_disabled(CAP_KEY_MANAGEMENT))
331    }
332}
333
334#[cfg(feature = "sdk-async")]
335pub trait SdkBackendAsyncEvents: SdkBackend {
336    fn subscribe_events(&self, start: SubscriptionStart) -> Result<EventSubscription, SdkError>;
337}
338
339#[cfg(not(feature = "sdk-async"))]
340pub trait SdkBackendAsyncEvents: SdkBackend {}
341
342pub mod mobile_ble;
343
344#[cfg(all(feature = "rpc-backend", feature = "std"))]
345pub mod rpc;
346
347#[cfg(test)]
348mod tests {
349    use super::{
350        KeyProviderClass, SdkBackend, SdkBackendKeyManagement, SdkKeyPurpose, SdkStoredKey,
351    };
352    use crate::capability::{EffectiveLimits, NegotiationRequest, NegotiationResponse};
353    use crate::error::{code, ErrorCategory, SdkError};
354    use crate::event::{EventBatch, EventCursor};
355    use crate::types::{
356        Ack, CancelResult, ConfigPatch, DeliverySnapshot, MessageId, RuntimeSnapshot, RuntimeState,
357        SendRequest, ShutdownMode, TickBudget,
358    };
359    use std::collections::BTreeMap;
360
361    struct NoKeyBackend;
362
363    impl SdkBackend for NoKeyBackend {
364        fn negotiate(&self, _req: NegotiationRequest) -> Result<NegotiationResponse, SdkError> {
365            Ok(NegotiationResponse {
366                runtime_id: "test-runtime".to_owned(),
367                active_contract_version: 2,
368                effective_capabilities: vec![],
369                effective_limits: EffectiveLimits {
370                    max_poll_events: 16,
371                    max_event_bytes: 4096,
372                    max_batch_bytes: 65_536,
373                    max_extension_keys: 8,
374                    idempotency_ttl_ms: 1_000,
375                },
376                contract_release: "v2.5".to_owned(),
377                schema_namespace: "v2".to_owned(),
378            })
379        }
380
381        fn send(&self, _req: SendRequest) -> Result<MessageId, SdkError> {
382            Ok(MessageId("msg-test".to_owned()))
383        }
384
385        fn cancel(&self, _id: MessageId) -> Result<CancelResult, SdkError> {
386            Ok(CancelResult::NotFound)
387        }
388
389        fn status(&self, _id: MessageId) -> Result<Option<DeliverySnapshot>, SdkError> {
390            Ok(None)
391        }
392
393        fn configure(&self, _expected_revision: u64, _patch: ConfigPatch) -> Result<Ack, SdkError> {
394            Ok(Ack { accepted: true, revision: Some(1) })
395        }
396
397        fn poll_events(
398            &self,
399            _cursor: Option<crate::event::EventCursor>,
400            _max: usize,
401        ) -> Result<EventBatch, SdkError> {
402            Ok(EventBatch {
403                events: Vec::new(),
404                next_cursor: EventCursor("cursor-0".to_owned()),
405                dropped_count: 0,
406                snapshot_high_watermark_seq_no: None,
407                extensions: BTreeMap::new(),
408            })
409        }
410
411        fn snapshot(&self) -> Result<RuntimeSnapshot, SdkError> {
412            Ok(RuntimeSnapshot {
413                runtime_id: "test-runtime".to_owned(),
414                state: RuntimeState::Running,
415                active_contract_version: 2,
416                event_stream_position: 0,
417                config_revision: 1,
418                queued_messages: 0,
419                in_flight_messages: 0,
420            })
421        }
422
423        fn shutdown(&self, _mode: ShutdownMode) -> Result<Ack, SdkError> {
424            Ok(Ack { accepted: true, revision: Some(2) })
425        }
426
427        fn tick(&self, _budget: TickBudget) -> Result<crate::types::TickResult, SdkError> {
428            Err(SdkError::new(
429                code::CAPABILITY_DISABLED,
430                ErrorCategory::Capability,
431                "manual ticking disabled",
432            ))
433        }
434    }
435
436    impl SdkBackendKeyManagement for NoKeyBackend {}
437
438    #[test]
439    fn sdk_backend_key_management_defaults_to_capability_disabled() {
440        let backend = NoKeyBackend;
441        for result in [
442            backend.key_provider_class().map(|_| ()),
443            backend.key_get("key-a").map(|_| ()),
444            backend
445                .key_put(SdkStoredKey {
446                    key_id: "key-a".to_owned(),
447                    purpose: SdkKeyPurpose::IdentitySigning,
448                    material: vec![1, 2, 3, 4],
449                })
450                .map(|_| ()),
451            backend.key_delete("key-a").map(|_| ()),
452            backend.key_list_ids().map(|_| ()),
453        ] {
454            let err = result.expect_err("key management methods should be disabled by default");
455            assert_eq!(err.code(), code::CAPABILITY_DISABLED);
456            assert_eq!(err.category, ErrorCategory::Capability);
457            assert_eq!(
458                err.details.get("capability_id").and_then(serde_json::Value::as_str),
459                Some("sdk.capability.key_management")
460            );
461        }
462    }
463
464    #[test]
465    fn sdk_backend_key_management_types_roundtrip() {
466        let value = SdkStoredKey {
467            key_id: "hsm-identity".to_owned(),
468            purpose: SdkKeyPurpose::IdentitySigning,
469            material: vec![42, 7, 9],
470        };
471        let json = serde_json::to_value(&value).expect("serialize key");
472        let parsed: SdkStoredKey = serde_json::from_value(json).expect("deserialize key");
473        assert_eq!(parsed.key_id, "hsm-identity");
474
475        let provider = KeyProviderClass::OsKeystore;
476        let provider_json = serde_json::to_string(&provider).expect("serialize provider");
477        assert_eq!(provider_json, "\"os_keystore\"");
478    }
479}