1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-11-08 23:25:51 +01:00

feat: relocate core packages (#1906)

* chore: clean lint warnings

* refactor: move core packages
This commit is contained in:
Juan Picado 2020-08-19 20:27:35 +02:00
parent 33f8b00080
commit 3838d3d212
125 changed files with 7656 additions and 76 deletions

4
.vscode/launch.json vendored

@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
@ -21,12 +22,11 @@
"name": "Unit Tests",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js",
"program": "${workspaceRoot}/node_modules/bin/jest",
"stopOnEntry": false,
"args": [
"--debug=true" ],
"cwd": "${workspaceRoot}",
"preLaunchTask": "pre-test",
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"

@ -2,11 +2,11 @@
{
"files.exclude": {
"**/.nyc_output": true,
"**/build": true,
"**/build": false,
"**/coverage": true,
".idea": true,
"storage_default_storage": true,
".yarn": true
},
"typescript.tsdk": "node_modules/typescript/lib"
}
}

@ -1,19 +1,21 @@
const setup = jest.fn();
const debug = require('debug')('verdaccio:test');
const setup = debug;
const logger = {
child: jest.fn(() => ({
debug: jest.fn(),
trace: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
debug,
trace: debug,
warn: debug,
info: debug,
error: debug,
fatal: debug,
})),
debug: jest.fn(),
trace: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
debug: debug,
trace: debug,
warn: debug,
info: debug,
error: debug,
fatal: debug,
};
export { setup, logger };

@ -31,6 +31,7 @@
"@verdaccio/middleware": "5.0.0-alpha.0",
"@verdaccio/store": "5.0.0-alpha.0",
"@verdaccio/utils": "5.0.0-alpha.0",
"debug": "^4.1.1",
"cookies": "0.8.0",
"express": "4.17.1",
"lodash": "4.17.15",
@ -42,7 +43,7 @@
"@verdaccio/dev-types": "5.0.0-alpha.0",
"@verdaccio/types": "9.5.0",
"body-parser": "1.19.0",
"express": "4.17.1"
"supertest": "next"
},
"gitHead": "7c246ede52ff717707fcae66dd63fc4abd536982"
}

