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