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