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