@ti-engine/core 1.4.1 → 1.6.1

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 CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  This document contains the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
4
4
 
5
+ ## Version 1.6.1
6
+
7
+ * build(deps): update `@dotenvx/dotenvx` from ^1.73.1 to ^1.75.1
8
+ * build(deps): remove `zeromq` as optional dependency
9
+
10
+ ## Version 1.6.0
11
+
12
+ * refactor(exchange)!: replace the blake2 keyed hash with native `node:crypto` HMAC-SHA256 in the message integrity check
13
+ * build(deps)!: remove the `blake2` native addon dependency (no longer builds on Node 26; replaced by the built-in `node:crypto`)
14
+ * fix(exchange): use a constant-time comparison for the message security hash
15
+ * fix(exchange): ship an empty default `securityHashKey` and warn (once) when the security hash is enabled with a missing/default key
16
+ * test(exchange): add the first core test suite — message-hash determinism, tamper detection, and the constant-time comparison
17
+
18
+ > **BREAKING (wire):** the message security hash changes from keyed-BLAKE2b (128 hex) to HMAC-SHA256 (64 hex). Old and new core versions produce mutually incompatible hashes. When upgrading across this version with `securityHashEnabled=true`, redeploy all services together — recommended order: set `securityHashEnabled=false` everywhere → deploy the new core to all services → re-enable. HMAC also accepts keys of any length (custom keys >64 bytes that previously failed under blake2b now work). The default `securityHashKey` is now empty: set `TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY` in any real deployment, otherwise tamper protection is ineffective.
19
+
20
+ ## Version 1.5.1
21
+
22
+ * build(deps): update `@dotenvx/dotenvx` from ^1.66.0 to ^1.73.1
23
+ * build(deps): update `ioredis` from ^5.10.1 to ^5.11.1
24
+
25
+ ## Version 1.5.0
26
+
27
+ * feat(exceptions): add `E_APP_RESOURCE_ALREADY_EXISTS` (5006) for resource-creation conflicts; intended to be raised with HTTP `409`
28
+
29
+ ## Version 1.4.6
30
+
31
+ * build(deps): update `@dotenvx/dotenvx` from ^1.65.0 to ^1.66.0
32
+
33
+ ## Version 1.4.5
34
+
35
+ * feat(cache): add array-based path support to all `*JSON` methods — passing an array of strings as `path` treats each element as a literal key name using bracket notation, preventing misinterpretation of key names containing dots or other JSONPath special characters
36
+
37
+ ## Version 1.4.4
38
+
39
+ * build(deps): update `@dotenvx/dotenvx` from ^1.60.0 to ^1.65.0
40
+
41
+ ## Version 1.4.3
42
+
43
+ * build(deps): update `@dotenvx/dotenvx` from ^1.59.1 to ^1.60.0
44
+
45
+ ## Version 1.4.2
46
+
47
+ * build(deps): update `@dotenvx/dotenvx` from ^1.55.1 to ^1.59.1
48
+ * build(deps): update `ioredis` from ^5.10.0 to ^5.10.1
49
+ * build(deps): update `lodash` from ^4.17.23 to ^4.18.1
50
+
5
51
  ## Version 1.4.1
6
52
 
7
53
  * build(deps): update `@dotenvx/dotenvx` from ^1.54.1 to ^1.55.1
package/README.md CHANGED
@@ -479,12 +479,12 @@ MESSAGE_EXCHANGE_MESSAGE_STORE (Advanced)
479
479
  MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED (Advanced)
480
480
  : JSON path `messageExchange.securityHashEnabled`, type `boolean`, default `true`
481
481
  : ENV variable `TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED`
482
- : This setting controls whether the message exchange will use a control hash mechanism to ensure there is no tampering with the messages in between service calls. In most cases you would want to keep this enabled since it ensures the integrity of your data. If you are concerned about performance (hashing with `blake2` is rapid, but it still eats some milliseconds) you might want to try and disable this to see if it makes any notable difference.
482
+ : This setting controls whether the message exchange will use a control hash mechanism to ensure there is no tampering with the messages in between service calls. In most cases you would want to keep this enabled since it ensures the integrity of your data. If you are concerned about performance (the keyed `HMAC-SHA256` hash is rapid, but it still eats some milliseconds) you might want to try and disable this to see if it makes any notable difference.
483
483
 
