Skip to main content

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 */