1use std::path::Path;
7
8use serde_json::Value;
9
10use crate::codec::{FrameReader, Inbound, MAX_FRAME_BYTES, write_frame};
11use crate::protocol::{
12 AuditSummaryResult, BackendSpec, BlobFetchCancelParams, BlobFetchCancelResult, BlobFetchParams,
13 BlobFetchResult, BlobGrantParams, BlobPublishParams, BlobPublishResult, BlobScopeList, Hello,
14 InviteParams, InviteResult, OpenSessionParams, OrgJoinParams, OrgJoinResult, PairParams,
15 PairResult, PeerEndorseParams, PeerEndorseResult, PeerIntroduceParams, PeerRemoveParams,
16 PeerRenameParams, PeerServicesParams, PeerServicesResult, RegisterServiceParams, Request,
17 RosterInstallParams, RosterInstallResult, ServiceAllowParams, SetAppMetadataParams,
18 SetNicknameParams, SetRelaysParams, SetRelaysResult, SetRosterUrlParams, StatusResult,
19 StreamFrame, UnregisterServiceParams,
20};
21use crate::transport::{connect_local, split_local};
22
23pub type ControlRead = Box<dyn tokio::io::AsyncRead + Send + Unpin>;
27pub type ControlWrite = Box<dyn tokio::io::AsyncWrite + Send + Unpin>;
29
30pub struct ControlClient {
32 hello: Hello,
33 reader: FrameReader<ControlRead>,
34 writer: ControlWrite,
35}
36
37impl std::fmt::Debug for ControlClient {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 f.debug_struct("ControlClient")
42 .field("hello", &self.hello)
43 .finish_non_exhaustive()
44 }
45}
46
47#[derive(Debug)]
54pub enum ClientError {
55 Io(std::io::Error),
56 Closed(&'static str),
57 Malformed(&'static str),
58 WrongApi { got: String, want: &'static str },
59 Api(Value),
60}
61
62impl std::fmt::Display for ClientError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 ClientError::Io(err) => write!(f, "io: {err}"),
66 ClientError::Closed(what) => write!(f, "connection closed before {what}"),
67 ClientError::Malformed(what) => write!(f, "malformed {what} frame"),
68 ClientError::WrongApi { got, want } => {
69 write!(f, "unexpected api: got {got:?}, want {want:?}")
70 }
71 ClientError::Api(err) => write!(f, "control API error: {err}"),
72 }
73 }
74}
75
76impl std::error::Error for ClientError {}
77
78impl From<std::io::Error> for ClientError {
79 fn from(err: std::io::Error) -> Self {
80 ClientError::Io(err)
81 }
82}
83
84impl ControlClient {
85 pub fn hello(&self) -> &Hello {
86 &self.hello
87 }
88
89 pub async fn request(&mut self, request: Request) -> Result<Value, ClientError> {
92 let frame = serde_json::to_value(&request).expect("Request serializes");
93 self.request_value(&frame).await
94 }
95
96 pub async fn request_value(&mut self, request: &Value) -> Result<Value, ClientError> {
101 write_frame(&mut self.writer, request).await?;
102 match self.reader.next().await? {
103 Some(Inbound::Frame(resp)) => {
104 if let Some(err) = resp.get("error") {
105 return Err(ClientError::Api(err.clone()));
106 }
107 Ok(resp.get("result").cloned().unwrap_or(Value::Null))
108 }
109 Some(Inbound::Violation(_)) => Err(ClientError::Malformed("response")),
110 None => Err(ClientError::Closed("response")),
111 }
112 }
113
114 pub async fn open_session(
121 mut self,
122 peer: String,
123 service: String,
124 ) -> Result<(FrameReader<ControlRead>, ControlWrite), ClientError> {
125 let frame = serde_json::to_value(Request::OpenSession(OpenSessionParams { peer, service }))
126 .expect("Request serializes");
127 write_frame(&mut self.writer, &frame).await?;
128 Ok((self.reader, self.writer))
129 }
130
131 pub async fn open_stream(
139 mut self,
140 method: &str,
141 ) -> Result<(FrameReader<ControlRead>, ControlWrite), ClientError> {
142 let frame = serde_json::json!({ "method": method });
143 write_frame(&mut self.writer, &frame).await?;
144 Ok((self.reader, self.writer))
145 }
146
147 async fn request_typed<T: serde::de::DeserializeOwned>(
152 &mut self,
153 request: Request,
154 what: &'static str,
155 ) -> Result<T, ClientError> {
156 let v = self.request(request).await?;
157 serde_json::from_value(v).map_err(|_| ClientError::Malformed(what))
158 }
159
160 async fn request_ack(&mut self, request: Request) -> Result<(), ClientError> {
163 self.request(request).await.map(|_| ())
164 }
165
166 pub async fn status(&mut self) -> Result<StatusResult, ClientError> {
169 self.request_typed(Request::Status, "status result").await
170 }
171
172 pub async fn register_service(
175 &mut self,
176 name: &str,
177 backend: BackendSpec,
178 allow: Vec<String>,
179 ) -> Result<(), ClientError> {
180 self.register_service_with(name, backend, allow, false)
181 .await
182 }
183
184 pub async fn register_service_with(
189 &mut self,
190 name: &str,
191 backend: BackendSpec,
192 allow: Vec<String>,
193 ephemeral: bool,
194 ) -> Result<(), ClientError> {
195 self.request_ack(Request::RegisterService(RegisterServiceParams {
196 name: name.to_string(),
197 backend,
198 allow,
199 ephemeral,
200 rate_limit_per_min: None,
201 }))
202 .await
203 }
204
205 pub async fn invite(&mut self, services: Vec<String>) -> Result<InviteResult, ClientError> {
209 self.invite_with(services, None).await
210 }
211
212 pub async fn invite_with(
215 &mut self,
216 services: Vec<String>,
217 app_label: Option<String>,
218 ) -> Result<InviteResult, ClientError> {
219 self.invite_multi(services, app_label, None).await
220 }
221
222 pub async fn invite_multi(
230 &mut self,
231 services: Vec<String>,
232 app_label: Option<String>,
233 max_uses: Option<u32>,
234 ) -> Result<InviteResult, ClientError> {
235 self.invite_named(services, app_label, max_uses, None).await
236 }
237
238 pub async fn invite_named(
244 &mut self,
245 services: Vec<String>,
246 app_label: Option<String>,
247 max_uses: Option<u32>,
248 peer_nickname: Option<String>,
249 ) -> Result<InviteResult, ClientError> {
250 self.invite_full(services, app_label, max_uses, peer_nickname, false)
251 .await
252 }
253
254 pub async fn invite_full(
260 &mut self,
261 services: Vec<String>,
262 app_label: Option<String>,
263 max_uses: Option<u32>,
264 peer_nickname: Option<String>,
265 as_self: bool,
266 ) -> Result<InviteResult, ClientError> {
267 self.request_typed(
268 Request::Invite(InviteParams {
269 services,
270 app_label,
271 max_uses,
272 peer_nickname,
273 as_self,
274 }),
275 "invite result",
276 )
277 .await
278 }
279
280 pub async fn endorse_peer(
285 &mut self,
286 subject: &str,
287 subject_user_id: Option<String>,
288 ) -> Result<PeerEndorseResult, ClientError> {
289 self.request_typed(
290 Request::PeerEndorse(PeerEndorseParams {
291 subject: subject.to_string(),
292 subject_user_id,
293 }),
294 "peer endorse result",
295 )
296 .await
297 }
298
299 pub async fn introduce_peer(&mut self, params: PeerIntroduceParams) -> Result<(), ClientError> {
305 self.request_ack(Request::PeerIntroduce(params)).await
306 }
307
308 pub async fn pair(&mut self, invite_line: &str) -> Result<PairResult, ClientError> {
311 self.pair_as(invite_line, None).await
312 }
313
314 pub async fn pair_as(
321 &mut self,
322 invite_line: &str,
323 as_nickname: Option<String>,
324 ) -> Result<PairResult, ClientError> {
325 self.pair_opts(invite_line, as_nickname, false).await
326 }
327
328 pub async fn pair_opts(
341 &mut self,
342 invite_line: &str,
343 as_nickname: Option<String>,
344 allow_self_enroll: bool,
345 ) -> Result<PairResult, ClientError> {
346 self.request_typed(
347 Request::Pair(PairParams {
348 invite_line: invite_line.to_string(),
349 as_nickname,
350 allow_self_enroll,
351 }),
352 "pair result",
353 )
354 .await
355 }
356
357 pub async fn peer_remove(&mut self, nickname: &str) -> Result<(), ClientError> {
360 self.request_ack(Request::PeerRemove(PeerRemoveParams {
361 nickname: nickname.to_string(),
362 }))
363 .await
364 }
365
366 pub async fn peer_rename(
371 &mut self,
372 user_id: Option<String>,
373 nickname: Option<String>,
374 to: &str,
375 ) -> Result<(), ClientError> {
376 self.request_ack(Request::PeerRename(PeerRenameParams {
377 user_id,
378 nickname,
379 to: to.to_string(),
380 }))
381 .await
382 }
383
384 pub async fn roster_install(
387 &mut self,
388 path: &str,
389 org_root_pk: Option<String>,
390 ) -> Result<RosterInstallResult, ClientError> {
391 self.request_typed(
392 Request::RosterInstall(RosterInstallParams {
393 path: path.to_string(),
394 org_root_pk,
395 }),
396 "roster_install result",
397 )
398 .await
399 }
400
401 pub async fn org_join(
404 &mut self,
405 org_id: &str,
406 org_root_pk: &str,
407 user_id: &str,
408 user_key: &str,
409 ) -> Result<OrgJoinResult, ClientError> {
410 self.request_typed(
411 Request::OrgJoin(OrgJoinParams {
412 org_id: org_id.to_string(),
413 org_root_pk: org_root_pk.to_string(),
414 user_id: user_id.to_string(),
415 user_key: user_key.to_string(),
416 }),
417 "org_join result",
418 )
419 .await
420 }
421
422 pub async fn set_roster_url(&mut self, url: &str) -> Result<(), ClientError> {
425 self.request_ack(Request::SetRosterUrl(SetRosterUrlParams {
426 url: url.to_string(),
427 }))
428 .await
429 }
430
431 pub async fn peer_services(&mut self, peer: &str) -> Result<Vec<String>, ClientError> {
435 self.request_typed::<PeerServicesResult>(
436 Request::PeerServices(PeerServicesParams {
437 peer: peer.to_string(),
438 }),
439 "peer_services",
440 )
441 .await
442 .map(|r| r.services)
443 }
444
445 pub async fn unregister_service(&mut self, name: &str) -> Result<(), ClientError> {
449 self.request_ack(Request::UnregisterService(UnregisterServiceParams {
450 name: name.to_string(),
451 }))
452 .await
453 }
454
455 pub async fn service_allow_grant(
458 &mut self,
459 service: &str,
460 principal: &str,
461 ) -> Result<(), ClientError> {
462 self.request_ack(Request::ServiceAllowGrant(ServiceAllowParams {
463 service: service.to_string(),
464 principal: principal.to_string(),
465 }))
466 .await
467 }
468
469 pub async fn service_allow_revoke(
473 &mut self,
474 service: &str,
475 principal: &str,
476 ) -> Result<(), ClientError> {
477 self.request_ack(Request::ServiceAllowRevoke(ServiceAllowParams {
478 service: service.to_string(),
479 principal: principal.to_string(),
480 }))
481 .await
482 }
483
484 pub async fn set_app_metadata(&mut self, metadata: &str) -> Result<(), ClientError> {
488 self.request_ack(Request::SetAppMetadata(SetAppMetadataParams {
489 metadata: metadata.to_string(),
490 }))
491 .await
492 }
493
494 pub async fn set_relays(
503 &mut self,
504 relay_urls: &[String],
505 ) -> Result<SetRelaysResult, ClientError> {
506 self.request_typed::<SetRelaysResult>(
507 Request::SetRelays(SetRelaysParams {
508 relay_urls: relay_urls.to_vec(),
509 }),
510 "set_relays",
511 )
512 .await
513 }
514
515 pub async fn set_nickname(&mut self, nickname: &str) -> Result<(), ClientError> {
519 self.request_ack(Request::SetNickname(SetNicknameParams {
520 nickname: nickname.to_string(),
521 }))
522 .await
523 }
524
525 pub async fn audit_summary(&mut self) -> Result<AuditSummaryResult, ClientError> {
528 self.request_typed(Request::AuditSummary, "audit_summary result")
529 .await
530 }
531
532 pub async fn blob_publish(
534 &mut self,
535 scope: &str,
536 path: &str,
537 ) -> Result<BlobPublishResult, ClientError> {
538 self.request_typed(
539 Request::BlobPublish(BlobPublishParams {
540 scope: scope.to_string(),
541 path: path.to_string(),
542 }),
543 "blob_publish result",
544 )
545 .await
546 }
547
548 pub async fn blob_list(&mut self) -> Result<BlobScopeList, ClientError> {
553 self.blob_list_paged(Default::default()).await
554 }
555
556 pub async fn blob_list_paged(
558 &mut self,
559 params: crate::BlobListParams,
560 ) -> Result<BlobScopeList, ClientError> {
561 self.request_typed(Request::BlobList(params), "blob_list result")
562 .await
563 }
564
565 pub async fn blob_fetch(
568 &mut self,
569 ticket: &str,
570 dest_path: &str,
571 ) -> Result<BlobFetchResult, ClientError> {
572 self.request_typed(
573 Request::BlobFetch(BlobFetchParams {
574 ticket: ticket.to_string(),
575 dest_path: dest_path.to_string(),
576 }),
577 "blob_fetch result",
578 )
579 .await
580 }
581
582 pub async fn blob_fetch_cancel(
594 &mut self,
595 hash: &str,
596 ) -> Result<BlobFetchCancelResult, ClientError> {
597 self.request_typed(
598 Request::BlobFetchCancel(BlobFetchCancelParams {
599 hash: hash.to_string(),
600 }),
601 "blob_fetch_cancel result",
602 )
603 .await
604 }
605
606 pub async fn blob_grant(&mut self, scope: &str, principal: &str) -> Result<(), ClientError> {
612 self.request_ack(Request::BlobGrant(BlobGrantParams {
613 scope: scope.to_string(),
614 principal: principal.to_string(),
615 }))
616 .await
617 }
618
619 pub async fn subscribe(self) -> Result<StreamSubscription, ClientError> {
624 let (reader, writer) = self.open_stream("subscribe").await?;
625 Ok(StreamSubscription {
626 reader,
627 _writer: writer,
628 })
629 }
630}
631
632pub struct StreamSubscription {
637 reader: FrameReader<ControlRead>,
638 _writer: ControlWrite,
639}
640
641impl std::fmt::Debug for StreamSubscription {
643 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644 f.debug_struct("StreamSubscription").finish_non_exhaustive()
645 }
646}
647
648impl StreamSubscription {
649 pub async fn next(&mut self) -> Result<Option<StreamFrame>, ClientError> {
654 match self.reader.next().await? {
655 Some(Inbound::Frame(v)) => serde_json::from_value(v)
656 .map(Some)
657 .map_err(|_| ClientError::Malformed("stream frame")),
658 Some(Inbound::Violation(_)) => Err(ClientError::Malformed("stream frame")),
659 None => Ok(None),
660 }
661 }
662}
663
664pub async fn connect_control_io(
668 reader: impl tokio::io::AsyncRead + Send + Unpin + 'static,
669 writer: impl tokio::io::AsyncWrite + Send + Unpin + 'static,
670) -> Result<ControlClient, ClientError> {
671 let mut reader = FrameReader::new(Box::new(reader) as ControlRead, MAX_FRAME_BYTES);
672 let hello: Hello = match reader.next().await? {
673 Some(Inbound::Frame(v)) => {
674 serde_json::from_value(v).map_err(|_| ClientError::Malformed("hello"))?
675 }
676 Some(Inbound::Violation(_)) => return Err(ClientError::Malformed("hello")),
677 None => return Err(ClientError::Closed("hello")),
678 };
679 if hello.api != crate::protocol::API_NAME {
680 return Err(ClientError::WrongApi {
681 got: hello.api,
682 want: crate::protocol::API_NAME,
683 });
684 }
685 Ok(ControlClient {
686 hello,
687 reader,
688 writer: Box::new(writer) as ControlWrite,
689 })
690}
691
692pub async fn connect_control(path: &Path) -> Result<ControlClient, ClientError> {
694 let stream = connect_local(path).await?;
695 let (read_half, write_half) = split_local(stream);
696 connect_control_io(read_half, write_half).await
697}
698
699pub async fn connect_control_default() -> Result<ControlClient, ClientError> {
704 connect_control(&crate::paths::default_endpoint()?).await
705}
706
707#[cfg(all(test, feature = "service"))]
715mod tests {
716 use super::*;
717 use crate::protocol::{API_NAME, API_VERSION, BackendKind, ServiceInfo, StatusResult};
718 use crate::transport::{LocalListener, bind_local, split_local};
719 use tokio::io::AsyncWriteExt;
720
721 #[cfg(unix)]
726 fn test_endpoint(tag: &str) -> (std::path::PathBuf, tempfile::TempDir) {
727 let dir = tempfile::tempdir().unwrap();
728 let path = dir.path().join(format!("{tag}.sock"));
729 (path, dir)
730 }
731 #[cfg(windows)]
732 fn test_endpoint(tag: &str) -> (std::path::PathBuf, ()) {
733 use std::sync::atomic::{AtomicU64, Ordering};
734 static SEQ: AtomicU64 = AtomicU64::new(0);
735 let n = SEQ.fetch_add(1, Ordering::Relaxed);
736 let path = std::path::PathBuf::from(format!(
737 r"\\.\pipe\mcpmesh-client-test-{}-{tag}-{n}",
738 std::process::id()
739 ));
740 (path, ())
741 }
742
743 async fn stub_daemon(mut listener: LocalListener) {
745 let stream = listener.accept().await.unwrap();
746 let (read_half, mut writer) = split_local(stream);
747 write_frame(
748 &mut writer,
749 &serde_json::to_value(Hello {
750 api: API_NAME.into(),
751 api_version: API_VERSION.into(),
752 api_minor: 0,
753 stack_version: "0.1.0".into(),
754 })
755 .unwrap(),
756 )
757 .await
758 .unwrap();
759 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
760 let req = match reader.next().await.unwrap().unwrap() {
761 Inbound::Frame(v) => v,
762 Inbound::Violation(_) => panic!("violation"),
763 };
764 assert_eq!(req["method"], "status");
765 let result = StatusResult {
766 stack_version: "0.1.0".into(),
767 services: vec![ServiceInfo {
768 name: "kb".into(),
769 allow: vec![],
770 allow_display: vec![],
771 backend: BackendKind::Socket,
772 ephemeral: false,
773 }],
774 peers: vec![],
775 roster: None,
776 presence: vec![],
777 self_user_id: None,
778 recent_pairings: vec![],
779 reachability: vec![],
780 self_nickname: String::new(),
781 storage: None,
782 self_network: None,
783 };
784 write_frame(
785 &mut writer,
786 &serde_json::json!({ "jsonrpc": "2.0", "id": 1, "result": result }),
787 )
788 .await
789 .unwrap();
790 writer.flush().await.unwrap();
791 }
792
793 #[tokio::test]
796 async fn connect_control_io_handshakes_over_a_duplex() {
797 let (client_io, mut server_io) = tokio::io::duplex(4096);
798 tokio::spawn(async move {
799 write_frame(
800 &mut server_io,
801 &serde_json::to_value(Hello {
802 api: API_NAME.into(),
803 api_version: API_VERSION.into(),
804 api_minor: 0,
805 stack_version: "in-proc".into(),
806 })
807 .unwrap(),
808 )
809 .await
810 .unwrap();
811 });
812 let (r, w) = tokio::io::split(client_io);
813 let client = connect_control_io(r, w).await.expect("handshake");
814 assert_eq!(client.hello().stack_version, "in-proc");
815 }
816
817 #[tokio::test]
818 async fn connect_reads_hello_asserts_api_and_requests() {
819 let (sock, _guard) = test_endpoint("status");
820 let listener = bind_local(&sock).unwrap();
821 let server = tokio::spawn(stub_daemon(listener));
822
823 let mut client = connect_control(&sock).await.unwrap();
824 assert_eq!(client.hello().api, API_NAME);
825 let result = client.request(Request::Status).await.unwrap();
826 assert_eq!(result["services"][0]["name"], "kb");
827 assert_eq!(result["services"][0]["backend"], "socket");
828 server.await.unwrap();
829 }
830
831 #[tokio::test]
832 async fn wrong_api_hello_is_rejected() {
833 let (sock, _guard) = test_endpoint("wrongapi");
834 let listener = bind_local(&sock).unwrap();
835 tokio::spawn(async move {
836 let mut listener = listener;
837 let stream = listener.accept().await.unwrap();
838 let (_r, mut w) = split_local(stream);
839 write_frame(
840 &mut w,
841 &serde_json::json!({"api":"other/1","api_version":"1.0","stack_version":"0"}),
842 )
843 .await
844 .unwrap();
845 w.flush().await.unwrap();
846 });
847 match connect_control(&sock).await {
848 Err(ClientError::WrongApi { got, want }) => {
849 assert_eq!(got, "other/1");
850 assert_eq!(want, API_NAME);
851 }
852 other => panic!("expected WrongApi, got {other:?}"),
853 }
854 }
855
856 #[tokio::test]
857 async fn blob_fetch_and_publish_deserialize_typed_results() {
858 use crate::protocol::{BlobFetchResult, BlobPublishResult};
859 let (sock, _guard) = test_endpoint("blob");
860 let listener = bind_local(&sock).unwrap();
861 let server = tokio::spawn(async move {
862 let mut listener = listener;
863 let stream = listener.accept().await.unwrap();
864 let (read_half, mut writer) = split_local(stream);
865 write_frame(
866 &mut writer,
867 &serde_json::to_value(Hello {
868 api: API_NAME.into(),
869 api_version: API_VERSION.into(),
870 api_minor: 0,
871 stack_version: "0.1.0".into(),
872 })
873 .unwrap(),
874 )
875 .await
876 .unwrap();
877 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
878 let req = match reader.next().await.unwrap().unwrap() {
880 Inbound::Frame(v) => v,
881 Inbound::Violation(_) => panic!("violation"),
882 };
883 assert_eq!(req["method"], "blob_publish");
884 assert_eq!(req["params"]["scope"], "eng");
885 write_frame(
886 &mut writer,
887 &serde_json::json!({"jsonrpc":"2.0","id":1,"result":{"ticket":"blobT","hash":"ab"}}),
888 )
889 .await
890 .unwrap();
891 let req = match reader.next().await.unwrap().unwrap() {
893 Inbound::Frame(v) => v,
894 Inbound::Violation(_) => panic!("violation"),
895 };
896 assert_eq!(req["method"], "blob_fetch");
897 assert_eq!(req["params"]["ticket"], "blobT");
898 assert_eq!(req["params"]["dest_path"], "/tmp/out.bin");
899 write_frame(
900 &mut writer,
901 &serde_json::json!({"jsonrpc":"2.0","id":2,"result":{"hash":"cd","bytes_len":7}}),
902 )
903 .await
904 .unwrap();
905 let _ = (
906 BlobFetchResult {
907 hash: "cd".into(),
908 bytes_len: 7,
909 },
910 BlobPublishResult {
911 ticket: "blobT".into(),
912 hash: "ab".into(),
913 },
914 );
915 });
916
917 let mut client = connect_control(&sock).await.unwrap();
918 let pub_res = client.blob_publish("eng", "/tmp/a.bin").await.unwrap();
919 assert_eq!(pub_res.ticket, "blobT");
920 assert_eq!(pub_res.hash, "ab");
921 let fetch_res = client.blob_fetch("blobT", "/tmp/out.bin").await.unwrap();
922 assert_eq!(fetch_res.hash, "cd");
923 assert_eq!(fetch_res.bytes_len, 7);
924 server.await.unwrap();
925 }
926
927 #[tokio::test]
934 async fn frame_pipelined_behind_hello_survives_open_session_rebox() {
935 use tokio::io::AsyncRead;
936
937 let (sock, _guard) = test_endpoint("pipelined");
938 let listener = bind_local(&sock).unwrap();
939 let server = tokio::spawn(async move {
940 let mut listener = listener;
941 let stream = listener.accept().await.unwrap();
942 let (read_half, mut writer) = split_local(stream);
943 let mut bytes = serde_json::to_vec(
946 &serde_json::to_value(Hello {
947 api: API_NAME.into(),
948 api_version: API_VERSION.into(),
949 api_minor: 0,
950 stack_version: "0.1.0".into(),
951 })
952 .unwrap(),
953 )
954 .unwrap();
955 bytes.push(b'\n');
956 bytes.extend_from_slice(b"{\"jsonrpc\":\"2.0\",\"id\":42,\"result\":{}}\n");
957 writer.write_all(&bytes).await.unwrap();
958 writer.flush().await.unwrap();
959 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
961 let req = match reader.next().await.unwrap().unwrap() {
962 Inbound::Frame(v) => v,
963 Inbound::Violation(_) => panic!("violation"),
964 };
965 assert_eq!(req["method"], "open_session");
966 });
967
968 let client = connect_control(&sock).await.unwrap();
969 let (reader, _writer) = client
970 .open_session("peer".into(), "kb".into())
971 .await
972 .unwrap();
973 let boxed: Box<dyn AsyncRead + Unpin + Send> = Box::new(reader.into_inner());
975 let mut reframed = FrameReader::new(boxed, MAX_FRAME_BYTES);
976 match reframed.next().await.unwrap() {
977 Some(Inbound::Frame(v)) => assert_eq!(v["id"], 42),
978 other => panic!("pipelined frame was lost across the rebox: {other:?}"),
979 }
980 server.await.unwrap();
981 }
982
983 #[tokio::test]
984 async fn blob_grant_issues_request_and_acks() {
985 let (sock, _guard) = test_endpoint("grant");
986 let listener = bind_local(&sock).unwrap();
987 let server = tokio::spawn(async move {
988 let mut listener = listener;
989 let stream = listener.accept().await.unwrap();
990 let (read_half, mut writer) = split_local(stream);
991 write_frame(
992 &mut writer,
993 &serde_json::to_value(Hello {
994 api: API_NAME.into(),
995 api_version: API_VERSION.into(),
996 api_minor: 0,
997 stack_version: "0.1.0".into(),
998 })
999 .unwrap(),
1000 )
1001 .await
1002 .unwrap();
1003 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
1004 let req = match reader.next().await.unwrap().unwrap() {
1005 Inbound::Frame(v) => v,
1006 Inbound::Violation(_) => panic!("violation"),
1007 };
1008 assert_eq!(req["method"], "blob_grant");
1009 assert_eq!(req["params"]["scope"], "kb-sync");
1010 assert_eq!(req["params"]["principal"], "alice");
1011 write_frame(
1012 &mut writer,
1013 &serde_json::json!({"jsonrpc":"2.0","id":1,"result":{"ok":true}}),
1014 )
1015 .await
1016 .unwrap();
1017 });
1018 let mut client = connect_control(&sock).await.unwrap();
1019 client.blob_grant("kb-sync", "alice").await.unwrap();
1020 server.await.unwrap();
1021 }
1022
1023 #[tokio::test]
1027 async fn typed_status_helper_deserializes_the_result() {
1028 let (sock, _guard) = test_endpoint("typedstatus");
1029 let listener = bind_local(&sock).unwrap();
1030 let server = tokio::spawn(stub_daemon(listener));
1031
1032 let mut client = connect_control(&sock).await.unwrap();
1033 let status = client.status().await.unwrap();
1034 assert_eq!(status.stack_version, "0.1.0");
1035 assert_eq!(status.services[0].name, "kb");
1036 assert_eq!(status.services[0].backend, BackendKind::Socket);
1037 assert!(status.peers.is_empty());
1038 server.await.unwrap();
1039 }
1040
1041 #[tokio::test]
1044 async fn typed_ack_helpers_issue_requests_and_surface_api_errors() {
1045 let (sock, _guard) = test_endpoint("typedack");
1046 let listener = bind_local(&sock).unwrap();
1047 let server = tokio::spawn(async move {
1048 let mut listener = listener;
1049 let stream = listener.accept().await.unwrap();
1050 let (read_half, mut writer) = split_local(stream);
1051 write_frame(
1052 &mut writer,
1053 &serde_json::to_value(Hello {
1054 api: API_NAME.into(),
1055 api_version: API_VERSION.into(),
1056 api_minor: 0,
1057 stack_version: "0.1.0".into(),
1058 })
1059 .unwrap(),
1060 )
1061 .await
1062 .unwrap();
1063 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
1064 let req = match reader.next().await.unwrap().unwrap() {
1066 Inbound::Frame(v) => v,
1067 Inbound::Violation(_) => panic!("violation"),
1068 };
1069 assert_eq!(req["method"], "peer_remove");
1070 assert_eq!(req["params"]["nickname"], "bob");
1071 write_frame(
1072 &mut writer,
1073 &serde_json::json!({"jsonrpc":"2.0","id":1,"result":{}}),
1074 )
1075 .await
1076 .unwrap();
1077 let req = match reader.next().await.unwrap().unwrap() {
1079 Inbound::Frame(v) => v,
1080 Inbound::Violation(_) => panic!("violation"),
1081 };
1082 assert_eq!(req["method"], "peer_rename");
1083 assert_eq!(req["params"]["to"], "Bobby");
1084 write_frame(
1085 &mut writer,
1086 &serde_json::json!({"jsonrpc":"2.0","id":2,"error":{"code":-32000,"message":"taken"}}),
1087 )
1088 .await
1089 .unwrap();
1090 });
1091
1092 let mut client = connect_control(&sock).await.unwrap();
1093 client.peer_remove("bob").await.unwrap();
1094 match client.peer_rename(None, Some("bob".into()), "Bobby").await {
1095 Err(ClientError::Api(e)) => assert_eq!(e["message"], "taken"),
1096 other => panic!("expected Api error, got {other:?}"),
1097 }
1098 server.await.unwrap();
1099 }
1100
1101 #[tokio::test]
1104 async fn typed_subscribe_yields_frames_then_end() {
1105 use crate::protocol::{ActiveSession, AuditRecord, PeerReachability};
1106
1107 let (sock, _guard) = test_endpoint("subscribe");
1108 let listener = bind_local(&sock).unwrap();
1109 let server = tokio::spawn(async move {
1110 let mut listener = listener;
1111 let stream = listener.accept().await.unwrap();
1112 let (read_half, mut writer) = split_local(stream);
1113 write_frame(
1114 &mut writer,
1115 &serde_json::to_value(Hello {
1116 api: API_NAME.into(),
1117 api_version: API_VERSION.into(),
1118 api_minor: 0,
1119 stack_version: "0.1.0".into(),
1120 })
1121 .unwrap(),
1122 )
1123 .await
1124 .unwrap();
1125 let mut reader = FrameReader::new(read_half, MAX_FRAME_BYTES);
1126 let req = match reader.next().await.unwrap().unwrap() {
1127 Inbound::Frame(v) => v,
1128 Inbound::Violation(_) => panic!("violation"),
1129 };
1130 assert_eq!(req["method"], "subscribe");
1131 for frame in [
1132 StreamFrame::Snapshot {
1133 self_network: None,
1134 active_sessions: vec![ActiveSession {
1135 peer: "bob".into(),
1136 service: "notes".into(),
1137 opened_at: 7,
1138 principal: Some("eid:bob".into()),
1139 }],
1140 reachability: vec![PeerReachability {
1141 name: "bob".into(),
1142 reachable: true,
1143 rtt_ms: Some(42),
1144 age_secs: Some(3),
1145 meta: String::new(),
1146 principal: None,
1147 path: Default::default(),
1148 }],
1149 },
1150 StreamFrame::Event {
1151 record: Box::new(AuditRecord::session_open(
1152 "2026-07-03T14:02:11.480Z".into(),
1153 Some("bob".into()),
1154 "notes".into(),
1155 None,
1156 )),
1157 },
1158 StreamFrame::Lagged { dropped: 12 },
1159 ] {
1160 write_frame(&mut writer, &serde_json::to_value(&frame).unwrap())
1161 .await
1162 .unwrap();
1163 }
1164 writer.flush().await.unwrap();
1165 });
1167
1168 let client = connect_control(&sock).await.unwrap();
1169 let mut sub = client.subscribe().await.unwrap();
1170 match sub.next().await.unwrap().unwrap() {
1171 StreamFrame::Snapshot {
1172 active_sessions,
1173 reachability,
1174 ..
1175 } => {
1176 assert_eq!(active_sessions[0].peer, "bob");
1177 assert_eq!(reachability[0].rtt_ms, Some(42));
1178 }
1179 other => panic!("expected the snapshot first, got {other:?}"),
1180 }
1181 match sub.next().await.unwrap().unwrap() {
1182 StreamFrame::Event { record } => {
1183 assert_eq!(record.peer.as_deref(), Some("bob"));
1184 assert_eq!(record.service.as_deref(), Some("notes"));
1185 }
1186 other => panic!("expected the event, got {other:?}"),
1187 }
1188 assert_eq!(
1189 sub.next().await.unwrap(),
1190 Some(StreamFrame::Lagged { dropped: 12 })
1191 );
1192 assert_eq!(sub.next().await.unwrap(), None, "clean end of stream");
1193 server.await.unwrap();
1194 }
1195}