484
484
  MESSAGE_EXCHANGE_SECURITY_HASH_KEY (Advanced)
485
- : JSON path `messageExchange.securityHashKey`, type `string`, default `random uuid`
485
+ : JSON path `messageExchange.securityHashKey`, type `string`, default `empty (must be set)`
486
486
  : ENV variable `TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY`
487
- : This setting holds the encryption key used by the message exchange control hash mechanism. By default, this has a random uuid value that can be used for development only. For production environments you absolutely must provide your own encryption key via the ENV variable. Depending on your configuration and infrastructure, it might come from a secure storage, HSM, key vault, etc.
487
+ : This setting holds the HMAC key used by the message exchange control hash mechanism. By default it is empty, which leaves tamper protection ineffective and logs a startup warning. You absolutely must provide your own private key via the ENV variable in any real deployment. Depending on your configuration and infrastructure, it might come from a secure storage, HSM, key vault, etc.
488
488
 
489
489
  MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME
490
490
  : JSON path `messageExchange.traceExpirationTime`, type `number`, default `3600`
package/bin/settings.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "messageQueuePrefix": "ti:messages:",
27
27
  "messageStore": "ti:messages:store",
28
28
  "securityHashEnabled": true,
29
- "securityHashKey": "23e7bdc7-a793-41f9-856e-6760332f0c73",
29
+ "securityHashKey": "",
30
30
  "traceExpirationTime": 3600,
31
31
  "traceLogEnabled": false,
32
32
  "traceRepository": "ti:messages:trace"
@@ -8,12 +8,15 @@
8
8
 
9
9
  const ConnectionObserver = require( "#connection-observer" );
10
10
  const _ = require( "lodash" );
11
- const blake2 = require( "blake2" );
11
+ const crypto = require( "node:crypto" );
12
12
  const exceptions = require( "#exceptions" );
13
13
  const logger = require( "#logger" );
14
14
  const tools = require( "#tools" );
15
15
  const config = require( "#config" );
16
16
 
17
+ const OLD_DEFAULT_HASH_KEY = "23e7bdc7-a793-41f9-856e-6760332f0c73";
18
+ let keyWarningEmitted = false;
19
+
17
20
  /**
18
21
  * An abstract class that defines a basic message handler behavior.
19
22
  * <br/>
@@ -120,10 +123,17 @@ class MessageHandler extends ConnectionObserver {
120
123
  * @public
121
124
  */
122
125
  createMessageHash( message ) {
126
+ let key = config.getSetting( config.setting.MESSAGE_EXCHANGE_SECURITY_HASH_KEY );
127
+ if ( keyWarningEmitted === false ) {
128
+ keyWarningEmitted = true;
129
+ if ( !key || key === OLD_DEFAULT_HASH_KEY ) {
130
+ logger.log( "Message-exchange security hash is enabled but no private key is set ('securityHashKey' is missing or the published default). Set TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY to a private value, otherwise tamper protection is ineffective.", logger.logSeverity.WARNING );
131
+ }
132
+ }
123
133
  let transformed = tools.decomposeJSON( tools.decycle( message ) );
124
- let hash = blake2.createKeyedHash( "blake2b", Buffer.from( config.getSetting( config.setting.MESSAGE_EXCHANGE_SECURITY_HASH_KEY ) ) );
125
- hash.update( Buffer.from( transformed ) );
126
- return hash.digest( "hex" );
134
+ let hmac = crypto.createHmac( "sha256", Buffer.from( key ) );
135
+ hmac.update( Buffer.from( transformed ) );
136
+ return hmac.digest( "hex" );
127
137
  }
128
138
 
