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