v2.0.1: Consolidate and promise (#5)
* v2.0: Native promise support, consolidation This version is a breaking change and now only supports native promises vs callbacks. It is 2020. This also removes the deps for keyvault config resolver, env resolver, and config-as-code, as those libraries have now been archived. Their source lives on here. * Removing unused library * Updating library for 2.0.1 launch
This commit is contained in:
Родитель
df7f796149
Коммит
a7db593c55
|
@ -16,6 +16,11 @@ lib-cov
|
|||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# local testing
|
||||
test.js
|
||||
test.json
|
||||
.vscode
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
|
|
28
README.md
28
README.md
|
@ -2,6 +2,8 @@
|
|||
|
||||
Yet another opinionated Node.js configuration library providing a set of default resolvers to enable rapid, rich configuration object graphs powered by the deployment environment, config-as-code, and Azure KeyVault secrets.
|
||||
|
||||
This library now requires a modern Node LTS+ version and uses native promises.
|
||||
|
||||
## Resolving variables into a simple configuration graph
|
||||
|
||||
This library takes an object (a configuration graph) containing simple variables or
|
||||
|
@ -22,12 +24,11 @@ In lieu of a configuration graph object, a special `config/` directory structure
|
|||
with JSON and JS files can be used to build the configuration object at startup,
|
||||
making it easy to compartmentalize values.
|
||||
|
||||
## Part of the painless-config family
|
||||
## Built on painless-config
|
||||
|
||||
This module is a part of the `painless-config` family of configuration libraries.
|
||||
|
||||
- [painless-config](https://github.com/Microsoft/painless-config): resolving a variable from an `env.json` file or the environment with a simple `get(key)` method
|
||||
- [painless-config-as-code](https://github.com/Microsoft/painless-config-as-code): resolving a variable from an environment-specific configuration file located within a repository, enabling configuration-as-code, including code reviews and pull requests for config changes.
|
||||
|
||||
## How to use
|
||||
|
||||
|
@ -41,13 +42,10 @@ const graph = {
|
|||
app: 'my app',
|
||||
};
|
||||
|
||||
resolver.resolve(graph, (error, config) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function myApp() {
|
||||
const config = await resolver.resolve(graph);
|
||||
// ... config has the resolved values ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
After calling `resolve` the `config` object might look like:
|
||||
|
@ -59,7 +57,13 @@ After calling `resolve` the `config` object might look like:
|
|||
}
|
||||
```
|
||||
|
||||
### Unofficial but useful
|
||||
## Consolidation
|
||||
|
||||
As of v2.0.0, this library has merged in the `painless-config-as-code`, environment, and keyvault
|
||||
environment providers to make it easier to keep the library up-to-date. This admits that this library
|
||||
is really coupled with KeyVault enough that it is OK to include those dependencies.
|
||||
|
||||
## Unofficial but useful
|
||||
|
||||
This component was developed by the Open Source Programs Office at Microsoft. The OSPO team
|
||||
uses Node.js for some of its applications and has found this component to be useful. We are
|
||||
|
@ -85,6 +89,12 @@ with any additional questions or comments.
|
|||
|
||||
# Changes
|
||||
|
||||
## 2.0.0
|
||||
|
||||
- Node >= 10.x required (suggest LTS 12+)
|
||||
- Callbacks removed. The library is built on native Promises now.
|
||||
- Merges dependent modules `painless-config-as-code`, `environment-configuration-resolver`, `keyvault-configuration-resolver` as native capabilities to reduce the package count and improve updates, publishing and debugging
|
||||
|
||||
## 1.1.4
|
||||
|
||||
- Updated dependencies
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
> This is the former README for the `keyvault-configuration-resolver-node` repo that has been integrated
|
||||
|
||||
# Azure KeyVault configuration secrets resolver
|
||||
|
||||
This library extends the official Azure KeyVault client library for Node.js with a new
|
||||
method, `resolveObjectSecrets`, that takes an object graph (typically a set of configuration
|
||||
data), walks the object to identify strings that use a custom syntax to identify KeyVault
|
||||
secrets, and then gets the secret(s) using the KeyVault client, updating the value with the
|
||||
resolved secret.
|
||||
|
||||
At this time the library is available on npm with the module name `keyvault-configuration-resolver`.
|
||||
|
||||
```
|
||||
npm install keyvault-configuration-resolver --save
|
||||
```
|
||||
|
||||
## Intended scenario
|
||||
|
||||
The purpose of this library is to help resolve a bunch of secrets at app startup, or other
|
||||
times.
|
||||
|
||||
The goal is to prevent developer mistakes by keeping secrets in a KeyVault instead of checking in
|
||||
credentials to a repository. It also helps keep production secrets more secret by not authorizing
|
||||
the production key vault instance to work with other applications.
|
||||
|
||||
Since the resolution of key vault secrets in this case requires a client ID and secret, anyone
|
||||
with the client ID and secret could resolve the secrets, but for us it's enough to prevent most
|
||||
mistakes.
|
||||
|
||||
## Limitations/ known issues
|
||||
|
||||
- `npm-shrinkwrap.json`: shrinkwrap files may break this script
|
||||
|
||||
### Unofficial but useful
|
||||
|
||||
This component was developed by the Open Source Programs Office at Microsoft. The OSPO team
|
||||
uses Node.js for some of its applications and has found this component to be useful. We are
|
||||
sharing this in the hope that others may find it useful.
|
||||
|
||||
It's important to understand that this library was developed for use by a team at Microsoft, but
|
||||
that this is not an official library or module built by the KeyVault team.
|
||||
|
||||
### Why a custom URI protocol syntax
|
||||
|
||||
We use a custom URI syntax to identify KeyVault secrets that we would like to address inside of
|
||||
an object.
|
||||
|
||||
This is as opposed to trying to identify strings that are URIs and contain KeyVault addresses; we
|
||||
have many valid scenarios where we resolve KeyVault secrets at runtime, so we do not want to
|
||||
resolve them just once.
|
||||
|
||||
The custom syntax makes it very clear that the developer/configuration explicitly wants to take
|
||||
the string with the `keyvault://` syntax and resolve it.
|
||||
|
||||
## How to use the library
|
||||
|
||||
The library simply extends the provided Azure KeyVault client for Node.js, or creates a new
|
||||
instance, with a new method called `getObjectSecrets`.
|
||||
|
||||
### getObjectSecrets method
|
||||
|
||||
The get object secrets method takes in an object and a callback. Anything identified as a
|
||||
KeyVault string URI (by having the custom `keyvault://` scheme) will
|
||||
be resolved using KeyVault. The application will need to have permission to the vault(s)
|
||||
or it will error out.
|
||||
|
||||
### example
|
||||
|
||||
Here is the environment that we are making available to this application:
|
||||
|
||||
```
|
||||
AZUREAD_CLIENT_ID=our_AAD_app_id
|
||||
AZUREAD_CLIENT_SECRET=our_AAD_app_secret
|
||||
SESSION_SALT=keyvault://keyvaultname.vault.azure.net/secrets/our-app-session-salt
|
||||
AZURE_STORAGE_ACCOUNT=keyvault://account@keyvaultname.vault.azure.net/secrets/azure-storage-account/7038c64d4b094ee897bc62dd43b29640
|
||||
AZURE_STORAGE_KEY=keyvault://keyvaultname.vault.azure.net/secrets/azure-storage-account/7038c64d4b094ee897bc62dd43b29640
|
||||
```
|
||||
|
||||
Note that this AAD client ID has been authorized as a service principal to have secret GET
|
||||
access to the KeyVault. You can configure this in the Azure portal or using PowerShell, etc.
|
||||
|
||||
Our Azure storage key in this example also has a custom "tag" set called "account" that
|
||||
includes the account name. This is to demonstrate how to refer to tag values stored
|
||||
alongside the secret version.
|
||||
|
||||
Here is a sample object graph that contains some configuration information that our
|
||||
application would like to use:
|
||||
|
||||
```
|
||||
let config = {
|
||||
session: {
|
||||
salt: process.env.SESSION_SALT,
|
||||
},
|
||||
storage: {
|
||||
account: process.env.AZURE_STORAGE_ACCOUNT,
|
||||
key: process.env.AZURE_STORAGE_KEY,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
And here is how we go ahead at startup inside of our Express app to resolve these secrets:
|
||||
|
||||
```
|
||||
const keyVaultResolver = require('keyvault-configuration-resolver');
|
||||
|
||||
const keyVaultConfig = {
|
||||
clientId: process.env.AZUREAD_CLIENT_ID,
|
||||
clientSecret: process.env.AZUREAD_CLIENT_SECRET,
|
||||
};
|
||||
|
||||
keyVaultResolver(keyVaultConfig).getObjectSecrets(config, (resolutionError) => {
|
||||
if (resolutionError) {
|
||||
throw resolutionError;
|
||||
}
|
||||
// at this point, config values such as config.storage.key now have the secrets
|
||||
|
||||
// ... continue your app work, or store in middleware
|
||||
});
|
||||
```
|
||||
|
||||
## How to instantiate the library
|
||||
|
||||
### With an AAD client ID and secret
|
||||
|
||||
Pass in the `clientId` and `clientSecret` as options to the library.
|
||||
|
||||
Alternatively, you can pass in a function called `getClientCredentials` that will be called when they are needed. `clientId` and `clientSecret` values are expected at this time.
|
||||
|
||||
### With an existing KeyVault credentials instance
|
||||
|
||||
Pass in the credentials as a property called `credentials`, and the KeyVaultClient will be
|
||||
instantiated for you, along with the new method `getObjectSecrets`.
|
||||
|
||||
### With an existing KeyVault client instance
|
||||
|
||||
Simply pass in the client instance as the value of a property called `client`. The client
|
||||
will then have a `getObjectSecrets` method.
|
||||
|
||||
You may also just pass in a KeyVault client instance. This is detected by the presence of
|
||||
a function called "getSecret" on the passed-in object.
|
||||
|
||||
### With an AAD client certificate
|
||||
|
||||
_Not supported_
|
||||
|
||||
This work is not yet complete but may be available in a future update. At this time the app
|
||||
secret must be provided for KeyVault resolution to work with Node.js apps.
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Contributing
|
||||
|
||||
Pull requests will gladly be considered! A CLA may be needed.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of
|
||||
Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct
|
||||
FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com)
|
||||
with any additional questions or comments.
|
||||
|
||||
# Changes
|
||||
|
||||
## 1.0.4
|
||||
|
||||
- Thrown errors in validation by the key vault clients are propagated
|
||||
|
||||
## 1.0.3
|
||||
|
||||
- Newer dependencies
|
||||
- Removes `eslint` dev dep
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Uses a newer KeyVault client
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- For each resolution call (a set of changes through the object graph), a cache of secret responses is maintained. This improves performance for apps that include many sub-secret tag values or reuse the same secret many times.
|
||||
|
||||
## 0.9.7
|
||||
|
||||
- First stable release
|
|
@ -0,0 +1,74 @@
|
|||
> This is the former README for the `painless-config-as-code` repo that has been integrated
|
||||
|
||||
# painless-config-as-code
|
||||
|
||||
Environment variable resolution using configuration-as-code logic on top of painless-config. For Node.js apps.
|
||||
|
||||
## Environment value resolution
|
||||
|
||||
order | type | provider of value | notes
|
||||
------------ | -------------------- | ----------------------- | ----------
|
||||
1 | process env variable | painless-config | process.env in node
|
||||
2 | env.json file value | painless-config | will walk up the directory hierarchy until finding an env.json
|
||||
3 | _env_.json file val | painless-config-as-code | will look for an ./env/_env_.json, such as ./env/prod.json
|
||||
4 | env package | npm package | package defined in package.json or ENVIRONMENT_MODULES_NAME
|
||||
|
||||
## How to use the library
|
||||
|
||||
```
|
||||
const painlessConfigAsCode = require('painless-config-as-code');
|
||||
const someValue = painlessConfigAsCode.get('SOME_VALUE');
|
||||
```
|
||||
|
||||
### Unofficial but useful
|
||||
|
||||
This component was developed by the Open Source Programs Office at Microsoft. The OSPO team
|
||||
uses Node.js for some of its applications and has found this component to be useful. We are
|
||||
sharing this in the hope that others may find it useful.
|
||||
|
||||
It's important to understand that this library was developed for use by a team at Microsoft, but
|
||||
that this is not an official library or module built by the KeyVault team.
|
||||
|
||||
# Other Node configuration libraries
|
||||
|
||||
There are many other configuration libraries for Node, but of course everyone
|
||||
has their own favorite. Consider one of these if you'd like a more fully supported
|
||||
library.
|
||||
|
||||
This library is most like `node-config`, with the different being that it is
|
||||
limited to just JSON files at this time for values, and its use of `painless-config`
|
||||
to resolve environment variables or other configuration values located up the
|
||||
directory hierarchy.
|
||||
|
||||
In deciding to build this library many other libraries were considered.
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Contributing
|
||||
|
||||
Pull requests will gladly be considered! A CLA may be needed.
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of
|
||||
Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct
|
||||
FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com)
|
||||
with any additional questions or comments.
|
||||
|
||||
# Changes
|
||||
|
||||
## 0.0.2
|
||||
|
||||
- Adds support for environment-containing npm package(s)
|
||||
- Multiple packages are supported, with the first package values winning
|
||||
- Packages can be defined in an app's `package.json` as well as environment variables such as `ENVIRONMENT_MODULES`
|
||||
- Environment-variable based package names have higher precedence than `package.json`-based
|
||||
- Can be required without calling as a function when no custom initialization options are needed
|
||||
- The environment directory name can now be configured via the `ENVIRONMENT_DIRECTORY` key (and also `ENVIRONMENT_DIRECTORY_KEY` to change that variable name)
|
||||
- The variable keys used to define the configuration environment can now be customized via `CONFIGURATION_ENVIRONMENT_KEYS`
|
||||
|
||||
## 0.0.1
|
||||
|
||||
- Initial release
|
|
@ -0,0 +1,127 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
'use strict';
|
||||
|
||||
const objectPath = require('object-path');
|
||||
const url = require('url');
|
||||
|
||||
// Configuration Assumptions:
|
||||
// In URL syntax, we define a custom scheme of "env://" which resolves
|
||||
// an environment variable in the object, directly overwriting the
|
||||
// original value.
|
||||
//
|
||||
// For example:
|
||||
// "env://HOSTNAME" will resolve on a Windows machine to its hostname
|
||||
//
|
||||
// Note that this use of a custom scheme called "env" is not an officially
|
||||
// recommended or supported thing, but it has worked great for us!
|
||||
|
||||
const envProtocol = 'env:';
|
||||
|
||||
function getUrlIfEnvironmentVariable(value) {
|
||||
try {
|
||||
const u = url.parse(value, true /* parse query string */);
|
||||
if (u.protocol === envProtocol) {
|
||||
return u;
|
||||
}
|
||||
}
|
||||
catch (typeError) {
|
||||
/* ignore */
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function identifyPaths(node, prefix) {
|
||||
prefix = prefix !== undefined ? prefix + '.' : '';
|
||||
const paths = {};
|
||||
for (const property in node) {
|
||||
const value = node[property];
|
||||
if (typeof value === 'object') {
|
||||
Object.assign(paths, identifyPaths(value, prefix + property));
|
||||
continue;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
const envUrl = getUrlIfEnvironmentVariable(value);
|
||||
if (!envUrl) {
|
||||
continue;
|
||||
}
|
||||
const originalHostname = value.substr(value.indexOf(envProtocol) + envProtocol.length + 2, envUrl.hostname.length);
|
||||
if (originalHostname.toLowerCase() === envUrl.hostname.toLowerCase()) {
|
||||
envUrl.hostname = originalHostname;
|
||||
}
|
||||
paths[prefix + property] = envUrl;
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
function defaultProvider() {
|
||||
return {
|
||||
get: (key) => {
|
||||
return process.env[key];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createClient(options) {
|
||||
options = options || {};
|
||||
let provider = options.provider || defaultProvider();
|
||||
return {
|
||||
resolveObjectVariables: async (object) => {
|
||||
let paths = null;
|
||||
try {
|
||||
paths = identifyPaths(object);
|
||||
} catch(parseError) {
|
||||
throw parseError;
|
||||
}
|
||||
const names = Object.getOwnPropertyNames(paths);
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const path = names[i];
|
||||
const parsed = paths[path];
|
||||
const variableName = parsed.hostname;
|
||||
let variableValue = provider.get(variableName);
|
||||
|
||||
// Support for default variables
|
||||
if (variableValue === undefined && parsed.query && parsed.query.default) {
|
||||
variableValue = parsed.query.default;
|
||||
}
|
||||
|
||||
// Loose equality "true" for boolean values
|
||||
if (parsed.query && parsed.query.trueIf) {
|
||||
variableValue = parsed.query.trueIf == /* loose */ variableValue;
|
||||
}
|
||||
|
||||
// Cast if a type is set to 'boolean' or 'integer'
|
||||
if (parsed.query && parsed.query.type) {
|
||||
const currentValue = variableValue;
|
||||
switch (parsed.query.type) {
|
||||
case 'boolean':
|
||||
case 'bool':
|
||||
if (currentValue && currentValue !== 'false' && currentValue != '0' && currentValue !== 'False') {
|
||||
variableValue = true;
|
||||
} else {
|
||||
variableValue = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'integer':
|
||||
case 'int':
|
||||
variableValue = parseInt(currentValue, 10);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`The "type" parameter for the env:// string was set to "${parsed.query.type}", a type that is currently not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
objectPath.set(object, path, variableValue);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = createClient;
|
|
@ -23,11 +23,15 @@ function jsonProcessor(api, config, p) {
|
|||
return require(p);
|
||||
}
|
||||
|
||||
module.exports = (api, dirPath, callback) => {
|
||||
if (!callback && typeof (api) === 'function') {
|
||||
callback = api;
|
||||
api = null;
|
||||
}
|
||||
function fsReadDir(dirPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(dirPath, (directoryError, files) => {
|
||||
return directoryError ? reject(directoryError) : resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = async (api, dirPath) => {
|
||||
api = api || {};
|
||||
const options = api.options || {};
|
||||
|
||||
|
@ -35,37 +39,38 @@ module.exports = (api, dirPath, callback) => {
|
|||
const requireConfigurationDirectory = options.requireConfigurationDirectory || false;
|
||||
|
||||
const config = {};
|
||||
fs.readdir(dirPath, (directoryError, files) => {
|
||||
if (directoryError && requireConfigurationDirectory) {
|
||||
return callback(directoryError);
|
||||
let files = [];
|
||||
try {
|
||||
files = await fsReadDir(dirPath);
|
||||
} catch (directoryError) {
|
||||
// behavior change: version 1.x of this library through whenever this error'd, not just if required
|
||||
if (requireConfigurationDirectory) {
|
||||
throw directoryError;
|
||||
}
|
||||
if (directoryError) {
|
||||
return callback(directoryError);
|
||||
}
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = path.join(dirPath, files[i]);
|
||||
const ext = path.extname(file);
|
||||
const nodeName = path.basename(file, ext);
|
||||
const processor = supportedExtensions.get(ext);
|
||||
if (!processor) {
|
||||
continue;
|
||||
}
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = path.join(dirPath, files[i]);
|
||||
const ext = path.extname(file);
|
||||
const nodeName = path.basename(file, ext);
|
||||
const processor = supportedExtensions.get(ext);
|
||||
if (!processor) {
|
||||
continue;
|
||||
try {
|
||||
const value = processor(api, config, file);
|
||||
if (value && typeof(value) === 'string' && value === dirPath) {
|
||||
// Skip the index.js for local hybrid package scenarios
|
||||
} else if (value !== undefined) {
|
||||
objectPath.set(config, nodeName, value);
|
||||
}
|
||||
try {
|
||||
const value = processor(api, config, file);
|
||||
if (value && typeof(value) === 'string' && value === dirPath) {
|
||||
// Skip the index.js for local hybrid package scenarios
|
||||
} else if (value !== undefined) {
|
||||
objectPath.set(config, nodeName, value);
|
||||
}
|
||||
} catch (ex) {
|
||||
ex.path = file;
|
||||
if (treatErrorsAsWarnings) {
|
||||
objectPath.set(config, nodeName, ex);
|
||||
} else {
|
||||
return callback(ex);
|
||||
}
|
||||
} catch (ex) {
|
||||
ex.path = file;
|
||||
if (treatErrorsAsWarnings) {
|
||||
objectPath.set(config, nodeName, ex);
|
||||
} else {
|
||||
return callback(ex);
|
||||
}
|
||||
}
|
||||
return callback(null, config);
|
||||
});
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
|
63
lib/index.js
63
lib/index.js
|
@ -5,11 +5,10 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
const environmentConfigurationResolver = require('environment-configuration-resolver');
|
||||
const environmentConfigurationResolver = require('./environmentConfigurationResolver');
|
||||
const multiGraphBuilder = require('./multiGraphBuilder');
|
||||
const keyVaultConfigurationResolver = require('keyvault-configuration-resolver');
|
||||
const painlessConfigAsCode = require('painless-config-as-code');
|
||||
const keyVaultConfigurationResolver = require('./keyVaultConfigurationResolver');
|
||||
const painlessConfigAsCode = require('./painlessConfigAsCode');
|
||||
|
||||
const keyVaultClientIdFallbacks = [
|
||||
// 0: the value of the KEYVAULT_CLIENT_ID_KEY variable
|
||||
|
@ -33,11 +32,12 @@ function createDefaultResolvers(libraryOptions) {
|
|||
} catch (ignoreError) {
|
||||
/* ignore */
|
||||
}
|
||||
// let applicationName = libraryOptions.applicationName || environmentProvider.applicationName;
|
||||
|
||||
// KeyVault today needs a client ID and secret to bootstrap the
|
||||
// resolver. This does mean that the secret cannot be stored in
|
||||
// the vault.
|
||||
// the vault. The newer Azure service endpoint allows for
|
||||
// secrets resolution without storing such a value. This does
|
||||
// not yet support that concept, sorry.
|
||||
const keyVaultOptions = {
|
||||
getClientCredentials: () => {
|
||||
unshiftOptionalVariable(keyVaultClientIdFallbacks, environmentProvider, 'KEYVAULT_CLIENT_ID_KEY');
|
||||
|
@ -86,21 +86,20 @@ function getEnvironmentValue(environmentProvider, potentialNames) {
|
|||
}
|
||||
}
|
||||
|
||||
function getConfigGraph(libraryOptions, options, environmentProvider, callback) {
|
||||
async function getConfigGraph(libraryOptions, options, environmentProvider) {
|
||||
if (options.graph) {
|
||||
return callback(null, options.graph);
|
||||
return options.graph;
|
||||
}
|
||||
let graphProvider = options.graphProvider || libraryOptions.graphProvider || multiGraphBuilder;
|
||||
if (!graphProvider) {
|
||||
return callback(new Error('No graph provider configured for this environment.'));
|
||||
throw new Error('No graph provider configured for this environment: no options.graphProvider or libraryOptions.graphProvider or multiGraphBuilder');
|
||||
}
|
||||
const graphLibraryApi = {
|
||||
options: options,
|
||||
options,
|
||||
environment: environmentProvider,
|
||||
};
|
||||
graphProvider(graphLibraryApi, (graphBuildError, graph) => {
|
||||
return callback(graphBuildError ? graphBuildError : null, graphBuildError ? undefined : graph);
|
||||
});
|
||||
const graph = await graphProvider(graphLibraryApi);
|
||||
return graph;
|
||||
}
|
||||
|
||||
function initialize(libraryOptions) {
|
||||
|
@ -111,33 +110,33 @@ function initialize(libraryOptions) {
|
|||
}
|
||||
const environmentProvider = resolvers.environment;
|
||||
return {
|
||||
resolve: function (options, callback) {
|
||||
if (!callback && typeof(options) === 'function') {
|
||||
callback = options;
|
||||
options = null;
|
||||
resolve: async function (options) {
|
||||
if (typeof(options) === 'function') {
|
||||
const deprecatedCallback = options;
|
||||
return deprecatedCallback(new Error('This library no longer supports callbacks. Please use native JavaScript promises, i.e. const config = await painlessConfigResolver.resolve();'));
|
||||
}
|
||||
options = options || {};
|
||||
// Find, build or dynamically generate the configuration graph
|
||||
getConfigGraph(libraryOptions, options, environmentProvider, (buildGraphError, graph) => {
|
||||
if (buildGraphError) {
|
||||
return callback(buildGraphError);
|
||||
const graph = await getConfigGraph(libraryOptions, options, environmentProvider);
|
||||
if (!graph) {
|
||||
throw new Error('No configuration "graph" provided as an option to this library. Unless using a configuration graph provider, the graph option must be included.');
|
||||
}
|
||||
try {
|
||||
// Synchronously, in order, resolve the graph
|
||||
for (const resolver of resolvers) {
|
||||
await resolver(graph);
|
||||
}
|
||||
if (!graph) {
|
||||
return callback(new Error('No configuration "graph" provided as an option to this library. Unless using a configuration graph provider, the graph option must be included.'));
|
||||
}
|
||||
// Synchronously, in order, resolve the graph
|
||||
async.eachSeries(resolvers, (resolver, next) => {
|
||||
resolver(graph, next);
|
||||
}, (err) => {
|
||||
return callback(err ? err : null, err ? null : graph);
|
||||
});
|
||||
});
|
||||
} catch (resolveConfigurationError) {
|
||||
console.warn(`Error while resolving the graph with a resolver: ${resolveConfigurationError}`);
|
||||
throw resolveConfigurationError;
|
||||
}
|
||||
return graph;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
initialize.resolve = function moduleWithoutInitialization(options, callback) {
|
||||
initialize().resolve(options, callback);
|
||||
initialize.resolve = function moduleWithoutInitialization(options) {
|
||||
return initialize().resolve(options);
|
||||
};
|
||||
|
||||
module.exports = initialize;
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
'use strict';
|
||||
|
||||
const adalNode = require('adal-node');
|
||||
const azureKeyVault = require('azure-keyvault');
|
||||
const objectPath = require('object-path');
|
||||
const url = require('url');
|
||||
const URL = url.URL;
|
||||
|
||||
// Key Vault Configuration Assumptions:
|
||||
// In URL syntax, we define a custom scheme of "keyvault://" which resolves
|
||||
// a KeyVault secret ID, replacing the original. To use a tag (a custom
|
||||
// attribute on a secret - could be a username for example), use the tag
|
||||
// name as the auth parameter of the URL.
|
||||
//
|
||||
// For example:
|
||||
// keyvault://myCustomTag@keyvaultname.vault.azure.net/secrets/secret-value-name/secretVersion",
|
||||
//
|
||||
// Would resolve the "myCustomTag" value instead of the secret value.
|
||||
//
|
||||
// You can also chose to leave the version off, so that the most recent version
|
||||
// of the secret will be resolved during the resolution process.
|
||||
//
|
||||
// In the case that a KeyVault secret ID is needed inside the app, and not
|
||||
// handled at startup, then the secret ID (a URI) can be included without
|
||||
// the custom keyvault:// scheme.
|
||||
//
|
||||
// Note that this use of a custom scheme called "keyvault" is not an officially
|
||||
// recommended or supported approach for KeyVault use in applications, and may
|
||||
// not be endorsed by the engineering team responsible for KeyVault, but for our
|
||||
// group and our Node apps, it has been very helpful.
|
||||
|
||||
const keyVaultProtocol = 'keyvault:';
|
||||
const httpsProtocol = 'https:';
|
||||
const secretsPath = '/secrets/';
|
||||
|
||||
function getSecretAsPromise(keyVaultClient, secretStash, secretId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return getSecret(keyVaultClient, secretStash, secretId, (error, result) => {
|
||||
return error ? reject(error) : resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getSecret(keyVaultClient, secretStash, secretId, callback) {
|
||||
const cached = secretStash.get(secretId);
|
||||
if (cached) {
|
||||
return callback(null, cached);
|
||||
}
|
||||
const secretUrl = new URL(secretId);
|
||||
const vaultBaseUrl = secretUrl.origin;
|
||||
const i = secretUrl.pathname.indexOf(secretsPath);
|
||||
if (i < 0) {
|
||||
return callback(new Error('The requested resource must be a KeyVault secret'));
|
||||
}
|
||||
let secretName = secretUrl.pathname.substr(i + secretsPath.length);
|
||||
let version = '';
|
||||
const versionIndex = secretName.indexOf('/');
|
||||
if (versionIndex >= 0) {
|
||||
version = secretName.substr(versionIndex + 1);
|
||||
secretName = secretName.substr(0, versionIndex);
|
||||
}
|
||||
|
||||
try {
|
||||
keyVaultClient.getSecret(vaultBaseUrl, secretName, version, (getSecretError, secretResponse) => {
|
||||
if (getSecretError) {
|
||||
return callback(getSecretError);
|
||||
}
|
||||
secretStash.set(secretId, secretResponse);
|
||||
return callback(null, secretResponse);
|
||||
});
|
||||
} catch (keyVaultValidationError) {
|
||||
return callback(keyVaultValidationError);
|
||||
}
|
||||
}
|
||||
|
||||
function getUrlIfVault(value) {
|
||||
try {
|
||||
const keyVaultUrl = url.parse(value);
|
||||
if (keyVaultUrl.protocol === keyVaultProtocol) {
|
||||
return keyVaultUrl;
|
||||
}
|
||||
}
|
||||
catch (typeError) {
|
||||
/* ignore */
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function identifyKeyVaultValuePaths(node, prefix) {
|
||||
prefix = prefix !== undefined ? prefix + '.' : '';
|
||||
const paths = {};
|
||||
for (const property in node) {
|
||||
const value = node[property];
|
||||
if (typeof value === 'object') {
|
||||
Object.assign(paths, identifyKeyVaultValuePaths(value, prefix + property));
|
||||
continue;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
const keyVaultUrl = getUrlIfVault(value);
|
||||
if (keyVaultUrl === undefined) {
|
||||
continue;
|
||||
}
|
||||
paths[prefix + property] = keyVaultUrl;
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
function wrapClient(keyVaultClient) {
|
||||
keyVaultClient.getObjectSecrets = async function resolveSecrets(object) {
|
||||
let paths = null;
|
||||
try {
|
||||
paths = identifyKeyVaultValuePaths(object);
|
||||
} catch(parseError) {
|
||||
throw parseError;
|
||||
}
|
||||
|
||||
// Build a unique list of secrets, fetch them at once
|
||||
const uniqueUris = new Set();
|
||||
const properties = new Map();
|
||||
for (const path in paths) {
|
||||
const value = paths[path];
|
||||
const tag = value.auth;
|
||||
value.protocol = httpsProtocol;
|
||||
value.auth = null;
|
||||
const uri = url.format(value);
|
||||
properties.set(path, [uri, tag]);
|
||||
uniqueUris.add(uri);
|
||||
}
|
||||
const secretStash = new Map();
|
||||
const uniques = Array.from(uniqueUris.values());
|
||||
for (const uniqueSecretId of uniques) {
|
||||
try {
|
||||
await getSecretAsPromise(keyVaultClient, secretStash, uniqueSecretId);
|
||||
} catch (resolveSecretError) {
|
||||
console.log(`Error resolving secret with ID ${uniqueSecretId}: ${resolveSecretError}`);
|
||||
throw resolveSecretError;
|
||||
}
|
||||
}
|
||||
for (const path in paths) {
|
||||
const [uri, tag] = properties.get(path);
|
||||
const secretResponse = secretStash.get(uri);
|
||||
|
||||
let value = undefined;
|
||||
if (tag === null) {
|
||||
value = secretResponse.value;
|
||||
} else if (secretResponse.tags) {
|
||||
value = secretResponse.tags[tag];
|
||||
}
|
||||
|
||||
objectPath.set(object, path, value);
|
||||
}
|
||||
};
|
||||
return keyVaultClient;
|
||||
}
|
||||
|
||||
function createAndWrapKeyVaultClient(options) {
|
||||
if (!options) {
|
||||
throw new Error('No options provided for the key vault resolver.');
|
||||
}
|
||||
let client = options && options.getSecret && typeof(options.getSecret) === 'function' ? options : options.client;
|
||||
if (options.credentials && !client) {
|
||||
client = new azureKeyVault.KeyVaultClient(options.credentials);
|
||||
}
|
||||
if (!client) {
|
||||
let clientId = null;
|
||||
let clientSecret = null;
|
||||
let getClientCredentials = options.getClientCredentials;
|
||||
if (!getClientCredentials) {
|
||||
if (!options.clientId) {
|
||||
throw new Error('Must provide an Azure Active Directory "clientId" value to the key vault resolver.');
|
||||
}
|
||||
if (!options.clientSecret) {
|
||||
throw new Error('Must provide an Azure Active Directory "clientSecret" value to the key vault resolver.');
|
||||
}
|
||||
clientId = options.clientId;
|
||||
clientSecret = options.clientSecret;
|
||||
}
|
||||
|
||||
const authenticator = (challenge, authCallback) => {
|
||||
const context = new adalNode.AuthenticationContext(challenge.authorization);
|
||||
|
||||
// Support optional delayed secret resolution
|
||||
if (getClientCredentials && (!clientId || !clientSecret)) {
|
||||
try {
|
||||
const ret = getClientCredentials();
|
||||
if (ret) {
|
||||
clientId = ret.clientId;
|
||||
clientSecret = ret.clientSecret;
|
||||
}
|
||||
} catch (getClientCredentialsError) {
|
||||
return authCallback(getClientCredentialsError);
|
||||
}
|
||||
if (!clientId || !clientSecret) {
|
||||
return authCallback(new Error('After calling getClientCredentials, "clientId" and/or "clientSecret" remained unset. These values are required to authenticate with the vault.'));
|
||||
}
|
||||
}
|
||||
|
||||
return context.acquireTokenWithClientCredentials(challenge.resource, clientId, clientSecret, (tokenAcquisitionError, tokenResponse) => {
|
||||
if (tokenAcquisitionError) {
|
||||
return authCallback(tokenAcquisitionError);
|
||||
}
|
||||
const authorizationValue = `${tokenResponse.tokenType} ${tokenResponse.accessToken}`;
|
||||
return authCallback(null, authorizationValue);
|
||||
});
|
||||
};
|
||||
const credentials = new azureKeyVault.KeyVaultCredentials(authenticator);
|
||||
client = new azureKeyVault.KeyVaultClient(credentials);
|
||||
}
|
||||
return wrapClient(client);
|
||||
}
|
||||
|
||||
module.exports = createAndWrapKeyVaultClient;
|
|
@ -6,18 +6,13 @@
|
|||
'use strict';
|
||||
|
||||
const appRoot = require('app-root-path');
|
||||
const async = require('async');
|
||||
const deepmerge = require('deepmerge');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const graphBuilder = require('./graphBuilder');
|
||||
|
||||
function composeGraphs(api, callback) {
|
||||
if (!callback && typeof (api) === 'function') {
|
||||
callback = api;
|
||||
api = null;
|
||||
}
|
||||
async function composeGraphs(api) {
|
||||
api = api || {};
|
||||
const options = api.options || {};
|
||||
let applicationRoot = (options.applicationRoot || appRoot).toString();
|
||||
|
@ -46,27 +41,21 @@ function composeGraphs(api, callback) {
|
|||
}
|
||||
|
||||
if (paths.length === 0) {
|
||||
return callback(new Error('No configuration packages or directories were found to process. Consider using "options.graph" as an option to the configuration resolver if you do not need to use configuration directories. Otherwise, check that you have configured your package.json or other environment values as needed.'));
|
||||
throw new Error('No configuration packages or directories were found to process. Consider using "options.graph" as an option to the configuration resolver if you do not need to use configuration directories. Otherwise, check that you have configured your package.json or other environment values as needed.');
|
||||
}
|
||||
|
||||
// Build the graph
|
||||
// ---------------
|
||||
let graph = {};
|
||||
async.eachSeries(paths.reverse(), (p, next) => {
|
||||
graphBuilder(api, p, (buildError, result) => {
|
||||
if (buildError) {
|
||||
return next(buildError);
|
||||
}
|
||||
const overwriteMerge = (destinationArray, sourceArray/* , options*/) => sourceArray;
|
||||
graph = deepmerge(graph, result, { arrayMerge: overwriteMerge });
|
||||
return next();
|
||||
});
|
||||
}, error => {
|
||||
if (!error && (!graph || Object.getOwnPropertyNames(graph).length === 0)) {
|
||||
error = new Error(`Successfully processed ${paths.length} configuration graph packages or directories, yet the resulting graph object did not have properties. This is likely an error or issue that should be corrected. Or, alternatively, use options.graph as an input to the resolver.`);
|
||||
}
|
||||
return callback(error, error ? null : graph);
|
||||
});
|
||||
for (const p of paths.reverse()) {
|
||||
const result = await graphBuilder(api, p);
|
||||
const overwriteMerge = (destinationArray, sourceArray/* , options*/) => sourceArray;
|
||||
graph = deepmerge(graph, result, { arrayMerge: overwriteMerge });
|
||||
}
|
||||
if (!graph || Object.getOwnPropertyNames(graph).length === 0) {
|
||||
throw new Error(`Successfully processed ${paths.length} configuration graph packages or directories, yet the resulting graph object did not have properties. This is likely an error or issue that should be corrected. Or, alternatively, use options.graph as an input to the resolver.`);
|
||||
}
|
||||
return graph;
|
||||
}
|
||||
|
||||
function addConfigPackages(paths, applicationRoot, painlessConfigObjects) {
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
'use strict';
|
||||
|
||||
const appRoot = require('app-root-path');
|
||||
const painlessConfig = require('painless-config');
|
||||
const path = require('path');
|
||||
|
||||
let unconfigured = null;
|
||||
|
||||
function objectProvider(json, applicationName) {
|
||||
const appKey = applicationName ? `app:${applicationName}` : null;
|
||||
return {
|
||||
get: function get(key) {
|
||||
if (json && json[appKey] && json[appKey][key]) {
|
||||
return json[appKey][key];
|
||||
}
|
||||
return json[key];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function configurePackageEnvironments(providers, environmentModules, environment, appName) {
|
||||
let environmentInstances = [];
|
||||
for (let i = 0; i < environmentModules.length; i++) {
|
||||
// CONSIDER: Should the name strip any @ after the first slash, in case it is a version-appended version?
|
||||
const npmName = environmentModules[i].trim();
|
||||
if (!npmName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let environmentPackage = null;
|
||||
try {
|
||||
environmentPackage = require(npmName);
|
||||
} catch (packageRequireError) {
|
||||
const packageMissing = new Error(`Unable to require the "${npmName}" environment package for the "${environment}" environment`);
|
||||
packageMissing.innerError = packageRequireError;
|
||||
throw packageMissing;
|
||||
}
|
||||
if (!environmentPackage) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let values = null;
|
||||
if (typeof(environmentPackage) === 'function') {
|
||||
environmentInstances.push(environmentPackage);
|
||||
try {
|
||||
values = environmentPackage(environment);
|
||||
} catch (problemCalling) {
|
||||
const asText = problemCalling.toString();
|
||||
const error = new Error(`While calling the environment package "${npmName}" for the "${environment}" environment an error was thrown: ${asText}`);
|
||||
error.innerError = problemCalling;
|
||||
throw error;
|
||||
}
|
||||
} else if (typeof(environmentPackage) === 'object') {
|
||||
values = environmentPackage;
|
||||
}
|
||||
|
||||
if (!values) {
|
||||
throw new Error(`Could not determine what to do with the environment package "${npmName}" for the "${environment}" environment (no values or unexpected type)`);
|
||||
}
|
||||
providers.push(objectProvider(values, appName));
|
||||
|
||||
return environmentInstances;
|
||||
}
|
||||
}
|
||||
|
||||
function configureLocalEnvironment(providers, appRoot, directoryName, environment, applicationName) {
|
||||
const envFile = `${environment}.json`;
|
||||
const envPath = path.join(appRoot, directoryName, envFile);
|
||||
try {
|
||||
const json = require(envPath);
|
||||
providers.push(objectProvider(json, applicationName));
|
||||
} catch (noFile) {
|
||||
// no file
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetPackage(appRoot) {
|
||||
try {
|
||||
const packagePath = path.join(appRoot, 'package.json');
|
||||
const pkg = require(packagePath);
|
||||
return pkg;
|
||||
} catch (noPackage) {
|
||||
// If there is no package.json for the app, well, that's OK
|
||||
}
|
||||
}
|
||||
|
||||
function initialize(options) {
|
||||
options = options || {};
|
||||
const applicationRoot = options.applicationRoot || appRoot;
|
||||
const applicationName = options.applicationName || undefined;
|
||||
const provider = options.provider || painlessConfig;
|
||||
let environmentInstances = null;
|
||||
|
||||
let configurationEnvironmentKeyNames = (provider.get('CONFIGURATION_ENVIRONMENT_KEYS') || 'CONFIGURATION_ENVIRONMENT,NODE_ENV').split(',');
|
||||
if (!configurationEnvironmentKeyNames || configurationEnvironmentKeyNames.length === 0) {
|
||||
throw new Error('No configuration environment key name(s) defined');
|
||||
}
|
||||
|
||||
let environment = null;
|
||||
for (let i = 0; !environment && i < configurationEnvironmentKeyNames.length; i++) {
|
||||
environment = provider.get(configurationEnvironmentKeyNames[i]);
|
||||
}
|
||||
if (!environment) {
|
||||
return provider;
|
||||
}
|
||||
|
||||
const providers = [
|
||||
provider,
|
||||
];
|
||||
|
||||
if (provider.testConfiguration) {
|
||||
providers.push(objectProvider(provider.testConfiguration[environment], applicationName));
|
||||
} else {
|
||||
const appRoot = applicationRoot.toString();
|
||||
const pkg = tryGetPackage(appRoot);
|
||||
const appName = applicationName || (pkg && pkg.painlessConfigApplicationName ? pkg.painlessConfigApplicationName : undefined);
|
||||
|
||||
const environmentDirectoryKey = provider.get('ENVIRONMENT_DIRECTORY_KEY') || 'ENVIRONMENT_DIRECTORY';
|
||||
const directoryName = options.directoryName || provider.get(environmentDirectoryKey) || 'env';
|
||||
configureLocalEnvironment(providers, appRoot, directoryName, environment, appName);
|
||||
|
||||
const environmentModulesKey = provider.get('ENVIRONMENT_MODULES_KEY') || 'ENVIRONMENT_MODULES';
|
||||
const environmentModules = (provider.get(environmentModulesKey) || '').split(',');
|
||||
let painlessConfigEnvironments = pkg ? pkg.painlessConfigEnvironments : null;
|
||||
if (painlessConfigEnvironments) {
|
||||
if (Array.isArray(painlessConfigEnvironments)) {
|
||||
// This is ready-to-use as-is
|
||||
} else if (painlessConfigEnvironments.split) {
|
||||
painlessConfigEnvironments = painlessConfigEnvironments.split(',');
|
||||
} else {
|
||||
throw new Error('Unknown how to process the painlessConfigEnvironments values in package.json');
|
||||
}
|
||||
environmentModules.push(...painlessConfigEnvironments);
|
||||
}
|
||||
environmentInstances = configurePackageEnvironments(providers, environmentModules, environment, appName);
|
||||
}
|
||||
|
||||
return {
|
||||
environmentInstances: environmentInstances,
|
||||
get: function (key) {
|
||||
for (let i = 0; i < providers.length; i++) {
|
||||
const value = providers[i].get(key);
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
initialize.get = function getWithoutInitialize(key) {
|
||||
if (!unconfigured) {
|
||||
unconfigured = initialize();
|
||||
}
|
||||
return unconfigured.get(key);
|
||||
};
|
||||
|
||||
module.exports = initialize;
|
|
@ -1,24 +1,24 @@
|
|||
{
|
||||
"name": "painless-config-resolver",
|
||||
"version": "1.1.3",
|
||||
"version": "2.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "8.10.51",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.51.tgz",
|
||||
"integrity": "sha512-cArrlJp3Yv6IyFT/DYe+rlO8o3SIHraALbBW/+CcCYW/a9QucpLI+n2p4sRxAvl2O35TiecpX2heSZtJjvEO+Q=="
|
||||
"version": "8.10.60",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.60.tgz",
|
||||
"integrity": "sha512-YjPbypHFuiOV0bTgeF07HpEEqhmHaZqYNSdCKeBJa+yFoQ/7BC+FpJcwmi34xUIIRVFktnUyP1dPU8U0612GOg=="
|
||||
},
|
||||
"adal-node": {
|
||||
"version": "0.1.28",
|
||||
"resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz",
|
||||
"integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.2.0.tgz",
|
||||
"integrity": "sha512-DzkdOpBbOnqErw2TWT0PFA5gsKgmxIfdr/ZgL+if8+ln/tz8JAkk/MtUhH3ftnInrcJVsnR7re4UhpR+KDRnTw==",
|
||||
"requires": {
|
||||
"@types/node": "^8.0.47",
|
||||
"async": ">=0.6.0",
|
||||
"async": "^2.6.3",
|
||||
"date-utils": "*",
|
||||
"jws": "3.x.x",
|
||||
"request": ">= 2.52.0",
|
||||
"request": "^2.88.0",
|
||||
"underscore": ">= 1.3.1",
|
||||
"uuid": "^3.1.0",
|
||||
"xmldom": ">= 0.1.x",
|
||||
|
@ -26,20 +26,20 @@
|
|||
}
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.10.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
|
||||
"integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
|
||||
"version": "6.12.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
|
||||
"integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"app-root-path": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz",
|
||||
"integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA=="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
|
||||
"integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw=="
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
|
@ -55,9 +55,12 @@
|
|||
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
|
||||
},
|
||||
"async": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.1.0.tgz",
|
||||
"integrity": "sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ=="
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
|
||||
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
|
@ -70,17 +73,17 @@
|
|||
"integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
|
||||
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
|
||||
},
|
||||
"azure-keyvault": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/azure-keyvault/-/azure-keyvault-3.0.4.tgz",
|
||||
"integrity": "sha1-t3M9j1jZmmb5rnZkUVVus7BY2uU=",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/azure-keyvault/-/azure-keyvault-3.0.5.tgz",
|
||||
"integrity": "sha512-59fzKRq9dnzv03lEuImvgXc3QjRJoSJtK0gv1WXoqCivBuPdFNK+x6hAjoEDS2WEOXG+7m3uiJWqpMh/8NW3ow==",
|
||||
"requires": {
|
||||
"ms-rest": "^2.3.2",
|
||||
"ms-rest-azure": "^2.5.5"
|
||||
"ms-rest": "^2.5.0",
|
||||
"ms-rest-azure": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
|
@ -128,9 +131,9 @@
|
|||
"integrity": "sha1-YfsWzcEnSzyayq/+n8ad+HIKK2Q="
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.0.0.tgz",
|
||||
"integrity": "sha512-YZ1rOP5+kHor4hMAH+HRQnBQHg+wvS1un1hAOuIcxcBy0hzcUf6Jg2a1w65kpoOUnurOfZbERwjI1TfZxNjcww=="
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
|
||||
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
|
@ -159,21 +162,6 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"environment-configuration-resolver": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/environment-configuration-resolver/-/environment-configuration-resolver-0.1.2.tgz",
|
||||
"integrity": "sha512-oc64INP1mj94ytfTqexshn8fA2/BJmLcoWWr6LOqlb+seHi1pGxzX4pX9zDM/LuiI5TF9Z8MVNmUnDnDgnJv6A==",
|
||||
"requires": {
|
||||
"object-path": "0.11.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"object-path": {
|
||||
"version": "0.11.3",
|
||||
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.3.tgz",
|
||||
"integrity": "sha1-PiGkKtByNNgVQprp4VwcXzgFBVQ="
|
||||
}
|
||||
}
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
|
@ -185,14 +173,14 @@
|
|||
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
|
||||
"integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
|
||||
},
|
||||
"fast-json-stable-stringify": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
|
||||
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"forever-agent": {
|
||||
"version": "0.6.1",
|
||||
|
@ -311,33 +299,22 @@
|
|||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"keyvault-configuration-resolver": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/keyvault-configuration-resolver/-/keyvault-configuration-resolver-1.0.4.tgz",
|
||||
"integrity": "sha512-rdDudoEDC6AygefHOSra8y2pa40bJUM9Li1NeSyLiEgwIHLE72XKr3n8NUeW3Rxkr0GluN4yTnWU0hm1LrNG6A==",
|
||||
"requires": {
|
||||
"adal-node": "0.1.28",
|
||||
"async": "3.1.0",
|
||||
"azure-keyvault": "3.0.4",
|
||||
"object-path": "0.11.4"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
|
||||
"version": "1.43.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz",
|
||||
"integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"version": "2.1.26",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz",
|
||||
"integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==",
|
||||
"requires": {
|
||||
"mime-db": "1.40.0"
|
||||
"mime-db": "1.43.0"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
|
@ -346,9 +323,9 @@
|
|||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||
},
|
||||
"ms-rest": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/ms-rest/-/ms-rest-2.5.3.tgz",
|
||||
"integrity": "sha512-p0CnzrTzEkS8UTEwgCqT2O5YVK9E8KGBBlJVm3hFtMZvf0dmncKYXWFPyUa4PAsfBL7h4jfu39tOIFTu6exntg==",
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/ms-rest/-/ms-rest-2.5.4.tgz",
|
||||
"integrity": "sha512-VeqCbawxRM6nhw0RKNfj7TWL7SL8PB6MypqwgylXCi+u412uvYoyY/kSmO8n06wyd8nIcnTbYToCmSKFMI1mCg==",
|
||||
"requires": {
|
||||
"duplexer": "^0.1.1",
|
||||
"is-buffer": "^1.1.6",
|
||||
|
@ -373,6 +350,22 @@
|
|||
"uuid": "^3.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"adal-node": {
|
||||
"version": "0.1.28",
|
||||
"resolved": "https://registry.npmjs.org/adal-node/-/adal-node-0.1.28.tgz",
|
||||
"integrity": "sha1-RoxLs+u9lrEnBmn0ucuk4AZepIU=",
|
||||
"requires": {
|
||||
"@types/node": "^8.0.47",
|
||||
"async": ">=0.6.0",
|
||||
"date-utils": "*",
|
||||
"jws": "3.x.x",
|
||||
"request": ">= 2.52.0",
|
||||
"underscore": ">= 1.3.1",
|
||||
"uuid": "^3.1.0",
|
||||
"xmldom": ">= 0.1.x",
|
||||
"xpath.js": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
|
||||
|
@ -409,24 +402,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"painless-config-as-code": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/painless-config-as-code/-/painless-config-as-code-1.0.0.tgz",
|
||||
"integrity": "sha512-UK6QqFoV712fLmkxveRc+UtHVBOFCjX2lXWveXnzGokzlDaOzLfZ3cw1/UrmnkbDQYNo1eO2OivVM2cV0egGyw==",
|
||||
"requires": {
|
||||
"app-root-path": "^2.0.1",
|
||||
"painless-config": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz",
|
||||
"integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag=="
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
|
||||
"integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ=="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
|
@ -439,9 +423,9 @@
|
|||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
},
|
||||
"request": {
|
||||
"version": "2.88.0",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
|
||||
"integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
|
||||
"version": "2.88.2",
|
||||
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
|
||||
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
|
@ -450,7 +434,7 @@
|
|||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.0",
|
||||
"har-validator": "~5.1.3",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
|
@ -460,7 +444,7 @@
|
|||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.4.3",
|
||||
"tough-cookie": "~2.5.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
}
|
||||
|
@ -497,19 +481,12 @@
|
|||
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
|
||||
"integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"requires": {
|
||||
"psl": "^1.1.24",
|
||||
"punycode": "^1.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
}
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
|
@ -531,9 +508,9 @@
|
|||
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
|
||||
"integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg=="
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz",
|
||||
"integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg=="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.2.2",
|
||||
|
@ -544,9 +521,9 @@
|
|||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
},
|
||||
"verror": {
|
||||
"version": "1.10.0",
|
||||
|
@ -564,9 +541,9 @@
|
|||
"integrity": "sha1-DfjRGOMrSyhORDp50jCwBmksrnU="
|
||||
},
|
||||
"xmldom": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
|
||||
"integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk="
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.3.0.tgz",
|
||||
"integrity": "sha512-z9s6k3wxE+aZHgXYxSTpGDo7BYOUfJsIRyoZiX6HTjwpwfS2wpQBQKa2fD+ShLyPkqDYo5ud7KitmLZ2Cd6r0g=="
|
||||
},
|
||||
"xpath.js": {
|
||||
"version": "1.1.0",
|
||||
|
|
17
package.json
17
package.json
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "painless-config-resolver",
|
||||
"version": "1.1.4",
|
||||
"version": "2.0.1",
|
||||
"description": "Yet another opinionated Node.js configuration library providing a set of default resolvers to enable rapid, rich configuration object graphs powered by the deployment environment, config-as-code, and Azure KeyVault secrets.",
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": "~6.9.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no tests for this project\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"config",
|
||||
|
@ -31,12 +31,11 @@
|
|||
"license": "MIT",
|
||||
"repository": "Microsoft/painless-config-resolver",
|
||||
"dependencies": {
|
||||
"app-root-path": "2.2.1",
|
||||
"async": "3.1.0",
|
||||
"deepmerge": "4.0.0",
|
||||
"environment-configuration-resolver": "0.1.2",
|
||||
"keyvault-configuration-resolver": "1.0.4",
|
||||
"adal-node": "0.2.0",
|
||||
"app-root-path": "3.0.0",
|
||||
"azure-keyvault": "3.0.5",
|
||||
"deepmerge": "4.2.2",
|
||||
"object-path": "0.11.4",
|
||||
"painless-config-as-code": "1.0.0"
|
||||
"painless-config": "0.1.1"
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче