Socket
Socket
Sign inDemoInstall

@opuscapita/config

Package Overview
Dependencies
15
Maintainers
25
Versions
48
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 3.0.2 to 3.0.3

lib/ConfigClient.js

402

index.js

@@ -1,401 +0,3 @@

const consul = require('consul');
const crypto = require('crypto');
const Promise = require('bluebird');
const fs = require('fs');
const retry = require('bluebird-retry');
const Cache = require('ocbesbn-cache');
const extend = require('extend');
const Logger = require('ocbesbn-logger');
const helper = require('./helper.js');
const cwd = process.cwd();
const { ConfigClient } = require('./lib');
const cachedInstances = { };
/**
* Object providing endpoint configuration.
* @typedef {object} Endpoint
* @property {string} host Hostname or IP address of an endpoint.
* @property {number} port Port of an endpoint.
*/
/**
* Gets the name of the current service this module is running in. It simply takes the last directory
* name of the main processes path.
*/
module.exports.serviceName = cwd.slice(cwd.lastIndexOf('/') + 1);
/**
* Defines a default configuration with all possible configuration options.
*
* @prop {string} host - Hostname of a consul service registry.
* @prop {number} port - TCP port of a consul service registry.
* @prop {number} retryCount - Amount of retries for connecting to consul (retryCount * retryTimeout).
* @prop {number} retryTimeout - Time in milliseconds between retries for connecting to consul (retryCount * retryTimeout).
* @prop {string} serviceSecretPath - Path to a file with consul encryption secret (defaults to /run/secrets/<serviceName>-consul-key)
*/
module.exports.DefaultConfig = {
host: 'consul',
port: 8500,
retryCount : 50,
retryTimeout : 1000,
serviceSecretPath : `/run/secrets/${module.exports.serviceName}-consul-key`
}
module.exports.init = function(config)
{
config = extend(true, { }, this.DefaultConfig, config);
logger = new Logger({ context : { serviceName : this.serviceName } });
const cacheKey = crypto.createHash('md5').update(JSON.stringify(config)).digest("hex");
if(cachedInstances[cacheKey])
{
return Promise.resolve(cachedInstances[cacheKey]);
}
else
{
return Promise.resolve(initInternal.call(this, extend(true, { }, config), logger).then(result => cachedInstances[cacheKey] = result)
.catch(e =>
{
logger.error(e.message);
throw e;
}));
}
}
async function initInternal(config, logger)
{
this.config = config;
this.logger = logger;
this.cache = new Cache({ defaultExpire : 30 });
logger.info('Reading encryption key: %s', config.serviceSecretPath);
try
{
this.secret = fs.readFileSync(config.serviceSecretPath, 'utf8').trim();
}
catch(e)
{
logger.warn('Cannot read encryption key from %s. Falling back to default key.', config.serviceSecretPath);
this.secret = 'default';
}
logger.info('Connecting to consul service: %s:%s', config.host, config.port);
this.consul = consul({
host : config.host,
port : config.port,
promisify : true
});
try
{
await retry(() => this.consul.status.leader(), { max_tries: config.retryCount, interval: config.retryTimeout });
}
catch(e)
{
logger.error('Could not connect to consul.');
logger.error(e);
throw e;
}
return this;
}
/**
* Gets the full qualified name for a key including the prefixed [serviceName]{@link serviceName}.
* @param {string} key - Key to check.
* @returns {string} Full qualified key name.
*/
module.exports.getFullKey = function(key)
{
return this.serviceName + '/' + key
}
/**
* Checks whenever the passed key follows the rules for key naming and is therefor a valid key.
* Requesting an invalid key from the system will throw an error.
*
* A key can only consist out of lower case caracters (a-z, A-Z), digits (0-9), hyphens (-) and slashes (/).
* A key must not begin or end with a slash (/).
*
* @param {string} key - Key to request. Will automatically get prefixed with [serviceName]{@link serviceName}.
* @returns {boolean} Returns true if the key is valid.
*/
module.exports.checkKey = function(key)
{
return key && /^([0-9a-zA-Z-]+\/?)+[0-9a-zA-Z-]+$/.test(this.getFullKey(key));
}
/**
* Checks whenever the passed prefix follows the rules for key prefixing.
* Requesting an invalid prefix from the system will throw an error.
*
* A prefix can only consist out of lower case caracters (a-z, A-Z), digits (0-9), hyphens (-) and slashes(/)
* and has to end with a slash (/). A prefix must not begin with a slash (/).
*
* @param {string} prefix - Prefix to request. Will automatically get prefixed with [serviceName]{@link serviceName}.
* @returns {boolean} Returns true if the prefix is valid.
*/
module.exports.checkKeyPrefix = function(prefix)
{
return prefix && /^([0-9a-zA-Z-]+\/)+$/.test(prefix);
}
/**
* Encrypts a given object using an internal secret.
* @param {object} data Object to be encrypted.
* @returns {string}
*/
module.exports.encryptData = function(data)
{
const cipher = crypto.createCipher('aes-256-cbc', this.secret);
return cipher.update(JSON.stringify(data), 'utf8', 'base64') + cipher.final('base64');
}
/**
* Decrypts a given string using an internal secret.
* @param {string} encrypted Encrypted data.
* @returns {object}
*/
module.exports.decryptData = function(encrypted)
{
const cipher = crypto.createDecipher('aes-256-cbc', this.secret);
return JSON.parse(cipher.update(encrypted, 'base64', 'utf8') + cipher.final('utf8'));
}
/**
* Gets a single or a list of values from consul's key-value store.
* To get a list of values you can either use key-prefixing or an array of keys or key-prefixes.
* In order to use key-prefixing, the *recusrive* parameter has to be set to true.
*
* If an array is passed in order to get a list of values, the returned promise resolves with an array of
* values where the output position inside the array equals the input position of the corresponding key.
*
* If key-prefixing is used, the returned promise resolves with an object where the object key represents the
* key inside consul and points to the received value.
*
* This method uses retry in order to bypass problems with network connections, service latencies or
* remote service capacity problems. For further details have a look at the [DefaultConfig]{@link DefaultConfig}.
* @param {mixed} keyOrPrefix - Key(s) to request. Can be either a single key string, a key-prefix, an array of keys or and array of key-prefixes. For using key-prefixes, the *recusrive* parameter has to be set to true. All keys an prefixes will automatically get prefixed with [serviceName]{@link serviceName}.
* @param {boolean} recursive - If set to true, the *keyOrPrefix* parameter is used as key-prefix to recursively list all keys below this prefix.
* @param {boolean} silent - If set to true, this method will not use retry to get something from consul so it will return faster and it will not pass any errors up to the calling code.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html}.
*/
module.exports.getProperty = function(keyOrPrefix, recursive, silent)
{
if(Array.isArray(keyOrPrefix))
return Promise.all(keyOrPrefix.map(k => this.getProperty(k, recursive)));
const keyName = this.getFullKey(keyOrPrefix);
const config = this.config;
const isValidKey = !recursive && this.checkKey(keyOrPrefix);
const isValidPrefix = recursive && this.checkKeyPrefix(keyOrPrefix);
if(isValidKey || isValidPrefix)
{
const task = () => Promise.resolve(getConsulValues.call(this, keyName, recursive));
if(silent)
return task().catch(e => this.logger.error(e));
else
return retry(task, { max_tries: config.retryCount, interval: config.retryTimeout })
.catch(e => { this.logger.error(e); throw e; });
}
else
{
return Promise.reject(new Error('The passed key or prefix is invalid: ' + keyOrPrefix));
}
};
/**
* Sets a value to consul's key-value store.
* @param {string} key - Key to set. Will automatically get prefixed with [serviceName]{@link serviceName}.
* @param {object} value - Value to set.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html}.
*/
module.exports.setProperty = function(key, value)
{
const keyName = this.getFullKey(key);
if(this.checkKey(key))
{
return new Promise((resolve, reject) =>
{
this.consul.kv.set(keyName, value).then(() => this.cache.put(keyName, value))
.then(resolve).catch(e => { this.logger.warn('Config key %s not be set: %s', keyName, e.message); reject(e); });
});
}
else
{
return Promise.reject(new Error('The passed key is invalid: ' + key));
}
};
/**
* Gets an endpoint from consul's service registry.
* Be aware, that this method only returns service endpoints that are marked *healthy* by consul.
* Endpoints that did not pass all health checks will not be available.
*
* This method uses retry in order to bypass problems with network connections, service latencies or
* remote service capacity problems. For further details have a look at the [DefaultConfig]{@link DefaultConfig}.
* @param {string} serviceName - Name of service to request.
* @param {boolean} silent - If set to true, this method will not use retry to get something from consul so it will return faster and it will not pass any errors up to the calling code.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html} containing and [Endpoint]{@link module:ocbesbn-config~Endpoint} object.
*/
module.exports.getEndPoint = function(serviceName, silent)
{
if(Array.isArray(serviceName))
return Promise.all(serviceName.map(k => this.getEndPoint(k, silent)));
const task = () => Promise.resolve(getConsulEndpoint.call(this, serviceName));
if(silent)
return task().catch(e => this.logger.error(e));
else
return retry(task, { max_tries: this.config.retryCount, interval: this.config.retryTimeout })
.catch(e => { this.logger.error(e); throw e; });
};
/**
* Gets a single or a list of values from consul's key-value store. Alias for [getProperty()]{@link getProperty}.
* To get a list of values you can either use key-prefixing or and array of keys and key-prefixes.
* In order to use key-prefixing, the *recusrive* parameter has to be set to true.
*
* If an array is passed in order to get a list of values, the returned promise resolves with an array of
* values where the output position inside the array equals the input position of the corresponding key.
*
* If key-prefixing is used, the returned promise resolves with an object where the object key represents the
* key inside consul and points to the received value.
*
* This method uses retry in order to bypass problems with network connections, service latencies or
* remote service capacity problems. For further details have a look at the [DefaultConfig]{@link DefaultConfig}.
* @param {mixed} keyOrPrefix - Key(s) to request. Can be either a single key string, a key-prefix, an array of keys or and array of key-prefixes. For using key-prefixes, the *recusrive* parameter has to be set to true. All keys an prefixes will automatically get prefixed with [serviceName]{@link serviceName}.
* @param {boolean} recursive - If set to true, the *keyOrPrefix* parameter is used as key-prefix to recursively list all keys below this prefix.
* @param {boolean} silent - If set to true, this method will not use retry to get something from consul so it will return faster and it will not pass any errors up to the calling code.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html}.
*/
module.exports.get = module.exports.getProperty;
/**
* Sets a value to consul's key-value store. Alias for [setProperty()]{@link setProperty}.
* @param {string} key - Key to set. Will automatically get prefixed with [serviceName]{@link serviceName}.
* @param {object} value - Value to set.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html}.
*/
module.exports.set = module.exports.setProperty;
/**
* Gets an encrypted value from consul's key-value store.
* This method uses retry in order to bypass problems with network connections, service latencies or
* remote service capacity problems. For further details have a look at the [DefaultConfig]{@link DefaultConfig}.
* @param {mixed} keyOrPrefix - Key(s) to request. Can be either a single key string, a key-prefix, an array of keys or and array of key-prefixes. For using key-prefixes, the *recusrive* parameter has to be set to true. All keys an prefixes will automatically get prefixed with [serviceName]{@link serviceName}.
* @param {boolean} recursive - If set to true, the *keyOrPrefix* parameter is used as key-prefix to recursively list all keys below this prefix.
* @param {boolean} silent - If set to true, this method will not use retry to get something from consul so it will return faster and it will not pass any errors up to the calling code.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html}.
*/
module.exports.getPassword = function(keyOrPrefix, recursive, silent)
{
return this.get(keyOrPrefix, recursive, silent).then(values =>
{
if(Array.isArray(keyOrPrefix) || recursive)
return Object.keys(values).reduce((all, key) => { all[key] = this.decryptData(values[key]); return all }, [ ]);
return this.decryptData(values);
});
}
/**
* Sets an encrypted value to consul's key-value store.
* @param {string} key - Key to set. Will automatically get prefixed with [serviceName]{@link serviceName}.
* @param {object} value - Value to set.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html}.
*/
module.exports.setPassword = function(key, value)
{
const encrypted = this.encryptData(value);
return this.set(key, encrypted);
};
/**
* Waits a certain time for a list of endpoints to be available. If all endpoints are available before the passed timeout is reached, this method will return a promise
* resolving to *true*. Otherwise the returned promise gets rejected with an error.
*
* @param {string|array} serviceNames - A single service name or an array of service names to wait for.
* @param {number?} timeout - The maximum time in milliseconds to wait for the requested services to be available.
* @returns {Promise} Returns a bluebird [Promise]{@link http://bluebirdjs.com/docs/api-reference.html} that gets rejected with an error if the passed timeout is reached.
*/
module.exports.waitForEndpoints = function(serviceNames, timeout = 5000)
{
return retry(() => this.getEndPoint(serviceNames).then(() => true), { interval : 1000, backOff : 1.2, max_interval : 20000, max_tries : Number.MAX_SAFE_INTEGER, timeout : timeout });
}
async function getConsulValues(keyName, recursive)
{
const value = await this.cache.get(keyName);
if(value)
return value;
const values = await this.consul.kv.get({ key : keyName, recurse : recursive });
if(!values)
throw new Error('Key was not found: ' + keyName);
if(recursive && Array.isArray(values))
{
const result = { };
values.forEach(v =>
{
const serviceIndex = v.Key.indexOf(this.serviceName);
const key = v.Key.substr(serviceIndex + this.serviceName.length + 1);
result[key] = v.Value;
this.cache.put(v.Key, v.Value);
});
await this.cache.put(keyName, result);
return result;
}
else if(values.Value)
{
await this.cache.put(keyName, values.Value);
return values.Value;
}
else
{
throw new Error('An unknown error occured: Empty result received for key: ' + keyName);
}
}
async function getConsulEndpoint(serviceName)
{
const cacheName = this.serviceName + '.ep.' + serviceName;
const cachedEndpoints = await this.cache.get(cacheName);
if(Array.isArray(cachedEndpoints) && cachedEndpoints.length > 0)
return helper.getRandomValue(cachedEndpoints);
const [ service, health ] = await Promise.all([
this.consul.health.service({ service : serviceName, passing : true }),
this.consul.catalog.service.nodes(serviceName)
]);
const endpoints = helper.joinArraysMatch(service, health, (health, node) => health.Node.ID === node.ID, (health, node) => node);
if(Array.isArray(endpoints) && endpoints.length > 0)
{
const results = endpoints.map(ep => ({ host : ep.ServiceAddress || ep.Address, port : ep.ServicePort }));
await this.cache.put(cacheName, results);
return helper.getRandomValue(results);
}
else
{
throw new Error('The requested endpoint could not be found or did not pass health checks: ' + serviceName);
}
}
module.exports = new ConfigClient();
{
"name": "@opuscapita/config",
"version": "3.0.2",
"version": "3.0.3",
"description": "Configuration API connector module for OpusCapita Business Network Portal.",

@@ -13,3 +13,3 @@ "main": "index.js",

"upload-coverage": "cat ./coverage/lcov.info | npx coveralls",
"api-doc": "npx jsdoc2md index.js > wiki/Home.md",
"api-doc": "npx jsdoc2md --files ./lib/* > wiki/Home.md",
"doc": "npm run api-doc",

@@ -30,17 +30,11 @@ "prepublishOnly": "npm version patch -m 'Version set to %s. [skip ci]'"

"index.js",
"helper.js"
"lib"
],
"nyc": {
"exclude": [
"test",
"logger.js"
]
},
"dependencies": {
"bluebird": "^3.5.1",
"bluebird-retry": "^0.11.0",
"consul": "^0.30.0",
"extend": "^3.0.1",
"ocbesbn-cache": "^1.0.7",
"ocbesbn-logger": "^1.0.5"
"bluebird": "~3.5.1",
"bluebird-retry": "~0.11.0",
"consul": "~0.33.1",
"extend": "~3.0.1",
"ocbesbn-cache": "~1.0.9",
"ocbesbn-logger": "~1.0.8"
},

@@ -47,0 +41,0 @@ "devDependencies": {

@@ -5,5 +5,16 @@ # @opuscapita/config

This module provides easy access to instances of the **consul** service registry. It helps with accessing **key-value** stored configuration data, local data **encryption** and service **endpoint discovery** with health checking.
This module provides easy access to instances of the **consul** service registry. It helps with accessing **key-value** stored configuration data, local data **encryption** and service **endpoint discovery** with health checking and change notifications.
It provides automatic key prefixing for *+key** storage **isolation** and **caching** for keys and endpoints. For further details, please have a look at the [wiki](https://github.com/OpusCapita/config/wiki).
[Minimum setup](#minimum-setup)
[Getting endpoints](#getting-endpoints)
[Automatic change events](#automatic-change-events)
[Requesting multiple keys](#requesting-multiple-keys)
[Minimum setup](#minimum-setup)
[Keys by prefix](#keys-by-prefix)
[Keys by list](#keys-by-list)
[Data encryption](#data-encryption)
[Default configuration](#default-configuration)
[Wiki](https://github.com/OpusCapita/config/wiki)

@@ -28,4 +39,7 @@ ---

// can be found at the .DefaultConfig module property.
config.init({}).then(() => config.setProperty('my-key', 'my-value'))
.then(() => config.getProperty('my-key')).then(console.log).catch(console.log);
await config.init({});
await config.setProperty('my-key', 'my-value');
const value = await config.getProperty('my-key');
console.log(value);
```

@@ -37,3 +51,3 @@

### Getting (service) endpoints
### Getting endpoints

@@ -43,10 +57,22 @@ ```JS

config.init().then(() => config.getEndPoint('service-name')).then(endpoint =>
{
console.log('Host: ' + endpoint.host);
console.log('Port: ' + endpoint.port);
})
.catch(console.log);
await config.init();
const endpoint = await config.getEndPoint('service-name');
console.log('Host: ' + endpoint.host);
console.log('Port: ' + endpoint.port);
```
### Automatic change events
The config module provides two different events that are emitted once a service health or a key-value setting has been changed. This is internally used to update the module's cache but it can also be used to write services, that react on changes in the environment. Smartly written, this can be used to change a service's configuration without restarting it.
```JS
const config = require('@opuscapita/config');
await config.init();
config.on('endpointChanged', (serviceName, endpoints) => console.log(serviceName, endpoints));
config.on('propertyChanged', (key, value) => console.log(key, value));
```
---

@@ -66,9 +92,16 @@

```JS
config.init({}).then(() => config.setProperty("path/to/key1", "value1"))
.then(() => config.setProperty("path/to/key2", "value2"))
.then(() => config.setProperty("path/to/key3", "value3"))
.then(() => config.getProperty("path/", true))
.then(console.log)
.catch(console.log);
const config = require('@opuscapita/config');
await config.init();
await Promise.all([
config.setProperty("path/to/key1", "value1"),
config.setProperty("path/to/key2", "value2"),
config.setProperty("path/to/key3", "value3")
]);
const values = await config.getProperty("path/", true);
console.log(values);
/* Should output:

@@ -86,9 +119,16 @@ { 'path/to/key1': 'value1',

```JS
config.init({}).then(() => config.setProperty("key1", "value1"))
.then(() => config.setProperty("key2", "value2"))
.then(() => config.setProperty("key3", "value3"))
.then(() => config.getProperty([ "key1", "key2", "key3" ]))
.then(console.log)
.catch(console.log);
const config = require('@opuscapita/config');
await config.init();
await Promise.all([
config.setProperty("path/to/key1", "value1"),
config.setProperty("path/to/key2", "value2"),
config.setProperty("path/to/key3", "value3")
]);
const values = await config.getProperty([ "key1", "key2", "key3" ]);
console.log(values);
/* Should output:

@@ -118,8 +158,9 @@ [ 'value1', 'value2', 'value3' ]

{
host: 'consul',
port: 8500,
host : 'consul',
port : 8500,
retryCount : 50,
retryTimeout : 1000,
serviceSecretPath : `/run/secrets/${module.exports.serviceName}-consul-key`
logger : null,
serviceSecretPath : `/run/secrets/${ConfigClientBase.serviceName}-consul-key`
}
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc