product_os_command_control/
registry.rs

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