feat: Logins log for Fail2Ban (#986)

* login logs and output

* fix(merge)

* fix(wiston) unistall

* fix(winston) installation in api

* fix(logger) new module
This commit is contained in:
Marco Beretta 2023-09-24 18:18:10 +02:00 committed by GitHub
parent 7c0379ba51
commit 1bf6c259b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 198 additions and 27 deletions

View file

@ -61,6 +61,7 @@
"sharp": "^0.32.5", "sharp": "^0.32.5",
"tiktoken": "^1.0.10", "tiktoken": "^1.0.10",
"ua-parser-js": "^1.0.36", "ua-parser-js": "^1.0.36",
"winston": "^3.10.0",
"zod": "^3.22.2" "zod": "^3.22.2"
}, },
"devDependencies": { "devDependencies": {

View file

@ -1,7 +1,7 @@
const { Strategy: PassportLocalStrategy } = require('passport-local'); const { Strategy: PassportLocalStrategy } = require('passport-local');
const User = require('../models/User'); const User = require('../models/User');
const { loginSchema, errorsToString } = require('./validators'); const { loginSchema, errorsToString } = require('./validators');
const DebugControl = require('../utils/debug.js'); const logger = require('../utils/logger');
async function validateLoginRequest(req) { async function validateLoginRequest(req) {
const { error } = loginSchema.safeParse(req.body); const { error } = loginSchema.safeParse(req.body);
@ -28,21 +28,25 @@ async function passportLogin(req, email, password, done) {
const validationError = await validateLoginRequest(req); const validationError = await validateLoginRequest(req);
if (validationError) { if (validationError) {
logError('Passport Local Strategy - Validation Error', { reqBody: req.body }); logError('Passport Local Strategy - Validation Error', { reqBody: req.body });
logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`);
return done(null, false, { message: validationError }); return done(null, false, { message: validationError });
} }
const user = await findUserByEmail(email); const user = await findUserByEmail(email);
if (!user) { if (!user) {
logError('Passport Local Strategy - User Not Found', { email }); logError('Passport Local Strategy - User Not Found', { email });
logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`);
return done(null, false, { message: 'Email does not exist.' }); return done(null, false, { message: 'Email does not exist.' });
} }
const isMatch = await comparePassword(user, password); const isMatch = await comparePassword(user, password);
if (!isMatch) { if (!isMatch) {
logError('Passport Local Strategy - Password does not match', { isMatch }); logError('Passport Local Strategy - Password does not match', { isMatch });
logger.error(`[Login] [Login failed] [Username: ${email}] [Request-IP: ${req.ip}]`);
return done(null, false, { message: 'Incorrect password.' }); return done(null, false, { message: 'Incorrect password.' });
} }
logger.info(`[Login] [Login successful] [Username: ${email}] [Request-IP: ${req.ip}]`);
return done(null, user); return done(null, user);
} catch (err) { } catch (err) {
return done(err); return done(err);
@ -51,10 +55,7 @@ async function passportLogin(req, email, password, done) {
function logError(title, parameters) { function logError(title, parameters) {
const entries = Object.entries(parameters).map(([name, value]) => ({ name, value })); const entries = Object.entries(parameters).map(([name, value]) => ({ name, value }));
DebugControl.log.functionName(title); logger.error(title, { parameters: entries });
if (entries) {
DebugControl.log.parameters(entries);
}
} }
module.exports = () => module.exports = () =>

View file

@ -1,23 +1,4 @@
const pino = require('pino'); const logger = require('./logger');
const logger = pino({
level: 'info',
redact: {
paths: [
// List of Paths to redact from the logs (https://getpino.io/#/docs/redaction)
'env.OPENAI_API_KEY',
'env.BINGAI_TOKEN',
'env.CHATGPT_TOKEN',
'env.MEILI_MASTER_KEY',
'env.GOOGLE_CLIENT_SECRET',
'env.JWT_SECRET',
'env.JWT_SECRET_DEV',
'env.JWT_SECRET_PROD',
'newUser.password',
], // See example to filter object class instances
censor: '***', // Redaction character
},
});
// Sanitize outside the logger paths. This is useful for sanitizing variables directly with Regex and patterns. // Sanitize outside the logger paths. This is useful for sanitizing variables directly with Regex and patterns.
const redactPatterns = [ const redactPatterns = [

12
api/utils/logger.js Normal file
View file

@ -0,0 +1,12 @@
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'login-logs.log' }),
],
});
module.exports = logger;

180
package-lock.json generated
View file

@ -82,6 +82,7 @@
"sharp": "^0.32.5", "sharp": "^0.32.5",
"tiktoken": "^1.0.10", "tiktoken": "^1.0.10",
"ua-parser-js": "^1.0.36", "ua-parser-js": "^1.0.36",
"winston": "^3.10.0",
"zod": "^3.22.2" "zod": "^3.22.2"
}, },
"devDependencies": { "devDependencies": {
@ -3552,6 +3553,14 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true "dev": true
}, },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@csstools/cascade-layer-name-parser": { "node_modules/@csstools/cascade-layer-name-parser": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.4.tgz",
@ -4276,6 +4285,16 @@
"postcss-selector-parser": "^6.0.10" "postcss-selector-parser": "^6.0.10"
} }
}, },
"node_modules/@dabh/diagnostics": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz",
"integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==",
"dependencies": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@dqbd/tiktoken": { "node_modules/@dqbd/tiktoken": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@dqbd/tiktoken/-/tiktoken-1.0.7.tgz",
@ -7569,6 +7588,11 @@
"integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==",
"dev": true "dev": true
}, },
"node_modules/@types/triple-beam": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.3.tgz",
"integrity": "sha512-6tOUG+nVHn0cJbVp25JFayS5UE6+xlbcNF9Lo9mU7U0zk3zeUShZied4YEQZjy1JBF043FSkdXw8YkUJuVtB5g=="
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.8.tgz",
@ -8548,8 +8572,7 @@
"node_modules/async": { "node_modules/async": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
"dev": true
}, },
"node_modules/asynciterator.prototype": { "node_modules/asynciterator.prototype": {
"version": "1.0.0", "version": "1.0.0",
@ -9715,6 +9738,37 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true "dev": true
}, },
"node_modules/colorspace": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz",
"integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==",
"dependencies": {
"color": "^3.1.3",
"text-hex": "1.0.x"
}
},
"node_modules/colorspace/node_modules/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dependencies": {
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/colorspace/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/colorspace/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -10628,6 +10682,11 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
}, },
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -11816,6 +11875,11 @@
"bser": "2.1.1" "bser": "2.1.1"
} }
}, },
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"node_modules/fetch-undici": { "node_modules/fetch-undici": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/fetch-undici/-/fetch-undici-3.0.1.tgz", "resolved": "https://registry.npmjs.org/fetch-undici/-/fetch-undici-3.0.1.tgz",
@ -12063,6 +12127,11 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true "dev": true
}, },
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"node_modules/follow-redirects": { "node_modules/follow-redirects": {
"version": "1.15.2", "version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
@ -15207,6 +15276,11 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"node_modules/langchainhub": { "node_modules/langchainhub": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/langchainhub/-/langchainhub-0.0.6.tgz", "resolved": "https://registry.npmjs.org/langchainhub/-/langchainhub-0.0.6.tgz",
@ -15652,6 +15726,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/logform": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.5.1.tgz",
"integrity": "sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==",
"dependencies": {
"@colors/colors": "1.5.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
}
},
"node_modules/loglevel": { "node_modules/loglevel": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz",
@ -17578,6 +17665,14 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/onetime": { "node_modules/onetime": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@ -21245,6 +21340,14 @@
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"dev": true "dev": true
}, },
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"engines": {
"node": "*"
}
},
"node_modules/stack-utils": { "node_modules/stack-utils": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@ -21922,6 +22025,11 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"node_modules/text-table": { "node_modules/text-table": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -22074,6 +22182,14 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/trough": { "node_modules/trough": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz",
@ -23317,6 +23433,66 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/winston": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.10.0.tgz",
"integrity": "sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==",
"dependencies": {
"@colors/colors": "1.5.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.4.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.5.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz",
"integrity": "sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==",
"dependencies": {
"logform": "^2.3.2",
"readable-stream": "^3.6.0",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 6.4.0"
}
},
"node_modules/winston-transport/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/winston/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/wordwrap": { "node_modules/wordwrap": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",