diff --git a/.eslintrc b/.eslintrc index 9279f50..d228d2d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,6 @@ { "parserOptions": { - "ecmaVersion": 2017 + "ecmaVersion": 2018 }, "env": { diff --git a/cdk-iam.json b/cdk-iam.json new file mode 100644 index 0000000..21cce74 --- /dev/null +++ b/cdk-iam.json @@ -0,0 +1,25 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": ["cloudformation:*"], + "Resource": "*", + "Effect": "Allow" + }, + { + "Condition": { + "ForAnyValue:StringEquals": { + "aws:CalledVia": ["cloudformation.amazonaws.com"] + } + }, + "Action": "*", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "s3:*", + "Resource": "arn:aws:s3:::cdktoolkit-stagingbucket-*", + "Effect": "Allow" + } + ] +} diff --git a/index.js b/index.js index 40af797..dcc7666 100644 --- a/index.js +++ b/index.js @@ -9,6 +9,9 @@ const { createLogger } = require("./lib/io/logs"); const OutputService = require("./lib/io/output-service"); const { registerCommands } = require("./lib/commander"); const { Builder } = require("./lib/container"); +const { CognitoAuthAdapter } = require("./lib/auth/cognito/adapter"); +const AuthService = require("./lib/auth/service"); +const AuthCommand = require("./lib/auth/command"); const ConfigCommand = require("./lib/config/command"); const PluginCommand = require("./lib/plugin/command"); const ConfigService = require("./lib/config/service"); @@ -146,6 +149,11 @@ class Miles { builder.register("plugin.command", PluginCommand.create, [ "commander-visitor", ]); + builder.register("auth.adapter.cognito", CognitoAuthAdapter.create, [ + "auth-adapter", + ]); + builder.register("auth.service", AuthService.create); + builder.register("auth.command", AuthCommand.create, ["commander-visitor"]); return await builder.build(); } diff --git a/lib/auth/adapter.js b/lib/auth/adapter.js new file mode 100644 index 0000000..06ca68f --- /dev/null +++ b/lib/auth/adapter.js @@ -0,0 +1,72 @@ +const { User } = require("./user"); + +const NAME = Symbol("name"); +const USER = Symbol("user"); + +/** + * An abstract superclass for authentication adapters. + */ +class AuthAdapter { + /** + * Creates a new AuthAdapter. + * + * @param {string} name - The name of the adapter. + */ + constructor(name) { + this[NAME] = `${name}`; + this[USER] = null; + } + + /** + * Gets the adapter's name. + */ + get name() { + return this[NAME]; + } + + /** + * @private + */ + get missingUser() { + return this[USER] === null; + } + + /** + * Gets the current user. + * + * @return {User} The current user (may be anonymous or authenticated). + */ + get user() { + return this[USER] || new User(); + } + + /** + * Sets the current user. + * + * @param {User} value - The user to set. + */ + set user(value) { + if (!(value instanceof User)) { + throw new TypeError("Value must be an instance of the User class"); + } + this[USER] = value; + } + + /** + * Gets the initial login challenge. + * + * @return {Challenge} The login challenge. + */ + logIn() { + throw new Error("This method must be implemented in a subclass"); + } + + /** + * Clears all saved authentication information. + */ + logOut() { + throw new Error("This method must be implemented in a subclass"); + } +} + +module.exports = { AuthAdapter }; diff --git a/lib/auth/cognito/adapter.js b/lib/auth/cognito/adapter.js new file mode 100644 index 0000000..a2e3b00 --- /dev/null +++ b/lib/auth/cognito/adapter.js @@ -0,0 +1,128 @@ +const { + CognitoUserPool, + CognitoUser, + AuthenticationDetails, +} = require("amazon-cognito-identity-js"); +const { User } = require("../user"); +const { AuthAdapter } = require("../adapter"); +const { CognitoFacade } = require("./facade"); +const { CognitoStorage } = require("./storage"); +const { CognitoIdpUser } = require("./user"); +const { CognitoLoginChallenge } = require("./result"); + +const CONFIG_SERVICE = Symbol("configService"); +const SECRET_SERVICE = Symbol("secretService"); +const FACADE = Symbol("facade"); + +/** + * An authentication adapter for Amazon Cognito. + */ +class CognitoAuthAdapter extends AuthAdapter { + /** + * Creates a new CognitoAuthAdapter. + * + * @param {ConfigService} configService - The config service. + * @param {SecretService} secretService - The secret service. + */ + constructor(configService, secretService) { + super("cognito"); + this[CONFIG_SERVICE] = configService; + this[SECRET_SERVICE] = secretService; + } + + /** + * Creates a new AuthService. + * + * @param {container.Container} - The dependency injection container. + */ + static async create(container) { + const [configService, secretService] = await container.getAll([ + "config.service", + "secret.service", + ]); + return new CognitoAuthAdapter(configService, secretService); + } + + /** + * Creates a new CognitoUserPool. + * + * @private + * @param {CognitoStorage} clientStorage - The credentials storage map. + * @return {@amazon-cognito-identity-js.CognitoUserPool} The new user pool + */ + createCognitoUserPool(clientStorage) { + return new CognitoUserPool({ + UserPoolId: this[CONFIG_SERVICE].get("auth", "cognito.user-pool"), + ClientId: this[CONFIG_SERVICE].get("auth", "cognito.app-client-id"), + Storage: clientStorage, + }); + } + + /** + * Creates a new CognitoStorage. + * + * @private + * @return {CognitoStorage} The credentials storage map + */ + createCognitoStorage() { + return CognitoStorage.create(this[SECRET_SERVICE]); + } + + /** + * @return {CognitoFacade} The cognito facade + */ + get cognitoFacade() { + if (!this[FACADE]) { + const clientStorage = this.createCognitoStorage(); + const userPool = this.createCognitoUserPool(clientStorage); + this[FACADE] = new CognitoFacade(userPool, clientStorage); + const saveSecrets = () => { + clientStorage.store(this[SECRET_SERVICE]).then(() => {}); + }; + this[FACADE].addEventListener("login", saveSecrets); + this[FACADE].addEventListener("logout", saveSecrets); + } + return this[FACADE]; + } + + /** + * Gets the current user. + * + * @return {User} The current user (may be anonymous or authenticated) + */ + get user() { + if (super.missingUser) { + const cu = this.cognitoFacade.currentUser; + this.user = + cu === null ? new User() : new CognitoIdpUser(cu.username, cu); + } + return super.user; + } + + /** + * Sets the current user. + * + * @param {User} The current user + */ + set user(user) { + super.user = user; + } + + /** + * Gets the initial login challenge. + * + * @return {CognitoLoginChallenge} The login challenge. + */ + logIn() { + return new CognitoLoginChallenge(this.cognitoFacade); + } + + /** + * Clears all saved authentication information. + */ + async logOut() { + return await this.cognitoFacade.logOut(); + } +} + +module.exports = { CognitoAuthAdapter }; diff --git a/lib/auth/cognito/facade.js b/lib/auth/cognito/facade.js new file mode 100644 index 0000000..2969e9a --- /dev/null +++ b/lib/auth/cognito/facade.js @@ -0,0 +1,190 @@ +const { EventTarget, Event } = require("event-target-shim"); +const { + CognitoUserPool, + CognitoUser, + AuthenticationDetails, +} = require("amazon-cognito-identity-js"); + +const CLIENT_STORAGE = Symbol("clientStorage"); +const USER_POOL = Symbol("userPool"); + +/** + * A wrapper for the Amazon Cognito library. + */ +class CognitoFacade extends EventTarget { + /** + * Creates a new CognitoFacade. + * + * @param {@amazon-cognito-identity-js.CognitoUserPool} userPool - The User Pool + * @param {CognitoStorage} clientStorage - The auth creds storage object + */ + constructor(userPool, clientStorage) { + super(); + this[USER_POOL] = userPool; + this[CLIENT_STORAGE] = clientStorage; + } + + /** + * Gets the Cognito User Pool object. + * + * @return {@amazon-cognito-identity-js.CognitoUserPool} The User Pool. + */ + get userPool() { + return this[USER_POOL]; + } + + /** + * Gets the Cognito auth creds storage object. + * + * @return {CognitoStorage} The Storage API implementing object for Cognito. + */ + get clientStorage() { + return this[CLIENT_STORAGE]; + } + + /** + * Gets the current authenticated user or `null`. + * + * @return {@amazon-cognito-identity-js.CognitoUser|null} The current user + */ + get currentUser() { + return this.userPool.getCurrentUser(); + } + + /** + * Creates a new CognitoUser. + * + * @private + * @param {string} username - The username of the user. + * @return {@amazon-cognito-identity-js.CognitoUser} The new user + */ + createCognitoUser(username) { + return new CognitoUser({ + Username: username, + Pool: this.userPool, + Storage: this.clientStorage, + }); + } + + /** + * Returns a callback object for use with the Cognito library. + * + * @param {@amazon-cognito-identity-js.CognitoUser} user - The user + * @param {Function} resolve - The promise resolve function + * @param {Function} reject - The promise reject function + * @return {Object} The callback object + * @private + */ + createCallbackObject(user, resolve, reject) { + const self = this; + const challengeResolver = (name, parameters) => { + resolve({ incomplete: true, challenge: true, name, parameters, user }); + }; + return { + onSuccess: (session, userConfirmationNecessary) => { + self.dispatchEvent(new Event("login")); + resolve({ incomplete: false, user, username: user.username, session }); + }, + onFailure: (err) => { + if (err.name === "PasswordResetRequiredException") { + resolve({ + incomplete: true, + challenge: true, + name: "PASSWORD_RESET_REQUIRED", + user, + }); + } else { + reject(err); + } + }, + newPasswordRequired: (userAttributes, requiredAttributes) => { + resolve({ + incomplete: true, + challenge: true, + name: "NEW_PASSWORD_REQUIRED", + requiredAttributes, + user, + }); + }, + mfaRequired: challengeResolver, + totpRequired: challengeResolver, + customChallenge: (challengeParameters) => { + challengeResolver("CUSTOM_CHALLENGE", challengeParameters); + }, + mfaSetup: challengeResolver, + selectMFAType: challengeResolver, + }; + } + + /** + * Authenticates using the provided details. + * + * @return {Promise} A promise that resolves to the Cognito authentication. + */ + logIn(username, password) { + const authenticationDetails = new AuthenticationDetails({ + Username: username, + Password: password, + }); + const cognitoUser = this.createCognitoUser(username); + return new Promise((resolve, reject) => { + cognitoUser.authenticateUser( + authenticationDetails, + this.createCallbackObject(cognitoUser, resolve, reject) + ); + }); + } + + logOut() { + const self = this; + const cognitoUser = this.currentUser; + if (!cognitoUser) { + throw new Error("There is no user to log out"); + } + return new Promise((resolve, reject) => { + cognitoUser.signOut((err) => { + if (err instanceof Error) { + reject(err); + } + self.dispatchEvent(new Event("logout")); + resolve(); + }); + }); + } + + /** + * Provides the password to respond to the new-password-needed challenge. + * + * @param {@amazon-cognito-identity-js.CognitoUser} user - The user + * @param {string} password – The new password. + * @param {Object} userAttributes - The user attributes to complete signup. + */ + confirmNewPassword(cognitoUser, password, userAttributes = {}) { + return new Promise((resolve, reject) => { + cognitoUser.completeNewPasswordChallenge( + password, + userAttributes, + this.createCallbackObject(cognitoUser, resolve, reject) + ); + }); + } + + /** + * Provides the password to respond to the password reset challenge. + * + * @param {@amazon-cognito-identity-js.CognitoUser} user - The user + * @param {string} verificationCode – The verification code. + * @param {string} password – The new password. + */ + confirmPasswordReset(cognitoUser, verificationCode, password) { + return new Promise((resolve, reject) => { + cognitoUser.confirmPassword( + verificationCode, + password, + this.createCallbackObject(cognitoUser, resolve, reject) + ); + }); + } +} + +module.exports = { CognitoFacade }; diff --git a/lib/auth/cognito/result.js b/lib/auth/cognito/result.js new file mode 100644 index 0000000..07e7bac --- /dev/null +++ b/lib/auth/cognito/result.js @@ -0,0 +1,254 @@ +const { Challenge, Authentication } = require("../result"); +const { Prompt, createEmptyValidator } = require("../../io/prompt"); +const { CognitoIdpUser } = require("./user"); + +const REQUIRED_ATTRIBUTES = Symbol("requiredAttributes"); +const COGNITO_USER = Symbol("cognitoUser"); +const COGNITO_USER_SESSION = Symbol("cognitoUserSession"); +const FACADE = Symbol("facade"); + +/** + * Superclass for Cognito challenges. + */ +class CognitoChallenge extends Challenge { + /** + * Creates a new CognitoChallenge. + * + * @param {CognitoFacade} facade - The Cognito facade + */ + constructor(facade) { + super(); + this[FACADE] = facade; + } + + /** + * Translates responses from the Cognito facade into subclasses of Result. + * + * @param {Object} result - The raw result from the Cognito facade + * @return {Result} An authentication result + */ + processCognitoResult(result) { + const cognitoUser = result.user; + if (!result.incomplete) { + const username = result.username; + const user = new CognitoIdpUser(username, cognitoUser); + return new CognitoAuthentication(user, result.session); + } else if (result.challenge) { + if (result.name === "NEW_PASSWORD_REQUIRED") { + return new CognitoNewPasswordChallenge( + this[FACADE], + cognitoUser, + result.requiredAttributes + ); + } else if (result.name === "PASSWORD_RESET_REQUIRED") { + return new CognitoPasswordResetChallenge(this[FACADE], cognitoUser); + } else { + throw new Error(`Unknown challenge type: ${result.name}`); + } + } else { + throw new Error("Unknown result: neither complete nor a challenge"); + } + } +} + +/** + * The initial challenge to provide a username and password. + */ +class CognitoLoginChallenge extends CognitoChallenge { + async prompt(inputService, outputService) { + const usernamePrompt = new Prompt({ intro: "Username" }); + const passwordPrompt = new Prompt({ intro: "Password", hidden: true }); + const username = await inputService.dispatch(usernamePrompt); + const password = await inputService.dispatch(passwordPrompt); + return { username, password }; + } + + /** + * Submit a response to the challenge. + * + * @param {Object} response - The challenge response + * @param {string} response.username - The username + * @param {string} response.password - The password + * @return {Result} The authentication result (possibly another challenge) + */ + async complete(response) { + const { username, password } = response; + const result = await this[FACADE].logIn(username, password); + return this.processCognitoResult(result); + } +} + +/** + * Abstract challenge for password creation or reset. + */ +class CognitoPasswordChallenge extends CognitoChallenge { + /** + * Creates a new CognitoPasswordChallenge. + * + * @param {CognitoFacade} facade - The Cognito facade + * @param {@amazon-cognito-identity-js.CognitoUser} cognitoUser - The cognito user + */ + constructor(facade, cognitoUser) { + super(facade); + this[COGNITO_USER] = cognitoUser; + } + + /** + * Prompts the user for a new password. + * + * @return {Promise} The newly entered password + */ + async prompt(inputService, outputService) { + const validator = createEmptyValidator("Password cannot be blank"); + + const passwordPrompt = new Prompt({ + name: "new-password", + intro: "New Password", + hidden: true, + validator, + }); + const confirmPrompt = new Prompt({ + name: "new-password-confirm", + intro: "Confirm New Password", + hidden: true, + validator, + }); + + outputService.write("You must create a new password."); + let password; + do { + const newPassword = await inputService.dispatch(passwordPrompt); + const newPasswordConfirm = await inputService.dispatch(confirmPrompt); + if (newPassword !== newPasswordConfirm) { + outputService.error("Passwords do not match"); + } else { + password = newPassword; + } + } while (password === undefined || password === ""); + + return { password }; + } +} + +/** + * A challenge to reset a password. + */ +class CognitoPasswordResetChallenge extends CognitoPasswordChallenge { + /** + * Prompts the user for a new password and verification code. + * + * @return {Promise} The newly entered password + */ + async prompt(inputService, outputService) { + const { password } = await super.prompt(inputService, outputService); + const validator = createEmptyValidator("Verification code cannot be blank"); + const prompt = new Prompt({ + name: "verification-code", + intro: "Verification Code", + validator, + }); + outputService.write("A verification code was sent to you"); + let verificationCode; + do { + verificationCode = await inputService.dispatch(prompt); + } while (verificationCode === undefined || verificationCode === ""); + + return { password, verificationCode }; + } + + /** + * Submit a response to the challenge. + * + * @param {Object} response - The challenge response + * @param {string} response.verificationCode - The verification code + * @param {string} response.password - The password + * @return {Result} The authentication result (possibly another challenge) + */ + async complete(response) { + const { verificationCode, password } = response; + const result = await this[FACADE].confirmPasswordReset( + this[COGNITO_USER], + verificationCode, + password + ); + return this.processCognitoResult(result); + } +} + +/** + * A challenge to create a new password. + */ +class CognitoNewPasswordChallenge extends CognitoPasswordChallenge { + /** + * Creates a new CognitoNewPasswordChallenge. + * + * @param {CognitoFacade} facade - The Cognito facade + * @param {@amazon-cognito-identity-js.CognitoUser} cognitoUser - The cognito user + * @param {Array} requiredAttributes - The attributes required for users + */ + constructor(facade, cognitoUser, requiredAttributes) { + super(facade, cognitoUser); + this[REQUIRED_ATTRIBUTES] = requiredAttributes; + } + + /** + * Gets the required attributes for users in the user pool. + * @return {Array} The list of required attributes. + */ + get requiredAttributes() { + return this[REQUIRED_ATTRIBUTES]; + } + + /** + * Submit a response to the challenge. + * + * @param {Object} response - The challenge response + * @param {string} response.password - The password + * @param {string} response.userAttributes - The required attribute values + * @return {Result} The authentication result (possibly another challenge) + */ + async complete(response) { + const { password, userAttributes } = response; + const result = await this[FACADE].confirmNewPassword( + this[COGNITO_USER], + password, + userAttributes + ); + return this.processCognitoResult(result); + } +} + +/** + * A successful Cognito authentication result. + */ +class CognitoAuthentication extends Authentication { + /** + * Creates a new CognitoAuthentication. + * + * @param {CognitoIdpUser} user - The wrapped Cognito user + * @param {@amazon-cognito-identity-js.CognitoUserSession} session - The cognito user session + */ + constructor(user, cognitoUserSession) { + const idToken = cognitoUserSession.getIdToken().getJwtToken(); + const accessToken = cognitoUserSession.getAccessToken().getJwtToken(); + const refreshToken = cognitoUserSession.getRefreshToken().getToken(); + super(user, idToken, accessToken, refreshToken); + this[COGNITO_USER_SESSION] = cognitoUserSession; + } + + /** + * @return {@amazon-cognito-identity-js.CognitoUserSession} The user session + */ + get session() { + return this[COGNITO_USER_SESSION]; + } +} + +module.exports = { + CognitoChallenge, + CognitoLoginChallenge, + CognitoPasswordChallenge, + CognitoNewPasswordChallenge, + CognitoPasswordResetChallenge, + CognitoAuthentication, +}; diff --git a/lib/auth/cognito/storage.js b/lib/auth/cognito/storage.js new file mode 100644 index 0000000..3921da6 --- /dev/null +++ b/lib/auth/cognito/storage.js @@ -0,0 +1,98 @@ +const VALUES = Symbol("values"); +const NAMESPACE = "auth.cognito"; + +/** + * Stores cognito credentials using the SecretService. + * + * The amazon-cognito-identity-js library allows for custom storage of auth data + * (by default, it has a memory-based store and one for browser local storage). + * This class implements the same API. + */ +class CognitoStorage { + /** + * Creates a new CognitoStorage. + * + * @param {Map} values - The secret values. + */ + constructor(values) { + this[VALUES] = values || new Map(); + } + + /** + * Creates a new CognitoStorage. + * + * @return {CognitoStorage} A new instance of this class + */ + static create(secretService) { + return new CognitoStorage(secretService.all(NAMESPACE)); + } + + /** + * When passed a key name and value, will add that key to the storage, or + * update that key's value if it already exists. + * + * @param {string} key - the key for the item + * @param {object} value - the value + */ + setItem(key, value) { + this[VALUES].set(key, value); + } + + /** + * When passed a key name, will return that key's value. + * + * @param {string} key - the key for the item + * @returns {any} the data item + */ + getItem(key) { + return this[VALUES].has(key) ? this[VALUES].get(key) : null; + } + + /** + * When passed a key name, will remove that key from the storage. + * + * @param {string} key - the name of the key you want to remove + */ + removeItem(key) { + this[VALUES].delete(key); + } + + /** + * When invoked, will empty all keys out of the storage. + */ + clear() { + this[VALUES].clear(); + } + + /** + * @return {int} The length of the Map. + */ + get length() { + return this[VALUES].size; + } + + /** + * @return {Map} gets a copy of the values. + */ + get values() { + return this[VALUES]; + } + + /** + * Saves the cognito values to disk. + * + * @param {SecretService} secretService - The secret service. + */ + async store(secretService) { + if (this[VALUES].size === 0) { + secretService.clear(NAMESPACE); + } else { + for (const [key, value] of this[VALUES]) { + secretService.set(NAMESPACE, key, value); + } + } + await secretService.save(); + } +} + +module.exports = { CognitoStorage }; diff --git a/lib/auth/cognito/user.js b/lib/auth/cognito/user.js new file mode 100644 index 0000000..a6cafe6 --- /dev/null +++ b/lib/auth/cognito/user.js @@ -0,0 +1,52 @@ +const { CognitoUser } = require("amazon-cognito-identity-js"); +const { User } = require("../user"); + +const COGNITO_USER = Symbol("cognitoUser"); + +class CognitoIdpUser extends User { + constructor(username, cognitoUser) { + super(username); + this[COGNITO_USER] = cognitoUser; + } + + get provider() { + return "cognito"; + } + + /** + * @return {@amazon-cognito-identity-js.CognitoUser} The Cognito user + */ + get cognitoUser() { + return this[COGNITO_USER]; + } + + /** + * Resume a Cognito session if possible. + * + * @return {Promise<@amazon-cognito-identity-js.CognitoUserSession>} The promised CognitoUserSession + */ + getCognitoSession() { + return new Promise((resolve, reject) => { + this[COGNITO_USER].getSession((err, session) => { + if (err) { + reject(err); + } else { + if (session.isValid()) { + resolve(session); + } else { + reject(new Error("Cognito session refreshed but is invalid")); + } + } + }); + }); + } + + async getLabel() { + const session = await this.getCognitoSession(); + return `Amazon Cognito user: ${session.getIdToken().payload.email} (${ + this.username + })`; + } +} + +module.exports = { CognitoIdpUser }; diff --git a/lib/auth/command.js b/lib/auth/command.js new file mode 100644 index 0000000..35e6fb8 --- /dev/null +++ b/lib/auth/command.js @@ -0,0 +1,121 @@ +const { Command } = require("commander"); +const { AbstractCommand } = require("../commander"); +const { Challenge } = require("./result"); + +const SERVICE = Symbol("service"); +const OUTPUT_SERVICE = Symbol("outputService"); +const INPUT_SERVICE = Symbol("inputService"); + +/** + * Handles the `miles auth` command. + */ +class AuthCommand extends AbstractCommand { + /** + * Creates a new AuthCommand. + * + * @param {AuthService} authService - The authentication service. + * @param {OutputService} outputService - The output service. + * @param {InputService} inputService - The input service. + */ + constructor(authService, outputService, inputService) { + super(); + this[SERVICE] = authService; + this[OUTPUT_SERVICE] = outputService; + this[INPUT_SERVICE] = inputService; + } + + /** + * Factory function. + * + * @param {container.Container} container - The dependency injection container. + * @return {ConfigCommand} a new instance of this class. + */ + static async create(container) { + const [authService, outputService, inputService] = await container.getAll([ + "auth.service", + "io.output-service", + "io.input-service", + ]); + return new AuthCommand(authService, outputService, inputService); + } + + /** + * Creates a Commander command to be added to the Miles program. + * + * @return {commander.Command} the Commander command to register. + */ + createCommand() { + const command = new Command("auth"); + const loginCommand = new Command("login"); + const logoutCommand = new Command("logout"); + const whoamiCommand = new Command("whoami"); + return command + .description("Authentication related commands") + .addCommand( + whoamiCommand + .description("Gets current authenticated username") + .action(this.whoami.bind(this)) + ) + .addCommand( + logoutCommand.description("Logout").action(this.logout.bind(this)) + ) + .addCommand( + loginCommand.description("Login").action(this.login.bind(this)) + ); + } + + /** + * The whomai command. + */ + async whoami() { + const user = this[SERVICE].user; + const username = user.anonymous + ? "You are not logged in" + : `${await user.getLabel()}`; + this[OUTPUT_SERVICE].write(username); + } + + /** + * The login command. + */ + async login() { + const user = this[SERVICE].user; + + if (!user.anonymous) { + this[OUTPUT_SERVICE].write( + `You're already logged in as ${await user.getLabel()}` + ); + return; + } + + let result = this[SERVICE].logIn(); + while (result instanceof Challenge) { + const challenge = result; + const response = await challenge.prompt( + this[INPUT_SERVICE], + this[OUTPUT_SERVICE] + ); + result = await this[OUTPUT_SERVICE].spinForPromise( + challenge.complete(response), + "Submitting challenge response" + ); + } + + this[OUTPUT_SERVICE].write( + `You're logged in as: ${await result.user.getLabel()}` + ); + } + + async logout() { + const user = this[SERVICE].user; + + if (user.anonymous) { + this[OUTPUT_SERVICE].write("You are not logged in"); + return; + } + + this[OUTPUT_SERVICE].spinForPromise(this[SERVICE].logOut(), "Logging out"); + } +} + +module.exports = AuthCommand; diff --git a/lib/auth/result.js b/lib/auth/result.js new file mode 100644 index 0000000..3b00c1b --- /dev/null +++ b/lib/auth/result.js @@ -0,0 +1,116 @@ +const { CognitoIdpUser } = require("./user"); + +const USER = Symbol("user"); +const ID_TOKEN = Symbol("idToken"); +const ACCESS_TOKEN = Symbol("accessToken"); +const REFRESH_TOKEN = Symbol("refreshToken"); + +/** + * Abstract superclass for results from authentication adapters. + */ +class Result { + /** + * Whether the result is incomplete (e.g. new password needed, MFA challenge) + * @return {boolean} True if incomplete, false if authentication succeeded + */ + get incomplete() { + return true; + } + + /** + * Whether this result represents a challenge that needs a response. + * @return {boolean} True if this result is a challenge + */ + get challenge() { + return false; + } +} + +/** + * Abstract superclass for results representing a challenge requiring response. + */ +class Challenge extends Result { + get challenge() { + return true; + } + + /** + * Prompt the user for the challenge response. + * + * @param {InputService} inputService - The input service. + * @param {OutputService} outputService - The output service. + * @return {Promise} The received input. + */ + prompt(inputService, outputService) { + return Promise.resolve(undefined); + } + + /** + * Submit a response to the challenge. + * + * @param {any} response - The challenge response + * @return {Result} The authentication result (possibly another challenge) + */ + complete(response) { + throw new Error("Subclasses must override this method"); + } +} + +/** + * A response that represents successful authentication. + */ +class Authentication extends Result { + /** + * Creates a new Authentication. + * + * @param {User} user - The authenticated user + * @param {string} idToken - The JWT identity token + * @param {string} accessToken - The JWT access token + * @param {string} refreshToken - The JWT refresh token + */ + constructor(user, idToken, accessToken, refreshToken) { + super(); + this[USER] = user; + this[ID_TOKEN] = idToken; + this[ACCESS_TOKEN] = accessToken; + this[REFRESH_TOKEN] = refreshToken; + } + + get incomplete() { + return false; + } + + /** + * Gets the authenticated user. + * @return {User} The authenticated user + */ + get user() { + return this[USER]; + } + + /** + * Gets the JWT identity token. + * @return {string} The JWT identity token + */ + get idToken() { + return this[ID_TOKEN]; + } + + /** + * Gets the JWT access token. + * @return {string} The JWT access token + */ + get accessToken() { + return this[ACCESS_TOKEN]; + } + + /** + * Gets the JWT refresh token. + * @return {string} The JWT refresh token + */ + get refreshToken() { + return this[REFRESH_TOKEN]; + } +} + +module.exports = { Result, Challenge, Authentication }; diff --git a/lib/auth/service.js b/lib/auth/service.js new file mode 100644 index 0000000..688ebbc --- /dev/null +++ b/lib/auth/service.js @@ -0,0 +1,73 @@ +const CONFIG_SERVICE = Symbol("configService"); +const AUTH_ADAPTER = Symbol("authAdapter"); + +/** + * Locates the authentication adapter. + * + * @param {container.Container} container - The dependency injection container + * @param {string} [name=cognito] - The name of the adapter to locate. + * @return {AuthAdapter} The authentication adapter found. + */ +async function locateAuthAdapter(container, name = "cognito") { + let authAdapter; + + if (container.has(`auth.adapter.${name}`)) { + authAdapter = await container.get(`auth.adapter.${name}`); + } else { + const adapters = await container.getAllTagged("auth-adapter"); + authAdapter = adapters.find((a) => name === a.name); + } + + if (!authAdapter) { + throw new Error(`No authentication adapter found for name: ${name}`); + } + + return authAdapter; +} + +/** + * Handles authentication. + */ +class AuthService { + /** + * Creates a new AuthService. + * + * @param {ConfigService} configService - The config service. + * @param {AuthAdapter} authAdapter - The auth adapter. + */ + constructor(configService, authAdapter) { + this[CONFIG_SERVICE] = configService; + this[AUTH_ADAPTER] = authAdapter; + } + + /** + * Creates a new AuthService. + * + * @param {container.Container} - The dependency injection container. + */ + static async create(container) { + const [configService] = await container.getAll(["config.service"]); + const authAdapterName = await configService.get("auth", "adapter"); + const authAdapter = await locateAuthAdapter(container, authAdapterName); + return new AuthService(configService, authAdapter); + } + + /** + * Gets the current user. + * + * @return {User} The current user (may be anonymous or authenticated) + */ + get user() { + return this[AUTH_ADAPTER].user; + } + + logIn() { + return this[AUTH_ADAPTER].logIn(); + } + + async logOut() { + return await this[AUTH_ADAPTER].logOut(); + } +} + +module.exports = AuthService; diff --git a/lib/auth/user.js b/lib/auth/user.js new file mode 100644 index 0000000..e69cb91 --- /dev/null +++ b/lib/auth/user.js @@ -0,0 +1,40 @@ +const USERNAME = Symbol("username"); + +/** + * An authenticated or anonymous user. + */ +class User { + /** + * Creates a new User. + * + * @param {string|null} [username=null] - The username (null if anonymous) + */ + constructor(username = null) { + this[USERNAME] = username; + } + + /** + * Gets the provider, as in, the adapter that created this object. + * + * @return {string} The provider name + */ + get provider() { + return "anonymous"; + } + + /** + * @return {boolean} Whether this user is anonymous + */ + get anonymous() { + return this[USERNAME] === null; + } + + /** + * @return {string|null} This user's username + */ + get username() { + return this[USERNAME]; + } +} + +module.exports = { User }; diff --git a/lib/config/value-set.js b/lib/config/value-set.js index fed40d3..83279b3 100644 --- a/lib/config/value-set.js +++ b/lib/config/value-set.js @@ -46,17 +46,55 @@ class ValueSet { this[VALUES][namespace][key] = value; } + /** + * Removes a value from the configuration. + * + * @param {string} namespace - The namespace under which to save the value. + * @param {string} key - The key to remove. + */ + remove(namespace, key) { + if (!(namespace in this[VALUES])) { + return; + } + delete this[VALUES][namespace][key]; + } + + /** + * Clears all values from a namespace. + * + * @param {string} namespace - The namespace under which to save the value. + */ + clear(namespace) { + if (!(namespace in this[VALUES])) { + return; + } + delete this[VALUES][namespace]; + } + /** * Gets a value from the configuration. * * @param {string} namespace - The namespace under which to save the value. * @param {string} key - The key under which to save the value. * @param {number|string|boolean} defaultValue - If not found, return this. + * @return {any} The value found, or the `defaultValue` parameter if none. */ get(namespace, key, defaultValue) { return (this[VALUES][namespace] || {})[key] || defaultValue; } + /** + * Gets all values in a namespace. + * + * @param {string} namespace - The namespace of the values to retrieve. + * @return {Map} All values within the given namespace. + */ + all(namespace) { + return namespace in this[VALUES] + ? new Map(Object.entries(this[VALUES][namespace])) + : new Map(); + } + /** * Exports a deep copy of the configuration values. * diff --git a/lib/io/input-service.js b/lib/io/input-service.js index cd73905..b9b2610 100644 --- a/lib/io/input-service.js +++ b/lib/io/input-service.js @@ -1,9 +1,25 @@ const promptly = require("promptly"); +const { Prompt } = require("./prompt"); /** * Controls input from stdin. */ class InputService { + /** + * Dispatches a Prompt object and retrieves the user input. + * + * @param {Prompt} prompt - The prompt object + * @return {any} The user response, filtered by the validator + */ + async dispatch(prompt) { + const { name, intro, hint, validator, hidden } = prompt; + const message = [`${intro}`]; + if (hint) { + message.push(` [${hint}]`); + } + return await this.prompt(message.join(""), validator, hidden); + } + /** * Prompts the user for a value. * @@ -15,12 +31,16 @@ class InputService { * * @param {string} message - The message to show when we prompt for the value. * @param {Function} validator - The validation function; must return sanitized value. + * @param {boolean} [password=false] – Provide `true` to mask input and not trim. * @return {Promise} resolves to the sanitized value, or throws when invalid. */ - async prompt(message, validator) { + async prompt(message, validator, password = false) { try { + const options = password + ? { silent: true, trim: false, replace: "*", default: "" } + : {}; // If the prompted value is invalid, promptly will catch error and retry. - return await promptly.prompt(message, { validator }); + return await promptly.prompt(message, { validator, ...options }); } catch (e) { // Handle the error caused by the user sending an interruption signal. if (e.name === "Error" && e.message === "canceled") { diff --git a/lib/io/output-service.js b/lib/io/output-service.js index 66ba68f..8b4c369 100644 --- a/lib/io/output-service.js +++ b/lib/io/output-service.js @@ -20,6 +20,24 @@ class OutputService { return this[SPINNER]; } + /** + * Send some data to stdout. + * + * @param {any} value - The value to write. + */ + write(value) { + console.log(value); + } + + /** + * Send some data to stderr. + * + * @param {any} value - The value to write. + */ + error(value) { + console.error(value); + } + /** * Spins the loading indicator as long as the supplied promise is in-flight. * diff --git a/lib/io/prompt.js b/lib/io/prompt.js new file mode 100644 index 0000000..1253481 --- /dev/null +++ b/lib/io/prompt.js @@ -0,0 +1,147 @@ +const NAME = Symbol("name"); +const INTRO = Symbol("intro"); +const HINT = Symbol("hint"); +const VALIDATOR = Symbol("validator"); +const HIDDEN = Symbol("hidden"); +const IDENTITY = (a) => a; + +/** + * Produces a function that can be used as a validator for empty strings. + * + * @param {string} failureMessage - The message to show if validation fails + * @param {Function} [inputFilter] - The function to transform the result once validation succeeds + * @return {Function} The validator function + */ +function createEmptyValidator(failureMessage, inputFilter) { + const filter = inputFilter instanceof Function ? inputFilter : IDENTITY; + return (v) => { + if (v === undefined || v === null || v === "") { + throw new Error(failureMessage); + } + return filter(v); + }; +} + +/** + * Produces a function that can be used as a validator based on a regular expression. + * + * @param {RegExp|string} pattern - The regular expression for validation + * @param {string} failureMessage - The message to show if validation fails + * @param {Function} [inputFilter] - The function to transform the result once validation succeeds + * @return {Function} The validator function + */ +function createRegExpValidator(pattern, failureMessage, inputFilter) { + const regularExpression = + pattern instanceof RegExp ? pattern : new RegExp(`${pattern}`); + const filter = inputFilter instanceof Function ? inputFilter : IDENTITY; + return (v) => { + if (!regularExpression.test(v)) { + throw new Error(failureMessage); + } + return filter(v); + }; +} + +/** + * A prompt for the user to complete. + */ +class Prompt { + /** + * Creates a new Prompt. + * + * The `validator` function is passed a `string` value to sanitize and return. + * If the user just hits the "enter" key at the prompt, the function will be + * provided an empty string. In this case, you can choose to return a default + * value, depending on your use case. It may also throw an `Error` if the user + * has entered an invalid value. + * + * @param {Object} options - The construction options. + * @param {string} options.name - The type of prompt (e.g. "NewPassword") + * @param {string} [options.intro] - The intro (e.g. "Enter a new password") + * @param {string} [options.hint] - The hint (e.g. "y/n") + * @param {Function} [options.validator] - The validator function; must return sanitized input + * @param {boolean} [options.hidden] - Whether the user input should be masked + */ + constructor(options) { + const { name, intro, hint, validator, hidden, defaultValue } = options; + this[NAME] = name === undefined ? "" : `${name}`; + this[INTRO] = intro ? `${intro}` : undefined; + this[HINT] = hint ? `${hint}` : undefined; + this[VALIDATOR] = validator instanceof Function ? validator : IDENTITY; + this[HIDDEN] = Boolean(hidden); + } + + /** + * @return {string} The type of prompt. + */ + get name() { + return this[NAME]; + } + + /** + * @return {string|undefined} The intro; text to display before the prompt + */ + get intro() { + return this[INTRO]; + } + + /** + * @return {string|undefined} The hint; assistive text such as acceptable values + */ + get hint() { + return this[HINT]; + } + + /** + * @return {Function} The validator function + */ + get validator() { + return this[VALIDATOR]; + } + + /** + * @return {boolean} Whether the input should be masked with asterisks + */ + get hidden() { + return this[HIDDEN]; + } +} + +/** + * A prompt for Y, y, N, and n that returns a boolean. + */ +class YesNoPrompt extends Prompt { + /** + * Creates a new YesNoPrompt. + * + * @param {string} intro - The intro (e.g. "Would you walk 500 miles?") + * @param {boolean} [fallback] - The default value if the user just hits `enter` + */ + constructor(intro, fallback) { + let hint = "y/n"; + let filter = (v) => v === "y" || v === "Y"; + let regexp = /^[nyNY]$/; + if (fallback !== undefined) { + hint = fallback ? "Y/n" : "y/N"; + filter = (v) => (v === "" ? Boolean(fallback) : v === "y" || v === "Y"); + regexp = /^[nyNY]{0,1}$/; + } + super({ + name: "yes-no", + hint, + intro, + validator: createRegExpValidator( + regexp, + "You must enter Y, y, N, or n", + filter + ), + }); + } +} + +module.exports = { + Prompt, + YesNoPrompt, + createEmptyValidator, + createRegExpValidator, +}; diff --git a/lib/secret/service.js b/lib/secret/service.js index f14bac2..0f258fd 100644 --- a/lib/secret/service.js +++ b/lib/secret/service.js @@ -58,6 +58,35 @@ class SecretService { return this[SECRETS].get(namespace, key); } + /** + * Gets all values in a namespace. + * + * @param {string} namespace - The namespace of the values to retrieve. + * @return {Map} All values within the given namespace. + */ + all(namespace) { + return this[SECRETS].all(namespace); + } + + /** + * Remove a key from a namespace. + * + * @param {string} namespace - The namespace of the key to remove + * @param {string} key - The key remove + */ + remove(namespace, key) { + this[SECRETS].remove(namespace, key); + } + + /** + * Remove all secrets in a namespace. + * + * @param {string} namespace - The namespace of the values to clear + */ + clear(namespace) { + this[SECRETS].clear(namespace); + } + /** * Sets a secret value. * diff --git a/lib/spinner.js b/lib/spinner.js new file mode 100644 index 0000000..b8d2d9e --- /dev/null +++ b/lib/spinner.js @@ -0,0 +1,24 @@ +const ora = require("ora"); + +const pauseSpinnerDuring = (callback) => { + spinner.clear(); + try { + callback(); + } finally { + spinner.render(); + } +}; + +const spinner = ora({ text: "Loading the thing", spinner: "dots2" }).start(); + +const interval2 = setInterval(() => { + pauseSpinnerDuring(() => { + console.error("Hey", new Date()); + }); +}, 75); + +setTimeout(() => { + clearInterval(interval2); + spinner.succeed(); + process.exit(0); +}, 10000); diff --git a/package-lock.json b/package-lock.json index 4e3eee4..318087d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -287,6 +287,18 @@ "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", "dev": true }, + "amazon-cognito-identity-js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-5.2.2.tgz", + "integrity": "sha512-kPsIhemt5CTGcafkzjVrfYSPV43YVMKMJ4wTTOOE60YfsAAwe82IMWk84MQu+dVQJaWKALI9tG1nwMkLzRLoJQ==", + "requires": { + "buffer": "4.9.2", + "crypto-js": "^4.1.1", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^2.2.1" + } + }, "ansi-escapes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", @@ -358,6 +370,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -383,6 +400,16 @@ "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", "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==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -571,6 +598,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -897,6 +929,11 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", "dev": true }, + "fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1131,6 +1168,11 @@ "sshpk": "^1.7.0" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore": { "version": "3.3.10", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", @@ -1255,6 +1297,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "requires": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -1282,6 +1333,11 @@ "semver": "^6.0.0" } }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1693,6 +1749,14 @@ "path-to-regexp": "^1.7.0" } }, + "node-fetch": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", + "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -1733,14 +1797,12 @@ "dependencies": { "ansi-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "bundled": true, "dev": true }, "append-transform": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "bundled": true, "dev": true, "requires": { "default-require-extensions": "^2.0.0" @@ -1748,20 +1810,17 @@ }, "archy": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "bundled": true, "dev": true }, "arrify": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "bundled": true, "dev": true }, "async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "bundled": true, "dev": true, "requires": { "lodash": "^4.17.11" @@ -1769,14 +1828,12 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "bundled": true, "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==", + "bundled": true, "dev": true, "requires": { "balanced-match": "^1.0.0", @@ -1785,8 +1842,7 @@ }, "caching-transform": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.1.tgz", - "integrity": "sha512-Y1KTLNwSPd4ljsDrFOtyXVmm7Gnk42yQitNq43AhE+cwUR/e4T+rmOHs1IPtzBg8066GBJfTOj1rQYFSWSsH2g==", + "bundled": true, "dev": true, "requires": { "hasha": "^3.0.0", @@ -1797,14 +1853,12 @@ }, "camelcase": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "bundled": true, "dev": true }, "cliui": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "bundled": true, "dev": true, "requires": { "string-width": "^2.1.1", @@ -1814,33 +1868,28 @@ }, "code-point-at": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "bundled": true, "dev": true }, "commander": { "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "bundled": true, "dev": true, "optional": true }, "commondir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "bundled": true, "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=", + "bundled": true, "dev": true }, "convert-source-map": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "bundled": true, "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -1848,8 +1897,7 @@ }, "cross-spawn": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "bundled": true, "dev": true, "requires": { "lru-cache": "^4.0.1", @@ -1858,8 +1906,7 @@ }, "debug": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "bundled": true, "dev": true, "requires": { "ms": "^2.1.1" @@ -1867,14 +1914,12 @@ }, "decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "bundled": true, "dev": true }, "default-require-extensions": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "bundled": true, "dev": true, "requires": { "strip-bom": "^3.0.0" @@ -1882,8 +1927,7 @@ }, "end-of-stream": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "bundled": true, "dev": true, "requires": { "once": "^1.4.0" @@ -1891,8 +1935,7 @@ }, "error-ex": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "bundled": true, "dev": true, "requires": { "is-arrayish": "^0.2.1" @@ -1900,14 +1943,12 @@ }, "es6-error": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "bundled": true, "dev": true }, "execa": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "bundled": true, "dev": true, "requires": { "cross-spawn": "^6.0.0", @@ -1921,8 +1962,7 @@ "dependencies": { "cross-spawn": { "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "bundled": true, "dev": true, "requires": { "nice-try": "^1.0.4", @@ -1936,8 +1976,7 @@ }, "find-cache-dir": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.0.0.tgz", - "integrity": "sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==", + "bundled": true, "dev": true, "requires": { "commondir": "^1.0.1", @@ -1947,8 +1986,7 @@ }, "find-up": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "bundled": true, "dev": true, "requires": { "locate-path": "^3.0.0" @@ -1956,8 +1994,7 @@ }, "foreground-child": { "version": "1.5.6", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", - "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", + "bundled": true, "dev": true, "requires": { "cross-spawn": "^4", @@ -1966,20 +2003,17 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "bundled": true, "dev": true }, "get-caller-file": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "bundled": true, "dev": true }, "get-stream": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "bundled": true, "dev": true, "requires": { "pump": "^3.0.0" @@ -1987,8 +2021,7 @@ }, "glob": { "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "bundled": true, "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -2001,14 +2034,12 @@ }, "graceful-fs": { "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "bundled": true, "dev": true }, "handlebars": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", - "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", + "bundled": true, "dev": true, "requires": { "async": "^2.5.0", @@ -2019,22 +2050,19 @@ "dependencies": { "source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "bundled": true, "dev": true } } }, "has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "bundled": true, "dev": true }, "hasha": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", - "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "bundled": true, "dev": true, "requires": { "is-stream": "^1.0.1" @@ -2042,20 +2070,17 @@ }, "hosted-git-info": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "bundled": true, "dev": true }, "imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "bundled": true, "dev": true }, "inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "bundled": true, "dev": true, "requires": { "once": "^1.3.0", @@ -2064,50 +2089,42 @@ }, "inherits": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "bundled": true, "dev": true }, "invert-kv": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", + "bundled": true, "dev": true }, "is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "bundled": true, "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=", + "bundled": true, "dev": true }, "is-stream": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "bundled": true, "dev": true }, "isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "bundled": true, "dev": true }, "istanbul-lib-coverage": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-dKWuzRGCs4G+67VfW9pBFFz2Jpi4vSp/k7zBcJ888ofV5Mi1g5CUML5GvMvV6u9Cjybftu+E8Cgp+k0dI1E5lw==", + "bundled": true, "dev": true }, "istanbul-lib-hook": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.3.tgz", - "integrity": "sha512-CLmEqwEhuCYtGcpNVJjLV1DQyVnIqavMLFHV/DP+np/g3qvdxu3gsPqYoJMXm15sN84xOlckFB3VNvRbf5yEgA==", + "bundled": true, "dev": true, "requires": { "append-transform": "^1.0.0" @@ -2115,8 +2132,7 @@ }, "istanbul-lib-report": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.4.tgz", - "integrity": "sha512-sOiLZLAWpA0+3b5w5/dq0cjm2rrNdAfHWaGhmn7XEFW6X++IV9Ohn+pnELAl9K3rfpaeBfbmH9JU5sejacdLeA==", + "bundled": true, "dev": true, "requires": { "istanbul-lib-coverage": "^2.0.3", @@ -2126,8 +2142,7 @@ "dependencies": { "supports-color": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "bundled": true, "dev": true, "requires": { "has-flag": "^3.0.0" @@ -2137,8 +2152,7 @@ }, "istanbul-lib-source-maps": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.2.tgz", - "integrity": "sha512-JX4v0CiKTGp9fZPmoxpu9YEkPbEqCqBbO3403VabKjH+NRXo72HafD5UgnjTEqHL2SAjaZK1XDuDOkn6I5QVfQ==", + "bundled": true, "dev": true, "requires": { "debug": "^4.1.1", @@ -2150,16 +2164,14 @@ "dependencies": { "source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "bundled": true, "dev": true } } }, "istanbul-reports": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.1.1.tgz", - "integrity": "sha512-FzNahnidyEPBCI0HcufJoSEoKykesRlFcSzQqjH9x0+LC8tnnE/p/90PBLu8iZTxr8yYZNyTtiAujUqyN+CIxw==", + "bundled": true, "dev": true, "requires": { "handlebars": "^4.1.0" @@ -2167,14 +2179,12 @@ }, "json-parse-better-errors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "bundled": true, "dev": true }, "lcid": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", + "bundled": true, "dev": true, "requires": { "invert-kv": "^2.0.0" @@ -2182,8 +2192,7 @@ }, "load-json-file": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "bundled": true, "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -2194,8 +2203,7 @@ }, "locate-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "bundled": true, "dev": true, "requires": { "p-locate": "^3.0.0", @@ -2204,20 +2212,17 @@ }, "lodash": { "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "bundled": true, "dev": true }, "lodash.flattendeep": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "bundled": true, "dev": true }, "lru-cache": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "bundled": true, "dev": true, "requires": { "pseudomap": "^1.0.2", @@ -2226,8 +2231,7 @@ }, "make-dir": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "bundled": true, "dev": true, "requires": { "pify": "^3.0.0" @@ -2235,8 +2239,7 @@ }, "map-age-cleaner": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "bundled": true, "dev": true, "requires": { "p-defer": "^1.0.0" @@ -2244,8 +2247,7 @@ }, "mem": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", - "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", + "bundled": true, "dev": true, "requires": { "map-age-cleaner": "^0.1.1", @@ -2255,8 +2257,7 @@ }, "merge-source-map": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "bundled": true, "dev": true, "requires": { "source-map": "^0.6.1" @@ -2264,22 +2265,19 @@ "dependencies": { "source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "bundled": true, "dev": true } } }, "mimic-fn": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "bundled": true, "dev": true }, "minimatch": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "bundled": true, "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -2287,14 +2285,12 @@ }, "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "bundled": true, "dev": true }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "bundled": true, "dev": true, "requires": { "minimist": "0.0.8" @@ -2302,28 +2298,24 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "bundled": true, "dev": true } } }, "ms": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "bundled": true, "dev": true }, "nice-try": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "bundled": true, "dev": true }, "normalize-package-data": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "bundled": true, "dev": true, "requires": { "hosted-git-info": "^2.1.4", @@ -2334,8 +2326,7 @@ }, "npm-run-path": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "bundled": true, "dev": true, "requires": { "path-key": "^2.0.0" @@ -2343,14 +2334,12 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "bundled": true, "dev": true }, "once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "bundled": true, "dev": true, "requires": { "wrappy": "1" @@ -2358,8 +2347,7 @@ }, "optimist": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "bundled": true, "dev": true, "requires": { "minimist": "~0.0.1", @@ -2368,14 +2356,12 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "bundled": true, "dev": true }, "os-locale": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", + "bundled": true, "dev": true, "requires": { "execa": "^1.0.0", @@ -2385,26 +2371,22 @@ }, "p-defer": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "bundled": true, "dev": true }, "p-finally": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "bundled": true, "dev": true }, "p-is-promise": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", - "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==", + "bundled": true, "dev": true }, "p-limit": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "bundled": true, "dev": true, "requires": { "p-try": "^2.0.0" @@ -2412,8 +2394,7 @@ }, "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==", + "bundled": true, "dev": true, "requires": { "p-limit": "^2.0.0" @@ -2421,14 +2402,12 @@ }, "p-try": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "bundled": true, "dev": true }, "package-hash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", - "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "bundled": true, "dev": true, "requires": { "graceful-fs": "^4.1.15", @@ -2439,8 +2418,7 @@ }, "parse-json": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "bundled": true, "dev": true, "requires": { "error-ex": "^1.3.1", @@ -2449,32 +2427,27 @@ }, "path-exists": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "bundled": true, "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=", + "bundled": true, "dev": true }, "path-key": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "bundled": true, "dev": true }, "path-parse": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "bundled": true, "dev": true }, "path-type": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "bundled": true, "dev": true, "requires": { "pify": "^3.0.0" @@ -2482,14 +2455,12 @@ }, "pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "bundled": true, "dev": true }, "pkg-dir": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "bundled": true, "dev": true, "requires": { "find-up": "^3.0.0" @@ -2497,14 +2468,12 @@ }, "pseudomap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "bundled": true, "dev": true }, "pump": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "bundled": true, "dev": true, "requires": { "end-of-stream": "^1.1.0", @@ -2513,8 +2482,7 @@ }, "read-pkg": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "bundled": true, "dev": true, "requires": { "load-json-file": "^4.0.0", @@ -2524,8 +2492,7 @@ }, "read-pkg-up": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "bundled": true, "dev": true, "requires": { "find-up": "^3.0.0", @@ -2534,8 +2501,7 @@ }, "release-zalgo": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "bundled": true, "dev": true, "requires": { "es6-error": "^4.0.1" @@ -2543,20 +2509,17 @@ }, "require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "bundled": true, "dev": true }, "require-main-filename": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "bundled": true, "dev": true }, "resolve": { "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "bundled": true, "dev": true, "requires": { "path-parse": "^1.0.6" @@ -2564,14 +2527,12 @@ }, "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==", + "bundled": true, "dev": true }, "rimraf": { "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "bundled": true, "dev": true, "requires": { "glob": "^7.1.3" @@ -2579,26 +2540,22 @@ }, "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==", + "bundled": true, "dev": true }, "semver": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "bundled": true, "dev": true }, "set-blocking": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "bundled": true, "dev": true }, "shebang-command": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "bundled": true, "dev": true, "requires": { "shebang-regex": "^1.0.0" @@ -2606,20 +2563,17 @@ }, "shebang-regex": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "bundled": true, "dev": true }, "signal-exit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "bundled": true, "dev": true }, "spawn-wrap": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", - "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "bundled": true, "dev": true, "requires": { "foreground-child": "^1.5.6", @@ -2632,8 +2586,7 @@ }, "spdx-correct": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "bundled": true, "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -2642,14 +2595,12 @@ }, "spdx-exceptions": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "bundled": true, "dev": true }, "spdx-expression-parse": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "bundled": true, "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -2658,14 +2609,12 @@ }, "spdx-license-ids": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", + "bundled": true, "dev": true }, "string-width": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "bundled": true, "dev": true, "requires": { "is-fullwidth-code-point": "^2.0.0", @@ -2674,8 +2623,7 @@ }, "strip-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "bundled": true, "dev": true, "requires": { "ansi-regex": "^3.0.0" @@ -2683,20 +2631,17 @@ }, "strip-bom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "bundled": true, "dev": true }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "bundled": true, "dev": true }, "test-exclude": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.1.0.tgz", - "integrity": "sha512-gwf0S2fFsANC55fSeSqpb8BYk6w3FDvwZxfNjeF6FRgvFa43r+7wRiA/Q0IxoRU37wB/LE8IQ4221BsNucTaCA==", + "bundled": true, "dev": true, "requires": { "arrify": "^1.0.1", @@ -2707,8 +2652,7 @@ }, "uglify-js": { "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "bundled": true, "dev": true, "optional": true, "requires": { @@ -2718,8 +2662,7 @@ "dependencies": { "source-map": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "bundled": true, "dev": true, "optional": true } @@ -2727,14 +2670,12 @@ }, "uuid": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "bundled": true, "dev": true }, "validate-npm-package-license": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "bundled": true, "dev": true, "requires": { "spdx-correct": "^3.0.0", @@ -2743,8 +2684,7 @@ }, "which": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "bundled": true, "dev": true, "requires": { "isexe": "^2.0.0" @@ -2752,20 +2692,17 @@ }, "which-module": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "bundled": true, "dev": true }, "wordwrap": { "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "bundled": true, "dev": true }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "bundled": true, "dev": true, "requires": { "string-width": "^1.0.1", @@ -2774,14 +2711,12 @@ "dependencies": { "ansi-regex": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "bundled": true, "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "bundled": true, "dev": true, "requires": { "number-is-nan": "^1.0.0" @@ -2789,8 +2724,7 @@ }, "string-width": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "bundled": true, "dev": true, "requires": { "code-point-at": "^1.0.0", @@ -2800,8 +2734,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "bundled": true, "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -2811,14 +2744,12 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "bundled": true, "dev": true }, "write-file-atomic": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", + "bundled": true, "dev": true, "requires": { "graceful-fs": "^4.1.11", @@ -2828,20 +2759,17 @@ }, "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==", + "bundled": true, "dev": true }, "yallist": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "bundled": true, "dev": true }, "yargs": { "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "bundled": true, "dev": true, "requires": { "cliui": "^4.0.0", @@ -2860,8 +2788,7 @@ }, "yargs-parser": { "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "bundled": true, "dev": true, "requires": { "camelcase": "^5.0.0", @@ -3539,6 +3466,11 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "triple-beam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", @@ -3586,6 +3518,11 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3634,6 +3571,20 @@ "defaults": "^1.0.3" } }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 0f2007a..6705b2c 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ }, "dependencies": { "@folder/xdg": "^3.1.2", + "amazon-cognito-identity-js": "^5.2.2", "commander": "^7.2.0", "cross-spawn": "^7.0.3", "event-target-shim": "^6.0.2", diff --git a/test/auth/adapter.spec.js b/test/auth/adapter.spec.js new file mode 100644 index 0000000..b7193a7 --- /dev/null +++ b/test/auth/adapter.spec.js @@ -0,0 +1,41 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { AuthAdapter } = require("../../lib/auth/adapter"); +const { User } = require("../../lib/auth/user"); + +describe("AuthAdapter", () => { + describe("#name", () => { + it("should return provided name", async () => { + const name = "foobar"; + const obj = new AuthAdapter(name); + assert.strictEqual(obj.name, name); + }); + }); + + describe("#user", () => { + it("should return the anonymous user", async () => { + const obj = new AuthAdapter("foobar"); + assert.ok(obj.user.anonymous); + }); + it("should allow setting users", async () => { + const user = new User("testing"); + const obj = new AuthAdapter("foobar"); + assert.doesNotThrow(() => { + obj.user = user; + }, TypeError); + assert.strictEqual(obj.user, user); + }); + it("should throw an error for non-user values", async () => { + const obj = new AuthAdapter("foobar"); + assert.throws( + () => { + obj.user = "testing"; + }, + { + name: "TypeError", + message: "Value must be an instance of the User class", + } + ); + }); + }); +}); diff --git a/test/auth/cognito/adapter.spec.js b/test/auth/cognito/adapter.spec.js new file mode 100644 index 0000000..5ef4ea7 --- /dev/null +++ b/test/auth/cognito/adapter.spec.js @@ -0,0 +1,140 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { Event } = require("event-target-shim"); +const { CognitoUser, CognitoUserPool } = require("amazon-cognito-identity-js"); +const { Container } = require("../../../lib/container"); +const ConfigService = require("../../../lib/config/service"); +const SecretService = require("../../../lib/secret/service"); +const { User } = require("../../../lib/auth/user"); +const { CognitoAuthAdapter } = require("../../../lib/auth/cognito/adapter"); +const { CognitoFacade } = require("../../../lib/auth/cognito/facade"); +const { CognitoLoginChallenge } = require("../../../lib/auth/cognito/result"); +const { CognitoStorage } = require("../../../lib/auth/cognito/storage"); +const { CognitoIdpUser } = require("../../../lib/auth/cognito/user"); + +describe("CognitoAuthAdapter", () => { + describe("#create", () => { + it("should return a CognitoAuthAdapter", async () => { + const container = sinon.createStubInstance(Container); + container.getAll.resolves([null, null]); + const obj = await CognitoAuthAdapter.create(container); + assert.ok(obj instanceof CognitoAuthAdapter); + }); + }); + describe("#constructor", () => { + it("should give the adapter name", async () => { + const obj = new CognitoAuthAdapter(null, null); + assert.strictEqual(obj.name, "cognito"); + }); + }); + describe("#cognitoFacade", () => { + it("should lazy load the facade", async () => { + const configService = sinon.createStubInstance(ConfigService); + const secretService = sinon.createStubInstance(SecretService); + secretService.all.returns(new Map()); + const userPool = sinon.createStubInstance(CognitoUserPool); + const obj = new CognitoAuthAdapter(configService, secretService); + sinon.stub(obj, "createCognitoUserPool").returns(userPool); + const facade = obj.cognitoFacade; + assert.ok(facade instanceof CognitoFacade); + }); + it("should return the lazy loaded facade again", async () => { + const configService = sinon.createStubInstance(ConfigService); + const secretService = sinon.createStubInstance(SecretService); + secretService.all.returns(new Map()); + const userPool = sinon.createStubInstance(CognitoUserPool); + const obj = new CognitoAuthAdapter(configService, secretService); + sinon.stub(obj, "createCognitoUserPool").returns(userPool); + const facade = obj.cognitoFacade; + assert.strictEqual(obj.cognitoFacade, facade); + }); + it("should receive events from the facade", async () => { + const configService = sinon.createStubInstance(ConfigService); + const secretService = sinon.createStubInstance(SecretService); + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const storeStub = sinon.stub(cognitoStorage, "store"); + storeStub.withArgs(secretService).onCall(0).resolves(undefined); + storeStub.withArgs(secretService).onCall(1).resolves(undefined); + const obj = new CognitoAuthAdapter(configService, secretService); + sinon.stub(obj, "createCognitoStorage").returns(cognitoStorage); + sinon.stub(obj, "createCognitoUserPool").returns(userPool); + const facade = obj.cognitoFacade; + facade.dispatchEvent(new Event("login")); + facade.dispatchEvent(new Event("logout")); + assert.ok(storeStub.calledTwice); + }); + }); + describe("#createCognitoUserPool", () => { + it("should return a user pool with our arguments", async () => { + const configService = sinon.createStubInstance(ConfigService); + const poolId = "us-east-1_Abcdefghi"; + const clientId = "AbcDef123456GhiJkl789"; + configService.get.withArgs("auth", "cognito.user-pool").returns(poolId); + configService.get + .withArgs("auth", "cognito.app-client-id") + .returns(clientId); + const secretService = sinon.createStubInstance(SecretService); + secretService.all.returns(new Map()); + const obj = new CognitoAuthAdapter(configService, secretService); + const cognitoStorage = new CognitoStorage(); + const userPool = obj.createCognitoUserPool(cognitoStorage); + assert.ok(userPool instanceof CognitoUserPool); + assert.strictEqual(userPool.userPoolId, poolId); + assert.strictEqual(userPool.clientId, clientId); + assert.strictEqual(userPool.storage, cognitoStorage); + }); + }); + describe("#user", () => { + it("should set the anonymous user if missing", async () => { + const cognitoFacade = sinon.createStubInstance(CognitoFacade); + sinon.stub(cognitoFacade, "currentUser").get(() => null); + const obj = new CognitoAuthAdapter(null, null); + sinon.stub(obj, "cognitoFacade").get(() => cognitoFacade); + const user = obj.user; + assert.deepEqual(user, new User()); + }); + it("should set a cognito user if missing", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.username = "foobar"; + const cognitoFacade = sinon.createStubInstance(CognitoFacade); + sinon.stub(cognitoFacade, "currentUser").get(() => cognitoUser); + const obj = new CognitoAuthAdapter(null, null); + sinon.stub(obj, "cognitoFacade").get(() => cognitoFacade); + const user = obj.user; + assert.deepEqual( + user, + new CognitoIdpUser(cognitoUser.username, cognitoUser) + ); + }); + it("should return the same value after creation", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.username = "foobar"; + const cognitoFacade = sinon.createStubInstance(CognitoFacade); + sinon.stub(cognitoFacade, "currentUser").get(() => cognitoUser); + const obj = new CognitoAuthAdapter(null, null); + sinon.stub(obj, "cognitoFacade").get(() => cognitoFacade); + const user = obj.user; + assert.strictEqual(obj.user, user); + }); + }); + describe("#logIn", () => { + it("should return a CognitoLoginChallenge", async () => { + const obj = new CognitoAuthAdapter(null, null); + const cognitoFacade = sinon.createStubInstance(CognitoFacade); + sinon.stub(obj, "cognitoFacade").get(() => cognitoFacade); + const result = obj.logIn(); + assert.ok(result instanceof CognitoLoginChallenge); + }); + }); + describe("#logout", () => { + it("should invoke the facade logout", async () => { + const obj = new CognitoAuthAdapter(null, null); + const cognitoFacade = sinon.createStubInstance(CognitoFacade); + cognitoFacade.logOut.resolves(undefined); + sinon.stub(obj, "cognitoFacade").get(() => cognitoFacade); + const result = await obj.logOut(); + assert.ok(cognitoFacade.logOut.calledOnce); + }); + }); +}); diff --git a/test/auth/cognito/facade.spec.js b/test/auth/cognito/facade.spec.js new file mode 100644 index 0000000..e17fa08 --- /dev/null +++ b/test/auth/cognito/facade.spec.js @@ -0,0 +1,346 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { + CognitoUserPool, + CognitoUser, + CognitoUserSession, +} = require("amazon-cognito-identity-js"); +const { CognitoStorage } = require("../../../lib/auth/cognito/storage"); +const { CognitoFacade } = require("../../../lib/auth/cognito/facade"); + +describe("CognitoFacade", () => { + describe("#constructor", () => { + it("should return the cognitoStorage provided", async () => { + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(null, cognitoStorage); + assert.strictEqual(obj.clientStorage, cognitoStorage); + }); + it("should return the userPool provided", async () => { + const userPool = sinon.createStubInstance(CognitoUserPool); + const obj = new CognitoFacade(userPool, null); + assert.strictEqual(obj.userPool, userPool); + }); + }); + describe("#currentUser", () => { + it("should return the current cognito user", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const userPool = sinon.createStubInstance(CognitoUserPool); + userPool.getCurrentUser.returns(cognitoUser); + const obj = new CognitoFacade(userPool, null); + assert.strictEqual(obj.currentUser, cognitoUser); + }); + }); + describe("#createCognitoUser", () => { + it("should return a CognitoUser", async () => { + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const cognitoUser = obj.createCognitoUser("foobar"); + assert.ok(cognitoUser instanceof CognitoUser); + }); + it("should have the properties provided", async () => { + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const username = "foobar"; + const cognitoUser = obj.createCognitoUser(username); + assert.strictEqual(cognitoUser.username, username); + assert.strictEqual(cognitoUser.pool, userPool); + assert.strictEqual(cognitoUser.storage, cognitoStorage); + }); + }); + describe("#login", () => { + it("should return a promise", async () => { + const username = "foobar"; + const password = "Password123"; + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoStorage = new CognitoStorage(); + const facadeResult = { incomplete: false, user: cognitoUser }; + const obj = new CognitoFacade(userPool, cognitoStorage); + sinon.stub(obj, "createCognitoUser").returns(cognitoUser); + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve(facadeResult)); + const result = obj.logIn(username, password); + assert.ok(result instanceof Promise); + }); + it("should resolve to a result", async () => { + const username = "foobar"; + const password = "Password123"; + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoStorage = new CognitoStorage(); + const facadeResult = { incomplete: false, user: cognitoUser }; + const obj = new CognitoFacade(userPool, cognitoStorage); + sinon.stub(obj, "createCognitoUser").returns(cognitoUser); + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve(facadeResult)); + const result = await obj.logIn(username, password); + assert.strictEqual(result, facadeResult); + }); + }); + describe("#logout", () => { + it("should return a Promise", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const userPool = sinon.createStubInstance(CognitoUserPool); + userPool.getCurrentUser.returns(cognitoUser); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const result = obj.logOut(); + assert.ok(result instanceof Promise); + }); + it("should throw an error without a session user", async () => { + const userPool = sinon.createStubInstance(CognitoUserPool); + userPool.getCurrentUser.returns(null); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + assert.throws(() => obj.logOut(), { + name: "Error", + message: "There is no user to log out", + }); + }); + it("should resolve to undefined", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.signOut.callsFake((fn) => fn()); + const userPool = sinon.createStubInstance(CognitoUserPool); + userPool.getCurrentUser.returns(cognitoUser); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const result = await obj.logOut(); + assert.strictEqual(result, undefined); + }); + it("should reject if error sent from cognito", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const err = new Error("An example Cognito error"); + cognitoUser.signOut.callsFake((fn) => fn(err)); + const userPool = sinon.createStubInstance(CognitoUserPool); + userPool.getCurrentUser.returns(cognitoUser); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + await assert.rejects(() => obj.logOut(), err); + }); + }); + describe("#confirmNewPassword", () => { + it("should return a Promise", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.completeNewPasswordChallenge.callsFake( + (password, attributes, callbackObject) => {} + ); + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const password = "Password123"; + const attributes = ["username"]; + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve({})); + const result = obj.confirmNewPassword(cognitoUser, password, attributes); + assert.ok(result instanceof Promise); + }); + it("should resolve to a result", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.completeNewPasswordChallenge.callsFake( + (password, attributes, callbackObject) => {} + ); + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const password = "Password123"; + const attributes = ["username"]; + const facadeResult = { incomplete: true }; + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve(facadeResult)); + const result = await obj.confirmNewPassword( + cognitoUser, + password, + attributes + ); + assert.strictEqual(result, facadeResult); + }); + it("should resolve to a result without attributes object", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.completeNewPasswordChallenge.callsFake( + (password, attributes, callbackObject) => {} + ); + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const password = "Password123"; + const attributes = ["username"]; + const facadeResult = { incomplete: true }; + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve(facadeResult)); + const result = await obj.confirmNewPassword(cognitoUser, password); + assert.deepEqual(result, facadeResult); + }); + }); + describe("#confirmPasswordReset", () => { + it("should return a Promise", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.confirmPassword.callsFake( + (verificationCode, password, callbackObject) => {} + ); + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const password = "Password123"; + const verificationCode = "123456"; + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve({})); + const result = obj.confirmPasswordReset( + cognitoUser, + verificationCode, + password + ); + assert.ok(result instanceof Promise); + }); + it("should resolve to a result", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.confirmPassword.callsFake( + (verificationCode, password, callbackObject) => {} + ); + const userPool = sinon.createStubInstance(CognitoUserPool); + const cognitoStorage = new CognitoStorage(); + const obj = new CognitoFacade(userPool, cognitoStorage); + const password = "Password123"; + const verificationCode = "123456"; + const facadeResult = { incomplete: true }; + sinon + .stub(obj, "createCallbackObject") + .callsFake((user, resolve, reject) => resolve(facadeResult)); + const result = await obj.confirmPasswordReset( + cognitoUser, + verificationCode, + password + ); + assert.strictEqual(result, facadeResult); + }); + }); + describe("#createCallbackObject", () => { + it("should return an object with function values", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoFacade(null, null); + const callbackObject = obj.createCallbackObject( + cognitoUser, + () => {}, + () => {} + ); + assert.ok(typeof callbackObject === "object"); + for (const key of [ + "onSuccess", + "onFailure", + "newPasswordRequired", + "mfaRequired", + "totpRequired", + "customChallenge", + "mfaSetup", + "selectMFAType", + ]) { + assert.ok(typeof callbackObject[key] === "function"); + } + }); + it("should resolve a facade result on success", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const session = sinon.createStubInstance(CognitoUserSession); + cognitoUser.username = "foobar"; + const obj = new CognitoFacade(null, null); + const promise = new Promise((resolve, reject) => { + const callback = obj.createCallbackObject(cognitoUser, resolve, reject); + callback.onSuccess(session, false); + }); + const facadeResult = await promise; + assert.deepEqual(facadeResult, { + incomplete: false, + user: cognitoUser, + username: "foobar", + session, + }); + }); + it("should resolve a facade result in common scenarios", async () => { + const challengeName = "FOOBAR_CHALLENGE"; + const challengeParams = { foo: "bar" }; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoFacade(null, null); + const promise = new Promise((resolve, reject) => { + const callback = obj.createCallbackObject(cognitoUser, resolve, reject); + callback.mfaRequired(challengeName, challengeParams); + }); + const facadeResult = await promise; + assert.deepEqual(facadeResult, { + incomplete: true, + challenge: true, + name: challengeName, + parameters: challengeParams, + user: cognitoUser, + }); + }); + it("should resolve a facade result for custom challenges", async () => { + const challengeParams = { foo: "bar" }; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoFacade(null, null); + const promise = new Promise((resolve, reject) => { + const callback = obj.createCallbackObject(cognitoUser, resolve, reject); + callback.customChallenge(challengeParams); + }); + const facadeResult = await promise; + assert.deepEqual(facadeResult, { + incomplete: true, + challenge: true, + name: "CUSTOM_CHALLENGE", + parameters: challengeParams, + user: cognitoUser, + }); + }); + it("should resolve a facade result for new password required", async () => { + const userAttributes = { foo: "bar" }; + const requiredAttributes = ["username"]; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoFacade(null, null); + const promise = new Promise((resolve, reject) => { + const callback = obj.createCallbackObject(cognitoUser, resolve, reject); + callback.newPasswordRequired(userAttributes, requiredAttributes); + }); + const facadeResult = await promise; + assert.deepEqual(facadeResult, { + incomplete: true, + challenge: true, + name: "NEW_PASSWORD_REQUIRED", + requiredAttributes, + user: cognitoUser, + }); + }); + it("should resolve a facade result for password reset", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoFacade(null, null); + const err = { name: "PasswordResetRequiredException" }; + const promise = new Promise((resolve, reject) => { + const callback = obj.createCallbackObject(cognitoUser, resolve, reject); + callback.onFailure(err); + }); + const facadeResult = await promise; + assert.deepEqual(facadeResult, { + incomplete: true, + challenge: true, + name: "PASSWORD_RESET_REQUIRED", + user: cognitoUser, + }); + }); + it("should reject with errors", async () => { + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoFacade(null, null); + const err = new Error("Something went wrong"); + const promise = new Promise((resolve, reject) => { + const callback = obj.createCallbackObject(cognitoUser, resolve, reject); + callback.onFailure(err); + }); + await assert.rejects(async () => { + await promise; + }, err); + }); + }); +}); diff --git a/test/auth/cognito/result.spec.js b/test/auth/cognito/result.spec.js new file mode 100644 index 0000000..2ecc41a --- /dev/null +++ b/test/auth/cognito/result.spec.js @@ -0,0 +1,329 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { + CognitoUser, + CognitoUserSession, +} = require("amazon-cognito-identity-js"); +const InputService = require("../../../lib/io/input-service"); +const OutputService = require("../../../lib/io/output-service"); +const { CognitoIdpUser } = require("../../../lib/auth/cognito/user"); +const { CognitoFacade } = require("../../../lib/auth/cognito/facade"); +const { + CognitoChallenge, + CognitoLoginChallenge, + CognitoAuthentication, + CognitoPasswordChallenge, + CognitoNewPasswordChallenge, + CognitoPasswordResetChallenge, +} = require("../../../lib/auth/cognito/result"); + +describe("CognitoChallenge", () => { + describe("#processCognitoResult", () => { + it("should return a CognitoAuthentication", async () => { + const username = "foobar"; + const password = "Password123"; + const facade = sinon.createStubInstance(CognitoFacade); + const user = { username }; + const session = sinon.createStubInstance(CognitoUserSession); + session.getIdToken.returns({ getJwtToken: () => "" }); + session.getAccessToken.returns({ getJwtToken: () => "" }); + session.getRefreshToken.returns({ getToken: () => "" }); + const obj = new CognitoChallenge(facade); + const facadeResult = { + incomplete: false, + user, + username: user.username, + session, + }; + const result = obj.processCognitoResult(facadeResult); + assert.ok(result instanceof CognitoAuthentication); + assert.ok(result.user instanceof CognitoIdpUser); + assert.strictEqual(result.user.username, username); + assert.strictEqual(result.session, session); + }); + it("should return a new password required challenge", async () => { + const username = "foobar"; + const password = "Password123"; + const facade = sinon.createStubInstance(CognitoFacade); + const user = { username }; + const obj = new CognitoChallenge(facade); + const facadeResult = { + incomplete: true, + challenge: true, + name: "NEW_PASSWORD_REQUIRED", + requiredAttributes: {}, + user, + }; + const result = obj.processCognitoResult(facadeResult); + assert.ok(result instanceof CognitoNewPasswordChallenge); + }); + it("should return a password reset required challenge", async () => { + const username = "foobar"; + const password = "Password123"; + const facade = sinon.createStubInstance(CognitoFacade); + const user = { username }; + const facadeResult = { + incomplete: true, + challenge: true, + name: "PASSWORD_RESET_REQUIRED", + user, + }; + const obj = new CognitoChallenge(facade); + const result = obj.processCognitoResult(facadeResult); + assert.ok(result instanceof CognitoPasswordResetChallenge); + }); + it("should error for weird challenge results", async () => { + const username = "foobar"; + const facade = sinon.createStubInstance(CognitoFacade); + const user = { username }; + const facadeResult = { + incomplete: true, + challenge: true, + name: "WHAT", + user, + }; + const obj = new CognitoChallenge(facade); + assert.throws( + () => { + obj.processCognitoResult(facadeResult); + }, + { + name: "Error", + message: "Unknown challenge type: WHAT", + } + ); + }); + it("should error for weird results", async () => { + const username = "foobar"; + const facade = sinon.createStubInstance(CognitoFacade); + const user = { username }; + const facadeResult = { + incomplete: true, + challenge: false, + name: "WHAT", + user, + }; + const obj = new CognitoChallenge(facade); + assert.throws( + () => { + obj.processCognitoResult(facadeResult); + }, + { + name: "Error", + message: "Unknown result: neither complete nor a challenge", + } + ); + }); + }); +}); + +describe("CognitoLoginChallenge", () => { + describe("#prompt", () => { + it("should call dispatch", async () => { + const facade = sinon.createStubInstance(CognitoFacade); + const obj = new CognitoLoginChallenge(facade); + const inputService = sinon.createStubInstance(InputService); + const username = "foobar"; + const password = "Password123"; + inputService.dispatch.onCall(0).resolves(username); + inputService.dispatch.onCall(1).resolves(password); + const outputService = sinon.createStubInstance(OutputService); + assert.deepEqual(await obj.prompt(inputService, outputService), { + username, + password, + }); + }); + }); + describe("#complete", () => { + it("should invoke the result processor", async () => { + const username = "foobar"; + const password = "Password123"; + const user = { username }; + const facade = sinon.createStubInstance(CognitoFacade); + const facadeResult = { incomplete: false, user, username }; + facade.logIn.resolves(facadeResult); + const obj = new CognitoLoginChallenge(facade); + const session = sinon.createStubInstance(CognitoUserSession); + session.getIdToken.returns({ getJwtToken: () => "" }); + session.getAccessToken.returns({ getJwtToken: () => "" }); + session.getRefreshToken.returns({ getToken: () => "" }); + const cognitoIdpUser = new CognitoIdpUser(username, user); + const authentication = new CognitoAuthentication(cognitoIdpUser, session); + const stub = sinon.stub(obj, "processCognitoResult"); + stub.returns(authentication); + const result = await obj.complete({ username, password }); + assert.strictEqual(result, authentication); + }); + }); +}); +describe("CognitoPasswordChallenge", () => { + describe("#prompt", () => { + it("should call dispatch", async () => { + const facade = sinon.createStubInstance(CognitoFacade); + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoPasswordChallenge(facade, cognitoUser); + const inputService = sinon.createStubInstance(InputService); + const password = "Password123"; + inputService.dispatch.onCall(0).resolves(password); + inputService.dispatch.onCall(1).resolves(password); + const outputService = sinon.createStubInstance(OutputService); + const value = await obj.prompt(inputService, outputService); + assert.deepEqual(value, { password }); + }); + it("should output an error if passwords don't match", async () => { + const facade = sinon.createStubInstance(CognitoFacade); + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoPasswordChallenge(facade, cognitoUser); + const inputService = sinon.createStubInstance(InputService); + const password = "Password123"; + inputService.dispatch.onCall(0).resolves(password); + inputService.dispatch.onCall(1).resolves("foo"); + inputService.dispatch.onCall(2).resolves(password); + inputService.dispatch.onCall(3).resolves(password); + const outputService = sinon.createStubInstance(OutputService); + outputService.error.callsFake(() => {}); + const value = await obj.prompt(inputService, outputService); + assert.deepEqual(value, { password }); + assert.ok(outputService.error.calledOnce); + assert.ok(outputService.error.calledWith("Passwords do not match")); + }); + }); +}); +describe("CognitoNewPasswordChallenge", () => { + describe("#requiredAttributes", () => { + it("should return the required attributes", async () => { + const facade = sinon.createStubInstance(CognitoFacade); + const cognitoUser = sinon.createStubInstance(CognitoUser); + const requiredAttributes = ["username"]; + const obj = new CognitoNewPasswordChallenge( + facade, + cognitoUser, + requiredAttributes + ); + assert.deepEqual(obj.requiredAttributes, requiredAttributes); + }); + }); + describe("#complete", () => { + it("should invoke the result processor", async () => { + const username = "foobar"; + const password = "Password123"; + const user = { username }; + const facade = sinon.createStubInstance(CognitoFacade); + const facadeResult = { incomplete: false, user, username }; + facade.logIn.resolves(facadeResult); + const obj = new CognitoNewPasswordChallenge(facade); + const session = sinon.createStubInstance(CognitoUserSession); + session.getIdToken.returns({ getJwtToken: () => "" }); + session.getAccessToken.returns({ getJwtToken: () => "" }); + session.getRefreshToken.returns({ getToken: () => "" }); + const cognitoIdpUser = new CognitoIdpUser(username, user); + const authentication = new CognitoAuthentication(cognitoIdpUser, session); + const stub = sinon.stub(obj, "processCognitoResult"); + stub.returns(authentication); + const result = await obj.complete({ username, password }); + assert.strictEqual(result, authentication); + }); + }); +}); +describe("CognitoPasswordResetChallenge", () => { + describe("#prompt", () => { + it("should call dispatch", async () => { + const facade = sinon.createStubInstance(CognitoFacade); + const obj = new CognitoPasswordResetChallenge(facade); + const inputService = sinon.createStubInstance(InputService); + const password = "Password123"; + const verificationCode = "123456"; + inputService.dispatch.onCall(0).resolves(password); + inputService.dispatch.onCall(1).resolves(password); + inputService.dispatch.onCall(2).resolves(verificationCode); + const outputService = sinon.createStubInstance(OutputService); + assert.deepEqual(await obj.prompt(inputService, outputService), { + password, + verificationCode, + }); + }); + it("should invoke the validator", async () => { + const facade = sinon.createStubInstance(CognitoFacade); + const obj = new CognitoPasswordResetChallenge(facade); + const inputService = sinon.createStubInstance(InputService); + const password = "Password123"; + const verificationCode = "123456"; + inputService.dispatch.onCall(0).resolves(password); + inputService.dispatch.onCall(1).resolves(password); + inputService.dispatch.onCall(2).callsFake(function (prompt) { + return Promise.resolve(prompt.validator(verificationCode)); + }); + const outputService = sinon.createStubInstance(OutputService); + assert.deepEqual(await obj.prompt(inputService, outputService), { + password, + verificationCode, + }); + }); + }); + describe("#complete", () => { + it("should invoke the result processor", async () => { + const username = "foobar"; + const password = "Password123"; + const user = { username }; + const facade = sinon.createStubInstance(CognitoFacade); + const facadeResult = { incomplete: false, user, username }; + facade.logIn.resolves(facadeResult); + const obj = new CognitoPasswordResetChallenge(facade); + const session = sinon.createStubInstance(CognitoUserSession); + session.getIdToken.returns({ getJwtToken: () => "" }); + session.getAccessToken.returns({ getJwtToken: () => "" }); + session.getRefreshToken.returns({ getToken: () => "" }); + const cognitoIdpUser = new CognitoIdpUser(username, user); + const authentication = new CognitoAuthentication(cognitoIdpUser, session); + const stub = sinon.stub(obj, "processCognitoResult"); + stub.returns(authentication); + const result = await obj.complete({ username, password }); + assert.strictEqual(result, authentication); + }); + }); +}); +describe("CognitoAuthentication", () => { + describe("#construct", () => { + it("should have the session property", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoSession = sinon.createStubInstance(CognitoUserSession); + cognitoSession.isValid.returns(true); + cognitoSession.getIdToken.returns({ getJwtToken: () => "idToken" }); + cognitoSession.getAccessToken.returns({ + getJwtToken: () => "accessToken", + }); + cognitoSession.getRefreshToken.returns({ + getToken: () => "refreshToken", + }); + cognitoUser.getSession.callsFake((callback) => { + callback(undefined, cognitoSession); + }); + const user = new CognitoIdpUser(username, cognitoUser); + const obj = new CognitoAuthentication(user, cognitoSession); + assert.strictEqual(await obj.session, cognitoSession); + }); + it("should call the super constructor correctly", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoSession = sinon.createStubInstance(CognitoUserSession); + cognitoSession.isValid.returns(true); + cognitoSession.getIdToken.returns({ getJwtToken: () => "idToken" }); + cognitoSession.getAccessToken.returns({ + getJwtToken: () => "accessToken", + }); + cognitoSession.getRefreshToken.returns({ + getToken: () => "refreshToken", + }); + cognitoUser.getSession.callsFake((callback) => { + callback(undefined, cognitoSession); + }); + const user = new CognitoIdpUser(username, cognitoUser); + const obj = new CognitoAuthentication(user, cognitoSession); + assert.strictEqual(obj.user, user); + assert.strictEqual(obj.accessToken, "accessToken"); + assert.strictEqual(obj.idToken, "idToken"); + assert.strictEqual(obj.refreshToken, "refreshToken"); + }); + }); +}); diff --git a/test/auth/cognito/storage.spec.js b/test/auth/cognito/storage.spec.js new file mode 100644 index 0000000..66cf4b9 --- /dev/null +++ b/test/auth/cognito/storage.spec.js @@ -0,0 +1,107 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { CognitoStorage } = require("../../../lib/auth/cognito/storage"); +const SecretService = require("../../../lib/secret/service"); + +describe("CognitoStorage", () => { + describe("#values", () => { + it("should return same Map passed to constructor", async () => { + const values = new Map([ + ["foo", 123], + ["bar", 456], + ]); + const obj = new CognitoStorage(values); + assert.deepEqual(obj.values, values); + }); + it("should use an empty Map if no map is provided", async () => { + const obj = new CognitoStorage(); + assert.deepEqual(obj.values, new Map()); + }); + }); + describe("#clear", () => { + it("should remove all values", async () => { + const values = new Map([ + ["foo", 123], + ["bar", 456], + ]); + const obj = new CognitoStorage(values); + obj.clear(); + assert.deepEqual(obj.values, new Map()); + }); + }); + describe("#length", () => { + it("should give the proper length", async () => { + const values = new Map([ + ["foo", 123], + ["bar", 456], + ["baz", 789], + ]); + const obj = new CognitoStorage(values); + assert.strictEqual(obj.length, 3); + }); + it("should give zero for empty map", async () => { + const values = new Map(); + const obj = new CognitoStorage(values); + assert.strictEqual(obj.length, 0); + }); + }); + describe("#getItem", () => { + it("should return the correct value", async () => { + const values = new Map([["foo", 123]]); + const obj = new CognitoStorage(values); + assert.strictEqual(obj.getItem("foo"), 123); + }); + it("should return null for nonexistent item", async () => { + const values = new Map(); + const obj = new CognitoStorage(values); + assert.strictEqual(obj.getItem("foo"), null); + }); + }); + describe("#setItem", () => { + it("should return the correct value once set", async () => { + const values = new Map(); + const obj = new CognitoStorage(values); + obj.setItem("foo", 123); + assert.strictEqual(obj.getItem("foo"), 123); + }); + }); + describe("#removeItem", () => { + it("should return null after item is removed", async () => { + const values = new Map([["foo", 123]]); + const obj = new CognitoStorage(values); + obj.removeItem("foo"); + assert.strictEqual(obj.getItem("foo"), null); + }); + }); + describe("#create", () => { + it("should invoke the SecretService", async () => { + const values = new Map([["foo", "bar"]]); + const secretService = sinon.createStubInstance(SecretService); + secretService.all.withArgs("auth.cognito").returns(values); + const obj = CognitoStorage.create(secretService); + assert.strictEqual(obj.values, values); + }); + }); + describe("#store", () => { + it("should clear the namespace without values", async () => { + const values = new Map(); + const obj = new CognitoStorage(values); + const secretService = sinon.createStubInstance(SecretService); + obj.store(secretService); + assert.ok(secretService.clear.calledOnce); + assert.ok(secretService.clear.calledWith("auth.cognito")); + }); + it("should save the values", async () => { + const values = new Map([ + ["foo", 123], + ["bar", 456], + ]); + const obj = new CognitoStorage(values); + const secretService = sinon.createStubInstance(SecretService); + obj.store(secretService); + assert.ok(secretService.set.calledTwice); + assert.ok(secretService.set.calledWith("auth.cognito", "foo", 123)); + assert.ok(secretService.set.calledWith("auth.cognito", "bar", 456)); + }); + }); +}); diff --git a/test/auth/cognito/user.spec.js b/test/auth/cognito/user.spec.js new file mode 100644 index 0000000..1faa9e7 --- /dev/null +++ b/test/auth/cognito/user.spec.js @@ -0,0 +1,93 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { + CognitoUser, + CognitoUserSession, +} = require("amazon-cognito-identity-js"); +const { CognitoIdpUser } = require("../../../lib/auth/cognito/user"); + +describe("CognitoIdpUser", () => { + describe("#getCognitoSession", () => { + it("should return a Promise", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + cognitoUser.getSession.callsFake((callback) => { + callback(undefined, { isValid: () => true }); + }); + const obj = new CognitoIdpUser(username, cognitoUser); + assert.ok(obj.getCognitoSession() instanceof Promise); + }); + it("should resolve to the session if successful", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoSession = sinon.createStubInstance(CognitoUserSession); + cognitoSession.isValid.returns(true); + cognitoUser.getSession.callsFake((callback) => { + callback(undefined, cognitoSession); + }); + const obj = new CognitoIdpUser(username, cognitoUser); + assert.strictEqual(await obj.getCognitoSession(), cognitoSession); + }); + it("should reject if an error occurs", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoSession = sinon.createStubInstance(CognitoUserSession); + const error = new Error("Test error"); + cognitoUser.getSession.callsFake((callback) => { + callback(error, undefined); + }); + const obj = new CognitoIdpUser(username, cognitoUser); + await assert.rejects(async () => obj.getCognitoSession(), error); + }); + it("should reject if the session is invalid", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoSession = sinon.createStubInstance(CognitoUserSession); + cognitoSession.isValid.returns(false); + cognitoUser.getSession.callsFake((callback) => { + callback(undefined, cognitoSession); + }); + const obj = new CognitoIdpUser(username, cognitoUser); + await assert.rejects(async () => obj.getCognitoSession(), { + name: "Error", + message: "Cognito session refreshed but is invalid", + }); + }); + }); + + describe("#provider", () => { + it("should always return cognito", async () => { + const obj = new CognitoIdpUser("", {}); + assert.strictEqual(obj.provider, "cognito"); + }); + }); + + describe("#cognitoUser", () => { + it("should return the object provided", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const obj = new CognitoIdpUser(username, cognitoUser); + assert.strictEqual(obj.cognitoUser, cognitoUser); + }); + }); + + describe("#getLabel", () => { + it("should return the string", async () => { + const username = "foobar"; + const cognitoUser = sinon.createStubInstance(CognitoUser); + const cognitoSession = sinon.createStubInstance(CognitoUserSession); + cognitoSession.isValid.returns(true); + cognitoSession.getIdToken.returns({ + payload: { email: `${username}@example.com` }, + }); + cognitoUser.getSession.callsFake((callback) => { + callback(undefined, cognitoSession); + }); + const obj = new CognitoIdpUser(username, cognitoUser); + assert.strictEqual( + await obj.getLabel(), + `Amazon Cognito user: foobar@example.com (foobar)` + ); + }); + }); +}); diff --git a/test/auth/result.spec.js b/test/auth/result.spec.js new file mode 100644 index 0000000..111d367 --- /dev/null +++ b/test/auth/result.spec.js @@ -0,0 +1,90 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const InputService = require("../../lib/io/input-service"); +const OutputService = require("../../lib/io/output-service"); +const { Result, Challenge, Authentication } = require("../../lib/auth/result"); + +describe("Result", () => { + describe("#incomplete", () => { + it("should always return true", async () => { + const obj = new Result(); + assert.ok(obj.incomplete); + }); + }); + + describe("#challenge", () => { + it("should always return false", async () => { + const obj = new Result(); + assert.ok(!obj.challenge); + }); + }); +}); + +describe("Challenge", () => { + describe("#challenge", () => { + it("should always return true", async () => { + const obj = new Challenge(); + assert.ok(obj.challenge); + }); + }); + describe("#prompt", () => { + it("should return resolved promise", async () => { + const obj = new Challenge(); + const inputService = new InputService(); + const outputService = new OutputService(); + const actual = obj.prompt(inputService, outputService); + assert.ok(actual instanceof Promise); + const result = await actual; + assert.strictEqual(result, undefined); + }); + }); + describe("#complete", () => { + it("should always throw an exception", async () => { + const obj = new Challenge(); + assert.throws( + () => { + obj.complete({}); + }, + { + name: "Error", + message: "Subclasses must override this method", + } + ); + }); + }); +}); + +describe("Authentication", () => { + describe("#constructor", () => { + it("should return a user", async () => { + const user = { foo: "bar" }; + const obj = new Authentication(user, "", "", ""); + assert.strictEqual(obj.user, user); + }); + it("should return the id token", async () => { + const user = { foo: "bar" }; + const idToken = "foobar"; + const obj = new Authentication(user, idToken, "", ""); + assert.strictEqual(obj.idToken, idToken); + }); + it("should return the access token", async () => { + const user = { foo: "bar" }; + const accessToken = "foobar"; + const obj = new Authentication(user, "", accessToken, ""); + assert.strictEqual(obj.accessToken, accessToken); + }); + it("should return the refresh token", async () => { + const user = { foo: "bar" }; + const refreshToken = "foobar"; + const obj = new Authentication(user, "", "", refreshToken); + assert.strictEqual(obj.refreshToken, refreshToken); + }); + }); + describe("#incomplete", () => { + it("should always return false", async () => { + const user = { foo: "bar" }; + const obj = new Authentication(user, "", "", ""); + assert.ok(!obj.incomplete); + }); + }); +}); diff --git a/test/auth/user.spec.js b/test/auth/user.spec.js new file mode 100644 index 0000000..c50f679 --- /dev/null +++ b/test/auth/user.spec.js @@ -0,0 +1,31 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { User } = require("../../lib/auth/user"); + +describe("User", () => { + describe("#anonymous", () => { + it("should return true if null username", async () => { + const obj = new User(); + assert.ok(obj.anonymous); + }); + it("should return false if string username", async () => { + const obj = new User("foobar"); + assert.ok(!obj.anonymous); + }); + }); + + describe("#provider", () => { + it("should always return anonymous", async () => { + const obj = new User(); + assert.strictEqual(obj.provider, "anonymous"); + }); + }); + + describe("#username", () => { + it("should return the username provided", async () => { + const username = "foobar"; + const obj = new User(username); + assert.strictEqual(obj.username, username); + }); + }); +}); diff --git a/test/config/command.spec.js b/test/config/command.spec.js index fa80a1c..83e8bb2 100644 --- a/test/config/command.spec.js +++ b/test/config/command.spec.js @@ -1,11 +1,20 @@ const assert = require("assert"); const sinon = require("sinon"); +const { Command } = require("commander"); const ConfigCommand = require("../../lib/config/command"); const ConfigService = require("../../lib/config/service"); const OutputService = require("../../lib/io/output-service"); const Yaml = require("../../lib/io/yaml"); describe("ConfigCommand", function () { + describe("#createCommand", () => { + it("should return a Commander instance", async () => { + const configService = new ConfigService(); + const outputService = new OutputService(); + const obj = new ConfigCommand(configService, outputService); + assert.ok(obj.createCommand() instanceof Command); + }); + }); describe("#get", function () { it("should call the get method", async function () { const configService = new ConfigService(); diff --git a/test/config/value-set.spec.js b/test/config/value-set.spec.js index 42ffb0a..ecea12b 100644 --- a/test/config/value-set.spec.js +++ b/test/config/value-set.spec.js @@ -73,15 +73,68 @@ describe("ValueSet", function () { }); describe("#set", function () { - it("returns what is there", async function () { - const obj = new ValueSet(); - assert.deepEqual(obj.export(), {}); + it("sets a value", async () => { + const obj = new ValueSet({}); const expected = 123; obj.set("foo", "bar", expected); assert.strictEqual(obj.get("foo", "bar"), expected); - const expected2 = 123; + }); + it("sets a value in a non-existent namespace", async function () { + const obj = new ValueSet({ foo: { bar: 123 } }); + const expected2 = 234; obj.set("foo", "baz", expected2); assert.strictEqual(obj.get("foo", "baz"), expected2); }); }); + + describe("#remove", () => { + it("shouldn't error if the namespace doesn't exist", async () => { + const obj = new ValueSet(); + assert.doesNotThrow(() => { + obj.remove("foo", "bar"); + }, Error); + assert.deepEqual(obj.get("foo", "bar"), undefined); + }); + it("should remove a value", async () => { + const obj = new ValueSet({ foo: { bar: 123 } }); + obj.remove("foo", "bar"); + assert.deepEqual(obj.get("foo", "bar"), undefined); + }); + }); + + describe("#all", function () { + it("should return a namespace", async () => { + const obj = new ValueSet({ foo: { bar: 123, baz: 234 } }); + assert.deepEqual( + obj.all("foo"), + new Map([ + ["bar", 123], + ["baz", 234], + ]) + ); + }); + it("should return an empty map for a nonexistent namespace", async () => { + const obj = new ValueSet({}); + assert.deepEqual(obj.all("foo"), new Map()); + }); + }); + + describe("#clear", function () { + it("shouldn't error if the namespace doesn't exist", async () => { + const obj = new ValueSet({}); + assert.doesNotThrow(() => { + obj.clear("foo"); + }, Error); + }); + it("should delete a namespace", async () => { + const obj = new ValueSet({ foo: { bar: 123, baz: 234 } }); + obj.clear("foo"); + assert.deepEqual(obj.export(), {}); + }); + it("should not clobber other namespaces", async () => { + const obj = new ValueSet({ foo: { bar: 123 }, oof: { rab: 234 } }); + obj.clear("foo"); + assert.deepEqual(obj.export(), { oof: { rab: 234 } }); + }); + }); }); diff --git a/test/index.spec.js b/test/index.spec.js index fee90f8..2ffd9bc 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -7,13 +7,6 @@ const ora = require("ora"); const xdg = require("@folder/xdg"); const Config = require("../lib/config/value-set"); const { Container } = require("../lib/container"); -const OutputService = require("../lib/io/output-service"); -const Yaml = require("../lib/io/yaml"); -const ConfigService = require("../lib/config/service"); -const SecretService = require("../lib/secret/service"); -const PluginService = require("../lib/plugin/service"); -const ActivationSet = require("../lib/plugin/activation-set"); -const PluginManager = require("../lib/plugin/manager"); const Miles = require("../"); describe("Miles", function () { diff --git a/test/io/input-service.spec.js b/test/io/input-service.spec.js index 138e194..58ae2f3 100644 --- a/test/io/input-service.spec.js +++ b/test/io/input-service.spec.js @@ -2,6 +2,7 @@ const assert = require("assert"); const sinon = require("sinon"); const promptly = require("promptly"); const InputService = require("../../lib/io/input-service"); +const { Prompt } = require("../../lib/io/prompt"); describe("InputService", () => { describe("#prompt", () => { @@ -50,6 +51,27 @@ describe("InputService", () => { stub1.restore(); } }); + it("should use promptly for passwords correctly", async () => { + const message = "foo"; + const validator = (a) => a; + const object = new InputService(); + const stub1 = sinon.stub(promptly, "prompt"); + try { + await object.prompt(message, validator, true); + assert(stub1.calledOnce); + assert( + stub1.calledWith(message, { + validator, + silent: true, + trim: false, + replace: "*", + default: "", + }) + ); + } finally { + stub1.restore(); + } + }); }); describe("#getOptionOrPrompt", () => { it("should prompt if key not in the options array", async () => { @@ -136,4 +158,35 @@ describe("InputService", () => { assert.ok(spy.calledWith(expected)); }); }); + describe("#dispatch", () => { + it("should call promptly without hint", async () => { + const intro = "foo"; + const validator = (a) => a; + const prompt = new Prompt({ name: "test", intro, validator }); + const object = new InputService(); + const stub1 = sinon.stub(promptly, "prompt"); + try { + await object.dispatch(prompt); + assert(stub1.calledOnce); + assert(stub1.calledWith(`${intro}`, { validator })); + } finally { + stub1.restore(); + } + }); + it("should call promptly with hint", async () => { + const intro = "foo"; + const hint = "y/n"; + const validator = (a) => a; + const prompt = new Prompt({ name: "test", intro, hint, validator }); + const object = new InputService(); + const stub1 = sinon.stub(promptly, "prompt"); + try { + await object.dispatch(prompt); + assert(stub1.calledOnce); + assert(stub1.calledWith(`${intro} [${hint}]`, { validator })); + } finally { + stub1.restore(); + } + }); + }); }); diff --git a/test/io/output-service.spec.js b/test/io/output-service.spec.js index dcb7a83..fd8ea13 100644 --- a/test/io/output-service.spec.js +++ b/test/io/output-service.spec.js @@ -10,6 +10,30 @@ describe("OutputService", function () { assert.strictEqual(object.spinner, spinner); }); }); + describe("#error", () => { + it("should call console.error", async () => { + const object = new OutputService(); + const consoleStub = sinon.stub(console, "error"); + try { + object.error("foo"); + assert.ok(consoleStub.calledWith("foo")); + } finally { + consoleStub.restore(); + } + }); + }); + describe("#write", () => { + it("should call console.log", async () => { + const object = new OutputService(); + const consoleStub = sinon.stub(console, "log"); + try { + object.write("foobar"); + assert.ok(consoleStub.calledWith("foobar")); + } finally { + consoleStub.restore(); + } + }); + }); describe("#spinForPromise", function () { it("should call succeed on resolution", async () => { const spinner = { diff --git a/test/io/prompt.spec.js b/test/io/prompt.spec.js new file mode 100644 index 0000000..a7a1d92 --- /dev/null +++ b/test/io/prompt.spec.js @@ -0,0 +1,151 @@ +const assert = require("assert"); +const sinon = require("sinon"); +const { + createEmptyValidator, + createRegExpValidator, + Prompt, + YesNoPrompt, +} = require("../../lib/io/prompt"); + +describe("Prompt", () => { + describe("#createRegExpValidator", () => { + it("should return a function", async () => { + const validator = createRegExpValidator( + /^[a-z]+$/, + "Lowercase letters only" + ); + assert.strictEqual(typeof validator, "function"); + }); + it("should fail appropriately", async () => { + const validator = createRegExpValidator( + "/^[a-z]+$/", + "Lowercase letters only" + ); + assert.throws(() => validator(1), { + name: "Error", + message: "Lowercase letters only", + }); + }); + it("should use the default filter", async () => { + const validator = createRegExpValidator(/^[0-9]+$/, "Numbers only"); + assert.strictEqual(validator("123"), "123"); + }); + it("should use the supplied filter", async () => { + const validator = createRegExpValidator(/^[0-9]+$/, "Numbers only", (v) => + parseInt(v) + ); + assert.strictEqual(validator("123"), 123); + }); + }); + describe("#createEmptyValidator", () => { + it("should return a function", async () => { + const validator = createEmptyValidator("Empty values are not allowed"); + assert.strictEqual(typeof validator, "function"); + }); + it("should fail appropriately", async () => { + const errorMessage = "Empty values are not allowed"; + const validator = createEmptyValidator(errorMessage); + assert.throws(() => validator(""), { + name: "Error", + message: errorMessage, + }); + }); + it("should use the default filter", async () => { + const validator = createEmptyValidator("Empty values are not allowed"); + assert.strictEqual(validator("123"), "123"); + }); + it("should use the supplied filter", async () => { + const validator = createEmptyValidator( + "Empty values are not allowed", + (v) => parseInt(v) + ); + assert.strictEqual(validator("123"), 123); + }); + }); + describe("#construct", () => { + it("should return defaults", async () => { + const options = {}; + const prompt = new Prompt(options); + assert.strictEqual(prompt.name, ""); + assert.strictEqual(prompt.intro, undefined); + assert.strictEqual(prompt.hint, undefined); + assert.strictEqual(typeof prompt.validator, "function"); + assert.strictEqual(prompt.hidden, false); + }); + it("should return name", async () => { + const options = { name: "foobar" }; + const prompt = new Prompt(options); + assert.strictEqual(prompt.name, options.name); + }); + it("should return intro", async () => { + const options = { name: "foobar", intro: "Enter some text" }; + const prompt = new Prompt(options); + assert.strictEqual(prompt.intro, options.intro); + }); + it("should return hint", async () => { + const options = { name: "foobar", hint: "y/n" }; + const prompt = new Prompt(options); + assert.strictEqual(prompt.hint, options.hint); + }); + it("should return validator", async () => { + const options = { name: "foobar", validator: (v) => v }; + const prompt = new Prompt(options); + assert.strictEqual(prompt.validator, options.validator); + }); + }); +}); +describe("YesNoPrompt", () => { + describe("#construct", () => { + it("has correct name", async () => { + const prompt = new YesNoPrompt("Should this pass?"); + assert.strictEqual(prompt.name, "yes-no"); + }); + it("has correct intro", async () => { + const intro = "Should this pass?"; + const prompt = new YesNoPrompt(intro); + assert.strictEqual(prompt.intro, intro); + }); + it("has correct hint", async () => { + const prompt = new YesNoPrompt("Should this pass?"); + assert.strictEqual(prompt.hint, "y/n"); + }); + it("validator passes correctly", async () => { + const prompt = new YesNoPrompt("Intro text"); + assert.strictEqual(prompt.validator("y"), true); + assert.strictEqual(prompt.validator("Y"), true); + assert.strictEqual(prompt.validator("n"), false); + assert.strictEqual(prompt.validator("N"), false); + }); + it("validator fails correctly", async () => { + const prompt = new YesNoPrompt("Intro text"); + assert.throws( + () => { + prompt.validator(""); + }, + { name: "Error", message: "You must enter Y, y, N, or n" } + ); + assert.throws( + () => { + prompt.validator("a"); + }, + { name: "Error", message: "You must enter Y, y, N, or n" } + ); + }); + it("uses true fallback correctly", async () => { + const fallback = true; + const prompt = new YesNoPrompt("Intro text", fallback); + assert.strictEqual(prompt.validator(""), fallback); + assert.strictEqual(prompt.hint, "Y/n"); + assert.strictEqual(prompt.validator("n"), false); + assert.strictEqual(prompt.validator("y"), true); + }); + it("uses false fallback correctly", async () => { + const fallback = false; + const prompt = new YesNoPrompt("Intro text", fallback); + assert.strictEqual(prompt.validator(""), fallback); + assert.strictEqual(prompt.hint, "y/N"); + assert.strictEqual(prompt.validator("n"), false); + assert.strictEqual(prompt.validator("y"), true); + }); + }); +}); diff --git a/test/plugin/command.spec.js b/test/plugin/command.spec.js index a8e9034..0633db2 100644 --- a/test/plugin/command.spec.js +++ b/test/plugin/command.spec.js @@ -1,5 +1,6 @@ const assert = require("assert"); const sinon = require("sinon"); +const { Command } = require("commander"); const Npm = require("../../lib/npm"); const PluginCommand = require("../../lib/plugin/command"); const PluginService = require("../../lib/plugin/service"); @@ -8,6 +9,15 @@ const OutputService = require("../../lib/io/output-service"); const Yaml = require("../../lib/io/yaml"); describe("PluginCommand", function () { + describe("#createCommand", () => { + it("should return a Commander instance", async () => { + const pluginService = sinon.createStubInstance(PluginService); + const outputService = new OutputService(); + let logstub = { info: () => {} }; + const obj = new PluginCommand(logstub, outputService, pluginService); + assert.ok(obj.createCommand() instanceof Command); + }); + }); describe("#install", function () { it("should install the plugin", async function () { const pluginService = sinon.createStubInstance(PluginService);