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}