🤖 Tests: Authentication

This commit is contained in:
Nicolas Meienberger 2022-05-12 21:02:03 +02:00
parent fa0a7d0764
commit 371c6dde36
10 changed files with 477 additions and 97 deletions

View file

@ -6,9 +6,8 @@ env:
ROOT_FOLDER: /test
jobs:
cache-and-install:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

36
package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "runtipi",
"version": "0.0.1",
"description": "A homeserver for everyone",
"dependencies": {
"eslint": "^8.15.0",
"eslint-config-next": "^12.1.4",
"eslint-import-resolver-node": "^0.3.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-module-utils": "^2.7.3",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-react": "^7.29.1",
"eslint-config-prettier": "^8.5.0",
"eslint-scope": "^7.1.1",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"prettier": "^2.6.2",
"prettier-linter-helpers": "^1.0.0",
"eslint-import-resolver-typescript": "^2.4.0"
},
"devDependencies": {},
"repository": {
"type": "git",
"url": "git+https://github.com/meienberger/runtipi.git"
},
"author": "",
"license": "GNU General Public License v3.0",
"bugs": {
"url": "https://github.com/meienberger/runtipi/issues"
},
"homepage": "https://github.com/meienberger/runtipi#readme"
}

View file

@ -11,6 +11,7 @@
"clean": "rimraf dist",
"lint": "eslint . --ext .ts",
"test": "jest",
"test:watch": "jest --watch",
"build-prod": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --minify --analyze=verbose --external:./node_modules/* --format=esm",
"build:watch": "esbuild --bundle src/server.ts --outdir=dist --allow-overwrite --sourcemap --platform=node --external:./node_modules/* --format=esm --watch",
"start:dev": "NODE_ENV=development nodemon --trace-deprecation --trace-warnings --watch dist dist/server.js",
@ -20,6 +21,7 @@
"author": "",
"license": "ISC",
"dependencies": {
"argon2": "^0.28.5",
"bcrypt": "^5.0.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
@ -29,6 +31,7 @@
"helmet": "^5.0.2",
"internal-ip": "^6.0.0",
"jsonwebtoken": "^8.5.1",
"mock-fs": "^5.1.2",
"node-port-scanner": "^3.0.1",
"p-iteration": "^1.1.8",
"passport": "^0.5.2",
@ -36,8 +39,7 @@
"passport-http-bearer": "^1.0.1",
"public-ip": "^5.0.0",
"systeminformation": "^5.11.9",
"tcp-port-used": "^1.0.2",
"mock-fs": "^5.1.2"
"tcp-port-used": "^1.0.2"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",

View file

@ -0,0 +1,147 @@
import { Request, Response } from 'express';
import fs from 'fs';
import * as argon2 from 'argon2';
import config from '../../../config';
import AuthController from '../auth.controller';
let user: any;
jest.mock('fs');
const next = jest.fn();
const MOCK_USER_REGISTERED = () => ({
[`${config.ROOT_FOLDER}/state/users.json`]: `[${user}]`,
});
const MOCK_NO_USER = {
[`${config.ROOT_FOLDER}/state/users.json`]: '[]',
};
beforeAll(async () => {
const hash = await argon2.hash('password');
user = JSON.stringify({
email: 'username',
password: hash,
});
});
describe('Login', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should put cookie in response after login', async () => {
const json = jest.fn();
const res = { cookie: jest.fn(), status: jest.fn(() => ({ json })), json: jest.fn() } as unknown as Response;
const req = { body: { email: 'username', password: 'password' } } as Request;
await AuthController.login(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('tipi_token', expect.any(String), expect.any(Object));
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ token: expect.any(String) });
expect(next).not.toHaveBeenCalled();
});
it('Should throw if username is not provided in request', async () => {
const res = { cookie: jest.fn(), status: jest.fn(), json: jest.fn() } as unknown as Response;
const req = { body: { password: 'password' } } as Request;
await AuthController.login(req, res, next);
expect(res.cookie).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(Error));
});
it('Should throw if password is not provided in request', async () => {
const res = { cookie: jest.fn(), status: jest.fn(), json: jest.fn() } as unknown as Response;
const req = { body: { email: 'username' } } as Request;
await AuthController.login(req, res, next);
expect(res.cookie).not.toHaveBeenCalled();
expect(next).toHaveBeenCalledWith(expect.any(Error));
});
});
describe('Register', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_NO_USER);
});
it('Should put cookie in response after register', async () => {
const json = jest.fn();
const res = { cookie: jest.fn(), status: jest.fn(() => ({ json })), json: jest.fn() } as unknown as Response;
const req = { body: { email: 'username', password: 'password', name: 'name' } } as Request;
await AuthController.register(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('tipi_token', expect.any(String), expect.any(Object));
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ token: expect.any(String) });
});
});
describe('Me', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return user if present in request', async () => {
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = { user } as Request;
await AuthController.me(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ user });
});
it('Should return null if user is not present in request', async () => {
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = {} as Request;
await AuthController.me(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ user: null });
});
});
describe('isConfigured', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_NO_USER);
});
it('Should return false if no user is registered', async () => {
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = {} as Request;
await AuthController.isConfigured(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ configured: false });
});
it('Should return true if user is registered', async () => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
const json = jest.fn();
const res = { status: jest.fn(() => ({ json })) } as unknown as Response;
const req = { user } as Request;
await AuthController.isConfigured(req, res, next);
expect(res.status).toHaveBeenCalledWith(200);
expect(json).toHaveBeenCalledWith({ configured: true });
});
});

View file

@ -0,0 +1,71 @@
import * as argon2 from 'argon2';
import fs from 'fs';
import config from '../../../config';
import { IUser } from '../../../config/types';
import AuthHelpers from '../auth.helpers';
let user: IUser;
beforeAll(async () => {
const hash = await argon2.hash('password');
user = { email: 'username', password: hash, name: 'name' };
});
jest.mock('fs');
const MOCK_USER_REGISTERED = () => ({
[`${config.ROOT_FOLDER}/state/users.json`]: `[${JSON.stringify(user)}]`,
});
describe('TradeTokenForUser', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return null if token is invalid', () => {
const result = AuthHelpers.tradeTokenForUser('invalid token');
expect(result).toBeNull();
});
it('Should return user if token is valid', async () => {
const token = await AuthHelpers.getJwtToken(user, 'password');
const result = AuthHelpers.tradeTokenForUser(token);
expect(result).toEqual(user);
});
});
describe('GetJwtToken', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return token if user and password are valid', async () => {
const token = await AuthHelpers.getJwtToken(user, 'password');
expect(token).toBeDefined();
});
it('Should throw if password is invalid', async () => {
await expect(AuthHelpers.getJwtToken(user, 'invalid password')).rejects.toThrow('Wrong password');
});
});
describe('getUser', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return null if user is not found', () => {
const result = AuthHelpers.getUser('invalid token');
expect(result).toBeUndefined();
});
it('Should return user if token is valid', async () => {
const result = AuthHelpers.getUser('username');
expect(result).toEqual(user);
});
});

View file

@ -0,0 +1,103 @@
import fs from 'fs';
// import bcrypt from 'bcrypt';
import jsonwebtoken from 'jsonwebtoken';
import * as argon2 from 'argon2';
import config from '../../../config';
import AuthService from '../auth.service';
import { IUser } from '../../../config/types';
jest.mock('fs');
let user: any;
const MOCK_USER_REGISTERED = () => ({
[`${config.ROOT_FOLDER}/state/users.json`]: `[${user}]`,
});
const MOCK_NO_USER = {
[`${config.ROOT_FOLDER}/state/users.json`]: '[]',
};
beforeAll(async () => {
const hash = await argon2.hash('password');
user = JSON.stringify({
email: 'username',
password: hash,
});
});
describe('Login', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_USER_REGISTERED());
});
it('Should return token after login', async () => {
const token = await AuthService.login('username', 'password');
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
expect(token).toBeDefined();
expect(email).toBe('username');
});
it('Should throw if user does not exist', async () => {
await expect(AuthService.login('username1', 'password')).rejects.toThrowError('User not found');
});
it('Should throw if password is incorrect', async () => {
await expect(AuthService.login('username', 'password1')).rejects.toThrowError('Wrong password');
});
});
describe('Register', () => {
beforeEach(() => {
// @ts-ignore
fs.__createMockFiles(MOCK_NO_USER);
});
it('Should return token after register', async () => {
const token = await AuthService.register('username', 'password', 'name');
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
expect(token).toBeDefined();
expect(email).toBe('username');
});
it('Should correctly write user to file', async () => {
await AuthService.register('username', 'password', 'name');
const users: IUser[] = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/users.json`, 'utf8'));
expect(users.length).toBe(1);
expect(users[0].email).toBe('username');
expect(users[0].name).toBe('name');
const valid = await argon2.verify(users[0].password, 'password');
expect(valid).toBeTruthy();
});
it('Should throw if user already exists', async () => {
await AuthService.register('username', 'password', 'name');
await expect(AuthService.register('username', 'password', 'name')).rejects.toThrowError('There is already an admin user');
});
it('Should throw if email is not provided', async () => {
await expect(AuthService.register('', 'password', 'name')).rejects.toThrowError('Missing email or password');
});
it('Should throw if password is not provided', async () => {
await expect(AuthService.register('username', '', 'name')).rejects.toThrowError('Missing email or password');
});
it('Does not throw if name is not provided', async () => {
await AuthService.register('username', 'password', '');
const users: IUser[] = JSON.parse(fs.readFileSync(`${config.ROOT_FOLDER}/state/users.json`, 'utf8'));
expect(users.length).toBe(1);
});
});

View file

@ -1,8 +1,7 @@
import { NextFunction, Request, Response } from 'express';
import bcrypt from 'bcrypt';
import { IUser } from '../../config/types';
import { readJsonFile, writeFile } from '../fs/fs.helpers';
import { getJwtToken, getUser } from './auth.helpers';
import { readJsonFile } from '../fs/fs.helpers';
import AuthService from './auth.service';
const login = async (req: Request, res: Response, next: NextFunction) => {
try {
@ -12,13 +11,7 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
throw new Error('Missing id or password');
}
const user = getUser(email);
if (!user) {
throw new Error('User not found');
}
const token = await getJwtToken(user, password);
const token = await AuthService.login(email, password);
res.cookie('tipi_token', token, {
httpOnly: false,
@ -34,26 +27,9 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
const register = async (req: Request, res: Response, next: NextFunction) => {
try {
const users: IUser[] = readJsonFile('/state/users.json');
if (users.length > 0) {
throw new Error('There is already an admin user');
}
const { email, password, name } = req.body;
if (!email || !password) {
throw new Error('Missing email or password');
}
if (users.find((user) => user.email === email)) {
throw new Error('User already exists');
}
const hash = await bcrypt.hash(password, 10);
const newuser: IUser = { email, name, password: hash };
const token = await getJwtToken(newuser, password);
const token = await AuthService.register(email, password, name);
res.cookie('tipi_token', token, {
httpOnly: false,
@ -61,28 +37,34 @@ const register = async (req: Request, res: Response, next: NextFunction) => {
maxAge: 1000 * 60 * 60 * 24 * 7,
});
writeFile('/state/users.json', JSON.stringify([newuser]));
res.status(200).json({ token });
} catch (e) {
next(e);
}
};
const me = async (req: Request, res: Response) => {
const { user } = req;
const me = async (req: Request, res: Response, next: NextFunction) => {
try {
const { user } = req;
if (user) {
res.status(200).json({ user });
} else {
res.status(200).json({ user: null });
if (user) {
res.status(200).json({ user });
} else {
res.status(200).json({ user: null });
}
} catch (e) {
next(e);
}
};
const isConfigured = async (req: Request, res: Response) => {
const users: IUser[] = readJsonFile('/state/users.json');
const isConfigured = async (req: Request, res: Response, next: NextFunction) => {
try {
const users: IUser[] = readJsonFile('/state/users.json');
res.status(200).json({ configured: users.length > 0 });
res.status(200).json({ configured: users.length > 0 });
} catch (e) {
next(e);
}
};
export default { login, me, register, isConfigured };

View file

@ -1,10 +1,10 @@
import jsonwebtoken from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import * as argon2 from 'argon2';
import { IUser, Maybe } from '../../config/types';
import { readJsonFile } from '../fs/fs.helpers';
import config from '../../config';
export const getUser = (email: string): Maybe<IUser> => {
const getUser = (email: string): Maybe<IUser> => {
const savedUser: IUser[] = readJsonFile('/state/users.json');
const user = savedUser.find((u) => u.email === email);
@ -13,17 +13,19 @@ export const getUser = (email: string): Maybe<IUser> => {
};
const compareHashPassword = (password: string, hash = ''): Promise<boolean> => {
return bcrypt.compare(password, hash || '');
return argon2.verify(hash, password);
};
const getJwtToken = async (user: IUser, password: string) => {
const validPassword = await compareHashPassword(password, user.password || '');
const validPassword = await compareHashPassword(password, user.password);
if (validPassword) {
if (config.JWT_SECRET) {
return jsonwebtoken.sign({ email: user.email }, config.JWT_SECRET, {
expiresIn: '7d',
});
} else {
throw new Error('JWT_SECRET is not set');
}
}
@ -33,6 +35,7 @@ const getJwtToken = async (user: IUser, password: string) => {
const tradeTokenForUser = (token: string): Maybe<IUser> => {
try {
const { email } = jsonwebtoken.verify(token, config.JWT_SECRET) as { email: string };
const users: IUser[] = readJsonFile('/state/users.json');
return users.find((user) => user.email === email);
@ -41,4 +44,4 @@ const tradeTokenForUser = (token: string): Maybe<IUser> => {
}
};
export { tradeTokenForUser, getJwtToken };
export default { tradeTokenForUser, getJwtToken, getUser };

View file

@ -0,0 +1,48 @@
import * as argon2 from 'argon2';
import { IUser } from '../../config/types';
import { readJsonFile, writeFile } from '../fs/fs.helpers';
import AuthHelpers from './auth.helpers';
const login = async (email: string, password: string) => {
const user = AuthHelpers.getUser(email);
if (!user) {
throw new Error('User not found');
}
const token = await AuthHelpers.getJwtToken(user, password);
return token;
};
const register = async (email: string, password: string, name: string) => {
const users: IUser[] = readJsonFile('/state/users.json');
if (users.length > 0) {
throw new Error('There is already an admin user');
}
if (!email || !password) {
throw new Error('Missing email or password');
}
if (users.find((user) => user.email === email)) {
throw new Error('User already exists');
}
const hash = await argon2.hash(password); // bcrypt.hash(password, 10);
const newuser: IUser = { email, name, password: hash };
const token = await AuthHelpers.getJwtToken(newuser, password);
writeFile('/state/users.json', JSON.stringify([newuser]));
return token;
};
const AuthService = {
login,
register,
};
export default AuthService;

View file

@ -93,6 +93,7 @@ importers:
'@types/validator': ^13.7.2
'@typescript-eslint/eslint-plugin': ^5.18.0
'@typescript-eslint/parser': ^5.22.0
argon2: ^0.28.5
bcrypt: ^5.0.1
compression: ^1.7.4
concurrently: ^7.1.0
@ -124,6 +125,7 @@ importers:
ts-jest: ^28.0.2
typescript: 4.6.4
dependencies:
argon2: 0.28.5
bcrypt: 5.0.1
compression: 1.7.4
cookie-parser: 1.4.6
@ -162,7 +164,7 @@ importers:
eslint: 8.15.0
eslint-config-airbnb-typescript: 17.0.0_c2ouaf3l4ivgkc6ae4nebvztom
eslint-config-prettier: 8.5.0_eslint@8.15.0
eslint-plugin-import: 2.26.0_6nacgdzqm4zbhelsxkmd2vkvxy
eslint-plugin-import: 2.26.0_eslint@8.15.0
eslint-plugin-prettier: 4.0.0_iqftbjqlxzn3ny5nablrkczhqi
jest: 28.1.0
nodemon: 2.0.16
@ -1891,6 +1893,11 @@ packages:
fastq: 1.13.0
dev: true
/@phc/format/1.0.0:
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
dev: false
/@popperjs/core/2.11.5:
resolution: {integrity: sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==}
dev: false
@ -2684,6 +2691,19 @@ packages:
resolution: {integrity: sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA==}
dev: true
/argon2/0.28.5:
resolution: {integrity: sha512-kGFCctzc3VWmR1aCOYjNgvoTmVF5uVBUtWlXCKKO54d1K+31zRz45KAcDIqMo2746ozv/52d25nfEekitaXP0w==}
engines: {node: '>=12.0.0'}
requiresBuild: true
dependencies:
'@mapbox/node-pre-gyp': 1.0.9
'@phc/format': 1.0.0
node-addon-api: 4.3.0
transitivePeerDependencies:
- encoding
- supports-color
dev: false
/argparse/1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
dependencies:
@ -2902,8 +2922,6 @@ packages:
raw-body: 2.5.1
type-is: 1.6.18
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/boxen/5.1.2:
@ -3170,8 +3188,6 @@ packages:
on-headers: 1.0.2
safe-buffer: 5.1.2
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/compute-scroll-into-view/1.0.14:
@ -3328,37 +3344,15 @@ packages:
/debug/2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
/debug/3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
dev: true
/debug/3.2.7_supports-color@5.5.0:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
supports-color: 5.5.0
dev: true
/debug/4.3.1:
resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==}
engines: {node: '>=6.0'}
@ -3877,7 +3871,7 @@ packages:
dependencies:
confusing-browser-globals: 1.0.11
eslint: 8.15.0
eslint-plugin-import: 2.26.0_6nacgdzqm4zbhelsxkmd2vkvxy
eslint-plugin-import: 2.26.0_eslint@8.15.0
object.assign: 4.1.2
object.entries: 1.1.5
semver: 6.3.0
@ -3910,7 +3904,7 @@ packages:
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
eslint: 8.15.0
eslint-config-airbnb-base: 15.0.0_gwd37gqv3vjv3xlpl7ju3ag2qu
eslint-plugin-import: 2.26.0_6nacgdzqm4zbhelsxkmd2vkvxy
eslint-plugin-import: 2.26.0_eslint@8.15.0
dev: true
/eslint-config-airbnb-typescript/17.0.0_r46exuh3jlhq2wmrnqx2ufqspa:
@ -3998,6 +3992,14 @@ packages:
- supports-color
dev: true
/eslint-module-utils/2.7.3:
resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==}
engines: {node: '>=4'}
dependencies:
debug: 3.2.7
find-up: 2.1.0
dev: true
/eslint-module-utils/2.7.3_sysdrzuw2ki4kxpuwc4tznw2ha:
resolution: {integrity: sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==}
engines: {node: '>=4'}
@ -4082,24 +4084,19 @@ packages:
- supports-color
dev: true
/eslint-plugin-import/2.26.0_6nacgdzqm4zbhelsxkmd2vkvxy:
/eslint-plugin-import/2.26.0_eslint@8.15.0:
resolution: {integrity: sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
array-includes: 3.1.5
array.prototype.flat: 1.3.0
debug: 2.6.9
doctrine: 2.1.0
eslint: 8.15.0
eslint-import-resolver-node: 0.3.6
eslint-module-utils: 2.7.3_wex3ustmkv4ospy3s77r6ihlwq
eslint-module-utils: 2.7.3
has: 1.0.3
is-core-module: 2.9.0
is-glob: 4.0.3
@ -4107,10 +4104,6 @@ packages:
object.values: 1.1.5
resolve: 1.22.0
tsconfig-paths: 3.14.1
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
dev: true
/eslint-plugin-import/2.26.0_hhyjdrupy4c2vgtpytri6cjwoy:
@ -4462,8 +4455,6 @@ packages:
type-is: 1.6.18
utils-merge: 1.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
dev: false
/fast-deep-equal/3.1.3:
@ -4536,8 +4527,6 @@ packages:
parseurl: 1.3.3
statuses: 2.0.1
unpipe: 1.0.0
transitivePeerDependencies:
- supports-color
dev: false
/find-root/1.1.0:
@ -6162,6 +6151,10 @@ packages:
resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==}
dev: false
/node-addon-api/4.3.0:
resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==}
dev: false
/node-fetch/2.6.7:
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
engines: {node: 4.x || >=6.0.0}
@ -6193,7 +6186,7 @@ packages:
requiresBuild: true
dependencies:
chokidar: 3.5.3
debug: 3.2.7_supports-color@5.5.0
debug: 3.2.7
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
@ -7056,8 +7049,6 @@ packages:
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: false
/serve-static/1.15.0:
@ -7068,8 +7059,6 @@ packages:
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
transitivePeerDependencies:
- supports-color
dev: false
/set-blocking/2.0.0: