From d8134314c68746b70a30a42c3d3278a327649e91 Mon Sep 17 00:00:00 2001 From: joostdecock Date: Mon, 31 Oct 2022 08:36:36 +0100 Subject: [PATCH] feat(backend): Added new and legacy password handling --- sites/backend/package.json | 4 +- sites/backend/scripts/mongo-to-sqlite.mjs | 40 +++++++-- sites/backend/src/controllers/user.mjs | 11 +-- sites/backend/src/index.mjs | 2 +- sites/backend/src/utils/crypto.mjs | 81 +++++++++++++++++ sites/backend/src/utils/log.mjs | 3 + sites/backend/src/utils/log/index.js | 45 ---------- yarn.lock | 103 +++++++++++++++++++++- 8 files changed, 227 insertions(+), 62 deletions(-) create mode 100644 sites/backend/src/utils/log.mjs delete mode 100644 sites/backend/src/utils/log/index.js diff --git a/sites/backend/package.json b/sites/backend/package.json index 039a70d5e0e..9072952c555 100644 --- a/sites/backend/package.json +++ b/sites/backend/package.json @@ -12,8 +12,10 @@ }, "dependencies": { "@prisma/client": "4.5.0", + "bcryptjs": "^2.4.3", "crypto": "^1.0.1", - "express": "4.18.2" + "express": "4.18.2", + "pino": "^8.7.0" }, "devDependencies": { "mocha": "^10.1.0", diff --git a/sites/backend/scripts/mongo-to-sqlite.mjs b/sites/backend/scripts/mongo-to-sqlite.mjs index 466eb7fbb99..22f6856b0f5 100644 --- a/sites/backend/scripts/mongo-to-sqlite.mjs +++ b/sites/backend/scripts/mongo-to-sqlite.mjs @@ -27,7 +27,7 @@ if (filter) { // Dump filtered data from raw data const file = 'freesewing-dump.json' data = filterData(JSON.parse(fs.readFileSync(path.resolve(dir, file), { encoding: 'utf-8' }))) - console.log(JSON.stringify(data, null ,2)) + console.log(JSON.stringify(data, null, 2)) } else { // Load filtered data for migration const file = 'freesewing-filtered.json' @@ -47,6 +47,8 @@ if (filter) { console.log() await migratePatterns(data.patterns) console.log() + await migrateSubscribers(data.subscribers) + console.log() } function progress(text) { @@ -55,6 +57,27 @@ function progress(text) { process.stdout.write(text) } +async function migrateSubscribers(subscribers) { + const total = subscribers.length + let i = 0 + for (const sub of subscribers) { + i++ + progress(` 📰 subscriber ${i}/${total}`) + await createSubscriber(sub) + } +} + +async function createSubscriber(sub) { + const record = await prisma.subscriber.create({ + data: { + createdAt: sub.createdAt, + data: JSON.stringify({}), + ehash: hash(clean(sub.email)), + email: encrypt(clean(sub.email)), + }, + }) +} + async function migratePatterns(patterns) { const total = Object.keys(patterns).length let i = 0 @@ -109,7 +132,7 @@ async function migrateUsers(users) { } async function createUser(user) { - const ehash = hash(user.email) + const ehash = hash(clean(user.email)) const record = await prisma.user.create({ data: { consent: user.consent, @@ -122,7 +145,7 @@ async function createUser(user) { newsletter: user.newsletter, password: JSON.stringify({ type: 'v2', - data: user.password + data: user.password, }), patron: user.patron, role: user.role, @@ -133,13 +156,12 @@ async function createUser(user) { data.userhandles[user.handle] = record.id } - /* * Only migrate user data if the user was active in the last 6 months * Unless they are patrons. Keep patrons regardless coz patrons rock. */ function filterData(data) { - let i=0 + let i = 0 const filtered = { users: {}, people: {}, @@ -171,9 +193,9 @@ function filterData(data) { * Migrates role field */ function getRole(entry) { - if (entry.handle === 'joost') return "admin" - if (entry.patron > 0) return "patron" - return "user" + if (entry.handle === 'joost') return 'admin' + if (entry.patron > 0) return 'patron' + return 'user' } /* @@ -225,6 +247,6 @@ function migratePattern(entry) { function migrateSubscriber(entry) { return { createdAt: entry.created, - email: entry.email + email: entry.email, } } diff --git a/sites/backend/src/controllers/user.mjs b/sites/backend/src/controllers/user.mjs index 0508b94181c..f7c570816c1 100644 --- a/sites/backend/src/controllers/user.mjs +++ b/sites/backend/src/controllers/user.mjs @@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken' //import fs from 'fs' import Zip from 'jszip' //import rimraf from 'rimraf' -import { ehash } from '../utils/crypto.mjs' +import { clean, hash } from '../utils/crypto.mjs' export function UserController() {} @@ -16,15 +16,16 @@ UserController.prototype.signup = async (req, res, tools) => { if (!req.body.language) return res.status(400).send('languageMissing') // Requests looks ok - does the user exist? - const hash = ehash(req.body.email) + const emailhash = hash(clean(req.body.email)) // Destructure what we need from tools const { prisma, config, encrypt } = tools - if ((await prisma.user.findUnique({ where: { ehash: hash } }))) return res.status(400).send('emailExists') + if (await prisma.user.findUnique({ where: { ehash: emailhash } })) + return res.status(400).send('emailExists') // It does not. Creating user entry - const username = `user-${hash.slice(0,6)}-${time().slice(-6)}` // Temporary username + const username = `user-${hash.slice(0, 6)}-${time().slice(-6)}` // Temporary username //const user = await.prisma.user.create({ // ehash: hash, // Hash of the email to search on // ihash: hash, // Hash of the (initial) email to search on @@ -37,7 +38,7 @@ UserController.prototype.signup = async (req, res, tools) => { password String */ return res.status(200).send({}) - /* + /* (err, user) => { if (err) return res.sendStatus(500) if (user !== null) return res.status(400).send('userExists') diff --git a/sites/backend/src/index.mjs b/sites/backend/src/index.mjs index ff993a3d1ff..619373b3162 100644 --- a/sites/backend/src/index.mjs +++ b/sites/backend/src/index.mjs @@ -12,7 +12,7 @@ import { verifyConfig } from './config.mjs' // Middleware import { loadExpressMiddleware, loadPassportMiddleware } from './middleware.mjs' // Encryption -import { ehash, encryption } from './utils/crypto.mjs' +import { encryption } from './utils/crypto.mjs' // Bootstrap const config = verifyConfig() diff --git a/sites/backend/src/utils/crypto.mjs b/sites/backend/src/utils/crypto.mjs index a107053e5b9..cf2f3d603a2 100644 --- a/sites/backend/src/utils/crypto.mjs +++ b/sites/backend/src/utils/crypto.mjs @@ -1,4 +1,6 @@ +import bcrypt from 'bcryptjs' // Required for legacy password hashes import { createHash, createCipheriv, createDecipheriv, scryptSync, randomBytes } from 'crypto' +import { log } from './log.mjs' /* * Cleans a string (typically email) for hashing @@ -100,3 +102,82 @@ export const encryption = (stringKey, salt = 'FreeSewing') => { }, } } + +/* + * Salts and hashes a password + */ +function hashPassword(input, salt = false) { + if (salt === false) salt = Buffer.from(randomBytes(16)) + else salt = Buffer.from(salt, 'hex') + const hash = scryptSync(input, salt, 64) + + return { + hash: hash.toString('hex'), + salt: salt.toString('hex'), + } +} + +/* + * Verifies a (user-provided) password against the stored hash + salt + * + * Note that: + * - For legacy password hashes, the password field will hold serialized + * JSON with a 'type' field set to 'v2' and a 'data' field holding the + * legacy hash info to pass to the verifyLegacyPassword() method below. + * - For new password hashes, the password field will hold serialized + * JSON with a 'type' field set to 'v3' and a 'hash' and 'salt' field. + * - When legacy passwords are confirmed, they will be re-hashed and + * updated in the database. The database update is not handled here but + * prepared, by returning the new value for the password field as the + * second element in the returned array. + */ +export function verifyPassword(input, passwordField) { + let data + try { + data = JSON.parse(passwordField) + } catch { + /* + * This should not happen. Let's just log a warning and return false + */ + log.warn(passwordField, 'Unable to parse JSON in password field') + return [false, false] + } + // Is this a legacy password field? + if (data.type === 'v2') { + const result = verifyLegacyPassword(input, data.data) + if (result) { + // Correct password for legacy password. Re-hash and return. + return [true, hashPassword(input)] + } + } else if (data.type === 'v3') { + if (data.hash && data.salt) { + const verify = hashPassword(input, data.salt) + if (data.hash === verify.hash && data.salt === verify.salt) { + // Son of a bitch, you're in + return [true, false] + } + } + } + + return [false, false] +} + +/* + * Verifies a legacy password hash + * + * Legacy means that an account was imported from the v2 FreeSewing backend + * which used MongoDB as a database with Mongoose as an ORM. + * Passwords were handles with the mongoose-bcrypt plugin and have been + * imported from a database dump. + * + * So to verify these passwords, we need to verify the original logic of + * the mongoose plugin which uses the bcryptjs library. + * + * Each time a user with a legacy password field logs in with the correct + * password, we re-hash the password field with the new (crypto) hasing method. + * This way, in a while all users will be migrated, and we can drop this method + * and the cryptojs dependency + */ +function verifyLegacyPassword(password, hash) { + return bcrypt.compareSync(password, hash) +} diff --git a/sites/backend/src/utils/log.mjs b/sites/backend/src/utils/log.mjs new file mode 100644 index 00000000000..165c1c36bfd --- /dev/null +++ b/sites/backend/src/utils/log.mjs @@ -0,0 +1,3 @@ +import logger from 'pino' + +export const log = logger() diff --git a/sites/backend/src/utils/log/index.js b/sites/backend/src/utils/log/index.js deleted file mode 100644 index 73585da55b5..00000000000 --- a/sites/backend/src/utils/log/index.js +++ /dev/null @@ -1,45 +0,0 @@ -import dateFormat from 'dateformat' - -// FIXME: This needs work - -const now = () => dateFormat(new Date(), 'yyyy-mm-dd HH:MM:ss') - -const logWorthy = (msg, data) => { - let d = { at: now() } - switch (msg) { - case 'ping': - case 'login': - case 'wrongPassword': - case 'passwordSet': - case 'dataExport': - d.user = data.user.handle - d.from = data.req.ip - d.with = data.req.headers['user-agent'] - break - case 'signupRequest': - d.email = data.email - d.confirmation = data.confirmation - break - case 'accountRemovalFailed': - d.err = data.err - d.user = data.user.handle - d.from = data.req.ip - d.with = data.req.headers['user-agent'] - break - default: - d.data = data - break - } - - return d -} - -const log = (type, msg, data) => { - console.log(type, msg, logWorthy(msg, data)) -} - -log.info = (msg, data) => log('info', msg, data) -log.warning = (msg, data) => log('warning', msg, data) -log.error = (msg, data) => log('error', msg, data) - -export default log diff --git a/yarn.lock b/yarn.lock index a6d40e926fa..98a5aba5b52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6104,6 +6104,13 @@ abbrev@1, abbrev@^1.0.0: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -6903,6 +6910,11 @@ atob@^2.1.2: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +atomic-sleep@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== + attr-accept@^1.1.0: version "1.1.3" resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52" @@ -11367,12 +11379,17 @@ event-source-polyfill@1.0.25: resolved "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.25.tgz#d8bb7f99cb6f8119c2baf086d9f6ee0514b6d9c8" integrity sha512-hQxu6sN1Eq4JjoI7ITdQeGGUN193A2ra83qC0Ltm9I2UJVAten3OFVN6k5RX4YWeCS0BoC8xg/5czOCIHVosQg== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.0.0: +events@^3.0.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== @@ -11694,6 +11711,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-redact@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" + integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== + fast-safe-stringify@^2.0.7: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" @@ -18839,6 +18861,11 @@ omit.js@^2.0.2: resolved "https://registry.yarnpkg.com/omit.js/-/omit.js-2.0.2.tgz#dd9b8436fab947a5f3ff214cb2538631e313ec2f" integrity sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg== +on-exit-leak-free@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -19766,6 +19793,36 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== +pino-abstract-transport@v1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== + dependencies: + readable-stream "^4.0.0" + split2 "^4.0.0" + +pino-std-serializers@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz#4c20928a1bafca122fdc2a7a4a171ca1c5f9c526" + integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ== + +pino@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-8.7.0.tgz#58621608a3d8540ae643cdd9194cdd94130c78d9" + integrity sha512-l9sA5uPxmZzwydhMWUcm1gI0YxNnYl8MfSr2h8cwLvOAzQLBLewzF247h/vqHe3/tt6fgtXeG9wdjjoetdI/vA== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport v1.0.0 + pino-std-serializers "^6.0.0" + process-warning "^2.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^3.1.0" + thread-stream "^2.0.0" + pirates@^4.0.0, pirates@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" @@ -20747,6 +20804,11 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" +process-warning@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.0.0.tgz#341dbeaac985b90a04ebcd844d50097c7737b2ee" + integrity sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -21034,6 +21096,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-format-unescaped@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== + quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -21580,6 +21647,16 @@ readable-stream@1.1.x: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.2.0.tgz#a7ef523d3b39e4962b0db1a1af22777b10eeca46" + integrity sha512-gJrBHsaI3lgBoGMW/jHZsQ/o/TIWiu5ENCJG1BB7fuCKzpFM8GaS2UoBVt9NO+oI+3FcrBNbUkl3ilDe09aY4A== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + readdir-glob@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.2.tgz#b185789b8e6a43491635b6953295c5c5e3fd224c" @@ -21613,6 +21690,11 @@ readdirp@^3.4.0, readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +real-require@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== + recursive-readdir@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -23352,6 +23434,13 @@ socks@^2.7.1: ip "^2.0.0" smart-buffer "^4.2.0" +sonic-boom@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.0.tgz#ce9f2de7557e68be2e52c8df6d9b052e7d348143" + integrity sha512-SbbZ+Kqj/XIunvIAgUZRlqd6CGQYq71tRRbXR92Za8J/R3Yh4Av+TWENiSiEgnlwckYLyP0YZQWVfyNC0dzLaA== + dependencies: + atomic-sleep "^1.0.0" + sort-keys-length@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" @@ -23525,6 +23614,11 @@ split2@^3.0.0, split2@^3.2.2: dependencies: readable-stream "^3.0.0" +split2@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -24365,6 +24459,13 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +thread-stream@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8" + integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== + dependencies: + real-require "^0.2.0" + throttle-debounce@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"