diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 09e47bb..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -test/results -node_modules diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 0b94b7e..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - "extends": "airbnb-base", - "plugins": [ - "chai-expect" - ], - "rules": { - "func-names": "off", - - // doesn't work in node v4 :( - "strict": "off", - "prefer-rest-params": "off", - "react/require-extension": "off", - "import/no-extraneous-dependencies": "off", - "class-methods-use-this": "off", - "eqeqeq": "off" - }, - "env": { - "mocha": true - } -}; \ No newline at end of file diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 0000000..b69221d --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,11 @@ +env: + mocha: true + node: true + es6: true +extends: + - eslint:recommended + - prettier +parserOptions: + ecmaVersion: 10 +plugins: + - prettier diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8274ab5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + ignore: + - dependency-name: "aws-sdk" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..bc57dcd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node: ['10', '12', '14'] + name: node-${{ matrix.node }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2-beta + with: + node-version: ${{ matrix.node }} + - run: npm ci + - run: npm run test diff --git a/.gitignore b/.gitignore index 61910da..fac8d8e 100755 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,3 @@ node_modules #OS STUFF .DS_Store .tmp - - -#Test results folder -test/results \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/.travis.yml b/.travis.yml deleted file mode 100755 index 5dd6bd0..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: node_js - -node_js: - - "node" - - "8" - - "6" -sudo: false - -install: - - travis_retry npm install - -after_success: - - cat ./test/results/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/LICENSE.txt b/LICENSE.txt index 5862fb7..23143f4 100755 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,8 +1,22 @@ -The MIT License (MIT) -Copyright (c) 2016 Vivint Solar +(The MIT License) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2012-present -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index f55fb8e..71a858d 100755 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -nodecredstash -============= +# aws-credstash -[![Build Status](https://travis-ci.org/DavidTanner/nodecredstash.svg?branch=master)](https://travis-ci.org/DavidTanner/nodecredstash) -[![Coverage Status](https://coveralls.io/repos/github/DavidTanner/nodecredstash/badge.svg?branch=master)](https://coveralls.io/github/DavidTanner/nodecredstash?branch=master) -[![npm version](https://badge.fury.io/js/nodecredstash.svg)](https://badge.fury.io/js/nodecredstash) -[![dependencies](https://img.shields.io/david/DavidTanner%2Fnodecredstash.svg)](https://www.npmjs.com/package/nodecredstash) +Node.js port of [credstash](https://github.com/fugue/credstash) -[Node.js](https://nodejs.org/en/) port of [credstash](https://github.com/fugue/credstash) +Fork of [nodecredstash](https://www.npmjs.com/package/nodecredstash), largely unmodified. -============= +Main changes: +- Added requirement of >= Node 10 +- Copied `@types/nodecredstash` into this module +- Moved `aws-sdk` to a peerdependency: the api of the module remains unchanged, and you should be able to swap nodecredstash out for this without anything breaking. + +## Installation $ npm install --save nodecredstash @@ -23,9 +24,9 @@ credstash.putSecret({name: 'Death Star vulnerability', secret: 'Exhaust vent', v ``` -Options -======= +## API +### Options table ----- @@ -64,8 +65,7 @@ Options that are specific to the KMS configuration. Defaults can still be assig kms here - -Function arguments +Arguments ================== name @@ -99,7 +99,7 @@ credstash.getSecret({ }) ``` -Functions +API ========= .createDdbTable([cb]) @@ -250,7 +250,3 @@ credstash.deleteSecrets({name: 'Death Star vulnerability'}) ```js [] ``` - - - - diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..77d1db1 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,69 @@ +// Type definitions for nodecredstash 2.0 +// Project: https://github.com/DavidTanner/nodecredstash +// Definitions by: Mike Cook +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/// +import * as AWS from "aws-sdk"; + +interface CredstashConfig { + table?: string; + awsOpts?: AWS.KMS.ClientConfiguration; + dynamoOpts?: AWS.DynamoDB.ClientConfiguration; + kmsKey?: string; + kmsOpts?: AWS.KMS.ClientConfiguration; +} + +interface CredstashContext { + [key: string]: string; +} + +interface PutSecretOptions { + name: string; + secret: string; + context: CredstashContext; + digest?: string; + version?: number; +} + +interface Credstash { + getHighestVersion: (options: { + name: string; + }) => Promise; + incrementVersion: (options: { name: string }) => Promise; + putSecret: ( + options: PutSecretOptions + ) => Promise; + decryptStash: ( + stash: { key: string }, + context?: CredstashContext + ) => Promise; + getAllVersions: (options: { + name: string; + context?: CredstashContext; + limit?: number; + }) => Promise>; + getSecret: (options: { + name: string; + context?: CredstashContext; + version?: number; + }) => Promise; + deleteSecrets: (options: { + name: string; + }) => Promise; + deleteSecret: (options: { + name: string; + version: number; + }) => Promise; + listSecrets: () => Promise; + getAllSecrets: (options: { + version?: number; + context?: CredstashContext; + startsWith?: string; + }) => Promise<{ [key: string]: string }>; + createDdbTable: () => Promise; +} + +declare function Credstash(config: CredstashConfig): Credstash; + +export = Credstash; diff --git a/js/index.js b/index.js similarity index 50% rename from js/index.js rename to index.js index 0ef6f2e..c09f5ab 100755 --- a/js/index.js +++ b/index.js @@ -1,16 +1,14 @@ -'use strict'; +"use strict"; -const debug = require('debug')('credstash'); +const debug = require("debug")("credstash"); -const DynamoDB = require('./lib/dynamoDb'); -const KMS = require('./lib/kms'); - - -const encrypter = require('./lib/encrypter'); -const decrypter = require('./lib/decrypter'); -const defaults = require('./defaults'); -const utils = require('./lib/utils'); +const DynamoDB = require("./lib/dynamoDb"); +const KMS = require("./lib/kms"); +const encrypter = require("./lib/encrypter"); +const decrypter = require("./lib/decrypter"); +const defaults = require("./lib/defaults"); +const utils = require("./lib/utils"); module.exports = function (mainConfig) { const config = Object.assign({}, mainConfig); @@ -23,7 +21,6 @@ module.exports = function (mainConfig) { const kmsOpts = Object.assign({}, config.awsOpts, config.kmsOpts); const kms = new KMS(kmsKey, kmsOpts); - class Credstash { constructor() { const credstash = this; @@ -33,10 +30,11 @@ module.exports = function (mainConfig) { const args = Array.from(arguments); const lastArg = args.slice(-1)[0]; let cb; - if (typeof lastArg === 'function') { + if (typeof lastArg === "function") { cb = args.pop(); } - return method.apply(credstash, args) + return method + .apply(credstash, args) .then((res) => { if (cb) { return cb(undefined, res); @@ -77,19 +75,18 @@ module.exports = function (mainConfig) { /** * Retrieve the highest version of `name` in the table * - * @param opts - * @returns {Promise.} */ getHighestVersion(opts) { const options = Object.assign({}, opts); const { name } = options; if (!name) { - return Promise.reject(new Error('name is a required parameter')); + return Promise.reject(new Error("name is a required parameter")); } - return ddb.getLatestVersion(name) - .then(res => res.Items[0]) + return ddb + .getLatestVersion(name) + .then((res) => res.Items[0]) .then((res) => { const { version = 0 } = res || {}; return version; @@ -102,9 +99,11 @@ module.exports = function (mainConfig) { if (Number.parseInt(version, 10) == version) { return Number.parseInt(version, 10); } - throw new Error(`Can not autoincrement version. The current version: ${version} is not an int`); + throw new Error( + `Can not autoincrement version. The current version: ${version} is not an int` + ); }) - .then(version => utils.paddedInt(defaults.PAD_LEN, version + 1)); + .then((version) => utils.paddedInt(defaults.PAD_LEN, version + 1)); } putSecret(opts) { @@ -116,28 +115,37 @@ module.exports = function (mainConfig) { digest = defaults.DEFAULT_DIGEST, } = options; if (!name) { - return Promise.reject(new Error('name is a required parameter')); + return Promise.reject(new Error("name is a required parameter")); } if (!secret) { - return Promise.reject(new Error('secret is a required parameter')); + return Promise.reject(new Error("secret is a required parameter")); } const version = utils.sanitizeVersion(options.version, 1); // optional - return kms.getEncryptionKey(context) + return kms + .getEncryptionKey(context) .catch((err) => { - if (err.code == 'NotFoundException') { + if (err.code == "NotFoundException") { throw err; } - throw new Error(`Could not generate key using KMS key ${kmsKey}, error:${JSON.stringify(err, null, 2)}`); + throw new Error( + `Could not generate key using KMS key ${kmsKey}, error:${JSON.stringify( + err, + null, + 2 + )}` + ); }) - .then(kmsData => encrypter.encrypt(digest, secret, kmsData)) - .then(data => Object.assign({ name, version }, data)) - .then(data => ddb.createSecret(data)) + .then((kmsData) => encrypter.encrypt(digest, secret, kmsData)) + .then((data) => Object.assign({ name, version }, data)) + .then((data) => ddb.createSecret(data)) .catch((err) => { - if (err.code == 'ConditionalCheckFailedException') { - throw new Error(`${name} version ${version} is already in the credential store.`); + if (err.code == "ConditionalCheckFailedException") { + throw new Error( + `${name} version ${version} is already in the credential store.` + ); } else { throw err; } @@ -145,23 +153,24 @@ module.exports = function (mainConfig) { } decryptStash(stash, context) { const key = utils.b64decode(stash.key); - return kms.decrypt(key, context) - .catch((err) => { - let msg = `Decryption error: ${JSON.stringify(err, null, 2)}`; - - if (err.code == 'InvalidCiphertextException') { - if (context) { - msg = 'Could not decrypt hmac key with KMS. The encryption ' + - 'context provided may not match the one used when the ' + - 'credential was stored.'; - } else { - msg = 'Could not decrypt hmac key with KMS. The credential may ' + - 'require that an encryption context be provided to decrypt ' + - 'it.'; - } + return kms.decrypt(key, context).catch((err) => { + let msg = `Decryption error: ${JSON.stringify(err, null, 2)}`; + + if (err.code == "InvalidCiphertextException") { + if (context) { + msg = + "Could not decrypt hmac key with KMS. The encryption " + + "context provided may not match the one used when the " + + "credential was stored."; + } else { + msg = + "Could not decrypt hmac key with KMS. The credential may " + + "require that an encryption context be provided to decrypt " + + "it."; } - throw new Error(msg); - }); + } + throw new Error(msg); + }); } getAllVersions(opts) { @@ -173,96 +182,93 @@ module.exports = function (mainConfig) { } = options; if (!name) { - return Promise.reject(new Error('name is a required parameter')); + return Promise.reject(new Error("name is a required parameter")); } - return ddb.getAllVersions(name, { limit }) + return ddb + .getAllVersions(name, { limit }) .then((results) => { - const dataKeyPromises = results.Items.map(stash => - this.decryptStash(stash, context) - .then(decryptedDataKey => - Object.assign(stash, { decryptedDataKey }))); + const dataKeyPromises = results.Items.map((stash) => + this.decryptStash(stash, context).then((decryptedDataKey) => + Object.assign(stash, { decryptedDataKey }) + ) + ); return Promise.all(dataKeyPromises); - }).then(stashes => - stashes.map(stash => ({ + }) + .then((stashes) => + stashes.map((stash) => ({ version: stash.version, secret: decrypter.decrypt(stash, stash.decryptedDataKey), - }))); + })) + ); } getSecret(opts) { const options = Object.assign({}, opts); - const { - name, - context, - } = options; + const { name, context } = options; if (!name) { - return Promise.reject(new Error('name is a required parameter')); + return Promise.reject(new Error("name is a required parameter")); } const version = utils.sanitizeVersion(options.version); // optional - const func = version == undefined ? - ddb.getLatestVersion(name).then(res => res.Items[0]) : - ddb.getByVersion(name, version).then(res => res.Item); + const func = + version == undefined + ? ddb.getLatestVersion(name).then((res) => res.Items[0]) + : ddb.getByVersion(name, version).then((res) => res.Item); return func .then((stash) => { if (!stash || !stash.key) { throw new Error(`Item {'name': '${name}'} could not be found.`); } - return Promise.all([ - stash, - this.decryptStash(stash, context), - ]); + return Promise.all([stash, this.decryptStash(stash, context)]); }) - .then(res => decrypter.decrypt(res[0], res[1])); + .then((res) => decrypter.decrypt(res[0], res[1])); } deleteSecrets(opts) { const options = Object.assign({}, opts); - const { - name, - } = options; + const { name } = options; if (!name) { - return Promise.reject(new Error('name is a required parameter')); + return Promise.reject(new Error("name is a required parameter")); } - return ddb.getAllVersions(name) - .then(res => res.Items) - .then(secrets => utils.mapPromise(secrets, secret => this.deleteSecret({ - name: secret.name, - version: secret.version, - }))); + return ddb + .getAllVersions(name) + .then((res) => res.Items) + .then((secrets) => + utils.mapPromise(secrets, (secret) => + this.deleteSecret({ + name: secret.name, + version: secret.version, + }) + ) + ); } deleteSecret(opts) { const options = Object.assign({}, opts); - const { - name, - } = options; + const { name } = options; if (!name) { - return Promise.reject(new Error('name is a required parameter')); + return Promise.reject(new Error("name is a required parameter")); } const version = utils.sanitizeVersion(options.version); if (!version) { - return Promise.reject(new Error('version is a required parameter')); + return Promise.reject(new Error("version is a required parameter")); } debug(`Deleting ${name} -- version ${version}`); return ddb.deleteSecret(name, version); } listSecrets() { - return ddb.getAllSecretsAndVersions() - .then(res => res.Items.sort(utils.sortSecrets)); + return ddb + .getAllSecretsAndVersions() + .then((res) => res.Items.sort(utils.sortSecrets)); } getAllSecrets(opts) { const options = Object.assign({}, opts); - const { - version, - context, - startsWith, - } = options; + const { version, context, startsWith } = options; const unOrdered = {}; return this.listSecrets() @@ -270,27 +276,38 @@ module.exports = function (mainConfig) { const position = {}; const filtered = []; secrets - .filter(secret => secret.version == (version || secret.version)) - .filter(secret => !startsWith || secret.name.startsWith(startsWith)) + .filter((secret) => secret.version == (version || secret.version)) + .filter( + (secret) => !startsWith || secret.name.startsWith(startsWith) + ) .forEach((next) => { - position[next.name] = position[next.name] ? - position[next.name] : filtered.push(next); + position[next.name] = position[next.name] + ? position[next.name] + : filtered.push(next); }); return filtered; }) - .then(secrets => - utils.mapPromise(secrets, secret => - this.getSecret({ name: secret.name, version: secret.version, context }) + .then((secrets) => + utils.mapPromise(secrets, (secret) => + this.getSecret({ + name: secret.name, + version: secret.version, + context, + }) .then((plainText) => { unOrdered[secret.name] = plainText; }) - .catch(() => undefined))) + .catch(() => undefined) + ) + ) .then(() => { const ordered = {}; - Object.keys(unOrdered).sort().forEach((key) => { - ordered[key] = unOrdered[key]; - }); + Object.keys(unOrdered) + .sort() + .forEach((key) => { + ordered[key] = unOrdered[key]; + }); return ordered; }); } diff --git a/js/defaults.js b/js/defaults.js deleted file mode 100755 index 467476a..0000000 --- a/js/defaults.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = { - DEFAULT_REGION: 'us-east-1', - DEFAULT_TABLE: 'credential-store', - DEFAULT_KMS_KEY: 'alias/credstash', - PAD_LEN: 19, - DEFAULT_DIGEST: 'SHA256', -}; diff --git a/js/lib/test/dynamo.spec.js b/js/lib/test/dynamo.spec.js deleted file mode 100755 index f27bd4c..0000000 --- a/js/lib/test/dynamo.spec.js +++ /dev/null @@ -1,306 +0,0 @@ -'use strict'; - -/* eslint-disable no-unused-expressions, no-undef */ - -require('../../test/setup'); - -const _ = require('lodash'); - -const DynamoDb = require('../dynamoDb'); - -const AWS = require('aws-sdk-mock'); - -function findKeyIndex(items, keys) { - const index = items.findIndex((item) => { - let matches = true; - _.forEach(keys, (value, key) => { - matches = matches && item[key] == value; - }); - return matches; - }); - return index; -} - -function sliceItems(items, params) { - const limit = params.Limit || items.length; - let startIndex = 0; - - if (params.ExclusiveStartKey) { - startIndex = findKeyIndex(items, params.ExclusiveStartKey) + 1; - } - - const Items = items.slice(startIndex, startIndex + limit); - - const lastIndex = (startIndex + limit) - 1; - let LastEvaluatedKey; - - const last = items[lastIndex]; - if (lastIndex < (items.length - 1) && last) { - LastEvaluatedKey = { name: last.name, version: last.version }; - } - - const Count = Items.length; - const ScannedCount = Count; - - const results = { - LastEvaluatedKey, - Items, - ScannedCount, - Count, - }; - return results; -} - -function compareParams(actual, expected) { - if (expected.TableName) { - actual.TableName.should.eql(expected.TableName); - } - if (expected.ExpressionAttributeNames) { - actual.ExpressionAttributeNames.should.eql(expected.ExpressionAttributeNames); - } - if (expected.KeyConditionExpression) { - actual.KeyConditionExpression.should.eql(expected.KeyConditionExpression); - } - - if (expected.ProjectionExpression) { - actual.ProjectionExpression.should.eql(expected.ProjectionExpression); - } - - if (expected.Limit) { - expect(actual.Limit).to.exist; - actual.Limit.should.eql(expected.Limit); - } - - if (expected.ExpressionAttributeValues) { - actual.ExpressionAttributeValues.should.eql(expected.ExpressionAttributeValues); - } -} - -function mockQueryScan(error, items, expectedParams) { - function fn(params, done) { - compareParams(params, expectedParams); - const results = sliceItems(items, params); - - done(error, results); - } - - AWS.mock('DynamoDB.DocumentClient', 'query', fn); - - AWS.mock('DynamoDB.DocumentClient', 'scan', fn); -} - - -describe('dynmaodDb', () => { - let dynamo; - let items; - const TableName = 'credentials-store'; - - beforeEach(() => { - AWS.restore(); - items = Array.from({ length: 30 }, (v, i) => ({ name: i, version: i })); - }); - - afterEach(() => { - AWS.restore(); - }); - - describe('#getAllSecretsAndVersions', () => { - it('should properly page through many results', () => { - mockQueryScan(undefined, items, { - Limit: 10, - TableName, - }); - - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.getAllSecretsAndVersions({ limit: 10 }) - .then(res => res.Items) - .then((secrets) => { - secrets.length.should.be.equal(items.length); - secrets.should.eql(items); - }); - }); - }); - - describe('#getAllVersions', () => { - it('should properly page through many results', () => { - mockQueryScan(undefined, items, { - Limit: 10, - TableName, - }); - - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.getAllVersions('', { limit: 10 }) - .then(res => res.Items) - .then((secrets) => { - secrets.length.should.be.equal(items.length); - secrets.should.eql(items); - }); - }); - }); - - describe('#getLatestVersion', () => { - it('should only get one item back', () => { - mockQueryScan(undefined, items, { - Limit: 1, - TableName, - }); - - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.getLatestVersion('') - .then((res) => { - expect(res).to.exist; - expect(res.Items).to.exist; - expect(res.Items[0]).to.exist; - res.Items[0].should.equal(items[0]); - }); - }); - }); - - - describe('#getByVersion', () => { - it('should only get one item back', () => { - const name = 'name'; - const version = 'version'; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - params.TableName.should.equal(TableName); - expect(params.Key).to.exist; - params.Key.name.should.equal(name); - params.Key.version.should.equal(version); - cb(undefined, { Item: 'Success' }); - }); - - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.getByVersion(name, version) - .then((res) => { - expect(res).to.exist; - res.Item.should.equal('Success'); - }); - }); - }); - - describe('#createSecret', () => { - it('should create an item in DynamoDB', () => { - const item = items[0]; - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => { - params.TableName.should.equal(TableName); - expect(params.ConditionExpression).to.exist; - params.ConditionExpression.should.equal('attribute_not_exists(#name)'); - expect(params.ExpressionAttributeNames).to.exist; - params.ExpressionAttributeNames.should.deep.equal({ - '#name': 'name', - }); - params.Item.should.deep.equal(item); - cb(undefined, 'Success'); - }); - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.createSecret(item) - .then(res => res.should.equal('Success')); - }); - }); - - - describe('#deleteSecret', () => { - it('should delete the secret by name and version', () => { - const name = 'name'; - const version = 'version'; - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => { - params.TableName.should.equal(TableName); - expect(params.Key).to.exist; - params.Key.name.should.equal(name); - params.Key.version.should.equal(version); - cb(undefined, 'Success'); - }); - - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.deleteSecret(name, version) - .then((secret) => { - expect(secret).to.exist; - secret.should.equal('Success'); - }); - }); - }); - - describe('#createTable', () => { - it('should create the table with the HASH as name and RANGE as version', function () { - this.timeout(5e3); - AWS.mock('DynamoDB', 'describeTable', (params, cb) => cb({ code: 'ResourceNotFoundException' })); - AWS.mock('DynamoDB', 'createTable', (params, cb) => { - expect(params.TableName).to.exist; - params.TableName.should.equal(TableName); - expect(params.KeySchema).to.exist; - expect(params.KeySchema.find).to.exist; - params.KeySchema.length.should.equal(2); - - const hash = params.KeySchema.find(next => next.KeyType == 'HASH'); - expect(hash).to.exist; - hash.should.deep.equal({ - AttributeName: 'name', - KeyType: 'HASH', - }); - const range = params.KeySchema.find(next => next.KeyType == 'RANGE'); - expect(range).to.exist; - range.should.deep.equal({ - AttributeName: 'version', - KeyType: 'RANGE', - }); - expect(params.AttributeDefinitions).to.exist; - expect(params.AttributeDefinitions.find).to.exist; - params.AttributeDefinitions.length.should.equal(2); - const name = params.AttributeDefinitions.find(next => next.AttributeName == 'name'); - expect(name).to.exist; - name.should.deep.equal({ - AttributeName: 'name', - AttributeType: 'S', - }); - const version = params.AttributeDefinitions.find(next => next.AttributeName == 'version'); - expect(version).to.exist; - version.should.deep.equal({ - AttributeName: 'version', - AttributeType: 'S', - }); - expect(params.ProvisionedThroughput).to.exist; - params.ProvisionedThroughput.should.deep.equal({ - ReadCapacityUnits: 1, - WriteCapacityUnits: 1, - }); - cb(); - }); - AWS.mock('DynamoDB', 'waitFor', (status, params, cb) => { - expect(status).to.exist; - status.should.equal('tableExists'); - expect(params).to.exist; - expect(params.TableName).to.exist; - params.TableName.should.equal(TableName); - cb(); - }); - - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.createTable(); - }); - - it('should not create a table if one exists', () => { - AWS.mock('DynamoDB', 'describeTable', (params, cb) => cb()); - AWS.mock('DynamoDB', 'createTable', (params, cb) => { - expect(params).to.not.exist; - cb(); - }); - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.createTable(); - }); - - - it('should throw any exception that is not ResourceNotFoundException', () => { - AWS.mock('DynamoDB', 'describeTable', (params, cb) => cb(new Error('Error'))); - AWS.mock('DynamoDB', 'createTable', (params, cb) => { - expect(params).to.not.exist; - cb(new Error('Error')); - }); - dynamo = new DynamoDb(TableName, { region: 'us-east-1' }); - return dynamo.createTable() - .then(() => { - throw new Error('Should not reach here'); - }) - .catch(err => err.message.should.equal('Error')); - }); - }); -}); diff --git a/js/lib/test/utils.spec.js b/js/lib/test/utils.spec.js deleted file mode 100644 index 0ad1c0c..0000000 --- a/js/lib/test/utils.spec.js +++ /dev/null @@ -1,163 +0,0 @@ -'use strict'; - -/* eslint-disable no-unused-expressions, no-undef */ - -require('../../test/setup'); -const utils = require('../utils'); - -function fisherYates(arrayArg) { - const array = arrayArg; - let count = array.length; - let randomnumber; - let temp; - while (count) { - randomnumber = Math.floor((Math.random() * count)); - count -= 1; - temp = array[count]; - array[count] = array[randomnumber]; - array[randomnumber] = temp; - } -} - -describe('utils', () => { - describe('#paddedInt', () => { - it('should left pad with zeros', () => { - const padded = utils.paddedInt(4, 1); - padded.should.equal('0001'); - }); - - it('should not pad larger integers', () => { - const padded = utils.paddedInt(4, 12345); - padded.should.equal('12345'); - }); - }); - - describe('#sanitizeVersion', () => { - it('should convert a number into a padded string', () => { - const version = utils.sanitizeVersion(1); - version.should.equal('0000000000000000001'); - }); - - it('should not change a string version', () => { - const rawVersion = 'version'; - const version = utils.sanitizeVersion(rawVersion); - version.should.equal(rawVersion); - }); - - it('should default to version 1, padded', () => { - const version = utils.sanitizeVersion(undefined, true); - version.should.equal('0000000000000000001'); - }); - }); - - describe('#sortSecrets', () => { - it('should sort by name in ascending order', () => { - const array = Array.from({ length: 10 }, (k, i) => ({ name: `0${i}` })); - fisherYates(array); - fisherYates(array); - fisherYates(array); - array.sort(utils.sortSecrets); - array.forEach((next, idx) => next.name.should.equal(`0${idx}`)); - }); - - it('should sort by version in descending order', () => { - const array = Array.from({ length: 10 }, (k, i) => ({ name: 'same', version: `0${i}` })); - fisherYates(array); - fisherYates(array); - fisherYates(array); - array.sort(utils.sortSecrets); - array.forEach((next, idx) => next.version.should.equal(`0${9 - idx}`)); - }); - - it('should sort by name in ascending order, then version in descending order', () => { - const array = Array.from({ length: 100 }, (k, i) => ({ - name: `0${i % 10}`, - version: `0${Math.floor((i / 10))}`, - })); - fisherYates(array); - fisherYates(array); - fisherYates(array); - array.sort(utils.sortSecrets); - array.forEach((next, idx) => { - const name = `0${Math.floor(idx / 10)}`; - const version = `0${Math.floor((99 - idx) % 10)}`; - next.name.should.equal(name); - next.version.should.equal(version); - }); - }); - }); - - describe('#asPromise', () => { - it('should return a promise', () => { - const result = utils.asPromise({}, () => { - }); - expect(result.then).to.exist; - }); - - it('should insert a callback', () => { - const fn = function (cb) { - expect(cb).to.exist; - cb(undefined, 'Success'); - }; - return utils.asPromise({}, fn) - .then(res => res.should.equal('Success')); - }); - - it('should handle successful calls', () => { - const fn = function (cb) { - cb(undefined, 'Success'); - }; - return utils.asPromise({}, fn) - .then(res => res.should.equal('Success')); - }); - - it('should insert the correct arguments', () => { - const arg1 = 'arg1'; - const arg2 = 'arg2'; - - const fn = function (one, two, cb) { - expect(one).to.exist; - one.should.equal(arg1); - expect(two).to.exist; - two.should.equal(arg2); - expect(cb).to.exist; - cb(undefined, 'Success'); - }; - - return utils.asPromise({}, fn, arg1, arg2) - .then(res => res.should.equal('Success')); - }); - - it('should handle errors', () => { - const fn = function (cb) { - cb(new Error('Error')); - }; - return utils.asPromise({}, fn) - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.equal('Error')); - }); - }); - - describe('#mapPromise', () => { - it('calls the promises in order', function () { - this.timeout(10e3); - const array = Array.from({ length: 5 }, (v, k) => k); - let finishedOrder = []; - - function updatFinished(idx) { - finishedOrder.push(idx); - } - - return utils.mapPromise(array, i => new Promise(resolve => - setTimeout(resolve, 100 * (10 - i), i)) - .then(updatFinished)) - .then(() => { - finishedOrder.forEach((next, i) => next.should.equal(i)); - finishedOrder = []; - }) - .then(() => Promise.all(array.map((next, i) => new Promise(resolve => - setTimeout(resolve, 100 * (10 - i), i)).then(updatFinished)))) - .then(() => finishedOrder.forEach((next, i) => next.should.equal(4 - i))); - }); - }); -}); diff --git a/js/test/coverage.spec.js b/js/test/coverage.spec.js deleted file mode 100644 index eaf27f2..0000000 --- a/js/test/coverage.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -/* eslint-disable no-unused-expressions, no-undef */ - -require('./setup'); -const glob = require('glob'); - -const files = glob.sync('js/**/*.js', null); - -describe('coverage', () => { - files.forEach((file) => { - if (file.indexOf('/test/') >= 0) { - return; - } - it(`can require ${file}`, () => { - // require the file to get it instrumented for coverage reports - require(`../../${file}`); // eslint-disable-line global-require, import/no-dynamic-require - }); - }); -}); diff --git a/js/test/index.spec.js b/js/test/index.spec.js deleted file mode 100644 index 64196ac..0000000 --- a/js/test/index.spec.js +++ /dev/null @@ -1,962 +0,0 @@ -'use strict'; - -/* eslint-disable no-unused-expressions, no-undef */ - -require('./setup'); -const AWS = require('aws-sdk-mock'); - -const Credstash = require('../index'); -const encryption = require('./utils/encryption'); - -const encrypter = require('../lib/encrypter'); -const defaults = require('../defaults'); - - -describe('index', () => { - const defCredstash = options => new Credstash(Object.assign({ awsOpts: { region: 'us-east-1' } }, options)); - - beforeEach(() => { - AWS.restore(); - }); - - afterEach(() => { - AWS.restore(); - }); - - describe('#constructor', () => { - it('has methods to match credstash', () => { - const credstash = defCredstash(); - credstash.paddedInt.should.exist; - credstash.getHighestVersion.should.exist; - credstash.listSecrets.should.exist; - credstash.putSecret.should.exist; - credstash.getAllSecrets.should.exist; - credstash.getAllVersions.should.exist; - credstash.getSecret.should.exist; - credstash.deleteSecrets.should.exist; - credstash.createDdbTable.should.exist; - }); - - it('should use a callback if provided', (done) => { - const table = 'TableNameNonDefault'; - AWS.mock('DynamoDB', 'describeTable', (params, cb) => { - params.TableName.should.equal(table); - cb(); - }); - const credstash = defCredstash({ table }); - credstash.createDdbTable((err) => { - expect(err).to.not.exist; - done(); - }); - }); - - it('should use a callback for errors, and not throw an exception', (done) => { - const table = 'TableNameNonDefault'; - AWS.mock('DynamoDB', 'describeTable', (params, cb) => { - params.TableName.should.equal(table); - cb('Error'); - }); - const credstash = defCredstash({ table }); - credstash.createDdbTable((err) => { - expect(err).to.exist; - err.should.equal('Error'); - }) - .then(done); - }); - - it('should return the configuration', () => { - const region = 'us-east-1'; - const credstash = defCredstash(); - const newConfig = credstash.getConfiguration(); - newConfig.should.eql({ - config: { - awsOpts: { - region, - }, - }, - dynamoConfig: { - table: defaults.DEFAULT_TABLE, - opts: { - region, - }, - }, - kmsConfig: { - kmsKey: defaults.DEFAULT_KMS_KEY, - opts: { - region, - }, - }, - }); - }); - - it('should allow separate options for KMS and DynamoDB', () => { - const region = 'us-east-1'; - - const dynamoOpts = { - region: 'us-west-1', - endpoint: 'https://service1.region.amazonaws.com', - }; - - const kmsOpts = { - region: 'us-west-2', - endpoint: 'https://service2.region.amazonaws.com', - }; - - const credstash = defCredstash({ - dynamoOpts, - kmsOpts, - }); - const newConfig = credstash.getConfiguration(); - newConfig.should.eql({ - config: { - dynamoOpts, - kmsOpts, - awsOpts: { - region, - }, - }, - dynamoConfig: { - table: defaults.DEFAULT_TABLE, - opts: dynamoOpts, - }, - kmsConfig: { - kmsKey: defaults.DEFAULT_KMS_KEY, - opts: kmsOpts, - }, - }); - }); - }); - - describe('#getHighestVersion', () => { - it('should return the highest version', () => { - const Items = [ - { - name: 'name1', - version: 'version1', - }, - { - name: 'name1', - version: 'version2', - }, - ]; - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb(undefined, { Items })); - const credstash = defCredstash(); - return credstash.getHighestVersion({ - name: 'name1', - }) - .then(version => version.should.equal(Items[0].version)); - }); - - it('should default to version 0', () => { - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb(undefined, { Items: [] })); - const credstash = defCredstash(); - return credstash.getHighestVersion({ - name: 'name', - }) - .then(version => version.should.equal(0)); - }); - - it('should request by name', () => { - const name = 'name'; - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - params.ExpressionAttributeValues.should.deep.equal({ - ':name': name, - }); - cb(undefined, { - Items: [], - }); - }); - const credstash = defCredstash(); - return credstash.getHighestVersion({ - name: 'name', - }) - .then(version => version.should.equal(0)); - }); - - it('should reject a missing name', () => { - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.getHighestVersion() - .then(() => { - throw new Error('Error'); - }) - .catch(err => err.message.should.contain('name is a required parameter')); - }); - }); - - describe('#incrementVersion', () => { - it('should reject non integer versions', () => { - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb( - undefined, - { - Items: [ - { - version: 'hello world', - }, - ], - } // eslint-disable-line comma-dangle - )); - const credstash = defCredstash(); - return credstash.incrementVersion({ name: 'name' }) - .then(() => 'Should not get here') - .catch((err) => { - expect(err.message).to.exist; - err.message.should.contain('is not an int'); - }); - }); - - it('should return a padded version integer', () => { - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb( - undefined, - { Items: [{ version: '1' }] } // eslint-disable-line comma-dangle - )); - const credstash = defCredstash(); - return credstash.incrementVersion({ - name: 'name', - }) - .then(version => version.should.equal('0000000000000000002')); - }); - - it('should accept name as a param', () => { - const name = 'name'; - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - params.ExpressionAttributeValues.should.deep.equal({ ':name': name }); - cb(undefined, { - Items: [ - { - version: '1', - }, - ], - }); - }); - const credstash = defCredstash(); - return credstash.incrementVersion({ name }) - .then(version => version.should.equal('0000000000000000002')); - }); - }); - - describe('#putSecret', () => { - let realOne; - beforeEach(() => { - realOne = Object.assign({}, encryption.credstashKey); - }); - - it('should create a new stash', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(undefined, realOne.kms)); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => { - params.Item.hmac.should.equal(realOne.hmac); - params.Item.key.should.equal(realOne.key); - params.Item.name.should.equal(realOne.name); - params.Item.contents.should.equal(realOne.contents); - params.Item.version.should.equal(realOne.version); - params.Item.digest.should.equal(realOne.digest); - cb(undefined, 'Success'); - }); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - version: realOne.version, - }) - .then(res => res.should.equal('Success')); - }); - - it('should default the version to a zero padded 1', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(undefined, realOne.kms)); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => { - params.Item.version.should.equal('0000000000000000001'); - cb(undefined, 'Success'); - }); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - }) - .then(res => res.should.equal('Success')); - }); - - it('should convert numerical versions to padded strings', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(undefined, realOne.kms)); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => { - params.Item.version.should.equal('0000000000000000042'); - cb(undefined, 'Success'); - }); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - version: 42, - }) - .then(res => res.should.equal('Success')); - }); - - it('should default the digest to SHA256', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(undefined, realOne.kms)); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => { - params.Item.digest.should.equal('SHA256'); - cb(undefined, 'Success'); - }); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - }) - .then(res => res.should.equal('Success')); - }); - - it('should use the correct context', () => { - const context = { key: 'value' }; - AWS.mock('KMS', 'generateDataKey', (params, cb) => { - params.EncryptionContext.should.deep.equal(context); - cb(undefined, realOne.kms); - }); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => cb(undefined, 'Success')); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - version: realOne.version, - context, - }) - .then(res => res.should.equal('Success')); - }); - - it('should use the provided digest', () => { - const digest = 'MD5'; - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(undefined, realOne.kms)); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => { - params.Item.digest.should.equal(digest); - cb(undefined, 'Success'); - }); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - version: realOne.version, - digest, - }) - .then(res => res.should.equal('Success')); - }); - - it('should rethrow a NotFoundException from KMS', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb({ - code: 'NotFoundException', - message: 'Success', - random: 1234, - })); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - }) - .then(res => expect(res).to.not.exist) - .catch((err) => { - err.message.should.equal('Success'); - err.code.should.equal('NotFoundException'); - err.random.should.equal(1234); - }); - }); - - it('should throw an error for a bad KMS key', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb({ - code: 'Key Exception of some other sort', - message: 'Failure', - })); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash({ - kmsKey: 'test', - }); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - }) - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.contains('Could not generate key using KMS key test')); - }); - - it('should notify of duplicate name/version pairs', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(undefined, realOne.kms)); - AWS.mock('DynamoDB.DocumentClient', 'put', (params, cb) => cb({ - code: 'ConditionalCheckFailedException', - })); - const credstash = defCredstash({ - kmsKey: 'test', - }); - return credstash.putSecret({ - name: realOne.name, - secret: realOne.plainText, - }) - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.contain('is already in the credential store.')); - }); - - it('should reject missing options', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.putSecret() - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should reject a missing name', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.putSecret({ - secret: 'secret', - }) - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should reject a missing secret', () => { - AWS.mock('KMS', 'generateDataKey', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.putSecret({ - name: 'name', - }) - .then(() => { throw new Error('Error'); }) - .catch(err => err.message.should.equal('secret is a required parameter')); - }); - }); - - describe('#getAllVersions', () => { - it('should reject requests without a name', () => { - const limit = 5; - const credstash = defCredstash(); - return credstash.getAllVersions({ - limit, - }) - .then(() => { throw new Error('Error'); }) - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should fetch and decode the secrets', () => { - const name = 'name'; - const limit = 5; - const rawItem = encryption.credstashKey; - - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - params.ExpressionAttributeValues[':name'].should.equal(name); - params.Limit.should.equal(limit); - cb(undefined, { - Items: [ - { - version: '0000000000000000006', - contents: rawItem.contents, - key: rawItem.key, - hmac: rawItem.hmac, - }, - ], - }); - }); - - AWS.mock('KMS', 'decrypt', (params, cb) => { - params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); - cb(undefined, rawItem.kms); - }); - - const credentials = defCredstash(); - return credentials.getAllVersions({ - name, - limit, - }).then((allVersions) => { - allVersions[0].version.should.equal('0000000000000000006'); - allVersions[0].secret.should.equal(rawItem.plainText); - }); - }); - - it('should default to all versions', () => { - const name = 'name'; - const rawItem = encryption.credstashKey; - - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - params.ExpressionAttributeValues[':name'].should.equal(name); - expect(params.Limit).to.not.exist; - cb(undefined, { - Items: [ - { - version: '0000000000000000006', - contents: rawItem.contents, - key: rawItem.key, - hmac: rawItem.hmac, - }, - ], - }); - }); - - AWS.mock('KMS', 'decrypt', (params, cb) => { - params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); - cb(undefined, rawItem.kms); - }); - - const credentials = defCredstash(); - return credentials.getAllVersions({ - name, - }).then((allVersions) => { - allVersions[0].version.should.equal('0000000000000000006'); - allVersions[0].secret.should.equal(rawItem.plainText); - }); - }); - }); - - describe('#getSecret', () => { - it('should fetch and decode a secret', () => { - const name = 'name'; - const version = 'version1'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - params.Key.name.should.equal(name); - params.Key.version.should.equal(version); - cb(undefined, { - Item: { - contents: rawItem.contents, - key: rawItem.key, - hmac: rawItem.hmac, - }, - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); - cb(undefined, rawItem.kms); - }); - - const credentials = defCredstash(); - return credentials.getSecret({ - name, - version, - }) - .then(secret => secret.should.equal(rawItem.plainText)); - }); - - it('should reject a missing name', () => { - const credentials = defCredstash(); - return credentials.getSecret({ version: 'version' }) - .then(() => { - throw new Error('Should not succeed'); - }) - .catch(err => err.message.should.contain('name is a required parameter')); - }); - - it('should reject a missing name with default options', () => { - const credentials = defCredstash(); - return credentials.getSecret() - .then(() => { - throw new Error('Should not succeed'); - }) - .catch(err => err.message.should.contain('name is a required parameter')); - }); - - it('should not reject a missing version', () => { - const version = 'version1'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - cb(undefined, { - Items: [ - { - contents: rawItem.contents, - key: rawItem.key, - hmac: rawItem.hmac, - version, - }, - ], - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - cb(undefined, rawItem.kms); - }); - const credentials = defCredstash(); - return credentials.getSecret({ name: 'name' }) - .then(secret => secret.should.equal(rawItem.plainText)) - .catch(err => expect(err).to.not.exist); - }); - - it('should default version to the latest', () => { - const name = 'name'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - cb(new Error('Error')); - }); - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - params.ExpressionAttributeValues[':name'].should.equal(name); - cb(undefined, { - Items: [ - { - contents: rawItem.contents, - key: rawItem.key, - hmac: rawItem.hmac, - }, - ], - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); - cb(undefined, rawItem.kms); - }); - const credentials = defCredstash(); - return credentials.getSecret({ - name: 'name', - }) - .then(secret => secret.should.equal(rawItem.plainText)) - .catch(err => expect(err).to.not.exist); - }); - - it('should throw an exception for a missing key', () => { - const name = 'name'; - const version = 'version1'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - cb(undefined, { - Item: { - contents: rawItem.contents, - hmac: rawItem.hmac, - }, - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - cb(new Error('Error')); - }); - - const credentials = defCredstash(); - return credentials.getSecret({ - name, - version, - }) - .then(() => { - throw new Error('Error'); - }) - .catch((err) => { - expect(err.message).to.exist; - err.message.should.contain('could not be found'); - }); - }); - - it('should throw an exception for invalid cipherText, no context', () => { - const name = 'name'; - const version = 'version1'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - cb(undefined, { - Item: { - contents: rawItem.contents, - hmac: rawItem.hmac, - key: rawItem.key, - }, - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - cb({ code: 'InvalidCiphertextException' }); - }); - - const credentials = defCredstash(); - return credentials.getSecret({ - name, - version, - }) - .then(() => { - throw new Error('Error'); - }) - .catch((err) => { - expect(err.message).to.exist; - err.message.should.contain('The credential may require that an encryption'); - }); - }); - - it('should throw an exception for invalid cipherText, with context', () => { - const name = 'name'; - const version = 'version1'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - cb(undefined, { - Item: { - contents: rawItem.contents, - hmac: rawItem.hmac, - key: rawItem.key, - }, - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - cb({ code: 'InvalidCiphertextException' }); - }); - - const credentials = defCredstash(); - return credentials.getSecret({ - name, - version, - context: { - key: 'value', - }, - }) - .then(() => { - throw new Error('Error'); - }) - .catch((err) => { - expect(err.message).to.exist; - err.message.should.contain('The encryption context provided may not match'); - }); - }); - - it('should throw an exception for invalid cipherText, with context', () => { - const name = 'name'; - const version = 'version1'; - const rawItem = encryption.credstashKey; - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - cb(undefined, { - Item: { - contents: rawItem.contents, - hmac: rawItem.hmac, - key: rawItem.key, - }, - }); - }); - AWS.mock('KMS', 'decrypt', (params, cb) => { - cb(new Error('Correct Error')); - }); - - const credentials = defCredstash(); - return credentials.getSecret({ - name, - version, - context: { - key: 'value', - }, - }) - .then(() => { - throw new Error('Error'); - }) - .catch((err) => { - expect(err.message).to.exist; - err.message.should.contain('Decryption error'); - }); - }); - }); - - describe('#deleteSecrets', () => { - it('should reject empty options', () => { - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb(new Error('Error'))); - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.deleteSecrets() - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should reject missing name', () => { - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => cb(new Error('Error'))); - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => cb(new Error('Error'))); - const credstash = defCredstash(); - return credstash.deleteSecrets({}) - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should delete all versions of a given name', () => { - const name = 'name'; - const Items = Array.from({ length: 10 }, (v, i) => ({ name, version: `${i}` })); - AWS.mock('DynamoDB.DocumentClient', 'query', (params, cb) => { - params.ExpressionAttributeValues[':name'].should.equal(name); - cb(undefined, { Items }); - }); - let counter = 0; - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => { - params.Key.name.should.equal(name); - params.Key.version.should.equal(`${counter}`); - counter += 1; - cb(undefined, 'Success'); - }); - - const credstash = defCredstash(); - return credstash.deleteSecrets({ - name, - }) - .then(res => res.forEach(next => next.should.equal('Success'))); - }); - }); - - describe('#deleteSecret', () => { - const name = 'name'; - const version = 'version'; - const numVer = 42; - - it('should reject empty options', () => { - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => cb(new Error('Error'))); - - const credstash = defCredstash(); - return credstash.deleteSecret() - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should reject a missing name', () => { - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => cb(new Error('Error'))); - - const credstash = defCredstash(); - return credstash.deleteSecret({ version }) - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.equal('name is a required parameter')); - }); - - it('should reject missing version', () => { - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => cb(new Error('Error'))); - - const credstash = defCredstash(); - return credstash.deleteSecret({ name }) - .then(res => expect(res).to.not.exist) - .catch(err => err.message.should.equal('version is a required parameter')); - }); - - - it('should delete the correct item', () => { - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => { - params.Key.name.should.equal(name); - params.Key.version.should.equal(version); - cb(undefined, 'Success'); - }); - - const credstash = defCredstash(); - return credstash.deleteSecret({ name, version }) - .then(res => res.should.equal('Success')); - }); - - it('should convert numerical versions into strings', () => { - AWS.mock('DynamoDB.DocumentClient', 'delete', (params, cb) => { - params.Key.name.should.equal(name); - params.Key.version.should.equal(`00000000000000000${numVer}`); - cb(undefined, 'Success'); - }); - - const credstash = defCredstash(); - return credstash.deleteSecret({ name, version: numVer }) - .then(res => res.should.equal('Success')); - }); - }); - - describe('#listSecrets', () => { - it('should return all secret names and versions', () => { - const items = [{ name: 'name', version: 'version1' }, { name: 'name', version: 'version2' }]; - AWS.mock('DynamoDB.DocumentClient', 'scan', (params, cb) => cb(undefined, { Items: items })); - const credstash = defCredstash(); - return credstash.listSecrets() - .then((results) => { - results.length.should.equal(2); - results[0].name.should.equal('name'); - results[0].version.should.equal('version2'); - results[1].name.should.equal('name'); - results[1].version.should.equal('version1'); - }); - }); - }); - - describe('#getAllSecrets', () => { - let items; - let kms; - - const item1 = encryption.item; - const item2 = encryption.credstashKey; - - function addItem(item) { - items[item.name] = items[item.name] || {}; - items[item.name][item.version] = { - contents: item.contents, - key: item.key, - hmac: item.hmac || item.hmacSha256, - name: item.name, - version: item.version, - }; - kms[item.key] = item.kms; - } - - beforeEach(() => { - items = {}; - kms = {}; - - addItem(item1); - addItem(item2); - - AWS.mock('DynamoDB.DocumentClient', 'scan', (params, cb) => { - const Items = []; - Object.keys(items).forEach((name) => { - const next = items[name]; - Object.keys(next).forEach(version => Items.push(next[version])); - }); - cb(undefined, { Items }); - }); - - AWS.mock('DynamoDB.DocumentClient', 'get', (params, cb) => { - const Item = items[params.Key.name][params.Key.version]; - cb(undefined, { Item }); - }); - - AWS.mock('KMS', 'decrypt', (params, cb) => { - cb(undefined, kms[params.CiphertextBlob.toString('base64')]); - }); - }); - - it('should return all secrets', () => { - const credstash = defCredstash(); - return credstash.getAllSecrets() - .then((res) => { - Object.keys(res).length.should.equal(2); - const unsorted = Object.keys(res); - const sorted = Object.keys(res).sort(); - unsorted.should.deep.equal(sorted); - }); - }); - - it('should return all secrets starts with "some.secret"', () => { - const credstash = defCredstash(); - return credstash.getAllSecrets({ startsWith: 'some.secret' }) - .then((res) => { - Object.keys(res).length.should.equal(1); - Object.keys(res)[0].should.startWith('some.secret'); - const unsorted = Object.keys(res); - const sorted = Object.keys(res).sort(); - unsorted.should.deep.equal(sorted); - }); - }); - - it('should ignore bad secrets', () => { - const item3 = Object.assign({}, item1); - item3.contents += 'hello broken'; - item3.name = 'differentName'; - addItem(item3); - const credstash = defCredstash(); - return credstash.getAllSecrets() - .then((res) => { - Object.keys(res).length.should.equal(2); - const unsorted = Object.keys(res); - const sorted = Object.keys(res).sort(); - unsorted.should.deep.equal(sorted); - }); - }); - - it('should return all secrets, but only latest version', () => { - const item3 = Object.assign({}, item1); - item3.version = item3.version.replace('1', '2'); - item3.plainText = 'This is a new plaintext'; - const encrypted = encrypter.encrypt(undefined, item3.plainText, item3.kms); - item3.contents = encrypted.contents; - item3.hmac = encrypted.hmac; - - addItem(item3); - - const credstash = defCredstash(); - return credstash.getAllSecrets() - .then((res) => { - Object.keys(res).length.should.equal(2); - res[item3.name].should.equal(item3.plainText); - }); - }); - }); - - describe('#createDdbTable', () => { - it('should call createTable with the table name provided', () => { - const table = 'TableNameNonDefault'; - AWS.mock('DynamoDB', 'describeTable', (params, cb) => { - params.TableName.should.equal(table); - cb(); - }); - const credstash = defCredstash({ table }); - return credstash.createDdbTable() - .catch(err => expect(err).to.not.exist); - }); - }); -}); diff --git a/js/lib/decrypter.js b/lib/decrypter.js similarity index 81% rename from js/lib/decrypter.js rename to lib/decrypter.js index 495ec80..fd8ce25 100644 --- a/js/lib/decrypter.js +++ b/lib/decrypter.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const aesjs = require('aes-js'); +const aesjs = require("aes-js"); -const utils = require('./utils'); +const utils = require("./utils"); module.exports = { decryptAes(key, encrypted) { @@ -17,12 +17,7 @@ module.exports = { }, decrypt(item, kms) { - const { - name, - contents, - hmac, - digest, - } = item; + const { name, contents, hmac, digest } = item; const keys = utils.splitKmsKey(kms.Plaintext); diff --git a/lib/defaults.js b/lib/defaults.js new file mode 100755 index 0000000..cd02abf --- /dev/null +++ b/lib/defaults.js @@ -0,0 +1,9 @@ +"use strict"; + +module.exports = { + DEFAULT_REGION: "us-east-1", + DEFAULT_TABLE: "credential-store", + DEFAULT_KMS_KEY: "alias/credstash", + PAD_LEN: 19, + DEFAULT_DIGEST: "SHA256", +}; diff --git a/js/lib/dynamoDb.js b/lib/dynamoDb.js similarity index 61% rename from js/lib/dynamoDb.js rename to lib/dynamoDb.js index d14d596..1fc9f96 100755 --- a/js/lib/dynamoDb.js +++ b/lib/dynamoDb.js @@ -1,10 +1,10 @@ -'use strict'; +"use strict"; -const AWS = require('aws-sdk'); +const AWS = require("aws-sdk"); -const debug = require('debug')('credstash'); +const debug = require("debug")("credstash"); -const utils = require('./utils'); +const utils = require("./utils"); function combineResults(curr, next) { if (!curr) { @@ -19,19 +19,19 @@ function combineResults(curr, next) { return combined; } - function pageResults(that, fn, parameters, curr) { const params = Object.assign({}, parameters); if (curr) { params.ExclusiveStartKey = curr.LastEvaluatedKey; } - return utils.asPromise(that, fn, params) - .then((next) => { - const combined = combineResults(curr, next); - const nextStep = next.LastEvaluatedKey ? pageResults(that, fn, params, combined) : combined; - return nextStep; - }); + return utils.asPromise(that, fn, params).then((next) => { + const combined = combineResults(curr, next); + const nextStep = next.LastEvaluatedKey + ? pageResults(that, fn, params, combined) + : combined; + return nextStep; + }); } function createAllVersionsQuery(table, name) { @@ -39,12 +39,12 @@ function createAllVersionsQuery(table, name) { TableName: table, ConsistentRead: true, ScanIndexForward: false, - KeyConditionExpression: '#name = :name', + KeyConditionExpression: "#name = :name", ExpressionAttributeNames: { - '#name': 'name', + "#name": "name", }, ExpressionAttributeValues: { - ':name': name, + ":name": name, }, }; return params; @@ -63,16 +63,15 @@ function DynamoDB(table, awsOpts) { return pageResults(docClient, docClient.query, params); }; - this.getAllSecretsAndVersions = (opts) => { const options = Object.assign({}, opts); const params = { TableName: table, Limit: options.limit, - ProjectionExpression: '#name, #version', + ProjectionExpression: "#name, #version", ExpressionAttributeNames: { - '#name': 'name', - '#version': 'version', + "#name": "name", + "#version": "version", }, }; return pageResults(docClient, docClient.scan, params); @@ -96,11 +95,11 @@ function DynamoDB(table, awsOpts) { this.createSecret = (item) => { const params = { Item: item, - ConditionExpression: 'attribute_not_exists(#name)', // Never update an existing key + ConditionExpression: "attribute_not_exists(#name)", // Never update an existing key TableName: table, ExpressionAttributeNames: { - '#name': 'name', + "#name": "name", }, }; return utils.asPromise(docClient, docClient.put, params); @@ -119,22 +118,22 @@ function DynamoDB(table, awsOpts) { TableName: table, KeySchema: [ { - AttributeName: 'name', - KeyType: 'HASH', + AttributeName: "name", + KeyType: "HASH", }, { - AttributeName: 'version', - KeyType: 'RANGE', + AttributeName: "version", + KeyType: "RANGE", }, ], AttributeDefinitions: [ { - AttributeName: 'name', - AttributeType: 'S', + AttributeName: "name", + AttributeType: "S", }, { - AttributeName: 'version', - AttributeType: 'S', + AttributeName: "version", + AttributeType: "S", }, ], ProvisionedThroughput: { @@ -143,19 +142,29 @@ function DynamoDB(table, awsOpts) { }, }; - return utils.asPromise(ddb, ddb.describeTable, { TableName: table }) - .then(() => debug('Credential Store table already exists')) + return utils + .asPromise(ddb, ddb.describeTable, { TableName: table }) + .then(() => debug("Credential Store table already exists")) .catch((err) => { - if (err.code != 'ResourceNotFoundException') { + if (err.code != "ResourceNotFoundException") { throw err; } - debug('Creating table...'); - return utils.asPromise(ddb, ddb.createTable, params) - .then(() => debug('Waiting for table to be created...')) - .then(() => new Promise(resolve => setTimeout(resolve, 2e3))) - .then(() => utils.asPromise(ddb, ddb.waitFor, 'tableExists', { TableName: table })) - .then(() => debug('Table has been created ' + - 'Go read the README about how to create your KMS key')); + debug("Creating table..."); + return utils + .asPromise(ddb, ddb.createTable, params) + .then(() => debug("Waiting for table to be created...")) + .then(() => new Promise((resolve) => setTimeout(resolve, 2e3))) + .then(() => + utils.asPromise(ddb, ddb.waitFor, "tableExists", { + TableName: table, + }) + ) + .then(() => + debug( + "Table has been created " + + "Go read the README about how to create your KMS key" + ) + ); }); }; } diff --git a/js/lib/encrypter.js b/lib/encrypter.js similarity index 91% rename from js/lib/encrypter.js rename to lib/encrypter.js index 4ac60cb..ceeb39d 100755 --- a/js/lib/encrypter.js +++ b/lib/encrypter.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const aesjs = require('aes-js'); +const aesjs = require("aes-js"); -const utils = require('./utils'); +const utils = require("./utils"); module.exports = { encryptAes(key, plaintext) { diff --git a/js/lib/kms.js b/lib/kms.js similarity index 86% rename from js/lib/kms.js rename to lib/kms.js index aa179e2..3c2d946 100755 --- a/js/lib/kms.js +++ b/lib/kms.js @@ -1,8 +1,8 @@ -'use strict'; +"use strict"; -const AWS = require('aws-sdk'); +const AWS = require("aws-sdk"); -const utils = require('./utils'); +const utils = require("./utils"); function KMS(kmsKey, awsOpts) { const kms = new AWS.KMS(awsOpts); diff --git a/js/lib/utils.js b/lib/utils.js similarity index 68% rename from js/lib/utils.js rename to lib/utils.js index f930335..13f48fc 100755 --- a/js/lib/utils.js +++ b/lib/utils.js @@ -1,18 +1,19 @@ -'use strict'; +"use strict"; -const crypto = require('crypto'); +const crypto = require("crypto"); -const defaults = require('../defaults'); +const defaults = require("./defaults"); module.exports = { calculateHmac(digestArg, key, encrypted) { - const digest = digestArg || 'SHA256'; + const digest = digestArg || "SHA256"; const decoded = this.b64decode(encrypted); // compute an HMAC using the hmac key and the ciphertext - const hmac = crypto.createHmac(digest.toLowerCase(), key) + const hmac = crypto + .createHmac(digest.toLowerCase(), key) .update(decoded) .digest() - .toString('hex'); + .toString("hex"); return hmac; }, @@ -21,7 +22,8 @@ module.exports = { const dataKey = buffer.slice(0, 32); const hmacKey = buffer.slice(32); return { - dataKey, hmacKey, + dataKey, + hmacKey, }; }, @@ -31,27 +33,27 @@ module.exports = { sanitized = sanitized || 1; } - if (typeof sanitized == 'number') { + if (typeof sanitized == "number") { sanitized = this.paddedInt(defaults.PAD_LEN, sanitized); } - sanitized = (sanitized == undefined) ? sanitized : `${sanitized}`; + sanitized = sanitized == undefined ? sanitized : `${sanitized}`; return sanitized; }, b64decode(string) { - return Buffer.from(string, 'base64'); + return Buffer.from(string, "base64"); }, b64encode(buffer) { - return Buffer.from(buffer).toString('base64'); + return Buffer.from(buffer).toString("base64"); }, paddedInt(padLength, i) { const iStr = `${i}`; let pad = padLength - iStr.length; pad = pad < 0 ? 0 : pad; - return `${'0'.repeat(pad)}${iStr}`; + return `${"0".repeat(pad)}${iStr}`; }, sortSecrets(a, b) { @@ -65,12 +67,15 @@ module.exports = { const fn = args.shift(); return new Promise((resolve, reject) => { - fn.apply(that, args.concat((err, res) => { - if (err) { - return reject(err); - } - return resolve(res); - })); + fn.apply( + that, + args.concat((err, res) => { + if (err) { + return reject(err); + } + return resolve(res); + }) + ); }); }, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2063fb0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2560 @@ +{ + "name": "aws-credstash", + "version": "2.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.1.0.tgz", + "integrity": "sha512-42nyaQOVunX5Pm6GRJobmzbS7iLI+fhERITnETXzzwDZh+TtDr/Au3yAvXVjFmZ4wEUaE4Y3NFZfKv0bV0cbtg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/is-windows": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/is-windows/-/is-windows-1.0.0.tgz", + "integrity": "sha512-tJ1rq04tGKuIJoWIH0Gyuwv4RQ3+tIu7wQrC0MV47raQ44kIzXSSFKfrxFUOWVRvesoF7mrTqigXmqoZJsXwTg==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "aes-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", + "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" + }, + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "aws-sdk": { + "version": "2.742.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.742.0.tgz", + "integrity": "sha512-zntDB0BpMn/y+B4RQvXuqY8DmJDYPkeFjZ6BbZ6vdNrsdB5TRz8p53ats4D3mLG068RB4M4AmVioFnU69nDXyQ==", + "dev": true, + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + } + }, + "aws-sdk-mock": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/aws-sdk-mock/-/aws-sdk-mock-5.1.0.tgz", + "integrity": "sha512-Wa5eCSo8HX0Snqb7FdBylaXMmfrAWoWZ+d7MFhiYsgHPvNvMEGjV945FF2qqE1U0Tolr1ALzik1fcwgaOhqUWQ==", + "dev": true, + "requires": { + "aws-sdk": "^2.637.0", + "sinon": "^9.0.1", + "traverse": "^0.6.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", + "dev": true + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "c8": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-7.3.0.tgz", + "integrity": "sha512-DEWiHtepo/KB50K+gw2z5MWnQbgzACy8xqTREBmAAg4j5UOlkmDeJfl8jWjk2iDTDaJfXR0sbnqx7iNCwgsblQ==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.2", + "find-up": "^4.0.0", + "foreground-child": "^2.0.0", + "furi": "^2.0.0", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.0.2", + "rimraf": "^3.0.0", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^5.0.0", + "yargs": "^15.0.0", + "yargs-parser": "^18.0.0" + }, + "dependencies": { + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chai-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chai-string/-/chai-string-1.5.0.tgz", + "integrity": "sha512-sydDC3S3pNAQMYwJrs6dQX0oBQ6KfIPuOZ78n7rocW0eJJlsHPh2t3kwW7xfwYA/1Bf6/arGtSUo16rxR2JFlw==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "dev": true, + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", + "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.0", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^1.3.0", + "espree": "^7.2.0", + "esquery": "^1.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash": "^4.17.19", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "eslint-config-prettier": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", + "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-plugin-prettier": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", + "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", + "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", + "dev": true, + "requires": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "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==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "dependencies": { + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "furi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/furi/-/furi-2.0.0.tgz", + "integrity": "sha512-uKuNsaU0WVaK/vmvj23wW1bicOFfyqSsAIH71bRZx8kA4Xj+YCHin7CJKJJjkIsmxYaPFLk9ljmjEyB7xF7WvQ==", + "dev": true, + "requires": { + "@types/is-windows": "^1.0.0", + "is-windows": "^1.0.2" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "dev": true + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", + "dev": true + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", + "dev": true + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "dev": true, + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=", + "dev": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "just-extend": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", + "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==", + "dev": true, + "requires": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.4.2", + "debug": "4.1.1", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "object.assign": "4.1.0", + "promise.allsettled": "1.0.2", + "serialize-javascript": "4.0.0", + "strip-json-comments": "3.0.1", + "supports-color": "7.1.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.0", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", + "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "promise.allsettled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", + "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", + "dev": true, + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.0" + } + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "readdirp": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sinon": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.3.tgz", + "integrity": "sha512-IKo9MIM111+smz9JGwLmw5U1075n1YXeAq8YeSFlndCLhAL5KGn6bLgu7b/4AYHTV/LcEMcRm2wU2YiL55/6Pg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.2", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.1.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + } + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + } + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "dev": true + }, + "v8-to-istanbul": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz", + "integrity": "sha512-mbDNjuDajqYe3TXFk5qxcQy8L1msXNE37WTlLoqqpBfRsimbNcrlhQlDPntmECEcUvdC+AQ8CyMMf6EUx1r74Q==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "workerpool": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", + "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "yargs-unparser": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", + "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "decamelize": "^1.2.0", + "flat": "^4.1.0", + "is-plain-obj": "^1.1.0", + "yargs": "^14.2.3" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + } + } +} diff --git a/package.json b/package.json index 7b75d89..01efc52 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,58 @@ { - "name": "nodecredstash", + "name": "aws-credstash", "version": "2.0.0", - "description": "Node.js credstash implementation", - "main": "js/index.js", + "description": "Node.js credstash using AWS DynamoDB and KMS. Port of nodecredstash", + "main": "index.js", "engines": { - "node": ">=4.2.3", - "npm": ">=2.14.7" + "node": ">=10.0.0" + }, + "files": [ + "lib", + "index.js", + "index.d.ts" + ], + "devDependencies": { + "aws-sdk-mock": "^5.1.0", + "c8": "^7.3.0", + "chai": "^4.2.0", + "chai-string": "^1.5.0", + "eslint": "^7.7.0", + "eslint-config-prettier": "^6.11.0", + "eslint-plugin-prettier": "^3.1.4", + "lodash": "^4.17.20", + "mocha": "^8.1.3", + "prettier": "^2.1.1" + }, + "dependencies": { + "aes-js": "^3.1.2", + "debug": "^4.1.1" + }, + "peerDependencies": { + "aws-sdk": "^2.742.0" }, "scripts": { - "test": "npm run lint && npm run coverage", - "coverage": "node ./test/mocha.js junit coverage", - "lint": "./node_modules/.bin/eslint ." + "test": "npm run lint && c8 --reporter=text mocha", + "watch": "mocha -w", + "lint": "eslint --ignore-path .gitignore . && prettier --config .prettierrc.json --ignore-path .gitignore --check \"**/*.{js,json,ts}\"", + "fix": "eslint --ignore-path .gitignore --fix . && prettier --config .prettierrc.json --ignore-path .gitignore --write \"**/*.{js,json,ts}\"" + }, + "repository": { + "type": "git", + "url": "https://github.com/tongrhj/aws-credstash.git" }, "keywords": [ "credstash", + "dynamodb", "kms", "aws", - "node.js" + "node.js", + "secrets", + "credentials" ], - "author": "David Tanner", + "author": "Jared Tong ", "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/DavidTanner/nodecredstash.git" + "bugs": { + "url": "https://github.com/tongrhj/aws-credstash/issues" }, - "devDependencies": { - "aws-sdk-mock": "^1.7.0", - "chai": "4.1.2", - "chai-string": "^1.4.0", - "coveralls": "3.0.0", - "eslint": "4.13.1", - "eslint-config-airbnb": "16.1.0", - "eslint-config-airbnb-base": "12.1.0", - "eslint-plugin-chai-expect": "^1.1.1", - "eslint-plugin-import": "2.8.0", - "eslint-plugin-jsx-a11y": "6.0.3", - "eslint-plugin-react": "7.5.1", - "glob": "7.1.2", - "istanbul": "^0.4.5", - "lodash": "^4.17.4", - "mocha": "4.0.1", - "mocha-junit-reporter": "1.15.0", - "rimraf": "2.6.2" - }, - "dependencies": { - "aes-js": "^3.1.0", - "aws-sdk": "^2.171.0", - "debug": "^3.1.0" - } + "homepage": "https://github.com/tongrhj/aws-credstash" } diff --git a/test/index.spec.js b/test/index.spec.js new file mode 100644 index 0000000..68dcb8f --- /dev/null +++ b/test/index.spec.js @@ -0,0 +1,1105 @@ +"use strict"; + +/* eslint-disable no-unused-expressions, no-undef */ + +require("./setup"); +const AWS = require("aws-sdk-mock"); + +const Credstash = require("../index"); +const encryption = require("./utils/encryption"); + +const encrypter = require("../lib/encrypter"); +const defaults = require("../lib/defaults"); + +describe("index", () => { + const defCredstash = (options) => + new Credstash(Object.assign({ awsOpts: { region: "us-east-1" } }, options)); + + beforeEach(() => { + AWS.restore(); + }); + + afterEach(() => { + AWS.restore(); + }); + + describe("#constructor", () => { + it("has methods to match credstash", () => { + const credstash = defCredstash(); + credstash.paddedInt.should.exist; + credstash.getHighestVersion.should.exist; + credstash.listSecrets.should.exist; + credstash.putSecret.should.exist; + credstash.getAllSecrets.should.exist; + credstash.getAllVersions.should.exist; + credstash.getSecret.should.exist; + credstash.deleteSecrets.should.exist; + credstash.createDdbTable.should.exist; + }); + + it("should use a callback if provided", (done) => { + const table = "TableNameNonDefault"; + AWS.mock("DynamoDB", "describeTable", (params, cb) => { + params.TableName.should.equal(table); + cb(); + }); + const credstash = defCredstash({ table }); + credstash.createDdbTable((err) => { + expect(err).to.not.exist; + done(); + }); + }); + + it("should use a callback for errors, and not throw an exception", (done) => { + const table = "TableNameNonDefault"; + AWS.mock("DynamoDB", "describeTable", (params, cb) => { + params.TableName.should.equal(table); + cb("Error"); + }); + const credstash = defCredstash({ table }); + credstash + .createDdbTable((err) => { + expect(err).to.exist; + err.should.equal("Error"); + }) + .then(done); + }); + + it("should return the configuration", () => { + const region = "us-east-1"; + const credstash = defCredstash(); + const newConfig = credstash.getConfiguration(); + newConfig.should.eql({ + config: { + awsOpts: { + region, + }, + }, + dynamoConfig: { + table: defaults.DEFAULT_TABLE, + opts: { + region, + }, + }, + kmsConfig: { + kmsKey: defaults.DEFAULT_KMS_KEY, + opts: { + region, + }, + }, + }); + }); + + it("should allow separate options for KMS and DynamoDB", () => { + const region = "us-east-1"; + + const dynamoOpts = { + region: "us-west-1", + endpoint: "https://service1.region.amazonaws.com", + }; + + const kmsOpts = { + region: "us-west-2", + endpoint: "https://service2.region.amazonaws.com", + }; + + const credstash = defCredstash({ + dynamoOpts, + kmsOpts, + }); + const newConfig = credstash.getConfiguration(); + newConfig.should.eql({ + config: { + dynamoOpts, + kmsOpts, + awsOpts: { + region, + }, + }, + dynamoConfig: { + table: defaults.DEFAULT_TABLE, + opts: dynamoOpts, + }, + kmsConfig: { + kmsKey: defaults.DEFAULT_KMS_KEY, + opts: kmsOpts, + }, + }); + }); + }); + + describe("#getHighestVersion", () => { + it("should return the highest version", () => { + const Items = [ + { + name: "name1", + version: "version1", + }, + { + name: "name1", + version: "version2", + }, + ]; + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb(undefined, { Items }) + ); + const credstash = defCredstash(); + return credstash + .getHighestVersion({ + name: "name1", + }) + .then((version) => version.should.equal(Items[0].version)); + }); + + it("should default to version 0", () => { + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb(undefined, { Items: [] }) + ); + const credstash = defCredstash(); + return credstash + .getHighestVersion({ + name: "name", + }) + .then((version) => version.should.equal(0)); + }); + + it("should request by name", () => { + const name = "name"; + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + params.ExpressionAttributeValues.should.deep.equal({ + ":name": name, + }); + cb(undefined, { + Items: [], + }); + }); + const credstash = defCredstash(); + return credstash + .getHighestVersion({ + name: "name", + }) + .then((version) => version.should.equal(0)); + }); + + it("should reject a missing name", () => { + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .getHighestVersion() + .then(() => { + throw new Error("Error"); + }) + .catch((err) => + err.message.should.contain("name is a required parameter") + ); + }); + }); + + describe("#incrementVersion", () => { + it("should reject non integer versions", () => { + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb( + undefined, + { + Items: [ + { + version: "hello world", + }, + ], + } // eslint-disable-line comma-dangle + ) + ); + const credstash = defCredstash(); + return credstash + .incrementVersion({ name: "name" }) + .then(() => "Should not get here") + .catch((err) => { + expect(err.message).to.exist; + err.message.should.contain("is not an int"); + }); + }); + + it("should return a padded version integer", () => { + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb( + undefined, + { Items: [{ version: "1" }] } // eslint-disable-line comma-dangle + ) + ); + const credstash = defCredstash(); + return credstash + .incrementVersion({ + name: "name", + }) + .then((version) => version.should.equal("0000000000000000002")); + }); + + it("should accept name as a param", () => { + const name = "name"; + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + params.ExpressionAttributeValues.should.deep.equal({ ":name": name }); + cb(undefined, { + Items: [ + { + version: "1", + }, + ], + }); + }); + const credstash = defCredstash(); + return credstash + .incrementVersion({ name }) + .then((version) => version.should.equal("0000000000000000002")); + }); + }); + + describe("#putSecret", () => { + let realOne; + beforeEach(() => { + realOne = Object.assign({}, encryption.credstashKey); + }); + + it("should create a new stash", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(undefined, realOne.kms) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => { + params.Item.hmac.should.equal(realOne.hmac); + params.Item.key.should.equal(realOne.key); + params.Item.name.should.equal(realOne.name); + params.Item.contents.should.equal(realOne.contents); + params.Item.version.should.equal(realOne.version); + params.Item.digest.should.equal(realOne.digest); + cb(undefined, "Success"); + }); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + version: realOne.version, + }) + .then((res) => res.should.equal("Success")); + }); + + it("should default the version to a zero padded 1", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(undefined, realOne.kms) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => { + params.Item.version.should.equal("0000000000000000001"); + cb(undefined, "Success"); + }); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + }) + .then((res) => res.should.equal("Success")); + }); + + it("should convert numerical versions to padded strings", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(undefined, realOne.kms) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => { + params.Item.version.should.equal("0000000000000000042"); + cb(undefined, "Success"); + }); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + version: 42, + }) + .then((res) => res.should.equal("Success")); + }); + + it("should default the digest to SHA256", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(undefined, realOne.kms) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => { + params.Item.digest.should.equal("SHA256"); + cb(undefined, "Success"); + }); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + }) + .then((res) => res.should.equal("Success")); + }); + + it("should use the correct context", () => { + const context = { key: "value" }; + AWS.mock("KMS", "generateDataKey", (params, cb) => { + params.EncryptionContext.should.deep.equal(context); + cb(undefined, realOne.kms); + }); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => + cb(undefined, "Success") + ); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + version: realOne.version, + context, + }) + .then((res) => res.should.equal("Success")); + }); + + it("should use the provided digest", () => { + const digest = "MD5"; + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(undefined, realOne.kms) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => { + params.Item.digest.should.equal(digest); + cb(undefined, "Success"); + }); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + version: realOne.version, + digest, + }) + .then((res) => res.should.equal("Success")); + }); + + it("should rethrow a NotFoundException from KMS", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb({ + code: "NotFoundException", + message: "Success", + random: 1234, + }) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + }) + .then((res) => expect(res).to.not.exist) + .catch((err) => { + err.message.should.equal("Success"); + err.code.should.equal("NotFoundException"); + err.random.should.equal(1234); + }); + }); + + it("should throw an error for a bad KMS key", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb({ + code: "Key Exception of some other sort", + message: "Failure", + }) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash({ + kmsKey: "test", + }); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + }) + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.contains( + "Could not generate key using KMS key test" + ) + ); + }); + + it("should notify of duplicate name/version pairs", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(undefined, realOne.kms) + ); + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => + cb({ + code: "ConditionalCheckFailedException", + }) + ); + const credstash = defCredstash({ + kmsKey: "test", + }); + return credstash + .putSecret({ + name: realOne.name, + secret: realOne.plainText, + }) + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.contain("is already in the credential store.") + ); + }); + + it("should reject missing options", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .putSecret() + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should reject a missing name", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .putSecret({ + secret: "secret", + }) + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should reject a missing secret", () => { + AWS.mock("KMS", "generateDataKey", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .putSecret({ + name: "name", + }) + .then(() => { + throw new Error("Error"); + }) + .catch((err) => + err.message.should.equal("secret is a required parameter") + ); + }); + }); + + describe("#getAllVersions", () => { + it("should reject requests without a name", () => { + const limit = 5; + const credstash = defCredstash(); + return credstash + .getAllVersions({ + limit, + }) + .then(() => { + throw new Error("Error"); + }) + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should fetch and decode the secrets", () => { + const name = "name"; + const limit = 5; + const rawItem = encryption.credstashKey; + + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + params.ExpressionAttributeValues[":name"].should.equal(name); + params.Limit.should.equal(limit); + cb(undefined, { + Items: [ + { + version: "0000000000000000006", + contents: rawItem.contents, + key: rawItem.key, + hmac: rawItem.hmac, + }, + ], + }); + }); + + AWS.mock("KMS", "decrypt", (params, cb) => { + params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); + cb(undefined, rawItem.kms); + }); + + const credentials = defCredstash(); + return credentials + .getAllVersions({ + name, + limit, + }) + .then((allVersions) => { + allVersions[0].version.should.equal("0000000000000000006"); + allVersions[0].secret.should.equal(rawItem.plainText); + }); + }); + + it("should default to all versions", () => { + const name = "name"; + const rawItem = encryption.credstashKey; + + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + params.ExpressionAttributeValues[":name"].should.equal(name); + expect(params.Limit).to.not.exist; + cb(undefined, { + Items: [ + { + version: "0000000000000000006", + contents: rawItem.contents, + key: rawItem.key, + hmac: rawItem.hmac, + }, + ], + }); + }); + + AWS.mock("KMS", "decrypt", (params, cb) => { + params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); + cb(undefined, rawItem.kms); + }); + + const credentials = defCredstash(); + return credentials + .getAllVersions({ + name, + }) + .then((allVersions) => { + allVersions[0].version.should.equal("0000000000000000006"); + allVersions[0].secret.should.equal(rawItem.plainText); + }); + }); + }); + + describe("#getSecret", () => { + it("should fetch and decode a secret", () => { + const name = "name"; + const version = "version1"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + params.Key.name.should.equal(name); + params.Key.version.should.equal(version); + cb(undefined, { + Item: { + contents: rawItem.contents, + key: rawItem.key, + hmac: rawItem.hmac, + }, + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); + cb(undefined, rawItem.kms); + }); + + const credentials = defCredstash(); + return credentials + .getSecret({ + name, + version, + }) + .then((secret) => secret.should.equal(rawItem.plainText)); + }); + + it("should reject a missing name", () => { + const credentials = defCredstash(); + return credentials + .getSecret({ version: "version" }) + .then(() => { + throw new Error("Should not succeed"); + }) + .catch((err) => + err.message.should.contain("name is a required parameter") + ); + }); + + it("should reject a missing name with default options", () => { + const credentials = defCredstash(); + return credentials + .getSecret() + .then(() => { + throw new Error("Should not succeed"); + }) + .catch((err) => + err.message.should.contain("name is a required parameter") + ); + }); + + it("should not reject a missing version", () => { + const version = "version1"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + cb(undefined, { + Items: [ + { + contents: rawItem.contents, + key: rawItem.key, + hmac: rawItem.hmac, + version, + }, + ], + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + cb(undefined, rawItem.kms); + }); + const credentials = defCredstash(); + return credentials + .getSecret({ name: "name" }) + .then((secret) => secret.should.equal(rawItem.plainText)) + .catch((err) => expect(err).to.not.exist); + }); + + it("should default version to the latest", () => { + const name = "name"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + cb(new Error("Error")); + }); + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + params.ExpressionAttributeValues[":name"].should.equal(name); + cb(undefined, { + Items: [ + { + contents: rawItem.contents, + key: rawItem.key, + hmac: rawItem.hmac, + }, + ], + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + params.CiphertextBlob.should.deep.equal(rawItem.kms.CiphertextBlob); + cb(undefined, rawItem.kms); + }); + const credentials = defCredstash(); + return credentials + .getSecret({ + name: "name", + }) + .then((secret) => secret.should.equal(rawItem.plainText)) + .catch((err) => expect(err).to.not.exist); + }); + + it("should throw an exception for a missing key", () => { + const name = "name"; + const version = "version1"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + cb(undefined, { + Item: { + contents: rawItem.contents, + hmac: rawItem.hmac, + }, + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + cb(new Error("Error")); + }); + + const credentials = defCredstash(); + return credentials + .getSecret({ + name, + version, + }) + .then(() => { + throw new Error("Error"); + }) + .catch((err) => { + expect(err.message).to.exist; + err.message.should.contain("could not be found"); + }); + }); + + it("should throw an exception for invalid cipherText, no context", () => { + const name = "name"; + const version = "version1"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + cb(undefined, { + Item: { + contents: rawItem.contents, + hmac: rawItem.hmac, + key: rawItem.key, + }, + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + cb({ code: "InvalidCiphertextException" }); + }); + + const credentials = defCredstash(); + return credentials + .getSecret({ + name, + version, + }) + .then(() => { + throw new Error("Error"); + }) + .catch((err) => { + expect(err.message).to.exist; + err.message.should.contain( + "The credential may require that an encryption" + ); + }); + }); + + it("should throw an exception for invalid cipherText, with context", () => { + const name = "name"; + const version = "version1"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + cb(undefined, { + Item: { + contents: rawItem.contents, + hmac: rawItem.hmac, + key: rawItem.key, + }, + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + cb({ code: "InvalidCiphertextException" }); + }); + + const credentials = defCredstash(); + return credentials + .getSecret({ + name, + version, + context: { + key: "value", + }, + }) + .then(() => { + throw new Error("Error"); + }) + .catch((err) => { + expect(err.message).to.exist; + err.message.should.contain( + "The encryption context provided may not match" + ); + }); + }); + + it("should throw an exception for invalid cipherText, with context", () => { + const name = "name"; + const version = "version1"; + const rawItem = encryption.credstashKey; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + cb(undefined, { + Item: { + contents: rawItem.contents, + hmac: rawItem.hmac, + key: rawItem.key, + }, + }); + }); + AWS.mock("KMS", "decrypt", (params, cb) => { + cb(new Error("Correct Error")); + }); + + const credentials = defCredstash(); + return credentials + .getSecret({ + name, + version, + context: { + key: "value", + }, + }) + .then(() => { + throw new Error("Error"); + }) + .catch((err) => { + expect(err.message).to.exist; + err.message.should.contain("Decryption error"); + }); + }); + }); + + describe("#deleteSecrets", () => { + it("should reject empty options", () => { + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb(new Error("Error")) + ); + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .deleteSecrets() + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should reject missing name", () => { + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => + cb(new Error("Error")) + ); + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => + cb(new Error("Error")) + ); + const credstash = defCredstash(); + return credstash + .deleteSecrets({}) + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should delete all versions of a given name", () => { + const name = "name"; + const Items = Array.from({ length: 10 }, (v, i) => ({ + name, + version: `${i}`, + })); + AWS.mock("DynamoDB.DocumentClient", "query", (params, cb) => { + params.ExpressionAttributeValues[":name"].should.equal(name); + cb(undefined, { Items }); + }); + let counter = 0; + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => { + params.Key.name.should.equal(name); + params.Key.version.should.equal(`${counter}`); + counter += 1; + cb(undefined, "Success"); + }); + + const credstash = defCredstash(); + return credstash + .deleteSecrets({ + name, + }) + .then((res) => res.forEach((next) => next.should.equal("Success"))); + }); + }); + + describe("#deleteSecret", () => { + const name = "name"; + const version = "version"; + const numVer = 42; + + it("should reject empty options", () => { + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => + cb(new Error("Error")) + ); + + const credstash = defCredstash(); + return credstash + .deleteSecret() + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should reject a missing name", () => { + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => + cb(new Error("Error")) + ); + + const credstash = defCredstash(); + return credstash + .deleteSecret({ version }) + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.equal("name is a required parameter") + ); + }); + + it("should reject missing version", () => { + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => + cb(new Error("Error")) + ); + + const credstash = defCredstash(); + return credstash + .deleteSecret({ name }) + .then((res) => expect(res).to.not.exist) + .catch((err) => + err.message.should.equal("version is a required parameter") + ); + }); + + it("should delete the correct item", () => { + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => { + params.Key.name.should.equal(name); + params.Key.version.should.equal(version); + cb(undefined, "Success"); + }); + + const credstash = defCredstash(); + return credstash + .deleteSecret({ name, version }) + .then((res) => res.should.equal("Success")); + }); + + it("should convert numerical versions into strings", () => { + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => { + params.Key.name.should.equal(name); + params.Key.version.should.equal(`00000000000000000${numVer}`); + cb(undefined, "Success"); + }); + + const credstash = defCredstash(); + return credstash + .deleteSecret({ name, version: numVer }) + .then((res) => res.should.equal("Success")); + }); + }); + + describe("#listSecrets", () => { + it("should return all secret names and versions", () => { + const items = [ + { name: "name", version: "version1" }, + { name: "name", version: "version2" }, + ]; + AWS.mock("DynamoDB.DocumentClient", "scan", (params, cb) => + cb(undefined, { Items: items }) + ); + const credstash = defCredstash(); + return credstash.listSecrets().then((results) => { + results.length.should.equal(2); + results[0].name.should.equal("name"); + results[0].version.should.equal("version2"); + results[1].name.should.equal("name"); + results[1].version.should.equal("version1"); + }); + }); + }); + + describe("#getAllSecrets", () => { + let items; + let kms; + + const item1 = encryption.item; + const item2 = encryption.credstashKey; + + function addItem(item) { + items[item.name] = items[item.name] || {}; + items[item.name][item.version] = { + contents: item.contents, + key: item.key, + hmac: item.hmac || item.hmacSha256, + name: item.name, + version: item.version, + }; + kms[item.key] = item.kms; + } + + beforeEach(() => { + items = {}; + kms = {}; + + addItem(item1); + addItem(item2); + + AWS.mock("DynamoDB.DocumentClient", "scan", (params, cb) => { + const Items = []; + Object.keys(items).forEach((name) => { + const next = items[name]; + Object.keys(next).forEach((version) => Items.push(next[version])); + }); + cb(undefined, { Items }); + }); + + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + const Item = items[params.Key.name][params.Key.version]; + cb(undefined, { Item }); + }); + + AWS.mock("KMS", "decrypt", (params, cb) => { + cb(undefined, kms[params.CiphertextBlob.toString("base64")]); + }); + }); + + it("should return all secrets", () => { + const credstash = defCredstash(); + return credstash.getAllSecrets().then((res) => { + Object.keys(res).length.should.equal(2); + const unsorted = Object.keys(res); + const sorted = Object.keys(res).sort(); + unsorted.should.deep.equal(sorted); + }); + }); + + it('should return all secrets starts with "some.secret"', () => { + const credstash = defCredstash(); + return credstash + .getAllSecrets({ startsWith: "some.secret" }) + .then((res) => { + Object.keys(res).length.should.equal(1); + Object.keys(res)[0].should.startWith("some.secret"); + const unsorted = Object.keys(res); + const sorted = Object.keys(res).sort(); + unsorted.should.deep.equal(sorted); + }); + }); + + it("should ignore bad secrets", () => { + const item3 = Object.assign({}, item1); + item3.contents += "hello broken"; + item3.name = "differentName"; + addItem(item3); + const credstash = defCredstash(); + return credstash.getAllSecrets().then((res) => { + Object.keys(res).length.should.equal(2); + const unsorted = Object.keys(res); + const sorted = Object.keys(res).sort(); + unsorted.should.deep.equal(sorted); + }); + }); + + it("should return all secrets, but only latest version", () => { + const item3 = Object.assign({}, item1); + item3.version = item3.version.replace("1", "2"); + item3.plainText = "This is a new plaintext"; + const encrypted = encrypter.encrypt( + undefined, + item3.plainText, + item3.kms + ); + item3.contents = encrypted.contents; + item3.hmac = encrypted.hmac; + + addItem(item3); + + const credstash = defCredstash(); + return credstash.getAllSecrets().then((res) => { + Object.keys(res).length.should.equal(2); + res[item3.name].should.equal(item3.plainText); + }); + }); + }); + + describe("#createDdbTable", () => { + it("should call createTable with the table name provided", () => { + const table = "TableNameNonDefault"; + AWS.mock("DynamoDB", "describeTable", (params, cb) => { + params.TableName.should.equal(table); + cb(); + }); + const credstash = defCredstash({ table }); + return credstash + .createDdbTable() + .catch((err) => expect(err).to.not.exist); + }); + }); +}); diff --git a/js/lib/test/decrypter.spec.js b/test/lib/decrypter.spec.js similarity index 68% rename from js/lib/test/decrypter.spec.js rename to test/lib/decrypter.spec.js index 820aba1..d5fffa5 100644 --- a/js/lib/test/decrypter.spec.js +++ b/test/lib/decrypter.spec.js @@ -1,27 +1,25 @@ -'use strict'; +"use strict"; /* eslint-disable no-unused-expressions, no-undef */ -require('../../test/setup'); -const decrypter = require('../decrypter'); -const encryption = require('../../test/utils/encryption'); +require("../../test/setup"); +const decrypter = require("../decrypter"); +const encryption = require("../../test/utils/encryption"); -describe('decrypter', () => { +describe("decrypter", () => { const encryptedItem = encryption.item; - describe('#decrypt', () => { + describe("#decrypt", () => { const decrypt = decrypter.decrypt.bind(decrypter); it(`can decrypt ${encryptedItem.name} with default digest`, () => { const stash = { - name: 'item', + name: "item", hmac: encryptedItem.hmacSha256, contents: encryptedItem.contents, }; - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; const plainText = decrypt(stash, kms); plainText.should.equal(encryptedItem.plainText); @@ -29,15 +27,13 @@ describe('decrypter', () => { it(`can decrypt ${encryptedItem.name} with explicit SHA256 digest`, () => { const stash = { - name: 'item', + name: "item", hmac: encryptedItem.hmacSha256, contents: encryptedItem.contents, - digest: 'SHA256', + digest: "SHA256", }; - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; const plainText = decrypt(stash, kms); plainText.should.equal(encryptedItem.plainText); @@ -45,15 +41,13 @@ describe('decrypter', () => { it(`can decrypt ${encryptedItem.name} with SHA512 digest`, () => { const stash = { - name: 'item', + name: "item", hmac: encryptedItem.hmacSha512, contents: encryptedItem.contents, - digest: 'SHA512', + digest: "SHA512", }; - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; const plainText = decrypt(stash, kms); plainText.should.equal(encryptedItem.plainText); @@ -61,46 +55,42 @@ describe('decrypter', () => { it(`can decrypt ${encryptedItem.name} with MD5 digest`, () => { const stash = { - name: 'item', + name: "item", hmac: encryptedItem.hmacMd5, contents: encryptedItem.contents, - digest: 'MD5', + digest: "MD5", }; - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; const plainText = decrypt(stash, kms); plainText.should.equal(encryptedItem.plainText); }); - it('will throw an exception if the contents has been messed with', () => { + it("will throw an exception if the contents has been messed with", () => { const stash = { - name: 'item', + name: "item", hmac: encryptedItem.hmacMd5, contents: `${encryptedItem.contents}some junk`, - digest: 'MD5', + digest: "MD5", }; - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; try { const plainText = decrypt(stash, kms); plainText.should.not.exist; } catch (e) { - e.message.should.contain('does not match stored HMAC'); + e.message.should.contain("does not match stored HMAC"); } }); }); - describe('#decryptAes', () => { + describe("#decryptAes", () => { const decryptAes = decrypter.decryptAes.bind(decrypter); const credstashItem = encryption.credstashKey; - it('correctly encrypts a key', () => { + it("correctly encrypts a key", () => { const encrypted = decryptAes( credstashItem.kms.Plaintext.slice(0, 32), credstashItem.contents // eslint-disable-line comma-dangle diff --git a/test/lib/dynamo.spec.js b/test/lib/dynamo.spec.js new file mode 100755 index 0000000..ffe98dd --- /dev/null +++ b/test/lib/dynamo.spec.js @@ -0,0 +1,315 @@ +"use strict"; + +/* eslint-disable no-unused-expressions, no-undef */ + +require("../../test/setup"); + +const _ = require("lodash"); + +const DynamoDb = require("../dynamoDb"); + +const AWS = require("aws-sdk-mock"); + +function findKeyIndex(items, keys) { + const index = items.findIndex((item) => { + let matches = true; + _.forEach(keys, (value, key) => { + matches = matches && item[key] == value; + }); + return matches; + }); + return index; +} + +function sliceItems(items, params) { + const limit = params.Limit || items.length; + let startIndex = 0; + + if (params.ExclusiveStartKey) { + startIndex = findKeyIndex(items, params.ExclusiveStartKey) + 1; + } + + const Items = items.slice(startIndex, startIndex + limit); + + const lastIndex = startIndex + limit - 1; + let LastEvaluatedKey; + + const last = items[lastIndex]; + if (lastIndex < items.length - 1 && last) { + LastEvaluatedKey = { name: last.name, version: last.version }; + } + + const Count = Items.length; + const ScannedCount = Count; + + const results = { + LastEvaluatedKey, + Items, + ScannedCount, + Count, + }; + return results; +} + +function compareParams(actual, expected) { + if (expected.TableName) { + actual.TableName.should.eql(expected.TableName); + } + if (expected.ExpressionAttributeNames) { + actual.ExpressionAttributeNames.should.eql( + expected.ExpressionAttributeNames + ); + } + if (expected.KeyConditionExpression) { + actual.KeyConditionExpression.should.eql(expected.KeyConditionExpression); + } + + if (expected.ProjectionExpression) { + actual.ProjectionExpression.should.eql(expected.ProjectionExpression); + } + + if (expected.Limit) { + expect(actual.Limit).to.exist; + actual.Limit.should.eql(expected.Limit); + } + + if (expected.ExpressionAttributeValues) { + actual.ExpressionAttributeValues.should.eql( + expected.ExpressionAttributeValues + ); + } +} + +function mockQueryScan(error, items, expectedParams) { + function fn(params, done) { + compareParams(params, expectedParams); + const results = sliceItems(items, params); + + done(error, results); + } + + AWS.mock("DynamoDB.DocumentClient", "query", fn); + + AWS.mock("DynamoDB.DocumentClient", "scan", fn); +} + +describe("dynmaodDb", () => { + let dynamo; + let items; + const TableName = "credentials-store"; + + beforeEach(() => { + AWS.restore(); + items = Array.from({ length: 30 }, (v, i) => ({ name: i, version: i })); + }); + + afterEach(() => { + AWS.restore(); + }); + + describe("#getAllSecretsAndVersions", () => { + it("should properly page through many results", () => { + mockQueryScan(undefined, items, { + Limit: 10, + TableName, + }); + + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo + .getAllSecretsAndVersions({ limit: 10 }) + .then((res) => res.Items) + .then((secrets) => { + secrets.length.should.be.equal(items.length); + secrets.should.eql(items); + }); + }); + }); + + describe("#getAllVersions", () => { + it("should properly page through many results", () => { + mockQueryScan(undefined, items, { + Limit: 10, + TableName, + }); + + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo + .getAllVersions("", { limit: 10 }) + .then((res) => res.Items) + .then((secrets) => { + secrets.length.should.be.equal(items.length); + secrets.should.eql(items); + }); + }); + }); + + describe("#getLatestVersion", () => { + it("should only get one item back", () => { + mockQueryScan(undefined, items, { + Limit: 1, + TableName, + }); + + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo.getLatestVersion("").then((res) => { + expect(res).to.exist; + expect(res.Items).to.exist; + expect(res.Items[0]).to.exist; + res.Items[0].should.equal(items[0]); + }); + }); + }); + + describe("#getByVersion", () => { + it("should only get one item back", () => { + const name = "name"; + const version = "version"; + AWS.mock("DynamoDB.DocumentClient", "get", (params, cb) => { + params.TableName.should.equal(TableName); + expect(params.Key).to.exist; + params.Key.name.should.equal(name); + params.Key.version.should.equal(version); + cb(undefined, { Item: "Success" }); + }); + + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo.getByVersion(name, version).then((res) => { + expect(res).to.exist; + res.Item.should.equal("Success"); + }); + }); + }); + + describe("#createSecret", () => { + it("should create an item in DynamoDB", () => { + const item = items[0]; + AWS.mock("DynamoDB.DocumentClient", "put", (params, cb) => { + params.TableName.should.equal(TableName); + expect(params.ConditionExpression).to.exist; + params.ConditionExpression.should.equal("attribute_not_exists(#name)"); + expect(params.ExpressionAttributeNames).to.exist; + params.ExpressionAttributeNames.should.deep.equal({ + "#name": "name", + }); + params.Item.should.deep.equal(item); + cb(undefined, "Success"); + }); + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo + .createSecret(item) + .then((res) => res.should.equal("Success")); + }); + }); + + describe("#deleteSecret", () => { + it("should delete the secret by name and version", () => { + const name = "name"; + const version = "version"; + AWS.mock("DynamoDB.DocumentClient", "delete", (params, cb) => { + params.TableName.should.equal(TableName); + expect(params.Key).to.exist; + params.Key.name.should.equal(name); + params.Key.version.should.equal(version); + cb(undefined, "Success"); + }); + + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo.deleteSecret(name, version).then((secret) => { + expect(secret).to.exist; + secret.should.equal("Success"); + }); + }); + }); + + describe("#createTable", () => { + it("should create the table with the HASH as name and RANGE as version", function () { + this.timeout(5e3); + AWS.mock("DynamoDB", "describeTable", (params, cb) => + cb({ code: "ResourceNotFoundException" }) + ); + AWS.mock("DynamoDB", "createTable", (params, cb) => { + expect(params.TableName).to.exist; + params.TableName.should.equal(TableName); + expect(params.KeySchema).to.exist; + expect(params.KeySchema.find).to.exist; + params.KeySchema.length.should.equal(2); + + const hash = params.KeySchema.find((next) => next.KeyType == "HASH"); + expect(hash).to.exist; + hash.should.deep.equal({ + AttributeName: "name", + KeyType: "HASH", + }); + const range = params.KeySchema.find((next) => next.KeyType == "RANGE"); + expect(range).to.exist; + range.should.deep.equal({ + AttributeName: "version", + KeyType: "RANGE", + }); + expect(params.AttributeDefinitions).to.exist; + expect(params.AttributeDefinitions.find).to.exist; + params.AttributeDefinitions.length.should.equal(2); + const name = params.AttributeDefinitions.find( + (next) => next.AttributeName == "name" + ); + expect(name).to.exist; + name.should.deep.equal({ + AttributeName: "name", + AttributeType: "S", + }); + const version = params.AttributeDefinitions.find( + (next) => next.AttributeName == "version" + ); + expect(version).to.exist; + version.should.deep.equal({ + AttributeName: "version", + AttributeType: "S", + }); + expect(params.ProvisionedThroughput).to.exist; + params.ProvisionedThroughput.should.deep.equal({ + ReadCapacityUnits: 1, + WriteCapacityUnits: 1, + }); + cb(); + }); + AWS.mock("DynamoDB", "waitFor", (status, params, cb) => { + expect(status).to.exist; + status.should.equal("tableExists"); + expect(params).to.exist; + expect(params.TableName).to.exist; + params.TableName.should.equal(TableName); + cb(); + }); + + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo.createTable(); + }); + + it("should not create a table if one exists", () => { + AWS.mock("DynamoDB", "describeTable", (params, cb) => cb()); + AWS.mock("DynamoDB", "createTable", (params, cb) => { + expect(params).to.not.exist; + cb(); + }); + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo.createTable(); + }); + + it("should throw any exception that is not ResourceNotFoundException", () => { + AWS.mock("DynamoDB", "describeTable", (params, cb) => + cb(new Error("Error")) + ); + AWS.mock("DynamoDB", "createTable", (params, cb) => { + expect(params).to.not.exist; + cb(new Error("Error")); + }); + dynamo = new DynamoDb(TableName, { region: "us-east-1" }); + return dynamo + .createTable() + .then(() => { + throw new Error("Should not reach here"); + }) + .catch((err) => err.message.should.equal("Error")); + }); + }); +}); diff --git a/js/lib/test/encrypter.spec.js b/test/lib/encrypter.spec.js similarity index 61% rename from js/lib/test/encrypter.spec.js rename to test/lib/encrypter.spec.js index e333e42..83559e2 100644 --- a/js/lib/test/encrypter.spec.js +++ b/test/lib/encrypter.spec.js @@ -1,20 +1,18 @@ -'use strict'; +"use strict"; /* eslint-disable no-unused-expressions, no-undef */ -require('../../test/setup'); -const encrypter = require('../encrypter'); -const encryption = require('../../test/utils/encryption'); +require("../../test/setup"); +const encrypter = require("../encrypter"); +const encryption = require("../../test/utils/encryption"); -describe('encryptor', () => { +describe("encryptor", () => { const encryptedItem = encryption.item; - describe('#encrypt', () => { + describe("#encrypt", () => { const encrypt = encrypter.encrypt.bind(encrypter); it(`can encrypt ${encryptedItem.name} with default HMAC`, () => { - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; const encrypted = encrypt(undefined, encryptedItem.plainText, kms); encrypted.contents.should.equal(encryptedItem.contents); @@ -22,42 +20,39 @@ describe('encryptor', () => { }); it(`can encrypt ${encryptedItem.name} with explicit SHA256 HMAC`, () => { - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; - const encrypted = encrypt('SHA256', encryptedItem.plainText, kms); + const encrypted = encrypt("SHA256", encryptedItem.plainText, kms); encrypted.contents.should.equal(encryptedItem.contents); encrypted.hmac.should.equal(encryptedItem.hmacSha256); }); it(`can encrypt ${encryptedItem.name} with SHA512 HMAC`, () => { - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; - const encrypted = encrypt('SHA512', encryptedItem.plainText, kms); + const encrypted = encrypt("SHA512", encryptedItem.plainText, kms); encrypted.contents.should.equal(encryptedItem.contents); encrypted.hmac.should.equal(encryptedItem.hmacSha512); }); it(`can encrypt ${encryptedItem.name} with MD5 HMAC`, () => { - const { - kms, - } = encryptedItem; + const { kms } = encryptedItem; - const encrypted = encrypt('MD5', encryptedItem.plainText, kms); + const encrypted = encrypt("MD5", encryptedItem.plainText, kms); encrypted.contents.should.equal(encryptedItem.contents); encrypted.hmac.should.equal(encryptedItem.hmacMd5); }); }); - describe('#encryptAes', () => { + describe("#encryptAes", () => { const encryptAes = encrypter.encryptAes.bind(encrypter); const item = encryption.credstashKey; - it('correctly encrypts a key', () => { - const encrypted = encryptAes(item.kms.Plaintext.slice(0, 32), item.plainText); + it("correctly encrypts a key", () => { + const encrypted = encryptAes( + item.kms.Plaintext.slice(0, 32), + item.plainText + ); encrypted.should.equal(item.contents); }); }); diff --git a/js/lib/test/kms.spec.js b/test/lib/kms.spec.js similarity index 57% rename from js/lib/test/kms.spec.js rename to test/lib/kms.spec.js index 71834b9..b9ca92f 100644 --- a/js/lib/test/kms.spec.js +++ b/test/lib/kms.spec.js @@ -1,18 +1,17 @@ -'use strict'; +"use strict"; /* eslint-disable no-unused-expressions, no-undef */ -require('../../test/setup'); -const AWS = require('aws-sdk-mock'); -const KMS = require('../kms'); +require("../../test/setup"); +const AWS = require("aws-sdk-mock"); +const KMS = require("../kms"); - -describe('kms', () => { - describe('#decrypt', () => { - it('should call decrypt with key and context', () => { - const key = 'key'; - const context = { key: 'value' }; - AWS.mock('KMS', 'decrypt', (params, cb) => { +describe("kms", () => { + describe("#decrypt", () => { + it("should call decrypt with key and context", () => { + const key = "key"; + const context = { key: "value" }; + AWS.mock("KMS", "decrypt", (params, cb) => { expect(params.CiphertextBlob).to.exist; params.CiphertextBlob.should.equal(key); expect(params.EncryptionContext).to.exist; @@ -24,11 +23,11 @@ describe('kms', () => { }); }); - describe('#getEncryptionKey', () => { - it('should call getEncryptionKey with correct params', () => { - const key = 'key'; - const context = { key: 'value' }; - AWS.mock('KMS', 'generateDataKey', (params, cb) => { + describe("#getEncryptionKey", () => { + it("should call getEncryptionKey with correct params", () => { + const key = "key"; + const context = { key: "value" }; + AWS.mock("KMS", "generateDataKey", (params, cb) => { expect(params.NumberOfBytes).to.exist; params.NumberOfBytes.should.equal(64); expect(params.EncryptionContext).to.exist; diff --git a/test/lib/utils.spec.js b/test/lib/utils.spec.js new file mode 100644 index 0000000..8de7988 --- /dev/null +++ b/test/lib/utils.spec.js @@ -0,0 +1,177 @@ +"use strict"; + +/* eslint-disable no-unused-expressions, no-undef */ + +require("../../test/setup"); +const utils = require("../utils"); + +function fisherYates(arrayArg) { + const array = arrayArg; + let count = array.length; + let randomnumber; + let temp; + while (count) { + randomnumber = Math.floor(Math.random() * count); + count -= 1; + temp = array[count]; + array[count] = array[randomnumber]; + array[randomnumber] = temp; + } +} + +describe("utils", () => { + describe("#paddedInt", () => { + it("should left pad with zeros", () => { + const padded = utils.paddedInt(4, 1); + padded.should.equal("0001"); + }); + + it("should not pad larger integers", () => { + const padded = utils.paddedInt(4, 12345); + padded.should.equal("12345"); + }); + }); + + describe("#sanitizeVersion", () => { + it("should convert a number into a padded string", () => { + const version = utils.sanitizeVersion(1); + version.should.equal("0000000000000000001"); + }); + + it("should not change a string version", () => { + const rawVersion = "version"; + const version = utils.sanitizeVersion(rawVersion); + version.should.equal(rawVersion); + }); + + it("should default to version 1, padded", () => { + const version = utils.sanitizeVersion(undefined, true); + version.should.equal("0000000000000000001"); + }); + }); + + describe("#sortSecrets", () => { + it("should sort by name in ascending order", () => { + const array = Array.from({ length: 10 }, (k, i) => ({ name: `0${i}` })); + fisherYates(array); + fisherYates(array); + fisherYates(array); + array.sort(utils.sortSecrets); + array.forEach((next, idx) => next.name.should.equal(`0${idx}`)); + }); + + it("should sort by version in descending order", () => { + const array = Array.from({ length: 10 }, (k, i) => ({ + name: "same", + version: `0${i}`, + })); + fisherYates(array); + fisherYates(array); + fisherYates(array); + array.sort(utils.sortSecrets); + array.forEach((next, idx) => next.version.should.equal(`0${9 - idx}`)); + }); + + it("should sort by name in ascending order, then version in descending order", () => { + const array = Array.from({ length: 100 }, (k, i) => ({ + name: `0${i % 10}`, + version: `0${Math.floor(i / 10)}`, + })); + fisherYates(array); + fisherYates(array); + fisherYates(array); + array.sort(utils.sortSecrets); + array.forEach((next, idx) => { + const name = `0${Math.floor(idx / 10)}`; + const version = `0${Math.floor((99 - idx) % 10)}`; + next.name.should.equal(name); + next.version.should.equal(version); + }); + }); + }); + + describe("#asPromise", () => { + it("should return a promise", () => { + const result = utils.asPromise({}, () => {}); + expect(result.then).to.exist; + }); + + it("should insert a callback", () => { + const fn = function (cb) { + expect(cb).to.exist; + cb(undefined, "Success"); + }; + return utils.asPromise({}, fn).then((res) => res.should.equal("Success")); + }); + + it("should handle successful calls", () => { + const fn = function (cb) { + cb(undefined, "Success"); + }; + return utils.asPromise({}, fn).then((res) => res.should.equal("Success")); + }); + + it("should insert the correct arguments", () => { + const arg1 = "arg1"; + const arg2 = "arg2"; + + const fn = function (one, two, cb) { + expect(one).to.exist; + one.should.equal(arg1); + expect(two).to.exist; + two.should.equal(arg2); + expect(cb).to.exist; + cb(undefined, "Success"); + }; + + return utils + .asPromise({}, fn, arg1, arg2) + .then((res) => res.should.equal("Success")); + }); + + it("should handle errors", () => { + const fn = function (cb) { + cb(new Error("Error")); + }; + return utils + .asPromise({}, fn) + .then((res) => expect(res).to.not.exist) + .catch((err) => err.message.should.equal("Error")); + }); + }); + + describe("#mapPromise", () => { + it("calls the promises in order", function () { + this.timeout(10e3); + const array = Array.from({ length: 5 }, (v, k) => k); + let finishedOrder = []; + + function updatFinished(idx) { + finishedOrder.push(idx); + } + + return utils + .mapPromise(array, (i) => + new Promise((resolve) => setTimeout(resolve, 100 * (10 - i), i)).then( + updatFinished + ) + ) + .then(() => { + finishedOrder.forEach((next, i) => next.should.equal(i)); + finishedOrder = []; + }) + .then(() => + Promise.all( + array.map((next, i) => + new Promise((resolve) => + setTimeout(resolve, 100 * (10 - i), i) + ).then(updatFinished) + ) + ) + ) + .then(() => + finishedOrder.forEach((next, i) => next.should.equal(4 - i)) + ); + }); + }); +}); diff --git a/test/mocha.js b/test/mocha.js deleted file mode 100755 index 431d1e5..0000000 --- a/test/mocha.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -const args = process.argv || []; -const Mocha = require('mocha'); -const glob = require('glob'); -const istanbul = require('istanbul'); -const rmrf = require('rimraf'); - -/** - * Specify files needed for testing - */ -const files = glob.sync('./js/**/*.spec.js'); -const testOuputFolder = './test/results/'; -rmrf.sync(testOuputFolder); - -/** - * Set up an environment variables we need for testing - */ - -/** - * Set up the mocha options we want, like don't quit if a test fails - * @type {{bail: boolean}} - */ -const mochaConfig = { - bail: false, -}; - - -function startMocha(func) { - const fn = func || process.exit; - const mocha = new Mocha(mochaConfig); - - files.forEach(mocha.addFile.bind(mocha)); - - mocha.run(fn); -} - -/** - * Add junit reporting - */ -if (args.indexOf('junit') >= 0) { - mochaConfig.reporter = 'mocha-junit-reporter'; - mochaConfig.reporterOptions = { - mochaFile: `${testOuputFolder}test-result.xml`, - }; -} - -/** - * Add coverage reports when testing. - */ -if (args.indexOf('coverage') >= 0) { - const instrumenter = new istanbul.Instrumenter(); - const collector = new istanbul.Collector(); - - const coberturaReport = istanbul.Report.create('cobertura', { dir: testOuputFolder }); - const lcovReport = istanbul.Report.create('lcov', { dir: testOuputFolder }); - - istanbul.matcherFor({ - includes: ['**/*.js'], - excludes: ['**/test/**', '**/node_modules/**'], - }, (error, matcher) => { - istanbul.hook.hookRequire(matcher, instrumenter.instrumentSync.bind(instrumenter)); - - startMocha((results) => { - collector.add(__coverage__); // eslint-disable-line no-undef - - lcovReport.on('done', () => process.exit(results)); - coberturaReport.on('done', () => lcovReport.writeReport(collector)); - - coberturaReport.writeReport(collector); - }); - }); -} else { - startMocha(); -} diff --git a/js/test/setup.js b/test/setup.js similarity index 72% rename from js/test/setup.js rename to test/setup.js index 14abc23..eeee795 100755 --- a/js/test/setup.js +++ b/test/setup.js @@ -1,7 +1,7 @@ -'use strict'; +"use strict"; -const chai = require('chai'); -chai.use(require('chai-string')); +const chai = require("chai"); +chai.use(require("chai-string")); chai.config.includeStack = true; diff --git a/js/test/utils/encryption.js b/test/utils/encryption.js similarity index 63% rename from js/test/utils/encryption.js rename to test/utils/encryption.js index afd5efd..6fd27f2 100644 --- a/js/test/utils/encryption.js +++ b/test/utils/encryption.js @@ -1,31 +1,37 @@ -'use strict'; +"use strict"; module.exports = { item: { - name: 'quotation', - version: '0000000000000000001', - key: 'quotationKey', + name: "quotation", + version: "0000000000000000001", + key: "quotationKey", plainText: `"Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit..." "There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain..."`, - contents: 'WfAxFvTQQaAWaLj7BgFeYc6wtpqdoaT0hLfT8gQyo2FHKEcEBETUG7f2sQpSkjZzo7660h0POrVYTG26I5xsmzbX7fJkzZ7qDbtsti/ucF7GUpyJTN6PQIbrfEPH7F5zYV7dUeca/awLUZJOq1qDqGYy6hCBdJqh2KnPDLc4Ewl5r00wXNQBzYf5w1JVmNLS3vfOM20wY5js3pLdcYUa/XdrfO8jg2JcRZbsL1dQWUDCK9SesI0CHyiMwJCMkBLq9BwiNzJZayzjIrfF65rw', - hmacMd5: '570ae3cf8d5bf0c0b26bfea941a84bfb', - hmacSha256: '74feaf25b284d319aa1342ebaadca39813465895ad4f924c2e7e45a77ee0b010', - hmacSha512: '61f7bb0d82cb8dee073529fc6a5c99486ca81b1a3800dd08f569afe381ab78f7a6eb32276491b0a8c47a9d6bf9446d731cb9a82e002b3c46349cbcba1d4bb999', + contents: + "WfAxFvTQQaAWaLj7BgFeYc6wtpqdoaT0hLfT8gQyo2FHKEcEBETUG7f2sQpSkjZzo7660h0POrVYTG26I5xsmzbX7fJkzZ7qDbtsti/ucF7GUpyJTN6PQIbrfEPH7F5zYV7dUeca/awLUZJOq1qDqGYy6hCBdJqh2KnPDLc4Ewl5r00wXNQBzYf5w1JVmNLS3vfOM20wY5js3pLdcYUa/XdrfO8jg2JcRZbsL1dQWUDCK9SesI0CHyiMwJCMkBLq9BwiNzJZayzjIrfF65rw", + hmacMd5: "570ae3cf8d5bf0c0b26bfea941a84bfb", + hmacSha256: + "74feaf25b284d319aa1342ebaadca39813465895ad4f924c2e7e45a77ee0b010", + hmacSha512: + "61f7bb0d82cb8dee073529fc6a5c99486ca81b1a3800dd08f569afe381ab78f7a6eb32276491b0a8c47a9d6bf9446d731cb9a82e002b3c46349cbcba1d4bb999", kms: { get Plaintext() { - return Buffer.from('Expenses as material breeding insisted building to in. Continual', 'UTF8'); + return Buffer.from( + "Expenses as material breeding insisted building to in. Continual", + "UTF8" + ); }, get CiphertextBlob() { - return Buffer.from('123'); + return Buffer.from("123"); }, }, }, credstashKey: { - name: 'some.secret.apiKey', - contents: 'RPg3JNZPfZJZ80pq7qQ=', - version: '0000000000000000001', - hmac: '910af4beee82fc5717cddf28c7c16c38d3c2e74424b3cc928869b28293ab937e', - digest: 'SHA256', + name: "some.secret.apiKey", + contents: "RPg3JNZPfZJZ80pq7qQ=", + version: "0000000000000000001", + hmac: "910af4beee82fc5717cddf28c7c16c38d3c2e74424b3cc928869b28293ab937e", + digest: "SHA256", get key() { return Buffer.from([ 254, @@ -44,9 +50,9 @@ module.exports = { 202, 0, 252, - ]).toString('base64'); + ]).toString("base64"); }, - plainText: 'someLongAPIKey', + plainText: "someLongAPIKey", kms: { get CiphertextBlob() { return Buffer.from([