@ -1,5 +1,6 @@
import _ from 'lodash';
import { Router } from 'express';
import buildDebug from 'debug';
import { allow } from '@verdaccio/middleware';
import { convertDistRemoteToLocalTarballUrls, getVersion, ErrorCode } from '@verdaccio/utils';
@ -7,6 +8,8 @@ import { HEADERS, DIST_TAGS, API_ERROR } from '@verdaccio/dev-commons';
import { Config, Package } from '@verdaccio/types';
import { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler } from '@verdaccio/dev-types';
const debug = buildDebug('verdaccio:api:package');
const downloadStream = (packageName: string, filename: string, storage: any, req: $RequestExtend, res: $ResponseExtend): void => {
const stream = storage.getTarball(packageName, filename);
@ -26,36 +29,52 @@ export default function (route: Router, auth: IAuth, storage: IStorageHandler, c
const can = allow(auth);
// TODO: anonymous user?
route.get('/:package/:version?', can('access'), function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
debug('init package by version');
const name = req.params.package;
const getPackageMetaCallback = function (err, metadata: Package): void {
if (err) {
debug('error on fetch metadata for %o with error %o', name, err.message);
return next(err);
}
metadata = convertDistRemoteToLocalTarballUrls(metadata, req, config.url_prefix);
debug('convert dist remote to local with prefix %o', config?.url_prefix);
metadata = convertDistRemoteToLocalTarballUrls(metadata, req, config?.url_prefix);
let queryVersion = req.params.version;
debug('query by param version: %o', queryVersion);
if (_.isNil(queryVersion)) {
debug('param %o version found', queryVersion);
return next(metadata);
}
let version = getVersion(metadata, queryVersion);
debug('query by latest version %o and result %o', queryVersion, version);
if (_.isNil(version) === false) {
debug('latest version found %o', version);
return next(version);
}
if (_.isNil(metadata[DIST_TAGS]) === false) {
if (_.isNil(metadata[DIST_TAGS][queryVersion]) === false) {
queryVersion = metadata[DIST_TAGS][queryVersion];
debug('dist-tag version found %o', queryVersion);
version = getVersion(metadata, queryVersion);
if (_.isNil(version) === false) {
debug('dist-tag found %o', version);
return next(version);
}
}
} else {
debug('dist tag not detected');
}
return next(ErrorCode.getNotFound(`${API_ERROR.VERSION_NOT_EXIST}: ${req.params.version}`));
debug('package version not found %o', queryVersion);
return next(ErrorCode.getNotFound(`${API_ERROR.VERSION_NOT_EXIST}: ${queryVersion}`));
};
debug('get package name %o', name);
debug('uplinks look up enabled');
storage.getPackage({
name: req.params.package,
name,
uplinksLook: true,
req,
callback: getPackageMetaCallback,

@ -2,6 +2,7 @@ import Path from 'path';
import _ from 'lodash';
import mime from 'mime';
import { Router } from 'express';
import buildDebug from 'debug';
import { IAuth, $ResponseExtend, $RequestExtend, $NextFunctionVer, IStorageHandler } from '@verdaccio/dev-types';
import { API_MESSAGE, HEADERS, DIST_TAGS, API_ERROR, HTTP_STATUS } from '@verdaccio/dev-commons';
@ -14,6 +15,8 @@ import { logger } from '@verdaccio/logger';
import star from './star';
import { isPublishablePackage } from './utils';
const debug = buildDebug('verdaccio:api:publish');
export default function publish(router: Router, auth: IAuth, storage: IStorageHandler, config: Config): void {
const can = allow(auth);
@ -106,7 +109,7 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
const packageName = req.params.package;
logger.debug({ packageName }, `publishing or updating a new version for @{packageName}`);
debug('publishing or updating a new version for %o', packageName);
/**
* Write tarball of stream data from package clients.
@ -114,14 +117,16 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
const createTarball = function (filename: string, data, cb: Callback): void {
const stream = storage.addTarball(packageName, filename);
stream.on('error', function (err) {
debug('error on stream a tarball %o for %o with error %o', filename, packageName, err.message);
cb(err);
});
stream.on('success', function () {
debug('success on stream a tarball %o for %o', filename, packageName);
cb();
});
// this is dumb and memory-consuming, but what choices do we have?
// flow: we need first refactor this file before decides which type use here
stream.end(new Buffer(data.data, 'base64'));
stream.end(Buffer.from(data.data, 'base64'));
stream.done();
};
@ -129,6 +134,7 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
* Add new package version in storage
*/
const createVersion = function (version: string, metadata: Version, cb: Callback): void {
debug('add a new package version %o to storage %o', version, metadata);
storage.addVersion(packageName, version, metadata, null, cb);
};
@ -136,20 +142,26 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
* Add new tags in storage
*/
const addTags = function (tags: MergeTags, cb: Callback): void {
debug('add new tag %o to storage', packageName);
storage.mergeTags(packageName, tags, cb);
};
const afterChange = function (error, okMessage, metadata: Package): void {
const metadataCopy: Package = { ...metadata };
debug('after change metadata %o', metadata);
const { _attachments, versions } = metadataCopy;
// `npm star` wouldn't have attachments
// and `npm deprecate` would have attachments as a empty object, i.e {}
if (_.isNil(_attachments) || JSON.stringify(_attachments) === '{}') {
debug('no attachments detected');
if (error) {
debug('no_attachments: after change error with %o', error.message);
return next(error);
}
debug('no_attachments: after change success');
res.status(HTTP_STATUS.CREATED);
return next({
ok: okMessage,
@ -165,11 +177,13 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
if (isInvalidBodyFormat) {
// npm is doing something strange again
// if this happens in normal circumstances, report it as a bug
debug('invalid body format');
logger.info({ packageName }, `wrong package format on publish a package @{packageName}`);
return next(ErrorCode.getBadRequest(API_ERROR.UNSUPORTED_REGISTRY_CALL));
}
if (error && error.status !== HTTP_STATUS.CONFLICT) {
debug('error on change or update a package with %o', error.message);
return next(error);
}
@ -177,7 +191,9 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
const [firstAttachmentKey] = Object.keys(_attachments);
createTarball(Path.basename(firstAttachmentKey), _attachments[firstAttachmentKey], function (error) {
debug('creating a tarball %o', firstAttachmentKey);
if (error) {
debug('error on create a tarball for %o with error %o', packageName, error.message);
return next(error);
}
@ -187,20 +203,24 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
createVersion(versionToPublish, versions[versionToPublish], function (error) {
if (error) {
debug('error on create a version for %o with error %o', packageName, error.message);
return next(error);
}
addTags(metadataCopy[DIST_TAGS], async function (error) {
if (error) {
debug('error on create a tag for %o with error %o', packageName, error.message);
return next(error);
}
try {
await notify(metadataCopy, config, req.remote_user, `${metadataCopy.name}@${versionToPublish}`);
} catch (error) {
debug('error on notify add a new tag %o', `${metadataCopy.name}@${versionToPublish}`);
logger.error({ error }, 'notify batch service has failed: @{error}');
}
debug('add a tag succesfully for %o', `${metadataCopy.name}@${versionToPublish}`);
res.status(HTTP_STATUS.CREATED);
return next({ ok: okMessage, success: true });
});
@ -209,33 +229,40 @@ export function publishPackage(storage: IStorageHandler, config: Config, auth: I
};
if (isPublishablePackage(req.body) === false && isObject(req.body.users)) {
debug('starting star a package');
return starApi(req, res, next);
}
try {
debug('pre validation metadata to publish %o', req.body);
const metadata = validateMetadata(req.body, packageName);
debug('post validation metadata to publish %o', metadata);
// treating deprecation as updating a package
if (req.params._rev || isRelatedToDeprecation(req.body)) {
logger.debug({ packageName }, `updating a new version for @{packageName}`);
debug('updating a new version for %o', packageName);
// we check unpublish permissions, an update is basically remove versions
const remote = req.remote_user;
auth.allow_unpublish({ packageName }, remote, (error) => {
debug('allowed to unpublish a package %o', packageName);
if (error) {
logger.debug({ packageName }, `not allowed to unpublish a version for @{packageName}`);
debug('not allowed to unpublish a version for %o', packageName);
return next(error);
}
debug('update a package');
storage.changePackage(packageName, metadata, req.params.revision, function (error) {
afterChange(error, API_MESSAGE.PKG_CHANGED, metadata);
});
});
} else {
logger.debug({ packageName }, `adding a new version for @{packageName}`);
debug('adding a new version for the package %o', packageName);
storage.addPackage(packageName, metadata, function (error) {
debug('package metadata updated %o', metadata);
afterChange(error, API_MESSAGE.PKG_CREATED, metadata);
});
}
} catch (error) {
debug('error on publish, bad package format %o', packageName);
logger.error({ packageName }, 'error on publish, bad package data for @{packageName}');
return next(ErrorCode.getBadData(API_ERROR.BAD_PACKAGE_DATA));
}
@ -287,12 +314,15 @@ export function addVersion(storage: IStorageHandler) {
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
const { version, tag } = req.params;
const packageName = req.params.package;
debug('add a new version %o and tag %o for %o', version, tag, packageName);
storage.addVersion(packageName, version, req.body, tag, function (error) {
if (error) {
debug('error on add new version');
return next(error);
}
debug('success on add new version');
res.status(HTTP_STATUS.CREATED);
return next({
ok: API_MESSAGE.PKG_PUBLISHED,

@ -2,9 +2,12 @@ import { USERS, HTTP_STATUS } from '@verdaccio/dev-commons';
import { Response } from 'express';
import _ from 'lodash';
import { logger } from '@verdaccio/logger';
import buildDebug from 'debug';
import { $RequestExtend, $NextFunctionVer, IStorageHandler } from '@verdaccio/dev-types';
const debug = buildDebug('verdaccio:api:publish:star');
export default function (storage: IStorageHandler): (req: $RequestExtend, res: Response, next: $NextFunctionVer) => void {
const validateInputs = (newUsers, localUsers, username, isStar): boolean => {
const isExistlocalUsers = _.isNil(localUsers[username]) === false;
@ -20,22 +23,27 @@ export default function (storage: IStorageHandler): (req: $RequestExtend, res: R
return (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
const name = req.params.package;
logger.debug({ name }, 'starring a package for @{name}');
debug('starring a package for %o', name);
const afterChangePackage = function (err?: Error) {
if (err) {
debug('error on update package for %o', name);
return next(err);
}
debug('succes update package for %o', name);
res.status(HTTP_STATUS.OK);
next({
success: true,
});
};
debug('get package info package for %o', name);
storage.getPackage({
name,
req,
callback: function (err, info) {
if (err) {
debug('error on get package info package for %o', name);
return next(err);
}
const newStarUser = req.body[USERS];
@ -43,6 +51,7 @@ export default function (storage: IStorageHandler): (req: $RequestExtend, res: R
const localStarUsers = info[USERS];
// Check is star or unstar
const isStar = Object.keys(newStarUser).includes(remoteUsername);
debug('is start? %o', isStar);
if (_.isNil(localStarUsers) === false && validateInputs(newStarUser, localStarUsers, remoteUsername, isStar)) {
return afterChangePackage();
}
@ -61,6 +70,7 @@ export default function (storage: IStorageHandler): (req: $RequestExtend, res: R
},
{}
);
debug('update package for %o', name);
storage.changePackage(name, { ...info, users }, req.body._rev, function (err) {
afterChangePackage(err);
});

@ -1,13 +1,12 @@
import _ from 'lodash';
import Cookies from 'cookies';
import { Response, Router } from 'express';
import { createRemoteUser, createSessionToken, getAuthenticatedMessage, validatePassword, ErrorCode } from '@verdaccio/utils';
import { createRemoteUser, getAuthenticatedMessage, validatePassword, ErrorCode } from '@verdaccio/utils';
import { getApiToken } from '@verdaccio/auth';
import { logger } from '@verdaccio/logger';
import { Config, RemoteUser } from '@verdaccio/types';
import { $RequestExtend, $ResponseExtend, $NextFunctionVer, IAuth } from '@verdaccio/dev-types';
import { $RequestExtend, $NextFunctionVer, IAuth } from '@verdaccio/dev-types';
import { API_ERROR, API_MESSAGE, HTTP_STATUS } from '@verdaccio/dev-commons';
export default function (route: Router, auth: IAuth, config: Config): void {

@ -15,10 +15,12 @@ jest.mock('@verdaccio/auth', () => ({
apiJWTmiddleware() {
return mockApiJWTmiddleware();
}
allow_access(_d, f_, cb) {
allow_access(_d, _f, cb) {
// always allow access
cb(null, true);
}
allow_publish(_d, f_, cb) {
allow_publish(_d, _f, cb) {
// always allow publish
cb(null, true);
}
},
@ -26,12 +28,12 @@ jest.mock('@verdaccio/auth', () => ({
describe('package', () => {
let app;
beforeAll(async () => {
beforeEach(async () => {
app = await initializeServer('package.yaml');
await publishVersion(app, 'package.yaml', 'foo', '1.0.0');
});
test('should return a package', async (done) => {
await publishVersion(app, 'package.yaml', 'foo', '1.0.0');
return supertest(app)
.get('/foo')
.set('Accept', HEADERS.JSON)
@ -44,33 +46,35 @@ describe('package', () => {
});
test('should return a package by version', async (done) => {
await publishVersion(app, 'package.yaml', 'foo2', '1.0.0');
return supertest(app)
.get('/foo/1.0.0')
.get('/foo2/1.0.0')
.set('Accept', HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK)
.then((response) => {
expect(response.body.name).toEqual('foo');
expect(response.body.name).toEqual('foo2');
done();
});
});
// TODO: investigate the 404
test.skip('should return a package by dist-tag', async (done) => {
// await publishVersion(app, 'package.yaml', 'foo3', '1.0.0');
await publishVersion(app, 'package.yaml', 'foo-tagged', '1.0.0');
await publishTaggedVersion(app, 'package.yaml', 'foo-tagged', '1.0.1', 'test');
return supertest(app)
.get('/foo-tagged/1.0.0')
.get('/foo-tagged/1.0.1')
.set('Accept', HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED)
.then((response) => {
expect(response.body.name).toEqual('foo');
expect(response.body.name).toEqual('foo3');
done();
});
});
test('should return 404', async () => {
test.skip('should return 404', async () => {
return supertest(app)
.get('/404-not-found')
.set('Accept', HEADERS.JSON)

@ -49,7 +49,7 @@ export function generatePackageMetadata(pkgName: string, version = '1.0.0', dist
[`${pkgName}-${version}.tgz`]: {
content_type: 'application/octet-stream',
data:
'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=',
'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=+2W32vbMBDH85y',
length: 512,
},
},

@ -0,0 +1,3 @@
{
"extends": "../../../.babelrc"
}

@ -0,0 +1,5 @@
node_modules
coverage/
lib/
.nyc_output
tests-report/

1
packages/core/file-locking/.gitignore vendored Normal file

@ -0,0 +1 @@
lib/

@ -0,0 +1,290 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.7.2](https://github.com/verdaccio/monorepo/compare/v9.7.1...v9.7.2) (2020-07-20)
**Note:** Version bump only for package @verdaccio/file-locking
## [9.7.1](https://github.com/verdaccio/monorepo/compare/v9.7.0...v9.7.1) (2020-07-10)
**Note:** Version bump only for package @verdaccio/file-locking
# [9.7.0](https://github.com/verdaccio/monorepo/compare/v9.6.1...v9.7.0) (2020-06-24)
**Note:** Version bump only for package @verdaccio/file-locking
## [9.6.1](https://github.com/verdaccio/monorepo/compare/v9.6.0...v9.6.1) (2020-06-07)
**Note:** Version bump only for package @verdaccio/file-locking
# [9.5.0](https://github.com/verdaccio/monorepo/compare/v9.4.1...v9.5.0) (2020-05-02)
**Note:** Version bump only for package @verdaccio/file-locking
# [9.4.0](https://github.com/verdaccio/monorepo/compare/v9.3.4...v9.4.0) (2020-03-21)
**Note:** Version bump only for package @verdaccio/file-locking
## [9.3.2](https://github.com/verdaccio/monorepo/compare/v9.3.1...v9.3.2) (2020-03-08)
**Note:** Version bump only for package @verdaccio/file-locking
## [9.3.1](https://github.com/verdaccio/monorepo/compare/v9.3.0...v9.3.1) (2020-02-23)
**Note:** Version bump only for package @verdaccio/file-locking
# [9.3.0](https://github.com/verdaccio/monorepo/compare/v9.2.0...v9.3.0) (2020-01-29)
**Note:** Version bump only for package @verdaccio/file-locking
# [9.0.0](https://github.com/verdaccio/monorepo/compare/v8.5.3...v9.0.0) (2020-01-07)
### Features
* **eslint-config:** enable eslint curly ([#308](https://github.com/verdaccio/monorepo/issues/308)) ([91acb12](https://github.com/verdaccio/monorepo/commit/91acb121847018e737c21b367fcaab8baa918347))
## [8.5.2](https://github.com/verdaccio/monorepo/compare/v8.5.1...v8.5.2) (2019-12-25)
**Note:** Version bump only for package @verdaccio/file-locking
## [8.5.1](https://github.com/verdaccio/monorepo/compare/v8.5.0...v8.5.1) (2019-12-24)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.5.0](https://github.com/verdaccio/monorepo/compare/v8.4.2...v8.5.0) (2019-12-22)
**Note:** Version bump only for package @verdaccio/file-locking
## [8.4.2](https://github.com/verdaccio/monorepo/compare/v8.4.1...v8.4.2) (2019-11-23)
**Note:** Version bump only for package @verdaccio/file-locking
## [8.4.1](https://github.com/verdaccio/monorepo/compare/v8.4.0...v8.4.1) (2019-11-22)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.4.0](https://github.com/verdaccio/monorepo/compare/v8.3.0...v8.4.0) (2019-11-22)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.3.0](https://github.com/verdaccio/monorepo/compare/v8.2.0...v8.3.0) (2019-10-27)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.2.0](https://github.com/verdaccio/monorepo/compare/v8.2.0-next.0...v8.2.0) (2019-10-23)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.2.0-next.0](https://github.com/verdaccio/monorepo/compare/v8.1.4...v8.2.0-next.0) (2019-10-08)
### Bug Fixes
* fixed lint errors ([5e677f7](https://github.com/verdaccio/monorepo/commit/5e677f7))
* fixed lint errors ([c80e915](https://github.com/verdaccio/monorepo/commit/c80e915))
* quotes should be single ([ae9aa44](https://github.com/verdaccio/monorepo/commit/ae9aa44))
## [8.1.2](https://github.com/verdaccio/monorepo/compare/v8.1.1...v8.1.2) (2019-09-29)
**Note:** Version bump only for package @verdaccio/file-locking
## [8.1.1](https://github.com/verdaccio/monorepo/compare/v8.1.0...v8.1.1) (2019-09-26)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.1.0](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.1...v8.1.0) (2019-09-07)
**Note:** Version bump only for package @verdaccio/file-locking
## [8.0.1-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.0...v8.0.1-next.1) (2019-08-29)
**Note:** Version bump only for package @verdaccio/file-locking
## [8.0.1-next.0](https://github.com/verdaccio/monorepo/compare/v8.0.0...v8.0.1-next.0) (2019-08-29)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.0.0](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.4...v8.0.0) (2019-08-22)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.0.0-next.4](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.3...v8.0.0-next.4) (2019-08-18)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.0.0-next.2](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.1...v8.0.0-next.2) (2019-08-03)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.0.0-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.0...v8.0.0-next.1) (2019-08-01)
**Note:** Version bump only for package @verdaccio/file-locking
# [8.0.0-next.0](https://github.com/verdaccio/monorepo/compare/v2.0.0...v8.0.0-next.0) (2019-08-01)
### Bug Fixes
* eslint and typescript errors ([8b3f153](https://github.com/verdaccio/monorepo/commit/8b3f153))
* lint issues ([d195fff](https://github.com/verdaccio/monorepo/commit/d195fff))
### Features
* remote lodash as dependency ([affb65b](https://github.com/verdaccio/monorepo/commit/affb65b))
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.0.4](https://github.com/verdaccio/file-locking/compare/v1.0.3...v1.0.4) (2019-07-16)
### Build System
* **deps:** update dependencies ([45b12de](https://github.com/verdaccio/file-locking/commit/45b12de))
* **deps:** update husky dependency ([bdb7bad](https://github.com/verdaccio/file-locking/commit/bdb7bad))
<a name="1.0.3"></a>
## [1.0.3](https://github.com/verdaccio/file-locking/compare/v1.0.2...v1.0.3) (2019-06-22)
### Bug Fixes
* update build script and remove source map ([ec3db50](https://github.com/verdaccio/file-locking/commit/ec3db50))
<a name="1.0.2"></a>
## [1.0.2](https://github.com/verdaccio/file-locking/compare/v1.0.1...v1.0.2) (2019-06-15)
<a name="1.0.1"></a>
## [1.0.1](https://github.com/verdaccio/file-locking/compare/v1.0.0...v1.0.1) (2019-06-15)
### Bug Fixes
* eslint and typescript errors ([3538e7c](https://github.com/verdaccio/file-locking/commit/3538e7c))

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Verdaccio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,22 @@
## Deprecated repository
**This repository has been moved to a monorepo you can find in [verdaccio/monorepo](https://github.com/verdaccio/monorepo). This package is located in [`core/file-locking` folder](https://github.com/verdaccio/monorepo/tree/master/core/file-locking)**
---
# File Locking
This an utility to lock and unlock files
[![verdaccio (latest)](https://img.shields.io/npm/v/@verdaccio/file-locking/latest.svg)](https://www.npmjs.com/package/verdaccio)
[![docker pulls](https://img.shields.io/docker/pulls/verdaccio/verdaccio.svg?maxAge=43200)](https://verdaccio.org/docs/en/docker.html)
[![backers](https://opencollective.com/verdaccio/tiers/backer/badge.svg?label=Backer&color=brightgreen)](https://opencollective.com/verdaccio)
[![stackshare](https://img.shields.io/badge/Follow%20on-StackShare-blue.svg?logo=stackshare&style=flat)](https://stackshare.io/verdaccio)
[![discord](https://img.shields.io/discord/388674437219745793.svg)](http://chat.verdaccio.org/)
[![node](https://img.shields.io/node/v/@verdaccio/file-locking/latest.svg)](https://www.npmjs.com/package/verdaccio)
![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/verdaccio/localized.svg)](https://crowdin.com/project/verdaccio)
[![Twitter followers](https://img.shields.io/twitter/follow/verdaccio_npm.svg?style=social&label=Follow)](https://twitter.com/verdaccio_npm)
[![Github](https://img.shields.io/github/stars/verdaccio/verdaccio.svg?style=social&label=Stars)](https://github.com/verdaccio/verdaccio/stargazers)

@ -0,0 +1,3 @@
const config = require('../../../jest/config');
module.exports = Object.assign({}, config, {});

@ -0,0 +1,47 @@
{
"name": "@verdaccio/file-locking",
"version": "10.0.0-beta",
"description": "library that handle file locking",
"keywords": [
"verdaccio",
"lock",
"fs"
],
"author": "Juan Picado <juanpicado19@gmail.com>",
"license": "MIT",
"homepage": "https://verdaccio.org",
"repository": {
"type": "git",
"url": "https://github.com/verdaccio/monorepo",
"directory": "core/file-locking"
},
"bugs": {
"url": "https://github.com/verdaccio/monorepo/issues"
},
"publishConfig": {
"access": "public"
},
"main": "build/index.js",
"types": "build/index.d.ts",
"files": [
"build"
],
"dependencies": {
"lockfile": "1.0.4"
},
"devDependencies": {
"@verdaccio/types": "workspace:*"
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"type-check": "tsc --noEmit",
"build:types": "tsc --emitDeclarationOnly --declaration true",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"build": "pnpm run build:js && pnpm run build:types"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio"
}
}

@ -0,0 +1,3 @@
export * from './unclock';
export * from './readFile';
export * from './lockfile';

@ -0,0 +1,29 @@
import { Callback } from '@verdaccio/types';
import { lockfile, statDir, statfile } from './utils';
/**
* locks a file by creating a lock file
* @param name
* @param callback
*/
const lockFile = function (name: string, callback: Callback): void {
Promise.resolve()
.then(() => {
return statDir(name);
})
.then(() => {
return statfile(name);
})
.then(() => {
return lockfile(name);
})
.then(() => {
callback(null);
})
.catch((err) => {
callback(err);
});
};
export { lockFile };

@ -0,0 +1,82 @@
import fs from 'fs';
import { Callback } from '@verdaccio/types';
import { lockFile } from './lockfile';
export type ReadFileOptions = {
parse?: boolean;
lock?: boolean;
};
/**
* Reads a local file, which involves
* optionally taking a lock
* reading the file contents
* optionally parsing JSON contents
* @param {*} name
* @param {*} options
* @param {*} callback
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
function readFile(name: string, options: ReadFileOptions = {}, callback: Callback = (): void => {}): void {
if (typeof options === 'function') {
callback = options;
options = {};
}
options.lock = options.lock || false;
options.parse = options.parse || false;
const lock = function (options: ReadFileOptions): Promise<null | NodeJS.ErrnoException> {
return new Promise((resolve, reject): void => {
if (!options.lock) {
return resolve(null);
}
lockFile(name, function (err: NodeJS.ErrnoException | null) {
if (err) {
return reject(err);
}
return resolve(null);
});
});
};
const read = function (): Promise<NodeJS.ErrnoException | string> {
return new Promise((resolve, reject): void => {
fs.readFile(name, 'utf8', function (err, contents) {
if (err) {
return reject(err);
}
resolve(contents);
});
});
};
const parseJSON = function (contents: string): Promise<unknown> {
return new Promise((resolve, reject): void => {
if (!options.parse) {
return resolve(contents);
}
try {
contents = JSON.parse(contents);
return resolve(contents);
} catch (err) {
return reject(err);
}
});
};
Promise.resolve()
.then(() => lock(options))
.then(() => read())
.then((content) => parseJSON(content as string))
.then(
(result) => callback(null, result),
(err) => callback(err)
);
}
export { readFile };

@ -0,0 +1,10 @@
import locker from 'lockfile';
import { Callback } from '@verdaccio/types';
// unlocks file by removing existing lock file
export function unlockFile(name: string, next: Callback): void {
const lockFileName = `${name}.lock`;
locker.unlock(lockFileName, function () {
return next(null);
});
}

@ -0,0 +1,56 @@
import fs from 'fs';
import path from 'path';
import locker from 'lockfile';
export const statDir = (name: string): Promise<Error | null> => {
return new Promise((resolve, reject): void => {
// test to see if the directory exists
const dirPath = path.dirname(name);
fs.stat(dirPath, function (err, stats) {
if (err) {
return reject(err);
} else if (!stats.isDirectory()) {
return resolve(new Error(`${path.dirname(name)} is not a directory`));
} else {
return resolve(null);
}
});
});
};
export const statfile = (name: string): Promise<Error | null> => {
return new Promise((resolve, reject): void => {
// test to see if the directory exists
fs.stat(name, function (err, stats) {
if (err) {
return reject(err);
} else if (!stats.isFile()) {
return resolve(new Error(`${path.dirname(name)} is not a file`));
} else {
return resolve(null);
}
});
});
};
export const lockfile = (name: string): Promise<unknown> => {
return new Promise((resolve): void => {
const lockOpts = {
// time (ms) to wait when checking for stale locks
wait: 1000,
// how often (ms) to re-check stale locks
pollPeriod: 100,
// locks are considered stale after 5 minutes
stale: 5 * 60 * 1000,
// number of times to attempt to create a lock
retries: 100,
// time (ms) between tries
retryWait: 100,
};
const lockFileName = `${name}.lock`;
locker.lock(lockFileName, lockOpts, () => {
resolve();
});
});
};

@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`testing locking readFile read file with options (parse, lock) should to be found to be read it as object 1`] = `
Object {
"name": "assets",
"version": "0.0.1",
}
`;
exports[`testing locking readFile read file with no options should to be found to be read it as object 1`] = `
Object {
"name": "assets",
"version": "0.0.1",
}
`;
exports[`testing locking readFile read file with no options should to be found to be read it as string 1`] = `
"{
\\"name\\": \\"assets\\",
\\"version\\": \\"0.0.1\\"
}
"
`;

@ -0,0 +1,4 @@
{
"name": "assets",
"version": "0.0.1"
}

@ -0,0 +1,4 @@
{
"name": "assets",
"version": "0.0.1"
}

@ -0,0 +1,4 @@
{
"name": "assets",
"version": "0.0.1",
}

@ -0,0 +1,117 @@
import path from 'path';
import fs from 'fs';
import { lockFile, unlockFile, readFile } from '../src/index';
interface Error {
message: string;
}
const getFilePath = (filename: string): string => {
return path.resolve(__dirname, `assets/${filename}`);
};
const removeTempFile = (filename: string): void => {
const filepath = getFilePath(filename);
fs.unlink(filepath, (error) => {
if (error) {
throw error;
}
});
};
describe('testing locking', () => {
describe('lockFile', () => {
test('file should be found to be locked', (done) => {
lockFile(getFilePath('package.json'), (error: Error) => {
expect(error).toBeNull();
removeTempFile('package.json.lock');
done();
});
});
test('file should fail to be found to be locked', (done) => {
lockFile(getFilePath('package.fail.json'), (error: Error) => {
expect(error.message).toMatch(/ENOENT: no such file or directory, stat '(.*)package.fail.json'/);
done();
});
});
});
describe('unlockFile', () => {
test('file should to be found to be unLock', (done) => {
unlockFile(getFilePath('package.json.lock'), (error: Error) => {
expect(error).toBeNull();
done();
});
});
});
describe('readFile', () => {
test('read file with no options should to be found to be read it as string', (done) => {
readFile(getFilePath('package.json'), {}, (error: Error, data: string) => {
expect(error).toBeNull();
expect(data).toMatchSnapshot();
done();
});
});
test('read file with no options should to be found to be read it as object', (done) => {
const options = {
parse: true,
};
readFile(getFilePath('package.json'), options, (error: Error, data: string) => {
expect(error).toBeNull();
expect(data).toMatchSnapshot();
done();
});
});
test('read file with options (parse) should to be not found to be read it', (done) => {
const options = {
parse: true,
};
readFile(getFilePath('package.fail.json'), options, (error: Error) => {
expect(error.message).toMatch(/ENOENT: no such file or directory, open '(.*)package.fail.json'/);
done();
});
});
test('read file with options should to be found to be read it and fails to be parsed', (done) => {
const options = {
parse: true,
};
const errorMessage = process.platform === 'win32' ? 'Unexpected token } in JSON at position 47' : 'Unexpected token } in JSON at position 44';
readFile(getFilePath('wrong.package.json'), options, (error: Error) => {
expect(error.message).toEqual(errorMessage);
done();
});
});
test('read file with options (parse, lock) should to be found to be read it as object', (done) => {
const options = {
parse: true,
lock: true,
};
readFile(getFilePath('package2.json'), options, (error: Error, data: string) => {
expect(error).toBeNull();
expect(data).toMatchSnapshot();
removeTempFile('package2.json.lock');
done();
});
});
test('read file with options (parse, lock) should to be found to be read it and fails to be parsed', (done) => {
const options = {
parse: true,
lock: true,
};
const errorMessage = process.platform === 'win32' ? 'Unexpected token } in JSON at position 47' : 'Unexpected token } in JSON at position 44';
readFile(getFilePath('wrong.package.json'), options, (error: Error) => {
expect(error.message).toEqual(errorMessage);
removeTempFile('wrong.package.json.lock');
done();
});
});
});
});

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts"]
}

@ -0,0 +1,20 @@
declare module 'lockfile' {
type Callback = (err?: Error) => void;
interface LockOptions {
wait?: number;
pollPeriod?: number;
stale?: number;
retries?: number;
retryWait?: number;
}
interface LockFileExport {
lock(fileName: string, opts: LockOptions, cb: Callback): void;
unlock(fileName: string, cb: Callback): void;
}
const lockFileExport: LockFileExport;
export default lockFileExport;
}

@ -0,0 +1,3 @@
{
"extends": "../../../.babelrc"
}

@ -0,0 +1,6 @@
node_modules
coverage/
lib/
.nyc_output
tests-report/
fixtures/

@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/no-use-before-define": "off"
}
}

@ -0,0 +1,328 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.7.2](https://github.com/verdaccio/monorepo/compare/v9.7.1...v9.7.2) (2020-07-20)
**Note:** Version bump only for package verdaccio-htpasswd
## [9.7.1](https://github.com/verdaccio/monorepo/compare/v9.7.0...v9.7.1) (2020-07-10)
### Bug Fixes
* update dependencies ([#375](https://github.com/verdaccio/monorepo/issues/375)) ([1e7aeec](https://github.com/verdaccio/monorepo/commit/1e7aeec31b056979285e272793a95b8c75d57c77))
# [9.7.0](https://github.com/verdaccio/monorepo/compare/v9.6.1...v9.7.0) (2020-06-24)
**Note:** Version bump only for package verdaccio-htpasswd
## [9.6.1](https://github.com/verdaccio/monorepo/compare/v9.6.0...v9.6.1) (2020-06-07)
**Note:** Version bump only for package verdaccio-htpasswd
# [9.5.0](https://github.com/verdaccio/monorepo/compare/v9.4.1...v9.5.0) (2020-05-02)
**Note:** Version bump only for package verdaccio-htpasswd
## [9.4.1](https://github.com/verdaccio/monorepo/compare/v9.4.0...v9.4.1) (2020-04-30)
### Bug Fixes
* **verdaccio-htpasswd:** generate non-constant legacy 2 byte salt ([#357](https://github.com/verdaccio/monorepo/issues/357)) ([d522595](https://github.com/verdaccio/monorepo/commit/d522595122b7deaac8e3bc568f73658041811aaf))
# [9.4.0](https://github.com/verdaccio/monorepo/compare/v9.3.4...v9.4.0) (2020-03-21)
**Note:** Version bump only for package verdaccio-htpasswd
## [9.3.2](https://github.com/verdaccio/monorepo/compare/v9.3.1...v9.3.2) (2020-03-08)
### Bug Fixes
* update dependencies ([#332](https://github.com/verdaccio/monorepo/issues/332)) ([b6165ae](https://github.com/verdaccio/monorepo/commit/b6165aea9b7e4012477081eae68bfa7159c58f56))
## [9.3.1](https://github.com/verdaccio/monorepo/compare/v9.3.0...v9.3.1) (2020-02-23)
**Note:** Version bump only for package verdaccio-htpasswd
# [9.3.0](https://github.com/verdaccio/monorepo/compare/v9.2.0...v9.3.0) (2020-01-29)
**Note:** Version bump only for package verdaccio-htpasswd
# [9.0.0](https://github.com/verdaccio/monorepo/compare/v8.5.3...v9.0.0) (2020-01-07)
### chore
* update dependencies ([68add74](https://github.com/verdaccio/monorepo/commit/68add743159867f678ddb9168d2bc8391844de47))
### Features
* **eslint-config:** enable eslint curly ([#308](https://github.com/verdaccio/monorepo/issues/308)) ([91acb12](https://github.com/verdaccio/monorepo/commit/91acb121847018e737c21b367fcaab8baa918347))
### BREAKING CHANGES
* @verdaccio/eslint-config requires ESLint >=6.8.0 and Prettier >=1.19.1 to fix compatibility with overrides.extends config
## [8.5.2](https://github.com/verdaccio/monorepo/compare/v8.5.1...v8.5.2) (2019-12-25)
**Note:** Version bump only for package verdaccio-htpasswd
## [8.5.1](https://github.com/verdaccio/monorepo/compare/v8.5.0...v8.5.1) (2019-12-24)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.5.0](https://github.com/verdaccio/monorepo/compare/v8.4.2...v8.5.0) (2019-12-22)
**Note:** Version bump only for package verdaccio-htpasswd
## [8.4.2](https://github.com/verdaccio/monorepo/compare/v8.4.1...v8.4.2) (2019-11-23)
**Note:** Version bump only for package verdaccio-htpasswd
## [8.4.1](https://github.com/verdaccio/monorepo/compare/v8.4.0...v8.4.1) (2019-11-22)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.4.0](https://github.com/verdaccio/monorepo/compare/v8.3.0...v8.4.0) (2019-11-22)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.3.0](https://github.com/verdaccio/monorepo/compare/v8.2.0...v8.3.0) (2019-10-27)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.2.0](https://github.com/verdaccio/monorepo/compare/v8.2.0-next.0...v8.2.0) (2019-10-23)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.2.0-next.0](https://github.com/verdaccio/monorepo/compare/v8.1.4...v8.2.0-next.0) (2019-10-08)
### Bug Fixes
* fixed lint errors ([5e677f7](https://github.com/verdaccio/monorepo/commit/5e677f7))
## [8.1.2](https://github.com/verdaccio/monorepo/compare/v8.1.1...v8.1.2) (2019-09-29)
**Note:** Version bump only for package verdaccio-htpasswd
## [8.1.1](https://github.com/verdaccio/monorepo/compare/v8.1.0...v8.1.1) (2019-09-26)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.1.0](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.1...v8.1.0) (2019-09-07)
**Note:** Version bump only for package verdaccio-htpasswd
## [8.0.1-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.0...v8.0.1-next.1) (2019-08-29)
**Note:** Version bump only for package verdaccio-htpasswd
## [8.0.1-next.0](https://github.com/verdaccio/monorepo/compare/v8.0.0...v8.0.1-next.0) (2019-08-29)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.0.0](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.4...v8.0.0) (2019-08-22)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.0.0-next.4](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.3...v8.0.0-next.4) (2019-08-18)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.0.0-next.2](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.1...v8.0.0-next.2) (2019-08-03)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.0.0-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.0...v8.0.0-next.1) (2019-08-01)
**Note:** Version bump only for package verdaccio-htpasswd
# [8.0.0-next.0](https://github.com/verdaccio/monorepo/compare/v2.0.0...v8.0.0-next.0) (2019-08-01)
**Note:** Version bump only for package verdaccio-htpasswd
# Change Log
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
# [2.0.0](https://github.com/verdaccio/verdaccio-htpasswd/compare/v2.0.0-beta.1...v2.0.0) (2019-04-14)
### Features
* drop node v6 suport ([d1d52e8](https://github.com/verdaccio/verdaccio-htpasswd/commit/d1d52e8))
<a name="2.0.0-beta.1"></a>
# [2.0.0-beta.1](https://github.com/verdaccio/verdaccio-htpasswd/compare/v2.0.0-beta.0...v2.0.0-beta.1) (2019-02-24)
### Bug Fixes
* package.json to reduce vulnerabilities ([259bdaf](https://github.com/verdaccio/verdaccio-htpasswd/commit/259bdaf))
* update [@verdaccio](https://github.com/verdaccio)/file-locking@1.0.0 ([ec0bbfd](https://github.com/verdaccio/verdaccio-htpasswd/commit/ec0bbfd))
<a name="2.0.0-beta.0"></a>
# [2.0.0-beta.0](https://github.com/verdaccio/verdaccio-htpasswd/compare/v1.0.1...v2.0.0-beta.0) (2019-02-03)
### Features
* migrate to typescript ([79f6937](https://github.com/verdaccio/verdaccio-htpasswd/commit/79f6937))
* remove Node6 from CircleCI ([d3a05ab](https://github.com/verdaccio/verdaccio-htpasswd/commit/d3a05ab))
* use verdaccio babel preset ([3a63f88](https://github.com/verdaccio/verdaccio-htpasswd/commit/3a63f88))
<a name="1.0.1"></a>
## [1.0.1](https://github.com/verdaccio/verdaccio-htpasswd/compare/v1.0.0...v1.0.1) (2018-09-30)
### Bug Fixes
* password hash & increase coverage ([6420c26](https://github.com/verdaccio/verdaccio-htpasswd/commit/6420c26))
<a name="1.0.0"></a>
# [1.0.0](https://github.com/verdaccio/verdaccio-htpasswd/compare/v0.2.2...v1.0.0) (2018-09-30)
### Bug Fixes
* adds error message for user registration ([0bab945](https://github.com/verdaccio/verdaccio-htpasswd/commit/0bab945))
### Features
* **change-passwd:** implement change password [#32](https://github.com/verdaccio/verdaccio-htpasswd/issues/32) ([830b143](https://github.com/verdaccio/verdaccio-htpasswd/commit/830b143))

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Verdaccio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,65 @@
[![verdaccio (latest)](https://img.shields.io/npm/v/verdaccio-htpasswd/latest.svg)](https://www.npmjs.com/package/verdaccio-htpasswd)
[![Known Vulnerabilities](https://snyk.io/test/github/verdaccio/verdaccio-htpasswd/badge.svg?targetFile=package.json)](https://snyk.io/test/github/verdaccio/verdaccio-htpasswd?targetFile=package.json)
[![CircleCI](https://circleci.com/gh/verdaccio/verdaccio-htpasswd.svg?style=svg)](https://circleci.com/gh/ayusharma/verdaccio-htpasswd) [![codecov](https://codecov.io/gh/ayusharma/verdaccio-htpasswd/branch/master/graph/badge.svg)](https://codecov.io/gh/ayusharma/verdaccio-htpasswd)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fverdaccio%2Fverdaccio-htpasswd.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fverdaccio%2Fverdaccio-htpasswd?ref=badge_shield)
[![backers](https://opencollective.com/verdaccio/tiers/backer/badge.svg?label=Backer&color=brightgreen)](https://opencollective.com/verdaccio)
[![discord](https://img.shields.io/discord/388674437219745793.svg)](http://chat.verdaccio.org/)
![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)
[![node](https://img.shields.io/node/v/verdaccio-htpasswd/latest.svg)](https://www.npmjs.com/package/verdaccio-htpasswd)
# Verdaccio Module For User Auth Via Htpasswd
`verdaccio-htpasswd` is a default authentication plugin for the [Verdaccio](https://github.com/verdaccio/verdaccio).
> This plugin is being used as dependency after `v3.0.0-beta.x`. The `v2.x` still contains this plugin built-in.
## Install
As simple as running:
$ npm install -g verdaccio-htpasswd
## Configure
auth:
htpasswd:
file: ./htpasswd
# Maximum amount of users allowed to register, defaults to "+infinity".
# You can set this to -1 to disable registration.
#max_users: 1000
## Logging In
To log in using NPM, run:
```
npm adduser --registry https://your.registry.local
```
## Generate htpasswd username/password combination
If you wish to handle access control using htpasswd file, you can generate
username/password combination form
[here](http://www.htaccesstools.com/htpasswd-generator/) and add it to htpasswd
file.
## How does it work?
The htpasswd file contains rows corresponding to a pair of username and password
separated with a colon character. The password is encrypted using the UNIX system's
crypt method and may use MD5 or SHA1.
## Plugin Development in Verdaccio
There are many ways to extend [Verdaccio](https://github.com/verdaccio/verdaccio),
currently it support authentication plugins, middleware plugins (since v2.7.0)
and storage plugins since (v3.x).
#### Useful Links
- [Plugin Development](http://www.verdaccio.org/docs/en/dev-plugins.html)
- [List of Plugins](http://www.verdaccio.org/docs/en/plugins.html)
## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fverdaccio%2Fverdaccio-htpasswd.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fverdaccio%2Fverdaccio-htpasswd?ref=badge_large)

@ -0,0 +1,3 @@
const config = require('../../../jest/config');
module.exports = Object.assign({}, config, {});

@ -0,0 +1,50 @@
{
"name": "@verdaccio/htpasswd",
"version": "10.0.0-beta",
"description": "htpasswd auth plugin for Verdaccio",
"keywords": [
"verdaccio",
"plugin",
"auth",
"htpasswd"
],
"author": "Ayush Sharma <ayush.aceit@gmail.com>",
"license": "MIT",
"homepage": "https://verdaccio.org",
"repository": {
"type": "git",
"url": "https://github.com/verdaccio/monorepo",
"directory": "plugins/htpasswd"
},
"bugs": {
"url": "https://github.com/verdaccio/monorepo/issues"
},
"main": "./build/index.js",
"types": "./build/index.d.ts",
"files": [
"build"
],
"dependencies": {
"@verdaccio/file-locking": "workspace:*",
"apache-md5": "1.1.2",
"bcryptjs": "2.4.3",
"http-errors": "1.8.0",
"unix-crypt-td-js": "1.1.4"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",
"@verdaccio/types": "workspace:*"
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"type-check": "tsc --noEmit",
"build:types": "tsc --emitDeclarationOnly --declaration true",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"build": "pnpm run build:js && pnpm run build:types"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio"
}
}

@ -0,0 +1,52 @@
/** Node.js Crypt(3) Library
Inspired by (and intended to be compatible with) sendanor/crypt3
see https://github.com/sendanor/node-crypt3
The key difference is the removal of the dependency on the unix crypt(3) function
which is not platform independent, and requires compilation. Instead, a pure
javascript version is used.
*/
import crypto from 'crypto';
import crypt from 'unix-crypt-td-js';
/**
* Create salt
* @param {string} type The type of salt: md5, blowfish (only some linux
* distros), sha256 or sha512. Default is sha512.
* @returns {string} Generated salt string
*/
export function createSalt(type = 'crypt'): string {
switch (type) {
case 'crypt':
// Legacy crypt salt with no prefix (only the first 2 bytes will be used).
return crypto.randomBytes(2).toString('base64');
case 'md5':
return '$1$' + crypto.randomBytes(10).toString('base64');
case 'blowfish':
return '$2a$' + crypto.randomBytes(10).toString('base64');
case 'sha256':
return '$5$' + crypto.randomBytes(10).toString('base64');
case 'sha512':
return '$6$' + crypto.randomBytes(10).toString('base64');
default:
throw new TypeError(`Unknown salt type at crypt3.createSalt: ${type}`);
}
}
/**
* Crypt(3) password and data encryption.
* @param {string} key user's typed password
* @param {string} salt Optional salt, for example SHA-512 use "$6$salt$".
* @returns {string} A generated hash in format $id$salt$encrypted
* @see https://en.wikipedia.org/wiki/Crypt_(C)
*/
export default function crypt3(key: string, salt: string = createSalt()): string {
return crypt(key, salt);
}

@ -0,0 +1,239 @@
import fs from 'fs';
import Path from 'path';
import { Callback, AuthConf, Config, IPluginAuth } from '@verdaccio/types';
import { unlockFile } from '@verdaccio/file-locking';
import { verifyPassword, lockAndRead, parseHTPasswd, addUserToHTPasswd, changePasswordToHTPasswd, sanityCheck } from './utils';
export interface VerdaccioConfigApp extends Config {
file: string;
}
/**
* HTPasswd - Verdaccio auth class
*/
export default class HTPasswd implements IPluginAuth<VerdaccioConfigApp> {
/**
*
* @param {*} config htpasswd file
* @param {object} stuff config.yaml in object from
*/
private users: {};
private stuff: {};
private config: {};
private verdaccioConfig: Config;
private maxUsers: number;
private path: string;
private logger: {};
private lastTime: any;
// constructor
public constructor(config: AuthConf, stuff: VerdaccioConfigApp) {
this.users = {};
// config for this module
this.config = config;
this.stuff = stuff;
// verdaccio logger
this.logger = stuff.logger;
// verdaccio main config object
this.verdaccioConfig = stuff.config;
// all this "verdaccio_config" stuff is for b/w compatibility only
this.maxUsers = config.max_users ? config.max_users : Infinity;
this.lastTime = null;
const { file } = config;
if (!file) {
throw new Error('should specify "file" in config');
}
this.path = Path.resolve(Path.dirname(this.verdaccioConfig.self_path), file);
}
/**
* authenticate - Authenticate user.
* @param {string} user
* @param {string} password
* @param {function} cd
* @returns {function}
*/
public authenticate(user: string, password: string, cb: Callback): void {
this.reload((err) => {
if (err) {
return cb(err.code === 'ENOENT' ? null : err);
}
if (!this.users[user]) {
return cb(null, false);
}
if (!verifyPassword(password, this.users[user])) {
return cb(null, false);
}
// authentication succeeded!
// return all usergroups this user has access to;
// (this particular package has no concept of usergroups, so just return
// user herself)
return cb(null, [user]);
});
}
/**
* Add user
* 1. lock file for writing (other processes can still read)
* 2. reload .htpasswd
* 3. write new data into .htpasswd.tmp
* 4. move .htpasswd.tmp to .htpasswd
* 5. reload .htpasswd
* 6. unlock file
*
* @param {string} user
* @param {string} password
* @param {function} realCb
* @returns {function}
*/
public adduser(user: string, password: string, realCb: Callback): any {
const pathPass = this.path;
let sanity = sanityCheck(user, password, verifyPassword, this.users, this.maxUsers);
// preliminary checks, just to ensure that file won't be reloaded if it's
// not needed
if (sanity) {
return realCb(sanity, false);
}
lockAndRead(pathPass, (err, res): void => {
let locked = false;
// callback that cleans up lock first
const cb = (err): void => {
if (locked) {
unlockFile(pathPass, () => {
// ignore any error from the unlock
realCb(err, !err);
});
} else {
realCb(err, !err);
}
};
if (!err) {
locked = true;
}
// ignore ENOENT errors, we'll just create .htpasswd in that case
if (err && err.code !== 'ENOENT') {
return cb(err);
}
const body = (res || '').toString('utf8');
this.users = parseHTPasswd(body);
// real checks, to prevent race conditions
// parsing users after reading file.
sanity = sanityCheck(user, password, verifyPassword, this.users, this.maxUsers);
if (sanity) {
return cb(sanity);
}
try {
this._writeFile(addUserToHTPasswd(body, user, password), cb);
} catch (err) {
return cb(err);
}
});
}
/**
* Reload users
* @param {function} callback
*/
public reload(callback: Callback): void {
fs.stat(this.path, (err, stats) => {
if (err) {
return callback(err);
}
if (this.lastTime === stats.mtime) {
return callback();
}
this.lastTime = stats.mtime;
fs.readFile(this.path, 'utf8', (err, buffer) => {
if (err) {
return callback(err);
}
Object.assign(this.users, parseHTPasswd(buffer));
callback();
});
});
}
private _stringToUt8(authentication: string): string {
return (authentication || '').toString();
}
private _writeFile(body: string, cb: Callback): void {
fs.writeFile(this.path, body, (err) => {
if (err) {
cb(err);
} else {
this.reload(() => {
cb(null);
});
}
});
}
/**
* changePassword - change password for existing user.
* @param {string} user
* @param {string} password
* @param {function} cd
* @returns {function}
*/
public changePassword(user: string, password: string, newPassword: string, realCb: Callback): void {
lockAndRead(this.path, (err, res) => {
let locked = false;
const pathPassFile = this.path;
// callback that cleans up lock first
const cb = (err): void => {
if (locked) {
unlockFile(pathPassFile, () => {
// ignore any error from the unlock
realCb(err, !err);
});
} else {
realCb(err, !err);
}
};
if (!err) {
locked = true;
}
if (err && err.code !== 'ENOENT') {
return cb(err);
}
const body = this._stringToUt8(res);
this.users = parseHTPasswd(body);
if (!this.users[user]) {
return cb(new Error('User not found'));
}
try {
this._writeFile(changePasswordToHTPasswd(body, user, password, newPassword), cb);
} catch (err) {
return cb(err);
}
});
}
}

@ -0,0 +1,11 @@
import HTPasswd from './htpasswd';
/**
* A new instance of HTPasswd class.
* @param {object} config
* @param {object} stuff
* @returns {object}
*/
export default function (config, stuff): HTPasswd {
return new HTPasswd(config, stuff);
}

@ -0,0 +1,174 @@
import crypto from 'crypto';
import md5 from 'apache-md5';
import bcrypt from 'bcryptjs';
import createError, { HttpError } from 'http-errors';
import { readFile } from '@verdaccio/file-locking';
import { Callback } from '@verdaccio/types';
import crypt3 from './crypt3';
// this function neither unlocks file nor closes it
// it'll have to be done manually later
export function lockAndRead(name: string, cb: Callback): void {
readFile(name, { lock: true }, (err, res) => {
if (err) {
return cb(err);
}
return cb(null, res);
});
}
/**
* parseHTPasswd - convert htpasswd lines to object.
* @param {string} input
* @returns {object}
*/
export function parseHTPasswd(input: string): Record<string, any> {
return input.split('\n').reduce((result, line) => {
const args = line.split(':', 3);
if (args.length > 1) {
result[args[0]] = args[1];
}
return result;
}, {});
}
/**
* verifyPassword - matches password and it's hash.
* @param {string} passwd
* @param {string} hash
* @returns {boolean}
*/
export function verifyPassword(passwd: string, hash: string): boolean {
if (hash.match(/^\$2(a|b|y)\$/)) {
return bcrypt.compareSync(passwd, hash);
} else if (hash.indexOf('{PLAIN}') === 0) {
return passwd === hash.substr(7);
} else if (hash.indexOf('{SHA}') === 0) {
return (
crypto
.createHash('sha1')
// https://nodejs.org/api/crypto.html#crypto_hash_update_data_inputencoding
.update(passwd, 'utf8')
.digest('base64') === hash.substr(5)
);
}
// for backwards compatibility, first check md5 then check crypt3
return md5(passwd, hash) === hash || crypt3(passwd, hash) === hash;
}
/**
* addUserToHTPasswd - Generate a htpasswd format for .htpasswd
* @param {string} body
* @param {string} user
* @param {string} passwd
* @returns {string}
*/
export function addUserToHTPasswd(body: string, user: string, passwd: string): string {
if (user !== encodeURIComponent(user)) {
const err = createError('username should not contain non-uri-safe characters');
err.status = 409;
throw err;
}
if (crypt3) {
passwd = crypt3(passwd);
} else {
passwd = '{SHA}' + crypto.createHash('sha1').update(passwd, 'utf8').digest('base64');
}
const comment = 'autocreated ' + new Date().toJSON();
let newline = `${user}:${passwd}:${comment}\n`;
if (body.length && body[body.length - 1] !== '\n') {
newline = '\n' + newline;
}
return body + newline;
}
/**
* Sanity check for a user
* @param {string} user
* @param {object} users
* @param {number} maxUsers
* @returns {object}
*/
export function sanityCheck(user: string, password: string, verifyFn: Callback, users: {}, maxUsers: number): HttpError | null {
let err;
// check for user or password
if (!user || !password) {
err = Error('username and password is required');
err.status = 400;
return err;
}
const hash = users[user];
if (maxUsers < 0) {
err = Error('user registration disabled');
err.status = 409;
return err;
}
if (hash) {
const auth = verifyFn(password, users[user]);
if (auth) {
err = Error('username is already registered');
err.status = 409;
return err;
}
err = Error('unauthorized access');
err.status = 401;
return err;
} else if (Object.keys(users).length >= maxUsers) {
err = Error('maximum amount of users reached');
err.status = 403;
return err;
}
return null;
}
export function getCryptoPassword(password: string): string {
return `{SHA}${crypto.createHash('sha1').update(password, 'utf8').digest('base64')}`;
}
/**
* changePasswordToHTPasswd - change password for existing user
* @param {string} body
* @param {string} user
* @param {string} passwd
* @param {string} newPasswd
* @returns {string}
*/
export function changePasswordToHTPasswd(body: string, user: string, passwd: string, newPasswd: string): string {
let lines = body.split('\n');
lines = lines.map((line) => {
const [username, password] = line.split(':', 3);
if (username === user) {
let _passwd;
let _newPasswd;
if (crypt3) {
_passwd = crypt3(passwd, password);
_newPasswd = crypt3(newPasswd);
} else {
_passwd = getCryptoPassword(passwd);
_newPasswd = getCryptoPassword(newPasswd);
}
if (password == _passwd) {
// replace old password hash with new password hash
line = line.replace(_passwd, _newPasswd);
} else {
throw new Error('Invalid old Password');
}
}
return line;
});
return lines.join('\n');
}

@ -0,0 +1,39 @@
storage: './test-storage'
listen: 'http://localhost:1443/'
auth:
htpasswd:
file: ./htpasswd
# Maximum amount of users allowed to register, defaults to "+inf".
# You can set this to -1 to disable registration.
max_users: 1000
# a list of other known repositories we can talk to
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'@*/*':
# scoped packages
access: $all
publish: $authenticated
'*':
# allow all users (including non-authenticated users) to read and
# publish all packages
#
# you can specify usernames/groupnames (depending on your auth plugin)
# and three keywords: "$all", "$anonymous", "$authenticated"
access: $all
# allow all known users to publish packages
# (anyone can register by default, remember?)
publish: $authenticated
# if package is not available locally, proxy requests to 'npmjs' registry
proxy: npmjs
# log settings
logs:
- {type: stdout, format: pretty, level: http}
#- {type: file, path: verdaccio.log, level: info}

@ -0,0 +1,2 @@
test:$6FrCaT/v0dwE:autocreated 2018-01-17T03:40:22.958Z
username:$66to3JK5RgZM:autocreated 2018-01-17T03:40:46.315Z

@ -0,0 +1,50 @@
export default class Config {
constructor() {
this.storage = './test-storage';
this.listen = 'http://localhost:1443/';
this.auth = {
htpasswd: {
file: './htpasswd',
max_users: 1000,
},
};
this.uplinks = {
npmjs: {
url: 'https://registry.npmjs.org',
cache: true,
},
};
this.packages = {
'@*/*': {
access: ['$all'],
publish: ['$authenticated'],
proxy: [],
},
'*': {
access: ['$all'],
publish: ['$authenticated'],
proxy: ['npmjs'],
},
'**': {
access: [],
publish: [],
proxy: [],
},
};
this.logs = [
{
type: 'stdout',
format: 'pretty',
level: 35,
},
];
this.self_path = './tests/__fixtures__/config.yaml';
this.https = {
enable: false,
};
this.user_agent = 'verdaccio/3.0.0-alpha.7';
this.users = {};
this.server_id = '5cf430af30a1';
this.secret = 'ebde3e3a2a789a0623bf3de58cd127f0b309f573686cc91dc6d0f8fc6214b542';
}
}

@ -0,0 +1 @@
export default class Logger {}

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`addUserToHTPasswd - crypt3 should add new htpasswd to the end 1`] = `
"username:$66to3JK5RgZM:autocreated 2018-01-14T11:17:40.712Z
"
`;
exports[`addUserToHTPasswd - crypt3 should add new htpasswd to the end in multiline input 1`] = `
"test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z
username:$66to3JK5RgZM:autocreated 2018-01-14T11:17:40.712Z
"
`;
exports[`addUserToHTPasswd - crypt3 should throw an error for incorrect username with space 1`] = `"username should not contain non-uri-safe characters"`;
exports[`changePasswordToHTPasswd should change the password 1`] = `"root:$6JaJqI5HUf.Q:autocreated 2018-08-20T13:38:12.164Z"`;

@ -0,0 +1,31 @@
import { createSalt } from '../src/crypt3';
jest.mock('crypto', () => {
return {
randomBytes: (): { toString: () => string } => {
return {
toString: (): string => '/UEGzD0RxSNDZA==',
};
},
};
});
describe('createSalt', () => {
test('should match with the correct salt type', () => {
expect(createSalt('crypt')).toEqual('/UEGzD0RxSNDZA==');
expect(createSalt('md5')).toEqual('$1$/UEGzD0RxSNDZA==');
expect(createSalt('blowfish')).toEqual('$2a$/UEGzD0RxSNDZA==');
expect(createSalt('sha256')).toEqual('$5$/UEGzD0RxSNDZA==');
expect(createSalt('sha512')).toEqual('$6$/UEGzD0RxSNDZA==');
});
test('should fails on unkwon type', () => {
expect(function () {
createSalt('bad');
}).toThrow(/Unknown salt type at crypt3.createSalt: bad/);
});
test('should generate legacy crypt salt by default', () => {
expect(createSalt()).toEqual(createSalt('crypt'));
});
});

@ -0,0 +1,285 @@
/* eslint-disable jest/no-mocks-import */
import crypto from 'crypto';
// @ts-ignore
import fs from 'fs';
import HTPasswd, { VerdaccioConfigApp } from '../src/htpasswd';
// FIXME: remove this mocks imports
import Logger from './__mocks__/Logger';
import Config from './__mocks__/Config';
const stuff = {
logger: new Logger(),
config: new Config(),
};
const config = {
file: './htpasswd',
max_users: 1000,
};
describe('HTPasswd', () => {
let wrapper;
beforeEach(() => {
wrapper = new HTPasswd(config, (stuff as unknown) as VerdaccioConfigApp);
jest.resetModules();
crypto.randomBytes = jest.fn(() => {
return {
toString: (): string => '$6',
};
});
});
describe('constructor()', () => {
test('should files whether file path does not exist', () => {
expect(function () {
new HTPasswd({}, ({
config: {},
} as unknown) as VerdaccioConfigApp);
}).toThrow(/should specify "file" in config/);
});
});
describe('authenticate()', () => {
test('it should authenticate user with given credentials', (done) => {
const callbackTest = (a, b): void => {
expect(a).toBeNull();
expect(b).toContain('test');
done();
};
const callbackUsername = (a, b): void => {
expect(a).toBeNull();
expect(b).toContain('username');
done();
};
wrapper.authenticate('test', 'test', callbackTest);
wrapper.authenticate('username', 'password', callbackUsername);
});
test('it should not authenticate user with given credentials', (done) => {
const callback = (a, b): void => {
expect(a).toBeNull();
expect(b).toBeFalsy();
done();
};
wrapper.authenticate('test', 'somerandompassword', callback);
});
});
describe('addUser()', () => {
test('it should not pass sanity check', (done) => {
const callback = (a): void => {
expect(a.message).toEqual('unauthorized access');
done();
};
wrapper.adduser('test', 'somerandompassword', callback);
});
test('it should add the user', (done) => {
let dataToWrite;
// @ts-ignore
fs.writeFile = jest.fn((name, data, callback) => {
dataToWrite = data;
callback();
});
const callback = (a, b): void => {
expect(a).toBeNull();
expect(b).toBeTruthy();
expect(fs.writeFile).toHaveBeenCalled();
expect(dataToWrite.indexOf('usernotpresent')).not.toEqual(-1);
done();
};
wrapper.adduser('usernotpresent', 'somerandompassword', callback);
});
describe('addUser() error handling', () => {
test('sanityCheck should return an Error', (done) => {
jest.doMock('../src/utils.ts', () => {
return {
sanityCheck: (): Error => Error('some error'),
};
});
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.adduser('sanityCheck', 'test', (sanity) => {
expect(sanity.message).toBeDefined();
expect(sanity.message).toMatch('some error');
done();
});
});
test('lockAndRead should return an Error', (done) => {
jest.doMock('../src/utils.ts', () => {
return {
sanityCheck: (): any => null,
lockAndRead: (_a, b): any => b(new Error('lock error')),
};
});
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.adduser('lockAndRead', 'test', (sanity) => {
expect(sanity.message).toBeDefined();
expect(sanity.message).toMatch('lock error');
done();
});
});
test('addUserToHTPasswd should return an Error', (done) => {
jest.doMock('../src/utils.ts', () => {
return {
sanityCheck: (): any => null,
parseHTPasswd: (): void => {},
lockAndRead: (_a, b): any => b(null, ''),
unlockFile: (_a, b): any => b(),
};
});
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.adduser('addUserToHTPasswd', 'test', () => {
done();
});
});
test('writeFile should return an Error', (done) => {
jest.doMock('../src/utils.ts', () => {
return {
sanityCheck: (): any => null,
parseHTPasswd: (): void => {},
lockAndRead: (_a, b): any => b(null, ''),
addUserToHTPasswd: (): void => {},
};
});
jest.doMock('fs', () => {
const original = jest.requireActual('fs');
return {
...original,
writeFile: jest.fn((_name, _data, callback) => {
callback(new Error('write error'));
}),
};
});
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.adduser('addUserToHTPasswd', 'test', (err) => {
expect(err).not.toBeNull();
expect(err.message).toMatch('write error');
done();
});
});
});
describe('reload()', () => {
test('it should read the file and set the users', (done) => {
const output = { test: '$6FrCaT/v0dwE', username: '$66to3JK5RgZM' };
const callback = (): void => {
expect(wrapper.users).toEqual(output);
done();
};
wrapper.reload(callback);
});
test('reload should fails on check file', (done) => {
jest.doMock('fs', () => {
return {
stat: (_name, callback): void => {
callback(new Error('stat error'), null);
},
};
});
const callback = (err): void => {
expect(err).not.toBeNull();
expect(err.message).toMatch('stat error');
done();
};
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.reload(callback);
});
test('reload times match', (done) => {
jest.doMock('fs', () => {
return {
stat: (_name, callback): void => {
callback(null, {
mtime: null,
});
},
};
});
const callback = (err): void => {
expect(err).toBeUndefined();
done();
};
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.reload(callback);
});
test('reload should fails on read file', (done) => {
jest.doMock('fs', () => {
return {
stat: jest.requireActual('fs').stat,
readFile: (_name, _format, callback): void => {
callback(new Error('read error'), null);
},
};
});
const callback = (err): void => {
expect(err).not.toBeNull();
expect(err.message).toMatch('read error');
done();
};
const HTPasswd = require('../src/htpasswd.ts').default;
const wrapper = new HTPasswd(config, stuff);
wrapper.reload(callback);
});
});
});
test('changePassword - it should throw an error for user not found', (done) => {
const callback = (error, isSuccess): void => {
expect(error).not.toBeNull();
expect(error.message).toBe('User not found');
expect(isSuccess).toBeFalsy();
done();
};
wrapper.changePassword('usernotpresent', 'oldPassword', 'newPassword', callback);
});
test('changePassword - it should throw an error for wrong password', (done) => {
const callback = (error, isSuccess): void => {
expect(error).not.toBeNull();
expect(error.message).toBe('Invalid old Password');
expect(isSuccess).toBeFalsy();
done();
};
wrapper.changePassword('username', 'wrongPassword', 'newPassword', callback);
});
test('changePassword - it should change password', (done) => {
let dataToWrite;
// @ts-ignore
fs.writeFile = jest.fn((_name, data, callback) => {
dataToWrite = data;
callback();
});
const callback = (error, isSuccess): void => {
expect(error).toBeNull();
expect(isSuccess).toBeTruthy();
expect(fs.writeFile).toHaveBeenCalled();
expect(dataToWrite.indexOf('username')).not.toEqual(-1);
done();
};
wrapper.changePassword('username', 'password', 'newPassword', callback);
});
});

@ -0,0 +1,252 @@
import crypto from 'crypto';
import { verifyPassword, lockAndRead, parseHTPasswd, addUserToHTPasswd, sanityCheck, changePasswordToHTPasswd, getCryptoPassword } from '../src/utils';
const mockReadFile = jest.fn();
const mockUnlockFile = jest.fn();
jest.mock('@verdaccio/file-locking', () => ({
readFile: () => mockReadFile(),
unlockFile: () => mockUnlockFile(),
}));
describe('parseHTPasswd', () => {
it('should parse the password for a single line', () => {
const input = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z';
const output = { test: '$6b9MlB3WUELU' };
expect(parseHTPasswd(input)).toEqual(output);
});
it('should parse the password for two lines', () => {
const input = `user1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
user2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`;
const output = { user1: '$6b9MlB3WUELU', user2: '$6FrCaT/v0dwE' };
expect(parseHTPasswd(input)).toEqual(output);
});
it('should parse the password for multiple lines', () => {
const input = `user1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
user2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z
user3:$6FrCdfd\v0dwE:autocreated 2017-12-14T13:30:20.838Z
user4:$6FrCasdvppdwE:autocreated 2017-12-14T13:30:20.838Z`;
const output = {
user1: '$6b9MlB3WUELU',
user2: '$6FrCaT/v0dwE',
user3: '$6FrCdfd\v0dwE',
user4: '$6FrCasdvppdwE',
};
expect(parseHTPasswd(input)).toEqual(output);
});
});
describe('verifyPassword', () => {
it('should verify the MD5/Crypt3 password with true', () => {
const input = ['test', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0'];
expect(verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the MD5/Crypt3 password with false', () => {
const input = ['testpasswordchanged', '$apr1$sKXK9.lG$rZ4Iy63Vtn8jF9/USc4BV0'];
expect(verifyPassword(input[0], input[1])).toBeFalsy();
});
it('should verify the plain password with true', () => {
const input = ['testpasswordchanged', '{PLAIN}testpasswordchanged'];
expect(verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the plain password with false', () => {
const input = ['testpassword', '{PLAIN}testpasswordchanged'];
expect(verifyPassword(input[0], input[1])).toBeFalsy();
});
it('should verify the crypto SHA password with true', () => {
const input = ['testpassword', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0='];
expect(verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the crypto SHA password with false', () => {
const input = ['testpasswordchanged', '{SHA}i7YRj4/Wk1rQh2o740pxfTJwj/0='];
expect(verifyPassword(input[0], input[1])).toBeFalsy();
});
it('should verify the bcrypt password with true', () => {
const input = ['testpassword', '$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO'];
expect(verifyPassword(input[0], input[1])).toBeTruthy();
});
it('should verify the bcrypt password with false', () => {
const input = ['testpasswordchanged', '$2y$04$Wqed4yN0OktGbiUdxSTwtOva1xfESfkNIZfcS9/vmHLsn3.lkFxJO'];
expect(verifyPassword(input[0], input[1])).toBeFalsy();
});
});
describe('addUserToHTPasswd - crypt3', () => {
beforeAll(() => {
// @ts-ignore
global.Date = jest.fn(() => {
return {
parse: jest.fn(),
toJSON: (): string => '2018-01-14T11:17:40.712Z',
};
});
crypto.randomBytes = jest.fn(() => {
return {
toString: (): string => '$6',
};
});
});
it('should add new htpasswd to the end', () => {
const input = ['', 'username', 'password'];
expect(addUserToHTPasswd(input[0], input[1], input[2])).toMatchSnapshot();
});
it('should add new htpasswd to the end in multiline input', () => {
const body = `test1:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z
test2:$6FrCaT/v0dwE:autocreated 2017-12-14T13:30:20.838Z`;
const input = [body, 'username', 'password'];
expect(addUserToHTPasswd(input[0], input[1], input[2])).toMatchSnapshot();
});
it('should throw an error for incorrect username with space', () => {
const [a, b, c] = ['', 'firstname lastname', 'password'];
expect(() => addUserToHTPasswd(a, b, c)).toThrowErrorMatchingSnapshot();
});
});
// ToDo: mock crypt3 with false
describe('addUserToHTPasswd - crypto', () => {
it('should create password with crypto', () => {
jest.resetModules();
jest.doMock('../src/crypt3.ts', () => false);
const input = ['', 'username', 'password'];
const utils = require('../src/utils.ts');
expect(utils.addUserToHTPasswd(input[0], input[1], input[2])).toEqual('username:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=:autocreated 2018-01-14T11:17:40.712Z\n');
});
});
describe('lockAndRead', () => {
it('should call the readFile method', () => {
// console.log(fileLocking);
// const spy = jest.spyOn(fileLocking, 'readFile');
const cb = (): void => {};
lockAndRead('.htpasswd', cb);
expect(mockReadFile).toHaveBeenCalled();
});
});
describe('sanityCheck', () => {
let users;
beforeEach(() => {
users = { test: '$6FrCaT/v0dwE' };
});
test('should throw error for user already exists', () => {
const verifyFn = jest.fn();
const input = sanityCheck('test', users.test, verifyFn, users, Infinity);
expect(input.status).toEqual(401);
expect(input.message).toEqual('unauthorized access');
expect(verifyFn).toHaveBeenCalled();
});
test('should throw error for registration disabled of users', () => {
const verifyFn = (): void => {};
const input = sanityCheck('username', users.test, verifyFn, users, -1);
expect(input.status).toEqual(409);
expect(input.message).toEqual('user registration disabled');
});
test('should throw error max number of users', () => {
const verifyFn = (): void => {};
const input = sanityCheck('username', users.test, verifyFn, users, 1);
expect(input.status).toEqual(403);
expect(input.message).toEqual('maximum amount of users reached');
});
test('should not throw anything and sanity check', () => {
const verifyFn = (): void => {};
const input = sanityCheck('username', users.test, verifyFn, users, 2);
expect(input).toBeNull();
});
test('should throw error for required username field', () => {
const verifyFn = (): void => {};
const input = sanityCheck(undefined, users.test, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400);
});
test('should throw error for required password field', () => {
const verifyFn = (): void => {};
const input = sanityCheck('username', undefined, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400);
});
test('should throw error for required username & password fields', () => {
const verifyFn = (): void => {};
const input = sanityCheck(undefined, undefined, verifyFn, users, 2);
expect(input.message).toEqual('username and password is required');
expect(input.status).toEqual(400);
});
test('should throw error for existing username and password', () => {
const verifyFn = jest.fn(() => true);
const input = sanityCheck('test', users.test, verifyFn, users, 2);
expect(input.status).toEqual(409);
expect(input.message).toEqual('username is already registered');
expect(verifyFn).toHaveBeenCalledTimes(1);
});
test('should throw error for existing username and password with max number of users reached', () => {
const verifyFn = jest.fn(() => true);
const input = sanityCheck('test', users.test, verifyFn, users, 1);
expect(input.status).toEqual(409);
expect(input.message).toEqual('username is already registered');
expect(verifyFn).toHaveBeenCalledTimes(1);
});
});
describe('changePasswordToHTPasswd', () => {
test('should throw error for wrong password', () => {
const body = 'test:$6b9MlB3WUELU:autocreated 2017-11-06T18:17:21.957Z';
try {
changePasswordToHTPasswd(body, 'test', 'somerandompassword', 'newPassword');
} catch (error) {
expect(error.message).toEqual('Invalid old Password');
}
});
test('should change the password', () => {
const body = 'root:$6qLTHoPfGLy2:autocreated 2018-08-20T13:38:12.164Z';
expect(changePasswordToHTPasswd(body, 'root', 'demo123', 'newPassword')).toMatchSnapshot();
});
test('should generate a different result on salt change', () => {
crypto.randomBytes = jest.fn(() => {
return {
toString: (): string => 'AB',
};
});
const body = 'root:$6qLTHoPfGLy2:autocreated 2018-08-20T13:38:12.164Z';
expect(changePasswordToHTPasswd(body, 'root', 'demo123', 'demo123')).toEqual('root:ABfaAAjDKIgfw:autocreated 2018-08-20T13:38:12.164Z');
});
test('should change the password when crypt3 is not available', () => {
jest.resetModules();
jest.doMock('../src/crypt3.ts', () => false);
const utils = require('../src/utils.ts');
const body = 'username:{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=:autocreated 2018-01-14T11:17:40.712Z';
expect(utils.changePasswordToHTPasswd(body, 'username', 'password', 'newPassword')).toEqual(
'username:{SHA}KD1HqTOO0RALX+Klr/LR98eZv9A=:autocreated 2018-01-14T11:17:40.712Z'
);
});
});
describe('getCryptoPassword', () => {
test('should return the password hash', () => {
const passwordHash = `{SHA}y9vkk2zovmMYTZ8uE/wkkjQ3G5o=`;
expect(getCryptoPassword('demo123')).toBe(passwordHash);
});
});

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts"]
}

@ -0,0 +1,3 @@
{
"extends": "../../../.babelrc"
}

@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/no-use-before-define": "off"
}
}

@ -0,0 +1,467 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.7.2](https://github.com/verdaccio/monorepo/compare/v9.7.1...v9.7.2) (2020-07-20)
**Note:** Version bump only for package @verdaccio/local-storage
## [9.7.1](https://github.com/verdaccio/monorepo/compare/v9.7.0...v9.7.1) (2020-07-10)
### Bug Fixes
* update dependencies ([#375](https://github.com/verdaccio/monorepo/issues/375)) ([1e7aeec](https://github.com/verdaccio/monorepo/commit/1e7aeec31b056979285e272793a95b8c75d57c77))
# [9.7.0](https://github.com/verdaccio/monorepo/compare/v9.6.1...v9.7.0) (2020-06-24)
**Note:** Version bump only for package @verdaccio/local-storage
## [9.6.1](https://github.com/verdaccio/monorepo/compare/v9.6.0...v9.6.1) (2020-06-07)
**Note:** Version bump only for package @verdaccio/local-storage
## [9.5.1](https://github.com/verdaccio/monorepo/compare/v9.5.0...v9.5.1) (2020-06-03)
### Bug Fixes
* restore Node v8 support ([#361](https://github.com/verdaccio/monorepo/issues/361)) ([9be55a1](https://github.com/verdaccio/monorepo/commit/9be55a1deebe954e8eef9edc59af9fd16e29daed))
# [9.5.0](https://github.com/verdaccio/monorepo/compare/v9.4.1...v9.5.0) (2020-05-02)
**Note:** Version bump only for package @verdaccio/local-storage
# [9.4.0](https://github.com/verdaccio/monorepo/compare/v9.3.4...v9.4.0) (2020-03-21)
**Note:** Version bump only for package @verdaccio/local-storage
## [9.3.4](https://github.com/verdaccio/monorepo/compare/v9.3.3...v9.3.4) (2020-03-11)
### Bug Fixes
* update mkdirp@1.0.3 ([#341](https://github.com/verdaccio/monorepo/issues/341)) ([96db337](https://github.com/verdaccio/monorepo/commit/96db3378a4f2334ec89cfb113af95e9a3a6eb050))
## [9.3.2](https://github.com/verdaccio/monorepo/compare/v9.3.1...v9.3.2) (2020-03-08)
### Bug Fixes
* update dependencies ([#332](https://github.com/verdaccio/monorepo/issues/332)) ([b6165ae](https://github.com/verdaccio/monorepo/commit/b6165aea9b7e4012477081eae68bfa7159c58f56))
## [9.3.1](https://github.com/verdaccio/monorepo/compare/v9.3.0...v9.3.1) (2020-02-23)
**Note:** Version bump only for package @verdaccio/local-storage
# [9.3.0](https://github.com/verdaccio/monorepo/compare/v9.2.0...v9.3.0) (2020-01-29)
**Note:** Version bump only for package @verdaccio/local-storage
# [9.0.0](https://github.com/verdaccio/monorepo/compare/v8.5.3...v9.0.0) (2020-01-07)
### Bug Fixes
* prevent circular structure exception ([#312](https://github.com/verdaccio/monorepo/issues/312)) ([f565461](https://github.com/verdaccio/monorepo/commit/f565461f5bb2873467eeb4372a12fbf4a4974d17))
## [8.5.2](https://github.com/verdaccio/monorepo/compare/v8.5.1...v8.5.2) (2019-12-25)
**Note:** Version bump only for package @verdaccio/local-storage
## [8.5.1](https://github.com/verdaccio/monorepo/compare/v8.5.0...v8.5.1) (2019-12-24)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.5.0](https://github.com/verdaccio/monorepo/compare/v8.4.2...v8.5.0) (2019-12-22)
**Note:** Version bump only for package @verdaccio/local-storage
## [8.4.2](https://github.com/verdaccio/monorepo/compare/v8.4.1...v8.4.2) (2019-11-23)
**Note:** Version bump only for package @verdaccio/local-storage
## [8.4.1](https://github.com/verdaccio/monorepo/compare/v8.4.0...v8.4.1) (2019-11-22)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.4.0](https://github.com/verdaccio/monorepo/compare/v8.3.0...v8.4.0) (2019-11-22)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.3.0](https://github.com/verdaccio/monorepo/compare/v8.2.0...v8.3.0) (2019-10-27)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.2.0](https://github.com/verdaccio/monorepo/compare/v8.2.0-next.0...v8.2.0) (2019-10-23)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.2.0-next.0](https://github.com/verdaccio/monorepo/compare/v8.1.4...v8.2.0-next.0) (2019-10-08)
### Bug Fixes
* fixed lint errors ([5e677f7](https://github.com/verdaccio/monorepo/commit/5e677f7))
## [8.1.2](https://github.com/verdaccio/monorepo/compare/v8.1.1...v8.1.2) (2019-09-29)
**Note:** Version bump only for package @verdaccio/local-storage
## [8.1.1](https://github.com/verdaccio/monorepo/compare/v8.1.0...v8.1.1) (2019-09-26)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.1.0](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.1...v8.1.0) (2019-09-07)
**Note:** Version bump only for package @verdaccio/local-storage
## [8.0.1-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.0...v8.0.1-next.1) (2019-08-29)
**Note:** Version bump only for package @verdaccio/local-storage
## [8.0.1-next.0](https://github.com/verdaccio/monorepo/compare/v8.0.0...v8.0.1-next.0) (2019-08-29)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.0.0](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.4...v8.0.0) (2019-08-22)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.0.0-next.4](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.3...v8.0.0-next.4) (2019-08-18)
**Note:** Version bump only for package @verdaccio/local-storage
# [8.0.0-next.3](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.2...v8.0.0-next.3) (2019-08-16)
### Bug Fixes
* restore closure ([32b9d7e](https://github.com/verdaccio/monorepo/commit/32b9d7e))
* **build:** error on types for fs callback ([cc35acb](https://github.com/verdaccio/monorepo/commit/cc35acb))
* Add DATE and VERSION in search result ([e352b75](https://github.com/verdaccio/monorepo/commit/e352b75))
* avoid open write stream if resource exist [#1191](https://github.com/verdaccio/monorepo/issues/1191) ([f041d3f](https://github.com/verdaccio/monorepo/commit/f041d3f))
* bug fixing integration ([6c75ac8](https://github.com/verdaccio/monorepo/commit/6c75ac8))
* build before publish ([cd6c7ff](https://github.com/verdaccio/monorepo/commit/cd6c7ff))
* check whether path exist before return result ([a4d2af1](https://github.com/verdaccio/monorepo/commit/a4d2af1))
* flow issues ([f42a284](https://github.com/verdaccio/monorepo/commit/f42a284))
* ignore flow on this one, we need it ([c8e0b2b](https://github.com/verdaccio/monorepo/commit/c8e0b2b))
* local storage requires package.json file for read, save and create all the time ([33c847b](https://github.com/verdaccio/monorepo/commit/33c847b))
* migration from main repository merge [#306](https://github.com/verdaccio/monorepo/issues/306) ([8fbe86e](https://github.com/verdaccio/monorepo/commit/8fbe86e))
* missing callback ([abfc422](https://github.com/verdaccio/monorepo/commit/abfc422))
* missing error code ([7121939](https://github.com/verdaccio/monorepo/commit/7121939))
* move to local storage the fs location handler ([3b12083](https://github.com/verdaccio/monorepo/commit/3b12083))
* mtimeMs is not backward compatible ([c6f74eb](https://github.com/verdaccio/monorepo/commit/c6f74eb))
* remove temp file whether is emtpy and fails ([655102f](https://github.com/verdaccio/monorepo/commit/655102f))
* remove uncessary async ([3e3e3a6](https://github.com/verdaccio/monorepo/commit/3e3e3a6))
* remove unused parameters ([554e301](https://github.com/verdaccio/monorepo/commit/554e301))
* restore build path ([4902042](https://github.com/verdaccio/monorepo/commit/4902042))
* return time as milliseconds ([15467ba](https://github.com/verdaccio/monorepo/commit/15467ba))
* sync after set secret ([2abae4f](https://github.com/verdaccio/monorepo/commit/2abae4f))
* temp files are written into the storage ([89a1dc8](https://github.com/verdaccio/monorepo/commit/89a1dc8))
* unit test ([995a27c](https://github.com/verdaccio/monorepo/commit/995a27c))
* update @verdaccio/file-locking@1.0.0 ([9bd36f0](https://github.com/verdaccio/monorepo/commit/9bd36f0))
* update lodash types ([184466c](https://github.com/verdaccio/monorepo/commit/184466c))
### Features
* token support with level.js ([#168](https://github.com/verdaccio/monorepo/issues/168)) ([ca877ff](https://github.com/verdaccio/monorepo/commit/ca877ff))
* **build:** standardize build ([33fe090](https://github.com/verdaccio/monorepo/commit/33fe090))
* change new db name to verdaccio ([#83](https://github.com/verdaccio/monorepo/issues/83)) ([edfca9f](https://github.com/verdaccio/monorepo/commit/edfca9f))
* drop node v6 support ([664f288](https://github.com/verdaccio/monorepo/commit/664f288))
* implement search ([2e2bb32](https://github.com/verdaccio/monorepo/commit/2e2bb32))
* migrate to typescript ([c439d25](https://github.com/verdaccio/monorepo/commit/c439d25))
* update database method with callbacks ([ef202a9](https://github.com/verdaccio/monorepo/commit/ef202a9))
* update minor dependencies ([007b026](https://github.com/verdaccio/monorepo/commit/007b026))
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [2.3.0](https://github.com/verdaccio/local-storage/compare/v2.2.1...v2.3.0) (2019-08-13)
### Bug Fixes
* restore closure ([8ec27f2](https://github.com/verdaccio/local-storage/commit/8ec27f2))
### Features
* add logging for each method ([3c915c7](https://github.com/verdaccio/local-storage/commit/3c915c7))
* token support with level.js ([#168](https://github.com/verdaccio/local-storage/issues/168)) ([16727bd](https://github.com/verdaccio/local-storage/commit/16727bd))
### [2.2.1](https://github.com/verdaccio/local-storage/compare/v2.2.0...v2.2.1) (2019-06-30)
### Bug Fixes
* **build:** error on types for fs callback ([774d808](https://github.com/verdaccio/local-storage/commit/774d808))
## [2.2.0](https://github.com/verdaccio/local-storage/compare/v2.1.0...v2.2.0) (2019-06-30)
### Features
* **build:** standardize build ([eba832e](https://github.com/verdaccio/local-storage/commit/eba832e))
# [2.1.0](https://github.com/verdaccio/local-storage/compare/v2.0.0...v2.1.0) (2019-03-29)
### Bug Fixes
* remove uncessary async ([23a09f3](https://github.com/verdaccio/local-storage/commit/23a09f3))
### Features
* drop node v6 support ([ef548e0](https://github.com/verdaccio/local-storage/commit/ef548e0))
# [2.0.0](https://github.com/verdaccio/local-storage/compare/v2.0.0-beta.3...v2.0.0) (2019-03-29)
<a name="2.0.0-beta.3"></a>
# [2.0.0-beta.3](https://github.com/verdaccio/local-storage/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2019-02-24)
### Bug Fixes
* update [@verdaccio](https://github.com/verdaccio)/file-locking@1.0.0 ([587245d](https://github.com/verdaccio/local-storage/commit/587245d))
<a name="2.0.0-beta.2"></a>
# [2.0.0-beta.2](https://github.com/verdaccio/local-storage/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2019-02-24)
### Bug Fixes
* avoid open write stream if resource exist [#1191](https://github.com/verdaccio/local-storage/issues/1191) ([b13904a](https://github.com/verdaccio/local-storage/commit/b13904a))
* package.json to reduce vulnerabilities ([97e9dc3](https://github.com/verdaccio/local-storage/commit/97e9dc3))
<a name="2.0.0-beta.1"></a>
# [2.0.0-beta.1](https://github.com/verdaccio/local-storage/compare/v2.0.0-beta.0...v2.0.0-beta.1) (2019-02-03)
<a name="2.0.0-beta.0"></a>
# [2.0.0-beta.0](https://github.com/verdaccio/local-storage/compare/v1.2.0...v2.0.0-beta.0) (2019-02-01)
### Bug Fixes
* **deps:** update dependency lodash to v4.17.11 ([682616a](https://github.com/verdaccio/local-storage/commit/682616a))
### Features
* custom storage location ([b1423cd](https://github.com/verdaccio/local-storage/commit/b1423cd))
* migrate to typescript ([fe8344b](https://github.com/verdaccio/local-storage/commit/fe8344b))
### BREAKING CHANGES
* we change from boolean value to string within the config file
<a name="1.2.0"></a>
# [1.2.0](https://github.com/verdaccio/local-storage/compare/v1.1.3...v1.2.0) (2018-08-25)
### Features
* change new db name to verdaccio ([#83](https://github.com/verdaccio/local-storage/issues/83)) ([143977d](https://github.com/verdaccio/local-storage/commit/143977d))
<a name="1.1.3"></a>
## [1.1.3](https://github.com/verdaccio/local-storage/compare/v1.1.2...v1.1.3) (2018-07-15)
### Bug Fixes
* remove unused parameters ([3ce374a](https://github.com/verdaccio/local-storage/commit/3ce374a))
<a name="1.1.2"></a>
## [1.1.2](https://github.com/verdaccio/local-storage/compare/v1.1.1...v1.1.2) (2018-06-09)
### Bug Fixes
* return time as milliseconds ([c98be85](https://github.com/verdaccio/local-storage/commit/c98be85))
<a name="1.1.1"></a>
## [1.1.1](https://github.com/verdaccio/local-storage/compare/v1.1.0...v1.1.1) (2018-06-08)
### Bug Fixes
* check whether path exist before return result ([cb5d4ef](https://github.com/verdaccio/local-storage/commit/cb5d4ef))
<a name="1.1.0"></a>
# [1.1.0](https://github.com/verdaccio/local-storage/compare/v1.0.3...v1.1.0) (2018-06-08)
### Bug Fixes
* **deps:** update dependency async to v2.6.1 ([487b095](https://github.com/verdaccio/local-storage/commit/487b095))
### Features
* implement search ([f884a24](https://github.com/verdaccio/local-storage/commit/f884a24))
<a name="0.2.0"></a>
# [0.2.0](https://github.com/verdaccio/local-storage/compare/v0.1.4...v0.2.0) (2018-01-17)
### Features
* update minor dependencies ([92daa81](https://github.com/verdaccio/local-storage/commit/92daa81))
<a name="0.1.4"></a>
## [0.1.4](https://github.com/verdaccio/local-storage/compare/v0.1.3...v0.1.4) (2018-01-17)
### Bug Fixes
* remove temp file whether is emtpy and fails ([593e162](https://github.com/verdaccio/local-storage/commit/593e162))
* unit test ([2573f30](https://github.com/verdaccio/local-storage/commit/2573f30))

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Verdaccio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,49 @@
# @verdaccio/local-storage
📦 File system storage plugin for verdaccio
[![verdaccio (latest)](https://img.shields.io/npm/v/@verdaccio/local-storage/latest.svg)](https://www.npmjs.com/package/@verdaccio/local-storage)
[![CircleCI](https://circleci.com/gh/verdaccio/local-storage/tree/master.svg?style=svg)](https://circleci.com/gh/verdaccio/local-storage/tree/master)
[![Known Vulnerabilities](https://snyk.io/test/github/verdaccio/local-storage/badge.svg?targetFile=package.json)](https://snyk.io/test/github/verdaccio/local-storage?targetFile=package.json)
[![codecov](https://codecov.io/gh/verdaccio/local-storage/branch/master/graph/badge.svg)](https://codecov.io/gh/verdaccio/local-storage)
[![backers](https://opencollective.com/verdaccio/tiers/backer/badge.svg?label=Backer&color=brightgreen)](https://opencollective.com/verdaccio)
[![discord](https://img.shields.io/discord/388674437219745793.svg)](http://chat.verdaccio.org/)
![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)
[![node](https://img.shields.io/node/v/@verdaccio/local-storage/latest.svg)](https://www.npmjs.com/package/@verdaccio/local-storage)
> This package is already built-in in verdaccio
```
npm install @verdaccio/local-storage
```
### API
### LocalDatabase
The main object that handle a JSON database the private packages.
#### Constructor
```
new LocalDatabase(config, logger);
```
* **config**: A verdaccio configuration instance.
* **logger**: A logger instance
### LocalFS
A class that handle an package instance in the File System
```
new LocalFS(packageStoragePath, logger);
```
## License
Verdaccio is [MIT licensed](https://github.com/verdaccio/local-storage/blob/master/LICENSE).
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fverdaccio%2Flocal-storage.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fverdaccio%2Flocal-storage?ref=badge_large)

@ -0,0 +1 @@
{}

@ -0,0 +1,3 @@
const config = require('../../../jest/config');
module.exports = Object.assign({}, config, {});

@ -0,0 +1,60 @@
{
"name": "@verdaccio/local-storage",
"version": "10.0.0-beta",
"description": "Local storage implementation",
"keywords": [
"plugin",
"verdaccio",
"storage",
"local-storage"
],
"author": "Juan Picado <juanpicado19@gmail.com>",
"license": "MIT",
"homepage": "https://verdaccio.org",
"repository": {
"type": "git",
"url": "https://github.com/verdaccio/monorepo",
"directory": "plugins/local-storage"
},
"bugs": {
"url": "https://github.com/verdaccio/monorepo/issues"
},
"publishConfig": {
"access": "public"
},
"main": "./build/index.js",
"types": "./build/index.d.ts",
"files": [
"build/"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"@verdaccio/commons-api": "^9.7.1",
"@verdaccio/file-locking": "workspace:*",
"@verdaccio/streams": "workspace:*",
"async": "^3.2.0",
"level": "5.0.1",
"lodash": "^4.17.19",
"mkdirp": "^0.5.5"
},
"devDependencies": {
"@types/minimatch": "^3.0.3",
"@verdaccio/types": "workspace:*",
"minimatch": "^3.0.4",
"rmdir-sync": "^1.0.1"
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"type-check": "tsc --noEmit",
"build:types": "tsc --emitDeclarationOnly --declaration true",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"build": "pnpm run build:js && pnpm run build:types"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio"
}
}

@ -0,0 +1,5 @@
import LocalDatabase from './local-database';
export { LocalDatabase };
export default LocalDatabase;

@ -0,0 +1,434 @@
import fs from 'fs';
import Path from 'path';
import stream from 'stream';
import _ from 'lodash';
import async from 'async';
import mkdirp from 'mkdirp';
import { Callback, Config, IPackageStorage, IPluginStorage, LocalStorage, Logger, StorageList, Token, TokenFilter } from '@verdaccio/types';
import level from 'level';
import { getInternalError } from '@verdaccio/commons-api/lib';
import LocalDriver, { noSuchFile } from './local-fs';
import { loadPrivatePackages } from './pkg-utils';
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;
}
/**
* Handle local database.
*/
class LocalDatabase 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) {
this.config = config;
this.path = this._buildStoragePath(config);
this.logger = logger;
this.locked = false;
this.data = this._fetchLocalPackages();
this.logger.trace({ config: this.config }, '[local-storage]: configuration: @{config}');
this._sync();
}
public getSecret(): Promise<string> {
return Promise.resolve(this.data.secret);
}
public setSecret(secret: string): Promise<Error | null> {
return new Promise((resolve): void => {
this.data.secret = secret;
resolve(this._sync());
});
}
/**
* 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);
this.logger.debug({ name }, '[local-storage]: the private package @{name} has been added');
cb(this._sync());
} else {
cb(null);
}
}
public search(onPackage: Callback, onEnd: Callback, validateName: (name: string) => boolean): void {
const storages = this._getCustomPackageLocalStorages();
this.logger.trace(`local-storage: [search]: ${JSON.stringify(storages)}`);
const base = Path.dirname(this.config.self_path);
const self = this;
const storageKeys = Object.keys(storages);
this.logger.trace(`local-storage: [search] base: ${base} keys ${storageKeys}`);
async.eachSeries(
storageKeys,
function (storage, cb) {
const position = storageKeys.indexOf(storage);
const base2 = Path.join(position !== 0 ? storageKeys[0] : '');
const storagePath: string = Path.resolve(base, base2, storage);
self.logger.trace({ storagePath, storage }, 'local-storage: [search] search path: @{storagePath} : @{storage}');
fs.readdir(storagePath, (err, files) => {
if (err) {
return cb(err);
}
async.eachSeries(
files,
function (file, cb) {
self.logger.trace({ file }, 'local-storage: [search] search file path: @{file}');
if (storageKeys.includes(file)) {
return cb();
}
if (file.match(/^@/)) {
// scoped
const fileLocation = Path.resolve(base, storage, file);
self.logger.trace({ fileLocation }, 'local-storage: [search] search scoped file location: @{fileLocation}');
fs.readdir(fileLocation, function (err, files) {
if (err) {
return cb(err);
}
async.eachSeries(
files,
(file2, cb) => {
if (validateName(file2)) {
const packagePath = Path.resolve(base, storage, file, file2);
fs.stat(packagePath, (err, stats) => {
if (_.isNil(err) === false) {
return cb(err);
}
const item = {
name: `${file}/${file2}`,
path: packagePath,
time: stats.mtime.getTime(),
};
onPackage(item, cb);
});
} else {
cb();
}
},
cb
);
});
} else if (validateName(file)) {
const base2 = Path.join(position !== 0 ? storageKeys[0] : '');
const packagePath = Path.resolve(base, base2, storage, file);
self.logger.trace({ packagePath }, 'local-storage: [search] search file location: @{packagePath}');
fs.stat(packagePath, (err, stats) => {
if (_.isNil(err) === false) {
return cb(err);
}
onPackage(
{
name: file,
path: packagePath,
time: self._getTime(stats.mtime.getTime(), stats.mtime),
},
cb
);
});
} else {
cb();
}
},
cb
);
});
},
// @ts-ignore
onEnd
);
}
/**
* Remove an element from the database.
* @param {*} name
* @return {Error|*}
*/
public remove(name: string, cb: Callback): void {
this.get((err, data) => {
if (err) {
cb(getInternalError('error remove private package'));
this.logger.error({ err }, '[local-storage/remove]: remove the private package has failed @{err}');
}
const pkgName = data.indexOf(name);
if (pkgName !== -1) {
this.data.list.splice(pkgName, 1);
this.logger.trace({ name }, 'local-storage: [remove] package @{name} has been removed');
}
cb(this._sync());
});
}
/**
* Return all database elements.
* @return {Array}
*/
public get(cb: Callback): void {
const list = this.data.list;
const totalItems = this.data.list.length;
cb(null, list);
this.logger.trace({ totalItems }, 'local-storage: [get] full list of packages (@{totalItems}) has been fetched');
}
public getPackageStorage(packageName: string): IPackageStorage {
const packageAccess = this.config.getMatchedPackagesSpec(packageName);
const packagePath: string = this._getLocalStoragePath(packageAccess ? packageAccess.storage : undefined);
this.logger.trace({ packagePath }, '[local-storage/getPackageStorage]: storage selected: @{packagePath}');
if (_.isString(packagePath) === false) {
this.logger.debug({ name: packageName }, 'this package has no storage defined: @{name}');
return;
}
const packageStoragePath: string = Path.join(Path.resolve(Path.dirname(this.config.self_path || ''), packagePath), packageName);
this.logger.trace({ packageStoragePath }, '[local-storage/getPackageStorage]: storage path: @{packageStoragePath}');
return new LocalDriver(packageStoragePath, this.logger);
}
public clean(): void {
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 {
return time ? time : mtime;
}
private _getCustomPackageLocalStorages(): object {
const storages = {};
// add custom storage if exist
if (this.config.storage) {
storages[this.config.storage] = true;
}
const { packages } = this.config;
if (packages) {
const listPackagesConf = Object.keys(packages || {});
listPackagesConf.map((pkg) => {
const storage = packages[pkg].storage;
if (storage) {
storages[storage] = false;
}
});
}
return storages;
}
/**
* Syncronize {create} database whether does not exist.
* @return {Error|*}
*/
private _sync(): Error | null {
this.logger.debug('[local-storage/_sync]: init sync database');
if (this.locked) {
this.logger.error('Database is locked, please check error message printed during startup to prevent data loss.');
return new Error('Verdaccio database is locked, please contact your administrator to checkout logs during verdaccio startup.');
}
// Uses sync to prevent ugly race condition
try {
// https://www.npmjs.com/package/mkdirp#mkdirpsyncdir-opts
const folderName = Path.dirname(this.path);
mkdirp.sync(folderName);
this.logger.debug({ folderName }, '[local-storage/_sync]: folder @{folderName} created succeed');
} catch (err) {
// perhaps a logger instance?
this.logger.debug({ err }, '[local-storage/_sync/mkdirp.sync]: sync failed @{err}');
return null;
}
try {
fs.writeFileSync(this.path, JSON.stringify(this.data));
this.logger.debug('[local-storage/_sync/writeFileSync]: sync write succeed');
return null;
} catch (err) {
this.logger.debug({ err }, '[local-storage/_sync/writeFileSync]: sync failed @{err}');
return err;
}
}
/**
* Verify the right local storage location.
* @param {String} path
* @return {String}
* @private
*/
private _getLocalStoragePath(storage: string | void): string {
const globalConfigStorage = this.config ? this.config.storage : undefined;
if (_.isNil(globalConfigStorage)) {
throw new Error('global storage is required for this plugin');
} else {
if (_.isNil(storage) === false && _.isString(storage)) {
return Path.join(globalConfigStorage as string, storage as string);
}
return globalConfigStorage as string;
}
}
/**
* Build the local database path.
* @param {Object} config
* @return {string|String|*}
* @private
*/
private _buildStoragePath(config: Config): string {
const sinopiadbPath: string = this._dbGenPath(DEPRECATED_DB_NAME, config);
try {
fs.accessSync(sinopiadbPath, fs.constants.F_OK);
return sinopiadbPath;
} catch (err) {
if (err.code === noSuchFile) {
return this._dbGenPath(DB_NAME, config);
}
throw err;
}
}
private _dbGenPath(dbName: string, config: Config): string {
return Path.join(Path.resolve(Path.dirname(config.self_path || ''), config.storage as string, dbName));
}
/**
* Fetch local packages.
* @private
* @return {Object}
*/
private _fetchLocalPackages(): LocalStorage {
const list: StorageList = [];
const emptyDatabase = { list, secret: '' };
try {
const db = loadPrivatePackages(this.path, this.logger);
return db;
} catch (err) {
// readFileSync is platform specific, macOS, Linux and Windows thrown an error
// Only recreate if file not found to prevent data loss
if (err.code !== noSuchFile) {
this.locked = true;
this.logger.error('Failed to read package database file, please check the error printed below:\n', `File Path: ${this.path}\n\n ${err.message}`);
}
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;

@ -0,0 +1,384 @@
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
import mkdirp from 'mkdirp';
import { UploadTarball, ReadTarball } from '@verdaccio/streams';
import { unlockFile, readFile } from '@verdaccio/file-locking';
import { Callback, Logger, Package, ILocalPackageManager, IUploadTarball } from '@verdaccio/types';
import { getCode, getInternalError, getNotFound, VerdaccioError } from '@verdaccio/commons-api/lib';
export const fileExist = 'EEXISTS';
export const noSuchFile = 'ENOENT';
export const resourceNotAvailable = 'EAGAIN';
export const pkgFileName = 'package.json';
export const fSError = function (message: string, code = 409): VerdaccioError {
const err: VerdaccioError = getCode(code, message);
// FIXME: we should return http-status codes here instead, future improvement
// @ts-ignore
err.code = message;
return err;
};
const tempFile = function (str): string {
return `${str}.tmp${String(Math.random()).substr(2)}`;
};
const renameTmp = function (src, dst, _cb): void {
const cb = (err): void => {
if (err) {
fs.unlink(src, () => {});
}
_cb(err);
};
if (process.platform !== 'win32') {
return fs.rename(src, dst, cb);
}
// windows can't remove opened file,
// but it seem to be able to rename it
const tmp = tempFile(dst);
fs.rename(dst, tmp, function (err) {
fs.rename(src, dst, cb);
if (!err) {
fs.unlink(tmp, () => {});
}
});
};
export type ILocalFSPackageManager = ILocalPackageManager & { path: string };
export default class LocalFS implements ILocalFSPackageManager {
public path: string;
public logger: Logger;
public constructor(path: string, logger: Logger) {
this.path = path;
this.logger = logger;
}
/**
* This function allows to update the package thread-safely
Algorithm:
1. lock package.json for writing
2. read package.json
3. updateFn(pkg, cb), and wait for cb
4. write package.json.tmp
5. move package.json.tmp package.json
6. callback(err?)
* @param {*} name
* @param {*} updateHandler
* @param {*} onWrite
* @param {*} transformPackage
* @param {*} onEnd
*/
public updatePackage(name: string, updateHandler: Callback, onWrite: Callback, transformPackage: Function, onEnd: Callback): void {
this._lockAndReadJSON(pkgFileName, (err, json) => {
let locked = false;
const self = this;
// callback that cleans up lock first
const unLockCallback = function (lockError: Error): void {
// eslint-disable-next-line prefer-rest-params
const _args = arguments;
if (locked) {
self._unlockJSON(pkgFileName, () => {
// ignore any error from the unlock
if (lockError !== null) {
self.logger.trace(
{
name,
lockError,
},
'[local-storage/updatePackage/unLockCallback] file: @{name} lock has failed lockError: @{lockError}'
);
}
onEnd.apply(lockError, _args);
});
} else {
self.logger.trace({ name }, '[local-storage/updatePackage/unLockCallback] file: @{name} has been updated');
onEnd(..._args);
}
};
if (!err) {
locked = true;
this.logger.trace(
{
name,
},
'[local-storage/updatePackage] file: @{name} has been locked'
);
}
if (_.isNil(err) === false) {
if (err.code === resourceNotAvailable) {
return unLockCallback(getInternalError('resource temporarily unavailable'));
} else if (err.code === noSuchFile) {
return unLockCallback(getNotFound());
} else {
return unLockCallback(err);
}
}
updateHandler(json, (err) => {
if (err) {
return unLockCallback(err);
}
onWrite(name, transformPackage(json), unLockCallback);
});
});
}
public deletePackage(packageName: string, callback: (err: NodeJS.ErrnoException | null) => void): void {
this.logger.debug({ packageName }, '[local-storage/deletePackage] delete a package @{packageName}');
return fs.unlink(this._getStorage(packageName), callback);
}
public removePackage(callback: (err: NodeJS.ErrnoException | null) => void): void {
this.logger.debug({ packageName: this.path }, '[local-storage/removePackage] remove a package: @{packageName}');
fs.rmdir(this._getStorage('.'), callback);
}
public createPackage(name: string, value: Package, cb: Callback): void {
this.logger.debug({ packageName: name }, '[local-storage/createPackage] create a package: @{packageName}');
this._createFile(this._getStorage(pkgFileName), this._convertToString(value), cb);
}
public savePackage(name: string, value: Package, cb: Callback): void {
this.logger.debug({ packageName: name }, '[local-storage/savePackage] save a package: @{packageName}');
this._writeFile(this._getStorage(pkgFileName), this._convertToString(value), cb);
}
public readPackage(name: string, cb: Callback): void {
this.logger.debug({ packageName: name }, '[local-storage/readPackage] read a package: @{packageName}');
this._readStorageFile(this._getStorage(pkgFileName)).then(
(res) => {
try {
const data: any = JSON.parse(res.toString('utf8'));
this.logger.trace({ packageName: name }, '[local-storage/readPackage/_readStorageFile] read a package succeed: @{packageName}');
cb(null, data);
} catch (err) {
this.logger.trace({ err }, '[local-storage/readPackage/_readStorageFile] error on read a package: @{err}');
cb(err);
}
},
(err) => {
this.logger.trace({ err }, '[local-storage/readPackage/_readStorageFile] error on read a package: @{err}');
return cb(err);
}
);
}
public writeTarball(name: string): IUploadTarball {
const uploadStream = new UploadTarball({});
this.logger.debug({ packageName: name }, '[local-storage/writeTarball] write a tarball for package: @{packageName}');
let _ended = 0;
uploadStream.on('end', function () {
_ended = 1;
});
const pathName: string = this._getStorage(name);
fs.access(pathName, (fileNotFound) => {
const exists = !fileNotFound;
if (exists) {
uploadStream.emit('error', fSError(fileExist));
} else {
const temporalName = path.join(this.path, `${name}.tmp-${String(Math.random()).replace(/^0\./, '')}`);
const file = fs.createWriteStream(temporalName);
const removeTempFile = (): void => fs.unlink(temporalName, () => {});
let opened = false;
uploadStream.pipe(file);
uploadStream.done = function (): void {
const onend = function (): void {
file.on('close', function () {
renameTmp(temporalName, pathName, function (err) {
if (err) {
uploadStream.emit('error', err);
} else {
uploadStream.emit('success');
}
});
});
file.end();
};
if (_ended) {
onend();
} else {
uploadStream.on('end', onend);
}
};
uploadStream.abort = function (): void {
if (opened) {
opened = false;
file.on('close', function () {
removeTempFile();
});
} else {
// if the file does not recieve any byte never is opened and has to be removed anyway.
removeTempFile();
}
file.end();
};
file.on('open', function () {
opened = true;
// re-emitting open because it's handled in storage.js
uploadStream.emit('open');
});
file.on('error', function (err) {
uploadStream.emit('error', err);
});
}
});
return uploadStream;
}
public readTarball(name: string): ReadTarball {
const pathName: string = this._getStorage(name);
this.logger.debug({ packageName: name }, '[local-storage/readTarball] read a tarball for package: @{packageName}');
const readTarballStream = new ReadTarball({});
const readStream = fs.createReadStream(pathName);
readStream.on('error', function (err) {
readTarballStream.emit('error', err);
});
readStream.on('open', function (fd) {
fs.fstat(fd, function (err, stats) {
if (_.isNil(err) === false) {
return readTarballStream.emit('error', err);
}
readTarballStream.emit('content-length', stats.size);
readTarballStream.emit('open');
readStream.pipe(readTarballStream);
});
});
readTarballStream.abort = function (): void {
readStream.close();
};
return readTarballStream;
}
private _createFile(name: string, contents: any, callback: Function): void {
this.logger.trace({ name }, '[local-storage/_createFile] create a new file: @{name}');
fs.open(name, 'wx', (err) => {
if (err) {
// native EEXIST used here to check exception on fs.open
if (err.code === 'EEXIST') {
this.logger.trace({ name }, '[local-storage/_createFile] file cannot be created, it already exists: @{name}');
return callback(fSError(fileExist));
}
}
this._writeFile(name, contents, callback);
});
}
private _readStorageFile(name: string): Promise<any> {
return new Promise((resolve, reject): void => {
this.logger.trace({ name }, '[local-storage/_readStorageFile] read a file: @{name}');
fs.readFile(name, (err, data) => {
if (err) {
this.logger.trace({ err }, '[local-storage/_readStorageFile] error on read the file: @{name}');
reject(err);
} else {
this.logger.trace({ name }, '[local-storage/_readStorageFile] read file succeed: @{name}');
resolve(data);
}
});
});
}
private _convertToString(value: Package): string {
return JSON.stringify(value, null, '\t');
}
private _getStorage(fileName = ''): string {
const storagePath: string = path.join(this.path, fileName);
return storagePath;
}
private _writeFile(dest: string, data: string, cb: Callback): void {
const createTempFile = (cb): void => {
const tempFilePath = tempFile(dest);
fs.writeFile(tempFilePath, data, (err) => {
if (err) {
this.logger.trace({ name: dest }, '[local-storage/_writeFile] new file: @{name} has been created');
return cb(err);
}
this.logger.trace({ name: dest }, '[local-storage/_writeFile] creating a new file: @{name}');
renameTmp(tempFilePath, dest, cb);
});
};
createTempFile((err) => {
if (err && err.code === noSuchFile) {
mkdirp(path.dirname(dest), function (err) {
if (err) {
return cb(err);
}
createTempFile(cb);
});
} else {
cb(err);
}
});
}
private _lockAndReadJSON(name: string, cb: Function): void {
const fileName: string = this._getStorage(name);
readFile(
fileName,
{
lock: true,
parse: true,
},
(err, res) => {
if (err) {
this.logger.trace({ name }, '[local-storage/_lockAndReadJSON] read new file: @{name} has failed');
return cb(err);
}
this.logger.trace({ name }, '[local-storage/_lockAndReadJSON] file: @{name} read');
return cb(null, res);
}
);
}
private _unlockJSON(name: string, cb: Function): void {
unlockFile(this._getStorage(name), cb);
}
}

@ -0,0 +1,29 @@
import fs from 'fs';
import _ from 'lodash';
import { LocalStorage, StorageList, Logger } from '@verdaccio/types';
export function loadPrivatePackages(path: string, logger: Logger): LocalStorage {
const list: StorageList = [];
const emptyDatabase = { list, secret: '' };
const data = fs.readFileSync(path, 'utf8');
if (_.isNil(data)) {
// readFileSync is platform specific, FreeBSD might return null
return emptyDatabase;
}
let db;
try {
db = JSON.parse(data);
} catch (err) {
logger.error(`Package database file corrupted (invalid JSON), please check the error printed below.\nFile Path: ${path}`, err);
throw Error('Package database file corrupted (invalid JSON)');
}
if (_.isEmpty(db)) {
return emptyDatabase;
}
return db;
}

@ -0,0 +1,75 @@
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
export function getFileStats(packagePath: string): Promise<fs.Stats> {
return new Promise((resolve, reject): void => {
fs.stat(packagePath, (err, stats) => {
if (_.isNil(err) === false) {
return reject(err);
}
resolve(stats);
});
});
}
export function readDirectory(packagePath: string): Promise<string[]> {
return new Promise((resolve, reject): void => {
fs.readdir(packagePath, (err, scopedPackages) => {
if (_.isNil(err) === false) {
return reject(err);
}
resolve(scopedPackages);
});
});
}
function hasScope(file: string): boolean {
return file.match(/^@/) !== null;
}
/* eslint-disable no-async-promise-executor */
export async function findPackages(storagePath: string, validationHandler: Function): Promise<{ name: string; path: string }[]> {
const listPackages: { name: string; path: string }[] = [];
return new Promise(async (resolve, reject) => {
try {
const scopePath = path.resolve(storagePath);
const storageDirs = await readDirectory(scopePath);
for (const directory of storageDirs) {
// we check whether has 2nd level
if (hasScope(directory)) {
// we read directory multiple
const scopeDirectory = path.resolve(storagePath, directory);
const scopedPackages = await readDirectory(scopeDirectory);
for (const scopedDirName of scopedPackages) {
if (validationHandler(scopedDirName)) {
// we build the complete scope path
const scopePath = path.resolve(storagePath, directory, scopedDirName);
// list content of such directory
listPackages.push({
name: `${directory}/${scopedDirName}`,
path: scopePath,
});
}
}
} else {
// otherwise we read as single level
if (validationHandler(directory)) {
const scopePath = path.resolve(storagePath, directory);
listPackages.push({
name: directory,
path: scopePath,
});
}
}
}
} catch (error) {
reject(error);
}
resolve(listPackages);
});
}
/* eslint-enable no-async-promise-executor */

@ -0,0 +1,6 @@
{
"list": [
"webpack",
],
"secret": "8aabe5a0174be5cb3d5a92c01869101f11864d631a8ec21d0bf16e37ad657e92"
}

@ -0,0 +1,20 @@
{
"list": [
"webpack",
"pnpm",
"yarn",
"react-redux",
"verdaccio",
"tslint",
"@test/pkg1",
"@verdaccio/types",
"@test/view",
"@my-co/my-package",
"verdaccio-authentication-plugin-sample",
"@verdaccio/streams",
"@verdaccio/file-locking",
"bar",
"foo"
],
"secret": "8aabe5a0174be5cb3d5a92c01869101f11864d631a8ec21d0bf16e37ad657e92"
}

@ -0,0 +1,50 @@
const json = {
_id: '@scope/pk1-test',
name: '@scope/pk1-test',
description: '',
'dist-tags': {
latest: '1.0.6',
},
versions: {
'1.0.6': {
name: '@scope/pk1-test',
version: '1.0.6',
description: '',
main: 'index.js',
scripts: {
test: 'echo "Error: no test specified" && exit 1',
},
keywords: [],
author: {
name: 'Juan Picado',
email: 'juan@jotadeveloper.com',
},
license: 'ISC',
dependencies: {
verdaccio: '^2.7.2',
},
readme: '# test',
readmeFilename: 'README.md',
_id: '@scope/pk1-test@1.0.6',
_npmVersion: '5.5.1',
_nodeVersion: '8.7.0',
_npmUser: {},
dist: {
integrity: 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==',
shasum: '2c03764f651a9f016ca0b7620421457b619151b9',
tarball: 'http://localhost:5555/@scope/pk1-test/-/@scope/pk1-test-1.0.6.tgz',
},
},
},
readme: '# test',
_attachments: {
'@scope/pk1-test-1.0.6.tgz': {
content_type: 'application/octet-stream',
data:
'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=',
length: 512,
},
},
};
export default json;

@ -0,0 +1,56 @@
{
"name": "readme-test",
"versions": {
"0.0.0": {
"name": "test-readme",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": ""
},
"author": "",
"license": "ISC",
"_id": "test-readme@0.0.0",
"dist": {
"shasum": "8ee7331cbc641581b1a8cecd9d38d744a8feb863",
"tarball": "http://localhost:1234/test-readme/-/test-readme-0.0.0.tgz"
},
"_from": ".",
"_npmVersion": "1.3.1",
"_npmUser": {
"name": "alex",
"email": "alex@kocharin.ru"
},
"maintainers": [
{
"name": "juan",
"email": "juanpicado19@gmail.com"
}
]
}
},
"dist-tags": {
"foo": "0.0.0",
"latest": "0.0.0"
},
"time": {
"modified": "2017-10-06T20:30:38.721Z",
"created": "2017-10-06T20:30:38.721Z",
"0.0.0": "2017-10-06T20:30:38.721Z"
},
"_distfiles": {},
"_attachments": {
"test-readme-0.0.0.tgz": {
"shasum": "8ee7331cbc641581b1a8cecd9d38d744a8feb863",
"version": "0.0.0"
}
},
"_uplinks": {},
"_rev": "5-d647003b88ff08a0",
"readme": "this is a readme"
}

@ -0,0 +1,89 @@
import minimatch from 'minimatch';
// FUTURE: we should use the same is on verdaccio
export function getMatchedPackagesSpec(pkgName: string, packages: object): object | undefined {
for (const i in packages) {
if (minimatch.makeRe(i).exec(pkgName)) {
return packages[i];
}
}
return;
}
export default class Config {
public constructor() {
this.storage = './test-storage';
this.listen = 'http://localhost:1443/';
this.auth = {
htpasswd: {
file: './htpasswd',
max_users: 1000,
},
};
this.uplinks = {
npmjs: {
url: 'https://registry.npmjs.org',
cache: true,
},
};
this.packages = {
'@*/*': {
access: ['$all'],
publish: ['$authenticated'],
proxy: [],
},
'local-private-custom-storage': {
access: ['$all'],
publish: ['$authenticated'],
storage: 'private_folder',
},
'*': {
access: ['$all'],
publish: ['$authenticated'],
proxy: ['npmjs'],
},
'**': {
access: [],
publish: [],
proxy: [],
},
};
this.logs = [
{
type: 'stdout',
format: 'pretty',
level: 35,
},
];
this.self_path = './tests/__fixtures__/config.yaml';
this.https = {
enable: false,
};
this.user_agent = 'verdaccio/3.0.0-alpha.7';
this.users = {};
this.server_id = 'severMockId';
this.getMatchedPackagesSpec = (pkgName) => getMatchedPackagesSpec(pkgName, this.packages);
this.checkSecretKey = (secret) => {
if (!secret) {
const newSecret = 'superNewSecret';
this.secret = newSecret;
return newSecret;
}
return secret;
};
}
}

@ -0,0 +1,14 @@
import { Logger } from '@verdaccio/types';
const logger: Logger = {
warn: jest.fn(),
error: jest.fn(),
// fatal: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
child: jest.fn(),
http: jest.fn(),
trace: jest.fn(),
};
export default logger;

@ -0,0 +1,273 @@
/* 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();
});
test('should create an instance', () => {
expect(optionsPlugin.logger.error).not.toHaveBeenCalled();
expect(locaDatabase).toBeDefined();
});
test('should display log error if fails on load database', () => {
loadPrivatePackages.mockImplementation(() => {
throw Error();
});
new LocalDatabase(optionsPlugin.config, optionsPlugin.logger);
expect(optionsPlugin.logger.error).toHaveBeenCalled();
expect(optionsPlugin.logger.error).toHaveBeenCalledTimes(2);
});
describe('should create set secret', () => {
test('should create get secret', async () => {
const secretKey = await locaDatabase.getSecret();
expect(secretKey).toBeDefined();
expect(typeof secretKey === 'string').toBeTruthy();
});
test('should create set secret', async () => {
await locaDatabase.setSecret(optionsPlugin.config.checkSecretKey(''));
expect(optionsPlugin.config.secret).toBeDefined();
expect(typeof optionsPlugin.config.secret === 'string').toBeTruthy();
const fetchedSecretKey = await locaDatabase.getSecret();
expect(optionsPlugin.config.secret).toBe(fetchedSecretKey);
});
});
describe('getPackageStorage', () => {
test('should get default storage', () => {
const pkgName = 'someRandomePackage';
const storage = locaDatabase.getPackageStorage(pkgName);
expect(storage).toBeDefined();
if (storage) {
const storagePath = path.normalize((storage as ILocalFSPackageManager).path).toLowerCase();
expect(storagePath).toBe(path.normalize(path.join(__dirname, '__fixtures__', optionsPlugin.config.storage || '', pkgName)).toLowerCase());
}
});
test('should use custom storage', () => {
const pkgName = 'local-private-custom-storage';
const storage = locaDatabase.getPackageStorage(pkgName);
expect(storage).toBeDefined();
if (storage) {
const storagePath = path.normalize((storage as ILocalFSPackageManager).path).toLowerCase();
expect(storagePath).toBe(
path.normalize(path.join(__dirname, '__fixtures__', optionsPlugin.config.storage || '', 'private_folder', pkgName)).toLowerCase()
);
}
});
});
describe('Database CRUD', () => {
test('should add an item to database', (done) => {
const pgkName = 'jquery';
locaDatabase.get((err, data) => {
expect(err).toBeNull();
expect(data).toHaveLength(0);
locaDatabase.add(pgkName, (err) => {
expect(err).toBeNull();
locaDatabase.get((err, data) => {
expect(err).toBeNull();
expect(data).toHaveLength(1);
done();
});
});
});
});
test('should remove an item to database', (done) => {
const pgkName = 'jquery';
locaDatabase.get((err, data) => {
expect(err).toBeNull();
expect(data).toHaveLength(0);
locaDatabase.add(pgkName, (err) => {
expect(err).toBeNull();
locaDatabase.remove(pgkName, (err) => {
expect(err).toBeNull();
locaDatabase.get((err, data) => {
expect(err).toBeNull();
expect(data).toHaveLength(0);
done();
});
});
});
});
});
});
describe('search', () => {
const onPackageMock = jest.fn((item, cb) => cb());
const validatorMock = jest.fn(() => true);
const callSearch = (db, numberTimesCalled, cb): void => {
db.search(
onPackageMock,
function onEnd() {
expect(onPackageMock).toHaveBeenCalledTimes(numberTimesCalled);
expect(validatorMock).toHaveBeenCalledTimes(numberTimesCalled);
cb();
},
validatorMock
);
};
test('should find scoped packages', (done) => {
const scopedPackages = ['@pkg1/test'];
const stats = { mtime: new Date() };
jest.spyOn(fs, 'stat').mockImplementation((_, cb) => cb(null, stats as fs.Stats));
jest.spyOn(fs, 'readdir').mockImplementation((storePath, cb) => cb(null, storePath.match('test-storage') ? scopedPackages : []));
callSearch(locaDatabase, 1, done);
});
test('should find non scoped packages', (done) => {
const nonScopedPackages = ['pkg1', 'pkg2'];
const stats = { mtime: new Date() };
jest.spyOn(fs, 'stat').mockImplementation((_, cb) => cb(null, stats as fs.Stats));
jest.spyOn(fs, 'readdir').mockImplementation((storePath, cb) => cb(null, storePath.match('test-storage') ? nonScopedPackages : []));
const db = new LocalDatabase(
assign({}, optionsPlugin.config, {
// clean up this, it creates noise
packages: {},
}),
optionsPlugin.logger
);
callSearch(db, 2, done);
});
test('should fails on read the storage', (done) => {
const spyInstance = jest.spyOn(fs, 'readdir').mockImplementation((_, cb) => cb(Error('fails'), null));
const db = new LocalDatabase(
assign({}, optionsPlugin.config, {
// clean up this, it creates noise
packages: {},
}),
optionsPlugin.logger
);
callSearch(db, 0, done);
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!');
});
});
});

@ -0,0 +1,344 @@
import path from 'path';
import fs from 'fs';
import mkdirp from 'mkdirp';
import rm from 'rmdir-sync';
import { Logger, ILocalPackageManager, Package } from '@verdaccio/types';
import LocalDriver, { fileExist, fSError, noSuchFile, resourceNotAvailable } from '../src/local-fs';
import pkg from './__fixtures__/pkg';
let localTempStorage: string;
const pkgFileName = 'package.json';
const logger: Logger = {
error: () => jest.fn(),
info: () => jest.fn(),
debug: () => jest.fn(),
warn: () => jest.fn(),
child: () => jest.fn(),
http: () => jest.fn(),
trace: () => jest.fn(),
};
beforeAll(() => {
localTempStorage = path.join('./_storage');
rm(localTempStorage);
});
describe('Local FS test', () => {
describe('savePackage() group', () => {
test('savePackage()', (done) => {
const data = {};
const localFs = new LocalDriver(path.join(localTempStorage, 'first-package'), logger);
localFs.savePackage('pkg.1.0.0.tar.gz', data as Package, (err) => {
expect(err).toBeNull();
done();
});
});
});
describe('readPackage() group', () => {
test('readPackage() success', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-test'), logger);
localFs.readPackage(pkgFileName, (err) => {
expect(err).toBeNull();
done();
});
});
test('readPackage() fails', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-testt'), logger);
localFs.readPackage(pkgFileName, (err) => {
expect(err).toBeTruthy();
done();
});
});
test('readPackage() fails corrupt', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-test-corrupt'), logger);
localFs.readPackage('corrupt.js', (err) => {
expect(err).toBeTruthy();
done();
});
});
});
describe('createPackage() group', () => {
test('createPackage()', (done) => {
const localFs = new LocalDriver(path.join(localTempStorage, 'createPackage'), logger);
localFs.createPackage(path.join(localTempStorage, 'package5'), pkg, (err) => {
expect(err).toBeNull();
done();
});
});
test('createPackage() fails by fileExist', (done) => {
const localFs = new LocalDriver(path.join(localTempStorage, 'createPackage'), logger);
localFs.createPackage(path.join(localTempStorage, 'package5'), pkg, (err) => {
expect(err).not.toBeNull();
expect(err.code).toBe(fileExist);
done();
});
});
describe('deletePackage() group', () => {
test('deletePackage()', (done) => {
const localFs = new LocalDriver(path.join(localTempStorage, 'createPackage'), logger);
// verdaccio removes the package.json instead the package name
localFs.deletePackage('package.json', (err) => {
expect(err).toBeNull();
done();
});
});
});
});
describe('removePackage() group', () => {
beforeEach(() => {
mkdirp.sync(path.join(localTempStorage, '_toDelete'));
});
test('removePackage() success', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(localTempStorage, '_toDelete'), logger);
localFs.removePackage((error) => {
expect(error).toBeNull();
done();
});
});
test('removePackage() fails', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(localTempStorage, '_toDelete_fake'), logger);
localFs.removePackage((error) => {
expect(error).toBeTruthy();
expect(error.code).toBe('ENOENT');
done();
});
});
});
describe('readTarball() group', () => {
test('readTarball() success', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-test'), logger);
const readTarballStream = localFs.readTarball('test-readme-0.0.0.tgz');
readTarballStream.on('error', function (err) {
expect(err).toBeNull();
});
readTarballStream.on('content-length', function (content) {
expect(content).toBe(352);
});
readTarballStream.on('end', function () {
done();
});
readTarballStream.on('data', function (data) {
expect(data).toBeDefined();
});
});
test('readTarball() fails', (done) => {
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-test'), logger);
const readTarballStream = localFs.readTarball('file-does-not-exist-0.0.0.tgz');
readTarballStream.on('error', function (err) {
expect(err).toBeTruthy();
done();
});
});
});
describe('writeTarball() group', () => {
beforeEach(() => {
const writeTarballFolder: string = path.join(localTempStorage, '_writeTarball');
rm(writeTarballFolder);
mkdirp.sync(writeTarballFolder);
});
test('writeTarball() success', (done) => {
const newFileName = 'new-readme-0.0.0.tgz';
const readmeStorage: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-test'), logger);
const writeStorage: ILocalPackageManager = new LocalDriver(path.join(__dirname, '../_storage'), logger);
const readTarballStream = readmeStorage.readTarball('test-readme-0.0.0.tgz');
const writeTarballStream = writeStorage.writeTarball(newFileName);
writeTarballStream.on('error', function (err) {
expect(err).toBeNull();
done();
});
writeTarballStream.on('success', function () {
const fileLocation: string = path.join(__dirname, '../_storage', newFileName);
expect(fs.existsSync(fileLocation)).toBe(true);
done();
});
readTarballStream.on('end', function () {
writeTarballStream.done();
});
writeTarballStream.on('end', function () {
done();
});
writeTarballStream.on('data', function (data) {
expect(data).toBeDefined();
});
readTarballStream.on('error', function (err) {
expect(err).toBeNull();
done();
});
readTarballStream.pipe(writeTarballStream);
});
test('writeTarball() abort', (done) => {
const newFileLocationFolder: string = path.join(localTempStorage, '_writeTarball');
const newFileName = 'new-readme-abort-0.0.0.tgz';
const readmeStorage: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/readme-test'), logger);
const writeStorage: ILocalPackageManager = new LocalDriver(newFileLocationFolder, logger);
const readTarballStream = readmeStorage.readTarball('test-readme-0.0.0.tgz');
const writeTarballStream = writeStorage.writeTarball(newFileName);
writeTarballStream.on('error', function (err) {
expect(err).toBeTruthy();
done();
});
writeTarballStream.on('data', function (data) {
expect(data).toBeDefined();
writeTarballStream.abort();
});
readTarballStream.pipe(writeTarballStream);
});
});
describe('updatePackage() group', () => {
const updateHandler = jest.fn((name, cb) => {
cb();
});
const onWrite = jest.fn((name, data, cb) => {
cb();
});
const transform = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
test('updatePackage() success', (done) => {
jest.doMock('@verdaccio/file-locking', () => {
return {
readFile: (name, _options, cb): any => cb(null, { name }),
unlockFile: (_something, cb): any => cb(null),
};
});
const LocalDriver = require('../src/local-fs').default;
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/update-package'), logger);
localFs.updatePackage('updatePackage', updateHandler, onWrite, transform, () => {
expect(transform).toHaveBeenCalledTimes(1);
expect(updateHandler).toHaveBeenCalledTimes(1);
expect(onWrite).toHaveBeenCalledTimes(1);
done();
});
});
describe('updatePackage() failures handler', () => {
test('updatePackage() whether locking fails', (done) => {
jest.doMock('@verdaccio/file-locking', () => {
return {
readFile: (name, _options, cb): any => cb(Error('whateverError'), { name }),
unlockFile: (_something, cb): any => cb(null),
};
});
require('../src/local-fs').default;
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/update-package'), logger);
localFs.updatePackage('updatePackage', updateHandler, onWrite, transform, (err) => {
expect(err).not.toBeNull();
expect(transform).toHaveBeenCalledTimes(0);
expect(updateHandler).toHaveBeenCalledTimes(0);
expect(onWrite).toHaveBeenCalledTimes(0);
done();
});
});
test('updatePackage() unlock a missing package', (done) => {
jest.doMock('@verdaccio/file-locking', () => {
return {
readFile: (name, _options, cb): any => cb(fSError(noSuchFile, 404), { name }),
unlockFile: (_something, cb): any => cb(null),
};
});
const LocalDriver = require('../src/local-fs').default;
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/update-package'), logger);
localFs.updatePackage('updatePackage', updateHandler, onWrite, transform, (err) => {
expect(err).not.toBeNull();
expect(transform).toHaveBeenCalledTimes(0);
expect(updateHandler).toHaveBeenCalledTimes(0);
expect(onWrite).toHaveBeenCalledTimes(0);
done();
});
});
test('updatePackage() unlock a resource non available', (done) => {
jest.doMock('@verdaccio/file-locking', () => {
return {
readFile: (name, _options, cb): any => cb(fSError(resourceNotAvailable, 503), { name }),
unlockFile: (_something, cb): any => cb(null),
};
});
const LocalDriver = require('../src/local-fs').default;
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/update-package'), logger);
localFs.updatePackage('updatePackage', updateHandler, onWrite, transform, (err) => {
expect(err).not.toBeNull();
expect(transform).toHaveBeenCalledTimes(0);
expect(updateHandler).toHaveBeenCalledTimes(0);
expect(onWrite).toHaveBeenCalledTimes(0);
done();
});
});
test('updatePackage() if updateHandler fails', (done) => {
jest.doMock('@verdaccio/file-locking', () => {
return {
readFile: (name, _options, cb): any => cb(null, { name }),
unlockFile: (_something, cb): any => cb(null),
};
});
const LocalDriver = require('../src/local-fs').default;
const localFs: ILocalPackageManager = new LocalDriver(path.join(__dirname, '__fixtures__/update-package'), logger);
const updateHandler = jest.fn((_name, cb) => {
cb(fSError('something wrong', 500));
});
localFs.updatePackage('updatePackage', updateHandler, onWrite, transform, (err) => {
expect(err).not.toBeNull();
expect(transform).toHaveBeenCalledTimes(0);
expect(updateHandler).toHaveBeenCalledTimes(1);
expect(onWrite).toHaveBeenCalledTimes(0);
done();
});
});
});
});
});

@ -0,0 +1,74 @@
import path from 'path';
import fs from 'fs';
import { findPackages } from '../src/utils';
import { loadPrivatePackages } from '../src/pkg-utils';
import { noSuchFile } from '../src/local-fs';
// FIXME: remove this mocks imports
// eslint-disable-next-line jest/no-mocks-import
import logger from './__mocks__/Logger';
describe('Utitlies', () => {
const loadDb = (name): string => path.join(__dirname, '__fixtures__/databases', `${name}.json`);
beforeEach(() => {
jest.resetModules();
});
test('should load private packages', () => {
const database = loadDb('ok');
const db = loadPrivatePackages(database, logger);
expect(db.list).toHaveLength(15);
});
test('should load and empty private packages if database file is valid and empty', () => {
const database = loadDb('empty');
const db = loadPrivatePackages(database, logger);
expect(db.list).toHaveLength(0);
});
test('should fails on load private packages', () => {
const database = loadDb('corrupted');
expect(() => {
loadPrivatePackages(database, logger);
}).toThrow();
});
test('should handle null read values and return empty database', () => {
const spy = jest.spyOn(fs, 'readFileSync');
spy.mockReturnValue(null);
const database = loadDb('ok');
const db = loadPrivatePackages(database, logger);
expect(db.list).toHaveLength(0);
spy.mockRestore();
});
describe('find packages', () => {
test('should fails on wrong storage path', async () => {
try {
await findPackages(
'./no_such_folder_fake',
jest.fn(() => true)
);
} catch (e) {
expect(e.code).toEqual(noSuchFile);
}
});
test('should fetch all packages from valid storage', async () => {
const storage = path.join(__dirname, '__fixtures__/findPackages');
const validator = jest.fn((file) => file.indexOf('.') === -1);
const pkgs = await findPackages(storage, validator);
// the result is 7 due number of packages on "findPackages" directory
expect(pkgs).toHaveLength(5);
expect(validator).toHaveBeenCalledTimes(8);
});
});
});

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts"]
}

@ -0,0 +1,3 @@
{
"extends": "../../../.babelrc"
}

1
packages/core/readme/.gitignore vendored Normal file

@ -0,0 +1 @@
lib/

@ -0,0 +1,332 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [9.7.3](https://github.com/verdaccio/monorepo/compare/v9.7.2...v9.7.3) (2020-07-30)
### Bug Fixes
* update marked / request security vulnerability ([#378](https://github.com/verdaccio/monorepo/issues/378)) ([4188e08](https://github.com/verdaccio/monorepo/commit/4188e088f42d0f6e090c948b869312ba1f30cd79))
## [9.7.2](https://github.com/verdaccio/monorepo/compare/v9.7.1...v9.7.2) (2020-07-20)
**Note:** Version bump only for package @verdaccio/readme
## [9.7.1](https://github.com/verdaccio/monorepo/compare/v9.7.0...v9.7.1) (2020-07-10)
**Note:** Version bump only for package @verdaccio/readme
# [9.7.0](https://github.com/verdaccio/monorepo/compare/v9.6.1...v9.7.0) (2020-06-24)
**Note:** Version bump only for package @verdaccio/readme
## [9.6.1](https://github.com/verdaccio/monorepo/compare/v9.6.0...v9.6.1) (2020-06-07)
**Note:** Version bump only for package @verdaccio/readme
## [9.5.1](https://github.com/verdaccio/monorepo/compare/v9.5.0...v9.5.1) (2020-06-03)
### Bug Fixes
* restore Node v8 support ([#361](https://github.com/verdaccio/monorepo/issues/361)) ([9be55a1](https://github.com/verdaccio/monorepo/commit/9be55a1deebe954e8eef9edc59af9fd16e29daed))
# [9.5.0](https://github.com/verdaccio/monorepo/compare/v9.4.1...v9.5.0) (2020-05-02)
**Note:** Version bump only for package @verdaccio/readme
# [9.4.0](https://github.com/verdaccio/monorepo/compare/v9.3.4...v9.4.0) (2020-03-21)
**Note:** Version bump only for package @verdaccio/readme
## [9.3.3](https://github.com/verdaccio/monorepo/compare/v9.3.2...v9.3.3) (2020-03-11)
### Bug Fixes
* update jsdom@16.2.1 ([#340](https://github.com/verdaccio/monorepo/issues/340)) ([6060769](https://github.com/verdaccio/monorepo/commit/6060769d52f796337dda9f1a54f149c5fb22ca17))
## [9.3.2](https://github.com/verdaccio/monorepo/compare/v9.3.1...v9.3.2) (2020-03-08)
### Bug Fixes
* security dependency jsdom@16.2.0 update ([#338](https://github.com/verdaccio/monorepo/issues/338)) ([0599f3e](https://github.com/verdaccio/monorepo/commit/0599f3e16fd1de993494943e2e7464d10b62d6be))
* update dependencies ([#332](https://github.com/verdaccio/monorepo/issues/332)) ([b6165ae](https://github.com/verdaccio/monorepo/commit/b6165aea9b7e4012477081eae68bfa7159c58f56))
## [9.3.1](https://github.com/verdaccio/monorepo/compare/v9.3.0...v9.3.1) (2020-02-23)
**Note:** Version bump only for package @verdaccio/readme
# [9.3.0](https://github.com/verdaccio/monorepo/compare/v9.2.0...v9.3.0) (2020-01-29)
**Note:** Version bump only for package @verdaccio/readme
# [9.0.0](https://github.com/verdaccio/monorepo/compare/v8.5.3...v9.0.0) (2020-01-07)
**Note:** Version bump only for package @verdaccio/readme
## [8.5.2](https://github.com/verdaccio/monorepo/compare/v8.5.1...v8.5.2) (2019-12-25)
**Note:** Version bump only for package @verdaccio/readme
## [8.5.1](https://github.com/verdaccio/monorepo/compare/v8.5.0...v8.5.1) (2019-12-24)
**Note:** Version bump only for package @verdaccio/readme
# [8.5.0](https://github.com/verdaccio/monorepo/compare/v8.4.2...v8.5.0) (2019-12-22)
**Note:** Version bump only for package @verdaccio/readme
## [8.4.2](https://github.com/verdaccio/monorepo/compare/v8.4.1...v8.4.2) (2019-11-23)
**Note:** Version bump only for package @verdaccio/readme
## [8.4.1](https://github.com/verdaccio/monorepo/compare/v8.4.0...v8.4.1) (2019-11-22)
**Note:** Version bump only for package @verdaccio/readme
# [8.4.0](https://github.com/verdaccio/monorepo/compare/v8.3.0...v8.4.0) (2019-11-22)
**Note:** Version bump only for package @verdaccio/readme
# [8.3.0](https://github.com/verdaccio/monorepo/compare/v8.2.0...v8.3.0) (2019-10-27)
**Note:** Version bump only for package @verdaccio/readme
# [8.2.0](https://github.com/verdaccio/monorepo/compare/v8.2.0-next.0...v8.2.0) (2019-10-23)
### Bug Fixes
* core/readme/package.json to reduce vulnerabilities ([#216](https://github.com/verdaccio/monorepo/issues/216)) ([40299ab](https://github.com/verdaccio/monorepo/commit/40299ab))
# [8.2.0-next.0](https://github.com/verdaccio/monorepo/compare/v8.1.4...v8.2.0-next.0) (2019-10-08)
### Bug Fixes
* fixed lint errors ([5e677f7](https://github.com/verdaccio/monorepo/commit/5e677f7))
* fixed lint errors ([c80e915](https://github.com/verdaccio/monorepo/commit/c80e915))
* quotes should be single ([ae9aa44](https://github.com/verdaccio/monorepo/commit/ae9aa44))
## [8.1.2](https://github.com/verdaccio/monorepo/compare/v8.1.1...v8.1.2) (2019-09-29)
### Bug Fixes
* **readme:** security vulnerabilities in marked dep ([ee604b1](https://github.com/verdaccio/monorepo/commit/ee604b1))
## [8.1.1](https://github.com/verdaccio/monorepo/compare/v8.1.0...v8.1.1) (2019-09-26)
### Bug Fixes
* **security:** Cross-site Scripting (XSS) for readme ([7b53e1b](https://github.com/verdaccio/monorepo/commit/7b53e1b))
# [8.1.0](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.1...v8.1.0) (2019-09-07)
**Note:** Version bump only for package @verdaccio/readme
## [8.0.1-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.1-next.0...v8.0.1-next.1) (2019-08-29)
**Note:** Version bump only for package @verdaccio/readme
## [8.0.1-next.0](https://github.com/verdaccio/monorepo/compare/v8.0.0...v8.0.1-next.0) (2019-08-29)
**Note:** Version bump only for package @verdaccio/readme
# [8.0.0](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.4...v8.0.0) (2019-08-22)
**Note:** Version bump only for package @verdaccio/readme
# [8.0.0-next.4](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.3...v8.0.0-next.4) (2019-08-18)
**Note:** Version bump only for package @verdaccio/readme
# [8.0.0-next.2](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.1...v8.0.0-next.2) (2019-08-03)
**Note:** Version bump only for package @verdaccio/readme
# [8.0.0-next.1](https://github.com/verdaccio/monorepo/compare/v8.0.0-next.0...v8.0.0-next.1) (2019-08-01)
**Note:** Version bump only for package @verdaccio/readme
# [8.0.0-next.0](https://github.com/verdaccio/monorepo/compare/v2.0.0...v8.0.0-next.0) (2019-08-01)
### Features
* **readme:** import readme package ([f4bbf3a](https://github.com/verdaccio/monorepo/commit/f4bbf3a))
* **readme:** modernize project ([0d8f963](https://github.com/verdaccio/monorepo/commit/0d8f963))
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [1.0.4](https://github.com/verdaccio/readme/compare/v1.0.3...v1.0.4) (2019-06-22)
### Bug Fixes
* update dependencies ([3316ccf](https://github.com/verdaccio/readme/commit/3316ccf))
### [1.0.3](https://github.com/verdaccio/readme/compare/v1.0.2...v1.0.3) (2019-05-15)
### Bug Fixes
* **build:** lib folder as main ([e1ac882](https://github.com/verdaccio/readme/commit/e1ac882))
### [1.0.2](https://github.com/verdaccio/readme/compare/v1.0.1...v1.0.2) (2019-05-15)
### Bug Fixes
* **build:** remove publish script ([9b36d5f](https://github.com/verdaccio/readme/commit/9b36d5f))
### 1.0.1 (2019-05-15)
### Tests
* add basic test ([774a54d](https://github.com/verdaccio/readme/commit/774a54d))
* add image test ([8c4639e](https://github.com/verdaccio/readme/commit/8c4639e))
* add xss scenarios ([81e43e8](https://github.com/verdaccio/readme/commit/81e43e8))
* add xss scenarios ([b211b97](https://github.com/verdaccio/readme/commit/b211b97))

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Verdaccio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,21 @@
# @verdaccio/readme
📃 Readme markdown parser
[![verdaccio (latest)](https://img.shields.io/npm/v/@verdaccio/readme/latest.svg)](https://www.npmjs.com/package/@verdaccio/readme)
[![CircleCI](https://circleci.com/gh/verdaccio/readme/tree/master.svg?style=svg)](https://circleci.com/gh/verdaccio/readme/tree/master)
[![Known Vulnerabilities](https://snyk.io/test/github/verdaccio/readme/badge.svg?targetFile=package.json)](https://snyk.io/test/github/verdaccio/readme?targetFile=package.json)
[![codecov](https://codecov.io/gh/verdaccio/readme/branch/master/graph/badge.svg)](https://codecov.io/gh/verdaccio/readme)
[![backers](https://opencollective.com/verdaccio/tiers/backer/badge.svg?label=Backer&color=brightgreen)](https://opencollective.com/verdaccio)
[![discord](https://img.shields.io/discord/388674437219745793.svg)](http://chat.verdaccio.org/)
![MIT](https://img.shields.io/github/license/mashape/apistatus.svg)
[![node](https://img.shields.io/node/v/@verdaccio/readme/latest.svg)](https://www.npmjs.com/package/@verdaccio/readme)
> This package is already built-in in verdaccio
```
npm install @verdaccio/readme
```
## License
Verdaccio is [MIT licensed](https://github.com/verdaccio/readme/blob/master/LICENSE).

@ -0,0 +1,3 @@
const config = require('../../../jest/config');
module.exports = Object.assign({}, config, {});

@ -0,0 +1,52 @@
{
"name": "@verdaccio/readme",
"version": "10.0.0-beta",
"description": "Readme markdown parser",
"keywords": [
"verdaccio",
"readme",
"markdown"
],
"author": {
"name": "Juan Picado",
"email": "juanpicado19@gmail.com"
},
"license": "MIT",
"homepage": "https://verdaccio.org",
"repository": {
"type": "git",
"url": "https://github.com/verdaccio/monorepo",
"directory": "core/readme"
},
"bugs": {
"url": "https://github.com/verdaccio/monorepo/issues"
},
"publishConfig": {
"access": "public"
},
"main": "./build/index.js",
"types": "./build/index.d.ts",
"files": [
"build"
],
"dependencies": {
"dompurify": "2.0.8",
"jsdom": "15.2.1",
"marked": "1.1.1"
},
"devDependencies": {
"@verdaccio/types": "workspace:*"
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"type-check": "tsc --noEmit",
"build:types": "tsc --emitDeclarationOnly --declaration true",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"build": "pnpm run build:js && pnpm run build:types"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio"
}
}

@ -0,0 +1,17 @@
import marked from 'marked';
import createDOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
const DOMPurify = createDOMPurify(new JSDOM('').window);
export default function parseReadme(readme: string): string | void {
if (readme) {
return DOMPurify.sanitize(
marked(readme, {
sanitize: false,
}).trim()
);
}
return;
}

@ -0,0 +1,5 @@
# mix html and XSS markdown
[Basic](javascript:alert('Basic'))
<a href="https://github.com/webpack/webpack"><img width="200" height="200" src="https://webpack.js.org/assets/icon-square-big.svg"></a>

@ -0,0 +1,201 @@
import fs from 'fs';
import path from 'path';
import parseReadme from '../src';
function readReadme(project: string, fileName = 'readme.md'): Promise<string> {
return new Promise((resolve, reject): void => {
fs.readFile(path.join(__dirname, 'partials', project, fileName), 'utf8', (err, data) => {
if (err) {
return reject(err);
}
return resolve(data.toString());
});
});
}
function clean(text: string): string {
return text.replace(/\n|\r/g, '').trim();
}
describe('readme', () => {
test('should handle empty readme', () => {
expect(parseReadme('')).toBeUndefined();
});
test('should handle single string readme', () => {
expect(parseReadme('this is a readme')).toEqual('<p>this is a readme</p>');
});
test('should handle wrong text', () => {
expect(parseReadme(undefined)).toBeUndefined();
});
describe('basic parsing', () => {
test('should parse basic', () => {
expect(parseReadme('# hi')).toEqual(`<h1 id=\"hi\">hi</h1>`);
});
test('should parse basic / js alert', () => {
expect(parseReadme("[Basic](javascript:alert('Basic'))")).toEqual('<p><a>Basic</a></p>');
});
test('should parse basic / local storage', () => {
expect(parseReadme('[Local Storage](javascript:alert(JSON.stringify(localStorage)))')).toEqual('<p><a>Local Storage</a></p>');
});
test('should parse basic / case insensitive', () => {
expect(parseReadme("[CaseInsensitive](JaVaScRiPt:alert('CaseInsensitive'))")).toEqual('<p><a>CaseInsensitive</a></p>');
});
test('should parse basic / url', () => {
expect(parseReadme("[URL](javascript://www.google.com%0Aalert('URL'))")).toEqual('<p><a>URL</a></p>');
});
test('should parse basic / in quotes', () => {
expect(parseReadme('[In Quotes](\'javascript:alert("InQuotes")\')')).toEqual('<p><a href="\'javascript:alert(%22InQuotes%22)\'">In Quotes</a></p>');
});
});
describe('should parse images', () => {
test('in quotes', () => {
expect(parseReadme('![Escape SRC - onload](https://www.example.com/image.png"onload="alert(\'ImageOnLoad\'))')).toEqual(
'<p><img alt="Escape SRC - onload" src="https://www.example.com/image.png%22onload=%22alert(\'ImageOnLoad\')"></p>'
);
});
test('in image error', () => {
expect(parseReadme('![Escape SRC - onerror]("onerror="alert(\'ImageOnError\'))')).toEqual(
'<p><img alt="Escape SRC - onerror" src="%22onerror=%22alert(\'ImageOnError\')"></p>'
);
});
});
describe('should test fuzzing', () => {
test('xss / document cookie', () => {
expect(parseReadme('[XSS](javascript:prompt(document.cookie))')).toEqual('<p><a>XSS</a></p>');
});
test('xss / white space cookie', () => {
expect(parseReadme('[XSS](j a v a s c r i p t:prompt(document.cookie))')).toEqual(
'<p>[XSS](j a v a s c r i p t:prompt(document.cookie))</p>'
);
});
test('xss / data test/html', () => {
expect(parseReadme('[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / data test/html encoded', () => {
expect(parseReadme('[XSS](&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29)')).toEqual(
'<p><a href="&amp;#x6A&amp;#x61&amp;#x76&amp;#x61&amp;#x73&amp;#x63&amp;#x72&amp;#x69&amp;#x70&amp;#x74&amp;#x3A&amp;#x61&amp;#x6C&amp;#x65&amp;#x72&amp;#x74&amp;#x28&amp;#x27&amp;#x58&amp;#x53&amp;#x53&amp;#x27&amp;#x29">XSS</a></p>'
);
});
test('xss / js prompt', () => {
expect(parseReadme('[XSS]: (javascript:prompt(document.cookie))')).toEqual('');
});
test('xss / js window error alert', () => {
expect(parseReadme('[XSS](javascript:window.onerror=alert;throw%20document.cookie)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js window encoded prompt', () => {
expect(parseReadme('[XSS](javascript://%0d%0aprompt(1))')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js window encoded prompt multiple statement', () => {
expect(parseReadme('[XSS](javascript://%0d%0aprompt(1);com)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js window encoded window error alert multiple statement', () => {
expect(parseReadme('[XSS](javascript:window.onerror=alert;throw%20document.cookie)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js window encoded window error alert throw error', () => {
expect(parseReadme('[XSS](javascript://%0d%0awindow.onerror=alert;throw%20document.cookie)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js window encoded data text/html base 64', () => {
expect(parseReadme('[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js vbscript alert', () => {
expect(parseReadme('[XSS](vbscript:alert(document.domain))')).toEqual('<p><a>XSS</a></p>');
});
describe('xss / js alert this', () => {
test('xss / js case #1', () => {
expect(parseReadme('[XSS](javascript:this;alert(1))')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js case #2', () => {
expect(parseReadme('[XSS](javascript:this;alert(1&#41;)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js case #3', () => {
expect(parseReadme('[XSS](javascript&#58this;alert(1&#41;)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js case #4', () => {
expect(parseReadme('[XSS](Javas&#99;ript:alert(1&#41;)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / js case #5', () => {
expect(parseReadme('[XSS](Javas%26%2399;ript:alert(1&#41;)')).toEqual('<p><a href="Javas%26%2399;ript:alert(1)">XSS</a></p>');
});
test('xss / js case #6', () => {
expect(parseReadme('[XSS](javascript:alert&#65534;(1&#41;)')).toEqual('<p><a>XSS</a></p>');
});
});
test('xss / js confirm', () => {
expect(parseReadme('[XSS](javascript:confirm(1)')).toEqual('<p><a>XSS</a></p>');
});
describe('xss / js url', () => {
test('xss / case #1', () => {
expect(parseReadme('[XSS](javascript://www.google.com%0Aprompt(1))')).toEqual('<p><a>XSS</a></p>');
});
test('xss / case #2', () => {
expect(parseReadme('[XSS](javascript://%0d%0aconfirm(1);com)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / case #3', () => {
expect(parseReadme('[XSS](javascript:window.onerror=confirm;throw%201)')).toEqual('<p><a>XSS</a></p>');
});
test('xss / case #4', () => {
expect(parseReadme('[XSS](<28>javascript:alert(document.domain&#41;)')).toEqual('<p><a href="%EF%BF%BDjavascript:alert(document.domain)">XSS</a></p>');
});
test('xss / case #5', () => {
expect(parseReadme('![XSS](javascript:prompt(document.cookie))\\')).toEqual('<p><img alt="XSS">\\</p>');
});
test('xss / case #6', () => {
expect(parseReadme('![XSS](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)\\')).toEqual(
'<p><img alt="XSS" src="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">\\</p>'
);
});
// FIXME: requires proper parsing
test.skip('xss / case #7', () => {
expect(parseReadme(`![XSS'"\`onerror=prompt(document.cookie)](x)\\`)).toEqual('<p>![XSS\'\\"`onerror=prompt(document.cookie)](x)\\\\</p>');
});
});
});
describe('mix readmes / markdown', () => {
test('should parse marked', async () => {
const readme: string = await readReadme('mixed-html-mk');
expect(clean(parseReadme(readme) as string)).toEqual(
`<h1 id=\"mix-html-and-xss-markdown\">mix html and XSS markdown</h1><p><a>Basic</a></p><p> <a href=\"https://github.com/webpack/webpack\"><img src=\"https://webpack.js.org/assets/icon-square-big.svg\" height=\"200\" width=\"200\"></a></p>`
);
});
});
});

@ -0,0 +1,9 @@
{
"extends": "../../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build"
},
"include": ["src/**/*"],
"exclude": ["src/**/*.test.ts"]
}

@ -0,0 +1,3 @@
{
"extends": "../../../.babelrc"
}

@ -0,0 +1,6 @@
node_modules
coverage/
lib/
.nyc_output
tests-report/
build/

@ -0,0 +1,5 @@
{
"rules": {
"@typescript-eslint/no-use-before-define": "off"
}
}

Some files were not shown because too many files have changed in this diff Show More