0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-16 21:56:25 -05:00

feat: remove level dependency by lowdb for npm token cli as storage (#2043)

* feat: remove level for token by lowdb

* chore: fix format

* chore: fix config

* chore: add changeset
This commit is contained in:
Juan Picado 2021-01-02 08:11:32 +01:00
parent c3565f7157
commit e54ec4b5d0
15 changed files with 270 additions and 215 deletions

View file

@ -0,0 +1,20 @@
---
'@verdaccio/api': minor
'verdaccio-htpasswd': minor
'@verdaccio/local-storage': minor
---
feat: remove level dependency by lowdb for npm token cli as storage
### new npm token database
There will be a new database located in your storage named `.token-db.json` which
will store all references to created tokens, **it does not store tokens**, just
mask of them and related metadata required to reference them.
#### Breaking change
If you were relying on `npm token` experiment. This PR will replace the
used database (level) by a json plain based one (lowbd) which does not
require Node.js C++ compilation step and has less dependencies. Since was
a experiment there is no migration step.

View file

@ -50,6 +50,7 @@
"@types/jest": "^26.0.19",
"@types/lodash": "4.14.165",
"@types/mime": "2.0.2",
"@types/lowdb": "^1.0.9",
"@types/minimatch": "3.0.3",
"@types/node": "^14.14.7",
"@types/semver": "7.2.0",

View file

@ -65,11 +65,11 @@ export default function (
ping(app);
stars(app, storage);
if (_.get(config, 'experiments.search') === true) {
if (config?.flags?.search === true) {
v1Search(app, auth, storage);
}
if (_.get(config, 'experiments.token') === true) {
if (config?.flags?.token === true) {
token(app, auth, storage, config);
}
return app;

View file

@ -106,6 +106,8 @@ export const API_ERROR = {
VERSION_NOT_EXIST: "this version doesn't exist",
UNSUPORTED_REGISTRY_CALL: 'unsupported registry call',
FILE_NOT_FOUND: 'File not found',
REGISTRATION_DISABLED: 'user registration disabled',
UNAUTHORIZED_ACCESS: 'unauthorized access',
BAD_STATUS_CODE: 'bad status code',
PACKAGE_EXIST: 'this package is already present',
BAD_AUTH_HEADER: 'bad authorization header',

View file

@ -28,6 +28,7 @@
"node": ">=10"
},
"dependencies": {
"@verdaccio/commons-api": "workspace:10.0.0-alpha.1",
"@verdaccio/file-locking": "workspace:10.0.0-alpha.1",
"apache-md5": "1.1.2",
"bcryptjs": "2.4.3",

View file

@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
import createError, { HttpError } from 'http-errors';
import { readFile } from '@verdaccio/file-locking';
import { Callback } from '@verdaccio/types';
import { API_ERROR, HTTP_STATUS } from '@verdaccio/commons-api';
import crypt3 from './crypt3';
@ -70,7 +71,7 @@ export function addUserToHTPasswd(body: string, user: string, passwd: string): s
if (user !== encodeURIComponent(user)) {
const err = createError('username should not contain non-uri-safe characters');
err.status = 409;
err.status = HTTP_STATUS.CONFLICT;
throw err;
}
@ -106,32 +107,32 @@ export function sanityCheck(
// check for user or password
if (!user || !password) {
err = Error('username and password is required');
err.status = 400;
err = Error(API_ERROR.USERNAME_PASSWORD_REQUIRED);
err.status = HTTP_STATUS.BAD_REQUEST;
return err;
}
const hash = users[user];
if (maxUsers < 0) {
err = Error('user registration disabled');
err.status = 409;
err = Error(API_ERROR.REGISTRATION_DISABLED);
err.status = HTTP_STATUS.CONFLICT;
return err;
}
if (hash) {
const auth = verifyFn(password, users[user]);
if (auth) {
err = Error('username is already registered');
err.status = 409;
err = Error(API_ERROR.USERNAME_ALREADY_REGISTERED);
err.status = HTTP_STATUS.CONFLICT;
return err;
}
err = Error('unauthorized access');
err.status = 401;
err = Error(API_ERROR.UNAUTHORIZED_ACCESS);
err.status = HTTP_STATUS.UNAUTHORIZED;
return err;
} else if (Object.keys(users).length >= maxUsers) {
err = Error('maximum amount of users reached');
err.status = 403;
err = Error(API_ERROR.MAX_USERS_REACHED);
err.status = HTTP_STATUS.FORBIDDEN;
return err;
}

View file

@ -9,6 +9,9 @@
"references": [
{
"path": "../file-locking"
},
{
"path": "../commons-api"
}
]
}

View file

@ -36,9 +36,9 @@
"@verdaccio/streams": "workspace:10.0.0-alpha.1",
"async": "^3.2.0",
"debug": "^4.1.1",
"level": "5.0.1",
"lodash": "^4.17.20",
"mkdirp": "^0.5.5"
"mkdirp": "^0.5.5",
"lowdb": "1.0.0"
},
"devDependencies": {
"@types/minimatch": "^3.0.3",

View file

@ -1,6 +1,5 @@
import fs from 'fs';
import Path from 'path';
import stream from 'stream';
import buildDebug from 'debug';
import _ from 'lodash';
@ -14,47 +13,27 @@ import {
LocalStorage,
Logger,
StorageList,
Token,
TokenFilter,
} from '@verdaccio/types';
import level from 'level';
import { getInternalError } from '@verdaccio/commons-api';
import LocalDriver, { noSuchFile } from './local-fs';
import { loadPrivatePackages } from './pkg-utils';
import TokenActions from './token';
const DEPRECATED_DB_NAME = '.sinopia-db.json';
const DB_NAME = '.verdaccio-db.json';
const TOKEN_DB_NAME = '.token-db';
interface Level {
put(key: string, token, fn?: Function): void;
get(key: string, fn?: Function): void;
del(key: string, fn?: Function): void;
createReadStream(options?: object): stream.Readable;
}
const debug = buildDebug('verdaccio:plugin:local-storage');
/**
* Handle local database.
*/
class LocalDatabase implements IPluginStorage<{}> {
class LocalDatabase extends TokenActions implements IPluginStorage<{}> {
public path: string;
public logger: Logger;
public data: LocalStorage;
public config: Config;
public locked: boolean;
public tokenDb;
/**
* Load an parse the local json database.
* @param {*} path the database path
*/
public constructor(config: Config, logger: Logger) {
super(config);
this.config = config;
this.path = this._buildStoragePath(config);
this.logger = logger;
@ -75,11 +54,6 @@ class LocalDatabase implements IPluginStorage<{}> {
});
}
/**
* Add a new element.
* @param {*} name
* @return {Error|*}
*/
public add(name: string, cb: Callback): void {
if (this.data.list.indexOf(name) === -1) {
this.data.list.push(name);
@ -169,7 +143,7 @@ class LocalDatabase implements IPluginStorage<{}> {
{
name: file,
path: packagePath,
time: self._getTime(stats.mtime.getTime(), stats.mtime),
time: self.getTime(stats.mtime.getTime(), stats.mtime),
},
cb
);
@ -187,11 +161,6 @@ class LocalDatabase implements IPluginStorage<{}> {
);
}
/**
* Remove an element from the database.
* @param {*} name
* @return {Error|*}
*/
public remove(name: string, cb: Callback): void {
this.get((err, data) => {
if (err) {
@ -254,56 +223,7 @@ class LocalDatabase implements IPluginStorage<{}> {
this._sync();
}
public saveToken(token: Token): Promise<void> {
const key = this._getTokenKey(token);
const db = this.getTokenDb();
return new Promise((resolve, reject): void => {
db.put(key, token, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
public deleteToken(user: string, tokenKey: string): Promise<void> {
const key = this._compoundTokenKey(user, tokenKey);
const db = this.getTokenDb();
return new Promise((resolve, reject): void => {
db.del(key, (err) => {
if (err) {
reject(err);
return;
}
resolve();
});
});
}
public readTokens(filter: TokenFilter): Promise<Token[]> {
return new Promise((resolve, reject): void => {
const tokens: Token[] = [];
const key = filter.user + ':';
const db = this.getTokenDb();
const stream = db.createReadStream({
gte: key,
lte: String.fromCharCode(key.charCodeAt(0) + 1),
});
stream.on('data', (data) => {
tokens.push(data.value);
});
stream.once('end', () => resolve(tokens));
stream.once('error', (err) => reject(err));
});
}
private _getTime(time: number, mtime: Date): number | Date {
private getTime(time: number, mtime: Date): number | Date {
return time ? time : mtime;
}
@ -410,12 +330,6 @@ class LocalDatabase implements IPluginStorage<{}> {
}
}
private _dbGenPath(dbName: string, config: Config): string {
return Path.join(
Path.resolve(Path.dirname(config.config_path || ''), config.storage as string, dbName)
);
}
/**
* Fetch local packages.
* @private
@ -426,9 +340,7 @@ class LocalDatabase implements IPluginStorage<{}> {
const emptyDatabase = { list, secret: '' };
try {
const db = loadPrivatePackages(this.path, this.logger);
return db;
return loadPrivatePackages(this.path, this.logger);
} catch (err) {
// readFileSync is platform specific, macOS, Linux and Windows thrown an error
// Only recreate if file not found to prevent data loss
@ -443,25 +355,6 @@ class LocalDatabase implements IPluginStorage<{}> {
return emptyDatabase;
}
}
private getTokenDb(): Level {
if (!this.tokenDb) {
this.tokenDb = level(this._dbGenPath(TOKEN_DB_NAME, this.config), {
valueEncoding: 'json',
});
}
return this.tokenDb;
}
private _getTokenKey(token: Token): string {
const { user, key } = token;
return this._compoundTokenKey(user, key);
}
private _compoundTokenKey(user: string, key: string): string {
return `${user}:${key}`;
}
}
export default LocalDatabase;

View file

@ -0,0 +1,87 @@
import Path from 'path';
import _ from 'lodash';
import low from 'lowdb';
import FileAsync from 'lowdb/adapters/FileAsync';
import FileMemory from 'lowdb/adapters/Memory';
import buildDebug from 'debug';
import { ITokenActions, Config, Token, TokenFilter } from '@verdaccio/types';
const debug = buildDebug('verdaccio:plugin:local-storage:token');
const TOKEN_DB_NAME = '.token-db.json';
export default class TokenActions implements ITokenActions {
public config: Config;
public tokenDb: low.LowdbAsync<any> | null;
public constructor(config: Config) {
this.config = config;
this.tokenDb = null;
}
public _dbGenPath(dbName: string, config: Config): string {
return Path.join(
Path.resolve(Path.dirname(config.config_path || ''), config.storage as string, dbName)
);
}
private async getTokenDb(): Promise<low.LowdbAsync<any>> {
if (!this.tokenDb) {
debug('token database is not defined');
let adapter;
if (process.env.NODE_ENV === 'test') {
debug('token memory adapter');
adapter = new FileMemory('');
} else {
debug('token async adapter');
const pathDb = this._dbGenPath(TOKEN_DB_NAME, this.config);
adapter = new FileAsync(pathDb);
}
debug('token bd generated');
this.tokenDb = await low(adapter);
}
return this.tokenDb;
}
public async saveToken(token: Token): Promise<void> {
debug('token key %o', token.key);
const db = await this.getTokenDb();
const userData = await db.get(token.user).value();
debug('user data %o', userData);
if (_.isNil(userData)) {
await db.set(token.user, [token]).write();
debug('token user %o new database', token.user);
} else {
// types does not match with valid implementation
// @ts-ignore
await db.get(token.user).push(token).write();
}
debug('data %o', await db.getState());
debug('token saved %o', token.user);
}
public async deleteToken(user: string, tokenKey: string): Promise<void> {
const db = await this.getTokenDb();
const userTokens = await db.get(user).value();
if (_.isNil(userTokens)) {
throw new Error('user not found');
}
debug('tokens %o - %o', userTokens, userTokens.length);
const remainingTokens = userTokens.filter(({ key }) => {
debug('key %o', key);
return key !== tokenKey;
});
await db.set(user, remainingTokens).write();
debug('removed tokens key %o', tokenKey);
}
public async readTokens(filter: TokenFilter): Promise<Token[]> {
const { user } = filter;
debug('read tokens with %o', user);
const db = await this.getTokenDb();
const tokens = await db.get(user).value();
return tokens || [];
}
}

View file

@ -218,86 +218,4 @@ describe('Local Database', () => {
spyInstance.mockRestore();
});
});
describe('token', () => {
let token: Token;
beforeEach(() => {
(locaDatabase as LocalDatabase).tokenDb = {
put: jest.fn().mockImplementation((key, value, cb) => cb()),
del: jest.fn().mockImplementation((key, cb) => cb()),
createReadStream: jest.fn(),
};
token = {
user: 'someUser',
viewToken: 'viewToken',
key: 'someHash',
readonly: true,
createdTimestamp: new Date().getTime(),
};
});
test('should save token', async (done) => {
const db = (locaDatabase as LocalDatabase).tokenDb;
await locaDatabase.saveToken(token);
expect(db.put).toHaveBeenCalledWith('someUser:someHash', token, expect.anything());
done();
});
test('should delete token', async (done) => {
const db = (locaDatabase as LocalDatabase).tokenDb;
await locaDatabase.deleteToken('someUser', 'someHash');
expect(db.del).toHaveBeenCalledWith('someUser:someHash', expect.anything());
done();
});
test('should get tokens', async () => {
const db = (locaDatabase as LocalDatabase).tokenDb;
const events = { on: {}, once: {} };
const stream = {
on: (event, cb): void => {
events.on[event] = cb;
},
once: (event, cb): void => {
events.once[event] = cb;
},
};
db.createReadStream.mockImplementation(() => stream);
setTimeout(() => events.on['data']({ value: token }));
setTimeout(() => events.once['end']());
const tokens = await locaDatabase.readTokens({ user: 'someUser' });
expect(db.createReadStream).toHaveBeenCalledWith({
gte: 'someUser:',
lte: 't',
});
expect(tokens).toHaveLength(1);
expect(tokens[0]).toBe(token);
});
test('should fail getting tokens if something goes wrong', async () => {
const db = (locaDatabase as LocalDatabase).tokenDb;
const events = { on: {}, once: {} };
const stream = {
on: (event, cb): void => {
events.on[event] = cb;
},
once: (event, cb): void => {
events.once[event] = cb;
},
};
db.createReadStream.mockImplementation(() => stream);
setTimeout(() => events.once['error'](new Error('Unexpected error!')));
await expect(locaDatabase.readTokens({ user: 'someUser' })).rejects.toThrow(
'Unexpected error!'
);
});
});
});

View file

@ -0,0 +1,81 @@
/* eslint-disable jest/no-mocks-import */
import fs from 'fs';
import path from 'path';
import { assign } from 'lodash';
import { ILocalData, PluginOptions, Token } from '@verdaccio/types';
import LocalDatabase from '../src/local-database';
import { ILocalFSPackageManager } from '../src/local-fs';
import * as pkgUtils from '../src/pkg-utils';
// FIXME: remove this mocks imports
import Config from './__mocks__/Config';
import logger from './__mocks__/Logger';
const optionsPlugin: PluginOptions<{}> = {
logger,
config: new Config(),
};
let locaDatabase: ILocalData<{}>;
let loadPrivatePackages;
describe('Local Database', () => {
beforeEach(() => {
const writeMock = jest.spyOn(fs, 'writeFileSync').mockImplementation();
loadPrivatePackages = jest
.spyOn(pkgUtils, 'loadPrivatePackages')
.mockReturnValue({ list: [], secret: '' });
locaDatabase = new LocalDatabase(optionsPlugin.config, optionsPlugin.logger);
(locaDatabase as LocalDatabase).clean();
writeMock.mockClear();
});
afterEach(() => {
jest.clearAllMocks();
});
describe('token', () => {
let token: Token = {
user: 'someUser',
viewToken: 'viewToken',
key: 'someHash',
readonly: true,
createdTimestamp: new Date().getTime(),
};
test('should save and get token', async () => {
await locaDatabase.saveToken(token);
const tokens = await locaDatabase.readTokens({ user: token.user });
expect(tokens).toHaveLength(1);
expect(tokens[0]).toEqual(token);
});
test('should revoke and get token', async () => {
await locaDatabase.saveToken(token);
const tokens = await locaDatabase.readTokens({ user: token.user });
expect(tokens).toHaveLength(1);
expect(tokens[0]).toEqual(token);
await locaDatabase.deleteToken(token.user, token.key);
const tokens2 = await locaDatabase.readTokens({ user: token.user });
expect(tokens2).toHaveLength(0);
});
test('should fail on revoke', async () => {
await expect(locaDatabase.deleteToken({ user: 'foo', key: 'bar' })).rejects.toThrow(
'user not found'
);
});
test('should verify save more than one token', async () => {
await locaDatabase.saveToken(token);
const tokens = await locaDatabase.readTokens({ user: token.user });
expect(tokens).toHaveLength(1);
expect(tokens[0]).toEqual(token);
await locaDatabase.saveToken({ ...token, key: 'foo' });
expect(tokens).toHaveLength(2);
expect(tokens[1].key).toEqual('foo');
});
});
});

View file

@ -381,7 +381,7 @@ declare module '@verdaccio/types' {
https: HttpsConf;
}
interface ITokenActions {
export interface ITokenActions {
saveToken(token: Token): Promise<any>;
deleteToken(user: string, tokenKey: string): Promise<any>;
readTokens(filter: TokenFilter): Promise<Token[]>;

View file

@ -22,6 +22,6 @@ packages:
publish: $authenticated
logs:
- { type: stdout, format: pretty, level: error }
experiments:
flags:
## enable token for testing
token: true

View file

@ -38,6 +38,7 @@ importers:
'@types/jest': 26.0.19
'@types/js-base64': 3.0.0
'@types/lodash': 4.14.165
'@types/lowdb': 1.0.9
'@types/mime': 2.0.2
'@types/minimatch': 3.0.3
'@types/node': 14.14.7
@ -143,6 +144,7 @@ importers:
'@types/jest': ^26.0.19
'@types/js-base64': 3.0.0
'@types/lodash': 4.14.165
'@types/lowdb': ^1.0.9
'@types/mime': 2.0.2
'@types/minimatch': 3.0.3
'@types/node': ^14.14.7
@ -336,6 +338,7 @@ importers:
lockfile: 1.0.4
packages/core/htpasswd:
dependencies:
'@verdaccio/commons-api': 'link:../commons-api'
'@verdaccio/file-locking': 'link:../file-locking'
apache-md5: 1.1.2
bcryptjs: 2.4.3
@ -346,6 +349,7 @@ importers:
'@verdaccio/types': 'link:../types'
specifiers:
'@types/bcryptjs': ^2.4.2
'@verdaccio/commons-api': 'workspace:10.0.0-alpha.1'
'@verdaccio/file-locking': 'workspace:10.0.0-alpha.1'
'@verdaccio/types': 'workspace:10.0.0-alpha.1'
apache-md5: 1.1.2
@ -359,8 +363,8 @@ importers:
'@verdaccio/streams': 'link:../streams'
async: 3.2.0
debug: 4.1.1
level: 5.0.1
lodash: 4.17.20
lowdb: 1.0.0
mkdirp: 0.5.5
devDependencies:
'@types/minimatch': 3.0.3
@ -375,8 +379,8 @@ importers:
'@verdaccio/types': 'workspace:10.0.0-alpha.1'
async: ^3.2.0
debug: ^4.1.1
level: 5.0.1
lodash: ^4.17.20
lowdb: 1.0.0
minimatch: ^3.0.4
mkdirp: ^0.5.5
rmdir-sync: ^1.0.1
@ -6596,6 +6600,12 @@ packages:
dev: false
resolution:
integrity: sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
/@types/lowdb/1.0.9:
dependencies:
'@types/lodash': 4.14.165
dev: true
resolution:
integrity: sha512-LBRG5EPXFOJDoJc9jACstMhtMP+u+UkPYllBeGQXXKiaHc+uzJs9+/Aynb/5KkX33DtrIiKyzNVTPQc/4RcD6A==
/@types/mdast/3.0.3:
dependencies:
'@types/unist': 2.0.3
@ -7531,6 +7541,7 @@ packages:
dependencies:
level-concat-iterator: 2.0.1
xtend: 4.0.2
dev: true
engines:
node: '>=6'
resolution:
@ -7542,6 +7553,7 @@ packages:
level-concat-iterator: 2.0.1
level-supports: 1.0.1
xtend: 4.0.2
dev: true
engines:
node: '>=6'
resolution:
@ -7553,6 +7565,7 @@ packages:
level-concat-iterator: 2.0.1
level-supports: 1.0.1
xtend: 4.0.2
dev: true
engines:
node: '>=6'
resolution:
@ -11097,6 +11110,7 @@ packages:
dependencies:
abstract-leveldown: 6.2.3
inherits: 2.0.4
dev: true
engines:
node: '>=6'
resolution:
@ -11719,6 +11733,7 @@ packages:
inherits: 2.0.4
level-codec: 9.0.2
level-errors: 2.0.1
dev: true
engines:
node: '>=6'
resolution:
@ -15721,9 +15736,11 @@ packages:
resolution:
integrity: sha512-33AmZ+xjZhg2JMCe+vDf6a9mzWukE7l+wAtesjE7KyteqqKjzxv7aVQeWnul1Ve26mWvEQqyPwl0OctNBfSR9w==
/immediate/3.2.3:
dev: true
resolution:
integrity: sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=
/immediate/3.3.0:
dev: true
resolution:
integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==
/import-cwd/2.1.0:
@ -16424,7 +16441,6 @@ packages:
resolution:
integrity: sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
/is-promise/2.2.2:
dev: true
resolution:
integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
/is-regex/1.1.1:
@ -17651,11 +17667,13 @@ packages:
/level-codec/9.0.2:
dependencies:
buffer: 5.6.0
dev: true
engines:
node: '>=6'
resolution:
integrity: sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==
/level-concat-iterator/2.0.1:
dev: true
engines:
node: '>=6'
resolution:
@ -17663,6 +17681,7 @@ packages:
/level-errors/2.0.1:
dependencies:
errno: 0.1.7
dev: true
engines:
node: '>=6'
resolution:
@ -17672,6 +17691,7 @@ packages:
inherits: 2.0.4
readable-stream: 3.6.0
xtend: 4.0.2
dev: true
engines:
node: '>=6'
resolution:
@ -17683,12 +17703,14 @@ packages:
inherits: 2.0.4
ltgt: 2.2.1
typedarray-to-buffer: 3.1.5
dev: true
resolution:
integrity: sha512-PeGjZsyMG4O89KHiez1zoMJxStnkM+oBIqgACjoo5PJqFiSUUm3GNod/KcbqN5ktyZa8jkG7I1T0P2u6HN9lIg==
/level-packager/5.1.1:
dependencies:
encoding-down: 6.3.0
levelup: 4.4.0
dev: true
engines:
node: '>=6'
resolution:
@ -17696,6 +17718,7 @@ packages:
/level-supports/1.0.1:
dependencies:
xtend: 4.0.2
dev: true
engines:
node: '>=6'
resolution:
@ -17706,6 +17729,7 @@ packages:
level-packager: 5.1.1
leveldown: 5.6.0
opencollective-postinstall: 2.0.3
dev: true
engines:
node: '>=8.6.0'
requiresBuild: true
@ -17716,6 +17740,7 @@ packages:
abstract-leveldown: 6.2.3
napi-macros: 2.0.0
node-gyp-build: 4.1.1
dev: true
engines:
node: '>=8.6.0'
requiresBuild: true
@ -17728,6 +17753,7 @@ packages:
level-iterator-stream: 4.0.2
level-supports: 1.0.1
xtend: 4.0.2
dev: true
engines:
node: '>=6'
resolution:
@ -18290,6 +18316,18 @@ packages:
node: '>=8'
resolution:
integrity: sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==
/lowdb/1.0.0:
dependencies:
graceful-fs: 4.2.4
is-promise: 2.2.2
lodash: 4.17.20
pify: 3.0.0
steno: 0.4.4
dev: false
engines:
node: '>=4'
resolution:
integrity: sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==
/lower-case/2.0.1:
dependencies:
tslib: 1.13.0
@ -18356,6 +18394,7 @@ packages:
resolution:
integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
/ltgt/2.2.1:
dev: true
resolution:
integrity: sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=
/lunr-mutable-indexes/2.3.2:
@ -19146,6 +19185,7 @@ packages:
resolution:
integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
/napi-macros/2.0.0:
dev: true
resolution:
integrity: sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
/native-url/0.2.6:
@ -19280,6 +19320,7 @@ packages:
resolution:
integrity: sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==
/node-gyp-build/4.1.1:
dev: true
hasBin: true
resolution:
integrity: sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==
@ -19829,6 +19870,7 @@ packages:
resolution:
integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==
/opencollective-postinstall/2.0.3:
dev: true
hasBin: true
resolution:
integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
@ -23917,6 +23959,12 @@ packages:
node: '>=0.10.0'
resolution:
integrity: sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
/steno/0.4.4:
dependencies:
graceful-fs: 4.2.4
dev: false
resolution:
integrity: sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=
/stream-browserify/2.0.2:
dependencies:
inherits: 2.0.4