129
139
  /**
@@ -10,6 +10,7 @@ const MessageHandler = require( "#message-handler" );
10
10
  const logger = require( "#logger" );
11
11
  const exceptions = require( "#exceptions" );
12
12
  const config = require( "#config" );
13
+ const tools = require( "#tools" );
13
14
 
14
15
  /**
15
16
  * An abstract class that defines a basic message receiver behavior.
@@ -161,7 +162,7 @@ class MessageReceiver extends MessageHandler {
161
162
  let receivedHash = message.hash;
162
163
  delete message.hash;
163
164
  let currentHash = this.createMessageHash( message );
164
- if ( receivedHash && receivedHash === currentHash ) {
165
+ if ( tools.constantTimeEquals( receivedHash, currentHash ) ) {
165
166
  resolve( message );
166
167
  } else {
167
168
  reject( exceptions.raise( exceptions.exceptionCode.E_SEC_MESSAGE_TAMPERING_DETECTED, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/core",
3
- "version": "1.4.1",
3
+ "version": "1.6.1",
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
  "keywords": [
6
6
  "microservices",
@@ -26,6 +26,9 @@
26
26
  "./service-provider": "./components/service-provider.js",
27
27
  "./tools": "./utils/tools.js"
28
28
  },
29
+ "scripts": {
30
+ "test": "node --test"
31
+ },
29
32
  "imports": {
30
33
  "#auditing": "./components/auditing.js",
31
34
  "#cache": "./utils/cache.js",
@@ -56,14 +59,12 @@
56
59
  "#tools": "./utils/tools.js"
57
60
  },
58
61
  "dependencies": {
59
- "@dotenvx/dotenvx": "^1.55.1",
60
- "blake2": "^5.0.1",
61
- "ioredis": "^5.10.0",
62
- "lodash": "^4.17.23",
62
+ "@dotenvx/dotenvx": "^1.75.1",
63
+ "ioredis": "^5.11.1",
64
+ "lodash": "^4.18.1",
63
65
  "node-schedule": "^2.1.1"
64
66
  },
65
67
  "optionalDependencies": {
66
- "zeromq": "^6.5.0"
67
68
  },
68
69
  "files": [
69
70
  "bin/",
package/utils/cache.js CHANGED
@@ -341,12 +341,7 @@ class CommonMemoryCache extends ConnectionObserver {
341
341
  expireValue( key, seconds, name ) {
342
342
  return new Promise( ( resolve, reject ) => {
343
343
  if ( this.#isOperational === true ) {
344
- let commandExpire = [];
345
- if ( name ) {
346
- commandExpire = [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ];
347
- } else {
348
- commandExpire = [ redis.cacheCommands.EXPIRE, key, seconds ];
349
- }
344
+ let commandExpire = ( name ) ? [ redis.cacheCommands.HASH_EXPIRE, name, seconds, "FIELDS", 1, key ] : [ redis.cacheCommands.EXPIRE, key, seconds ];
350
345
  this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
351
346
  resolve( seconds );
352
347
  } ).catch( ( error ) => {
@@ -632,7 +627,7 @@ class CommonMemoryCache extends ConnectionObserver {
632
627
  * @method
633
628
  * @param {string} key
634
629
  * @param {Object} value
635
- * @param {string} [path="$"]
630
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
636
631
  * @param {number} [overrideMode=0] By default this allows full override for existing keys.
637
632
  * Option 1 will set the key only if it doesn't already exist. Option 2 will set it only if it already exists.
638
633
  * @returns {Promise}
@@ -667,7 +662,7 @@ class CommonMemoryCache extends ConnectionObserver {
667
662
  *
668
663
  * @method
669
664
  * @param {string} key
670
- * @param {string} [path="$"]
665
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
671
666
  * @returns {Promise<Object>}
672
667
  * @public
673
668
  */
@@ -698,7 +693,7 @@ class CommonMemoryCache extends ConnectionObserver {
698
693
  * @method
699
694
  * @param {string} key
700
695
  * @param {Object} value
701
- * @param {string} [path="$"]
696
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
702
697
  * @returns {Promise}
703
698
  * @public
704
699
  */
@@ -729,7 +724,7 @@ class CommonMemoryCache extends ConnectionObserver {
729
724
  * @method
730
725
  * @param {string} key
731
726
  * @param {Object} value
732
- * @param {string} [path="$"]
727
+ * @param {string|string[]} [path="$"] A dot-separated JSONPath string, or an array of literal key segments (use the array form when key names may contain dots or other special characters).
733
728
  * @returns {Promise}
734
729
  * @public
735
730
  */
@@ -756,13 +751,19 @@ class CommonMemoryCache extends ConnectionObserver {
756
751
 
757
752
  /**
758
753
  * Used to normalize a JSON path.
754
+ * <br/>
755
+ * NOTE: If "path" is an array, each element is treated as a literal key name and encoded with bracket notation,
756
+ * which correctly handles key names that contain dots or other JSONPath special characters.
759
757
  *
760
758
  * @method
761
- * @param {string} path
759
+ * @param {string|string[]} path
762
760
  * @returns {string}
763
761
  * @private
764
762
  */
765
763
  #normalizeJSONPath( path ) {
764
+ if ( Array.isArray( path ) ) {
765
+ return "$" + path.map( ( segment ) => `["${ String( segment ).replace( /\\/g, "\\\\" ).replace( /"/g, '\\"' ) }"]` ).join( "" );
766
+ }
766
767
  return ( path.startsWith( "$" ) === false ) ? ( "$." + path ) : path;
767
768
  }
768
769
 
package/utils/config.js CHANGED
@@ -1,104 +1,104 @@
1
- /*
2
- * 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.
3
- * Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
4
- * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
- * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
- * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
- */
8
-
9
- const _ = require( "lodash" );
10
- const tools = require( "#tools" );
11
-
12
- /**
13
- * Enum for listing all system settings.
14
- *
15
- * @readonly
16
- * @enum {string} Keys of this ENUM are strings.
17
- * @typedef {string} TiSetting
18
- */
19
- const settingsEnum = tools.enum( {
20
- AUDITING_LOG_CONSOLE_ENABLED: [ "auditing.logConsoleEnabled", "logConsoleEnabled", "" ],
21
- AUDITING_LOG_DETAILS: [ "auditing.logDetails", "logDetails", "" ],
22
- AUDITING_LOG_MIN_LEVEL: [ "auditing.logMinLevel", "logMinLevel", "" ],
23
- AUDITING_LOG_USES_JSON: [ "auditing.logUsesJSON", "logUsesJSON", "" ],
24
- GCLOUD_API_KEY: [ "gcloudIntegration.apiKey", "apiKey", "" ],
25
- GCLOUD_PROJECT_ID: [ "gcloudIntegration.projectID", "projectID", "" ],
26
- LOCALIZATION_LABELS_PATH: [ "localization.labelsPath", "labelsPath", "" ],
27
- LOCALIZATION_LANGUAGE: [ "localization.language", "language", "" ],
28
- MEMORY_CACHE_AUTH_KEY: [ "memoryCache.authKey", "authKey", "" ],
29
- MEMORY_CACHE_REDIS_DB: [ "memoryCache.redisDB", "redisDB", "" ],
30
- MEMORY_CACHE_REDIS_HOST: [ "memoryCache.redisHost", "redisHost", "" ],
31
- MEMORY_CACHE_REDIS_PORT: [ "memoryCache.redisPort", "redisPort", "" ],
32
- MEMORY_CACHE_RETRY_MAX_ATTEMPTS: [ "memoryCache.retryMaxAttempts", "retryMaxAttempts", "" ],
33
- MEMORY_CACHE_RETRY_MAX_INTERVAL: [ "memoryCache.retryMaxInterval", "retryMaxInterval", "" ],
34
- MEMORY_CACHE_USER: [ "memoryCache.user", "user", "" ],
35
- MESSAGE_EXCHANGE_QUEUE_PREFIX: [ "messageExchange.messageQueuePrefix", "messageQueuePrefix", "" ],
36
- MESSAGE_EXCHANGE_MESSAGE_STORE: [ "messageExchange.messageStore", "messageStore", "" ],
37
- MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED: [ "messageExchange.securityHashEnabled", "securityHashEnabled", "" ],
38
- MESSAGE_EXCHANGE_SECURITY_HASH_KEY: [ "messageExchange.securityHashKey", "securityHashKey", "" ],
39
- MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME: [ "messageExchange.traceExpirationTime", "traceExpirationTime", "" ],
40
- MESSAGE_EXCHANGE_TRACE_LOG_ENABLED: [ "messageExchange.traceLogEnabled", "traceLogEnabled", "" ],
41
- MESSAGE_EXCHANGE_TRACE_REPOSITORY: [ "messageExchange.traceRepository", "traceRepository", "" ],
42
- SERVICE_EXECUTION_TIMEOUT: [ "serviceConfig.executionTimeout", "executionTimeout", "" ],
43
- SERVICE_HEALTH_CHECK_ADDRESS: [ "serviceConfig.healthCheckAddress", "healthCheckAddress", "" ],
44
- SERVICE_HEALTH_CHECK_INTERVAL: [ "serviceConfig.healthCheckInterval", "healthCheckInterval", "" ],
45
- SERVICE_HEALTH_CHECK_TIMEOUT: [ "serviceConfig.healthCheckTimeout", "healthCheckTimeout", "" ],
46
- SERVICE_REGISTRY_ADDRESS: [ "serviceConfig.serviceRegistryAddress", "serviceRegistryAddress", "" ],
47
- OPERATION_MODE: [ "operationMode", "operationMode", "" ]
48
- } );
49
- module.exports.setting = settingsEnum;
50
-
51
- /** @type {SettingsMain} */
52
- const settings = require( "#settings" );
53
-
54
- // Override the remaining settings with ENV variables (if provided):
55
- if ( settings.auditing ) {
56
- settings.auditing.logConsoleEnabled = ( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED ) : settings.auditing.logConsoleEnabled;
57
- settings.auditing.logDetails = ( process.env.TI_AUDITING_LOG_DETAILS !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_DETAILS ) : settings.auditing.logDetails;
58
- settings.auditing.logMinLevel = ( process.env.TI_AUDITING_LOG_MIN_LEVEL !== undefined ) ? process.env.TI_AUDITING_LOG_MIN_LEVEL : settings.auditing.logMinLevel;
59
- settings.auditing.logUsesJSON = ( process.env.TI_AUDITING_LOG_USES_JSON !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_USES_JSON ) : settings.auditing.logUsesJSON;
60
- }
61
- if ( settings.localization ) {
62
- settings.localization.labelsPath = ( process.env.TI_LOCALIZATION_LABELS_PATH !== undefined )
63
- ? process.env.TI_LOCALIZATION_LABELS_PATH.split( "," ).map( ( p ) => p.trim() ).filter( Boolean )
64
- : settings.localization.labelsPath;
65
- settings.localization.language = ( process.env.TI_LOCALIZATION_LANGUAGE !== undefined ) ? process.env.TI_LOCALIZATION_LANGUAGE : settings.localization.language;
66
- }
67
- if ( settings.memoryCache ) {
68
- settings.memoryCache.authKey = ( process.env.TI_MEMORY_CACHE_AUTH_KEY !== undefined ) ? process.env.TI_MEMORY_CACHE_AUTH_KEY : settings.memoryCache.authKey;
69
- settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_REDIS_DB ) : settings.memoryCache.redisDB;
70
- settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_REDIS_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_HOST : settings.memoryCache.redisHost;
71
- settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_REDIS_PORT ) : settings.memoryCache.redisPort;
72
- settings.memoryCache.retryMaxAttempts = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS ) : settings.memoryCache.retryMaxAttempts;
73
- settings.memoryCache.retryMaxInterval = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL ) : settings.memoryCache.retryMaxInterval;
74
- settings.memoryCache.user = ( process.env.TI_MEMORY_CACHE_USER !== undefined ) ? process.env.TI_MEMORY_CACHE_USER : settings.memoryCache.user;
75
- }
76
- if ( settings.messageExchange ) {
77
- 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;
78
- settings.messageExchange.securityHashKey = ( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY !== undefined ) ? process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY : settings.messageExchange.securityHashKey;
79
- 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;
80
- }
81
-
82
- // Make sure GCloud is enabled before trying to set it up:
83
- if ( tools.toBool( process.env.TI_GCLOUD_ENABLED ) === true && settings.gcloudIntegration ) {
84
- settings.gcloudIntegration.apiKey = ( process.env.TI_GCLOUD_API_KEY !== undefined ) ? process.env.TI_GCLOUD_API_KEY : settings.gcloudIntegration.apiKey;
85
- settings.gcloudIntegration.projectID = ( process.env.TI_GCLOUD_PROJECT_ID !== undefined ) ? process.env.TI_GCLOUD_PROJECT_ID : settings.gcloudIntegration.projectID;
86
- }
87
-
88
- settings.operationMode = process.env.NODE_ENV || settings.operationMode;
89
-
90
- // Prevent further modifications to the settings object:
91
- tools.deepFreeze( settings );
92
-
93
- /**
94
- * A standard getter method for fetching a setting.
95
- *
96
- * @method
97
- * @param {string|TiSetting} setting Specifies either a dot-separated JSON path of the setting, or is a Setting from the settings enum.
98
- * @param {*} [defaultValue] The default value to be returned if the setting is not found in the current configuration.
99
- * @returns {*}
100
- * @public
101
- */
102
- module.exports.getSetting = ( setting, defaultValue ) => {
103
- return _.get( settings, setting, defaultValue );
1
+ /*
2
+ * 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.
3
+ * Copyright © 2021-2026 Boris Kostadinov <kostadinov.boris@gmail.com>
4
+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
5
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6
+ * You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
7
+ */
8
+
9
+ const _ = require( "lodash" );
10
+ const tools = require( "#tools" );
11
+
12
+ /**
13
+ * Enum for listing all system settings.
14
+ *
15
+ * @readonly
16
+ * @enum {string} Keys of this ENUM are strings.
17
+ * @typedef {string} TiSetting
18
+ */
19
+ const settingsEnum = tools.enum( {
20
+ AUDITING_LOG_CONSOLE_ENABLED: [ "auditing.logConsoleEnabled", "logConsoleEnabled", "" ],
21
+ AUDITING_LOG_DETAILS: [ "auditing.logDetails", "logDetails", "" ],
22
+ AUDITING_LOG_MIN_LEVEL: [ "auditing.logMinLevel", "logMinLevel", "" ],
23
+ AUDITING_LOG_USES_JSON: [ "auditing.logUsesJSON", "logUsesJSON", "" ],
24
+ GCLOUD_API_KEY: [ "gcloudIntegration.apiKey", "apiKey", "" ],
25
+ GCLOUD_PROJECT_ID: [ "gcloudIntegration.projectID", "projectID", "" ],
26
+ LOCALIZATION_LABELS_PATH: [ "localization.labelsPath", "labelsPath", "" ],
27
+ LOCALIZATION_LANGUAGE: [ "localization.language", "language", "" ],
28
+ MEMORY_CACHE_AUTH_KEY: [ "memoryCache.authKey", "authKey", "" ],
29
+ MEMORY_CACHE_REDIS_DB: [ "memoryCache.redisDB", "redisDB", "" ],
30
+ MEMORY_CACHE_REDIS_HOST: [ "memoryCache.redisHost", "redisHost", "" ],
31
+ MEMORY_CACHE_REDIS_PORT: [ "memoryCache.redisPort", "redisPort", "" ],
32
+ MEMORY_CACHE_RETRY_MAX_ATTEMPTS: [ "memoryCache.retryMaxAttempts", "retryMaxAttempts", "" ],
33
+ MEMORY_CACHE_RETRY_MAX_INTERVAL: [ "memoryCache.retryMaxInterval", "retryMaxInterval", "" ],
34
+ MEMORY_CACHE_USER: [ "memoryCache.user", "user", "" ],
35
+ MESSAGE_EXCHANGE_QUEUE_PREFIX: [ "messageExchange.messageQueuePrefix", "messageQueuePrefix", "" ],
36
+ MESSAGE_EXCHANGE_MESSAGE_STORE: [ "messageExchange.messageStore", "messageStore", "" ],
37
+ MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED: [ "messageExchange.securityHashEnabled", "securityHashEnabled", "" ],
38
+ MESSAGE_EXCHANGE_SECURITY_HASH_KEY: [ "messageExchange.securityHashKey", "securityHashKey", "" ],
39
+ MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME: [ "messageExchange.traceExpirationTime", "traceExpirationTime", "" ],
40
+ MESSAGE_EXCHANGE_TRACE_LOG_ENABLED: [ "messageExchange.traceLogEnabled", "traceLogEnabled", "" ],
41
+ MESSAGE_EXCHANGE_TRACE_REPOSITORY: [ "messageExchange.traceRepository", "traceRepository", "" ],
42
+ SERVICE_EXECUTION_TIMEOUT: [ "serviceConfig.executionTimeout", "executionTimeout", "" ],
43
+ SERVICE_HEALTH_CHECK_ADDRESS: [ "serviceConfig.healthCheckAddress", "healthCheckAddress", "" ],
44
+ SERVICE_HEALTH_CHECK_INTERVAL: [ "serviceConfig.healthCheckInterval", "healthCheckInterval", "" ],
45
+ SERVICE_HEALTH_CHECK_TIMEOUT: [ "serviceConfig.healthCheckTimeout", "healthCheckTimeout", "" ],
46
+ SERVICE_REGISTRY_ADDRESS: [ "serviceConfig.serviceRegistryAddress", "serviceRegistryAddress", "" ],
47
+ OPERATION_MODE: [ "operationMode", "operationMode", "" ]
48
+ } );
49
+ module.exports.setting = settingsEnum;
50
+
51
+ /** @type {SettingsMain} */
52
+ const settings = require( "#settings" );
53
+
54
+ // Override the remaining settings with ENV variables (if provided):
55
+ if ( settings.auditing ) {
56
+ settings.auditing.logConsoleEnabled = ( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED ) : settings.auditing.logConsoleEnabled;
57
+ settings.auditing.logDetails = ( process.env.TI_AUDITING_LOG_DETAILS !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_DETAILS ) : settings.auditing.logDetails;
58
+ settings.auditing.logMinLevel = ( process.env.TI_AUDITING_LOG_MIN_LEVEL !== undefined ) ? process.env.TI_AUDITING_LOG_MIN_LEVEL : settings.auditing.logMinLevel;
59
+ settings.auditing.logUsesJSON = ( process.env.TI_AUDITING_LOG_USES_JSON !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_USES_JSON ) : settings.auditing.logUsesJSON;
60
+ }
61
+ if ( settings.localization ) {
62
+ settings.localization.labelsPath = ( process.env.TI_LOCALIZATION_LABELS_PATH !== undefined )
63
+ ? process.env.TI_LOCALIZATION_LABELS_PATH.split( "," ).map( ( p ) => p.trim() ).filter( Boolean )
64
+ : settings.localization.labelsPath;
65
+ settings.localization.language = ( process.env.TI_LOCALIZATION_LANGUAGE !== undefined ) ? process.env.TI_LOCALIZATION_LANGUAGE : settings.localization.language;
66
+ }
67
+ if ( settings.memoryCache ) {
68
+ settings.memoryCache.authKey = ( process.env.TI_MEMORY_CACHE_AUTH_KEY !== undefined ) ? process.env.TI_MEMORY_CACHE_AUTH_KEY : settings.memoryCache.authKey;
69
+ settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_REDIS_DB ) : settings.memoryCache.redisDB;
70
+ settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_REDIS_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_HOST : settings.memoryCache.redisHost;
71
+ settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_REDIS_PORT ) : settings.memoryCache.redisPort;
72
+ settings.memoryCache.retryMaxAttempts = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS ) : settings.memoryCache.retryMaxAttempts;
73
+ settings.memoryCache.retryMaxInterval = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL ) : settings.memoryCache.retryMaxInterval;
74
+ settings.memoryCache.user = ( process.env.TI_MEMORY_CACHE_USER !== undefined ) ? process.env.TI_MEMORY_CACHE_USER : settings.memoryCache.user;
75
+ }
76
+ if ( settings.messageExchange ) {
77
+ 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;
78
+ settings.messageExchange.securityHashKey = ( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY !== undefined ) ? process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY : settings.messageExchange.securityHashKey;
79
+ 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;
80
+ }
81
+
82
+ // Make sure GCloud is enabled before trying to set it up:
83
+ if ( tools.toBool( process.env.TI_GCLOUD_ENABLED ) === true && settings.gcloudIntegration ) {
84
+ settings.gcloudIntegration.apiKey = ( process.env.TI_GCLOUD_API_KEY !== undefined ) ? process.env.TI_GCLOUD_API_KEY : settings.gcloudIntegration.apiKey;
85
+ settings.gcloudIntegration.projectID = ( process.env.TI_GCLOUD_PROJECT_ID !== undefined ) ? process.env.TI_GCLOUD_PROJECT_ID : settings.gcloudIntegration.projectID;
86
+ }
87
+
88
+ settings.operationMode = process.env.NODE_ENV || settings.operationMode;
89
+
90
+ // Prevent further modifications to the settings object:
91
+ tools.deepFreeze( settings );
92
+
93
+ /**
94
+ * A standard getter method for fetching a setting.
95
+ *
96
+ * @method
97
+ * @param {string|TiSetting} setting Specifies either a dot-separated JSON path of the setting, or is a Setting from the settings enum.
98
+ * @param {*} [defaultValue] The default value to be returned if the setting is not found in the current configuration.
99
+ * @returns {*}
100
+ * @public
101
+ */
102
+ module.exports.getSetting = ( setting, defaultValue ) => {
103
+ return _.get( settings, setting, defaultValue );
104
104
  };
@@ -60,7 +60,8 @@ const exceptionCodeEnum = tools.enum( {
60
60
  E_WEB_INVALID_REQUEST_CONTENT_ENCODING: [ 4009, "invalid request content encoding", "The request content encoding is not recognized or not supported." ],
61
61
  /** Application exceptions - codes under 5xxx */
62
62
  E_APP_RESOURCE_NOT_FOUND: [ 5004, "resource not found", "The requested resource cannot be found. See details for more information." ],
63
- E_APP_SERVICE_ERROR: [ 5005, "app service error", "The application service encountered an error. See details for more information." ]
63
+ E_APP_SERVICE_ERROR: [ 5005, "app service error", "The application service encountered an error. See details for more information." ],
64
+ E_APP_RESOURCE_ALREADY_EXISTS: [ 5006, "resource already exists", "The resource cannot be created because it already exists. See details for more information." ]
64
65
  } );
65
66
  module.exports.exceptionCode = exceptionCodeEnum;
66
67
 
package/utils/tools.js CHANGED
@@ -499,6 +499,26 @@ module.exports.decomposeJSON = ( input ) => {
499
499
  return decomposed;
500
500
  };
501
501
 
502
+ /**
503
+ * Constant-time string comparison. Mirrors the web-framework safe-compare idiom: coerces inputs to
504
+ * utf8 buffers, short-circuits on length mismatch, and never throws on hostile/non-string input.
505
+ *
506
+ * @method
507
+ * @param {*} a
508
+ * @param {*} b
509
+ * @returns {boolean} True only when both inputs coerce to equal-length, byte-identical strings.
510
+ * @public
511
+ */
512
+ module.exports.constantTimeEquals = ( a, b ) => {
513
+ try {
514
+ const ba = Buffer.from( String( a || "" ), "utf8" );
515
+ const bb = Buffer.from( String( b || "" ), "utf8" );
516
+ return ( ba.length !== bb.length ) ? false : crypto.timingSafeEqual( ba, bb );
517
+ } catch {
518
+ return false;
519
+ }
520
+ };
521
+
502
522
  /**
503
523
  * Used to create retry policy for the execution of an operation.
504
524
  *