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