From 2a327c4b335a656750472d13a0752fe3bead8863 Mon Sep 17 00:00:00 2001 From: Juan Picado <juanpicado19@gmail.com> Date: Sat, 2 Jan 2021 08:11:32 +0100 Subject: [PATCH] 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 --- .changeset/late-adults-love.md | 20 +++ package.json | 1 + packages/api/src/index.ts | 4 +- packages/core/commons-api/src/index.ts | 2 + packages/core/htpasswd/package.json | 1 + packages/core/htpasswd/src/utils.ts | 23 ++-- packages/core/htpasswd/tsconfig.json | 3 + packages/core/local-storage/package.json | 4 +- .../core/local-storage/src/local-database.ts | 119 +----------------- packages/core/local-storage/src/token.ts | 87 +++++++++++++ .../tests/local-database.test.ts | 82 ------------ .../core/local-storage/tests/token.test.ts | 81 ++++++++++++ packages/core/types/index.d.ts | 2 +- packages/server/test/token/token.spec.yaml | 2 +- pnpm-lock.yaml | 54 +++++++- 15 files changed, 270 insertions(+), 215 deletions(-) create mode 100644 .changeset/late-adults-love.md create mode 100644 packages/core/local-storage/src/token.ts create mode 100644 packages/core/local-storage/tests/token.test.ts diff --git a/.changeset/late-adults-love.md b/.changeset/late-adults-love.md new file mode 100644 index 000000000..51900bf22 --- /dev/null +++ b/.changeset/late-adults-love.md @@ -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. diff --git a/package.json b/package.json index 171440c19..9c3ea42fc 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 298d77477..993b138d4 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -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; diff --git a/packages/core/commons-api/src/index.ts b/packages/core/commons-api/src/index.ts index b0ddec62e..ac17b383d 100644 --- a/packages/core/commons-api/src/index.ts +++ b/packages/core/commons-api/src/index.ts @@ -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', diff --git a/packages/core/htpasswd/package.json b/packages/core/htpasswd/package.json index a897478e9..6b52dd07c 100644 --- a/packages/core/htpasswd/package.json +++ b/packages/core/htpasswd/package.json @@ -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", diff --git a/packages/core/htpasswd/src/utils.ts b/packages/core/htpasswd/src/utils.ts index c84d01dd7..078c60296 100644 --- a/packages/core/htpasswd/src/utils.ts +++ b/packages/core/htpasswd/src/utils.ts @@ -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; } diff --git a/packages/core/htpasswd/tsconfig.json b/packages/core/htpasswd/tsconfig.json index bbaf68bd5..50f2a6ab8 100644 --- a/packages/core/htpasswd/tsconfig.json +++ b/packages/core/htpasswd/tsconfig.json @@ -9,6 +9,9 @@ "references": [ { "path": "../file-locking" + }, + { + "path": "../commons-api" } ] } diff --git a/packages/core/local-storage/package.json b/packages/core/local-storage/package.json index 06a9cd90b..186bbe10e 100644 --- a/packages/core/local-storage/package.json +++ b/packages/core/local-storage/package.json @@ -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", diff --git a/packages/core/local-storage/src/local-database.ts b/packages/core/local-storage/src/local-database.ts index 5b987972a..f535e0482 100644 --- a/packages/core/local-storage/src/local-database.ts +++ b/packages/core/local-storage/src/local-database.ts @@ -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; diff --git a/packages/core/local-storage/src/token.ts b/packages/core/local-storage/src/token.ts new file mode 100644 index 000000000..ef54ec316 --- /dev/null +++ b/packages/core/local-storage/src/token.ts @@ -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 || []; + } +} diff --git a/packages/core/local-storage/tests/local-database.test.ts b/packages/core/local-storage/tests/local-database.test.ts index 08d745b42..aa041ab69 100644 --- a/packages/core/local-storage/tests/local-database.test.ts +++ b/packages/core/local-storage/tests/local-database.test.ts @@ -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!' - ); - }); - }); }); diff --git a/packages/core/local-storage/tests/token.test.ts b/packages/core/local-storage/tests/token.test.ts new file mode 100644 index 000000000..717e561f6 --- /dev/null +++ b/packages/core/local-storage/tests/token.test.ts @@ -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'); + }); + }); +}); diff --git a/packages/core/types/index.d.ts b/packages/core/types/index.d.ts index 73f98ef6d..1238b0de3 100644 --- a/packages/core/types/index.d.ts +++ b/packages/core/types/index.d.ts @@ -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[]>; diff --git a/packages/server/test/token/token.spec.yaml b/packages/server/test/token/token.spec.yaml index 5ca283b92..494287af3 100644 --- a/packages/server/test/token/token.spec.yaml +++ b/packages/server/test/token/token.spec.yaml @@ -22,6 +22,6 @@ packages: publish: $authenticated logs: - { type: stdout, format: pretty, level: error } -experiments: +flags: ## enable token for testing token: true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6bf7893c..fa929f3aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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