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