@ti-engine/core 1.0.10 → 1.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # ti-engine changelog
2
+
3
+ ## Version 1.0.14
4
+ * feat(message exchange): improve the behavior of `onConnectionRecovered` and `onConnectionDisrupted` events in modules `cache`, `message-handler` and `service-executor` to prevent cross-activation or deactivation
5
+ * feat(redis integration): improve Redis integration for greater stability, support of Redis Cloud, and support for Redis 7
6
+ * feat(config): enable setting `auditing.logDetails` by default for all logs
7
+ * docs: update general documentation
8
+
9
+ ## Version 1.0.13
10
+ * build(npm): update npm dependencies to their latest versions
11
+
12
+ ## Version 1.0.12
13
+ * build(npm): update npm dependencies to their latest versions
14
+
15
+ ## Version 1.0.11
16
+ * feat(message exchange): implement message tampering and insertion protection
17
+ * feat(config): provide option to turn on/off the message tampering and insertion protection
18
+ * feat(config): provide option to turn on/off message tracing
19
+ * build(npm): update npm dependencies to their latest versions
20
+ * refactor(config)!: rename all environment variables that set configuration settings to match their related setting
21
+ * docs: add a change log
22
+
23
+ ## Version 1.0.0
24
+ * feat: create initial version of the framework
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # ti-engine core
2
2
  Flexible framework for the creation of microservices with [node.js](https://nodejs.org/).
3
3
 
4
- ## introduction
4
+ ## Introduction
5
5
  The **ti-engine** is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using **node.js**. The architectural concept of the framework is based on a standard _messaging system_ that allows for certain customization but also provides predictability and traceability of its behavior.
6
6
 
7
- Being a messaging system, the **ti-engine** relies on a message broker for the actual exchange of messages between microservice instances. The default implementation of the framework uses [Redis](https://redis.io/) cache, however, you could create your own implementation using something like [Rabbit MQ](https://www.rabbitmq.com/). See the [advanced topics](#advanced-topics) section of this documentation for guides on how to do this.
7
+ Being a messaging system, the **ti-engine** relies on a message broker for the actual exchange of messages between microservice instances. The default implementation of the framework uses [Redis](https://redis.io/) cache, however, you could create your own implementation using something like [Rabbit MQ](https://www.rabbitmq.com/). See the [Advanced topics](#advanced-topics) section of this documentation for guides on how to do this.
8
8
 
9
9
  Please be aware, that this framework is under active development and will expand in the near future. Also, this documentation is still in the process of being created and refined. Make sure to keep an eye on the changes in case you want to use it in the meantime.
10
10
 
11
- ## why ti-engine?
11
+ ## Why ti-engine?
12
12
  The framework is created based on a decade of professional experience with the utilized technologies and architectural approach. It's primary goal is to provide you with a lightweight and flexible solution that can help you build quickly a microservice ecosystem with any degree of size and complexity.
13
13
 
14
14
  This is what you gain by using **ti-engine** in your project:
@@ -21,16 +21,16 @@ This is what you gain by using **ti-engine** in your project:
21
21
 
22
22
  These are just some benefits **ti-engine** offers. Get to know it better to find out more ways in which it can help you improve productivity.
23
23
 
24
- ## prerequisites & installation
24
+ ## Prerequisites & installation
25
25
  In order to run the basic ti-engine framework you will need a couple of things:
26
26
  * A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **14.17.0**
27
27
  * A local or remote [Redis cache installation](https://redis.io/download) with a minimum version of **5.0.14**
28
28
 
29
- If you are working under Windows OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/).
29
+ If you are working under Windows 10+ OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/). You could also use [Redis Cloud](https://app.redislabs.com/) for development purposes as it offers free basic account.
30
30
 
31
31
  To get the framework itself, use the command `npm install @ti-engine/core`. And to include it directly in your package.json dependencies execute `npm install @ti-engine/core --save-prod`.
32
32
 
33
- ## getting started
33
+ ## Getting started
34
34
  To start using the **ti-engine**, you will have to make sure that all prerequisites are available and operational. However, before we get to the fun part you need to also consider a couple of very important things while working with this framework:
35
35
  1. The runtime configuration of the framework can be customized using ENV variables. These can be provided to node.js in all the standard ways, but there is also an option to include an `.env` file.
36
36
  2. It loads your framework-related custom scripts and files dynamically, but it always assumes their provided paths are relative to the _current working directory_ (i.e. it uses `process.cwd()`). Be mindful of that whenever you declare file paths in the various settings.
@@ -61,14 +61,14 @@ At the start of the output log you can see a NOTICE that tells you a couple of i
61
61
  * The instance name - in this case `tester-service`. In the terminology of the framework, this is also known as a _service domain name_.
62
62
  * The _instance identificator_. It is an uuid string with a `ti-` prefix, that is generated by the framework at process start. It can and will be used to trace the messages during their movement through the microservice ecosystem. But more on that later.
63
63
 
64
- Following that come a couple of INFO lines that inform you about the microservice interface state. The framework starts with the process of registration of _business services_ within the service domain of the microservice `tester-service` and successfully adds 2 such services. The necessary information for this is read from a JSON config file included in the package. We'll get into more details on what this all means in the section [creating a microservice](#creating-a-microservice).
64
+ Following that come a couple of INFO lines that inform you about the microservice interface state. The framework starts with the process of registration of _business services_ within the service domain of the microservice `tester-service` and successfully adds 2 such services. The necessary information for this is read from a JSON config file included in the package. We'll get into more details on what this all means in the section [Creating a microservice](#creating-a-microservice).
65
65
 
66
66
  Once the initialization sequence has completed the framework informs you that the microservice instance has started successfully. If the framework encountered an error during initialization instead, you would see something like this:
67
67
  ```text
68
68
  [timestamp]: [instance-id] - notice - Starting new instance of type 'tester-service' with instance ID '[instance-id]'.
69
69
  [timestamp]: [instance-id] - alert - Error detected in the instance startup script!
70
70
  ```
71
- The following 5 lines inform you about the successful connection to Redis. The default configuration assumes that your Redis is running on localhost and uses the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [using the framework](#using-the-framework).
71
+ The following 5 lines inform you about the successful connection to Redis. The default configuration assumes that your Redis is running on localhost and uses the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [Using the framework](#using-the-framework).
72
72
 
73
73
  Finally, you should see a couple of execution statements with their results in JSON format.
74
74
 
@@ -84,24 +84,24 @@ The tester module gets its starting configuration from an `.env` file included i
84
84
  TI_INSTANCE_CLASS=tester-service.js
85
85
  TI_INSTANCE_CONFIG=tester-service.json
86
86
  TI_INSTANCE_NAME=tester-service
87
- TI_LOG_MIN_LEVEL=200
87
+ TI_AUDITING_LOG_MIN_LEVEL=200
88
88
  ```
89
89
  The first variable `TI_INSTANCE_CLASS` is mandatory for every microservice you create with the **ti-engine**. It needs to specify the path to the module that is your microservice. Remember, that this path has to be relative to the working directory in which you plan to execute the `node` command. This is especially important when you're configuring your microservices to work in containers. You can find the full list of available ENV variables and what they do below.
90
90
 
91
91
  Before moving on, also take a good look at the file `bin/start-instance.js`. It should give you an idea on how to the process of starting and stopping a microservice operates. In most cases this file should be sufficient as a starting script for your **ti-engine** based microservice applications. You can, of course, create your own starting script, but then you'll have to consider all necessary steps to properly handle the microservice instance.
92
92
 
93
- ## architecture
93
+ ## Architecture
94
94
  The architectural approach for the **ti-engine** is done in _tiers_ with lower tiers being unaware of the tiers above them. The framework prefers a high level of abstraction in all its tiers and provides many options for customization and extension. While the language is JavaScript, the structuring of the framework follows the OOP principles, and you will find a lot of abstract classes and methods that require you to implement them. These are always marked with the `@abstract` annotation but if you happen to miss one, the framework will raise an exception when you try to use it in your solution.
95
95
 
96
96
  There are three general tiers in the **ti-engine**:
97
- 1. message exchange
98
- 2. service interface
99
- 3. solution implementation
97
+ 1. Message exchange
98
+ 2. Service domains
99
+ 3. Solution implementation
100
100
 
101
101
  See the following sections for more information on each of them.
102
102
 
103
- ### tier 1 - message exchange
104
- This is the lowest framework tier, unless we count the actual data objects processed by the framework. As you already know, the foundational **ti-engine** concept is that of a messaging system. Therefore, the first tier provides an abstraction over a chosen message broker (Redis by default). That abstraction makes it easy to switch between message brokers whenever you want to without having to change anything above tier 1. It also provides several added bonuses that can accelerate your work - message encryption, message tracing, message observers, and others. More details about each of these features will be covered in section [using the framework](#using-the-framework).
103
+ ### Tier 1 - Message exchange
104
+ This is the lowest framework tier, unless we count the actual data objects processed by the framework. As you already know, the foundational **ti-engine** concept is that of a messaging system. Therefore, the first tier provides an abstraction over a chosen message broker (Redis by default). That abstraction makes it easy to switch between message brokers whenever you want to without having to change anything above tier 1. It also provides several added bonuses that can accelerate your work - message encryption, message tracing, message observers, and others. More details about each of these features will be covered in section [Using the framework](#using-the-framework).
105
105
 
106
106
  Another important aspect for you to remember is that the message exchange is entirely _asynchronous_. This helps reduce system load and optimizes the usage of the available resources. Even so each node.js process can handle a limited amount of load. Therefore, you should plan for running multiple identical senders and receives in order to scale your solution. But more on that later.
107
107
 
@@ -115,21 +115,35 @@ After the processing is done the message payload is modified and the receiver se
115
115
 
116
116
  In this scenario the framework utilizes _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. Other message brokers might utilize a slightly different approach, but they should still adhere to the same logical flow.
117
117
 
118
- ### tier 2 - service domains
118
+ The modules associated with this tier are all located in the `components/exchange/` folder. This is a short list of some terminology used here and in the JDoc inside the sourcecode itself:
119
+ * Message - this is the actual data object processed by the framework. It consists of two parts: an envelope containing service information and a payload containing the actual data to be processed.
120
+ * Message sender - a specialized connector that is responsible for sending a message on its way to its destination. It does not handle the actual dispatch and delivery.
121
+ * Message receiver - a specialized connector that is responsible for receiving messages at predefined destination.
122
+ * Message exchange - this is the actual message processing engine. It handles sending and receiving messages via preconfigured message senders and message receivers.
123
+ * Message observer - a custom event listener that can be used to react on message `send` and `received` events.
124
+
125
+ ### Tier 2 - Service domains
119
126
  This tier focuses on hosting and executing the _business logic_ of your application. It's comprised of _business services_ that process input data and return the result of the processing as output data. The business services are grouped in _service domains_, which are in turn hosted inside stateless _microservices_ also named _service instances_. There are two types of service instances in **ti-engine**:
120
127
  * Service consumers - these are service instances, that can call business services in any available service domain.
121
128
  * Service providers - these are service instances, that host and run a set of business services in a particular service domain. Every service provider is also a service consumer.
122
129
 
123
130
  The various service instances in a solution represent a network of interconnected service domains that contain the business logic of your application. All business services exchange data via _service calls_ using abstract _service addresses_. These service calls are transported from one address in the network to another via the underlying message exchange tier. This, however, is completely transparent to the service instances. In essence, tier 2 does not care about the actual data transportation method or protocol. You could in fact change completely the tier 1 approach without having to modify anything in your business logic and business flow.
124
131
 
125
- ### tier 3 - solution implementation
126
- Under development...
132
+ This tier is the place to utilize any databases, file storages, integrations with other applications, scheduling jobs, and so on. In general, it should focus on executing any granular tasks that are essential to backbone operation of your application. The business logic here should remain stateless and any user context should be provided at runtime to each invoked business service. We'll see more concrete examples for that later in section [Using the framework](#using-the-framework).
133
+
134
+ ### Tier 3 - Solution implementation
135
+ This tier comprises the actual implementation of your application. Its structure and behavior depends entirely on your vision and business goals. There are still a couple of points that remain constant:
136
+ * It needs to utilize the business logic defined in tier 2 by calling the business services
137
+ * It needs to take care of any type of stateful behavior like user sessions or transactions
138
+ * It needs to act as the primary interface between users and your application thus handling access management and user interactions
139
+
140
+ Depending on the type of software you are building, tier 3 can be an API Gateway, a Web application, backend for a Mobile application, or anything like that.
127
141
 
128
- ## creating a microservice
142
+ ## Creating a microservice
129
143
  Under development...
130
144
 
131
- ## using the framework
145
+ ## Using the framework
132
146
  Under development...
133
147
 
134
- ## advanced topics
148
+ ## Advanced topics
135
149
  Under development...
package/bin/settings.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "auditing": {
3
3
  "logConsoleEnabled": true,
4
- "logDetails": false,
4
+ "logDetails": true,
5
5
  "logMinLevel": 0,
6
6
  "logUsesJSON": false
7
7
  },
@@ -15,11 +15,14 @@
15
15
  "authKey": null,
16
16
  "redisDB": 0,
17
17
  "redisHost": "127.0.0.1",
18
- "redisPort": 6379
18
+ "redisPort": 6379,
19
+ "user": "default"
19
20
  },
20
21
  "messageExchange": {
21
22
  "messageQueuePrefix": "ti:messages:",
22
23
  "messageStore": "ti:messages:store",
24
+ "securityHashEnabled": true,
25
+ "securityHashKey": "23e7bdc7-a793-41f9-856e-6760332f0c73",
23
26
  "traceLogEnabled": true
24
27
  },
25
28
  "serviceConfig": {
@@ -74,7 +74,7 @@ class DefaultMessageReceiver extends MessageReceiver {
74
74
  onReceive() {
75
75
  return new Promise( ( resolve, reject ) => {
76
76
  this.#memoryCache.receiveMessage( this.receiveQueue ).then( ( lightweightMessage ) => {
77
- return this.#memoryCache.retrieveMessagePayload( lightweightMessage, config.getSetting( config.setting.MESSAGE_EXCHANGE_STORE ) );
77
+ return this.#memoryCache.retrieveMessagePayload( lightweightMessage, config.getSetting( config.setting.MESSAGE_EXCHANGE_MESSAGE_STORE ) );
78
78
  } ).then( ( message ) => {
79
79
  resolve( message );
80
80
  } ).catch( ( error ) => {
@@ -42,7 +42,7 @@ class DefaultMessageSender extends MessageSender {
42
42
  */
43
43
  onSend( message, queue ) {
44
44
  return new Promise( ( resolve, reject ) => {
45
- this.#memoryCache.storeMessagePayload( message.payload, config.getSetting( config.setting.MESSAGE_EXCHANGE_STORE ) ).then( ( storeID ) => {
45
+ this.#memoryCache.storeMessagePayload( message.payload, config.getSetting( config.setting.MESSAGE_EXCHANGE_MESSAGE_STORE ) ).then( ( storeID ) => {
46
46
  let lightweightMessage = _.cloneDeep( message );
47
47
  lightweightMessage.payload = storeID;
48
48
  return this.#memoryCache.sendMessage( lightweightMessage, queue );
@@ -25,8 +25,9 @@ const messageTracer = require( "#message-tracer" );
25
25
  * @property {string} chainID Unique identifier of the message chain if the message is part of one.
26
26
  * @property {number} chainLevel The node level of this message in the message chain tree.
27
27
  * @property {MessageDestination} destination The destination of the message.
28
+ * @property {string} [hash] Security hash for the message if the mechanism is enabled.
28
29
  * @property {string} messageID Unique message identifier.
29
- * @property {Object|string|undefined} payload The message contents to be processed in destination. If string, it is ID of the payload in the memory cache instead.
30
+ * @property {Object|string|undefined} payload The message contents to be processed in destination. If string it is ID of the payload in the memory cache instead.
30
31
  * Note that if this is not an Object or a string, there is no guarantee that it will be delivered in the same/proper format!
31
32
  * @property {MessageSource} source The source of the message.
32
33
  */
@@ -5,8 +5,11 @@
5
5
 
6
6
  const ConnectionObserver = require( "#connection-observer" );
7
7
  const _ = require( "lodash" );
8
+ const blake2 = require( "blake2" );
8
9
  const exceptions = require( "#exceptions" );
9
10
  const logger = require( "#logger" );
11
+ const tools = require( "#tools" );
12
+ const config = require( "#config" );
10
13
 
11
14
  /**
12
15
  * An abstract class that defines a basic message handler behavior.
@@ -98,6 +101,21 @@ class MessageHandler extends ConnectionObserver {
98
101
  return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_METHOD_CALL, { name: this.constructor.name + "." + this.disable.name } ) );
99
102
  }
100
103
 
104
+ /**
105
+ * Used to create a security hash from the message.
106
+ *
107
+ * @method
108
+ * @param {Message} message
109
+ * @returns {string}
110
+ * @public
111
+ */
112
+ createMessageHash( message ) {
113
+ let transformed = tools.decomposeJSON( tools.decycle( message ) );
114
+ let hash = blake2.createKeyedHash( "blake2b", Buffer.from( config.getSetting( config.setting.MESSAGE_EXCHANGE_SECURITY_HASH_KEY ) ) );
115
+ hash.update( Buffer.from( transformed ) );
116
+ return hash.digest( "hex" );
117
+ }
118
+
101
119
  /**
102
120
  * Used to register a new {@link MessageObserver} for events related to the messages passing through this handler.
103
121
  *
@@ -140,7 +158,7 @@ class MessageHandler extends ConnectionObserver {
140
158
  * @private
141
159
  */
142
160
  onConnectionRecovered( identifier ) {
143
- if ( this.#isAvailable === false ) {
161
+ if ( this.#isAvailable === false && identifier === this.#connectionIdentifier ) {
144
162
  this.#isAvailable = true;
145
163
  _.forEach( this.#messageObservers, ( messageObserver ) => {
146
164
  messageObserver.onConnectionRecovered( this.#connectionIdentifier );
@@ -160,7 +178,7 @@ class MessageHandler extends ConnectionObserver {
160
178
  * @private
161
179
  */
162
180
  onConnectionDisrupted( identifier ) {
163
- if ( this.#isAvailable === true ) {
181
+ if ( this.#isAvailable === true && identifier === this.#connectionIdentifier ) {
164
182
  this.#isAvailable = false;
165
183
  _.forEach( this.#messageObservers, ( messageObserver ) => {
166
184
  messageObserver.onConnectionDisrupted( this.#connectionIdentifier );
@@ -27,7 +27,8 @@ class MessageMemoryCache {
27
27
  let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
28
28
  let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
29
29
  let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
30
- this.#redisClient = redis.createRedisClient( identifier, host, port, authKey, db );
30
+ let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
31
+ this.#redisClient = redis.createRedisClient( identifier, host, port, authKey, user, db );
31
32
  }
32
33
 
33
34
  /* Public interface */
@@ -6,6 +6,7 @@
6
6
  const MessageHandler = require( "#message-handler" );
7
7
  const logger = require( "#logger" );
8
8
  const exceptions = require( "#exceptions" );
9
+ const config = require( "#config" );
9
10
 
10
11
  /**
11
12
  * An abstract class that defines a basic message receiver behavior.
@@ -84,6 +85,8 @@ class MessageReceiver extends MessageHandler {
84
85
  */
85
86
  receive() {
86
87
  this.onReceive().then( ( message ) => {
88
+ return this.#postReceive( message );
89
+ } ).then( ( message ) => {
87
90
  this.onMessage( message );
88
91
  } ).catch( ( error ) => {
89
92
  logger.log( `Error while trying to receive the next pending message from memory cache in receiver '${ this.connectionIdentifier }'! Resuming operation...`, logger.logSeverity.ERROR, error );
@@ -108,6 +111,37 @@ class MessageReceiver extends MessageHandler {
108
111
  return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_GEN_ABSTRACT_METHOD_CALL, { name: this.constructor.name + "." + this.onReceive.name } ) );
109
112
  }
110
113
 
114
+ /* Private interface */
115
+
116
+ /**
117
+ * Used to process the received message before providing it to any {@link MessageObserver}.
118
+ *
119
+ * @method
120
+ * @param {Message} message
121
+ * @returns {Promise<Message>}
122
+ * @private
123
+ */
124
+ #postReceive( message ) {
125
+ return new Promise( ( resolve, reject ) => {
126
+ if ( config.getSetting( config.setting.MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED ) === true ) {
127
+ let receivedHash = message.hash;
128
+ delete message.hash;
129
+ let currentHash = this.createMessageHash( message );
130
+ if ( receivedHash && receivedHash === currentHash ) {
131
+ resolve( message );
132
+ } else {
133
+ reject( exceptions.raise( exceptions.exceptionCode.E_SEC_MESSAGE_TAMPERING_DETECTED, {
134
+ messageID: message.messageID,
135
+ receivedHash: receivedHash,
136
+ currentHash: currentHash
137
+ } ) );
138
+ }
139
+ } else {
140
+ resolve( message );
141
+ }
142
+ } );
143
+ }
144
+
111
145
  }
112
146
 
113
147
  module.exports = MessageReceiver;
@@ -5,6 +5,7 @@
5
5
 
6
6
  const MessageHandler = require( "#message-handler" );
7
7
  const exceptions = require( "#exceptions" );
8
+ const config = require( "#config" );
8
9
 
9
10
  /**
10
11
  * An abstract class that defines a basic message sender behavior.
@@ -70,7 +71,7 @@ class MessageSender extends MessageHandler {
70
71
  */
71
72
  send( message, queue ) {
72
73
  return new Promise( ( resolve, reject ) => {
73
- this.#preSend().then( () => {
74
+ this.#preSend( message ).then( ( message ) => {
74
75
  return this.onSend( message, queue );
75
76
  } ).then( () => {
76
77
  return this.#postSend();
@@ -106,12 +107,17 @@ class MessageSender extends MessageHandler {
106
107
  * Used to do pre-send verifications and checks.
107
108
  *
108
109
  * @method
109
- * @returns {Promise}
110
+ * @param {Message} message
111
+ * @returns {Promise<Message>}
110
112
  * @private
111
113
  */
112
- #preSend() {
114
+ #preSend( message ) {
113
115
  if ( this.isAvailable === true ) {
114
- return Promise.resolve();
116
+ if ( config.getSetting( config.setting.MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED ) === true ) {
117
+ message.hash = this.createMessageHash( message );
118
+ }
119
+
120
+ return Promise.resolve( message );
115
121
  } else {
116
122
  return Promise.reject( exceptions.raise( exceptions.exceptionCode.E_COM_MESSAGE_SENDER_UNAVAILABLE ) );
117
123
  }
@@ -102,12 +102,18 @@ class ServiceExecutor extends MessageObserver {
102
102
  onConnectionRecovered( identifier ) {
103
103
  super.onConnectionRecovered( identifier );
104
104
 
105
- let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + ServiceInstance.serviceDomainName;
106
- _.forOwn( this.#serviceInterface, ( versions, serviceAlias ) => {
107
- cache.addToSet( serviceCatalog, serviceAlias ).catch( ( error ) => {
108
- logger.log( `Record for service '${ serviceAlias }' could not be added to the service registry.`, logger.logSeverity.ERROR, error );
109
- } );
110
- } );
105
+ if ( identifier !== cache.connectionIdentifier ) {
106
+ if ( cache.isOperational ) {
107
+ let serviceCatalog = config.getSetting( config.setting.SERVICE_REGISTRY_ADDRESS ) + ServiceInstance.serviceDomainName;
108
+ _.forOwn( this.#serviceInterface, ( versions, serviceAlias ) => {
109
+ cache.addToSet( serviceCatalog, serviceAlias ).catch( ( error ) => {
110
+ logger.log( `Record for service '${ serviceAlias }' could not be added to the service registry.`, logger.logSeverity.ERROR, error );
111
+ } );
112
+ } );
113
+ } else {
114
+ //TODO: Retry service registration after 0.5 seconds.
115
+ }
116
+ }
111
117
  }
112
118
 
113
119
  /**
@@ -269,10 +269,12 @@ class ServiceInstance {
269
269
  * @private
270
270
  */
271
271
  #reportHealthy() {
272
- let timestamp = new Date();
273
- cache.setValue( this.#serviceHealthCheck, timestamp.toISOString(), config.getSetting( config.setting.SERVICE_HEALTH_CHECK_TIMEOUT ) ).catch( ( error ) => {
274
- logger.log( `Error while trying to report for health check from '${ ServiceInstance.instanceID }'!`, logger.logSeverity.WARNING, error );
275
- } );
272
+ if ( cache.isOperational ) {
273
+ let timestamp = new Date();
274
+ cache.setValue( this.#serviceHealthCheck, timestamp.toISOString(), config.getSetting( config.setting.SERVICE_HEALTH_CHECK_TIMEOUT ) ).catch( ( error ) => {
275
+ logger.log( `Error while trying to report for health check from '${ ServiceInstance.instanceID }'!`, logger.logSeverity.WARNING, error );
276
+ } );
277
+ }
276
278
  }
277
279
 
278
280
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  const ConnectionObserver = require( "#connection-observer" );
7
- const ioredis = require( "ioredis" );
7
+ const Redis = require( "ioredis" );
8
8
  const tools = require( "#tools" );
9
9
  const logger = require( "#logger" );
10
10
  const exceptions = require( "#exceptions" );
@@ -49,7 +49,7 @@ module.exports.cacheCommands = cacheCommandsEnum;
49
49
  */
50
50
  class RedisClient {
51
51
 
52
- #clientIdentifier = "default";
52
+ #clientIdentifier;
53
53
  #retryMaxInterval = 1000;
54
54
  #retryMaxAttempts = undefined;
55
55
  #redisClient = undefined;
@@ -61,12 +61,13 @@ class RedisClient {
61
61
  * @param {string} host
62
62
  * @param {number} port
63
63
  * @param {string} authKey
64
+ * @param {string} user
64
65
  * @param {number} defaultDB
65
66
  * @param {boolean} autoRetryUnfulfilled
66
67
  * @param {number} maxRetries
67
68
  */
68
- constructor( identifier, host, port, authKey, defaultDB, autoRetryUnfulfilled, maxRetries ) {
69
- this.#clientIdentifier = identifier || this.#clientIdentifier;
69
+ constructor( identifier, host, port, authKey, user, defaultDB, autoRetryUnfulfilled, maxRetries ) {
70
+ this.#clientIdentifier = identifier || "redis-client-" + tools.getUUID();
70
71
 
71
72
  let retryStrategy = ( attempt ) => {
72
73
  let result = Math.min( attempt * 50, this.#retryMaxInterval );
@@ -87,6 +88,7 @@ class RedisClient {
87
88
  let options = {
88
89
  port: port,
89
90
  host: host,
91
+ username: user,
90
92
  password: authKey,
91
93
  db: defaultDB,
92
94
  autoResendUnfulfilledCommands: autoRetryUnfulfilled,
@@ -96,18 +98,11 @@ class RedisClient {
96
98
  };
97
99
 
98
100
  /** @type Redis */
99
- this.#redisClient = new ioredis( options );
101
+ this.#redisClient = new Redis( options );
100
102
 
101
103
  this.#redisClient.on( "ready", () => {
102
- logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO, {
103
- redis_version: this.#redisClient.serverInfo.redis_version,
104
- redis_mode: this.#redisClient.serverInfo.redis_mode,
105
- os: this.#redisClient.serverInfo.os,
106
- uptime_in_days: this.#redisClient.serverInfo.uptime_in_days,
107
- connected_clients: this.#redisClient.serverInfo.connected_clients,
108
- role: this.#redisClient.serverInfo.role,
109
- connected_slaves: this.#redisClient.serverInfo.connected_slaves
110
- } );
104
+ let serverInfo = this.#redisClient.serverInfo || {};
105
+ logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO, serverInfo );
111
106
 
112
107
  // notify all connection observers about this event:
113
108
  _.forEach( this.#connectionObservers, ( connectionObservers ) => {
@@ -250,12 +245,13 @@ class RedisClient {
250
245
  * @param {string} host
251
246
  * @param {number} [port=6379]
252
247
  * @param {string} [authKey=undefined]
248
+ * @param {string} [user="default"]
253
249
  * @param {number} [defaultDB=0]
254
250
  * @param {boolean} [autoRetryUnfulfilled=true]
255
251
  * @param {number} [maxRetries=20]
256
252
  * @return {RedisClient}
257
253
  * @public
258
254
  */
259
- module.exports.createRedisClient = ( identifier, host, port = 6379, authKey = undefined, defaultDB = 0, autoRetryUnfulfilled = true, maxRetries = 20 ) => {
260
- return Object.freeze( new RedisClient( identifier, host, port, authKey, defaultDB, autoRetryUnfulfilled, maxRetries ) );
255
+ module.exports.createRedisClient = ( identifier, host, port = 6379, authKey = undefined, user = "default", defaultDB = 0, autoRetryUnfulfilled = true, maxRetries = 20 ) => {
256
+ return Object.freeze( new RedisClient( identifier, host, port, authKey, user, defaultDB, autoRetryUnfulfilled, maxRetries ) );
261
257
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/core",
3
- "version": "1.0.10",
3
+ "version": "1.0.14",
4
4
  "description": "The ti-engine is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using node.js.",
5
5
  "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
6
  "license": "ISC",
@@ -41,15 +41,16 @@
41
41
  "#tools": "./utils/tools.js"
42
42
  },
43
43
  "dependencies": {
44
- "blake2": "^4.0.2",
45
- "dotenv": "^10.0.0",
46
- "fs-extra": "^10.0.0",
44
+ "blake2": "^5.0.0",
45
+ "dotenv": "^16.3.1",
46
+ "fs-extra": "^11.1.1",
47
47
  "lodash": "^4.17.21",
48
- "node-schedule": "^2.1.0",
49
- "ioredis": "^4.28.2"
48
+ "node-schedule": "^2.1.1",
49
+ "ioredis": "^5.3.2"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@google-cloud/error-reporting": "^2.0.4"
52
+ "@google-cloud/error-reporting": "^3.0.5",
53
+ "zeromq": "^6.0.0-beta.17"
53
54
  },
54
55
  "repository": {
55
56
  "type": "git",
package/utils/cache.js CHANGED
@@ -23,6 +23,7 @@ class CommonMemoryCache extends ConnectionObserver {
23
23
  static #instance = null;
24
24
  #redisClient = null;
25
25
  #isOperational = false;
26
+ #connectionIdentifier = "system-cache";
26
27
 
27
28
  /**
28
29
  * @constructor
@@ -36,7 +37,8 @@ class CommonMemoryCache extends ConnectionObserver {
36
37
  let port = config.getSetting( config.setting.MEMORY_CACHE_REDIS_PORT );
37
38
  let db = config.getSetting( config.setting.MEMORY_CACHE_REDIS_DB );
38
39
  let authKey = config.getSetting( config.setting.MEMORY_CACHE_AUTH_KEY );
39
- this.#redisClient = redis.createRedisClient( "system", host, port, authKey, db );
40
+ let user = config.getSetting( config.setting.MEMORY_CACHE_USER );
41
+ this.#redisClient = redis.createRedisClient( this.#connectionIdentifier, host, port, authKey, user, db );
40
42
  this.#redisClient.addConnectionObserver( this );
41
43
  CommonMemoryCache.#instance = this;
42
44
  }
@@ -54,6 +56,15 @@ class CommonMemoryCache extends ConnectionObserver {
54
56
  */
55
57
  get isOperational() { return this.#isOperational; }
56
58
 
59
+ /**
60
+ * Property returning the connection identifier of the cache service.
61
+ *
62
+ * @property
63
+ * @returns {string}
64
+ * @public
65
+ */
66
+ get connectionIdentifier() { return this.#connectionIdentifier; }
67
+
57
68
  /**
58
69
  * Needs to be invoked by the connection handler when the connection is disrupted.
59
70
  *
@@ -63,7 +74,9 @@ class CommonMemoryCache extends ConnectionObserver {
63
74
  * @public
64
75
  */
65
76
  onConnectionDisrupted( identifier ) {
66
- this.#isOperational = false;
77
+ if ( identifier === this.#connectionIdentifier ) {
78
+ this.#isOperational = false;
79
+ }
67
80
  }
68
81
 
69
82
  /**
@@ -75,7 +88,9 @@ class CommonMemoryCache extends ConnectionObserver {
75
88
  * @public
76
89
  */
77
90
  onConnectionRecovered( identifier ) {
78
- this.#isOperational = true;
91
+ if ( identifier === this.#connectionIdentifier ) {
92
+ this.#isOperational = true;
93
+ }
79
94
  }
80
95
 
81
96
  /**
package/utils/config.js CHANGED
@@ -20,13 +20,16 @@ const tools = require( "#tools" );
20
20
  * @property {EnvironmentVariable} env.TI_INSTANCE_CONFIG
21
21
  * @property {EnvironmentVariable} env.TI_INSTANCE_ID
22
22
  * @property {EnvironmentVariable} env.TI_INSTANCE_NAME
23
- * @property {EnvironmentVariable} env.TI_LOG_CONSOLE_ENABLED
24
- * @property {EnvironmentVariable} env.TI_LOG_MIN_LEVEL
25
- * @property {EnvironmentVariable} env.TI_LOG_USED_JSON
23
+ * @property {EnvironmentVariable} env.TI_AUDITING_LOG_CONSOLE_ENABLED
24
+ * @property {EnvironmentVariable} env.TI_AUDITING_LOG_MIN_LEVEL
25
+ * @property {EnvironmentVariable} env.TI_AUDITING_LOG_USES_JSON
26
26
  * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_AUTH_KEY
27
- * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_DB
28
- * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_HOST
29
- * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_PORT
27
+ * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_DB
28
+ * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_HOST
29
+ * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_PORT
30
+ * @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED
31
+ * @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY
32
+ * @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED
30
33
  * @property {EnvironmentVariable} env.TI_OPERATION_MODE
31
34
  */
32
35
 
@@ -64,12 +67,15 @@ const tools = require( "#tools" );
64
67
  * @property {number} redisDB
65
68
  * @property {string} redisHost
66
69
  * @property {number} redisPort
70
+ * @property {string} user
67
71
  */
68
72
 
69
73
  /**
70
74
  * @typedef {Object} SettingsMessageExchange
71
75
  * @property {string} messageQueuePrefix
72
76
  * @property {string} messageStore
77
+ * @property {boolean} securityHashEnabled
78
+ * @property {string} securityHashKey
73
79
  * @property {boolean} traceLogEnabled
74
80
  */
75
81
 
@@ -99,8 +105,11 @@ let settingsEnum = tools.enum( {
99
105
  MEMORY_CACHE_REDIS_DB: [ "memoryCache.redisDB", "redisDB", "" ],
100
106
  MEMORY_CACHE_REDIS_HOST: [ "memoryCache.redisHost", "redisHost", "" ],
101
107
  MEMORY_CACHE_REDIS_PORT: [ "memoryCache.redisPort", "redisPort", "" ],
108
+ MEMORY_CACHE_USER: [ "memoryCache.user", "user", "" ],
102
109
  MESSAGE_EXCHANGE_QUEUE_PREFIX: [ "messageExchange.messageQueuePrefix", "messageQueuePrefix", "" ],
103
- MESSAGE_EXCHANGE_STORE: [ "messageExchange.messageStore", "messageStore", "" ],
110
+ MESSAGE_EXCHANGE_MESSAGE_STORE: [ "messageExchange.messageStore", "messageStore", "" ],
111
+ MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED: [ "messageExchange.securityHashEnabled", "securityHashEnabled", "" ],
112
+ MESSAGE_EXCHANGE_SECURITY_HASH_KEY: [ "messageExchange.securityHashKey", "securityHashKey", "" ],
104
113
  MESSAGE_EXCHANGE_TRACE_LOG_ENABLED: [ "messageExchange.traceLogEnabled", "traceLogEnabled", "" ],
105
114
  SERVICE_EXECUTION_TIMEOUT: [ "serviceConfig.executionTimeout", "executionTimeout", "" ],
106
115
  SERVICE_HEALTH_CHECK_ADDRESS: [ "serviceConfig.healthCheckAddress", "healthCheckAddress", "" ],
@@ -120,18 +129,24 @@ const settings = require( "#settings" );
120
129
 
121
130
  // override remaining settings with ENV variables (if provided):
122
131
  if ( settings.auditing ) {
123
- settings.auditing.logMinLevel = ( process.env.TI_LOG_MIN_LEVEL !== undefined ) ? process.env.TI_LOG_MIN_LEVEL : settings.auditing.logMinLevel;
124
- settings.auditing.logConsoleEnabled = ( process.env.TI_LOG_CONSOLE_ENABLED !== undefined ) ? tools.toBool( process.env.TI_LOG_CONSOLE_ENABLED ) : settings.auditing.logConsoleEnabled;
125
- settings.auditing.logUsesJSON = ( process.env.TI_LOG_USED_JSON !== undefined ) ? tools.toBool( process.env.TI_LOG_USED_JSON ) : settings.auditing.logUsesJSON;
132
+ settings.auditing.logMinLevel = ( process.env.TI_AUDITING_LOG_MIN_LEVEL !== undefined ) ? process.env.TI_AUDITING_LOG_MIN_LEVEL : settings.auditing.logMinLevel;
133
+ settings.auditing.logConsoleEnabled = ( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED ) : settings.auditing.logConsoleEnabled;
134
+ settings.auditing.logUsesJSON = ( process.env.TI_AUDITING_LOG_USES_JSON !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_USES_JSON ) : settings.auditing.logUsesJSON;
126
135
  }
127
136
  if ( settings.memoryCache ) {
128
137
  settings.memoryCache.authKey = ( process.env.TI_MEMORY_CACHE_AUTH_KEY !== undefined ) ? process.env.TI_MEMORY_CACHE_AUTH_KEY : settings.memoryCache.authKey;
129
- settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_DB !== undefined ) ? process.env.TI_MEMORY_CACHE_DB : settings.memoryCache.redisDB;
130
- settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_HOST : settings.memoryCache.redisHost;
131
- settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_PORT !== undefined ) ? process.env.TI_MEMORY_CACHE_PORT : settings.memoryCache.redisPort;
138
+ settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_DB : settings.memoryCache.redisDB;
139
+ settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_REDIS_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_HOST : settings.memoryCache.redisHost;
140
+ settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_PORT : settings.memoryCache.redisPort;
141
+ settings.memoryCache.user = ( process.env.MEMORY_CACHE_USER !== undefined ) ? process.env.MEMORY_CACHE_USER : settings.memoryCache.user;
142
+ }
143
+ if ( settings.messageExchange ) {
144
+ settings.messageExchange.securityHashEnabled = ( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED !== undefined ) ? tools.toBool( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED ) : settings.messageExchange.securityHashEnabled;
145
+ settings.messageExchange.securityHashKey = ( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY !== undefined ) ? process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY : settings.messageExchange.securityHashKey;
146
+ settings.messageExchange.traceLogEnabled = ( process.env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED !== undefined ) ? tools.toBool( process.env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED ) : settings.messageExchange.traceLogEnabled;
132
147
  }
133
148
 
134
- // make sure GCloud is enabled before trying to setup it:
149
+ // make sure GCloud is enabled before trying to set it up:
135
150
  if ( process.env.TI_GCLOUD_ENABLED === true && settings.gcloudIntegration ) {
136
151
  settings.gcloudIntegration.apiKey = ( process.env.TI_GCLOUD_API_KEY !== undefined ) ? process.env.TI_GCLOUD_API_KEY : settings.gcloudIntegration.apiKey;
137
152
  settings.gcloudIntegration.projectID = ( process.env.TI_GCLOUD_PROJECT_ID !== undefined ) ? process.env.TI_GCLOUD_PROJECT_ID : settings.gcloudIntegration.projectID;
@@ -25,6 +25,7 @@ let exceptionCodeEnum = tools.enum( {
25
25
  E_SEC_INVALID_AUTH_TOKEN: [ 2000, "invalid auth token", "Invalid authorization token provided." ],
26
26
  E_SEC_INVALID_EXPIRED_SESSION: [ 2001, "invalid or expired session", "Invalid or expired session encountered." ],
27
27
  E_SEC_UNAUTHORIZED_ACCESS: [ 2002, "unauthorized access", "Attempt for unauthorized access detected." ],
28
+ E_SEC_MESSAGE_TAMPERING_DETECTED: [ 2003, "message tampering detected", "The system detected tampering with the message received via message exchange." ],
28
29
  /** Cross-Application Communication exceptions - codes under 3xxx */
29
30
  E_COM_GENERAL_ERROR: [ 3000, "general communication error", "General error during cross-application communication." ],
30
31
  E_COM_MESSAGE_SENDER_UNAVAILABLE: [ 3001, "message sender unavailable", "The message sender instance is currently unavailable." ],