product_os_command_control/registry.rs
1//! Node registry module
2//!
3//! Manages the registry of Product OS nodes in the cluster, including
4//! node information, capabilities, services, and features.
5
6use core::str::FromStr;
7use std::prelude::v1::*;
8
9use std::collections::BTreeMap;
10use std::sync::Arc;
11use serde::{ Deserialize, Serialize };
12
13use product_os_capabilities::{Features, ServiceError, Services, What};
14use product_os_security::{AsByteVector, DHKeyStore, RandomGenerator, certificates::Certificates, RandomGeneratorTemplate, RNG, StdRng, SeedableRng};
15use product_os_store::ProductOSKeyValueStore;
16
17use chrono::{DateTime, Utc };
18use parking_lot::Mutex;
19use product_os_request::Uri;
20
21#[derive(Debug, Deserialize, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct Node {
24 id: uuid::Uuid,
25 machine_id: String,
26
27 uri: String,
28 process_id: u32,
29
30 certificate: Vec<u8>,
31
32 capabilities: Vec<String>,
33 services: Services,
34 features: Features,
35
36 failures: u8,
37 created_at: DateTime<Utc>,
38 updated_at: DateTime<Utc>
39}
40
41impl AsByteVector for &Node {
42 fn as_byte_vector(&self) -> Vec<u8> {
43 let mut bytes = vec!();
44
45 bytes.extend_from_slice(self.id.clone().as_bytes());
46 bytes.extend_from_slice(self.machine_id.clone().as_bytes());
47 bytes.extend_from_slice(self.uri.to_string().as_bytes());
48 bytes.extend_from_slice(self.certificate.clone().as_slice());
49 bytes.extend_from_slice(&[self.failures.clone().clone()]);
50 bytes.extend_from_slice(self.created_at.to_string().as_bytes());
51 bytes.extend_from_slice(self.updated_at.to_string().as_bytes());
52 // bytes.extend_from_slice();
53
54 bytes
55 }
56}
57
58
59impl Node {
60 pub fn default(config: &product_os_configuration::Configuration, certificates: Certificates) -> Self {
61 let machine_uid = match machine_uid::get() {
62 Ok(uid) => uid,
63 Err(e) => panic!("Unable to generate machine id: {}", e)
64 };
65
66 Self {
67 id: uuid::Uuid::new_v4(),
68 uri: Uri::from_str(config.url_address().as_str()).unwrap().to_string(),
69 process_id: std::process::id(),
70 machine_id: product_os_security::create_string_hash(machine_uid.as_str()),
71 certificate: certificates.certificates.first().unwrap().to_owned(),
72 capabilities: Vec::new(),
73 services: Services::new(),
74 features: Features::new(),
75 failures: 0,
76 created_at: Utc::now(),
77 updated_at: Utc::now()
78 }
79 }
80
81 pub fn get_identifier(&self) -> String {
82 self.id.to_string()
83 }
84
85 pub fn get_protocol(&self) -> String {
86 let uri = Uri::from_str(self.uri.as_str()).unwrap();
87 match uri.scheme() {
88 None => String::new(),
89 Some(s) => s.to_string()
90 }
91 }
92
93 pub fn get_address(&self) -> Uri {
94 Uri::from_str(self.uri.as_str()).unwrap()
95 }
96
97 pub fn get_process_id(&self) -> u32 {
98 self.process_id
99 }
100
101 pub fn get_certificate(&self) -> Vec<u8> {
102 self.certificate.to_owned()
103 }
104
105 pub fn get_failures(&self) -> u8 {
106 self.failures
107 }
108
109 pub fn get_features(&self) -> &Features {
110 &self.features
111 }
112
113 pub fn get_services(&self) -> &Services {
114 &self.services
115 }
116
117 pub fn match_node(&self, selector: &str, search_value: &str) -> bool {
118 let mut matched = true;
119
120 let search = selector;
121 let value = search_value;
122
123 match search {
124 "feature" => {
125 match self.features.get(value) {
126 Some(_) => (),
127 None => matched = false
128 }
129 },
130 "capability" => {
131 for capability in &self.capabilities {
132 if capability != value { matched = false; }
133 }
134 },
135 "service.kind" => {
136 match self.services.find(value.to_string()) {
137 Some(_) => (),
138 None => matched = false
139 }
140 },
141 "service.enabled" => {
142 for (_, service) in self.services.list() {
143 if service.enabled.to_string() != value { matched = false; }
144 }
145 },
146 "service.active" => {
147 for (_, service) in self.services.list() {
148 if service.active.to_string() != value { matched = false; }
149 }
150 },
151 _ => ()
152 }
153
154 matched
155 }
156
157 pub fn match_node_query(&self, query: &BTreeMap<&str, &str>) -> bool {
158 let mut matched = true;
159
160 for (s, v) in query {
161 let search = s;
162 let value = v;
163
164 if !self.match_node(search, value) { matched = false }
165 }
166
167 matched
168 }
169
170 pub fn get_created_at(&self) -> DateTime<Utc> {
171 self.created_at
172 }
173
174 pub fn get_last_updated_at(&self) -> DateTime<Utc> {
175 self.updated_at
176 }
177}
178
179
180
181
182pub struct Registry {
183 me: Node,
184 nodes: BTreeMap<String, Node>,
185 key_store: DHKeyStore,
186
187 store: Arc<ProductOSKeyValueStore>,
188
189 max_failures: u8
190}
191
192impl Registry {
193 pub fn new(config: &product_os_configuration::Configuration, key_value_store: Arc<ProductOSKeyValueStore>, certificates: Certificates) -> Self {
194 let me = Node::default(config, certificates);
195
196 let registry = Registry {
197 me,
198 nodes: BTreeMap::new(),
199 key_store: DHKeyStore::new(),
200 store: key_value_store,
201 max_failures: config.get_cc_max_failures(),
202 };
203
204 registry
205 }
206
207 pub fn get_max_failures(&self) -> u8 {
208 self.max_failures.to_owned()
209 }
210
211 async fn upsert_me_remote(&mut self) {
212 self.store.group_set(self.me.id.to_string().as_str(), serde_json::to_string(&self.me).unwrap().as_str()).unwrap_or_default()
213 }
214
215 async fn upsert_node_remote(&mut self, node: &Node) {
216 tracing::info!("Upserting node: {:?}", node);
217 self.store.group_set(node.id.to_string().as_str(), serde_json::to_string(node).unwrap().as_str()).unwrap_or_default();
218 }
219
220 async fn remove_node_remote(&mut self, identifier: &str) {
221 self.store.group_remove(identifier).unwrap_or_default()
222 }
223
224 async fn get_node_remote(&mut self, id: &str) -> Option<Node> {
225 match self.store.group_get(id) {
226 Ok(v) => {
227 let mut node: Node = serde_json::from_str(v.as_str()).unwrap();
228 let _ = node.features.setup_router(); // Ignore result - setup_router() may fail but node is still usable
229 Some(node)
230 },
231 Err(_) => None
232 }
233 }
234
235 pub async fn check_me_remote(&mut self) -> Option<&Node> {
236 let id = self.me.id.to_string();
237
238 match self.get_node_remote(id.as_str()).await {
239 Some(node) => {
240 if node.id != self.me.id {
241 // Kill or try to do a revival of node - lost for some reason
242 None
243 }
244 else {
245 Some(&self.me)
246 }
247 },
248 None => { None }
249 }
250 }
251
252 /****** All other features query the local registry first ********/
253
254 pub fn get_me(&self) -> &Node {
255 &self.me
256 }
257
258 pub async fn update_me(&mut self) {
259 self.upsert_me_remote().await;
260 }
261
262 pub fn update_me_status(&mut self, success: bool) -> bool {
263 let failures = if success { 0 } else { self.me.failures.to_owned() + 1 };
264
265 if failures < self.max_failures {
266 self.me.failures = failures;
267 self.me.updated_at = Utc::now();
268
269 true
270 }
271 else {
272 false
273 }
274 }
275
276 pub async fn update_pulse_status(&mut self, id: &str, success: bool) -> bool {
277 match self.get_node_remote(id).await {
278 Some(mut node) => {
279 let failures = if success { 0 } else { node.failures + 1 };
280
281 if failures < self.max_failures {
282 node.failures = failures;
283 node.updated_at = Utc::now();
284
285 self.upsert_node_remote(&node).await;
286 self.nodes.insert(node.id.to_string(), node);
287
288 true
289 }
290 else {
291 tracing::info!("Removing node due to failures count {}: {:?}", failures, node);
292 self.remove_node(node.id.to_string().as_str()).await;
293
294 false
295 }
296 },
297 None => false
298 }
299 }
300
301 pub fn upsert_node_local(&mut self, identifier: String, mut node: Node) {
302 let _ = node.features.setup_router(); // Ignore result - setup_router() may fail but node is still usable
303 self.nodes.insert(identifier, node);
304 }
305
306 pub fn find_nodes(&self, query: BTreeMap<&str, &str>, exclude_me: bool) -> BTreeMap<String, &Node> {
307 let mut result = BTreeMap::new();
308 let me = self.me.id.to_string();
309
310 for (id, node) in &self.nodes {
311 if (!exclude_me || (exclude_me && !me.eq(id))) && node.match_node_query(&query) { result.insert(id.to_string(), node); }
312 }
313
314 result
315 }
316
317 pub fn get_node(&self, id: &str) -> Option<&Node> {
318 self.nodes.get(id)
319 }
320
321 pub fn get_nodes(&self, skip: u8, exclude_me: bool) -> BTreeMap<String, &Node> {
322 let mut nodes = BTreeMap::new();
323 let mut count = 0;
324 let me = self.me.id.to_string();
325
326 for (id, node) in self.nodes.iter() {
327 if (!exclude_me || (exclude_me && !me.eq(id))) && count >= skip { nodes.insert(id.to_string(), node); }
328 count = count + 1;
329 }
330
331 nodes
332 }
333
334 pub fn get_nodes_certificates(&self, skip: u8, exclude_me: bool) -> Vec<Vec<u8>> {
335 let mut certificates = Vec::new();
336 let mut count = 0;
337 let me = self.me.id.to_string();
338
339 for (id, node) in self.nodes.iter() {
340 if (!exclude_me || (exclude_me && !me.eq(id))) && count >= skip { certificates.push(node.get_certificate()) }
341 count = count + 1;
342 }
343
344 certificates
345 }
346
347 pub fn get_nodes_raw_certificates(&self, skip: u8, exclude_me: bool) -> Vec<Vec<u8>> {
348 let mut certificates = Vec::new();
349 let mut count = 0;
350 let me = self.me.id.to_string();
351
352 for (id, node) in self.nodes.iter() {
353 if (!exclude_me || (exclude_me && !me.eq(id))) && count >= skip { certificates.push(node.get_certificate()) }
354 count = count + 1;
355 }
356
357 certificates
358 }
359
360 pub fn get_nodes_endpoints(&self, skip: u8, exclude_me: bool) -> BTreeMap<String, (Uri, Option<Vec<u8>>)> {
361 let mut node_map = BTreeMap::new();
362 let mut count = 0;
363 let me = self.me.id.to_string();
364
365 for (id, node) in self.nodes.iter() {
366 if (!exclude_me || (exclude_me && !me.eq(id))) && count >= skip {
367 let address = node.get_address();
368 let key = self.get_key(id);
369
370 node_map.insert(id.to_string(), (address, key));
371 }
372
373 count = count + 1;
374 }
375
376 node_map
377 }
378
379 pub async fn remove_node(&mut self, identifier: &str) -> Option<Node> {
380 match self.nodes.remove(identifier) {
381 Some(node) => {
382 self.remove_node_remote(node.id.to_string().as_str()).await;
383 Some(node)
384 },
385 None => None
386 }
387 }
388
389 pub fn pick_node(&self, query: BTreeMap<&str, &str>) -> Option<&Node> {
390 let eligible_nodes = Vec::from_iter(self.find_nodes(query, false).into_iter());
391 let select = RandomGenerator::new(Some(RNG::Std(StdRng::from_entropy()))).get_random_usize(0, eligible_nodes.len());
392
393 match eligible_nodes.get(select) {
394 Some(value) => Some(value.1),
395 None => None
396 }
397 }
398
399 pub fn pick_node_for_capability(&self, capability: &str) -> Option<&Node> {
400 let mut query = BTreeMap::new();
401 query.insert("capability", capability);
402 self.pick_node(query)
403 }
404
405 pub async fn add_feature(&mut self, feature: Arc<dyn product_os_capabilities::Feature>, base_path: String, router: &mut product_os_router::ProductOSRouter) {
406 let _ = self.me.features.add(feature, base_path, router).await; // Ignore result
407 self.update_me().await;
408 }
409
410 pub async fn add_feature_mut(&mut self, feature: Arc<Mutex<dyn product_os_capabilities::Feature>>, base_path: String, router: &mut product_os_router::ProductOSRouter) {
411 let _ = self.me.features.add_mut(feature, base_path, router).await; // Ignore result
412 self.update_me().await;
413 }
414
415 pub fn pick_node_for_feature(&self, feature: &str) -> Option<&Node> {
416 let mut query = BTreeMap::new();
417 query.insert("feature", feature);
418 self.pick_node(query)
419 }
420
421 pub async fn remove_feature(&mut self, identifier: &str) {
422 let _ = self.me.features.remove(identifier); // Ignore result
423 self.update_me().await;
424 }
425
426 pub async fn add_service(&mut self, service: Arc<dyn product_os_capabilities::Service>) {
427 self.me.services.add(service).await;
428 self.update_me().await;
429 }
430
431 pub async fn add_service_mut(&mut self, service: Arc<Mutex<dyn product_os_capabilities::Service>>) {
432 let _ = self.me.services.add_mut(service).await; // Ignore result
433 self.update_me().await;
434 }
435
436 pub async fn set_service_active(&mut self, identifier: String, status: bool) {
437 let id = identifier.as_str();
438 match self.me.services.get_mut(id) {
439 None => (),
440 Some(s) => {
441 s.active = status;
442 self.update_me().await;
443 }
444 }
445 }
446
447 pub async fn remove_service(&mut self, identifier: &str) {
448 self.me.services.remove(identifier);
449 self.update_me().await;
450 }
451
452 pub async fn remove_inactive_services(&mut self, query: BTreeMap<&str, &str>) {
453 let mut matches = Vec::new();
454
455 for (identifier, _) in self.find_nodes(query, true) {
456 matches.push(identifier.to_owned());
457 }
458
459 for identifier in &matches {
460 self.remove_service(identifier).await;
461 }
462
463 if matches.len() > 0 { self.update_me().await };
464 }
465
466 pub async fn start_services(&mut self) -> Result<(), ()> {
467 for (_, service) in self.me.services.list_mut() {
468 match service.start().await {
469 Ok(_) => {}
470 Err(_) => return Err(())
471 }
472 }
473
474 Ok(())
475 }
476
477 pub async fn start_service(&mut self, identifier: &str) -> Result<(), ()> {
478 match self.me.services.get_mut(identifier) {
479 None => Err(()),
480 Some(s) => s.start().await
481 }
482 }
483
484 pub async fn stop_service(&mut self, identifier: &str) -> Result<(), ()> {
485 match self.me.services.get_mut(identifier) {
486 None => Err(()),
487 Some(s) => s.stop().await
488 }
489 }
490
491 pub async fn restart_service(&mut self, identifier: &str) -> Result<(), ()> {
492 match self.me.services.get_mut(identifier) {
493 None => Err(()),
494 Some(s) => s.restart().await
495 }
496 }
497
498 pub async fn call_service(&mut self, identifier: &str, action: &What, input: &Option<serde_json::Value>) -> Result<Option<serde_json::Value>, ServiceError> {
499 match self.me.services.get_mut(identifier) {
500 None => Err(ServiceError::GenericError(format!("Service {} not found", identifier))),
501 Some(s) => s.call(action, input).await
502 }
503 }
504
505 pub async fn discover_nodes(&mut self) {
506 let mut nodes = BTreeMap::new();
507
508 match self.store.group_find(None) {
509 Ok(ns) => {
510 nodes = ns;
511 },
512 Err(_) => {
513 tracing::error!("Error getting nodes from store store");
514 }
515 }
516
517 for (id, node) in nodes {
518 match serde_json::from_str(node.as_str()) {
519 Ok(n) => {
520 let mut node: Node = n;
521 let _ = node.features.setup_router(); // Ignore result
522 tracing::trace!("Importing remote node: {:?}", node.id);
523 self.upsert_node_local(node.id.to_string(), node);
524 },
525 Err(e) => {
526 tracing::error!("Error importing remote node {} - purging: {:?}", id, e);
527 self.remove_node_remote(id.as_str()).await;
528 }
529 }
530 }
531 }
532
533 pub fn get_key(&self, identifier: &str) -> Option<Vec<u8>> {
534 match self.key_store.get_key(identifier) {
535 Some(k) => Some(k.to_vec()),
536 None => None
537 }
538 }
539
540 pub fn create_key_session(&mut self) -> (String, [u8; 32]) {
541 self.key_store.create_session()
542 }
543
544 pub fn generate_key(&mut self, session_identifier: &str, remote_public_key: &[u8], association: String, remote_session_identifier: Option<String>) {
545 self.key_store.generate_key(session_identifier, remote_public_key, association, remote_session_identifier);
546 }
547}
548
549
550/*
551{
552 "_id": {
553 "$oid": "6160ed1181d34b02de2054d1"
554 },
555 "capabilities": [
556 "updateUsersStatus",
557 "cleanUpManagers",
558 "queue"
559 ],
560 "machineID": "bd674848a60c3e19cf25764bb5d6b1a124a46c344f9a30573b60e4fbb6615e2d",
561 "key": "Z_m=2Wg(0HD4c-%%Jkqu>,H0<a&3TJ>s",
562 "services": {
563 "authentication": {
564 "identifier": "authentication",
565 "path": "/authentication",
566 "remotes": {
567 "https://172.18.0.9:8421": ",J>mM]z>wnQQ4-nT?a/J4`XpU]Pjy1/d",
568 "https://172.18.0.3:8989": "Yf/bE`*O>3;^HsWj(&_<!v(Kq;HT9?[{",
569 "https://172.18.0.5:8888": "fcVk[[O65<FwRrsJwDnnxD!y`^m]B~S`",
570 "https://172.18.0.4:9102": "@P~3?^}6_}yph{@/L|a+tW7I#2^(O]cJ",
571 "https://172.18.0.2:9410": "}ub%oUYN)9e/<<qL%*|m1`f]c(*t!Rc7",
572 "https://172.18.0.8:8443": ".#z@4(_Q_o(yO#R!aDIR3(z-t[PeRgTN",
573 "https://172.18.0.7:9337": "(1|)v|D?24?pR4,tc*a{bI]ue-UHPw`_"
574 }
575 },
576 "servers": {
577 "identifier": "servers",
578 "path": "/servers",
579 "remotes": {
580 "https://172.18.0.9:8421": ",J>mM]z>wnQQ4-nT?a/J4`XpU]Pjy1/d",
581 "https://172.18.0.3:8989": "Yf/bE`*O>3;^HsWj(&_<!v(Kq;HT9?[{",
582 "https://172.18.0.5:8888": "fcVk[[O65<FwRrsJwDnnxD!y`^m]B~S`",
583 "https://172.18.0.4:9102": "@P~3?^}6_}yph{@/L|a+tW7I#2^(O]cJ",
584 "https://172.18.0.2:9410": "}ub%oUYN)9e/<<qL%*|m1`f]c(*t!Rc7",
585 "https://172.18.0.8:8443": ".#z@4(_Q_o(yO#R!aDIR3(z-t[PeRgTN",
586 "https://172.18.0.7:9337": "(1|)v|D?24?pR4,tc*a{bI]ue-UHPw`_"
587 }
588 }
589 },
590 "features": {
591 "service-cache": {
592 "identifier": "service-cache",
593 "paths": [
594 "/cache*",
595 "/cache/\*"
596 ],
597 "remotes": {
598 "https://172.18.0.9:8421": ",J>mM]z>wnQQ4-nT?a/J4`XpU]Pjy1/d",
599 "https://172.18.0.3:8989": "Yf/bE`*O>3;^HsWj(&_<!v(Kq;HT9?[{",
600 "https://172.18.0.5:8888": "fcVk[[O65<FwRrsJwDnnxD!y`^m]B~S`",
601 "https://172.18.0.4:9102": "@P~3?^}6_}yph{@/L|a+tW7I#2^(O]cJ",
602 "https://172.18.0.2:9410": "}ub%oUYN)9e/<<qL%*|m1`f]c(*t!Rc7",
603 "https://172.18.0.8:8443": ".#z@4(_Q_o(yO#R!aDIR3(z-t[PeRgTN",
604 "https://172.18.0.7:9337": "(1|)v|D?24?pR4,tc*a{bI]ue-UHPw`_"
605 }
606 },
607 "command/endpoint": {
608 "identifier": "command/endpoint",
609 "paths": [
610 "/command/:service/:instruction"
611 ],
612 "remotes": {
613 "https://172.18.0.9:8421": ",J>mM]z>wnQQ4-nT?a/J4`XpU]Pjy1/d",
614 "https://172.18.0.3:8989": "Yf/bE`*O>3;^HsWj(&_<!v(Kq;HT9?[{",
615 "https://172.18.0.5:8888": "fcVk[[O65<FwRrsJwDnnxD!y`^m]B~S`",
616 "https://172.18.0.4:9102": "@P~3?^}6_}yph{@/L|a+tW7I#2^(O]cJ",
617 "https://172.18.0.2:9410": "}ub%oUYN)9e/<<qL%*|m1`f]c(*t!Rc7",
618 "https://172.18.0.8:8443": ".#z@4(_Q_o(yO#R!aDIR3(z-t[PeRgTN",
619 "https://172.18.0.7:9337": "(1|)v|D?24?pR4,tc*a{bI]ue-UHPw`_"
620 }
621 }
622 },
623 "services": [
624 {
625 "_id": {
626 "$oid": "6160ed1481d34b02de205502"
627 },
628 "identifier": "queue0",
629 "key": "r@a{mjm{Q#ccwQhnQC7NB8/i,j^QUUCp",
630 "type": "queue",
631 "active": true,
632 "enabled": true,
633 "updatedAt": {
634 "$date": "2021-10-09T01:15:00.336Z"
635 },
636 "createdAt": {
637 "$date": "2021-10-09T01:15:00.274Z"
638 }
639 }
640 ],
641 "failures": 0,
642 "addressV4": "172.18.0.6",
643 "port": 9751,
644 "processID": "734",
645 "createdAt": {
646 "$date": "2021-10-09T01:14:57.245Z"
647 },
648 "updatedAt": {
649 "$date": "2021-10-09T08:58:00.373Z"
650 },
651 "__v": 0,
652 "remoteFeatures": {
653 "events/execute": {
654 "identifier": "events/execute",
655 "paths": [
656 "/api/events/trigger",
657 "/api/events/notification",
658 "/api/events/process"
659 ]
660 },
661 "flows/execute": {
662 "identifier": "flows/execute",
663 "paths": [
664 "/api/flows/execute"
665 ]
666 },
667 "conditions/execute": {
668 "identifier": "conditions/execute",
669 "paths": [
670 "/api/conditions/execute"
671 ]
672 },
673 "effects/execute": {
674 "identifier": "effects/execute",
675 "paths": [
676 "/api/effect/trigger",
677 "/api/effects/trigger",
678 "/api/effects/process"
679 ]
680 },
681 "authentication": {
682 "identifier": "authentication",
683 "paths": [
684 "/do-login/credentials",
685 "/do-login/token",
686 "/do-logout"
687 ]
688 },
689 "oidc2": {
690 "identifier": "oidc2",
691 "paths": [
692 "/oidc2/authorize",
693 "/oidc2/authorize/decision",
694 "/oidc2/verify",
695 "/oidc2/whoami",
696 "/oidc2/invalidate",
697 "/oidc2/token",
698 "/oidc2/refresh"
699 ]
700 },
701 "intelligent/type": {
702 "identifier": "intelligent/type",
703 "paths": [
704 "/api/intelligent/agent/categories/:name",
705 "/api/intelligent/agent/list/predefined",
706 "/api/intelligent/agent/list/userdefined"
707 ]
708 },
709 "ipreveal": {
710 "identifier": "ipreveal",
711 "paths": [
712 "/api/ipreveal/:experience/:ipaddress",
713 "/api/ipreveal/dataset/update/all"
714 ]
715 },
716 "analyzer": {
717 "identifier": "analyzer",
718 "paths": [
719 "/api/analytics/process"
720 ]
721 },
722 "processor": {
723 "identifier": "processor",
724 "paths": [
725 "/api/process/reaction",
726 "/api/process/signal"
727 ]
728 },
729 "tiny": {
730 "identifier": "tiny",
731 "paths": [
732 "/go/:tinyID"
733 ]
734 },
735 "talk": {
736 "identifier": "talk",
737 "paths": [
738 "/talk/load",
739 "/talk/style",
740 "/talk/:appID",
741 "/talk/:appID/resume"
742 ]
743 },
744 "responses/execute": {
745 "identifier": "responses/execute",
746 "paths": [
747 "/api/response/send/:key/:identifier/:uuid/:channel",
748 "/api/response/receive/:key/:identifier/:uuid"
749 ]
750 },
751 "media/get": {
752 "identifier": "media/get",
753 "paths": [
754 "/media/avatar/bot/:appID/:identifier",
755 "/media/image/:identifier",
756 "/media/file/:identifier",
757 "/media/video/:identifier",
758 "/media/audio/:identifier",
759 "/media/:identifier"
760 ]
761 },
762 "shared": {
763 "identifier": "shared",
764 "paths": [
765 "/shared/product-os/blog/rss",
766 "/shared/tracking/google",
767 "/shared/location/display/map",
768 "/shared/experience/loading",
769 "/shared/product-os/preview",
770 "/shared/product-os/chat",
771 "/shared/live/chat",
772 "/shared/courier/chat",
773 "/shared/product-os/settings",
774 "/shared/product-os/load",
775 "/shared/product-os/launch",
776 "/shared/product-os/journey/load",
777 "/shared/product-os/journey/launch",
778 "/shared/product-os/motion/load",
779 "/shared/product-os/motion/launch",
780 "/shared/avatar/:type",
781 "/shared/try/journey/designer"
782 ]
783 },
784 "assets/special": {
785 "identifier": "assets/special",
786 "paths": [
787 "/js/product.client.bundle.js",
788 "/js/product.designer.bundle.js",
789 "/ui/\*",
790 "/motion/\*",
791 "/.well-known/\*"
792 ]
793 },
794 "assets": {
795 "identifier": "assets",
796 "paths": [
797 "/js/\*",
798 "/css/\*",
799 "/images/\*",
800 "/webfonts/\*",
801 "/assets/\*",
802 "/favicon.ico",
803 "/robots.txt"
804 ]
805 },
806 "pipes/rest": {
807 "identifier": "pipes/rest",
808 "paths": [
809 "/api/plugins/rest/:pipe*"
810 ]
811 },
812 "pipes/stream": {
813 "identifier": "pipes/stream",
814 "paths": [
815 "/api/plugins/stream/:pipe"
816 ]
817 },
818 "pipes/graphql": {
819 "identifier": "pipes/graphql",
820 "paths": [
821 "/api/plugins/graphql/:pipe*"
822 ]
823 },
824 "pipes/ws": {
825 "identifier": "pipes/ws",
826 "paths": [
827 "/api/plugins/ws/:pipe"
828 ]
829 },
830 "pipes/mqtt": {
831 "identifier": "pipes/mqtt",
832 "paths": [
833 "/api/plugins/mqtt/:pipe"
834 ]
835 },
836 "channels/primus": {
837 "identifier": "channels/primus",
838 "paths": [
839 "/channels/product-os/primus*",
840 "/channels/product-os/primus/omega/supreme*",
841 "/channels/live/send",
842 "/channels/live/notify"
843 ]
844 },
845 "content": {
846 "identifier": "content",
847 "paths": [
848 "/error/:code",
849 "/",
850 "/\*"
851 ]
852 },
853 "swagger/api": {
854 "identifier": "swagger/api",
855 "paths": [
856 "/help/api/reference/openapi.json"
857 ]
858 },
859 "swagger/docs": {
860 "identifier": "swagger/docs",
861 "paths": [
862 "/help/api/reference/"
863 ]
864 },
865 "qrcode": {
866 "identifier": "qrcode",
867 "paths": [
868 "/api/qrcode/generate"
869 ]
870 },
871 "connect": {
872 "identifier": "connect",
873 "paths": [
874 "/api/connect/verify/:app",
875 "/api/connect/verify-complete/:app",
876 "/api/connect/authorize",
877 "/api/connect/request/:appID/:key/:action"
878 ]
879 },
880 "embedded": {
881 "identifier": "embedded",
882 "paths": [
883 "/api/embedded/:type/:action"
884 ]
885 },
886 "webhooks": {
887 "identifier": "webhooks",
888 "paths": [
889 "/api/hooks/store/:uuid",
890 "/api/hooks/subscribe",
891 "/api/hooks/unsubscribe/:key",
892 "/api/hooks/external/:key"
893 ]
894 },
895 "zapier": {
896 "identifier": "zapier",
897 "paths": [
898 "/api/hooks/zapier/send/:method/:type/:key",
899 "/api/hooks/zapier/send/:type"
900 ]
901 },
902 "ifttt": {
903 "identifier": "ifttt",
904 "paths": [
905 "/api/ifttt/status",
906 "/api/ifttt/test/setup",
907 "/api/ifttt/user/info",
908 "/api/ifttt/triggers/effect",
909 "/api/ifttt/actions/event",
910 "/api/hooks/ifttt/send/:type/:key"
911 ]
912 },
913 "msflow": {
914 "identifier": "msflow",
915 "paths": [
916 "/api/hooks/msflow/send/:type/:key"
917 ]
918 },
919 "integromat": {
920 "identifier": "integromat",
921 "paths": [
922 "/api/hooks/integromat/send/:type/:key"
923 ]
924 },
925 "notify": {
926 "identifier": "notify",
927 "paths": [
928 "/api/notify/:service/:type",
929 "/api/notifications/process",
930 "/api/notifications/email/process"
931 ]
932 },
933 "channels/telegram": {
934 "identifier": "channels/telegram",
935 "paths": [
936 "/channels/telegram/:uuid",
937 "/channels/telegram"
938 ]
939 },
940 "channels/facebook/messenger": {
941 "identifier": "channels/facebook/messenger",
942 "paths": [
943 "/channels/facebook/messenger"
944 ]
945 },
946 "channels/whatsapp": {
947 "identifier": "channels/whatsapp",
948 "paths": [
949 "/channels/whatsapp/whatsapp",
950 "/channels/whatsapp/connect",
951 "/channels/whatsapp/reconnect",
952 "/channels/whatsapp/disconnect",
953 "/channels/whatsapp/qr-refresh",
954 "/channels/whatsapp/connect-complete"
955 ]
956 },
957 "channels/line": {
958 "identifier": "channels/line",
959 "paths": [
960 "/channels/line/:uuid",
961 "/channels/line"
962 ]
963 },
964 "channels/viber": {
965 "identifier": "channels/viber",
966 "paths": [
967 "/channels/viber/:uuid",
968 "/channels/viber"
969 ]
970 },
971 "channels/twilio": {
972 "identifier": "channels/twilio",
973 "paths": [
974 "/channels/twilio/sms/:uuid",
975 "/channels/twilio/whatsapp/:uuid",
976 "/channels/twilio"
977 ]
978 },
979 "channels/wechat": {
980 "identifier": "channels/wechat",
981 "paths": [
982 "/channels/wechat/:uuid"
983 ]
984 },
985 "channels/slack": {
986 "identifier": "channels/slack",
987 "paths": [
988 "/channels/slack",
989 "/channels/slack/disconnect-all"
990 ]
991 },
992 "channels/microsoft/azurebot": {
993 "identifier": "channels/microsoft/azurebot",
994 "paths": [
995 "/channels/microsoft/azurebot/:uuid"
996 ]
997 },
998 "channels/dialogflow": {
999 "identifier": "channels/dialogflow",
1000 "paths": [
1001 "/channels/google/dialogflow/:uuid",
1002 "/channels/google/dialogflow"
1003 ]
1004 },
1005 "channels/amazon/alexa": {
1006 "identifier": "channels/amazon/alexa",
1007 "paths": [
1008 "/channels/amazon/alexa/:uuid",
1009 "/channels/amazon/alexa"
1010 ]
1011 },
1012 "channels/intercom": {
1013 "identifier": "channels/intercom",
1014 "paths": [
1015 "/channels/intercom"
1016 ]
1017 },
1018 "channels/signal": {
1019 "identifier": "channels/signal",
1020 "paths": [
1021 "/channels/signal/signal",
1022 "/channels/signal/connect",
1023 "/channels/signal/disconnect"
1024 ]
1025 },
1026 "channels/facebook/instagram": {
1027 "identifier": "channels/facebook/instagram",
1028 "paths": [
1029 "/channels/instagram/instagram",
1030 "/channels/instagram/connect",
1031 "/channels/instagram/disconnect"
1032 ]
1033 },
1034 "channels/product-os/rest": {
1035 "identifier": "channels/product-os/rest",
1036 "paths": [
1037 "/channels/product-os/rest/:mode"
1038 ]
1039 },
1040 "channels/product-os/ws": {
1041 "identifier": "channels/product-os/ws",
1042 "paths": [
1043 "/channels/product-os/ws"
1044 ]
1045 }
1046 },
1047 "remoteServices": {
1048 "stories": {
1049 "identifier": "stories",
1050 "path": "/stories"
1051 },
1052 "logging": {
1053 "identifier": "logging",
1054 "path": "/logging"
1055 },
1056 "users": {
1057 "identifier": "users",
1058 "path": "/users"
1059 },
1060 "oidc": {
1061 "identifier": "oidc",
1062 "path": "/oidc"
1063 },
1064 "manage-users": {
1065 "identifier": "manage-users",
1066 "path": "/manage-users"
1067 },
1068 "teams": {
1069 "identifier": "teams",
1070 "path": "/teams"
1071 },
1072 "motions": {
1073 "identifier": "motions",
1074 "path": "/motions"
1075 },
1076 "subscriptions": {
1077 "identifier": "subscriptions",
1078 "path": "/subscriptions"
1079 },
1080 "watchers": {
1081 "identifier": "watchers",
1082 "path": "/watchers"
1083 },
1084 "hooks": {
1085 "identifier": "hooks",
1086 "path": "/hooks"
1087 },
1088 "licences": {
1089 "identifier": "licences",
1090 "path": "/licences"
1091 },
1092 "notifications": {
1093 "identifier": "notifications",
1094 "path": "/notifications"
1095 },
1096 "invites": {
1097 "identifier": "invites",
1098 "path": "/invites"
1099 },
1100 "statistics": {
1101 "identifier": "statistics",
1102 "path": "/statistics"
1103 },
1104 "iplocations": {
1105 "identifier": "iplocations",
1106 "path": "/iplocations"
1107 },
1108 "templates": {
1109 "identifier": "templates",
1110 "path": "/templates"
1111 },
1112 "signals": {
1113 "identifier": "signals",
1114 "path": "/signals"
1115 },
1116 "plans": {
1117 "identifier": "plans",
1118 "path": "/plans"
1119 },
1120 "errors": {
1121 "identifier": "errors",
1122 "path": "/errors"
1123 },
1124 "queues": {
1125 "identifier": "queues",
1126 "path": "/queues"
1127 },
1128 "trainer": {
1129 "identifier": "trainer",
1130 "path": "/trainer"
1131 },
1132 "find-experiences": {
1133 "identifier": "find-experiences",
1134 "path": "/find-experiences"
1135 },
1136 "find-motions": {
1137 "identifier": "find-motions",
1138 "path": "/find-motions"
1139 },
1140 "experiences": {
1141 "identifier": "experiences",
1142 "path": "/experiences"
1143 },
1144 "journeys": {
1145 "identifier": "journeys",
1146 "path": "/journeys"
1147 },
1148 "liveexperiences": {
1149 "identifier": "liveexperiences",
1150 "path": "/liveexperiences"
1151 },
1152 "pages": {
1153 "identifier": "pages",
1154 "path": "/pages"
1155 },
1156 "reactions": {
1157 "identifier": "reactions",
1158 "path": "/reactions"
1159 },
1160 "tinyurls": {
1161 "identifier": "tinyurls",
1162 "path": "/tinyurls"
1163 },
1164 "media": {
1165 "identifier": "media",
1166 "path": "/media"
1167 },
1168 "connections": {
1169 "identifier": "connections",
1170 "path": "/connections"
1171 },
1172 "mappings": {
1173 "identifier": "mappings",
1174 "path": "/mappings"
1175 },
1176 "pipes": {
1177 "identifier": "pipes",
1178 "path": "/pipes"
1179 },
1180 "anatomies": {
1181 "identifier": "anatomies",
1182 "path": "/anatomies"
1183 }
1184 }
1185}
1186 */