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";
650pub const NODE_ONBOARDING_TRANSACTION_TTL_SECONDS: u64 = 5 * 60;
651pub const HUB_CONNECTION_PROOF_TARGET: &str = "/identity/hub/prove";
652pub const HUB_CONNECTION_PROOF_PROTOCOL: &str = "meow.hub.connection.proof.v1";
653
654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
655#[serde(rename_all = "camelCase")]
656pub struct HubConnectionProofRequest {
657 pub protocol: String,
658 pub hub_id: String,
659 pub tenant_id: String,
660 pub scope_id: String,
661 pub nonce: String,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
665#[serde(rename_all = "camelCase")]
666pub struct HubConnectionProofResponse {
667 pub protocol: String,
668 pub hub_id: String,
669 pub tenant_id: String,
670 pub scope_id: String,
671 pub nonce: String,
672 pub key_id: String,
673 pub signature: String,
674}
675
676pub fn hub_connection_proof_signing_payload(request: &HubConnectionProofRequest) -> Vec<u8> {
679 let fields = [
680 request.protocol.as_str(),
681 request.hub_id.as_str(),
682 request.tenant_id.as_str(),
683 request.scope_id.as_str(),
684 request.nonce.as_str(),
685 ];
686 let mut payload = Vec::new();
687 for field in fields {
688 payload.extend_from_slice(&(field.len() as u64).to_be_bytes());
689 payload.extend_from_slice(field.as_bytes());
690 }
691 payload
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
695#[serde(rename_all = "camelCase")]
696pub struct NodeChallengeEvidence {
697 pub protocol: String,
698 pub algorithm: String,
699 pub payload: String,
700 pub signature: String,
701 pub public_key: String,
702 pub fingerprint: String,
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct NodeChallengePayload {
708 pub protocol: String,
709 pub aud: String,
710 pub nonce: String,
711 pub scope_id: String,
712 pub node_type: String,
713 pub host_id: String,
714 pub fingerprint: String,
715 pub service_instance_id: String,
716 pub instance_policy: NodeInstancePolicy,
717 pub instance_slot: String,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
721#[serde(rename_all = "camelCase")]
722pub struct NodeTokenIssueResponse {
723 pub token: String,
724 pub node_id: String,
725 pub expires_in: i64,
726 pub hub_id: String,
727}
728
729#[derive(Debug, Clone, Serialize, Deserialize)]
730#[serde(rename_all = "camelCase")]
731pub struct NodeTokenRevokeRequest {
732 pub node_type: String,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub node_id: Option<String>,
735}
736
737#[derive(Debug, Clone, Serialize, Deserialize)]
738#[serde(rename_all = "camelCase")]
739pub struct NodeTokenRevokeResponse {
740 pub success: bool,
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize)]
744#[serde(rename_all = "camelCase")]
745pub struct NodeTokenListItem {
746 pub node_id: String,
747 pub node_type: String,
748 pub hub_id: String,
749 pub host_id: String,
750 pub fingerprint: String,
751 pub service_instance_id: String,
752 pub instance_policy: NodeInstancePolicy,
753 pub instance_slot: String,
754 pub status: NodeBindingStatus,
755 pub issued_at: i64,
756 pub expires_at: i64,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
760#[serde(rename_all = "camelCase")]
761pub struct NodeTokenListResponse {
762 #[serde(default)]
763 pub nodes: Vec<NodeTokenListItem>,
764}
765
766#[derive(Debug, Clone, Serialize, Deserialize)]
767#[serde(rename_all = "camelCase")]
768pub struct NodeInstanceListItem {
769 pub node_id: String,
770 pub node_type: String,
771 pub hub_id: String,
772 pub host_id: String,
773 #[serde(default, skip_serializing_if = "Option::is_none")]
774 pub host_name: Option<String>,
775 pub fingerprint: String,
776 pub service_instance_id: String,
777 pub instance_policy: NodeInstancePolicy,
778 pub instance_slot: String,
779 pub binding_status: NodeBindingStatus,
780 pub issued_at: i64,
781 pub expires_at: i64,
782 pub connected: bool,
783 pub runtime_status: String,
784}
785
786#[derive(Debug, Clone, Serialize, Deserialize)]
787#[serde(rename_all = "camelCase")]
788pub struct NodeInstanceListResponse {
789 #[serde(default)]
790 pub instances: Vec<NodeInstanceListItem>,
791}
792
793#[derive(Debug, Clone, Serialize, Deserialize)]
794#[serde(rename_all = "camelCase")]
795pub struct AuthenticatedSession {
796 pub tenant_id: String,
797 pub scope_id: String,
798 pub user_id: String,
799 pub is_test: bool,
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
803#[serde(rename_all = "camelCase")]
804pub struct Image {
805 #[serde(skip_serializing_if = "Option::is_none")]
806 pub url: Option<String>,
807 #[serde(skip_serializing_if = "Option::is_none")]
808 pub base64: Option<String>,
809 #[serde(skip_serializing_if = "Option::is_none")]
810 pub mime_type: Option<String>,
811 #[serde(skip_serializing_if = "Option::is_none")]
812 pub created_at: Option<i64>,
813}
814
815impl Image {
816 pub fn from_base64(base64: impl Into<String>, mime_type: impl Into<String>) -> Self {
817 Self {
818 base64: Some(base64.into()),
819 mime_type: Some(mime_type.into()),
820 ..Self::default()
821 }
822 }
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
826#[serde(rename_all = "camelCase")]
827pub struct ChatToolCallFunction {
828 pub name: String,
829 pub arguments: String,
830}
831
832#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
833#[serde(rename_all = "camelCase")]
834pub struct ChatToolCall {
835 pub id: String,
836 #[serde(rename = "type")]
837 pub call_type: String,
838 pub function: ChatToolCallFunction,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize, Default)]
842#[serde(rename_all = "camelCase")]
843pub struct ChatMessage {
844 pub role: String,
845 pub content: String,
846 #[serde(default, skip_serializing_if = "Vec::is_empty")]
847 pub images: Vec<Image>,
848 #[serde(default, skip_serializing_if = "Vec::is_empty")]
849 pub tool_calls: Vec<ChatToolCall>,
850 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub tool_call_id: Option<String>,
852 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub name: Option<String>,
854}
855
856#[derive(Debug, Clone, Serialize, Deserialize, Default)]
857#[serde(rename_all = "camelCase")]
858pub struct LlmGenerationOptions {
859 #[serde(default)]
860 pub temperature: Option<f32>,
861 #[serde(default)]
862 pub max_tokens: Option<u32>,
863}
864
865#[derive(Debug, Clone, Serialize, Deserialize, Default)]
866#[serde(rename_all = "camelCase")]
867pub struct LlmCompleteRequest {
868 #[serde(default)]
869 pub use_case: Option<String>,
870 #[serde(default)]
871 pub mode: Option<String>,
872 pub messages: Vec<ChatMessage>,
873 #[serde(default)]
874 pub tools: Option<serde_json::Value>,
875 #[serde(default)]
876 pub provider: Option<serde_json::Value>,
877 #[serde(default)]
878 pub response_format: Option<serde_json::Value>,
879 #[serde(default)]
880 pub options: LlmGenerationOptions,
881 #[serde(default)]
882 pub temperature: Option<f32>,
883 #[serde(default)]
884 pub max_tokens: Option<u32>,
885}
886
887#[derive(Debug, Clone, Serialize, Deserialize, Default)]
888#[serde(rename_all = "camelCase")]
889pub struct LlmRouteInfo {
890 pub provider: String,
891 pub model: String,
892 #[serde(default)]
893 pub mode: Option<String>,
894}
895
896#[derive(Debug, Clone, Serialize, Deserialize, Default)]
897#[serde(rename_all = "camelCase")]
898pub struct LlmCompleteResponse {
899 pub content: String,
900 #[serde(default)]
901 pub tool_calls: Vec<ProviderToolCall>,
902 #[serde(default)]
903 pub route: Option<LlmRouteInfo>,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
907#[serde(rename_all = "camelCase")]
908pub struct ProviderToolCall {
909 pub id: String,
910 pub name: String,
911 pub arguments_json: String,
912}
913
914#[derive(Debug, Clone, Serialize, Deserialize)]
915pub enum ServiceCoreInput {
916 ClientAuth {
917 message: MwsMessage,
918 ws_id: String,
919 scope_id: String,
920 },
921 ClientRequest {
922 target: String,
923 payload: String,
924 ws_id: String,
925 tenant_id: String,
926 scope_id: String,
927 user_id: String,
928 is_test: bool,
929 #[serde(default, skip_serializing_if = "Option::is_none")]
930 surface_id: Option<String>,
931 },
932 ConversationRequest {
935 target: String,
936 payload: String,
937 tenant_id: String,
938 scope_id: String,
939 actor_user_id: String,
940 surface_id: String,
941 is_test: bool,
942 },
943 NodeAuth {
944 request: NodeAuthRequest,
945 },
946 HubConnectionProof {
947 request: HubConnectionProofRequest,
948 },
949 NodeRequest {
950 target: String,
951 payload: String,
952 tenant_id: String,
953 scope_id: String,
954 node_type: String,
955 instance_id: String,
956 },
957 BackendStatusChanged {
958 tenant_id: String,
959 scope_id: String,
960 target: String,
961 payload: String,
962 },
963 ClientResponse {
964 message: MwsMessage,
965 },
966 NodeResponse {
967 message: MwsMessage,
968 },
969 IssueLocalSessionToken {
970 tenant_id: String,
971 scope_id: String,
972 user_id: String,
973 app_client_id: String,
974 },
975 SetLocalAppClientFocus {
976 ws_id: String,
977 focused: bool,
978 },
979 RefreshLocalAppClient {
980 ws_id: String,
981 },
982}
983
984#[derive(Debug, Clone, Serialize, Deserialize)]
985pub enum ServiceCoreResponse {
986 ClientAuth {
987 session: Option<AuthenticatedSession>,
988 response: MwsMessage,
989 },
990 ClientRequest {
991 response_payload: Option<String>,
992 client_info: MwsClientInfo,
993 },
994 ConversationRequest {
995 response_payload: Option<String>,
996 },
997 NodeAuth(NodeAuthResponse),
998 HubConnectionProof(HubConnectionProofResponse),
999 NodeRequest {
1000 response_payload: Option<String>,
1001 },
1002 LocalSessionToken(String),
1003 Ack {
1004 handled: bool,
1005 },
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub enum ServiceCoreEffect {
1010 Local {
1011 client_id: String,
1012 message: MwsMessage,
1013 },
1014 Bridge {
1015 tenant_id: String,
1016 scope_id: String,
1017 message: MwsMessage,
1018 },
1019 Node {
1020 connection_key: String,
1021 message: MwsMessage,
1022 },
1023 NodeDisconnect {
1024 connection_key: String,
1025 },
1026 Surface {
1027 tenant_id: String,
1028 scope_id: String,
1029 surface_id: String,
1030 target: String,
1031 payload: String,
1032 },
1033}
1034
1035#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1036pub struct ServiceCoreOutput {
1037 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub response: Option<ServiceCoreResponse>,
1039 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1040 pub effects: Vec<ServiceCoreEffect>,
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045pub struct UserSetting {
1046 #[serde(default)]
1047 pub script_mode: bool,
1048 #[serde(default)]
1049 pub debug_mode: bool,
1050 #[serde(default)]
1051 pub eng_account: bool,
1052}
1053
1054impl UserSetting {
1055 pub fn new() -> Self {
1056 Self {
1057 script_mode: false,
1058 debug_mode: false,
1059 eng_account: false,
1060 }
1061 }
1062}
1063
1064impl Default for UserSetting {
1065 fn default() -> Self {
1066 Self::new()
1067 }
1068}
1069
1070#[derive(Debug, Clone, Serialize, Deserialize)]
1071#[serde(rename_all = "camelCase")]
1072pub struct UserInfo {
1073 #[serde(skip_serializing_if = "Option::is_none")]
1074 pub id: Option<String>,
1075 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub name: Option<String>,
1077 #[serde(skip_serializing_if = "Option::is_none")]
1078 pub email: Option<String>,
1079 #[serde(skip_serializing_if = "Option::is_none")]
1080 pub setting: Option<UserSetting>,
1081 #[serde(default)]
1082 pub eng: bool,
1083}
1084
1085impl UserInfo {
1086 pub fn new(id: String) -> Self {
1087 Self {
1088 id: Some(id),
1089 name: None,
1090 email: None,
1091 setting: None,
1092 eng: false,
1093 }
1094 }
1095}
1096
1097#[derive(Debug, Clone, Serialize, Deserialize)]
1098#[serde(rename_all = "camelCase")]
1099pub struct ScopeMember {
1100 pub id: Option<String>,
1101 pub email: Option<String>,
1102 pub name: Option<String>,
1103 pub pending: bool,
1104 pub role: Option<String>,
1105}
1106
1107impl Default for ScopeMember {
1108 fn default() -> Self {
1109 Self {
1110 id: None,
1111 email: None,
1112 name: None,
1113 pending: false,
1114 role: None,
1115 }
1116 }
1117}
1118
1119#[derive(Debug, Clone, Serialize, Deserialize)]
1120#[serde(rename_all = "camelCase")]
1121pub struct ScopeInfo {
1122 pub name: Option<String>,
1123 pub id: Option<String>,
1124 pub pending: bool,
1125 pub members: Option<Vec<ScopeMember>>,
1126 pub execution_env: Option<String>,
1127 pub mode: Option<String>,
1128 pub connection_mode: Option<String>,
1129 pub agent_mode: Option<String>,
1130 pub default_active: bool,
1131 #[serde(default)]
1132 pub is_test: bool,
1133}
1134
1135impl Default for ScopeInfo {
1136 fn default() -> Self {
1137 Self {
1138 name: None,
1139 id: None,
1140 pending: false,
1141 members: None,
1142 execution_env: None,
1143 mode: None,
1144 connection_mode: None,
1145 agent_mode: None,
1146 default_active: false,
1147 is_test: false,
1148 }
1149 }
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct AuthConfig {
1155 pub tenant_id: String,
1156 pub heartbeat_interval: i32,
1157 pub command_timeout: i32,
1158 pub user_info: UserInfo,
1159 pub scope: ScopeInfo,
1160 #[serde(skip_serializing_if = "Option::is_none")]
1161 pub hub_id: Option<String>,
1162 #[serde(skip_serializing_if = "Option::is_none")]
1163 pub jwt_token: Option<String>,
1164}
1165
1166impl AuthConfig {
1167 pub fn new(
1168 tenant_id: String,
1169 heartbeat_interval: i32,
1170 command_timeout: i32,
1171 user_info: UserInfo,
1172 scope: ScopeInfo,
1173 ) -> Self {
1174 Self {
1175 tenant_id,
1176 heartbeat_interval,
1177 command_timeout,
1178 user_info,
1179 scope,
1180 hub_id: None,
1181 jwt_token: None,
1182 }
1183 }
1184}
1185
1186impl Default for AuthConfig {
1187 fn default() -> Self {
1188 Self {
1189 tenant_id: String::new(),
1190 heartbeat_interval: 240,
1191 command_timeout: 10,
1192 user_info: UserInfo::new(String::new()),
1193 scope: ScopeInfo::default(),
1194 hub_id: None,
1195 jwt_token: None,
1196 }
1197 }
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize)]
1201#[serde(rename_all = "camelCase")]
1202pub struct AuthRequest {
1203 pub token: String,
1204 pub source: String,
1205 pub scope_id: String,
1206 pub device_id: String,
1207 pub client_source: String,
1208 #[serde(default)]
1209 pub tenant_id: Option<String>,
1210 #[serde(default)]
1211 pub hub_id: Option<String>,
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize)]
1215#[serde(rename_all = "camelCase")]
1216pub struct HubMdnsInstanceRecord {
1217 pub tenant_id: String,
1218 pub scope_id: String,
1219 pub hub_id: String,
1220 pub scope_name: String,
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::{
1226 hub_connection_proof_signing_payload, HubConnectionProofRequest, MwsMessage,
1227 MwsMessageType, HUB_CONNECTION_PROOF_PROTOCOL,
1228 };
1229
1230 #[test]
1231 fn hub_connection_proof_payload_is_unambiguous_and_nonce_bound() {
1232 let request = HubConnectionProofRequest {
1233 protocol: HUB_CONNECTION_PROOF_PROTOCOL.to_string(),
1234 hub_id: "hub-1".to_string(),
1235 tenant_id: "tenant-1".to_string(),
1236 scope_id: "scope-1".to_string(),
1237 nonce: "nonce-1".to_string(),
1238 };
1239 let payload = hub_connection_proof_signing_payload(&request);
1240 let mut changed = request.clone();
1241 changed.nonce = "nonce-2".to_string();
1242
1243 assert_ne!(payload, hub_connection_proof_signing_payload(&changed));
1244 assert!(payload.starts_with(&(HUB_CONNECTION_PROOF_PROTOCOL.len() as u64).to_be_bytes()));
1245 }
1246
1247 #[test]
1248 fn scoped_server_data_builds_scope_envelope() {
1249 let message = MwsMessage::scoped_server_data(
1250 "scope-1".to_string(),
1251 "/dialog".to_string(),
1252 "{}".to_string(),
1253 None,
1254 );
1255
1256 assert_eq!(message.scope_id.as_deref(), Some("scope-1"));
1257 assert_eq!(message.target.as_deref(), Some("/dialog"));
1258 assert_eq!(message.r#type.as_deref(), Some(MwsMessageType::SERVER_DATA));
1259 assert_eq!(message.payload.as_deref(), Some("{}"));
1260 assert!(message.client_info.is_none());
1261 }
1262}