1#![allow(clippy::derivable_impls, clippy::should_implement_trait)]
2
3mod external_rpc;
4
5pub use external_rpc::*;
6
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9
10pub const MWS_MAX_MESSAGE_SIZE: usize = 16 * 1024 * 1024;
14pub const MWS_MAX_FRAME_SIZE: usize = 4 * 1024 * 1024;
15pub const MWS_MAX_WRITE_BUFFER_SIZE: usize = 32 * 1024 * 1024;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum ExceptionCode {
19 Unsupported,
20 NullData,
21 ErrorSubscribe,
22 ErrorUnsubscribe,
23 ErrorProcessDataReport,
24 ErrorProcessDataPoll,
25 ErrorLock,
26 ErrorUnlock,
27 FunctionNoImpl,
28 Unreachable,
29 Unauthenticated,
30 Unauthorized,
31 PreconditionFail,
32 SessionExpired,
33 Internal,
34 Unknown,
35 NotFound,
36 AlreadyExists,
37 BadRequest,
38 InvalidWidgetDefinition,
39 DeadlineExceeded,
40 TemporaryUnavailable,
41 ResourceLocked,
42 WsConnectionLost,
43 BillAutomationExceed,
44 BillTokenExceed,
45 MissingContext,
46}
47
48impl ExceptionCode {
49 pub fn from_str(s: &str) -> Self {
50 match s {
51 "UNSUPPORTED" => Self::Unsupported,
52 "NULL_DATA" => Self::NullData,
53 "ERROR_SUBSCRIBE" => Self::ErrorSubscribe,
54 "ERROR_UNSUBSCRIBE" => Self::ErrorUnsubscribe,
55 "ERROR_PROCESS_DATA_REPORT" => Self::ErrorProcessDataReport,
56 "ERROR_PROCESS_DATA_POLL" => Self::ErrorProcessDataPoll,
57 "ERROR_LOCK" => Self::ErrorLock,
58 "ERROR_UNLOCK" => Self::ErrorUnlock,
59 "FUNCTION_NO_IMPL" => Self::FunctionNoImpl,
60 "UNREACHABLE" => Self::Unreachable,
61 "UNAUTHENTICATED" => Self::Unauthenticated,
62 "UNAUTHORIZED" => Self::Unauthorized,
63 "PRECONDITION_FAIL" => Self::PreconditionFail,
64 "SESSION_EXPIRED" => Self::SessionExpired,
65 "INTERNAL" => Self::Internal,
66 "NOT_FOUND" => Self::NotFound,
67 "ALREADY_EXISTS" => Self::AlreadyExists,
68 "BAD_REQUEST" => Self::BadRequest,
69 "INVALID_WIDGET_DEFINITION" => Self::InvalidWidgetDefinition,
70 "DEADLINE_EXCEEDED" => Self::DeadlineExceeded,
71 "TEMPORARY_UNAVAILABLE" => Self::TemporaryUnavailable,
72 "RESOURCE_LOCKED" => Self::ResourceLocked,
73 "WS_CONNECTION_LOST" => Self::WsConnectionLost,
74 "BILL_AUTOMATION_EXCEED" => Self::BillAutomationExceed,
75 "BILL_TOKEN_EXCEED" => Self::BillTokenExceed,
76 "MISSING_CONTEXT" => Self::MissingContext,
77 _ => Self::Unknown,
78 }
79 }
80
81 pub fn as_str(&self) -> &'static str {
82 match self {
83 Self::Unsupported => "UNSUPPORTED",
84 Self::NullData => "NULL_DATA",
85 Self::ErrorSubscribe => "ERROR_SUBSCRIBE",
86 Self::ErrorUnsubscribe => "ERROR_UNSUBSCRIBE",
87 Self::ErrorProcessDataReport => "ERROR_PROCESS_DATA_REPORT",
88 Self::ErrorProcessDataPoll => "ERROR_PROCESS_DATA_POLL",
89 Self::ErrorLock => "ERROR_LOCK",
90 Self::ErrorUnlock => "ERROR_UNLOCK",
91 Self::FunctionNoImpl => "FUNCTION_NO_IMPL",
92 Self::Unreachable => "UNREACHABLE",
93 Self::Unauthenticated => "UNAUTHENTICATED",
94 Self::Unauthorized => "UNAUTHORIZED",
95 Self::PreconditionFail => "PRECONDITION_FAIL",
96 Self::SessionExpired => "SESSION_EXPIRED",
97 Self::Internal => "INTERNAL",
98 Self::Unknown => "UNKNOWN",
99 Self::NotFound => "NOT_FOUND",
100 Self::AlreadyExists => "ALREADY_EXISTS",
101 Self::BadRequest => "BAD_REQUEST",
102 Self::InvalidWidgetDefinition => "INVALID_WIDGET_DEFINITION",
103 Self::DeadlineExceeded => "DEADLINE_EXCEEDED",
104 Self::TemporaryUnavailable => "TEMPORARY_UNAVAILABLE",
105 Self::ResourceLocked => "RESOURCE_LOCKED",
106 Self::WsConnectionLost => "WS_CONNECTION_LOST",
107 Self::BillAutomationExceed => "BILL_AUTOMATION_EXCEED",
108 Self::BillTokenExceed => "BILL_TOKEN_EXCEED",
109 Self::MissingContext => "MISSING_CONTEXT",
110 }
111 }
112}
113
114impl From<ExceptionCode> for String {
115 fn from(value: ExceptionCode) -> Self {
116 value.as_str().to_string()
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
121pub struct ErrorResponse {
122 pub error: String,
123 pub message: String,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub details: Option<serde_json::Value>,
126}
127
128impl ErrorResponse {
129 pub fn new(error_code: impl Into<String>, message: impl Into<String>) -> Self {
130 Self {
131 error: error_code.into(),
132 message: message.into(),
133 details: None,
134 }
135 }
136
137 pub fn with_details(mut self, details: serde_json::Value) -> Self {
138 self.details = Some(details);
139 self
140 }
141}
142
143pub struct ClientIds;
144
145impl ClientIds {
146 pub fn from_cloud(peer_id: &str, ws_id: &str) -> String {
147 format!("C:{}:{}", peer_id, ws_id)
148 }
149
150 pub fn from_local(ws_id: &str) -> String {
151 format!("L:{}", ws_id)
152 }
153
154 pub fn from_telegram(bot_id: &str, chat_id: i64) -> String {
155 format!("M:telegram:{}:{}", bot_id, chat_id)
156 }
157
158 pub fn is_cloud(client_id: &str) -> bool {
159 client_id.starts_with("C:")
160 }
161
162 pub fn is_local(client_id: &str) -> bool {
163 client_id.starts_with("L:")
164 }
165
166 pub fn is_telegram(client_id: &str) -> bool {
167 client_id.starts_with("M:telegram:")
168 }
169
170 pub fn is_messaging(client_id: &str) -> bool {
171 client_id.starts_with("M:")
172 }
173
174 pub fn to_telegram_bot_id(client_id: &str) -> Option<i64> {
175 let parts: Vec<&str> = client_id.splitn(6, ':').collect();
176 parts.get(2)?.parse::<i64>().ok()
177 }
178
179 pub fn to_telegram_chat_id(client_id: &str) -> Option<i64> {
180 let parts: Vec<&str> = client_id.splitn(6, ':').collect();
181 parts.get(3)?.parse::<i64>().ok()
182 }
183
184 pub fn to_peer_id(client_id: &str) -> Option<String> {
185 let parts: Vec<&str> = client_id.splitn(3, ':').collect();
186 parts.get(1).map(|s| s.to_string())
187 }
188
189 pub fn to_device_id(client_id: &str) -> Option<String> {
190 let parts: Vec<&str> = client_id.splitn(3, ':').collect();
191 parts.get(1).map(|s| s.to_string())
192 }
193}
194
195pub struct MwsMessageType;
196
197impl MwsMessageType {
198 pub const HUB_REQ: &'static str = "hrq";
199 pub const HUB_RESP: &'static str = "hrp";
200 pub const HUB_DATA: &'static str = "hd";
201 pub const NODE_REQ: &'static str = "nrq";
202 pub const NODE_RESP: &'static str = "nrp";
203 pub const NODE_DATA: &'static str = "nd";
204 pub const AGENT_REQ: &'static str = "grq";
205 pub const AGENT_RESP: &'static str = "grp";
206 pub const AGENT_DATA: &'static str = "gd";
207 pub const CLOUD_REQ: &'static str = "crq";
208 pub const CLOUD_RESP: &'static str = "crp";
209 pub const CLOUD_DATA: &'static str = "cd";
210 pub const APP_REQ: &'static str = "arq";
211 pub const APP_RESP: &'static str = "arp";
212 pub const SERVER_REQ: &'static str = "srq";
213 pub const SERVER_RESP: &'static str = "srp";
214 pub const APP_DATA: &'static str = "ad";
215 pub const SERVER_DATA: &'static str = "sd";
216 pub const PING: &'static str = "pi";
217 pub const PONG: &'static str = "po";
218}
219
220pub struct MwsSource;
221
222impl MwsSource {
223 pub const IOS: &'static str = "ios";
224 pub const ANDROID: &'static str = "android";
225 pub const WINDOWS: &'static str = "windows";
226 pub const MAC: &'static str = "macos";
227 pub const LINUX: &'static str = "linux";
228 pub const WEB: &'static str = "web";
229 pub const PWA: &'static str = "pwa";
230 pub const MESSAGING: &'static str = "messaging";
231
232 pub fn is_desktop(source: &str) -> bool {
233 matches!(source, Self::MAC | Self::WINDOWS | Self::LINUX)
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
238#[serde(rename_all = "camelCase")]
239pub struct MwsClientInfo {
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub client_id: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub user_id: Option<String>,
244}
245
246impl MwsClientInfo {
247 pub fn new(client_id: String, user_id: String) -> Self {
248 Self {
249 client_id: Some(client_id),
250 user_id: Some(user_id),
251 }
252 }
253
254 pub fn from_ws_id(ws_id: String) -> Self {
255 Self {
256 client_id: Some(ClientIds::from_local(&ws_id)),
257 user_id: None,
258 }
259 }
260
261 pub fn from_client_id(client_id: String) -> Self {
262 Self {
263 client_id: Some(client_id),
264 user_id: None,
265 }
266 }
267
268 pub fn to_client_id(&self) -> String {
269 self.client_id
270 .clone()
271 .unwrap_or_else(|| ClientIds::from_local("unknown"))
272 }
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct MwsMessage {
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub scope_id: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub from: Option<String>,
282 #[serde(skip_serializing_if = "Option::is_none")]
283 pub target: Option<String>,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 pub sig: Option<String>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub r#type: Option<String>,
288 #[serde(skip_serializing_if = "Option::is_none")]
289 pub payload: Option<String>,
290 #[serde(skip_serializing_if = "Option::is_none")]
291 pub error: Option<ErrorResponse>,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 pub client_info: Option<MwsClientInfo>,
294}
295
296impl MwsMessage {
297 pub fn create(
298 scope_id: Option<String>,
299 target: String,
300 sig: String,
301 payload: String,
302 msg_type: String,
303 ) -> Self {
304 Self {
305 scope_id,
306 target: Some(target),
307 sig: Some(sig),
308 payload: Some(payload),
309 r#type: Some(msg_type),
310 from: None,
311 error: None,
312 client_info: None,
313 }
314 }
315
316 pub fn dummy() -> Self {
317 Self {
318 scope_id: None,
319 from: None,
320 target: None,
321 sig: None,
322 r#type: None,
323 payload: None,
324 error: None,
325 client_info: None,
326 }
327 }
328
329 pub fn server_response(
330 target: String,
331 sig: String,
332 payload: String,
333 client_info: MwsClientInfo,
334 ) -> Self {
335 Self {
336 scope_id: None,
337 from: None,
338 target: Some(target),
339 sig: Some(sig),
340 r#type: Some(MwsMessageType::SERVER_RESP.to_string()),
341 payload: Some(payload),
342 error: None,
343 client_info: Some(client_info),
344 }
345 }
346
347 pub fn server_response_error(
348 target: String,
349 sig: String,
350 error: ErrorResponse,
351 client_info: MwsClientInfo,
352 ) -> Self {
353 Self {
354 scope_id: None,
355 from: None,
356 target: Some(target),
357 sig: Some(sig),
358 r#type: Some(MwsMessageType::SERVER_RESP.to_string()),
359 payload: None,
360 error: Some(error),
361 client_info: Some(client_info),
362 }
363 }
364
365 pub fn unscoped_server_data(
366 target: String,
367 payload: String,
368 client_info: MwsClientInfo,
369 ) -> Self {
370 Self {
371 scope_id: None,
372 from: None,
373 target: Some(target),
374 sig: None,
375 r#type: Some(MwsMessageType::SERVER_DATA.to_string()),
376 payload: Some(payload),
377 error: None,
378 client_info: Some(client_info),
379 }
380 }
381
382 pub fn scoped_server_data(
383 scope_id: String,
384 target: String,
385 payload: String,
386 client_info: Option<MwsClientInfo>,
387 ) -> Self {
388 Self {
389 scope_id: Some(scope_id),
390 from: None,
391 target: Some(target),
392 sig: None,
393 r#type: Some(MwsMessageType::SERVER_DATA.to_string()),
394 payload: Some(payload),
395 error: None,
396 client_info,
397 }
398 }
399
400 pub fn hub_response(
401 target: String,
402 sig: String,
403 payload: String,
404 client_info: MwsClientInfo,
405 ) -> Self {
406 Self {
407 scope_id: None,
408 from: None,
409 target: Some(target),
410 sig: Some(sig),
411 r#type: Some(MwsMessageType::HUB_RESP.to_string()),
412 payload: Some(payload),
413 error: None,
414 client_info: Some(client_info),
415 }
416 }
417
418 pub fn hub_response_error(
419 target: String,
420 sig: String,
421 error: ErrorResponse,
422 client_info: MwsClientInfo,
423 ) -> Self {
424 Self {
425 scope_id: None,
426 from: None,
427 target: Some(target),
428 sig: Some(sig),
429 r#type: Some(MwsMessageType::HUB_RESP.to_string()),
430 payload: None,
431 error: Some(error),
432 client_info: Some(client_info),
433 }
434 }
435
436 pub fn hub_request(
437 scope_id: Option<String>,
438 target: String,
439 sig: String,
440 payload: String,
441 ) -> Self {
442 Self {
443 scope_id,
444 from: None,
445 target: Some(target),
446 sig: Some(sig),
447 r#type: Some(MwsMessageType::HUB_REQ.to_string()),
448 payload: Some(payload),
449 error: None,
450 client_info: None,
451 }
452 }
453
454 pub fn hub_data(
455 scope_id: Option<String>,
456 target: String,
457 sig: String,
458 payload: String,
459 ) -> Self {
460 Self {
461 scope_id,
462 from: None,
463 target: Some(target),
464 sig: Some(sig),
465 r#type: Some(MwsMessageType::HUB_DATA.to_string()),
466 payload: Some(payload),
467 error: None,
468 client_info: None,
469 }
470 }
471
472 pub fn set_from(&mut self, from: String) {
473 self.from = Some(from);
474 }
475
476 pub fn set_target(&mut self, target: String) {
477 self.target = Some(target);
478 }
479
480 pub fn set_sig(&mut self, sig: String) {
481 self.sig = Some(sig);
482 }
483
484 pub fn set_payload(&mut self, payload: String) {
485 self.payload = Some(payload);
486 }
487
488 pub fn set_type(&mut self, msg_type: String) {
489 self.r#type = Some(msg_type);
490 }
491
492 pub fn set_client_info(&mut self, client_info: MwsClientInfo) {
493 self.client_info = Some(client_info);
494 }
495}
496
497impl Default for MwsMessage {
498 fn default() -> Self {
499 Self::dummy()
500 }
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505pub struct NodeAuthRequest {
506 pub hub_id: String,
507 pub token: String,
508 pub node_type: String,
509 #[serde(default)]
510 pub node_id: String,
511 #[serde(default)]
512 pub instance_id: String,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub scope_id: Option<String>,
515 pub host_id: String,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub host_name: Option<String>,
518 pub fingerprint: String,
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize)]
522#[serde(rename_all = "camelCase")]
523pub struct NodeAuthResponse {
524 pub ok: bool,
525 #[serde(skip_serializing_if = "Option::is_none")]
526 pub session_id: Option<String>,
527 #[serde(skip_serializing_if = "Option::is_none")]
528 pub hub_id: Option<String>,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub tenant_id: Option<String>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub scope_id: Option<String>,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub error: Option<String>,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub error_code: Option<String>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub recovery_action: Option<String>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub node_id: Option<String>,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(rename_all = "camelCase")]
545pub struct NodeTokenIssueRequest {
546 pub node_type: String,
547 pub candidate_host_id: String,
548 pub candidate_fingerprint: String,
549 pub challenge: NodeChallengeEvidence,
550}
551
552#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
553#[serde(rename_all = "camelCase")]
554pub enum NodeInstancePolicy {
555 Multiple,
556 Singleton,
557 Shared,
558}
559
560impl NodeInstancePolicy {
561 pub fn as_str(self) -> &'static str {
562 match self {
563 Self::Multiple => "multiple",
564 Self::Singleton => "singleton",
565 Self::Shared => "shared",
566 }
567 }
568}
569
570impl Default for NodeInstancePolicy {
571 fn default() -> Self {
572 Self::Multiple
573 }
574}
575
576impl std::fmt::Display for NodeInstancePolicy {
577 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
578 f.write_str(self.as_str())
579 }
580}
581
582impl FromStr for NodeInstancePolicy {
583 type Err = String;
584
585 fn from_str(value: &str) -> Result<Self, Self::Err> {
586 match value.trim() {
587 "multiple" | "Multiple" => Ok(Self::Multiple),
588 "singleton" | "Singleton" => Ok(Self::Singleton),
589 "shared" | "Shared" => Ok(Self::Shared),
590 other => Err(format!("unsupported node instance policy: {other}")),
591 }
592 }
593}
594
595#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
596#[serde(rename_all = "camelCase")]
597pub enum NodeBindingStatus {
598 Pending,
599 Active,
600 Revoked,
601 Expired,
602 Failed,
603}
604
605impl NodeBindingStatus {
606 pub fn as_str(self) -> &'static str {
607 match self {
608 Self::Pending => "pending",
609 Self::Active => "active",
610 Self::Revoked => "revoked",
611 Self::Expired => "expired",
612 Self::Failed => "failed",
613 }
614 }
615}
616
617impl Default for NodeBindingStatus {
618 fn default() -> Self {
619 Self::Active
620 }
621}
622
623impl std::fmt::Display for NodeBindingStatus {
624 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
625 f.write_str(self.as_str())
626 }
627}
628
629impl FromStr for NodeBindingStatus {
630 type Err = String;
631
632 fn from_str(value: &str) -> Result<Self, Self::Err> {
633 match value.trim() {
634 "pending" | "Pending" => Ok(Self::Pending),
635 "active" | "Active" => Ok(Self::Active),
636 "revoked" | "Revoked" => Ok(Self::Revoked),
637 "expired" | "Expired" => Ok(Self::Expired),
638 "failed" | "Failed" => Ok(Self::Failed),
639 other => Err(format!("unsupported node binding status: {other}")),
640 }
641 }
642}
643
644pub const NODE_ONBOARDING_CHALLENGE_PROTOCOL: &str = "meow.node.onboarding.challenge";
645pub const NODE_ONBOARDING_CHALLENGE_AUDIENCE: &str = "meow-core:node-onboarding";
646pub const NODE_ONBOARDING_CHALLENGE_ALGORITHM: &str = "Ed25519";
647pub const NODE_ONBOARDING_TOKEN_ISSUER_PREFIX: &str = "meow-core:hub:";
648pub const NODE_ONBOARDING_TOKEN_AUDIENCE: &str = "meow-node:onboarding";
649pub const NODE_ONBOARDING_ES256_ALGORITHM: &str = "ES256";
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
652#[serde(rename_all = "camelCase")]
653pub struct NodeChallengeEvidence {
654 pub protocol: String,
655 pub algorithm: String,
656 pub payload: String,
657 pub signature: String,
658 pub public_key: String,
659 pub fingerprint: String,
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize)]
663#[serde(rename_all = "camelCase")]
664pub struct NodeChallengePayload {
665 pub protocol: String,
666 pub aud: String,
667 pub nonce: String,
668 pub scope_id: String,
669 pub node_type: String,
670 pub host_id: String,
671 pub fingerprint: String,
672 pub service_instance_id: String,
673 pub instance_policy: NodeInstancePolicy,
674 pub instance_slot: String,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct NodeOnboardingIssuerInfo {
680 pub tenant_id: String,
681 pub scope_id: String,
682 pub hub_id: String,
683 pub issuer: String,
684 pub audience: String,
685 pub algorithm: String,
686 pub kid: String,
687 pub public_key: String,
688 pub fingerprint: String,
689}
690
691impl NodeOnboardingIssuerInfo {
692 pub fn es256(
693 tenant_id: String,
694 scope_id: String,
695 hub_id: String,
696 issuer: String,
697 kid: String,
698 public_key: String,
699 fingerprint: String,
700 ) -> Self {
701 Self {
702 tenant_id,
703 scope_id,
704 hub_id,
705 issuer,
706 audience: NODE_ONBOARDING_TOKEN_AUDIENCE.to_string(),
707 algorithm: NODE_ONBOARDING_ES256_ALGORITHM.to_string(),
708 kid,
709 public_key,
710 fingerprint,
711 }
712 }
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub struct NodeTokenIssueResponse {
718 pub token: String,
719 pub node_id: String,
720 pub expires_in: i64,
721 pub hub_id: String,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
725#[serde(rename_all = "camelCase")]
726pub struct NodeTokenRevokeRequest {
727 pub node_type: String,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub node_id: Option<String>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct NodeTokenRevokeResponse {
735 pub success: bool,
736}
737
738#[derive(Debug, Clone, Serialize, Deserialize)]
739#[serde(rename_all = "camelCase")]
740pub struct NodeTokenListItem {
741 pub node_id: String,
742 pub node_type: String,
743 pub hub_id: String,
744 pub host_id: String,
745 pub fingerprint: String,
746 pub service_instance_id: String,
747 pub instance_policy: NodeInstancePolicy,
748 pub instance_slot: String,
749 pub status: NodeBindingStatus,
750 pub issued_at: i64,
751 pub expires_at: i64,
752}
753
754#[derive(Debug, Clone, Serialize, Deserialize)]
755#[serde(rename_all = "camelCase")]
756pub struct NodeTokenListResponse {
757 #[serde(default)]
758 pub nodes: Vec<NodeTokenListItem>,
759}
760
761#[derive(Debug, Clone, Serialize, Deserialize)]
762#[serde(rename_all = "camelCase")]
763pub struct NodeInstanceListItem {
764 pub node_id: String,
765 pub node_type: String,
766 pub hub_id: String,
767 pub host_id: String,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub host_name: Option<String>,
770 pub fingerprint: String,
771 pub service_instance_id: String,
772 pub instance_policy: NodeInstancePolicy,
773 pub instance_slot: String,
774 pub binding_status: NodeBindingStatus,
775 pub issued_at: i64,
776 pub expires_at: i64,
777 pub connected: bool,
778 pub runtime_status: String,
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[serde(rename_all = "camelCase")]
783pub struct NodeInstanceListResponse {
784 #[serde(default)]
785 pub instances: Vec<NodeInstanceListItem>,
786}
787
788#[derive(Debug, Clone, Serialize, Deserialize)]
789#[serde(rename_all = "camelCase")]
790pub struct AuthenticatedSession {
791 pub tenant_id: String,
792 pub scope_id: String,
793 pub user_id: String,
794 pub is_test: bool,
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
798#[serde(rename_all = "camelCase")]
799pub struct Image {
800 #[serde(skip_serializing_if = "Option::is_none")]
801 pub url: Option<String>,
802 #[serde(skip_serializing_if = "Option::is_none")]
803 pub base64: Option<String>,
804 #[serde(skip_serializing_if = "Option::is_none")]
805 pub mime_type: Option<String>,
806 #[serde(skip_serializing_if = "Option::is_none")]
807 pub created_at: Option<i64>,
808}
809
810impl Image {
811 pub fn from_base64(base64: impl Into<String>, mime_type: impl Into<String>) -> Self {
812 Self {
813 base64: Some(base64.into()),
814 mime_type: Some(mime_type.into()),
815 ..Self::default()
816 }
817 }
818}
819
820#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
821#[serde(rename_all = "camelCase")]
822pub struct ChatToolCallFunction {
823 pub name: String,
824 pub arguments: String,
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
828#[serde(rename_all = "camelCase")]
829pub struct ChatToolCall {
830 pub id: String,
831 #[serde(rename = "type")]
832 pub call_type: String,
833 pub function: ChatToolCallFunction,
834}
835
836#[derive(Debug, Clone, Serialize, Deserialize, Default)]
837#[serde(rename_all = "camelCase")]
838pub struct ChatMessage {
839 pub role: String,
840 pub content: String,
841 #[serde(default, skip_serializing_if = "Vec::is_empty")]
842 pub images: Vec<Image>,
843 #[serde(default, skip_serializing_if = "Vec::is_empty")]
844 pub tool_calls: Vec<ChatToolCall>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub tool_call_id: Option<String>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub name: Option<String>,
849}
850
851#[derive(Debug, Clone, Serialize, Deserialize, Default)]
852#[serde(rename_all = "camelCase")]
853pub struct LlmGenerationOptions {
854 #[serde(default)]
855 pub temperature: Option<f32>,
856 #[serde(default)]
857 pub max_tokens: Option<u32>,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize, Default)]
861#[serde(rename_all = "camelCase")]
862pub struct LlmCompleteRequest {
863 #[serde(default)]
864 pub use_case: Option<String>,
865 #[serde(default)]
866 pub mode: Option<String>,
867 pub messages: Vec<ChatMessage>,
868 #[serde(default)]
869 pub tools: Option<serde_json::Value>,
870 #[serde(default)]
871 pub provider: Option<serde_json::Value>,
872 #[serde(default)]
873 pub response_format: Option<serde_json::Value>,
874 #[serde(default)]
875 pub options: LlmGenerationOptions,
876 #[serde(default)]
877 pub temperature: Option<f32>,
878 #[serde(default)]
879 pub max_tokens: Option<u32>,
880}
881
882#[derive(Debug, Clone, Serialize, Deserialize, Default)]
883#[serde(rename_all = "camelCase")]
884pub struct LlmRouteInfo {
885 pub provider: String,
886 pub model: String,
887 #[serde(default)]
888 pub mode: Option<String>,
889}
890
891#[derive(Debug, Clone, Serialize, Deserialize, Default)]
892#[serde(rename_all = "camelCase")]
893pub struct LlmCompleteResponse {
894 pub content: String,
895 #[serde(default)]
896 pub tool_calls: Vec<ProviderToolCall>,
897 #[serde(default)]
898 pub route: Option<LlmRouteInfo>,
899}
900
901#[derive(Debug, Clone, Serialize, Deserialize)]
902#[serde(rename_all = "camelCase")]
903pub struct ProviderToolCall {
904 pub id: String,
905 pub name: String,
906 pub arguments_json: String,
907}
908
909#[derive(Debug, Clone, Serialize, Deserialize)]
910pub enum ServiceCoreInput {
911 ClientAuth {
912 message: MwsMessage,
913 ws_id: String,
914 scope_id: String,
915 },
916 ClientRequest {
917 target: String,
918 payload: String,
919 ws_id: String,
920 tenant_id: String,
921 scope_id: String,
922 user_id: String,
923 is_test: bool,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
925 surface_id: Option<String>,
926 },
927 NodeAuth {
928 request: NodeAuthRequest,
929 },
930 NodeRequest {
931 target: String,
932 payload: String,
933 tenant_id: String,
934 scope_id: String,
935 node_type: String,
936 instance_id: String,
937 },
938 BackendStatusChanged {
939 tenant_id: String,
940 scope_id: String,
941 target: String,
942 payload: String,
943 },
944 ClientResponse {
945 message: MwsMessage,
946 },
947 NodeResponse {
948 message: MwsMessage,
949 },
950 IssueLocalSessionToken {
951 tenant_id: String,
952 scope_id: String,
953 user_id: String,
954 app_client_id: String,
955 },
956 SetLocalAppClientFocus {
957 ws_id: String,
958 focused: bool,
959 },
960 RefreshLocalAppClient {
961 ws_id: String,
962 },
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize)]
966pub enum ServiceCoreResponse {
967 ClientAuth {
968 session: Option<AuthenticatedSession>,
969 response: MwsMessage,
970 },
971 ClientRequest {
972 response_payload: Option<String>,
973 client_info: MwsClientInfo,
974 },
975 NodeAuth(NodeAuthResponse),
976 NodeRequest {
977 response_payload: Option<String>,
978 },
979 LocalSessionToken(String),
980 Ack {
981 handled: bool,
982 },
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize)]
986pub enum ServiceCoreEffect {
987 Local {
988 client_id: String,
989 message: MwsMessage,
990 },
991 Bridge {
992 tenant_id: String,
993 scope_id: String,
994 message: MwsMessage,
995 },
996 Node {
997 connection_key: String,
998 message: MwsMessage,
999 },
1000 NodeDisconnect {
1001 connection_key: String,
1002 },
1003 Surface {
1004 tenant_id: String,
1005 scope_id: String,
1006 user_id: String,
1007 surface_id: String,
1008 target: String,
1009 payload: String,
1010 },
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1014pub struct ServiceCoreOutput {
1015 #[serde(skip_serializing_if = "Option::is_none")]
1016 pub response: Option<ServiceCoreResponse>,
1017 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1018 pub effects: Vec<ServiceCoreEffect>,
1019}
1020
1021#[derive(Debug, Clone, Serialize, Deserialize)]
1022#[serde(rename_all = "camelCase")]
1023pub struct UserSetting {
1024 #[serde(default)]
1025 pub script_mode: bool,
1026 #[serde(default)]
1027 pub debug_mode: bool,
1028 #[serde(default)]
1029 pub eng_account: bool,
1030}
1031
1032impl UserSetting {
1033 pub fn new() -> Self {
1034 Self {
1035 script_mode: false,
1036 debug_mode: false,
1037 eng_account: false,
1038 }
1039 }
1040}
1041
1042impl Default for UserSetting {
1043 fn default() -> Self {
1044 Self::new()
1045 }
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1049#[serde(rename_all = "camelCase")]
1050pub struct UserInfo {
1051 #[serde(skip_serializing_if = "Option::is_none")]
1052 pub id: Option<String>,
1053 #[serde(skip_serializing_if = "Option::is_none")]
1054 pub name: Option<String>,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub email: Option<String>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 pub setting: Option<UserSetting>,
1059 #[serde(default)]
1060 pub eng: bool,
1061}
1062
1063impl UserInfo {
1064 pub fn new(id: String) -> Self {
1065 Self {
1066 id: Some(id),
1067 name: None,
1068 email: None,
1069 setting: None,
1070 eng: false,
1071 }
1072 }
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize)]
1076#[serde(rename_all = "camelCase")]
1077pub struct ScopeMember {
1078 pub id: Option<String>,
1079 pub email: Option<String>,
1080 pub name: Option<String>,
1081 pub pending: bool,
1082 pub role: Option<String>,
1083}
1084
1085impl Default for ScopeMember {
1086 fn default() -> Self {
1087 Self {
1088 id: None,
1089 email: None,
1090 name: None,
1091 pending: false,
1092 role: None,
1093 }
1094 }
1095}
1096
1097#[derive(Debug, Clone, Serialize, Deserialize)]
1098#[serde(rename_all = "camelCase")]
1099pub struct ScopeInfo {
1100 pub name: Option<String>,
1101 pub id: Option<String>,
1102 pub pending: bool,
1103 pub members: Option<Vec<ScopeMember>>,
1104 pub execution_env: Option<String>,
1105 pub mode: Option<String>,
1106 pub connection_mode: Option<String>,
1107 pub agent_mode: Option<String>,
1108 pub default_active: bool,
1109 #[serde(default)]
1110 pub is_test: bool,
1111}
1112
1113impl Default for ScopeInfo {
1114 fn default() -> Self {
1115 Self {
1116 name: None,
1117 id: None,
1118 pending: false,
1119 members: None,
1120 execution_env: None,
1121 mode: None,
1122 connection_mode: None,
1123 agent_mode: None,
1124 default_active: false,
1125 is_test: false,
1126 }
1127 }
1128}
1129
1130#[derive(Debug, Clone, Serialize, Deserialize)]
1131#[serde(rename_all = "camelCase")]
1132pub struct AuthConfig {
1133 pub tenant_id: String,
1134 pub heartbeat_interval: i32,
1135 pub command_timeout: i32,
1136 pub user_info: UserInfo,
1137 pub scope: ScopeInfo,
1138 #[serde(skip_serializing_if = "Option::is_none")]
1139 pub hub_id: Option<String>,
1140 #[serde(skip_serializing_if = "Option::is_none")]
1141 pub jwt_token: Option<String>,
1142}
1143
1144impl AuthConfig {
1145 pub fn new(
1146 tenant_id: String,
1147 heartbeat_interval: i32,
1148 command_timeout: i32,
1149 user_info: UserInfo,
1150 scope: ScopeInfo,
1151 ) -> Self {
1152 Self {
1153 tenant_id,
1154 heartbeat_interval,
1155 command_timeout,
1156 user_info,
1157 scope,
1158 hub_id: None,
1159 jwt_token: None,
1160 }
1161 }
1162}
1163
1164impl Default for AuthConfig {
1165 fn default() -> Self {
1166 Self {
1167 tenant_id: String::new(),
1168 heartbeat_interval: 240,
1169 command_timeout: 10,
1170 user_info: UserInfo::new(String::new()),
1171 scope: ScopeInfo::default(),
1172 hub_id: None,
1173 jwt_token: None,
1174 }
1175 }
1176}
1177
1178#[derive(Debug, Clone, Serialize, Deserialize)]
1179#[serde(rename_all = "camelCase")]
1180pub struct AuthRequest {
1181 pub token: String,
1182 pub source: String,
1183 pub scope_id: String,
1184 pub device_id: String,
1185 pub client_source: String,
1186 #[serde(default)]
1187 pub tenant_id: Option<String>,
1188 #[serde(default)]
1189 pub hub_id: Option<String>,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize)]
1193#[serde(rename_all = "camelCase")]
1194pub struct HubMdnsInstanceRecord {
1195 pub tenant_id: String,
1196 pub scope_id: String,
1197 pub hub_id: String,
1198 pub scope_name: String,
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use super::{MwsMessage, MwsMessageType};
1204
1205 #[test]
1206 fn scoped_server_data_builds_scope_envelope() {
1207 let message = MwsMessage::scoped_server_data(
1208 "scope-1".to_string(),
1209 "/dialog".to_string(),
1210 "{}".to_string(),
1211 None,
1212 );
1213
1214 assert_eq!(message.scope_id.as_deref(), Some("scope-1"));
1215 assert_eq!(message.target.as_deref(), Some("/dialog"));
1216 assert_eq!(message.r#type.as_deref(), Some(MwsMessageType::SERVER_DATA));
1217 assert_eq!(message.payload.as_deref(), Some("{}"));
1218 assert!(message.client_info.is_none());
1219 }
1220}