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