Compare commits

...

6 Commits
0.3.0 ... 0.4.0

5 changed files with 253 additions and 13 deletions

View File

@ -2,9 +2,20 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [0.4.0](https://git.odit.services/kauft.es/linkylinky/compare/0.3.0...0.4.0)
- Basic jwt implementation :party: [`75473ca`](https://git.odit.services/kauft.es/linkylinky/commit/75473cabe79975296e473002e16d3abafbd2635e)
- Implemented jwtcount basics [`48cc380`](https://git.odit.services/kauft.es/linkylinky/commit/48cc380504206ea08b3a5082f19ad10bdd7cf773)
- Implemented jwt count validation and update on logout [`558b69e`](https://git.odit.services/kauft.es/linkylinky/commit/558b69eeaa78ea015473c674d5f919d64128a930)
- Switched to fastify-auth to support multiple auth providers [`6420ffb`](https://git.odit.services/kauft.es/linkylinky/commit/6420ffb055f08348c54cd08a193aba5fe5ebc13a)
- All authenticated entpoints now accept jwtauth [`2b22063`](https://git.odit.services/kauft.es/linkylinky/commit/2b22063a81193c3d698525a050ef300e542c1f05)
#### [0.3.0](https://git.odit.services/kauft.es/linkylinky/compare/0.2.0...0.3.0)
> 18 August 2021
- Added license [`5bb7212`](https://git.odit.services/kauft.es/linkylinky/commit/5bb7212420ba102e743d62a47074191cdb264d2a)
- 🚀RELEASE 0.3.0 [`a62ee63`](https://git.odit.services/kauft.es/linkylinky/commit/a62ee63c838a64d7fd28ae110cad474e2c531995)
- Added stats api route [`c89f2a2`](https://git.odit.services/kauft.es/linkylinky/commit/c89f2a2939b658ffc688646ccce32fc0e1530583)
- Added author info [`2fa520f`](https://git.odit.services/kauft.es/linkylinky/commit/2fa520fdde1d55b2766b4b03e394e21a4f6cbae5)
- Updated readme [`2226705`](https://git.odit.services/kauft.es/linkylinky/commit/2226705e3f57d7d2f3ef5a79947c3ab44ec62c38)

View File

@ -0,0 +1,10 @@
exports.up = function(knex) {
return knex.schema.table('users', function (table) {
table.integer("jwtcount").defaultTo(0);
});
};
exports.down = function(knex) {
};

View File

@ -1,6 +1,6 @@
{
"name": "@odit/shortener-backend",
"version": "0.3.0",
"version": "0.4.0",
"main": "index.js",
"license": "MIT",
"private": false,
@ -26,8 +26,10 @@
"argon2": "^0.28.2",
"dotenv": "^10.0.0",
"fastify": "^3.20.1",
"fastify-auth": "^1.1.0",
"fastify-basic-auth": "^2.1.0",
"fastify-cors": "^6.0.2",
"fastify-jwt": "^3.0.1",
"knex": "^0.21.21",
"sqlite3": "^5.0.2",
"uniqid": "^5.3.0"

View File

@ -6,8 +6,10 @@ const argon2 = require('argon2');
let config = {
domain: process.env.DOMAIN || "localhost:3000",
https: (process.env.SSL === 'true') || false,
env: process.env.NODE_ENV || 'development',
recognizeProviders: !(process.env.DISABLE_PROVIDERS === 'true'),
registrationEnabled: (process.env.ENABLE_REGISTER === 'true'),
jwt_secret: process.env.JWT_SECRET || "pleaseneverusethisdefaultsecret",
getBaseUrl() {
if (config.https) {
return `https://${config.domain}`;
@ -15,17 +17,54 @@ let config = {
return `http://${config.domain}`;
}
}
const environment = process.env.NODE_ENV || 'development';
const knexConfiguration = require('../knexfile')[environment];
const knexConfiguration = require('../knexfile')[config.env];
const knex = require('knex')(knexConfiguration);
const authenticate = { realm: 'Short' }
fastify.register(require('fastify-auth'))
fastify.register(require('fastify-basic-auth'), { validate, authenticate });
fastify.register(require('fastify-cors'), {
fastify.register(require('fastify-jwt'), {
secret: config.jwt_secret
});
fastify.register(require('fastify-cors'), {
origin: true,
preflight: true,
preflightContinue: true
})
})
fastify.decorate('verifyJWT', function async(request, reply, done) {
let token = request.headers.authorization;
if (!token || token == "" || token == "Bearer") {
done(new Error("No jwt provided"));
}
if (token.startsWith("Bearer")) {
token = token.replace("Bearer ", "");
fastify.log.info("Detected bearer and replaced it")
}
fastify.jwt.verify(token, async (err, decoded) => {
if (err) {
fastify.log.error(err)
done(new Error("JWT Validation failed"))
}
fastify.log.info(`Token verified. User is ${decoded.payload.user}`);
request.user = decoded.payload.user;
const jwtcount = (await knex.select('jwtcount')
.from('users')
.where('username', '=', decoded.payload.user)
.limit(1))[0].jwtcount;
if (decoded.payload.jwtcount < jwtcount || !decoded.payload.jwtcount) {
fastify.log.error("Auth ended at jwtcount")
done(new Error("JWT in no longer valid"))
}
fastify.log.info(`JWT count verified`);
done()
})
})
//Automagic Amazn redirects on /a/
fastify.get('/a/:id', async (req, res) => {
@ -219,7 +258,7 @@ fastify.post('/api/register', async (req, res) => {
//Anything in here has some kind of auth
fastify.after(() => {
//Get url api route
fastify.get('/api/:shortcode/visits', { onRequest: fastify.basicAuth }, async (req, res) => {
fastify.get('/api/:shortcode/visits', { onRequest: fastify.auth([fastify.basicAuth, fastify.verifyJWT]) }, async (req, res) => {
const shortcode = req.params.shortcode;
//This should never happen but better safe than 500
@ -243,7 +282,7 @@ fastify.after(() => {
});
//Get url api route
fastify.delete('/api/:shortcode', { onRequest: fastify.basicAuth }, async (req, res) => {
fastify.delete('/api/:shortcode', { onRequest: fastify.auth([fastify.basicAuth, fastify.verifyJWT]) }, async (req, res) => {
const shortcode = req.params.shortcode;
//This should never happen but better safe than 500
@ -260,13 +299,13 @@ fastify.after(() => {
});
//Get all urls api route
fastify.get('/api', { onRequest: fastify.basicAuth }, async (req, res) => {
fastify.get('/api', { onRequest: fastify.auth([fastify.basicAuth, fastify.verifyJWT]) }, async (req, res) => {
urls = await knex.select('target', 'shortcode')
.from('urls');
for (let url of urls) {
url.url = `${config.getBaseUrl()}/${url.shortcode}`
if(req.query.showVisits){
if (req.query.showVisits) {
url.visits = (await knex.select('timestamp')
.from('visits')
.where('shortcode', '=', url.shortcode)).length;
@ -276,6 +315,38 @@ fastify.after(() => {
return urls;
});
fastify.post('/api/auth/login', { onRequest: fastify.auth([fastify.basicAuth]) }, async (req, reply) => {
const jwtcount = (await knex.select('jwtcount')
.from('users')
.where('username', '=', req.user)
.limit(1))[0].jwtcount;
const payload = {
user: req.user,
jwtcount
};
const token = fastify.jwt.sign({ payload })
reply.send({ token });
});
fastify.post('/api/auth/check', { onRequest: fastify.auth([fastify.basicAuth, fastify.verifyJWT]) }, (req, reply) => {
return "logged in";
});
fastify.post('/api/auth/logout', { onRequest: fastify.auth([fastify.basicAuth, fastify.verifyJWT]) }, async (req, reply) => {
let jwtcount = (await knex.select('jwtcount')
.from('users')
.where('username', '=', req.user)
.limit(1))[0].jwtcount;
jwtcount += 1;
await knex('users')
.where('username', '=', req.user)
.update({
jwtcount
});
return "Done!";
});
});
@ -342,6 +413,7 @@ async function validate(username, password, req, reply) {
if (!(await argon2.verify(user[0].password, password))) {
return new Error('Wrong credentials');
}
req.user = username;
}
// Run the server!

153
yarn.lock
View File

@ -216,6 +216,13 @@
resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812"
integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==
"@types/jsonwebtoken@^8.5.0":
version "8.5.4"
resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz#50ccaf0aa6f5d7b9956e70fe323b76e582991913"
integrity sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==
dependencies:
"@types/node" "*"
"@types/keyv@*":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.2.tgz#5d97bb65526c20b6e0845f6b0d2ade4f28604ee5"
@ -567,6 +574,11 @@ braces@^3.0.1, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
@ -1052,6 +1064,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
emoji-regex@^7.0.1:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@ -1240,6 +1259,21 @@ fast-safe-stringify@^2.0.8:
resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f"
integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag==
fastfall@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94"
integrity sha1-P+4DMxpJ0dObPN96XpzWb0dee5Q=
dependencies:
reusify "^1.0.0"
fastify-auth@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fastify-auth/-/fastify-auth-1.1.0.tgz#75076c9c0664addaff07078907db6432086be1d6"
integrity sha512-8IajmAZB3QJ3wTP0q8Z3TG9DkxrIcAlS85TdPCBEfJi3mMKQd/sCYxtZ0dYv11v5hZaJ9z8XmNzhK3AH6/JpNw==
dependencies:
fastify-plugin "^3.0.0"
reusify "^1.0.4"
fastify-basic-auth@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fastify-basic-auth/-/fastify-basic-auth-2.1.0.tgz#a368e4e900f402a2c26f4cab927484b9f2ac539c"
@ -1262,6 +1296,17 @@ fastify-error@^0.3.0:
resolved "https://registry.yarnpkg.com/fastify-error/-/fastify-error-0.3.1.tgz#8eb993e15e3cf57f0357fc452af9290f1c1278d2"
integrity sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==
fastify-jwt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/fastify-jwt/-/fastify-jwt-3.0.1.tgz#aaec719c7f103eb9fb648fda22690e3cec7911d4"
integrity sha512-p6/7QSa9rdO/m4zCcq1oge63qXkfPsrdfPwVkay9HAGD4rh0UuntB/MeWLy+1ZHNay5foATvFB3A9nG15S7RsA==
dependencies:
"@types/jsonwebtoken" "^8.5.0"
fastify-plugin "^3.0.0"
http-errors "^1.8.0"
jsonwebtoken "^8.5.1"
steed "^1.1.3"
fastify-plugin@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.0.tgz#cf1b8c8098e3b5a7c8c30e6aeb06903370c054ca"
@ -1294,13 +1339,29 @@ fastify@^3.20.1:
semver "^7.3.2"
tiny-lru "^7.0.0"
fastq@^1.6.0, fastq@^1.6.1:
fastparallel@^2.2.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.4.0.tgz#65fbec1a5e5902494be772cf5765cbaaece08688"
integrity sha512-sacwQ7wwKlQXsa7TN24UvMBLZNLmVcPhmxccC9riFqb3N+fSczJL8eWdnZodZ/KijGVgNBBfvF/NeXER08uXnQ==
dependencies:
reusify "^1.0.4"
xtend "^4.0.2"
fastq@^1.3.0, fastq@^1.6.0, fastq@^1.6.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807"
integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw==
dependencies:
reusify "^1.0.4"
fastseries@^1.7.0:
version "1.7.2"
resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b"
integrity sha1-0izhO5Qz3/M4jZHb1ri9qbIaD0s=
dependencies:
reusify "^1.0.0"
xtend "^4.0.0"
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@ -1736,7 +1797,7 @@ http-cache-semantics@^4.0.0:
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http-errors@^1.7.3:
http-errors@^1.7.3, http-errors@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.0.tgz#75d1bbe497e1044f51e4ee9e704a62f28d336507"
integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==
@ -2229,6 +2290,22 @@ json-stringify-safe@~5.0.1:
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@ -2239,6 +2316,23 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
keyv@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@ -2339,6 +2433,41 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@4.17.21, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@ -3317,7 +3446,7 @@ retry@0.12.0:
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
reusify@^1.0.4:
reusify@^1.0.0, reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
@ -3418,7 +3547,7 @@ semver@7.3.5, semver@^7.3.2, semver@^7.3.4:
dependencies:
lru-cache "^6.0.0"
semver@^5.3.0, semver@^5.7.1:
semver@^5.3.0, semver@^5.6.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@ -3607,6 +3736,17 @@ static-extend@^0.1.1:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
steed@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5"
integrity sha1-8VJd1a2xLrIb90dJU3Zo1iW5q8U=
dependencies:
fastfall "^1.5.0"
fastparallel "^2.2.0"
fastq "^1.3.0"
fastseries "^1.7.0"
reusify "^1.0.0"
strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
@ -4107,6 +4247,11 @@ xdg-basedir@^4.0.0:
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
xtend@^4.0.0, xtend@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
yallist@^3.0.0, yallist@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"