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