diff --git a/package.json b/package.json index d17bb197d..34d0363a6 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "immutability-helper": "^2.2.0", "imports-loader": "^0.7.1", "inquirer": "^3.2.2", + "ioredis": "^3.1.4", "istanbul": "^1.1.0-alpha.1", "joi": "^10.6.0", "json-loader": "^0.5.7", @@ -162,7 +163,6 @@ "react-toastify": "^1.5.0", "react-transition-group": "^1.1.3", "recompose": "^0.23.1", - "redis": "^2.8.0", "redux": "^3.6.0", "redux-thunk": "^2.1.0", "resolve": "^1.4.0", diff --git a/services/cache.js b/services/cache.js index 31a8fd030..77db0ce1f 100644 --- a/services/cache.js +++ b/services/cache.js @@ -1,6 +1,5 @@ const redis = require('./redis'); const debug = require('debug')('talk:services:cache'); -const crypto = require('crypto'); const cache = module.exports = {}; @@ -52,60 +51,6 @@ cache.wrap = async (key, expiry, work, kf = keyfunc) => { return value; }; -// This is designed to increment a key and add an expiry iff the key already -// exists. -const INCR_SCRIPT = ` -if redis.call('GET', KEYS[1]) ~= false then - redis.call('INCR', KEYS[1]) - redis.call('EXPIRE', KEYS[1], ARGV[1]) -end -`; - -// This is designed to decrement a key and add an expiry iff the key already -// exists. -const DECR_SCRIPT = ` -if redis.call('GET', KEYS[1]) ~= false then - redis.call('DECR', KEYS[1]) - redis.call('EXPIRE', KEYS[1], ARGV[1]) -end -`; - -// Load the script into redis and track the script hash that we will use to exec -// increments on. -const loadScript = (name, script) => new Promise((resolve, reject) => { - - let shasum = crypto.createHash('sha1'); - shasum.update(script); - - let hash = shasum.digest('hex'); - - cache.client - .script('EXISTS', hash, (err, [exists]) => { - if (err) { - return reject(err); - } - - if (exists) { - debug(`already loaded ${name} as SHA[${hash}], not loading again`); - - return resolve(hash); - } - - debug(`${name} not loaded as SHA[${hash}], loading`); - - cache.client - .script('load', script, (err, hash) => { - if (err) { - return reject(err); - } - - debug(`loaded ${name} as SHA[${hash}]`); - - resolve(hash); - }); - }); -}); - /** * Init sets up the scripts used in Redis with the incr/decr commands. */ @@ -114,93 +59,77 @@ cache.init = async () => { // Create the redis instance. cache.client = redis.createClient(); - // Load the INCR_SCRIPT and DECR_SCRIPT into Redis. - let [incrScriptHash, decrScriptHash] = await Promise.all([ - loadScript('INCR_SCRIPT', INCR_SCRIPT), - loadScript('DECR_SCRIPT', DECR_SCRIPT) - ]); + // This is designed to increment a key and add an expiry iff the key already + // exists. + const INCR_SCRIPT = ` + if redis.call('GET', KEYS[1]) ~= false then + redis.call('INCR', KEYS[1]) + redis.call('EXPIRE', KEYS[1], ARGV[1]) + end + `; - // Set the globally scoped cache hashes. - cache.INCR_SCRIPT_HASH = incrScriptHash; - cache.DECR_SCRIPT_HASH = decrScriptHash; + cache.client.defineCommand('increx', { + numberOfKeys: 1, + lua: INCR_SCRIPT, + }); + + // This is designed to decrement a key and add an expiry iff the key already + // exists. + const DECR_SCRIPT = ` + if redis.call('GET', KEYS[1]) ~= false then + redis.call('DECR', KEYS[1]) + redis.call('EXPIRE', KEYS[1], ARGV[1]) + end + `; + + cache.client.defineCommand('decrex', { + numberOfKeys: 1, + lua: DECR_SCRIPT, + }); }; /** * This will increment a key in redis and update the expiry iff it already * exists, otherwise it will do nothing. */ -cache.incr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => { - cache.client - .evalsha(cache.INCR_SCRIPT_HASH, 1, kf(key), expiry, (err) => { - if (err) { - return reject(err); - } - - return resolve(); - }); -}); +cache.incr = async (key, expiry, kf = keyfunc) => cache.client.increx(kf(key), expiry); /** * This will decrement a key in redis and update the expiry iff it already * exists, otherwise it will do nothing. */ -cache.decr = (key, expiry, kf = keyfunc) => new Promise((resolve, reject) => { - cache.client - .evalsha(cache.DECR_SCRIPT_HASH, 1, kf(key), expiry, (err) => { - if (err) { - return reject(err); - } - - return resolve(); - }); -}); +cache.decr = async (key, expiry, kf = keyfunc) => cache.client.decrex(kf(key, expiry)); /** * This will increment many keys in redis and update the expiry iff it already * exists, otherwise it will do nothing. */ -cache.incrMany = (keys, expiry, kf = keyfunc) => { +cache.incrMany = async (keys, expiry, kf = keyfunc) => { let multi = cache.client.multi(); - keys.forEach((key) => { + for (const key of keys) { // Queue up the evalsha command. - multi.evalsha(cache.INCR_SCRIPT_HASH, 1, kf(key), expiry); - }); + multi.increx(kf(key), expiry); + } - return new Promise((resolve, reject) => { - multi.exec((err) => { - if (err) { - return reject(err); - } - - resolve(); - }); - }); + return multi.exec(); }; /** * This will decrement many keys in redis and update the expiry iff it already * exists, otherwise it will do nothing. */ -cache.decrMany = (keys, expiry, kf = keyfunc) => { +cache.decrMany = async (keys, expiry, kf = keyfunc) => { let multi = cache.client.multi(); - keys.forEach((key) => { + for (const key of keys) { // Queue up the evalsha command. - multi.evalsha(cache.DECR_SCRIPT_HASH, 1, kf(key), expiry); - }); + multi.decrex(kf(key), expiry); + } - return new Promise((resolve, reject) => { - multi.exec((err) => { - if (err) { - return reject(err); - } - - resolve(); - }); - }); + return multi.exec(); }; /** @@ -257,28 +186,12 @@ cache.wrapMany = async (keys, expiry, work, kf = keyfunc) => { * @param {Mixed} key Either an array of items composing a key or a string * @return {Promise} */ -cache.get = (key, kf = keyfunc) => new Promise((resolve, reject) => { - cache.client.get(kf(key), (err, reply) => { - if (err) { - return reject(err); - } +cache.get = async (key, kf = keyfunc) => cache.client.get(kf(key)).then((reply) => { + if (reply !== null) { - if (reply !== null) { - let value; - - try { - - // Parse the stored cache value from JSON. - value = JSON.parse(reply); - } catch (e) { - return reject(e); - } - - return resolve(value); - } - - resolve(null); - }); + // Parse the stored cache value from JSON. + return JSON.parse(reply); + } }); /** @@ -288,31 +201,22 @@ cache.get = (key, kf = keyfunc) => new Promise((resolve, reject) => { * @param {Function} [kf=keyfunc] optional key function to use to turn the * provided key into a string for the cache. */ -cache.getMany = (keys, kf = keyfunc) => new Promise((resolve, reject) => { - cache.client.mget(keys.map(kf), (err, replies) => { - if (err) { - return reject(err); +cache.getMany = async (keys, kf = keyfunc) => cache.client.mget(keys.map(kf)).then((replies) => { + + // Parse the replies. + for (let i = 0; i < replies.length; i++) { + let value = null; + + if (replies[i] != null) { + + // Parse the stored cache value from JSON. + value = JSON.parse(replies[i]); } - // Parse the replies. - for (let i = 0; i < replies.length; i++) { - let value = null; + replies[i] = value; + } - if (replies[i] != null) { - try { - - // Parse the stored cache value from JSON. - value = JSON.parse(replies[i]); - } catch (e) { - return reject(e); - } - } - - replies[i] = value; - } - - return resolve(replies); - }); + return replies; }); /** @@ -322,7 +226,7 @@ cache.getMany = (keys, kf = keyfunc) => new Promise((resolve, reject) => { * @param {Function} [kf=keyfunc] optional key function to use to turn the * provided key into a string for the cache. */ -cache.setMany = (keys, values, expiry, kf = keyfunc) => { +cache.setMany = async (keys, values, expiry, kf = keyfunc) => { let multi = cache.client.multi(); keys.forEach((key, index) => { @@ -334,15 +238,7 @@ cache.setMany = (keys, values, expiry, kf = keyfunc) => { multi.set(kf(key), reply, 'EX', expiry); }); - return new Promise((resolve, reject) => { - multi.exec((err) => { - if (err) { - return reject(err); - } - - resolve(); - }); - }); + return multi.exec(); }; /** @@ -350,18 +246,12 @@ cache.setMany = (keys, values, expiry, kf = keyfunc) => { * @param {Mixed} key Either an array of items composing a key or a string * @return {Promise} */ -cache.invalidate = (key, kf = keyfunc) => new Promise((resolve, reject) => { +cache.invalidate = async (key, kf = keyfunc) => { debug(`invalidate: ${kf(key)}`); - cache.client.del(kf(key), (err) => { - if (err) { - return reject(err); - } - - resolve(); - }); -}); + return cache.client.del(kf(key)); +}; /** * This sets a value on the key with the expiry and then resolves once it is @@ -371,16 +261,10 @@ cache.invalidate = (key, kf = keyfunc) => new Promise((resolve, reject) => { * @param {Integer} expiry Time in seconds for the cache entry to live for * @return {Promise} */ -cache.set = (key, value, expiry, kf = keyfunc) => new Promise((resolve, reject) => { +cache.set = async (key, value, expiry, kf = keyfunc) => { // Serialize the value as JSON. let reply = JSON.stringify(value); - cache.client.set(kf(key), reply, 'EX', expiry, (err) => { - if (err) { - return reject(err); - } - - return resolve(); - }); -}); + return cache.client.set(kf(key), reply, 'EX', expiry); +}; diff --git a/services/passport.js b/services/passport.js index f5eed001b..83635fa85 100644 --- a/services/passport.js +++ b/services/passport.js @@ -159,40 +159,33 @@ async function ValidateUserLogin(loginProfile, user, done) { /** * Revoke the token on the request. */ -const HandleLogout = (req, res, next) => { +const HandleLogout = async (req, res, next) => { const {jwt} = req; const now = new Date(); const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0); - client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry, (err) => { - if (err) { - return next(err); - } + try { + await client().set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry); + } catch (err) { + return next(err); + } - // Only clear the cookie on logout if enabled. - if (JWT_CLEAR_COOKIE_LOGOUT) { - debug('clearing the login cookie'); - res.clearCookie(JWT_SIGNING_COOKIE_NAME); - } + // Only clear the cookie on logout if enabled. + if (JWT_CLEAR_COOKIE_LOGOUT) { + debug('clearing the login cookie'); + res.clearCookie(JWT_SIGNING_COOKIE_NAME); + } - res.status(204).end(); - }); + res.status(204).end(); }; -const checkGeneralTokenBlacklist = (jwt) => new Promise((resolve, reject) => { - client().get(`jtir[${jwt.jti}]`, (err, expiry) => { - if (err) { - return reject(err); - } - +const checkGeneralTokenBlacklist = (jwt) => client().get(`jtir[${jwt.jti}]`) + .then((expiry) => { if (expiry != null) { - return reject(new errors.ErrAuthentication('token was revoked')); + throw new errors.ErrAuthentication('token was revoked'); } - - return resolve(); }); -}); /** * Check if the given token is already blacklisted, throw an error if it is. diff --git a/services/redis.js b/services/redis.js index b87e5e9fd..72d2d3343 100644 --- a/services/redis.js +++ b/services/redis.js @@ -1,4 +1,4 @@ -const redis = require('redis'); +const Redis = require('ioredis'); const debug = require('debug')('talk:services:redis'); const enabled = require('debug').enabled('talk:services:redis'); const { @@ -14,9 +14,10 @@ const attachMonitors = (client) => { // Debug events. if (enabled) { - client.on('ready', () => debug('client ready')); client.on('connect', () => debug('client connected')); + client.on('ready', () => debug('client ready')); client.on('reconnecting', () => debug('client connection lost, attempting to reconnect')); + client.on('close', () => debug('client closed the connection')); client.on('end', () => debug('client ended')); } @@ -64,19 +65,11 @@ const connectionOptions = { }; const createClient = () => { - let client = redis.createClient(connectionOptions); + let client = new Redis(connectionOptions); // Attach the monitors that will print debug messages to the console. attachMonitors(client); - client.ping((err) => { - if (err) { - console.error('Can\'t ping the redis server!'); - - throw err; - } - }); - return client; }; diff --git a/services/users.js b/services/users.js index 413421088..35810ef7f 100644 --- a/services/users.js +++ b/services/users.js @@ -69,33 +69,25 @@ module.exports = class UsersService { * Indicating that the account should be flagged as "login recaptcha required" * where future login attempts must be made with the recaptcha flag. */ - static recordLoginAttempt(email) { + static async recordLoginAttempt(email) { const rdskey = `la[${email.toLowerCase().trim()}]`; - return new Promise((resolve, reject) => { - client() - .multi() - .incr(rdskey) - .expire(rdskey, RECAPTCHA_WINDOW_SECONDS) - .exec((err, replies) => { - if (err) { - return reject(err); - } + const replies = await client() + .multi() + .incr(rdskey) + .expire(rdskey, RECAPTCHA_WINDOW_SECONDS) + .exec(); - // if this is new or has no expiry - if (replies[0] === 1 || replies[1] === -1) { + // if this is new or has no expiry + if (replies[0] === 1 || replies[1] === -1) { - // then expire it after the timeout - client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS); - } + // then expire it after the timeout + client().expire(rdskey, RECAPTCHA_WINDOW_SECONDS); + } - if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) { - return reject(errors.ErrLoginAttemptMaximumExceeded); - } - - resolve(); - }); - }); + if (replies[0] >= RECAPTCHA_INCORRECT_TRIGGER) { + throw errors.ErrLoginAttemptMaximumExceeded; + } } /** @@ -104,27 +96,17 @@ module.exports = class UsersService { * * errors.ErrLoginAttemptMaximumExceeded */ - static checkLoginAttempts(email) { + static async checkLoginAttempts(email) { const rdskey = `la[${email.toLowerCase().trim()}]`; - return new Promise((resolve, reject) => { - client() - .get(rdskey, (err, reply) => { - if (err) { - return reject(err); - } + const attempts = await client().get(rdskey); + if (!attempts) { + return; + } - if (!reply) { - return resolve(); - } - - if (reply >= RECAPTCHA_INCORRECT_TRIGGER) { - return reject(errors.ErrLoginAttemptMaximumExceeded); - } - - resolve(); - }); - }); + if (attempts >= RECAPTCHA_INCORRECT_TRIGGER) { + throw errors.ErrLoginAttemptMaximumExceeded; + } } /** @@ -217,24 +199,15 @@ module.exports = class UsersService { }); } - static changePassword(id, password) { - return new Promise((resolve, reject) => { - bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => { - if (err) { - return reject(err); - } + static async changePassword(id, password) { + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); - resolve(hashedPassword); - }); - }) - .then((hashedPassword) => { - return UserModel.update({id}, { - $inc: {__v: 1}, - $set: { - password: hashedPassword - } - }); - }); + return UserModel.update({id}, { + $inc: {__v: 1}, + $set: { + password: hashedPassword + } + }); } /** @@ -301,54 +274,48 @@ module.exports = class UsersService { * @param {String} username name of the display user * @param {Function} done callback */ - static createLocalUser(email, password, username) { + static async createLocalUser(email, password, username) { if (!email) { - return Promise.reject(errors.ErrMissingEmail); + throw errors.ErrMissingEmail; } email = email.toLowerCase().trim(); username = username.trim(); - return Promise.all([ + await Promise.all([ UsersService.isValidUsername(username), UsersService.isValidPassword(password) - ]) - .then(() => { // username is valid - return new Promise((resolve, reject) => { - bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => { - if (err) { - return reject(err); - } + ]); - let user = new UserModel({ - username, - lowercaseUsername: username.toLowerCase(), - password: hashedPassword, - roles: [], - profiles: [ - { - id: email, - provider: 'local' - } - ] - }); + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); - user.save((err) => { - if (err) { - if (err.code === 11000) { - if (err.message.match('Username')) { - return reject(errors.ErrUsernameTaken); - } - return reject(errors.ErrEmailTaken); - } - return reject(err); - } - return resolve(user); - }); - }); - }); - }); + let user = new UserModel({ + username, + lowercaseUsername: username.toLowerCase(), + password: hashedPassword, + roles: [], + profiles: [ + { + id: email, + provider: 'local' + } + ] + }); + + try { + user = await user.save(); + } catch (err) { + if (err.code === 11000) { + if (err.message.match('Username')) { + throw errors.ErrUsernameTaken; + } + throw errors.ErrEmailTaken; + } + throw err; + } + + return user; } /** @@ -387,14 +354,14 @@ module.exports = class UsersService { * @param {String} role role to add * @param {Function} done callback after the operation is complete */ - static addRoleToUser(id, role) { + static async addRoleToUser(id, role) { const roles = []; // Check to see if the user role is in the allowable set of roles. if (role && USER_ROLES.indexOf(role) === -1) { // User role is not supported! Error out here. - return Promise.reject(new Error(`role ${role} is not supported`)); + throw new Error(`role ${role} is not supported`); } else if(role) { roles.push(role); } @@ -408,13 +375,13 @@ module.exports = class UsersService { * @param {String} role role to remove * @param {Function} done callback after the operation is complete */ - static removeRoleFromUser(id, role) { + static async removeRoleFromUser(id, role) { // Check to see if the user role is in the allowable set of roles. if (USER_ROLES.indexOf(role) === -1) { // User role is not supported! Error out here. - return Promise.reject(new Error(`role ${role} is not supported`)); + throw new Error(`role ${role} is not supported`); } return UserModel.update({id}, { @@ -430,13 +397,13 @@ module.exports = class UsersService { * @param {String} status status to set * @param {Function} done callback after the operation is complete */ - static setStatus(id, status) { + static async setStatus(id, status) { // Check to see if the user status is in the allowable set of roles. if (USER_STATUS.indexOf(status) === -1) { // User status is not supported! Error out here. - return Promise.reject(new Error(`status ${status} is not supported`)); + throw new Error(`status ${status} is not supported`); } // TODO: current updating status behavior is weird. @@ -583,53 +550,52 @@ module.exports = class UsersService { * Creates a JWT from a user email. Only works for local accounts. * @param {String} email of the local user */ - static createPasswordResetToken(email, loc) { + static async createPasswordResetToken(email, loc) { if (!email || typeof email !== 'string') { - return Promise.reject('email is required when creating a JWT for resetting passord'); + throw new Error('email is required when creating a JWT for resetting passord'); } email = email.toLowerCase(); - return Promise.all([ + const [user, settings] = await Promise.all([ UserModel.findOne({profiles: {$elemMatch: {id: email}}}), - SettingsService.retrieve() - ]) - .then(([user, settings]) => { - if (!user) { + SettingsService.retrieve(), + ]); - // Since we don't want to reveal that the email does/doesn't exist - // just go ahead and resolve the Promise with null and check in the - // endpoint. - return; - } - let redirectDomain; - try { - const {hostname, port} = url.parse(loc); - redirectDomain = hostname; - if (port) { - redirectDomain += `:${port}`; - } - } catch (e) { - return Promise.reject('redirect location is invalid'); - } + if (!user) { - if (settings.domains.whitelist.indexOf(redirectDomain) === -1) { - return Promise.reject('redirect location is not on the list of acceptable domains'); - } + // Since we don't want to reveal that the email does/doesn't exist + // just go ahead and resolve the Promise with null and check in the + // endpoint. + return; + } + let redirectDomain; + try { + const {hostname, port} = url.parse(loc); + redirectDomain = hostname; + if (port) { + redirectDomain += `:${port}`; + } + } catch (e) { + throw new Error('redirect location is invalid'); + } - const payload = { - jti: uuid.v4(), - email, - loc, - userId: user.id, - version: user.__v - }; + if (settings.domains.whitelist.indexOf(redirectDomain) === -1) { + throw new Error('redirect location is not on the list of acceptable domains'); + } - return JWT_SECRET.sign(payload, { - expiresIn: '1d', - subject: PASSWORD_RESET_JWT_SUBJECT - }); - }); + const payload = { + jti: uuid.v4(), + email, + loc, + userId: user.id, + version: user.__v + }; + + return JWT_SECRET.sign(payload, { + expiresIn: '1d', + subject: PASSWORD_RESET_JWT_SUBJECT + }); } /** @@ -755,7 +721,7 @@ module.exports = class UsersService { */ static async createEmailConfirmToken(userID = null, email, referer = ROOT_URL) { if (!email || typeof email !== 'string') { - return Promise.reject('email is required when creating a JWT for resetting passord'); + throw new Error('email is required when creating a JWT for resetting passord'); } // Conform the email to lowercase. diff --git a/test/helpers/redis.js b/test/helpers/redis.js index b013dfe25..90e425c4a 100644 --- a/test/helpers/redis.js +++ b/test/helpers/redis.js @@ -3,12 +3,6 @@ const cache = require('../../services/cache'); const client = createClient(); beforeEach(() => Promise.all([ - new Promise((resolve, reject) => client.flushdb((err) => { - if (err) { - return reject(err); - } - - return resolve(); - })), + client.flushdb(), cache.init(), ])); diff --git a/yarn.lock b/yarn.lock index b88833b81..171ebb4c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1112,7 +1112,7 @@ bluebird@2.9.24: version "2.9.24" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.9.24.tgz#14a2e75f0548323dc35aa440d92007ca154e967c" -bluebird@3.5.0: +bluebird@3.5.0, bluebird@^3.3.4: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" @@ -1602,6 +1602,10 @@ clone@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" +cluster-key-slot@^1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.0.8.tgz#7654556085a65330932a2e8b5976f8e2d0b3e414" + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -2236,6 +2240,10 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +denque@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.2.2.tgz#e06cf7cf0da8badc88cbdaabf8fc0a70d659f1d4" + depd@1.1.0, depd@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" @@ -2908,6 +2916,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flexbuffer@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" + for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -3810,6 +3822,34 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +ioredis@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-3.1.4.tgz#8688293f5f2f1757e1c812ad17cce49f46d811bc" + dependencies: + bluebird "^3.3.4" + cluster-key-slot "^1.0.6" + debug "^2.2.0" + denque "^1.1.0" + flexbuffer "0.0.6" + lodash.assign "^4.2.0" + lodash.bind "^4.2.1" + lodash.clone "^4.5.0" + lodash.clonedeep "^4.5.0" + lodash.defaults "^4.2.0" + lodash.difference "^4.5.0" + lodash.flatten "^4.4.0" + lodash.foreach "^4.5.0" + lodash.isempty "^4.4.0" + lodash.keys "^4.2.0" + lodash.noop "^3.0.1" + lodash.partial "^4.2.1" + lodash.pick "^4.4.0" + lodash.sample "^4.2.1" + lodash.shuffle "^4.2.0" + lodash.values "^4.3.0" + redis-commands "^1.2.0" + redis-parser "^2.4.0" + ip-regex@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd" @@ -4586,7 +4626,7 @@ lodash.assignin@^4.0.9: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" -lodash.bind@^4.1.4: +lodash.bind@^4.1.4, lodash.bind@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" @@ -4594,6 +4634,14 @@ lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.clone@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + lodash.create@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" @@ -4609,19 +4657,23 @@ lodash.defaults@^3.1.2: lodash.assign "^3.0.0" lodash.restparam "^3.0.0" -lodash.defaults@^4.0.1: +lodash.defaults@^4.0.1, lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + lodash.filter@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" -lodash.flatten@^4.2.0: +lodash.flatten@^4.2.0, lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" -lodash.foreach@^4.3.0: +lodash.foreach@^4.3.0, lodash.foreach@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" @@ -4633,6 +4685,10 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" +lodash.isempty@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + lodash.isequal@^4.1.1, lodash.isequal@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" @@ -4653,6 +4709,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.keys@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" + lodash.map@^4.4.0, lodash.map@^4.5.1: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" @@ -4665,10 +4725,18 @@ lodash.merge@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" +lodash.noop@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" +lodash.partial@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.partial/-/lodash.partial-4.2.1.tgz#49f3d8cfdaa3bff8b3a91d127e923245418961d4" + lodash.pick@^4.2.1, lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" @@ -4685,6 +4753,14 @@ lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" +lodash.sample@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lodash.sample/-/lodash.sample-4.2.1.tgz#5e4291b0c753fa1abeb0aab8fb29df1b66f07f6d" + +lodash.shuffle@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b" + lodash.some@^4.4.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" @@ -4697,6 +4773,10 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" +lodash.values@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + lodash@3.10.1, lodash@^3.3.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" @@ -6644,7 +6724,7 @@ redis-commands@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b" -redis-parser@^2.0.0, redis-parser@^2.6.0: +redis-parser@^2.0.0, redis-parser@^2.4.0, redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" @@ -6652,7 +6732,7 @@ redis@^0.12.1: version "0.12.1" resolved "https://registry.yarnpkg.com/redis/-/redis-0.12.1.tgz#64df76ad0fc8acebaebd2a0645e8a48fac49185e" -redis@^2.6.3, redis@^2.8.0: +redis@^2.6.3: version "2.8.0" resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" dependencies: