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