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