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")]
306 pub control: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub error: Option<ErrorResponse>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub client_info: Option<MwsClientInfo>,
311}
312
313impl MwsMessage {
314 pub fn create(
315 scope_id: Option<String>,
316 target: String,
317 sig: String,
318 payload: String,
319 msg_type: String,
320 ) -> Self {
321 Self {
322 scope_id,
323 target: Some(target),
324 sig: Some(sig),
325 payload: Some(payload),
326 control: None,
327 r#type: Some(msg_type),
328 from: None,
329 error: None,
330 client_info: None,
331 }
332 }
333
334 pub fn dummy() -> Self {
335 Self {
336 scope_id: None,
337 from: None,
338 target: None,
339 sig: None,
340 r#type: None,
341 payload: None,
342 control: None,
343 error: None,
344 client_info: None,
345 }
346 }
347
348 pub fn server_response(
349 target: String,
350 sig: String,
351 payload: String,
352 client_info: MwsClientInfo,
353 ) -> Self {
354 Self {
355 scope_id: None,
356 from: None,
357 target: Some(target),
358 sig: Some(sig),
359 r#type: Some(MwsMessageType::SERVER_RESP.to_string()),
360 payload: Some(payload),
361 control: None,
362 error: None,
363 client_info: Some(client_info),
364 }
365 }
366
367 pub fn server_response_error(
368 target: String,
369 sig: String,
370 error: ErrorResponse,
371 client_info: MwsClientInfo,
372 ) -> Self {
373 Self {
374 scope_id: None,
375 from: None,
376 target: Some(target),
377 sig: Some(sig),
378 r#type: Some(MwsMessageType::SERVER_RESP.to_string()),
379 payload: None,
380 control: None,
381 error: Some(error),
382 client_info: Some(client_info),
383 }
384 }
385
386 pub fn unscoped_server_data(
387 target: String,
388 payload: String,
389 client_info: MwsClientInfo,
390 ) -> Self {
391 Self {
392 scope_id: None,
393 from: None,
394 target: Some(target),
395 sig: None,
396 r#type: Some(MwsMessageType::SERVER_DATA.to_string()),
397 payload: Some(payload),
398 control: None,
399 error: None,
400 client_info: Some(client_info),
401 }
402 }
403
404 pub fn scoped_server_data(
405 scope_id: String,
406 target: String,
407 payload: String,
408 client_info: Option<MwsClientInfo>,
409 ) -> Self {
410 Self {
411 scope_id: Some(scope_id),
412 from: None,
413 target: Some(target),
414 sig: None,
415 r#type: Some(MwsMessageType::SERVER_DATA.to_string()),
416 payload: Some(payload),
417 control: None,
418 error: None,
419 client_info,
420 }
421 }
422
423 pub fn hub_response(
424 target: String,
425 sig: String,
426 payload: String,
427 client_info: MwsClientInfo,
428 ) -> Self {
429 Self {
430 scope_id: None,
431 from: None,
432 target: Some(target),
433 sig: Some(sig),
434 r#type: Some(MwsMessageType::HUB_RESP.to_string()),
435 payload: Some(payload),
436 control: None,
437 error: None,
438 client_info: Some(client_info),
439 }
440 }
441
442 pub fn hub_response_error(
443 target: String,
444 sig: String,
445 error: ErrorResponse,
446 client_info: MwsClientInfo,
447 ) -> Self {
448 Self {
449 scope_id: None,
450 from: None,
451 target: Some(target),
452 sig: Some(sig),
453 r#type: Some(MwsMessageType::HUB_RESP.to_string()),
454 payload: None,
455 control: None,
456 error: Some(error),
457 client_info: Some(client_info),
458 }
459 }
460
461 pub fn hub_request(
462 scope_id: Option<String>,
463 target: String,
464 sig: String,
465 payload: String,
466 ) -> Self {
467 Self::hub_request_with_control(scope_id, target, sig, payload, None)
468 }
469
470 pub fn hub_request_with_control(
471 scope_id: Option<String>,
472 target: String,
473 sig: String,
474 payload: String,
475 control: Option<String>,
476 ) -> Self {
477 Self {
478 scope_id,
479 from: None,
480 target: Some(target),
481 sig: Some(sig),
482 r#type: Some(MwsMessageType::HUB_REQ.to_string()),
483 payload: Some(payload),
484 control,
485 error: None,
486 client_info: None,
487 }
488 }
489
490 pub fn hub_data(
491 scope_id: Option<String>,
492 target: String,
493 sig: String,
494 payload: String,
495 ) -> Self {
496 Self {
497 scope_id,
498 from: None,
499 target: Some(target),
500 sig: Some(sig),
501 r#type: Some(MwsMessageType::HUB_DATA.to_string()),
502 payload: Some(payload),
503 control: None,
504 error: None,
505 client_info: None,
506 }
507 }
508
509 pub fn set_from(&mut self, from: String) {
510 self.from = Some(from);
511 }
512
513 pub fn set_target(&mut self, target: String) {
514 self.target = Some(target);
515 }
516
517 pub fn set_sig(&mut self, sig: String) {
518 self.sig = Some(sig);
519 }
520
521 pub fn set_payload(&mut self, payload: String) {
522 self.payload = Some(payload);
523 }
524
525 pub fn set_type(&mut self, msg_type: String) {
526 self.r#type = Some(msg_type);
527 }
528
529 pub fn set_client_info(&mut self, client_info: MwsClientInfo) {
530 self.client_info = Some(client_info);
531 }
532}
533
534impl Default for MwsMessage {
535 fn default() -> Self {
536 Self::dummy()
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
541#[serde(rename_all = "camelCase")]
542pub struct NodeAuthRequest {
543 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub core_protocol_version: Option<u32>,
546 pub hub_id: String,
547 pub token: String,
548 pub node_type: String,
549 #[serde(default)]
550 pub node_id: String,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
552 pub scope_id: Option<String>,
553 pub host_id: String,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub host_name: Option<String>,
556 pub fingerprint: String,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
560#[serde(rename_all = "camelCase")]
561pub struct NodeAuthResponse {
562 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub core_protocol_version: Option<u32>,
565 pub ok: bool,
566 #[serde(skip_serializing_if = "Option::is_none")]
567 pub session_id: Option<String>,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub hub_id: Option<String>,
570 #[serde(skip_serializing_if = "Option::is_none")]
571 pub tenant_id: Option<String>,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub scope_id: Option<String>,
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub error: Option<String>,
576 #[serde(skip_serializing_if = "Option::is_none")]
577 pub error_code: Option<String>,
578 #[serde(skip_serializing_if = "Option::is_none")]
579 pub recovery_action: Option<String>,
580 #[serde(skip_serializing_if = "Option::is_none")]
581 pub node_id: Option<String>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize)]
585#[serde(rename_all = "camelCase")]
586pub struct NodeTokenIssueRequest {
587 pub transaction_id: String,
588 pub node_type: String,
589 pub candidate_host_id: String,
590 pub candidate_fingerprint: String,
591 pub challenge: NodeChallengeEvidence,
592}
593
594#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
595#[serde(rename_all = "camelCase")]
596pub enum NodeInstancePolicy {
597 Multiple,
598 Singleton,
599 Shared,
600}
601
602impl NodeInstancePolicy {
603 pub fn as_str(self) -> &'static str {
604 match self {
605 Self::Multiple => "multiple",
606 Self::Singleton => "singleton",
607 Self::Shared => "shared",
608 }
609 }
610}
611
612impl Default for NodeInstancePolicy {
613 fn default() -> Self {
614 Self::Multiple
615 }
616}
617
618impl std::fmt::Display for NodeInstancePolicy {
619 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620 f.write_str(self.as_str())
621 }
622}
623
624impl FromStr for NodeInstancePolicy {
625 type Err = String;
626
627 fn from_str(value: &str) -> Result<Self, Self::Err> {
628 match value.trim() {
629 "multiple" | "Multiple" => Ok(Self::Multiple),
630 "singleton" | "Singleton" => Ok(Self::Singleton),
631 "shared" | "Shared" => Ok(Self::Shared),
632 other => Err(format!("unsupported node instance policy: {other}")),
633 }
634 }
635}
636
637#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
638#[serde(rename_all = "camelCase")]
639pub enum NodeBindingStatus {
640 Pending,
641 Active,
642 Revoked,
643 Expired,
644 Failed,
645}
646
647impl NodeBindingStatus {
648 pub fn as_str(self) -> &'static str {
649 match self {
650 Self::Pending => "pending",
651 Self::Active => "active",
652 Self::Revoked => "revoked",
653 Self::Expired => "expired",
654 Self::Failed => "failed",
655 }
656 }
657}
658
659impl Default for NodeBindingStatus {
660 fn default() -> Self {
661 Self::Active
662 }
663}
664
665impl std::fmt::Display for NodeBindingStatus {
666 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667 f.write_str(self.as_str())
668 }
669}
670
671impl FromStr for NodeBindingStatus {
672 type Err = String;
673
674 fn from_str(value: &str) -> Result<Self, Self::Err> {
675 match value.trim() {
676 "pending" | "Pending" => Ok(Self::Pending),
677 "active" | "Active" => Ok(Self::Active),
678 "revoked" | "Revoked" => Ok(Self::Revoked),
679 "expired" | "Expired" => Ok(Self::Expired),
680 "failed" | "Failed" => Ok(Self::Failed),
681 other => Err(format!("unsupported node binding status: {other}")),
682 }
683 }
684}
685
686pub const NODE_ONBOARDING_CHALLENGE_PROTOCOL: &str = "meow.node.onboarding.challenge";
687pub const NODE_ONBOARDING_CHALLENGE_AUDIENCE: &str = "meow-core:node-onboarding";
688pub const NODE_ONBOARDING_CHALLENGE_ALGORITHM: &str = "Ed25519";
689pub const NODE_ONBOARDING_TOKEN_ISSUER_PREFIX: &str = "meow-core:hub:";
690pub const NODE_ONBOARDING_TOKEN_AUDIENCE: &str = "meow-node:onboarding";
691pub const NODE_ONBOARDING_ES256_ALGORITHM: &str = "ES256";
692pub const NODE_ONBOARDING_TRANSACTION_TTL_SECONDS: u64 = 5 * 60;
693pub const NODE_ONBOARDING_START_TARGET: &str = "/identity/node/onboarding/start";
694pub const NODE_TOKEN_ISSUE_TARGET: &str = "/identity/node/issue";
695pub const NODE_TOKEN_REVOKE_TARGET: &str = "/identity/node/revoke";
696pub const NODE_TOKEN_LIST_TARGET: &str = "/identity/node/list";
697pub const HUB_CONNECTION_PROOF_TARGET: &str = "/identity/hub/prove";
698pub const HUB_CONNECTION_PROOF_PROTOCOL: &str = "meow.hub.connection.proof.v1";
699
700#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
701#[serde(rename_all = "camelCase", deny_unknown_fields)]
702pub struct NodeOnboardingStartRequest {
703 pub node_type: String,
704 pub candidate_host_id: String,
705 pub candidate_fingerprint: String,
706 pub idempotency_key: String,
707}
708
709#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
710#[serde(rename_all = "camelCase", deny_unknown_fields)]
711pub struct NodeOnboardingStartResponse {
712 pub transaction_id: String,
713 pub nonce: String,
714 pub expires_at: i64,
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
718#[serde(rename_all = "camelCase")]
719pub struct HubConnectionProofRequest {
720 pub protocol: String,
721 pub hub_id: String,
722 pub tenant_id: String,
723 pub scope_id: String,
724 pub nonce: String,
725}
726
727#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
728#[serde(rename_all = "camelCase")]
729pub struct HubConnectionProofResponse {
730 pub protocol: String,
731 pub hub_id: String,
732 pub tenant_id: String,
733 pub scope_id: String,
734 pub nonce: String,
735 pub key_id: String,
736 pub signature: String,
737}
738
739pub fn hub_connection_proof_signing_payload(request: &HubConnectionProofRequest) -> Vec<u8> {
742 let fields = [
743 request.protocol.as_str(),
744 request.hub_id.as_str(),
745 request.tenant_id.as_str(),
746 request.scope_id.as_str(),
747 request.nonce.as_str(),
748 ];
749 let mut payload = Vec::new();
750 for field in fields {
751 payload.extend_from_slice(&(field.len() as u64).to_be_bytes());
752 payload.extend_from_slice(field.as_bytes());
753 }
754 payload
755}
756
757#[derive(Debug, Clone, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct NodeChallengeEvidence {
760 pub protocol: String,
761 pub algorithm: String,
762 pub payload: String,
763 pub signature: String,
764 pub public_key: String,
765 pub fingerprint: String,
766}
767
768#[derive(Debug, Clone, Serialize, Deserialize)]
769#[serde(rename_all = "camelCase")]
770pub struct NodeChallengePayload {
771 pub protocol: String,
772 pub aud: String,
773 pub nonce: String,
774 pub scope_id: String,
775 pub node_type: String,
776 pub host_id: String,
777 pub fingerprint: String,
778 pub service_instance_id: String,
779 pub instance_policy: NodeInstancePolicy,
780 pub instance_slot: String,
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
784#[serde(rename_all = "camelCase")]
785pub struct NodeTokenIssueResponse {
786 pub token: String,
787 pub node_id: String,
788 pub expires_in: i64,
789 pub hub_id: String,
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
793#[serde(rename_all = "camelCase")]
794pub struct NodeTokenRevokeRequest {
795 pub node_type: String,
796 #[serde(default, skip_serializing_if = "Option::is_none")]
797 pub node_id: Option<String>,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
801#[serde(rename_all = "camelCase")]
802pub struct NodeTokenRevokeResponse {
803 pub success: bool,
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize)]
807#[serde(rename_all = "camelCase")]
808pub struct NodeTokenListItem {
809 pub node_id: String,
810 pub node_type: String,
811 pub hub_id: String,
812 pub host_id: String,
813 pub fingerprint: String,
814 pub service_instance_id: String,
815 pub instance_policy: NodeInstancePolicy,
816 pub instance_slot: String,
817 pub status: NodeBindingStatus,
818 pub issued_at: i64,
819 pub expires_at: i64,
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
823#[serde(rename_all = "camelCase")]
824pub struct NodeTokenListResponse {
825 #[serde(default)]
826 pub nodes: Vec<NodeTokenListItem>,
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize)]
830#[serde(rename_all = "camelCase")]
831pub struct NodeInstanceListItem {
832 pub node_id: String,
833 pub node_type: String,
834 pub hub_id: String,
835 pub host_id: String,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
837 pub host_name: Option<String>,
838 pub fingerprint: String,
839 pub service_instance_id: String,
840 pub instance_policy: NodeInstancePolicy,
841 pub instance_slot: String,
842 pub binding_status: NodeBindingStatus,
843 pub issued_at: i64,
844 pub expires_at: i64,
845 pub connected: bool,
846 pub runtime_status: String,
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851pub struct NodeInstanceListResponse {
852 #[serde(default)]
853 pub instances: Vec<NodeInstanceListItem>,
854}
855
856#[derive(Debug, Clone, Serialize, Deserialize)]
857#[serde(rename_all = "camelCase")]
858pub struct AuthenticatedSession {
859 pub tenant_id: String,
860 pub scope_id: String,
861 pub user_id: String,
862 pub is_test: bool,
863}
864
865#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
866#[serde(rename_all = "snake_case")]
867pub enum ScopeAccessRequirement {
868 Member,
869 Owner,
870}
871
872#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
873#[serde(rename_all = "camelCase", deny_unknown_fields)]
874pub struct ScopeAuthorizationRequest {
875 pub tenant_id: String,
876 pub scope_id: String,
877 pub user_id: String,
878 pub requirement: ScopeAccessRequirement,
879}
880
881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
882#[serde(rename_all = "camelCase", deny_unknown_fields)]
883pub struct ScopeMembershipListRequest {
884 pub tenant_id: String,
885 pub user_id: String,
886}
887
888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
889#[serde(rename_all = "camelCase", deny_unknown_fields)]
890pub struct ScopeMembership {
891 pub tenant_id: String,
892 pub scope_id: String,
893 pub user_id: String,
894}
895
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
900#[serde(rename_all = "camelCase", deny_unknown_fields)]
901pub struct ServiceAppClientProjection {
902 pub tenant_id: String,
903 pub scope_id: String,
904 pub app_client_id: String,
905 #[serde(default, skip_serializing_if = "Option::is_none")]
906 pub user_id: Option<String>,
907 pub source: String,
908 pub device_type: String,
909 #[serde(default)]
910 pub scope_owned: bool,
911}
912
913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914#[serde(rename_all = "camelCase", deny_unknown_fields)]
915pub struct ServiceAppClientProjectionKey {
916 pub tenant_id: String,
917 pub scope_id: String,
918 pub app_client_id: String,
919}
920
921#[derive(Debug, Clone, Serialize, Deserialize)]
922pub enum ServiceCoreInput {
923 ClientAuth {
924 message: MwsMessage,
925 ws_id: String,
926 scope_id: String,
927 },
928 ClientRequest {
929 target: String,
930 payload: String,
931 ws_id: String,
932 tenant_id: String,
933 scope_id: String,
934 user_id: String,
935 is_test: bool,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
937 surface_id: Option<String>,
938 },
939 ConversationRequest {
942 target: String,
943 payload: String,
944 tenant_id: String,
945 scope_id: String,
946 actor_user_id: String,
947 surface_id: String,
948 is_test: bool,
949 },
950 ScopeAuthorization {
953 request: ScopeAuthorizationRequest,
954 },
955 ScopeMembershipList {
957 request: ScopeMembershipListRequest,
958 },
959 AppClientProjectionPut {
961 projection: ServiceAppClientProjection,
962 },
963 AppClientProjectionDelete {
965 key: ServiceAppClientProjectionKey,
966 },
967 NodeAuth {
968 request: NodeAuthRequest,
969 },
970 HubConnectionProof {
971 request: HubConnectionProofRequest,
972 },
973 NodeRequest {
974 target: String,
975 payload: String,
976 #[serde(default, skip_serializing_if = "Option::is_none")]
979 control: Option<String>,
980 tenant_id: String,
981 scope_id: String,
982 node_type: String,
983 node_id: String,
984 },
985 NodeStatusObserved {
989 connection_key: String,
990 status: Box<node::status::StatusPayload>,
991 },
992 ClientResponse {
993 message: MwsMessage,
994 },
995 NodeResponse {
996 message: MwsMessage,
997 },
998 IssueLocalSessionToken {
999 tenant_id: String,
1000 scope_id: String,
1001 user_id: String,
1002 app_client_id: String,
1003 },
1004 SetLocalAppClientFocus {
1005 ws_id: String,
1006 focused: bool,
1007 },
1008 RefreshLocalAppClient {
1009 ws_id: String,
1010 },
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1014pub enum ServiceCoreResponse {
1015 ClientAuth {
1016 session: Option<AuthenticatedSession>,
1017 response: MwsMessage,
1018 },
1019 ClientRequest {
1020 response_payload: Option<String>,
1021 client_info: MwsClientInfo,
1022 },
1023 ConversationRequest {
1024 response_payload: Option<String>,
1025 },
1026 ScopeAuthorization {
1027 authorized: bool,
1028 },
1029 ScopeMembershipList {
1030 memberships: Vec<ScopeMembership>,
1031 },
1032 AppClientProjection {
1033 applied: bool,
1034 },
1035 NodeAuth(NodeAuthResponse),
1036 HubConnectionProof(HubConnectionProofResponse),
1037 NodeRequest {
1038 response_payload: Option<String>,
1039 },
1040 LocalSessionToken(String),
1041 Ack {
1042 handled: bool,
1043 },
1044}
1045
1046#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1047#[serde(deny_unknown_fields)]
1048pub struct ServiceCoreOutput {
1049 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub response: Option<ServiceCoreResponse>,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1054#[serde(rename_all = "camelCase")]
1055pub struct UserSetting {
1056 #[serde(default)]
1057 pub script_mode: bool,
1058 #[serde(default)]
1059 pub debug_mode: bool,
1060 #[serde(default)]
1061 pub eng_account: bool,
1062}
1063
1064impl UserSetting {
1065 pub fn new() -> Self {
1066 Self {
1067 script_mode: false,
1068 debug_mode: false,
1069 eng_account: false,
1070 }
1071 }
1072}
1073
1074impl Default for UserSetting {
1075 fn default() -> Self {
1076 Self::new()
1077 }
1078}
1079
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1081#[serde(rename_all = "camelCase")]
1082pub struct UserInfo {
1083 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub id: Option<String>,
1085 #[serde(skip_serializing_if = "Option::is_none")]
1086 pub name: Option<String>,
1087 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub email: Option<String>,
1089 #[serde(skip_serializing_if = "Option::is_none")]
1090 pub setting: Option<UserSetting>,
1091 #[serde(default)]
1092 pub eng: bool,
1093}
1094
1095impl UserInfo {
1096 pub fn new(id: String) -> Self {
1097 Self {
1098 id: Some(id),
1099 name: None,
1100 email: None,
1101 setting: None,
1102 eng: false,
1103 }
1104 }
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize)]
1108#[serde(rename_all = "camelCase")]
1109pub struct ScopeMember {
1110 pub id: Option<String>,
1111 pub email: Option<String>,
1112 pub name: Option<String>,
1113 pub pending: bool,
1114 pub role: Option<String>,
1115}
1116
1117impl Default for ScopeMember {
1118 fn default() -> Self {
1119 Self {
1120 id: None,
1121 email: None,
1122 name: None,
1123 pending: false,
1124 role: None,
1125 }
1126 }
1127}
1128
1129#[derive(Debug, Clone, Serialize, Deserialize)]
1130#[serde(rename_all = "camelCase")]
1131pub struct ScopeInfo {
1132 pub name: Option<String>,
1133 pub id: Option<String>,
1134 pub pending: bool,
1135 pub members: Option<Vec<ScopeMember>>,
1136 pub execution_env: Option<String>,
1137 pub mode: Option<String>,
1138 pub connection_mode: Option<String>,
1139 pub agent_mode: Option<String>,
1140 pub default_active: bool,
1141 #[serde(default)]
1142 pub is_test: bool,
1143}
1144
1145impl Default for ScopeInfo {
1146 fn default() -> Self {
1147 Self {
1148 name: None,
1149 id: None,
1150 pending: false,
1151 members: None,
1152 execution_env: None,
1153 mode: None,
1154 connection_mode: None,
1155 agent_mode: None,
1156 default_active: false,
1157 is_test: false,
1158 }
1159 }
1160}
1161
1162#[derive(Debug, Clone, Serialize, Deserialize)]
1163#[serde(rename_all = "camelCase")]
1164pub struct AuthConfig {
1165 pub tenant_id: String,
1166 pub command_timeout: i32,
1167 pub user_info: UserInfo,
1168 pub scope: ScopeInfo,
1169 #[serde(skip_serializing_if = "Option::is_none")]
1170 pub hub_id: Option<String>,
1171 #[serde(skip_serializing_if = "Option::is_none")]
1172 pub jwt_token: Option<String>,
1173}
1174
1175impl AuthConfig {
1176 pub fn new(
1177 tenant_id: String,
1178 command_timeout: i32,
1179 user_info: UserInfo,
1180 scope: ScopeInfo,
1181 ) -> Self {
1182 Self {
1183 tenant_id,
1184 command_timeout,
1185 user_info,
1186 scope,
1187 hub_id: None,
1188 jwt_token: None,
1189 }
1190 }
1191}
1192
1193impl Default for AuthConfig {
1194 fn default() -> Self {
1195 Self {
1196 tenant_id: String::new(),
1197 command_timeout: 10,
1198 user_info: UserInfo::new(String::new()),
1199 scope: ScopeInfo::default(),
1200 hub_id: None,
1201 jwt_token: None,
1202 }
1203 }
1204}
1205
1206#[derive(Debug, Clone, Serialize, Deserialize)]
1207#[serde(rename_all = "camelCase")]
1208pub struct AuthRequest {
1209 pub token: String,
1210 pub source: String,
1211 pub scope_id: String,
1212 pub device_id: String,
1213 pub client_source: String,
1214 #[serde(default)]
1215 pub tenant_id: Option<String>,
1216 #[serde(default)]
1217 pub hub_id: Option<String>,
1218}
1219
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1221#[serde(rename_all = "camelCase")]
1222pub struct HubMdnsInstanceRecord {
1223 pub tenant_id: String,
1224 pub scope_id: String,
1225 pub hub_id: String,
1226 pub scope_name: String,
1227}
1228
1229#[cfg(test)]
1230mod tests {
1231 use super::{
1232 hub_connection_proof_signing_payload, HubConnectionProofRequest, MwsMessage,
1233 MwsMessageType, NodeOnboardingStartRequest, HUB_CONNECTION_PROOF_PROTOCOL,
1234 };
1235
1236 #[test]
1237 fn hub_connection_proof_payload_is_unambiguous_and_nonce_bound() {
1238 let request = HubConnectionProofRequest {
1239 protocol: HUB_CONNECTION_PROOF_PROTOCOL.to_string(),
1240 hub_id: "hub-1".to_string(),
1241 tenant_id: "tenant-1".to_string(),
1242 scope_id: "scope-1".to_string(),
1243 nonce: "nonce-1".to_string(),
1244 };
1245 let payload = hub_connection_proof_signing_payload(&request);
1246 let mut changed = request.clone();
1247 changed.nonce = "nonce-2".to_string();
1248
1249 assert_ne!(payload, hub_connection_proof_signing_payload(&changed));
1250 assert!(payload.starts_with(&(HUB_CONNECTION_PROOF_PROTOCOL.len() as u64).to_be_bytes()));
1251 }
1252
1253 #[test]
1254 fn scoped_server_data_builds_scope_envelope() {
1255 let message = MwsMessage::scoped_server_data(
1256 "scope-1".to_string(),
1257 "/dialog".to_string(),
1258 "{}".to_string(),
1259 None,
1260 );
1261
1262 assert_eq!(message.scope_id.as_deref(), Some("scope-1"));
1263 assert_eq!(message.target.as_deref(), Some("/dialog"));
1264 assert_eq!(message.r#type.as_deref(), Some(MwsMessageType::SERVER_DATA));
1265 assert_eq!(message.payload.as_deref(), Some("{}"));
1266 assert!(message.client_info.is_none());
1267 }
1268
1269 #[test]
1270 fn node_onboarding_start_contract_uses_camel_case_and_rejects_unknown_fields() {
1271 let request = NodeOnboardingStartRequest {
1272 node_type: "matter".to_string(),
1273 candidate_host_id: "host-1".to_string(),
1274 candidate_fingerprint: "sha256:abc".to_string(),
1275 idempotency_key: "attempt-1".to_string(),
1276 };
1277
1278 let json = serde_json::to_value(&request).expect("serialize start request");
1279 assert_eq!(json["candidateHostId"], "host-1");
1280 assert_eq!(json["idempotencyKey"], "attempt-1");
1281
1282 let invalid = serde_json::json!({
1283 "nodeType": "matter",
1284 "candidateHostId": "host-1",
1285 "candidateFingerprint": "sha256:abc",
1286 "idempotencyKey": "attempt-1",
1287 "nonce": "caller-must-not-supply-this"
1288 });
1289 assert!(serde_json::from_value::<NodeOnboardingStartRequest>(invalid).is_err());
1290 }
1291}