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