1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-20 17:05:52 +01:00

feat!: replace deprecated request dependency by got (#3100)

This commit is contained in:
Juan Picado 2022-07-29 20:51:45 +02:00 committed by GitHub
parent 743ccff5ef
commit 292c0a37fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
348 changed files with 18047 additions and 17715 deletions

@ -0,0 +1,54 @@
---
'@verdaccio/api': major
'@verdaccio/auth': major
'@verdaccio/cli': major
'@verdaccio/config': major
'@verdaccio/core': major
'@verdaccio/file-locking': major
'@verdaccio/readme': major
'@verdaccio/tarball': major
'@verdaccio/types': major
'@verdaccio/url': major
'@verdaccio/hooks': major
'@verdaccio/loaders': major
'@verdaccio/logger': major
'@verdaccio/logger-prettify': major
'@verdaccio/middleware': major
'@verdaccio/node-api': major
'verdaccio-audit': major
'verdaccio-auth-memory': major
'verdaccio-htpasswd': major
'@verdaccio/local-storage': major
'verdaccio-memory': major
'@verdaccio/ui-theme': major
'@verdaccio/proxy': major
'@verdaccio/server': major
'@verdaccio/store': major
'@verdaccio/utils': major
'verdaccio': major
'@verdaccio/web': major
'@verdaccio/e2e-cli': major
'@verdaccio/e2e-ui': major
---
feat!: replace deprecated request dependency by got
This is a big refactoring of the core, fetching dependencies, improve code, more tests and better stability. This is essential for the next release, will take some time but would allow modularize more the core.
## Notes
- Remove deprecated `request` by other `got`, retry improved, custom Agent ( got does not include it built-in)
- Remove `async` dependency from storage (used by core) it was linked with proxy somehow safe to remove now
- Refactor with promises instead callback wherever is possible
- ~Document the API~
- Improve testing, integration tests
- Bugfix
- Clean up old validations
- Improve performance
## 💥 Breaking changes
- Plugin API methods were callbacks based are returning promises, this will break current storage plugins, check documentation for upgrade.
- Write Tarball, Read Tarball methods parameters change, a new set of options like `AbortController` signals are being provided to the `addAbortSignal` can be internally used with Streams when a request is aborted. eg: `addAbortSignal(signal, fs.createReadStream(pathName));`
- `@verdaccio/streams` stream abort support is legacy is being deprecated removed
- Remove AWS and Google Cloud packages for future refactoring [#2574](https://github.com/verdaccio/verdaccio/pull/2574).

@ -10,7 +10,6 @@
"verdaccio-htpasswd": "11.0.0-alpha.0",
"@verdaccio/local-storage": "11.0.0-alpha.0",
"@verdaccio/readme": "11.0.0-alpha.0",
"@verdaccio/streams": "11.0.0-alpha.0",
"@verdaccio/types": "11.0.0-alpha.0",
"@verdaccio/hooks": "6.0.0-alpha.0",
"@verdaccio/loaders": "6.0.0-alpha.0",

@ -93,8 +93,7 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, windows-latest]
## Node 16 breaks UI test, jest issue
node_version: [16, 17]
node_version: [16, 18]
name: ${{ matrix.os }} / Node ${{ matrix.node_version }}
runs-on: ${{ matrix.os }}
steps:

78
.vscode/launch.json vendored

@ -4,88 +4,12 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Attach",
"port": 9229,
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
},
{
"name": "Verdaccio Debug",
"name": "Attach",
"port": 9229,
"request": "attach",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
},
{
"type": "node",
"request": "launch",
"name": "CLI Babel Registry",
"stopOnEntry": false,
"program": "${workspaceFolder}/debug/bootstrap.js",
"args": ["-l", "0.0.0.0:4873"],
"env": {
"BABEL_ENV": "registry"
},
"preLaunchTask": "npm: build:webui",
"console": "integratedTerminal"
},
{
"name": "Unit Tests",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/bin/jest",
"stopOnEntry": false,
"args": ["--debug=true"],
"cwd": "${workspaceRoot}",
"runtimeExecutable": null,
"runtimeArgs": ["--nolazy"],
"env": {
"NODE_ENV": "test",
"TZ": "UTC"
},
"console": "integratedTerminal"
},
{
"name": "Functional Tests",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/.bin/jest",
"stopOnEntry": false,
"args": [
"--config",
"./test/jest.config.functional.js",
"--testPathPattern",
"./test/functional/index*",
"--debug=false",
"--verbose",
"--useStderr",
"--detectOpenHandles"
],
"cwd": "${workspaceRoot}",
"env": {
"BABEL_ENV": "testOldEnv",
"VERDACCIO_DEBUG": "true",
"VERDACCIO_DEBUG_INJECT": "true",
"NODE_DEBUG": "TO_DEBUG_REQUEST_REMOVE_THIS_request"
},
"preLaunchTask": "pre-test",
"console": "integratedTerminal",
"runtimeExecutable": null,
"runtimeArgs": ["--nolazy"]
},
{
"type": "node",
"request": "launch",
"name": "Verdaccio Compiled",
"preLaunchTask": "npm: code:build",
"program": "${workspaceRoot}/bin/verdaccio",
"args": ["-l", "0.0.0.0:4873"],
"console": "integratedTerminal"
}
]
}

21
.vscode/tasks.json vendored

@ -1,21 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build:webui",
"problemMatcher": []
},
{
"type": "npm",
"script": "code:build",
"problemMatcher": []
},
{
"label": "pre-test",
"dependsOn": ["npm: code:build", "npm: test:clean"]
}
]
}

@ -7,4 +7,9 @@ module.exports = {
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!**/partials/**', '!**/fixture/**'],
coveragePathIgnorePatterns: ['node_modules', 'fixtures'],
coverageThreshold: {
global: {
lines: 90,
},
},
};

@ -56,6 +56,7 @@
"@types/jsonwebtoken": "8.5.1",
"@types/request": "2.48.8",
"@types/semver": "7.3.9",
"@types/node-fetch": "2.5.3",
"@types/supertest": "2.0.12",
"@types/testing-library__jest-dom": "5.14.2",
"@types/validator": "13.7.1",
@ -103,7 +104,6 @@
"ts-node": "10.4.0",
"typescript": "4.5.5",
"update-ts-references": "2.4.1",
"verdaccio": "5.5.0",
"verdaccio-audit": "workspace:*",
"verdaccio-auth-memory": "workspace:*",
"verdaccio-htpasswd": "workspace:*",
@ -116,7 +116,7 @@
"docker": "docker build -t verdaccio/verdaccio:local . --no-cache",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
"lint": "eslint --max-warnings 46 \"**/*.{js,jsx,ts,tsx}\"",
"lint": "eslint --max-warnings 100 \"**/*.{js,jsx,ts,tsx}\"",
"test": "pnpm recursive test --filter ./packages",
"test:e2e:cli": "pnpm test --filter ...@verdaccio/e2e-cli",
"test:e2e:ui": "pnpm test --filter ...@verdaccio/e2e-ui",

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

@ -1,3 +1,10 @@
const config = require('../../jest/config');
module.exports = Object.assign({}, config, {});
module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 60,
},
},
});

@ -30,7 +30,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
@ -42,7 +42,6 @@
"@verdaccio/auth": "workspace:6.0.0-6-next.22",
"@verdaccio/config": "workspace:6.0.0-6-next.14",
"@verdaccio/core": "workspace:6.0.0-6-next.5",
"@verdaccio/hooks": "workspace:6.0.0-6-next.13",
"@verdaccio/logger": "workspace:6.0.0-6-next.11",
"@verdaccio/middleware": "workspace:6.0.0-6-next.22",
"@verdaccio/store": "workspace:6.0.0-6-next.22",
@ -61,7 +60,9 @@
"@verdaccio/server": "workspace:6.0.0-6-next.31",
"@verdaccio/types": "workspace:11.0.0-6-next.12",
"@verdaccio/test-helper": "workspace:1.1.0-6-next.1",
"supertest": "6.2.2"
"supertest": "6.2.2",
"nock": "13.2.8",
"mockdate": "3.0.5"
},
"funding": {
"type": "opencollective",

@ -3,106 +3,119 @@ import _ from 'lodash';
import mime from 'mime';
import { IAuth } from '@verdaccio/auth';
import { VerdaccioError, constants } from '@verdaccio/core';
import { constants, errorUtils } from '@verdaccio/core';
import { allow, media } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
import { Package } from '@verdaccio/types';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
export default function (route: Router, auth: IAuth, storage: Storage): void {
const can = allow(auth);
const tag_package_version = function (
const addTagPackageVersionMiddleware = async function (
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer
): $NextFunctionVer {
): Promise<$NextFunctionVer> {
if (_.isString(req.body) === false) {
return next('route');
return next(errorUtils.getBadRequest('version is missing'));
}
const tags = {};
tags[req.params.tag] = req.body;
storage.mergeTags(req.params.package, tags, function (err: Error): $NextFunctionVer {
if (err) {
return next(err);
}
try {
await storage.mergeTagsNext(req.params.package, tags);
res.status(constants.HTTP_STATUS.CREATED);
return next({ ok: constants.API_MESSAGE.TAG_ADDED });
});
return next({
ok: constants.API_MESSAGE.TAG_ADDED,
});
} catch (err) {
next(err);
}
};
// tagging a package.
route.put('/:package/:tag', can('publish'), media(mime.getType('json')), tag_package_version);
route.post(
'/-/package/:package/dist-tags/:tag',
route.put(
'/:package/:tag',
can('publish'),
media(mime.getType('json')),
tag_package_version
addTagPackageVersionMiddleware
);
route.put(
'/-/package/:package/dist-tags/:tag',
can('publish'),
media(mime.getType('json')),
tag_package_version
addTagPackageVersionMiddleware
);
route.delete(
'/-/package/:package/dist-tags/:tag',
can('publish'),
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
async function (
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer
): Promise<void> {
const tags = {};
tags[req.params.tag] = null;
storage.mergeTags(req.params.package, tags, function (err: VerdaccioError): $NextFunctionVer {
if (err) {
return next(err);
}
try {
await storage.mergeTagsNext(req.params.package, tags);
res.status(constants.HTTP_STATUS.CREATED);
return next({
ok: constants.API_MESSAGE.TAG_REMOVED,
});
});
} catch (err) {
next(err);
}
}
);
route.get(
'/-/package/:package/dist-tags',
can('access'),
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
storage.getPackage({
name: req.params.package,
uplinksLook: true,
req,
callback: function (err: VerdaccioError, info: Package): $NextFunctionVer {
if (err) {
return next(err);
}
next(info[constants.DIST_TAGS]);
},
});
async function (
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer
): Promise<void> {
const name = req.params.package;
const requestOptions = {
protocol: req.protocol,
headers: req.headers as any,
// FIXME: if we migrate to req.hostname, the port is not longer included.
host: req.host,
remoteAddress: req.socket.remoteAddress,
};
try {
const manifest = await storage.getPackageByOptions({
name,
uplinksLook: true,
requestOptions,
});
next(manifest[constants.DIST_TAGS]);
} catch (err) {
next(err);
}
}
);
route.post(
'/-/package/:package/dist-tags',
can('publish'),
function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
storage.mergeTags(
req.params.package,
req.body,
function (err: VerdaccioError): $NextFunctionVer {
if (err) {
return next(err);
}
res.status(constants.HTTP_STATUS.CREATED);
return next({
ok: constants.API_MESSAGE.TAG_UPDATED,
});
}
);
async function (
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer
): Promise<void> {
try {
await storage.mergeTagsNext(req.params.package, req.body);
res.status(constants.HTTP_STATUS.CREATED);
return next({
ok: constants.API_MESSAGE.TAG_UPDATED,
});
} catch (err) {
next(err);
}
}
);
}

@ -1,6 +1,5 @@
import bodyParser from 'body-parser';
import express, { Router } from 'express';
import semver from 'semver';
import { IAuth } from '@verdaccio/auth';
import {
@ -25,10 +24,6 @@ import v1Search from './v1/search';
import token from './v1/token';
import whoami from './whoami';
if (semver.lte(process.version, 'v15.0.0')) {
global.AbortController = require('abortcontroller-polyfill/dist/cjs-ponyfill').AbortController;
}
export default function (config: Config, auth: IAuth, storage: Storage): Router {
/* eslint new-cap:off */
const app = express.Router();
@ -62,7 +57,7 @@ export default function (config: Config, auth: IAuth, storage: Storage): Router
search(app);
user(app, auth, config);
distTags(app, auth, storage);
publish(app, auth, storage, config);
publish(app, auth, storage);
ping(app);
stars(app, storage);
// @ts-ignore

@ -2,7 +2,7 @@ import buildDebug from 'debug';
import { Router } from 'express';
import { IAuth } from '@verdaccio/auth';
import { HEADERS, errorUtils } from '@verdaccio/core';
import { HEADERS, HEADER_TYPE } from '@verdaccio/core';
import { allow } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
@ -10,27 +10,6 @@ import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/cust
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);
stream.on('content-length', function (content): void {
res.header('Content-Length', content);
});
stream.on('error', function (err): void {
return res.locals.report_error(err);
});
res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);
stream.pipe(res);
};
export default function (route: Router, auth: IAuth, storage: Storage): void {
const can = allow(auth);
@ -44,28 +23,22 @@ export default function (route: Router, auth: IAuth, storage: Storage): void {
): Promise<void> {
debug('init package by version');
const name = req.params.package;
let queryVersion = req.params.version;
let version = req.params.version;
const write = req.query.write === 'true';
const requestOptions = {
protocol: req.protocol,
headers: req.headers as any,
// FIXME: if we migrate to req.hostname, the port is not longer included.
host: req.host,
remoteAddress: req.socket.remoteAddress,
byPassCache: write,
};
try {
// TODO: this is just temporary while I migrate all plugins to use the new API
// the method will be renamed to getPackage again but Promise Based.
if (!storage.getPackageByOptions) {
throw errorUtils.getInternalError(
'getPackageByOptions not implemented, check pr-2750 for more details'
);
}
const manifest = await storage.getPackageByOptions({
name,
uplinksLook: true,
req,
version: queryVersion,
version,
requestOptions,
});
next(manifest);
@ -78,18 +51,72 @@ export default function (route: Router, auth: IAuth, storage: Storage): void {
route.get(
'/:scopedPackage/-/:scope/:filename',
can('access'),
function (req: $RequestExtend, res: $ResponseExtend): void {
const { scopedPackage, filename } = req.params;
async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {
const { pkg, filename } = req.params;
const abort = new AbortController();
try {
const stream = (await storage.getTarballNext(pkg, filename, {
signal: abort.signal,
// enableRemote: true,
})) as any;
downloadStream(scopedPackage, filename, storage, req, res);
stream.on('content-length', (size) => {
res.header(HEADER_TYPE.CONTENT_LENGTH, size);
});
stream.once('error', (err) => {
res.locals.report_error(err);
next(err);
});
req.on('abort', () => {
debug('request aborted for %o', req.url);
abort.abort();
});
res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);
stream.pipe(res);
} catch (err: any) {
// console.log('catch API error request', err);
res.locals.report_error(err);
next(err);
}
}
);
route.get(
'/:package/-/:filename',
'/:pkg/-/:filename',
can('access'),
function (req: $RequestExtend, res: $ResponseExtend): void {
downloadStream(req.params.package, req.params.filename, storage, req, res);
async function (req: $RequestExtend, res: $ResponseExtend, next): Promise<void> {
const { pkg, filename } = req.params;
const abort = new AbortController();
try {
const stream = (await storage.getTarballNext(pkg, filename, {
signal: abort.signal,
// enableRemote: true,
})) as any;
stream.on('content-length', (size) => {
res.header(HEADER_TYPE.CONTENT_LENGTH, size);
});
stream.once('error', (err) => {
res.locals.report_error(err);
next(err);
});
req.on('abort', () => {
debug('request aborted for %o', req.url);
abort.abort();
});
res.header(HEADERS.CONTENT_TYPE, HEADERS.OCTET_STREAM);
stream.pipe(res);
} catch (err: any) {
// console.log('catch API error request', err);
res.locals.report_error(err);
next(err);
}
}
);
}

@ -1,40 +1,21 @@
import buildDebug from 'debug';
import { Router } from 'express';
import _ from 'lodash';
import mime from 'mime';
import Path from 'path';
import { IAuth } from '@verdaccio/auth';
import {
API_ERROR,
API_MESSAGE,
DIST_TAGS,
HEADERS,
HTTP_STATUS,
errorUtils,
} from '@verdaccio/core';
import { notify } from '@verdaccio/hooks';
import { API_MESSAGE, HTTP_STATUS } from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { allow, expectJson, media } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
import { Callback, CallbackAction, Config, MergeTags, Package, Version } from '@verdaccio/types';
import { hasDiffOneKey, isObject, validateMetadata } from '@verdaccio/utils';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
import star from './star';
import { isPublishablePackage, isRelatedToDeprecation } from './utils';
// import star from './star';
// import { isPublishablePackage, isRelatedToDeprecation } from './utils';
const debug = buildDebug('verdaccio:api:publish');
export default function publish(
router: Router,
auth: IAuth,
storage: Storage,
config: Config
): void {
const can = allow(auth);
/**
/**
* Publish a package / update package / un/start a package
*
* There are multiples scenarios here to be considered:
@ -73,13 +54,27 @@ export default function publish(
*
* Example flow of unpublish.
*
* npm http fetch GET 200 http://localhost:4873/@scope%2ftest1?write=true 1680ms
* npm http fetch PUT 201 http://localhost:4873/@scope%2ftest1/-rev/14-5d500cfce92f90fd
* 956606ms attempt #2
* npm http fetch GET 200 http://localhost:4873/@scope%2ftest1?write=true 1601ms
* npm http fetch DELETE 201 http://localhost:4873/@scope%2ftest1/-/test1-1.0.3.tgz/-rev/16
* -e11c8db282b2d992 19ms
* There are two possible flows:
*
* - Remove all pacakges (entirely)
* eg: npm unpublish package-name@* --force
* eg: npm unpublish package-name --force
*
* npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms
* npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms
*
* - Remove a specific version
* eg: npm unpublish package-name@1.0.0 --force
*
* Get fresh manifest
* npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1680ms
* Update manifest without the version to be unpublished
* npm http fetch PUT 201 http://localhost:4873/custom-name/-rev/14-5d500cfce92f90fd 956606ms
* Get fresh manifest (revision should be different)
* npm http fetch GET 200 http://localhost:4873/custom-name?write=true 1601ms
* Remove the tarball
* npm http fetch DELETE 201 http://localhost:4873/custom-name/-/test1-1.0.3.tgz/-rev/16-e11c8db282b2d992 19ms
*
* 3. Star a package
*
* Permissions: start a package depends of the publish and unpublish permissions, there is no
@ -98,12 +93,24 @@ export default function publish(
}
*
*/
export default function publish(router: Router, auth: IAuth, storage: Storage): void {
const can = allow(auth);
// publish (update manifest) v6
router.put(
'/:package/:_rev?/:revision?',
'/:package',
can('publish'),
media(mime.getType('json')),
expectJson,
publishPackage(storage, config, auth)
publishPackageNext(storage)
);
// unpublish a pacakge v6
router.put(
'/:package/-rev/:revision',
can('unpublish'),
media(mime.getType('json')),
expectJson,
publishPackageNext(storage)
);
/**
@ -111,330 +118,102 @@ export default function publish(
*
* This scenario happens when the first call detect there is only one version remaining
* in the metadata, then the client decides to DELETE the resource
* npm http fetch GET 304 http://localhost:4873/@scope%2ftest1?write=true 1076ms (from cache)
npm http fetch DELETE 201 http://localhost:4873/@scope%2ftest1/-rev/18-d8ebe3020bd4ac9c 22ms
* npm http fetch GET 304 http://localhost:4873/package-name?write=true 1076ms (from cache)
* npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms
*/
router.delete('/:package/-rev/*', can('unpublish'), unPublishPackage(storage));
// v6
router.delete(
'/:package/-rev/:revision',
can('unpublish'),
async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
const packageName = req.params.package;
const rev = req.params.revision;
// removing a tarball
logger.debug({ packageName }, `unpublishing @{packageName}`);
try {
await storage.removePackage(packageName, rev);
debug('package %s unpublished', packageName);
res.status(HTTP_STATUS.CREATED);
return next({ ok: API_MESSAGE.PKG_REMOVED });
} catch (err) {
return next(err);
}
}
);
/*
Remove a tarball, this happens when npm unpublish a package unique version.
npm http fetch DELETE 201 http://localhost:4873/package-name/-rev/18-d8ebe3020bd4ac9c 22ms
*/
router.delete(
'/:package/-/:filename/-rev/:revision',
can('unpublish'),
can('publish'),
removeTarball(storage)
);
// uploading package tarball
router.put(
'/:package/-/:filename/*',
can('publish'),
media(HEADERS.OCTET_STREAM),
uploadPackageTarball(storage)
);
// adding a version
router.put(
'/:package/:version/-tag/:tag',
can('publish'),
media(mime.getType('json')),
expectJson,
addVersion(storage)
);
}
/**
* Publish a package
*/
export function publishPackage(storage: Storage, config: Config, auth: IAuth): any {
const starApi = star(storage);
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
const packageName = req.params.package;
debug('publishing or updating a new version for %o', packageName);
/**
* Write tarball of stream data from package clients.
*/
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(Buffer.from(data.data, 'base64'));
stream.done();
};
/**
* Add new package version in storage
*/
const createVersion = function (version: string, metadata: Version, cb: CallbackAction): void {
debug('add a new package version %o to storage %o', version, metadata);
storage.addVersion(packageName, version, metadata, null, cb);
};
/**
* Add new tags in storage
*/
const addTags = function (tags: MergeTags, cb: CallbackAction): 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,
success: true,
});
}
/**
* npm-registry-client 0.3+ embeds tarball into the json upload
* issue https://github.com/rlidwka/sinopia/issues/31, dealing with it here:
*/
const isInvalidBodyFormat =
isObject(_attachments) === false ||
hasDiffOneKey(_attachments) ||
isObject(versions) === false ||
hasDiffOneKey(versions);
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(errorUtils.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);
}
// at this point document is either created or existed before
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);
}
const versionToPublish = Object.keys(versions)[0];
versions[versionToPublish].readme =
_.isNil(metadataCopy.readme) === false ? String(metadataCopy.readme) : '';
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: any) {
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 });
});
});
}
);
};
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)) {
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) {
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 {
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: any) {
debug('error on publish, bad package format %o', packageName);
logger.error({ packageName }, 'error on publish, bad package data for @{packageName}');
return next(errorUtils.getBadData(API_ERROR.BAD_PACKAGE_DATA));
}
};
}
/**
* un-publish a package
*/
export function unPublishPackage(storage: Storage) {
return async function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
const packageName = req.params.package;
logger.debug({ packageName }, `unpublishing @{packageName}`);
try {
await storage.removePackage(packageName);
} catch (err) {
if (err) {
return next(err);
}
}
res.status(HTTP_STATUS.CREATED);
return next({ ok: API_MESSAGE.PKG_REMOVED });
};
}
/**
* Delete tarball
*/
export function removeTarball(storage: Storage) {
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
const packageName = req.params.package;
const { filename, revision } = req.params;
logger.debug(
{ packageName, filename, revision },
`removing a tarball for @{packageName}-@{tarballName}-@{revision}`
);
storage.removeTarball(packageName, filename, revision, function (err) {
if (err) {
return next(err);
}
res.status(HTTP_STATUS.CREATED);
async function (
req: $RequestExtend,
res: $ResponseExtend,
next: $NextFunctionVer
): Promise<void> {
const packageName = req.params.package;
const { filename, revision } = req.params;
logger.debug(
{ packageName, filename, revision },
`success remove tarball for @{packageName}-@{tarballName}-@{revision}`
`removing a tarball for @{packageName}-@{tarballName}-@{revision}`
);
return next({ ok: API_MESSAGE.TARBALL_REMOVED });
});
};
}
/**
* Adds a new version
*/
export function addVersion(storage: Storage) {
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);
try {
await storage.removeTarball(packageName, filename, revision);
res.status(HTTP_STATUS.CREATED);
storage.addVersion(packageName, version, req.body, tag, function (error) {
if (error) {
debug('error on add new version');
return next(error);
logger.debug(
{ packageName, filename, revision },
`success remove tarball for @{packageName}-@{tarballName}-@{revision}`
);
return next({ ok: API_MESSAGE.TARBALL_REMOVED });
} catch (err) {
return next(err);
}
debug('success on add new version');
res.status(HTTP_STATUS.CREATED);
return next({
ok: API_MESSAGE.PKG_PUBLISHED,
});
});
};
}
);
}
/**
* uploadPackageTarball
*/
export function uploadPackageTarball(storage: Storage) {
return function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
export function publishPackageNext(storage: Storage): any {
return async function (
req: $RequestExtend,
_res: $ResponseExtend,
next: $NextFunctionVer
): Promise<void> {
const ac = new AbortController();
const packageName = req.params.package;
const stream = storage.addTarball(packageName, req.params.filename);
req.pipe(stream);
const { revision } = req.params;
const metadata = req.body;
// checking if end event came before closing
let complete = false;
req.on('end', function () {
complete = true;
stream.done();
});
req.on('close', function () {
if (!complete) {
stream.abort();
}
});
stream.on('error', function (err) {
return res.locals.report_error(err);
});
stream.on('success', function () {
res.status(HTTP_STATUS.CREATED);
return next({
ok: API_MESSAGE.TARBALL_UPLOADED,
try {
debug('publishing %s', packageName);
await storage.updateManifest(metadata, {
name: packageName,
revision,
signal: ac.signal,
requestOptions: {
host: req.hostname,
protocol: req.protocol,
// @ts-ignore
headers: req.headers,
},
});
});
_res.status(HTTP_STATUS.CREATED);
return next({
// TODO: this could be also Package Updated based on the
// action, deprecate, star, publish new version, or create a package
// the mssage some return from the method
ok: API_MESSAGE.PKG_CREATED,
success: true,
});
} catch (err: any) {
// TODO: review if we need the abort controller here
ac.abort();
next(err);
}
};
}

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import buildDebug from 'debug';
import { Response } from 'express';
import _ from 'lodash';
@ -27,61 +28,61 @@ export default function (
return (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
const name = req.params.package;
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);
}
// 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('succes update package for %o', name);
// res.status(HTTP_STATUS.OK);
// next({
// success: true,
// });
// };
debug('get package info package for %o', name);
// @ts-ignore
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];
const remoteUsername = req.remote_user.name;
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();
}
const users = isStar
? {
...localStarUsers,
[remoteUsername]: true,
}
: _.reduce(
localStarUsers,
(users, value, key) => {
if (key !== remoteUsername) {
users[key] = value;
}
return users;
},
{}
);
debug('update package for %o', name);
storage.changePackage(name, { ...info, users }, req.body._rev, function (err) {
afterChangePackage(err);
});
},
});
// 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];
// const remoteUsername = req.remote_user.name;
// 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();
// }
// const users = isStar
// ? {
// ...localStarUsers,
// [remoteUsername]: true,
// }
// : _.reduce(
// localStarUsers,
// (users, value, key) => {
// if (key !== remoteUsername) {
// users[key] = value;
// }
// return users;
// },
// {}
// );
// debug('update package for %o', name);
// storage.changePackage(name, { ...info, users }, req.body._rev, function (err) {
// afterChangePackage(err);
// });
// },
// });
};
}

@ -3,34 +3,32 @@ import _ from 'lodash';
import { HTTP_STATUS, USERS } from '@verdaccio/core';
import { Storage } from '@verdaccio/store';
import { Package } from '@verdaccio/types';
import { Version } from '@verdaccio/types';
import { $NextFunctionVer, $RequestExtend } from '../types/custom';
type Packages = Package[];
export default function (route: Router, storage: Storage): void {
route.get(
'/-/_view/starredByUser',
(req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
async (req: $RequestExtend, res: Response, next: $NextFunctionVer): Promise<void> => {
const remoteUsername = req.remote_user.name;
storage.getLocalDatabase((err, localPackages: Packages) => {
if (err) {
return next(err);
}
try {
const localPackages: Version[] = await storage.getLocalDatabaseNext();
const filteredPackages: Packages = localPackages.filter((localPackage: Package) =>
const filteredPackages: Version[] = localPackages.filter((localPackage: Version) =>
_.keys(localPackage[USERS]).includes(remoteUsername)
);
res.status(HTTP_STATUS.OK);
next({
rows: filteredPackages.map((filteredPackage: Package) => ({
rows: filteredPackages.map((filteredPackage: Version) => ({
value: filteredPackage.name,
})),
});
});
} catch (err: any) {
return next(err);
}
}
);
}

@ -8,7 +8,7 @@ import { createRemoteUser } from '@verdaccio/config';
import { API_ERROR, API_MESSAGE, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { Config, RemoteUser } from '@verdaccio/types';
import { getAuthenticatedMessage, validatePassword } from '@verdaccio/utils';
import { getAuthenticatedMessage, mask, validatePassword } from '@verdaccio/utils';
import { $NextFunctionVer, $RequestExtend } from '../types/custom';
@ -28,6 +28,23 @@ export default function (route: Router, auth: IAuth, config: Config): void {
}
);
/**
*
* body example
* req.body = {
_id: "org.couchdb.user:jjjj",
name: "jjjj",
password: "jjjj",
type: "user",
roles: [],
date: "2022-07-08T15:51:04.002Z",
}
*
* @export
* @param {Router} route
* @param {IAuth} auth
* @param {Config} config
*/
route.put(
'/-/user/:org_couchdb_user/:_rev?/:revision?',
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
@ -92,7 +109,7 @@ export default function (route: Router, auth: IAuth, config: Config): void {
const token =
name && password ? await getApiToken(auth, config, user, password) : undefined;
debug('adduser: new token %o', token);
debug('adduser: new token %o', mask(token as string, 4));
if (!token) {
return next(errorUtils.getUnauthorized());
}

@ -1,25 +0,0 @@
import _ from 'lodash';
import { Package } from '@verdaccio/types';
/**
* Check whether the package metadta has enough data to be published
* @param pkg metadata
*/
export function isPublishablePackage(pkg: Package): boolean {
// TODO: we can do better, no need get keys
const keys: string[] = Object.keys(pkg);
return _.includes(keys, 'versions');
}
export function isRelatedToDeprecation(pkgInfo: Package): boolean {
const { versions } = pkgInfo;
for (const version in versions) {
if (Object.prototype.hasOwnProperty.call(versions[version], 'deprecated')) {
return true;
}
}
return false;
}

@ -5,7 +5,7 @@ import { IAuth } from '@verdaccio/auth';
import { HTTP_STATUS, searchUtils } from '@verdaccio/core';
import { logger } from '@verdaccio/logger';
import { Storage } from '@verdaccio/store';
import { Package } from '@verdaccio/types';
import { Manifest } from '@verdaccio/types';
const debug = buildDebug('verdaccio:api:search');
@ -16,7 +16,7 @@ const debug = buildDebug('verdaccio:api:search');
* req: 'GET /-/v1/search?text=react&size=20&frpom=0&quality=0.65&popularity=0.98&maintenance=0.5'
*/
export default function (route, auth: IAuth, storage: Storage): void {
function checkAccess(pkg: any, auth: any, remoteUser): Promise<Package | null> {
function checkAccess(pkg: any, auth: any, remoteUser): Promise<Manifest | null> {
return new Promise((resolve, reject) => {
auth.allow_access({ packageName: pkg?.package?.name }, remoteUser, function (err, allowed) {
if (err) {
@ -49,7 +49,7 @@ export default function (route, auth: IAuth, storage: Storage): void {
from = parseInt(from, 10) || 0;
try {
data = await storage.searchManager?.search({
data = await storage.search({
query,
url,
abort,

@ -1,36 +1,20 @@
import buildDebug from 'debug';
import { Response, Router } from 'express';
import { errorUtils } from '@verdaccio/core';
import { $NextFunctionVer, $RequestExtend } from '../types/custom';
const debug = buildDebug('verdaccio:api:user');
export default function (route: Router): void {
route.get('/whoami', (req: $RequestExtend, res: Response, next: $NextFunctionVer): void => {
debug('whoami: reditect');
if (req.headers.referer === 'whoami') {
const username = req.remote_user.name;
// FIXME: this service should return 401 if user missing
// if (!username) {
// debug('whoami: user not found');
// return next(getUnauthorized('Unauthorized'));
// }
debug('whoami: logged by user');
return next({ username: username });
} else {
debug('whoami: redirect next route');
// redirect to the route below
return next('route');
route.get('/-/whoami', (req: $RequestExtend, _res: Response, next: $NextFunctionVer): any => {
// remote_user is set by the auth middleware
const username = req?.remote_user?.name;
if (!username) {
debug('whoami: user not found');
return next(errorUtils.getUnauthorized('Unauthorized'));
}
});
route.get('/-/whoami', (req: $RequestExtend, res: Response, next: $NextFunctionVer): any => {
const username = req.remote_user.name;
// FIXME: this service should return 401 if user missing
// if (!username) {
// debug('whoami: user not found');
// return next(getUnauthorized('Unauthorized'));
// }
debug('whoami: response %o', username);
return next({ username: username });

@ -1,83 +1,126 @@
import bodyParser from 'body-parser';
import express, { Application } from 'express';
import { Application } from 'express';
import _ from 'lodash';
import path from 'path';
import supertest from 'supertest';
import { Auth, IAuth } from '@verdaccio/auth';
import { Config, parseConfigFile } from '@verdaccio/config';
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { errorReportingMiddleware, final, handleError } from '@verdaccio/middleware';
import { parseConfigFile } from '@verdaccio/config';
import { HEADERS, HEADER_TYPE, HTTP_STATUS, TOKEN_BEARER } from '@verdaccio/core';
import { setup } from '@verdaccio/logger';
import { Storage } from '@verdaccio/store';
import { generatePackageMetadata } from '@verdaccio/test-helper';
import {
generatePackageMetadata,
initializeServer as initializeServerHelper,
} from '@verdaccio/test-helper';
import { GenericBody } from '@verdaccio/types';
import { buildToken, generateRandomHexString } from '@verdaccio/utils';
import apiEndpoints from '../../src';
import apiMiddleware from '../../src';
const getConf = (conf) => {
setup();
export const getConf = (conf) => {
const configPath = path.join(__dirname, 'config', conf);
return parseConfigFile(configPath);
const config = parseConfigFile(configPath);
// custom config to avoid conflict with other tests
config.auth.htpasswd.file = `${config.auth.htpasswd.file}-${generateRandomHexString()}`;
return config;
};
// TODO: replace by @verdaccio/test-helper
export async function initializeServer(configName): Promise<Application> {
const app = express();
const config = new Config(getConf(configName));
const storage = new Storage(config);
await storage.init(config, []);
const auth: IAuth = new Auth(config);
// TODO: this might not be need it, used in apiEndpoints
app.use(bodyParser.json({ strict: false, limit: '10mb' }));
// @ts-ignore
app.use(errorReportingMiddleware);
// @ts-ignore
app.use(apiEndpoints(config, auth, storage));
// @ts-ignore
app.use(handleError);
// @ts-ignore
app.use(final);
app.use(function (request, response) {
response.status(590);
console.log('respo', response);
response.json({ error: 'cannot handle this' });
});
return app;
return initializeServerHelper(getConf(configName), [apiMiddleware], Storage);
}
export function publishVersion(app, _configFile, pkgName, version): supertest.Test {
const pkgMetadata = generatePackageMetadata(pkgName, version);
export function createUser(app, name: string, password: string): supertest.Test {
return supertest(app)
.put(`/-/user/org.couchdb.user:${name}`)
.send({
name: name,
password: password,
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED);
}
export async function getNewToken(app: any, credentials: any): Promise<string> {
const response = await createUser(app, credentials.name, credentials.password);
const { token, ok } = response.body;
expect(ok).toBeDefined();
expect(token).toBeDefined();
expect(typeof token).toBe('string');
return token;
}
export async function generateTokenCLI(app, token, payload): Promise<any> {
return supertest(app)
.post('/-/npm/v1/tokens')
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(JSON.stringify(payload))
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET);
}
export async function deleteTokenCLI(app, token, tokenToDelete): Promise<any> {
return supertest(app)
.delete(`/-/npm/v1/tokens/token/${tokenToDelete}`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
}
export function publishVersionWithToken(
app,
pkgName: string,
version: string,
token: string,
distTags?: GenericBody
): supertest.Test {
const pkgMetadata = generatePackageMetadata(pkgName, version, distTags);
return supertest(app)
.put(`/${encodeURIComponent(pkgName)}`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
.send(JSON.stringify(pkgMetadata))
.set('accept', HEADERS.GZIP)
.set(HEADER_TYPE.ACCEPT_ENCODING, HEADERS.JSON);
}
export function publishVersion(
app,
pkgName: string,
version: string,
distTags?: GenericBody
): supertest.Test {
const pkgMetadata = generatePackageMetadata(pkgName, version, distTags);
return supertest(app)
.put(`/${encodeURIComponent(pkgName)}`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(JSON.stringify(pkgMetadata))
.set('accept', HEADERS.GZIP)
.set(HEADER_TYPE.ACCEPT_ENCODING, HEADERS.JSON)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);
.set(HEADER_TYPE.ACCEPT_ENCODING, HEADERS.JSON);
}
export async function publishTaggedVersion(
app,
configFile,
pkgName: string,
version: string,
tag: string
) {
const pkgMetadata = generatePackageMetadata(pkgName, version, {
[tag]: version,
});
export function getDisTags(app, pkgName) {
return supertest(app)
.put(
`/${encodeURIComponent(pkgName)}/${encodeURIComponent(version)}/-tag/${encodeURIComponent(
tag
)}`
)
.get(`/-/package/${encodeURIComponent(pkgName)}/dist-tags`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(JSON.stringify(pkgMetadata))
.expect(HTTP_STATUS.CREATED)
.set('accept', HEADERS.GZIP)
.set(HEADER_TYPE.ACCEPT_ENCODING, HEADERS.JSON)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON);
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
}
export function getPackage(
app: any,
token: string,
pkgName: string,
statusCode: number = HTTP_STATUS.OK
): supertest.Test {
const test = supertest(app).get(`/${pkgName}`);
if (_.isNil(token) === false || _.isEmpty(token) === false) {
test.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token));
}
return test.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET).expect(statusCode);
}

@ -0,0 +1,25 @@
storage: ./storage
auth:
htpasswd:
file: ./htpasswd-distTag
web:
enable: true
title: verdaccio
publish:
allow_offline: false
uplinks:
log: { type: stdout, format: pretty, level: trace }
packages:
'@*/*':
access: $anonymous
publish: $anonymous
'**':
access: $anonymous
publish: $anonymous
_debug: true

@ -1,13 +1,8 @@
store:
memory:
limit: 1000
storage: ./storage
auth:
auth-memory:
users:
test:
name: test
password: test
htpasswd:
file: ./htpasswd-package
web:
enable: true

@ -1,10 +1,6 @@
store:
memory:
limit: 10
auth:
auth-memory:
users:
htpasswd:
file: ./htpasswd-ping
web:
enable: true
title: verdaccio

@ -1,14 +1,6 @@
store:
memory:
limit: 1000
auth:
auth-memory:
users:
test:
name: test
password: test
htpasswd:
file: ./htpasswd-publish
web:
enable: true
title: verdaccio
@ -24,7 +16,9 @@ packages:
'@*/*':
access: $anonymous
publish: $anonymous
unpublish: $anonymous
'**':
access: $anonymous
publish: $anonymous
unpublish: $anonymous
_debug: true

@ -0,0 +1,29 @@
storage: ./storage
auth:
htpasswd:
file: ./htpasswd-search
web:
enable: true
title: verdaccio
uplinks:
log: { type: stdout, format: pretty, level: trace }
packages:
'private-*':
access: $all
publish: jota
'@private/*':
access: $all
publish: jota
'@*/*':
access: $all
publish: $authenticated
'**':
access: $all
publish: $authenticated
_debug: true

@ -1,6 +1,3 @@
storage: ./storage
plugins: ./plugins
security:
api:
jwt:
@ -9,6 +6,8 @@ security:
# to avoid invalid verification token, more info on JWT page
notBefore: 0
storage: ./storage
auth:
htpasswd:
file: ./htpasswd
@ -20,7 +19,9 @@ packages:
'only-you-can-publish':
access: $authenticated
publish: $authenticated
log: { type: stdout, format: pretty, level: warn }
log: { type: stdout, format: pretty, level: debug }
## enable token for testing
flags:
## enable token for testing
token: true

@ -0,0 +1,19 @@
storage: ./storage
auth:
htpasswd:
file: ./htpasswd
packages:
'@token/*':
access: $authenticated
publish: $authenticated
'only-you-can-publish':
access: $authenticated
publish: $authenticated
log: { type: stdout, format: pretty, level: debug }
## enable token for testing
flags:
token: true

@ -8,6 +8,10 @@ auth:
htpasswd:
file: ./htpasswd
uplinks:
ver:
url: https://registry.verdaccio.org
security:
api:
jwt:
@ -18,18 +22,16 @@ packages:
'@*/*':
access: $all
publish: $authenticated
proxy: remote
'vue':
access: $authenticated
publish: $authenticated
proxy: remote
proxy: ver
'**':
access: $all
publish: $authenticated
proxy: remote
middlewares:
audit:
enabled: true
log: { type: stdout, format: pretty, level: warn }
log: { type: stdout, format: pretty, level: info }

@ -1,21 +1,13 @@
store:
memory:
limit: 1000
auth:
auth-memory:
users:
test:
name: test
password: test
htpasswd:
file: ./htpasswd-user
web:
enable: true
title: verdaccio
uplinks:
npmjs:
url: https://registry.npmjs.org/
ver:
url: https://registry.verdaccio.org
log: { type: stdout, format: pretty, level: trace }
@ -24,13 +16,15 @@ packages:
access: $all
publish: $all
unpublish: $all
proxy: npmjs
'verdaccio':
access: $all
publish: $all
'vue':
access: $authenticated
publish: $authenticated
proxy: ver
'**':
access: $all
publish: $all
unpublish: $all
proxy: npmjs
_debug: true

@ -1,14 +1,3 @@
store:
memory:
limit: 1000
auth:
auth-memory:
users:
test:
name: test
password: test
web:
enable: true
title: verdaccio
@ -19,6 +8,10 @@ uplinks:
log: { type: stdout, format: pretty, level: trace }
auth:
htpasswd:
file: ./htpasswd-whoami
packages:
'@*/*':
access: $all

@ -0,0 +1,76 @@
import supertest from 'supertest';
import { API_MESSAGE, HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { getDisTags, initializeServer, publishVersion } from './_helper';
describe('package', () => {
let app;
beforeEach(async () => {
app = await initializeServer('distTag.yaml');
});
test.each([['foo'], ['@scope/foo']])('should display dist-tag (npm dist-tag ls)', async (pkg) => {
await publishVersion(app, pkg, '1.0.0');
await publishVersion(app, pkg, '1.0.1');
const response = await getDisTags(app, pkg);
expect(response.body).toEqual({ latest: '1.0.1' });
});
test('should add a version to a tag (npm dist-tag add)', async () => {
await publishVersion(app, encodeURIComponent('foo'), '1.0.0');
await publishVersion(app, encodeURIComponent('foo'), '1.0.1');
const response = await supertest(app)
.put(`/${encodeURIComponent('foo')}/test`)
.set(HEADERS.ACCEPT, HEADERS.GZIP)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(JSON.stringify('1.0.1'))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED);
expect(response.body.ok).toEqual(API_MESSAGE.TAG_ADDED);
const response2 = await getDisTags(app, 'foo');
expect(response2.body).toEqual({ latest: '1.0.1', test: '1.0.1' });
});
test('should fails if a version is missing (npm dist-tag add)', async () => {
await publishVersion(app, encodeURIComponent('foo'), '1.0.0');
await publishVersion(app, encodeURIComponent('foo'), '1.0.1');
await supertest(app)
.put(`/${encodeURIComponent('foo')}/test`)
.set(HEADERS.ACCEPT, HEADERS.GZIP)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(JSON.stringify({}))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.BAD_REQUEST);
});
test('should delete a previous added tag (npm dist-tag rm)', async () => {
await publishVersion(app, encodeURIComponent('foo'), '1.0.0');
await publishVersion(app, encodeURIComponent('foo'), '1.0.1');
const response = await supertest(app)
.put(`/${encodeURIComponent('foo')}/beta`)
.set(HEADERS.ACCEPT, HEADERS.GZIP)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(JSON.stringify('1.0.1'))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED);
expect(response.body.ok).toEqual(API_MESSAGE.TAG_ADDED);
const response2 = await getDisTags(app, 'foo');
expect(response2.body).toEqual({ latest: '1.0.1', beta: '1.0.1' });
const response3 = await supertest(app)
.delete(`/-/package/${encodeURIComponent('foo')}/dist-tags/beta`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED);
expect(response3.body.ok).toEqual(API_MESSAGE.TAG_REMOVED);
const response4 = await getDisTags(app, 'foo');
expect(response4.body).toEqual({ latest: '1.0.1' });
});
});

@ -2,32 +2,7 @@ import supertest from 'supertest';
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { $RequestExtend, $ResponseExtend } from '../../types/custom';
import { initializeServer, publishTaggedVersion, publishVersion } from './_helper';
const mockApiJWTmiddleware = jest.fn(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: 'foo', groups: [], real_groups: [] };
_next();
}
);
jest.mock('@verdaccio/auth', () => ({
Auth: class {
apiJWTmiddleware() {
return mockApiJWTmiddleware();
}
allow_access(_d, _f, cb) {
// always allow access
cb(null, true);
}
allow_publish(_d, _f, cb) {
// always allow publish
cb(null, true);
}
},
}));
import { initializeServer, publishVersion } from './_helper';
describe('package', () => {
let app;
@ -35,57 +10,39 @@ describe('package', () => {
app = await initializeServer('package.yaml');
});
test('should return a package', async () => {
await publishVersion(app, 'package.yaml', 'foo', '1.0.0');
return new Promise((resolve) => {
supertest(app)
.get('/foo')
.set('Accept', HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK)
.then((response) => {
expect(response.body.name).toEqual('foo');
resolve(response);
});
});
});
test('should return a package by version', async () => {
await publishVersion(app, 'package.yaml', 'foo2', '1.0.0');
return new Promise((resolve) => {
supertest(app)
.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('foo2');
resolve(response);
});
});
});
// FIXME: 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.1')
.set('Accept', HEADERS.JSON)
test.each([['foo'], ['@scope/foo']])('should return a foo private package', async (pkg) => {
await publishVersion(app, pkg, '1.0.0');
const response = await supertest(app)
.get(`/${pkg}`)
.set(HEADERS.ACCEPT, HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CREATED)
.then((response) => {
expect(response.body.name).toEqual('foo3');
done();
});
.expect(HTTP_STATUS.OK);
expect(response.body.name).toEqual(pkg);
});
test('should return 404', async () => {
return supertest(app)
.get('/404-not-found')
.set('Accept', HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.NOT_FOUND);
});
test.each([['foo'], ['@scope/foo']])(
'should return a foo private package by version',
async (pkg) => {
await publishVersion(app, pkg, '1.0.0');
const response = await supertest(app)
.get(`/${pkg}`)
.set(HEADERS.ACCEPT, HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response.body.name).toEqual(pkg);
}
);
test.each([['foo'], ['@scope/foo']])(
'should return a foo private package by version',
async (pkg) => {
await publishVersion(app, pkg, '1.0.0');
const response = await supertest(app)
.get(`/${pkg}`)
.set(HEADERS.ACCEPT, HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response.body.name).toEqual(pkg);
}
);
});

@ -6,11 +6,12 @@ import { initializeServer } from './_helper';
describe('ping', () => {
test('should return the reply the ping', async () => {
return supertest(await initializeServer('ping.yaml'))
const app = await initializeServer('ping.yaml');
const response = await supertest(app)
.get('/-/ping')
.set('Accept', HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK)
.then((response) => expect(response.body).toEqual({}));
.expect(HTTP_STATUS.OK);
expect(response.body).toEqual({});
});
});

@ -60,9 +60,9 @@ describe('publish', () => {
describe('handle invalid publish formats', () => {
const pkgName = 'test';
const pkgMetadata = generatePackageMetadata(pkgName, '1.0.0');
test.skip('should fail on publish a bad _attachments package', async (done) => {
test('should fail on publish a bad _attachments package', async () => {
const app = await initializeServer('publish.yaml');
return supertest(app)
const response = await supertest(app)
.put(`/${encodeURIComponent(pkgName)}`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(
@ -73,12 +73,8 @@ describe('publish', () => {
)
)
.set('accept', HEADERS.GZIP)
.expect(HTTP_STATUS.BAD_REQUEST)
.then((response) => {
console.log('response.body', response.body);
expect(response.body.error).toEqual(API_ERROR.UNSUPORTED_REGISTRY_CALL);
done();
});
.expect(HTTP_STATUS.BAD_REQUEST);
expect(response.body.error).toEqual(API_ERROR.UNSUPORTED_REGISTRY_CALL);
});
test('should fail on publish a bad versions package', async () => {
@ -97,7 +93,6 @@ describe('publish', () => {
.set('accept', HEADERS.GZIP)
.expect(HTTP_STATUS.BAD_REQUEST)
.then((response) => {
console.log('response.body', response.body);
expect(response.body.error).toEqual(API_ERROR.UNSUPORTED_REGISTRY_CALL);
resolve(response);
});
@ -109,7 +104,7 @@ describe('publish', () => {
test('should publish a package', async () => {
const app = await initializeServer('publish.yaml');
return new Promise((resolve) => {
publishVersion(app, 'publish.yaml', 'foo', '1.0.0')
publishVersion(app, 'foo', '1.0.0')
.expect(HTTP_STATUS.CREATED)
.then((response) => {
expect(response.body.ok).toEqual(API_MESSAGE.PKG_CREATED);
@ -126,13 +121,7 @@ describe('publish', () => {
supertest(app)
.put(`/${encodeURIComponent(pkgName)}`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.send(
JSON.stringify(
Object.assign({}, pkgMetadata, {
_attachments: null,
})
)
)
.send(JSON.stringify(Object.assign({}, pkgMetadata)))
.set('accept', HEADERS.GZIP)
.expect(HTTP_STATUS.CREATED)
.then((response) => {
@ -173,12 +162,11 @@ describe('publish', () => {
test('should fails on publish a duplicated package', async () => {
const app = await initializeServer('publish.yaml');
await publishVersion(app, 'publish.yaml', 'foo', '1.0.0');
await publishVersion(app, 'foo', '1.0.0');
return new Promise((resolve) => {
publishVersion(app, 'publish.yaml', 'foo', '1.0.0')
publishVersion(app, 'foo', '1.0.0')
.expect(HTTP_STATUS.CONFLICT)
.then((response) => {
console.log('response.body', response.body);
expect(response.body.error).toEqual(API_ERROR.PACKAGE_EXIST);
resolve(response);
});
@ -186,14 +174,61 @@ describe('publish', () => {
});
describe('unpublish a package', () => {
let app;
beforeEach(async () => {
app = await initializeServer('publish.yaml');
await publishVersion(app, 'publish.yaml', 'foo', '1.0.0');
test('should unpublish entirely a package', async () => {
const app = await initializeServer('publish.yaml');
await publishVersion(app, 'foo', '1.0.0');
const response = await supertest(app)
// FIXME: should be filtered by revision to avoid
// conflicts
.delete(`/${encodeURIComponent('foo')}/-rev/xxx`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HTTP_STATUS.CREATED);
expect(response.body.ok).toEqual(API_MESSAGE.PKG_REMOVED);
// package should be completely un published
await supertest(app)
.get('/foo')
.set('Accept', HEADERS.JSON)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.NOT_FOUND);
});
test('should unpublish a package', () => {});
test('should fails unpublish entirely a package', async () => {
const app = await initializeServer('publish.yaml');
const response = await supertest(app)
.delete(`/${encodeURIComponent('foo')}/-rev/1cf3-fe3`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HTTP_STATUS.NOT_FOUND);
expect(response.body.error).toEqual(API_ERROR.NO_PACKAGE);
});
test('should fails remove a tarball of a package does not exist', async () => {
const app = await initializeServer('publish.yaml');
const response = await supertest(app)
.delete(`/foo/-/foo-1.0.3.tgz/-rev/revision`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HTTP_STATUS.NOT_FOUND);
expect(response.body.error).toEqual(API_ERROR.NO_PACKAGE);
});
test('should fails on try remove a tarball does not exist', async () => {
const app = await initializeServer('publish.yaml');
await publishVersion(app, 'foo', '1.0.0');
const response = await supertest(app)
.delete(`/foo/-/foo-1.0.3.tgz/-rev/revision`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HTTP_STATUS.NOT_FOUND);
expect(response.body.error).toEqual(API_ERROR.NO_SUCH_FILE);
});
test('should remove a tarball that does exist', async () => {
const app = await initializeServer('publish.yaml');
await publishVersion(app, 'foo', '1.0.0');
const response = await supertest(app)
.delete(`/foo/-/foo-1.0.0.tgz/-rev/revision`)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HTTP_STATUS.CREATED);
expect(response.body.ok).toEqual(API_MESSAGE.TARBALL_REMOVED);
});
});
describe('star a package', () => {});

@ -0,0 +1,126 @@
import MockDate from 'mockdate';
import supertest from 'supertest';
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { createUser, initializeServer, publishVersionWithToken } from './_helper';
describe('search', () => {
let app;
beforeEach(async () => {
app = await initializeServer('search.yaml');
});
describe('search authenticated', () => {
test.each([['foo']])('should return a foo private package', async (pkg) => {
const mockDate = '2018-01-14T11:17:40.712Z';
MockDate.set(mockDate);
const res = await createUser(app, 'test', 'test');
await publishVersionWithToken(app, pkg, '1.0.0', res.body.token);
// this should not be displayed as part of the search
await publishVersionWithToken(app, 'private-auth', '1.0.0', res.body.token);
const response = await supertest(app)
.get(
`/-/v1/search?text=${encodeURIComponent(
pkg
)}&size=2000&from=0&quality=1&popularity=0.1&maintenance=0.1`
)
.set(HEADERS.ACCEPT, HEADERS.JSON)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HEADERS.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response.body).toEqual({
objects: [
{
package: {
author: {
email: 'user@domain.com',
name: 'User NPM',
},
date: mockDate,
description: 'package generated',
keywords: [],
links: {
npm: '',
},
name: pkg,
publisher: {},
scope: '',
version: '1.0.0',
},
score: {
detail: {
maintenance: 0,
popularity: 1,
quality: 1,
},
final: 1,
},
searchScore: 1,
verdaccioPkgCached: false,
verdaccioPrivate: true,
},
],
time: 'Sun, 14 Jan 2018 11:17:40 GMT',
total: 1,
});
});
test.each([['@scope/foo']])('should return a scoped foo private package', async (pkg) => {
const mockDate = '2018-01-14T11:17:40.712Z';
MockDate.set(mockDate);
const res = await createUser(app, 'test', 'test');
await publishVersionWithToken(app, pkg, '1.0.0', res.body.token);
// this should not be displayed as part of the search
await publishVersionWithToken(app, '@private/auth', '1.0.0', res.body.token);
const response = await supertest(app)
.get(
`/-/v1/search?text=${encodeURIComponent(
pkg
)}&size=2000&from=0&quality=1&popularity=0.1&maintenance=0.1`
)
.set(HEADERS.ACCEPT, HEADERS.JSON)
.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON)
.expect(HEADERS.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response.body).toEqual({
objects: [
{
package: {
author: {
email: 'user@domain.com',
name: 'User NPM',
},
date: mockDate,
description: 'package generated',
keywords: [],
links: {
npm: '',
},
name: pkg,
publisher: {},
scope: '@scope',
version: '1.0.0',
},
score: {
detail: {
maintenance: 0,
popularity: 1,
quality: 1,
},
final: 1,
},
searchScore: 1,
verdaccioPkgCached: false,
verdaccioPrivate: true,
},
],
time: 'Sun, 14 Jan 2018 11:17:40 GMT',
total: 1,
});
});
});
describe('error handling', () => {
test.todo('should able to abort the request');
});
});

@ -0,0 +1,124 @@
import _ from 'lodash';
import supertest from 'supertest';
import {
API_ERROR,
HEADERS,
HEADER_TYPE,
HTTP_STATUS,
SUPPORT_ERRORS,
TOKEN_BEARER,
} from '@verdaccio/core';
import { buildToken } from '@verdaccio/utils';
import { deleteTokenCLI, generateTokenCLI, getNewToken, initializeServer } from './_helper';
describe('token', () => {
describe('basics', () => {
test.each([['token.yaml'], ['token.jwt.yaml']])('should list empty tokens', async (conf) => {
const app = await initializeServer(conf);
const token = await getNewToken(app, { name: 'jota_token', password: 'secretPass' });
const response = await supertest(app)
.get('/-/npm/v1/tokens')
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response.body.objects).toHaveLength(0);
});
test.each([['token.yaml'], ['token.jwt.yaml']])('should generate one token', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota_token', password: 'secretPass' };
const token = await getNewToken(app, credentials);
await generateTokenCLI(app, token, {
password: credentials.password,
readonly: false,
cidr_whitelist: [],
});
const response = await supertest(app)
.get('/-/npm/v1/tokens')
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
const { objects, urls } = response.body;
expect(objects).toHaveLength(1);
const [tokenGenerated] = objects;
expect(tokenGenerated.user).toEqual(credentials.name);
expect(tokenGenerated.readonly).toBeFalsy();
expect(tokenGenerated.token).toMatch(/.../);
expect(_.isString(tokenGenerated.created)).toBeTruthy();
// we don't support pagination yet
expect(urls.next).toEqual('');
});
test.each([['token.yaml'], ['token.jwt.yaml']])('should delete a token', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota_token', password: 'secretPass' };
const token = await getNewToken(app, credentials);
const response = await generateTokenCLI(app, token, {
password: credentials.password,
readonly: false,
cidr_whitelist: [],
});
const key = response.body.key;
await deleteTokenCLI(app, token, key);
const response2 = await supertest(app)
.get('/-/npm/v1/tokens')
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
const { objects } = response2.body;
expect(objects).toHaveLength(0);
});
});
describe('handle errors', () => {
test.each([['token.yaml'], ['token.jwt.yaml']])('should delete a token', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota_token', password: 'secretPass' };
const token = await getNewToken(app, credentials);
const resp = await generateTokenCLI(app, token, {
password: 'wrongPassword',
readonly: false,
cidr_whitelist: [],
});
expect(resp.body.error).toEqual(API_ERROR.BAD_USERNAME_PASSWORD);
});
test.each([['token.yaml'], ['token.jwt.yaml']])(
'should fail if readonly is missing',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota_token', password: 'secretPass' };
const token = await getNewToken(app, credentials);
const resp = await generateTokenCLI(app, token, {
password: credentials.password,
cidr_whitelist: [],
});
expect(resp.body.error).toEqual(SUPPORT_ERRORS.PARAMETERS_NOT_VALID);
}
);
});
test.each([['token.yaml'], ['token.jwt.yaml']])(
'should fail if cidr_whitelist is missing',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota_token', password: 'secretPass' };
const token = await getNewToken(app, credentials);
const resp = await generateTokenCLI(app, token, {
password: credentials.password,
readonly: false,
});
expect(resp.body.error).toEqual(SUPPORT_ERRORS.PARAMETERS_NOT_VALID);
}
);
test.todo('handle failure if delete token');
test.todo('handle failure if getApiToken fails');
test.todo('handle failure if token creating fails');
test.todo('handle failure if token list fails');
});

@ -0,0 +1,87 @@
import nock from 'nock';
import supertest from 'supertest';
import { API_ERROR, HEADERS, HEADER_TYPE, HTTP_STATUS, TOKEN_BEARER } from '@verdaccio/core';
import { generateRemotePackageMetadata } from '@verdaccio/test-helper';
import { buildToken } from '@verdaccio/utils';
import { createUser, getPackage, initializeServer } from './_helper';
const FORBIDDEN_VUE = 'authorization required to access package vue';
describe('token', () => {
describe('basics', () => {
const FAKE_TOKEN: string = buildToken(TOKEN_BEARER, 'fake');
test.each([['user.yaml'], ['user.jwt.yaml']])('should test add a new user', async (conf) => {
const upstreamManifest = generateRemotePackageMetadata(
'vue',
'1.0.0',
'https://registry.verdaccio.org'
);
nock('https://registry.verdaccio.org').get(`/vue`).reply(201, upstreamManifest);
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const vueResponse = await getPackage(app, response.body.token, 'vue');
expect(vueResponse.body).toBeDefined();
expect(vueResponse.body.name).toMatch('vue');
const vueFailResp = await getPackage(app, FAKE_TOKEN, 'vue', HTTP_STATUS.UNAUTHORIZED);
expect(vueFailResp.body.error).toMatch(FORBIDDEN_VUE);
});
test.each([['user.yaml'], ['user.jwt.yaml']])('should test add a new user', async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'JotaJWT', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: credentials.password,
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.CONFLICT);
expect(response2.body.error).toBe(API_ERROR.USERNAME_ALREADY_REGISTERED);
});
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should fails on login if user credentials are invalid even if jwt',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'newFailsUser', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.put(`/-/user/org.couchdb.user:${credentials.name}`)
.send({
name: credentials.name,
password: 'BAD_PASSWORD',
})
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.UNAUTHORIZED);
expect(response2.body.error).toBe(API_ERROR.UNAUTHORIZED_ACCESS);
}
);
test.each([['user.yaml'], ['user.jwt.yaml']])(
'should verify if user is logged',
async (conf) => {
const app = await initializeServer(conf);
const credentials = { name: 'jota', password: 'secretPass' };
const response = await createUser(app, credentials.name, credentials.password);
expect(response.body.ok).toMatch(`user '${credentials.name}' created`);
const response2 = await supertest(app)
.get(`/-/user/org.couchdb.user:${credentials.name}`)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, response.body.token))
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response2.body.ok).toBe(`you are authenticated as '${credentials.name}'`);
}
);
});
});

@ -47,6 +47,7 @@ jest.mock('@verdaccio/auth', () => ({
},
}));
// FIXME: This might be covered with user.jwt.spec
describe('user', () => {
const credentials = { name: 'test', password: 'test' };

@ -1,53 +1,35 @@
import supertest from 'supertest';
import { HEADERS, HTTP_STATUS } from '@verdaccio/core';
import { HEADERS, HTTP_STATUS, TOKEN_BEARER } from '@verdaccio/core';
import { buildToken } from '@verdaccio/utils';
import { $RequestExtend, $ResponseExtend } from '../../types/custom';
import { initializeServer } from './_helper';
const mockApiJWTmiddleware = jest.fn(
() =>
(req: $RequestExtend, res: $ResponseExtend, _next): void => {
req.remote_user = { name: 'foo', groups: [], real_groups: [] };
_next();
}
);
jest.mock('@verdaccio/auth', () => ({
Auth: class {
apiJWTmiddleware() {
return mockApiJWTmiddleware();
}
allow_access(_d, f_, cb) {
cb(null, true);
}
},
}));
import { createUser, initializeServer } from './_helper';
describe('whoami', () => {
test.skip('should test referer /whoami endpoint', async (done) => {
return supertest(await initializeServer('whoami.yaml'))
.get('/whoami')
.set('referer', 'whoami')
.expect(HTTP_STATUS.OK)
.end(done);
});
test.skip('should test no referer /whoami endpoint', async (done) => {
return supertest(await initializeServer('whoami.yaml'))
.get('/whoami')
.expect(HTTP_STATUS.NOT_FOUND)
.end(done);
});
test('should return the logged username', async () => {
return supertest(await initializeServer('whoami.yaml'))
const app = await initializeServer('whoami.yaml');
// @ts-expect-error internal property
const { _body } = await createUser(app, 'test', 'test');
return supertest(app)
.get('/-/whoami')
.set('Accept', HEADERS.JSON)
.set(HEADERS.AUTHORIZATION, buildToken(TOKEN_BEARER, _body.token))
.expect('Content-Type', HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK)
.then((response) => {
expect(response.body.username).toEqual('foo');
expect(response.body.username).toEqual('test');
});
});
test('should fails with 401 if is not logged in', async () => {
const app = await initializeServer('whoami.yaml');
// @ts-expect-error internal property
const { _body } = await createUser(app, 'test', 'test');
return supertest(app)
.get('/-/whoami')
.set('Accept', HEADERS.JSON)
.set(HEADERS.AUTHORIZATION, buildToken('invalid-token', _body.token))
.expect('Content-Type', HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.UNAUTHORIZED);
});
});

@ -1,49 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Publish endpoints - publish package should change the existing package 1`] = `[MockFunction]`;
exports[`Publish endpoints - publish package should publish a new a new package 1`] = `
[MockFunction] {
"calls": Array [
Array [
"verdaccio",
Object {
"dist-tags": Object {},
"name": "verdaccio",
"time": Object {},
"versions": Object {},
},
[Function],
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`Publish endpoints - publish package test start should star a package 1`] = `
[MockFunction] {
"calls": Array [
Array [
"verdaccio",
Object {
"users": Object {
"verdaccio": true,
},
},
"15-e53a77096b0ee33e",
[Function],
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;

@ -0,0 +1,252 @@
// import { API_ERROR, HTTP_STATUS, errorUtils } from '@verdaccio/core';
// import { addVersion, publishPackage, removeTarball, unPublishPackage } from '../../src/publish';
// const REVISION_MOCK = '15-e53a77096b0ee33e';
// require('@verdaccio/logger').setup([{ type: 'stdout', format: 'pretty', level: 'info' }]);
// describe('Publish endpoints - add a tag', () => {
// let req;
// let res;
// let next;
// beforeEach(() => {
// req = {
// params: {
// version: '1.0.0',
// tag: 'tag',
// package: 'verdaccio',
// },
// body: '',
// };
// res = {
// status: jest.fn(),
// };
// next = jest.fn();
// });
// test('should add a version', (done) => {
// const storage = {
// addVersion: (packageName, version, body, tag, cb) => {
// expect(packageName).toEqual(req.params.package);
// expect(version).toEqual(req.params.version);
// expect(body).toEqual(req.body);
// expect(tag).toEqual(req.params.tag);
// cb();
// done();
// },
// };
// // @ts-ignore
// addVersion(storage)(req, res, next);
// expect(res.status).toHaveBeenLastCalledWith(HTTP_STATUS.CREATED);
// expect(next).toHaveBeenLastCalledWith({ ok: 'package published' });
// });
// test('when failed to add a version', (done) => {
// const storage = {
// addVersion: (packageName, version, body, tag, cb) => {
// const error = {
// message: 'failure',
// };
// cb(error);
// done();
// },
// };
// // @ts-ignore
// addVersion(storage)(req, res, next);
// expect(next).toHaveBeenLastCalledWith({ message: 'failure' });
// });
// });
// /**
// * Delete tarball: '/:package/-/:filename/-rev/:revision'
// */
// describe('Publish endpoints - delete tarball', () => {
// let req;
// let res;
// let next;
// beforeEach(() => {
// req = {
// params: {
// filename: 'verdaccio.gzip',
// package: 'verdaccio',
// revision: REVISION_MOCK,
// },
// };
// res = { status: jest.fn() };
// next = jest.fn();
// });
// test('should delete tarball successfully', (done) => {
// const storage = {
// removeTarball(packageName, filename, revision, cb) {
// expect(packageName).toEqual(req.params.package);
// expect(filename).toEqual(req.params.filename);
// expect(revision).toEqual(req.params.revision);
// cb();
// done();
// },
// };
// // @ts-ignore
// removeTarball(storage)(req, res, next);
// expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
// expect(next).toHaveBeenCalledWith({ ok: 'tarball removed' });
// });
// test('failed while deleting the tarball', (done) => {
// const error = {
// message: 'deletion failed',
// };
// const storage = {
// removeTarball(packageName, filename, revision, cb) {
// cb(error);
// done();
// },
// };
// // @ts-ignore
// removeTarball(storage)(req, res, next);
// expect(next).toHaveBeenCalledWith(error);
// });
// });
// /**
// * Un-publish package: '/:package/-rev/*'
// */
// describe('Publish endpoints - un-publish package', () => {
// let req;
// let res;
// let next;
// beforeEach(() => {
// req = {
// params: {
// package: 'verdaccio',
// },
// };
// res = { status: jest.fn() };
// next = jest.fn();
// });
// test('should un-publish package successfully', async () => {
// const storage = {
// removePackage(packageName) {
// expect(packageName).toEqual(req.params.package);
// return Promise.resolve();
// },
// };
// // @ts-ignore
// await unPublishPackage(storage)(req, res, next);
// expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
// expect(next).toHaveBeenCalledWith({ ok: 'package removed' });
// });
// test('un-publish failed', async () => {
// const storage = {
// removePackage(packageName) {
// expect(packageName).toEqual(req.params.package);
// return Promise.reject(errorUtils.getInternalError());
// },
// };
// // @ts-ignore
// await unPublishPackage(storage)(req, res, next);
// expect(next).toHaveBeenCalledWith(errorUtils.getInternalError());
// });
// });
// /**
// * Publish package: '/:package/:_rev?/:revision?'
// */
// describe('Publish endpoints - publish package', () => {
// let req;
// let res;
// let next;
// beforeEach(() => {
// req = {
// body: {
// name: 'verdaccio',
// },
// params: {
// package: 'verdaccio',
// },
// };
// res = { status: jest.fn() };
// next = jest.fn();
// });
// test('should change the existing package', () => {
// const storage = {
// changePackage: jest.fn(),
// };
// req.params._rev = REVISION_MOCK;
// // @ts-ignore
// publishPackage(storage)(req, res, next);
// expect(storage.changePackage).toMatchSnapshot();
// });
// test('should publish a new a new package', () => {
// const storage = {
// addPackage: jest.fn(),
// };
// // @ts-ignore
// publishPackage(storage)(req, res, next);
// expect(storage.addPackage).toMatchSnapshot();
// });
// test('should throw an error while publishing package', () => {
// const storage = {
// addPackage() {
// throw new Error();
// },
// };
// // @ts-ignore
// publishPackage(storage)(req, res, next);
// expect(next).toHaveBeenCalledWith(new Error(API_ERROR.BAD_PACKAGE_DATA));
// });
// describe('test start', () => {
// test('should star a package', () => {
// const storage = {
// changePackage: jest.fn(),
// getPackage({ callback }) {
// callback(null, {
// users: {},
// });
// },
// };
// req = {
// params: {
// package: 'verdaccio',
// },
// body: {
// _rev: REVISION_MOCK,
// users: {
// verdaccio: true,
// },
// },
// remote_user: {
// name: 'verdaccio',
// },
// };
// // @ts-ignore
// publishPackage(storage)(req, res, next);
// expect(storage.changePackage).toMatchSnapshot();
// });
// });
// });

@ -1,300 +0,0 @@
import { API_ERROR, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import {
addVersion,
publishPackage,
removeTarball,
unPublishPackage,
uploadPackageTarball,
} from '../../src/publish';
const REVISION_MOCK = '15-e53a77096b0ee33e';
require('@verdaccio/logger').setup([{ type: 'stdout', format: 'pretty', level: 'info' }]);
describe('Publish endpoints - add a tag', () => {
let req;
let res;
let next;
beforeEach(() => {
req = {
params: {
version: '1.0.0',
tag: 'tag',
package: 'verdaccio',
},
body: '',
};
res = {
status: jest.fn(),
};
next = jest.fn();
});
test('should add a version', (done) => {
const storage = {
addVersion: (packageName, version, body, tag, cb) => {
expect(packageName).toEqual(req.params.package);
expect(version).toEqual(req.params.version);
expect(body).toEqual(req.body);
expect(tag).toEqual(req.params.tag);
cb();
done();
},
};
// @ts-ignore
addVersion(storage)(req, res, next);
expect(res.status).toHaveBeenLastCalledWith(HTTP_STATUS.CREATED);
expect(next).toHaveBeenLastCalledWith({ ok: 'package published' });
});
test('when failed to add a version', (done) => {
const storage = {
addVersion: (packageName, version, body, tag, cb) => {
const error = {
message: 'failure',
};
cb(error);
done();
},
};
// @ts-ignore
addVersion(storage)(req, res, next);
expect(next).toHaveBeenLastCalledWith({ message: 'failure' });
});
});
/**
* upload package: '/:package/-/:filename/*'
*/
describe('Publish endpoints - upload package tarball', () => {
let req;
let res;
let next;
beforeEach(() => {
req = {
params: {
filename: 'verdaccio.gzip',
package: 'verdaccio',
},
pipe: jest.fn(),
on: jest.fn(),
};
res = { status: jest.fn(), locals: { report_error: jest.fn() } };
next = jest.fn();
});
test('should upload package tarball successfully', () => {
const stream = {
done: jest.fn(),
abort: jest.fn(),
on: jest.fn(() => (status, cb) => cb()),
};
const storage = {
addTarball(packageName, filename) {
expect(packageName).toEqual(req.params.package);
expect(filename).toEqual(req.params.filename);
return stream;
},
};
// @ts-ignore
uploadPackageTarball(storage)(req, res, next);
expect(req.pipe).toHaveBeenCalled();
expect(req.on).toHaveBeenCalled();
});
});
/**
* Delete tarball: '/:package/-/:filename/-rev/:revision'
*/
describe('Publish endpoints - delete tarball', () => {
let req;
let res;
let next;
beforeEach(() => {
req = {
params: {
filename: 'verdaccio.gzip',
package: 'verdaccio',
revision: REVISION_MOCK,
},
};
res = { status: jest.fn() };
next = jest.fn();
});
test('should delete tarball successfully', (done) => {
const storage = {
removeTarball(packageName, filename, revision, cb) {
expect(packageName).toEqual(req.params.package);
expect(filename).toEqual(req.params.filename);
expect(revision).toEqual(req.params.revision);
cb();
done();
},
};
// @ts-ignore
removeTarball(storage)(req, res, next);
expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
expect(next).toHaveBeenCalledWith({ ok: 'tarball removed' });
});
test('failed while deleting the tarball', (done) => {
const error = {
message: 'deletion failed',
};
const storage = {
removeTarball(packageName, filename, revision, cb) {
cb(error);
done();
},
};
// @ts-ignore
removeTarball(storage)(req, res, next);
expect(next).toHaveBeenCalledWith(error);
});
});
/**
* Un-publish package: '/:package/-rev/*'
*/
describe('Publish endpoints - un-publish package', () => {
let req;
let res;
let next;
beforeEach(() => {
req = {
params: {
package: 'verdaccio',
},
};
res = { status: jest.fn() };
next = jest.fn();
});
test('should un-publish package successfully', async () => {
const storage = {
removePackage(packageName) {
expect(packageName).toEqual(req.params.package);
return Promise.resolve();
},
};
// @ts-ignore
await unPublishPackage(storage)(req, res, next);
expect(res.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
expect(next).toHaveBeenCalledWith({ ok: 'package removed' });
});
test('un-publish failed', async () => {
const storage = {
removePackage(packageName) {
expect(packageName).toEqual(req.params.package);
return Promise.reject(errorUtils.getInternalError());
},
};
// @ts-ignore
await unPublishPackage(storage)(req, res, next);
expect(next).toHaveBeenCalledWith(errorUtils.getInternalError());
});
});
/**
* Publish package: '/:package/:_rev?/:revision?'
*/
describe('Publish endpoints - publish package', () => {
let req;
let res;
let next;
beforeEach(() => {
req = {
body: {
name: 'verdaccio',
},
params: {
package: 'verdaccio',
},
};
res = { status: jest.fn() };
next = jest.fn();
});
test('should change the existing package', () => {
const storage = {
changePackage: jest.fn(),
};
req.params._rev = REVISION_MOCK;
// @ts-ignore
publishPackage(storage)(req, res, next);
expect(storage.changePackage).toMatchSnapshot();
});
test('should publish a new a new package', () => {
const storage = {
addPackage: jest.fn(),
};
// @ts-ignore
publishPackage(storage)(req, res, next);
expect(storage.addPackage).toMatchSnapshot();
});
test('should throw an error while publishing package', () => {
const storage = {
addPackage() {
throw new Error();
},
};
// @ts-ignore
publishPackage(storage)(req, res, next);
expect(next).toHaveBeenCalledWith(new Error(API_ERROR.BAD_PACKAGE_DATA));
});
describe('test start', () => {
test('should star a package', () => {
const storage = {
changePackage: jest.fn(),
getPackage({ callback }) {
callback(null, {
users: {},
});
},
};
req = {
params: {
package: 'verdaccio',
},
body: {
_rev: REVISION_MOCK,
users: {
verdaccio: true,
},
},
remote_user: {
name: 'verdaccio',
},
};
// @ts-ignore
publishPackage(storage)(req, res, next);
expect(storage.changePackage).toMatchSnapshot();
});
});
});

@ -1,3 +1,10 @@
const config = require('../../jest/config');
module.exports = Object.assign({}, config, {});
module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 43,
},
},
});

@ -30,7 +30,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",

@ -497,10 +497,10 @@ class Auth implements IAuth {
next: Function
): void {
debug('handle legacy api middleware');
debug('api middleware secret %o', secret);
debug('api middleware authorization %o', authorization);
debug('api middleware secret %o', typeof secret === 'string');
debug('api middleware authorization %o', typeof authorization === 'string');
const credentials: any = getMiddlewareCredentials(security, secret, authorization);
debug('api middleware credentials %o', credentials);
debug('api middleware credentials %o', credentials?.name);
if (credentials) {
const { user, password } = credentials;
debug('authenticating %o', user);

@ -1 +1,10 @@
module.exports = require('../../jest/config');
const config = require('../../jest/config');
module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 4,
},
},
});

@ -35,7 +35,7 @@
"types": "build/index.d.ts",
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",

@ -3,7 +3,7 @@ import { Command, Option } from 'clipanion';
import { findConfigFile, parseConfigFile } from '@verdaccio/config';
import server from '@verdaccio/fastify-migration';
import { logger, setup } from '@verdaccio/logger';
import { ConfigRuntime } from '@verdaccio/types';
import { ConfigYaml } from '@verdaccio/types';
export const DEFAULT_PROCESS_NAME: string = 'verdaccio';
@ -25,7 +25,7 @@ export class FastifyServer extends Command {
description: 'use this configuration file (default: ./config.yaml)',
});
private initLogger(logConfig: ConfigRuntime) {
private initLogger(logConfig: ConfigYaml) {
try {
if (logConfig.log) {
throw Error('logger as array not longer supported');

@ -2,9 +2,9 @@ import { Command, Option } from 'clipanion';
import { findConfigFile, parseConfigFile } from '@verdaccio/config';
import { logger, setup } from '@verdaccio/logger';
import { LoggerConfigItem } from '@verdaccio/logger/src/logger';
import { LoggerConfigItem } from '@verdaccio/logger';
import { initServer } from '@verdaccio/node-api';
import { ConfigRuntime } from '@verdaccio/types';
import { ConfigYaml } from '@verdaccio/types';
export const DEFAULT_PROCESS_NAME: string = 'verdaccio';
@ -45,7 +45,7 @@ export class InitCommand extends Command {
description: 'use this configuration file (default: ./config.yaml)',
});
private initLogger(logConfig: ConfigRuntime) {
private initLogger(logConfig: ConfigYaml) {
try {
// @ts-expect-error
if (logConfig.logs) {

@ -1 +1,10 @@
module.exports = require('../../jest/config');
const config = require('../../jest/config');
module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 85,
},
},
});

@ -31,7 +31,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
@ -42,7 +42,7 @@
"@verdaccio/core": "workspace:6.0.0-6-next.5",
"@verdaccio/utils": "workspace:6.0.0-6-next.11",
"debug": "4.3.3",
"js-yaml": "3.14.1",
"yaml": "2.1.1",
"lodash": "4.17.21",
"minimatch": "3.0.4",
"yup": "0.32.11"

@ -0,0 +1,72 @@
import { merge } from 'lodash';
import {
AuthConf,
ConfigYaml,
LoggerConfItem,
PackageAccessYaml,
Security,
UpLinkConf,
} from '@verdaccio/types';
import { fromJStoYAML } from '.';
/**
* Helper configuration builder constructor, used to build the configuration for testing or
* programatically creating a configuration.
*/
export default class ConfigBuilder {
private config: ConfigYaml;
public constructor(config?: Partial<ConfigYaml>) {
// @ts-ignore
this.config = config ?? { uplinks: {}, packages: {}, security: {} };
}
public static build(config?: Partial<ConfigYaml>): ConfigBuilder {
return new ConfigBuilder(config);
}
public addPackageAccess(pattern: string, pkgAccess: PackageAccessYaml) {
// @ts-ignore
this.config.packages[pattern] = pkgAccess;
return this;
}
public addUplink(id: string, uplink: UpLinkConf) {
this.config.uplinks[id] = uplink;
return this;
}
public addSecurity(security: Partial<Security>) {
this.config.security = merge(this.config.security, security);
return this;
}
public addAuth(auth: Partial<AuthConf>) {
this.config.auth = merge(this.config.auth, auth);
return this;
}
public addLogger(log: LoggerConfItem) {
this.config.log = log;
return this;
}
public addStorage(storage: string | object) {
if (typeof storage === 'string') {
this.config.storage = storage;
} else {
this.config.store = storage;
}
return this;
}
public getConfig(): ConfigYaml {
return this.config;
}
public getAsYaml(): string {
return fromJStoYAML(this.config) as string;
}
}

@ -52,6 +52,7 @@ web:
# publicPath: http://somedomain.org/
# https://verdaccio.org/docs/configuration#authentication
auth:
htpasswd:
file: /verdaccio/storage/htpasswd

@ -6,7 +6,7 @@ import { APP_ERROR } from '@verdaccio/core';
import {
Config as AppConfig,
AuthConf,
ConfigRuntime,
ConfigYaml,
FlagsConfig,
PackageAccess,
PackageList,
@ -38,8 +38,11 @@ class Config implements AppConfig {
public users: any;
public auth: AuthConf;
public server_id: string;
// @deprecated use configPath instead
public config_path: string;
public configPath: string;
public storage: string | void;
public plugins: string | void;
public security: Security;
public serverSettings: ServerSettingsConf;
@ -47,10 +50,15 @@ class Config implements AppConfig {
public secret: string;
public flags: FlagsConfig;
public constructor(config: ConfigRuntime) {
public constructor(config: ConfigYaml & { config_path: string }) {
const self = this;
this.storage = process.env.VERDACCIO_STORAGE_PATH || config.storage;
this.config_path = config.config_path;
if (!config.configPath) {
throw new Error('config_path is required');
}
this.config_path = config.config_path ?? (config.configPath as string);
this.configPath = config.configPath;
debug('config path: %s', this.configPath);
this.plugins = config.plugins;
this.security = _.merge(defaultSecurity, config.security);
this.serverSettings = serverSettings;

@ -2,7 +2,8 @@ export * from './config';
export * from './config-path';
export * from './token';
export * from './package-access';
export * from './parse';
export { fromJStoYAML, parseConfigFile } from './parse';
export * from './uplinks';
export * from './security';
export * from './user';
export { default as ConfigBuilder } from './builder';

@ -8,6 +8,7 @@ export interface LegacyPackageList {
[key: string]: PackageAccess;
}
// @deprecated use @verdaccio/core:authUtils
export const ROLES = {
$ALL: '$all',
ALL: 'all',
@ -18,6 +19,7 @@ export const ROLES = {
DEPRECATED_ANONYMOUS: '@anonymous',
};
// @deprecated use @verdaccio/core:authUtils
export const PACKAGE_ACCESS = {
SCOPE: '@*/*',
ALL: '**',

@ -1,17 +1,35 @@
import buildDebug from 'debug';
import fs from 'fs';
import YAML from 'js-yaml';
import { isObject } from 'lodash';
import YAML from 'yaml';
import { APP_ERROR } from '@verdaccio/core';
import { ConfigRuntime, ConfigYaml } from '@verdaccio/types';
import { ConfigYaml } from '@verdaccio/types';
import { fileExists } from './config-utils';
const debug = buildDebug('verdaccio:config:parse');
/**
* Parse a config file from yaml to JSON.
* @param configPath the absolute path of the configuration file
*/
export function parseConfigFile(configPath: string): ConfigRuntime {
export function parseConfigFile(configPath: string): ConfigYaml & {
// @deprecated use configPath instead
config_path: string;
configPath: string;
} {
debug('parse config file %s', configPath);
if (!fileExists(configPath)) {
throw new Error(`config file does not exist or not reachable`);
}
debug('parsing config file: %o', configPath);
try {
if (/\.ya?ml$/i.test(configPath)) {
const yamlConfig = YAML.safeLoad(fs.readFileSync(configPath, 'utf8')) as ConfigYaml;
const yamlConfig = YAML.parse(fs.readFileSync(configPath, 'utf8'), {
strict: false,
}) as ConfigYaml;
return Object.assign({}, yamlConfig, {
configPath,
// @deprecated use configPath instead
@ -27,9 +45,19 @@ export function parseConfigFile(configPath: string): ConfigRuntime {
});
} catch (e: any) {
if (e.code !== 'MODULE_NOT_FOUND') {
debug('config module not found %o', configPath);
e.message = APP_ERROR.CONFIG_NOT_VALID;
}
throw e;
}
}
export function fromJStoYAML(config: Partial<ConfigYaml>): string | null {
debug('convert config from JSON to YAML');
if (isObject(config)) {
return YAML.stringify(config);
} else {
throw new Error(`config is not a valid object`);
}
}

@ -1,5 +0,0 @@
export function spliceURL(...args: string[]): string {
return Array.from(args)
.reduce((lastResult, current) => lastResult + current)
.replace(/([^:])(\/)+(.)/g, `$1/$3`);
}

@ -24,7 +24,7 @@ export function uplinkSanityCheck(
for (const uplink in newUplinks) {
if (Object.prototype.hasOwnProperty.call(newUplinks, uplink)) {
if (_.isNil(newUplinks[uplink].cache)) {
if (typeof newUplinks[uplink].cache === 'undefined') {
newUplinks[uplink].cache = true;
}
newUsers = sanityCheckNames(uplink, newUsers);

@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Config builder should create a configuration file as yaml 1`] = `
"uplinks:
upstream:
url: https://registry.verdaccio.org
upstream2:
url: https://registry.verdaccio.org
packages:
upstream/*:
access: public
publish: foo, bar
unpublish: foo, bar
proxy: some
security:
api:
legacy: true
log:
level: info
type: stdout
format: json
storage: /tmp/verdaccio
"
`;

@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parse fromJStoYAML basic conversion roundtrip 1`] = `
"storage: ./storage_default_storage
uplinks:
npmjs:
url: http://localhost:4873/
packages:
\\"@*/*\\":
access: $all
publish: $all
proxy: npmjs
forbidden-place:
access: nobody
publish: $all
react:
access: $all
publish: $all
proxy: npmjs
corrupted-package:
access: $all
publish: $all
proxy: npmjs
jquery:
access: $all
publish: $all
proxy: npmjs
auth-package:
access: $authenticated
publish: $authenticated
vue:
access: $authenticated
publish: $authenticated
proxy: npmjs
\\"*\\":
access: $all
publish: $all
proxy: npmjs
log:
type: stdout
format: pretty
level: warn
"
`;

@ -0,0 +1,65 @@
import { ConfigBuilder } from '../src';
describe('Config builder', () => {
test('should create a configuration file as object', () => {
const config = ConfigBuilder.build();
config
.addUplink('upstream', { url: 'https://registry.verdaccio.org' })
.addUplink('upstream2', { url: 'https://registry.verdaccio.org' })
.addPackageAccess('upstream/*', {
access: 'public',
publish: 'foo, bar',
unpublish: 'foo, bar',
proxy: 'some',
})
.addLogger({ level: 'info', type: 'stdout', format: 'json' })
.addStorage('/tmp/verdaccio')
.addSecurity({ api: { legacy: true } });
expect(config.getConfig()).toEqual({
security: {
api: {
legacy: true,
},
},
storage: '/tmp/verdaccio',
packages: {
'upstream/*': {
access: 'public',
publish: 'foo, bar',
unpublish: 'foo, bar',
proxy: 'some',
},
},
uplinks: {
upstream: {
url: 'https://registry.verdaccio.org',
},
upstream2: {
url: 'https://registry.verdaccio.org',
},
},
log: {
level: 'info',
type: 'stdout',
format: 'json',
},
});
});
test('should create a configuration file as yaml', () => {
const config = ConfigBuilder.build();
config
.addUplink('upstream', { url: 'https://registry.verdaccio.org' })
.addUplink('upstream2', { url: 'https://registry.verdaccio.org' })
.addPackageAccess('upstream/*', {
access: 'public',
publish: 'foo, bar',
unpublish: 'foo, bar',
proxy: 'some',
})
.addLogger({ level: 'info', type: 'stdout', format: 'json' })
.addStorage('/tmp/verdaccio')
.addSecurity({ api: { legacy: true } });
expect(config.getAsYaml()).toMatchSnapshot();
});
});

@ -1,44 +1,68 @@
import { parseConfigFile } from '../src';
import { writeFile } from 'fs/promises';
import path from 'path';
import { fileUtils } from '@verdaccio/core';
import { fromJStoYAML, parseConfigFile } from '../src';
import { parseConfigurationFile } from './utils';
describe('Package access utilities', () => {
describe('JSON format', () => {
test('parse default.json', () => {
const config = parseConfigFile(parseConfigurationFile('default.json'));
describe('parse', () => {
describe('parseConfigFile', () => {
describe('JSON format', () => {
test('parse default.json', () => {
const config = parseConfigFile(parseConfigurationFile('default.json'));
expect(config.storage).toBeDefined();
expect(config.storage).toBeDefined();
});
test('parse invalid.json', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('invalid.json'));
}).toThrow(/CONFIG: it does not look like a valid config file/);
});
test('parse not-exists.json', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('not-exists.json'));
}).toThrow(/config file does not exist or not reachable/);
});
});
test('parse invalid.json', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('invalid.json'));
}).toThrow(/CONFIG: it does not look like a valid config file/);
});
describe('JavaScript format', () => {
test('parse default.js', () => {
const config = parseConfigFile(parseConfigurationFile('default.js'));
test('parse not-exists.json', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('not-exists.json'));
}).toThrow(/Cannot find module/);
expect(config.storage).toBeDefined();
});
test('parse invalid.js', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('invalid.js'));
}).toThrow(/CONFIG: it does not look like a valid config file/);
});
test('parse not-exists.js', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('not-exists.js'));
}).toThrow(/config file does not exist or not reachable/);
});
});
});
describe('JavaScript format', () => {
test('parse default.js', () => {
const config = parseConfigFile(parseConfigurationFile('default.js'));
expect(config.storage).toBeDefined();
});
test('parse invalid.js', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('invalid.js'));
}).toThrow(/CONFIG: it does not look like a valid config file/);
});
test('parse not-exists.js', () => {
expect(function () {
parseConfigFile(parseConfigurationFile('not-exists.js'));
}).toThrow(/Cannot find module/);
describe('fromJStoYAML', () => {
test('basic conversion roundtrip', async () => {
// from to js to yaml
const config = require('./partials/config/js/from-js-to-yaml');
const yaml = fromJStoYAML(config) as string;
expect(yaml).toMatchSnapshot();
const tempFolder = await fileUtils.createTempFolder('fromJStoYAML-test');
const configPath = path.join(tempFolder, 'config.yaml');
await writeFile(configPath, yaml);
const parsed = parseConfigFile(configPath);
expect(parsed.configPath).toEqual(path.join(tempFolder, 'config.yaml'));
expect(parsed.storage).toEqual('./storage_default_storage');
expect(parsed.uplinks).toEqual({ npmjs: { url: 'http://localhost:4873/' } });
expect(parsed.log).toEqual({ type: 'stdout', format: 'pretty', level: 'warn' });
});
});
});

@ -0,0 +1,21 @@
import path from 'path';
import { fileExists, folderExists } from '../src/config-utils';
describe('config-utils', () => {
test('folderExists', () => {
expect(folderExists(path.join(__dirname, './partials/exist'))).toBeTruthy();
});
test('folderExists == false', () => {
expect(folderExists(path.join(__dirname, './partials/NOT_exist'))).toBeFalsy();
});
test('fileExists', () => {
expect(fileExists(path.join(__dirname, './partials/exist/README.md'))).toBeTruthy();
});
test('fileExists == false', () => {
expect(fileExists(path.join(__dirname, './partials/exist/NOT_EXIST.md'))).toBeFalsy();
});
});

@ -0,0 +1,15 @@
module.exports = {
storage: './storage_default_storage',
uplinks: { npmjs: { url: 'http://localhost:4873/' } },
packages: {
'@*/*': { access: '$all', publish: '$all', proxy: 'npmjs' },
'forbidden-place': { access: 'nobody', publish: '$all' },
react: { access: '$all', publish: '$all', proxy: 'npmjs' },
'corrupted-package': { access: '$all', publish: '$all', proxy: 'npmjs' },
jquery: { access: '$all', publish: '$all', proxy: 'npmjs' },
'auth-package': { access: '$authenticated', publish: '$authenticated' },
vue: { access: '$authenticated', publish: '$authenticated', proxy: 'npmjs' },
'*': { access: '$all', publish: '$all', proxy: 'npmjs' },
},
log: { type: 'stdout', format: 'pretty', level: 'warn' },
};

@ -0,0 +1 @@
just for testing purpose

@ -1,42 +1,27 @@
import { ROLES, createAnonymousRemoteUser, createRemoteUser } from '../src';
import { spliceURL } from '../src/string';
describe('spliceURL', () => {
test('should splice two strings and generate a url', () => {
const url: string = spliceURL('http://domain.com', '/-/static/logo.png');
expect(url).toMatch('http://domain.com/-/static/logo.png');
});
test('should splice a empty strings and generate a url', () => {
const url: string = spliceURL('', '/-/static/logo.png');
expect(url).toMatch('/-/static/logo.png');
});
describe('createRemoteUser and createAnonymousRemoteUser', () => {
test('should create a remote user with default groups', () => {
expect(createRemoteUser('12345', ['foo', 'bar'])).toEqual({
groups: [
'foo',
'bar',
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL,
],
name: '12345',
real_groups: ['foo', 'bar'],
});
describe('createRemoteUser and createAnonymousRemoteUser', () => {
test('should create a remote user with default groups', () => {
expect(createRemoteUser('12345', ['foo', 'bar'])).toEqual({
groups: [
'foo',
'bar',
ROLES.$ALL,
ROLES.$AUTH,
ROLES.DEPRECATED_ALL,
ROLES.DEPRECATED_AUTH,
ROLES.ALL,
],
name: '12345',
real_groups: ['foo', 'bar'],
});
});
test('should create a anonymous remote user with default groups', () => {
expect(createAnonymousRemoteUser()).toEqual({
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
name: undefined,
real_groups: [],
});
test('should create a anonymous remote user with default groups', () => {
expect(createAnonymousRemoteUser()).toEqual({
groups: [ROLES.$ALL, ROLES.$ANONYMOUS, ROLES.DEPRECATED_ALL, ROLES.DEPRECATED_ANONYMOUS],
name: undefined,
real_groups: [],
});
});
});

@ -37,15 +37,17 @@
"http-errors": "1.8.1",
"http-status-codes": "2.2.0",
"semver": "7.3.5",
"ajv": "8.11.0",
"process-warning": "1.0.0",
"core-js": "3.20.3"
},
"devDependencies": {
"lodash": "4.17.21",
"@verdaccio/types": "workspace:11.0.0-6-next.12"
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",

@ -19,6 +19,7 @@ export const CHARACTER_ENCODING = {
UTF8: 'utf8',
};
// @deprecated use Bearer instead
export const TOKEN_BASIC = 'Basic';
export const TOKEN_BEARER = 'Bearer';
@ -42,6 +43,7 @@ export const HEADERS = {
CSP: 'Content-Security-Policy',
CTO: 'X-Content-Type-Options',
XSS: 'X-XSS-Protection',
NONE_MATCH: 'If-None-Match',
ETAG: 'ETag',
JSON_CHARSET: 'application/json; charset=utf-8',
OCTET_STREAM: 'application/octet-stream; charset=utf-8',
@ -82,6 +84,7 @@ export const API_MESSAGE = {
TAG_UPDATED: 'tags updated',
TAG_REMOVED: 'tag removed',
TAG_ADDED: 'package tagged',
OK: 'ok',
LOGGED_OUT: 'Logged out',
};
@ -89,3 +92,18 @@ export const LOG_STATUS_MESSAGE =
"@{status}, user: @{user}(@{remoteIP}), req: '@{request.method} @{request.url}'";
export const LOG_VERDACCIO_ERROR = `${LOG_STATUS_MESSAGE}, error: @{!error}`;
export const LOG_VERDACCIO_BYTES = `${LOG_STATUS_MESSAGE}, bytes: @{bytes.in}/@{bytes.out}`;
export const ROLES = {
$ALL: '$all',
ALL: 'all',
$AUTH: '$authenticated',
$ANONYMOUS: '$anonymous',
DEPRECATED_ALL: '@all',
DEPRECATED_AUTH: '@authenticated',
DEPRECATED_ANONYMOUS: '@anonymous',
};
export const PACKAGE_ACCESS = {
SCOPE: '@*/*',
ALL: '**',
};

@ -20,10 +20,12 @@ export const API_ERROR = {
NOT_PACKAGE_UPLINK: 'package does not exist on uplink',
UPLINK_OFFLINE_PUBLISH: 'one of the uplinks is down, refuse to publish',
UPLINK_OFFLINE: 'uplink is offline',
NOT_MODIFIED_NO_DATA: 'no data',
CONTENT_MISMATCH: 'content length mismatch',
NOT_FILE_UPLINK: "file doesn't exist on uplink",
MAX_USERS_REACHED: 'maximum amount of users reached',
VERSION_NOT_EXIST: "this version doesn't exist",
NO_SUCH_FILE: 'no such file available',
UNSUPORTED_REGISTRY_CALL: 'unsupported registry call',
FILE_NOT_FOUND: 'File not found',
REGISTRATION_DISABLED: 'user registration disabled',

@ -1,3 +1,29 @@
import { mkdir, mkdtemp } from 'fs/promises';
import os from 'os';
import path from 'path';
export const Files = {
DatabaseName: '.verdaccio-db.json',
};
/**
* Create a temporary folder.
* @param prefix The prefix of the folder name.
* @returns string
*/
export async function createTempFolder(prefix: string): Promise<string> {
return await mkdtemp(path.join(os.tmpdir(), prefix));
}
/**
* Create temporary folder for an asset.
* @param prefix
* @param folder name
* @returns
*/
export async function createTempStorageFolder(prefix: string, folder = 'storage'): Promise<string> {
const tempFolder = await createTempFolder(prefix);
const storageFolder = path.join(tempFolder, folder);
await mkdir(storageFolder);
return storageFolder;
}

@ -1,9 +1,22 @@
import semver from 'semver';
import { URL } from 'url';
import { Package } from '@verdaccio/types';
import { Manifest } from '@verdaccio/types';
import { DIST_TAGS } from './constants';
/**
* Extract the tarball name from a registry dist url
* 'https://registry.npmjs.org/test/-/test-0.0.2.tgz'
* @param tarball tarball url
* @returns tarball filename
*/
export function extractTarballName(tarball: string) {
const urlObject: any = new URL(tarball);
const filename = urlObject.pathname.replace(/^.*\//, '');
return filename;
}
/**
* Function filters out bad semver versions and sorts the array.
* @return {Array} sorted Array
@ -24,7 +37,7 @@ export function semverSort(listVersions: string[]): string[] {
* Get the latest publihsed version of a package.
* @param package metadata
**/
export function getLatest(pkg: Package): string {
export function getLatest(pkg: Manifest): string {
const listVersions: string[] = Object.keys(pkg.versions);
if (listVersions.length < 1) {
throw Error('cannot get lastest version of none');
@ -42,8 +55,10 @@ export function getLatest(pkg: Package): string {
* @param {*} local
* @param {*} upstream
* @param {*} config sds
* @deprecated use @verdaccio/storage mergeVersions method
*/
export function mergeVersions(local: Package, upstream: Package) {
// @deprecated
export function mergeVersions(local: Manifest, upstream: Manifest) {
// copy new versions to a cache
// NOTE: if a certain version was updated, we can't refresh it reliably
for (const i in upstream.versions) {

@ -0,0 +1,38 @@
import Ajv, { JSONSchemaType } from 'ajv';
const ajv = new Ajv();
// FIXME: this could extend from @verdaccio/types but we need
// schemas from @verdaccio/types to be able to validate them
interface Manifest {
name: string;
versions: object;
_attachments: object;
}
const schema: JSONSchemaType<Manifest> = {
type: 'object',
properties: {
name: { type: 'string' },
versions: { type: 'object', maxProperties: 1 },
_attachments: { type: 'object', maxProperties: 1 },
},
required: ['name', 'versions', '_attachments'],
additionalProperties: true,
};
// validate is a type guard for MyData - type is inferred from schema type
const validate = ajv.compile(schema);
/**
* Validate if a manifest has the correct structure when a new package
* is being created. The properties name, versions and _attachments must contain 1 element.
* @param data a manifest object
* @returns boolean
*/
export function validatePublishSingleVersion(manifest: any) {
if (!manifest) {
return false;
}
return validate(manifest);
}

@ -1,9 +1,11 @@
import assert from 'assert';
import { Package } from '@verdaccio/types';
import { Manifest } from '@verdaccio/types';
import { DIST_TAGS } from './constants';
export { validatePublishSingleVersion } from './schemes/publish-manifest';
export function isPackageNameScoped(name: string): boolean {
return name.startsWith('@');
}
@ -62,27 +64,29 @@ export function validatePackage(name: string): boolean {
/**
* Validate the package metadata, add additional properties whether are missing within
* the metadata properties.
* @param {*} object
* @param {*} manifest
* @param {*} name
* @return {Object} the object with additional properties as dist-tags ad versions
* FUTURE: rename to normalizeMetadata
*/
export function validateMetadata(object: Package, name: string): Package {
assert(isObject(object), 'not a json object');
assert.strictEqual(object.name, name);
export function normalizeMetadata(manifest: Manifest, name: string): Manifest {
assert.strictEqual(manifest.name, name);
const _manifest = { ...manifest };
if (!isObject(object[DIST_TAGS])) {
object[DIST_TAGS] = {};
if (!isObject(manifest[DIST_TAGS])) {
_manifest[DIST_TAGS] = {};
}
if (!isObject(object['versions'])) {
object['versions'] = {};
// This may not be nee dit
if (!isObject(manifest['versions'])) {
_manifest['versions'] = {};
}
if (!isObject(object['time'])) {
object['time'] = {};
if (!isObject(manifest['time'])) {
_manifest['time'] = {};
}
return object;
return _manifest;
}
/**
@ -91,7 +95,7 @@ export function validateMetadata(object: Package, name: string): Package {
* @return {Boolean}
*/
export function isObject(obj: any): boolean {
if (obj === null || typeof obj === 'undefined') {
if (obj === null || typeof obj === 'undefined' || typeof obj === 'string') {
return false;
}

@ -0,0 +1,42 @@
import { DIST_TAGS, pkgUtils } from '../src';
describe('pkg-utils', () => {
test('extractTarballName', () => {
expect(pkgUtils.extractTarballName('https://registry.npmjs.org/test/-/test-0.0.2.tgz')).toBe(
'test-0.0.2.tgz'
);
});
test('extractTarballName with no tarball should not fails', () => {
expect(pkgUtils.extractTarballName('https://registry.npmjs.org/')).toBe('');
});
test('extractTarballName fails', () => {
expect(() =>
pkgUtils.extractTarballName('xxxxregistry.npmjs.org/test/-/test-0.0.2.tgz')
).toThrow();
});
test('getLatest fails if no versions', () => {
expect(() =>
// @ts-expect-error
pkgUtils.getLatest({
versions: {},
})
).toThrow('cannot get lastest version of none');
});
test('getLatest get latest', () => {
expect(
pkgUtils.getLatest({
versions: {
// @ts-expect-error
'1.0.0': {},
},
[DIST_TAGS]: {
latest: '1.0.0',
},
})
).toBe('1.0.0');
});
});

@ -1,4 +1,11 @@
import { isObject, validateName, validatePackage } from '../src/validation-utils';
import { DIST_TAGS } from '../src/constants';
import { validatePublishSingleVersion } from '../src/schemes/publish-manifest';
import {
isObject,
normalizeMetadata,
validateName,
validatePackage,
} from '../src/validation-utils';
describe('validatePackage', () => {
test('should validate package names', () => {
@ -19,13 +26,41 @@ describe('validatePackage', () => {
describe('isObject', () => {
test('isObject metadata', () => {
expect(isObject({ foo: 'bar' })).toBeTruthy();
expect(isObject('foo')).toBeTruthy();
// expect(isObject('foo')).toBeTruthy();
expect(isObject(['foo'])).toBeFalsy();
expect(isObject(null)).toBeFalsy();
expect(isObject(undefined)).toBeFalsy();
});
});
describe('normalizeMetadata', () => {
test('should fills an empty metadata object', () => {
// intended to fail with flow, do not remove
// @ts-ignore
expect(Object.keys(normalizeMetadata({}))).toContain(DIST_TAGS);
// @ts-ignore
expect(Object.keys(normalizeMetadata({}))).toContain('versions');
// @ts-ignore
expect(Object.keys(normalizeMetadata({}))).toContain('time');
});
test.skip('should fails the assertions is not an object', () => {
expect(function () {
// @ts-ignore
normalizeMetadata('');
// @ts-ignore
}).toThrow(expect.hasAssertions());
});
test('should fails the assertions is name does not match', () => {
expect(function () {
// @ts-ignore
normalizeMetadata({}, 'no-name');
// @ts-ignore
}).toThrow(expect.hasAssertions());
});
});
describe('validateName', () => {
test('should fails with no string', () => {
// intended to fail with Typescript, do not remove
@ -72,3 +107,62 @@ describe('validateName', () => {
expect(validateName('pk:g')).toBeFalsy();
});
});
describe('validatePublishSingleVersion', () => {
test('should be valid', () => {
expect(
validatePublishSingleVersion({
name: 'foo-pkg',
_attachments: { '2': {} },
versions: { '1': {} },
})
).toBeTruthy();
});
test('should be invalid if name is missing', () => {
expect(
validatePublishSingleVersion({
_attachments: { '2': {} },
versions: { '1': {} },
})
).toBeFalsy();
});
test('should be invalid if _attachments is missing', () => {
expect(
validatePublishSingleVersion({
name: 'foo-pkg',
versions: { '1': {} },
})
).toBeFalsy();
});
test('should be invalid if versions is missing', () => {
expect(
validatePublishSingleVersion({
name: 'foo-pkg',
_attachments: { '1': {} },
})
).toBeFalsy();
});
test('should be invalid if versions is more than 1', () => {
expect(
validatePublishSingleVersion({
name: 'foo-pkg',
versions: { '1': {}, '2': {} },
_attachments: { '1': {} },
})
).toBeFalsy();
});
test('should be invalid if _attachments is more than 1', () => {
expect(
validatePublishSingleVersion({
name: 'foo-pkg',
_attachments: { '1': {}, '2': {} },
versions: { '1': {} },
})
).toBeFalsy();
});
});

@ -44,7 +44,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"watch": "pnpm build:js -- --watch",

@ -49,7 +49,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"watch": "pnpm build:js -- --watch",

@ -29,6 +29,7 @@ describe('readme', () => {
});
test('should handle wrong text', () => {
// @ts-expect-error
expect(parseReadme(undefined)).toBeUndefined();
});

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

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

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

@ -1 +0,0 @@
lib/

@ -1,273 +0,0 @@
# Change Log
## 11.0.0-6-next.5
### Major Changes
- 794af76c: Remove Node 12 support
- We need move to the new `undici` and does not support Node.js 12
## 11.0.0-6-next.4
### Major Changes
- 459b6fa7: refactor: search v1 endpoint and local-database
- refactor search `api v1` endpoint, improve performance
- remove usage of `async` dependency https://github.com/verdaccio/verdaccio/issues/1225
- refactor method storage class
- create new module `core` to reduce the ammount of modules with utilities
- use `undici` instead `node-fetch`
- use `fastify` instead `express` for functional test
### Breaking changes
- plugin storage API changes
- remove old search endpoint (return 404)
- filter local private packages at plugin level
The storage api changes for methods `get`, `add`, `remove` as promise base. The `search` methods also changes and recieves a `query` object that contains all query params from the client.
```ts
export interface IPluginStorage<T> extends IPlugin {
add(name: string): Promise<void>;
remove(name: string): Promise<void>;
get(): Promise<any>;
init(): Promise<void>;
getSecret(): Promise<string>;
setSecret(secret: string): Promise<any>;
getPackageStorage(packageInfo: string): IPackageStorage;
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
saveToken(token: Token): Promise<any>;
deleteToken(user: string, tokenKey: string): Promise<any>;
readTokens(filter: TokenFilter): Promise<Token[]>;
}
```
## 10.0.0-alpha.3
### Patch Changes
- fecbb9be: chore: add release step to private regisry on merge changeset pr
## 10.0.0-alpha.2
### Minor Changes
- 54c58d1e: feat: add server rate limit protection to all request
To modify custom values, use the server settings property.
```markdown
server:
## https://www.npmjs.com/package/express-rate-limit#configuration-options
rateLimit:
windowMs: 1000
max: 10000
```
The values are intended to be high, if you want to improve security of your server consider
using different values.
## 10.0.0-alpha.1
### Major Changes
- d87fa026: feat!: experiments config renamed to flags
- The `experiments` configuration is renamed to `flags`. The functionality is exactly the same.
```js
flags: token: false;
search: false;
```
- The `self_path` property from the config file is being removed in favor of `config_file` full path.
- Refactor `config` module, better types and utilities
- da1ee9c8: - Replace signature handler for legacy tokens by removing deprecated crypto.createDecipher by createCipheriv
- Introduce environment variables for legacy tokens
### Code Improvements
- Add debug library for improve developer experience
### Breaking change
- The new signature invalidates all previous tokens generated by Verdaccio 4 or previous versions.
- The secret key must have 32 characters long.
### New environment variables
- `VERDACCIO_LEGACY_ALGORITHM`: Allows to define the specific algorithm for the token signature which by default is `aes-256-ctr`
- `VERDACCIO_LEGACY_ENCRYPTION_KEY`: By default, the token stores in the database, but using this variable allows to get it from memory
### Minor Changes
- 26b494cb: feat: add typescript project references settings
Reading https://ebaytech.berlin/optimizing-multi-package-apps-with-typescript-project-references-d5c57a3b4440 I realized I can use project references to solve the issue to pre-compile modules on develop mode.
It allows to navigate (IDE) trough the packages without need compile the packages.
Add two `tsconfig`, one using the previous existing configuration that is able to produce declaration files (`tsconfig.build`) and a new one `tsconfig` which is enables [_projects references_](https://www.typescriptlang.org/docs/handbook/project-references.html).
### Patch Changes
- b57b4338: Enable prerelease mode with **changesets**
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/streams
## [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/streams
# [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/streams
## [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/streams
# [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/streams
# [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/streams
## [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/streams
## [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/streams
# [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/streams
# [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/streams
## [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/streams
## [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/streams
# [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/streams
## [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/streams
## [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/streams
# [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/streams
# [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/streams
# [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/streams
# [8.2.0-next.0](https://github.com/verdaccio/monorepo/compare/v8.1.4...v8.2.0-next.0) (2019-10-08)
**Note:** Version bump only for package @verdaccio/streams
## [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/streams
## [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/streams
# [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/streams
## [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/streams
## [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/streams
# [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/streams
# [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/streams
# [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/streams
# [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/streams
# [8.0.0-next.0](https://github.com/verdaccio/monorepo/compare/v2.0.0...v8.0.0-next.0) (2019-08-01)
### Bug Fixes
- add es6 imports ([932a22d](https://github.com/verdaccio/monorepo/commit/932a22d))
- lint warnings ([444a99e](https://github.com/verdaccio/monorepo/commit/444a99e))
### Features
- drop node v6 support ([bb319c4](https://github.com/verdaccio/monorepo/commit/bb319c4))
- **build:** use typescript, jest 24 and babel 7 as stack BREAKING CHANGE: typescript build system requires a major release to avoid issues with old installations ([4743a9a](https://github.com/verdaccio/monorepo/commit/4743a9a))
- add stream library ([434628f](https://github.com/verdaccio/monorepo/commit/434628f))
- migration to typescript ([748ca92](https://github.com/verdaccio/monorepo/commit/748ca92))
# 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/streams/compare/v2.0.0-beta.0...v2.0.0) (2019-03-29)
### Features
- drop node v6 support ([5771eed](https://github.com/verdaccio/streams/commit/5771eed))
<a name="2.0.0-beta.0"></a>
# [2.0.0-beta.0](https://github.com/verdaccio/streams/compare/v1.0.0...v2.0.0-beta.0) (2019-01-27)
### Features
- migration to typescript ([4e1e959](https://github.com/verdaccio/streams/commit/4e1e959))
- **build:** use typescript, jest 24 and babel 7 as stack ([c93a980](https://github.com/verdaccio/streams/commit/c93a980))
### BREAKING CHANGES
- **build:** typescript build system requires a major release to avoid issues with old installations

@ -1,21 +0,0 @@
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.

@ -1,19 +0,0 @@
# Streams
[![CircleCI](https://circleci.com/gh/verdaccio/streams.svg?style=svg)](https://circleci.com/gh/ayusharma/@verdaccio/streams)
[![codecov](https://codecov.io/gh/verdaccio/streams/branch/master/graph/badge.svg)](https://codecov.io/gh/verdaccio/streams)
[![verdaccio (latest)](https://img.shields.io/npm/v/@verdaccio/streams/latest.svg)](https://www.npmjs.com/package/@verdaccio/streams)
[![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/streams/latest.svg)](https://www.npmjs.com/package/@verdaccio/streams)
This project provides an extension of `PassThrough` stream.
## Detail
It provides 2 additional methods `abort()` and `done()`. Those implementations are widely use in the verdaccio core for handle `tarballs`.
## License
MIT (http://www.opensource.org/licenses/mit-license.php)

@ -1,52 +0,0 @@
{
"name": "@verdaccio/streams",
"version": "11.0.0-6-next.5",
"description": "Stream extension for Verdaccio",
"keywords": [
"private",
"package",
"repository",
"registry",
"enterprise",
"modules",
"proxy",
"server",
"verdaccio"
],
"main": "./build/index.js",
"types": "./build/index.d.ts",
"author": "Juan Picado <juanpicado19@gmail.com>",
"license": "MIT",
"homepage": "https://verdaccio.org",
"engines": {
"node": ">=14",
"npm": ">=6"
},
"repository": {
"type": "https",
"url": "https://github.com/verdaccio/verdaccio",
"directory": "packages/core/streams"
},
"bugs": {
"url": "https://github.com/verdaccio/verdaccio/issues"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@verdaccio/types": "workspace:11.0.0-6-next.12"
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",
"watch": "pnpm build:js -- --watch",
"build": "pnpm run build:js && pnpm run build:types"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/verdaccio"
}
}

@ -1,84 +0,0 @@
import { PassThrough, TransformOptions } from 'stream';
export interface IReadTarball {
abort?: () => void;
}
export interface IUploadTarball {
done?: () => void;
abort?: () => void;
}
/**
* This stream is used to read tarballs from repository.
* @param {*} options
* @return {Stream}
*/
class ReadTarball extends PassThrough implements IReadTarball {
/**
*
* @param {Object} options
*/
public constructor(options: TransformOptions) {
super(options);
// called when data is not needed anymore
addAbstractMethods(this, 'abort');
}
public abort(): void {}
}
/**
* This stream is used to upload tarballs to a repository.
* @param {*} options
* @return {Stream}
*/
class UploadTarball extends PassThrough implements IUploadTarball {
/**
*
* @param {Object} options
*/
public constructor(options: any) {
super(options);
// called when user closes connection before upload finishes
addAbstractMethods(this, 'abort');
// called when upload finishes successfully
addAbstractMethods(this, 'done');
}
public abort(): void {}
public done(): void {}
}
/**
* This function intercepts abstract calls and replays them allowing.
* us to attach those functions after we are ready to do so
* @param {*} self
* @param {*} name
*/
// Perhaps someone knows a better way to write this
function addAbstractMethods(self: any, name: any): void {
self._called_methods = self._called_methods || {};
self.__defineGetter__(name, function () {
return function (): void {
self._called_methods[name] = true;
};
});
self.__defineSetter__(name, function (fn: any) {
delete self[name];
self[name] = fn;
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (self._called_methods && self._called_methods[name]) {
delete self._called_methods[name];
self[name]();
}
});
}
export { ReadTarball, UploadTarball };

@ -1,29 +0,0 @@
import { ReadTarball, UploadTarball } from '../src/index';
describe('mystreams', () => {
test('should delay events on ReadTarball abort', (cb) => {
const readTballStream = new ReadTarball({});
readTballStream.abort();
setTimeout(function () {
readTballStream.abort = function (): void {
cb();
};
readTballStream.abort = function (): never {
throw Error('fail');
};
}, 10);
});
test('should delay events on UploadTarball abort', (cb) => {
const uploadTballStream = new UploadTarball({});
uploadTballStream.abort();
setTimeout(function () {
uploadTballStream.abort = function (): void {
cb();
};
uploadTballStream.abort = function (): never {
throw Error('fail');
};
}, 10);
});
});

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

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

@ -46,7 +46,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",

@ -1,5 +1,5 @@
/// <reference types="node" />
import { PassThrough } from 'stream';
import { PassThrough, PipelinePromise, Readable, Stream, Writable } from 'stream';
declare module '@verdaccio/types' {
type StringValue = string | void | null;
@ -71,9 +71,17 @@ declare module '@verdaccio/types' {
bodyAfter?: string[];
} & CommonWebConf;
interface Signatures {
keyid: string;
sig: string;
}
interface Dist {
'npm-signature'?: string;
fileCount?: number;
integrity?: string;
shasum: string;
unpackedSize?: number;
tarball: string;
}
@ -156,8 +164,8 @@ declare module '@verdaccio/types' {
}
interface AttachMentsItem {
content_type?: string;
data?: string;
content_type?: string;
length?: number;
shasum?: string;
version?: string;
@ -205,29 +213,79 @@ declare module '@verdaccio/types' {
_rev: string;
}
interface Manifest {
interface PublishManifest {
/**
* The `_attachments` object has different usages:
*
* - When a package is published, it contains the tarball as an string, this string is used to be
* converted as a tarball, usually attached to the package but not stored in the database.
* - If user runs `npm star` the _attachments will be at the manifest body but empty.
*
* It has also an internal usage:
*
* - Used as a cache for the tarball, quick access to the tarball shasum, etc. Instead
* iterate versions and find the right one, just using the tarball as a key which is what
* the package manager sends to the registry.
*
* - A `_attachments` object is added every time a private tarball is published, upstream cached tarballs are
* not being part of this object, only for published private packages.
*
* Note: This field is removed when the package is accesed through the web user interface.
* */
_attachments: AttachMents;
}
/**
* Represents upstream manifest from another registry
*/
interface FullRemoteManifest {
_id?: string;
_rev?: string;
name: string;
versions: Versions;
description?: string;
'dist-tags': GenericBody;
time: GenericBody;
versions: Versions;
maintainers?: Author[];
/** store the latest readme **/
readme?: string;
/** store star assigned to this packages by users */
users?: PackageUsers;
// TODO: not clear what access exactly means
access?: any;
bugs?: { url: string };
license?: string;
homepage?: string;
repository?: string | { type?: string; url: string; directory?: string };
keywords?: string[];
}
interface Manifest extends FullRemoteManifest, PublishManifest {
// private fields only used by verdaccio
/**
* store fast access to the dist url of an specific tarball, instead search version
* by id, just the tarball id is faster.
*
* The _distfiles is created only when a package is being sync from an upstream.
* also used to fetch tarballs from upstream, the private publish tarballs are not stored in
* this object because they are not published in the upstream registry.
*/
_distfiles: DistFiles;
_attachments: AttachMents;
/**
* Store access cache metadata, to avoid to fetch the same metadata multiple times.
*
* The key represents the uplink id which is composed of a etag and a fetched timestamp.
*
* The fetched timestamp is the time when the metadata was fetched, used to avoid to fetch the
* same metadata until the metadata is older than the last fetch.
*/
_uplinks: UpLinks;
/**
* store the revision of the manifest
*/
_rev: string;
}
interface IUploadTarball extends PassThrough {
abort(): void;
done(): void;
}
interface IReadTarball extends PassThrough {
abort(): void;
}
interface UpLinkTokenConf {
type: 'Bearer' | 'Basic';
token?: string;
@ -262,6 +320,14 @@ declare module '@verdaccio/types' {
unpublish: string[];
}
interface PackageAccessYaml {
storage?: string;
publish?: string;
proxy?: string;
access?: string;
unpublish?: string;
}
// info passed to the auth plugin when a package is package is being published
interface AllowAccess {
name: string;
@ -275,12 +341,15 @@ declare module '@verdaccio/types' {
[key: string]: PackageAccess;
}
interface PackageListYaml {
[key: string]: PackageAccessYaml;
}
interface UpLinksConfList {
[key: string]: UpLinkConf;
}
type LoggerType = 'stdout' | 'stderr' | 'file';
type LoggerFormat = 'pretty' | 'pretty-timestamped' | 'file';
type LoggerFormat = 'pretty' | 'pretty-timestamped' | 'file' | 'json';
type LoggerLevel = 'http' | 'fatal' | 'warn' | 'info' | 'debug' | 'trace';
interface LoggerConfItem {
@ -326,7 +395,7 @@ declare module '@verdaccio/types' {
user: string;
}
type IPackageStorage = ILocalPackageManager | void;
type IPackageStorage = ILocalPackageManager | undefined;
type IPackageStorageManager = ILocalPackageManager;
type IPluginStorage<T> = ILocalData<T>;
@ -418,16 +487,19 @@ declare module '@verdaccio/types' {
basePath: string;
};
/**
* YAML configuration file available options.
*/
interface ConfigYaml {
_debug?: boolean;
storage?: string | void;
packages: PackageList;
packages?: PackageListYaml;
uplinks: UpLinksConfList;
// FUTURE: log should be mandatory
log?: LoggerConfItem;
web?: WebConf;
auth?: AuthConf;
security: Security;
security?: Security;
publish?: PublishOptions;
store?: any;
listen?: ListenAddress;
@ -444,19 +516,32 @@ declare module '@verdaccio/types' {
url_prefix?: string;
server?: ServerSettingsConf;
flags?: FlagsConfig;
// internal objects, added by internal yaml to JS config parser
// @deprecated use configPath instead
config_path?: string;
// save the configuration file path
configPath?: string;
}
interface ConfigRuntime extends ConfigYaml {
config_path: string;
}
interface Config extends ConfigYaml, ConfigRuntime {
/**
* Configuration object with additonal methods for configuration, includes yaml and internal medatada.
* @interface Config
* @extends {ConfigYaml}
*/
interface Config extends Omit<ConfigYaml, 'packages' | 'security' | 'configPath'> {
user_agent: string;
server_id: string;
secret: string;
// deprecated
// save the configuration file path, it's fails without thi configPath
configPath: string;
// packages from yaml file looks different from packages inside the config file
packages: PackageList;
// security object defaults is added by the config file but optional in the yaml file
security: Security;
// @deprecated (pending adding the replacement)
checkSecretKey(token: string): string;
getMatchedPackagesSpec(storage: string): PackageAccess | void;
// TODO: verify how to handle this in the future
[key: string]: any;
}
@ -508,55 +593,24 @@ declare module '@verdaccio/types' {
getPackageStorage(packageInfo: string): IPackageStorage;
}
type StorageUpdateCallback = (data: Package, cb: CallbackAction) => void;
type StorageUpdateHandler = (name: string, cb: StorageUpdateCallback) => void;
type StorageWriteCallback = (name: string, json: Package, callback: Callback) => void;
type PackageTransformer = (pkg: Package) => Package;
type ReadPackageCallback = (err: any | null, data?: Package) => void;
interface ILocalPackageManager {
logger: Logger;
writeTarball(pkgName: string): IUploadTarball;
readTarball(pkgName: string): IReadTarball;
readPackage(fileName: string, callback: ReadPackageCallback): void;
createPackage(pkgName: string, value: Package, cb: CallbackAction): void;
deletePackage(fileName: string): Promise<void>;
removePackage(): Promise<void>;
// @deprecated
updatePackage(
pkgFileName: string,
updateHandler: StorageUpdateCallback,
onWrite: StorageWriteCallback,
transformPackage: PackageTransformer,
onEnd: Callback
): void;
// @deprecated
savePackage(fileName: string, json: Package, callback: CallbackAction): void;
// next packages migration (this list is meant to replace the callback parent functions)
updatePackageNext(
updatePackage(
packageName: string,
handleUpdate: (manifest: Package) => Promise<Package>
): Promise<Package>;
savePackageNext(name: string, value: Package): Promise<void>;
}
interface TarballActions {
addTarball(name: string, filename: string): IUploadTarball;
getTarball(name: string, filename: string): IReadTarball;
removeTarball(name: string, filename: string, revision: string, callback: Callback): void;
}
interface StoragePackageActions extends TarballActions {
addVersion(
name: string,
version: string,
metadata: Version,
tag: StringValue,
callback: Callback
): void;
mergeTags(name: string, tags: MergeTags, callback: Callback): void;
removePackage(name: string, callback: Callback): void;
changePackage(name: string, metadata: Package, revision: string, callback: Callback): void;
handleUpdate: (manifest: Manifest) => Promise<Manifest>
): Promise<Manifest>;
readPackage(name: string): Promise<Manifest>;
savePackage(pkgName: string, value: Manifest): Promise<void>;
readTarball(pkgName: string, { signal }): Promise<Readable>;
createPackage(name: string, manifest: Manifest): Promise<void>;
writeTarball(tarballName: string, { signal }): Promise<Writable>;
// verify if tarball exist in the storage
hasTarball(fileName: string): Promise<boolean>;
// verify if package exist in the storage
hasPackage(): Promise<boolean>;
}
// @deprecated use IBasicAuth from @verdaccio/auth
@ -628,7 +682,7 @@ declare module '@verdaccio/types' {
}
interface IPluginStorageFilter<T> extends IPlugin<T> {
filter_metadata(packageInfo: Package): Promise<Package>;
filter_metadata(packageInfo: Manifest): Promise<Package>;
}
export type SearchResultWeb = {

@ -45,7 +45,7 @@
},
"scripts": {
"clean": "rimraf ./build",
"test": "cross-env NODE_ENV=test BABEL_ENV=test jest",
"test": "jest",
"type-check": "tsc --noEmit -p tsconfig.build.json",
"build:types": "tsc --emitDeclarationOnly -p tsconfig.build.json",
"build:js": "babel src/ --out-dir build/ --copy-files --extensions \".ts,.tsx\" --source-maps",

@ -93,6 +93,7 @@ export type RequestOptions = {
host: string;
protocol: string;
headers: { [key: string]: string };
remoteAddress?: string;
};
export function getPublicUrl(url_prefix: string = '', requestOptions: RequestOptions): string {

@ -8,47 +8,34 @@ async function distTagsRoute(fastify: FastifyInstance) {
// @ts-ignore
const { packageName } = request.params;
debug('dist-tags: response %o', packageName);
fastify.storage.getPackage({
const requestOptions = {
protocol: request.protocol,
headers: request.headers as any,
host: request.hostname,
remoteAddress: request.socket.remoteAddress,
};
const manifest = fastify.storage.getPackageByOptions({
name: packageName,
uplinksLook: true,
req: request.raw,
callback: function (err, info): void {
if (err) {
reply.send(err);
}
reply.code(fastify.statusCode.OK).send(info[fastify.constants.DIST_TAGS]);
},
keepUpLinkData: true,
requestOptions,
});
reply.code(fastify.statusCode.OK).send(manifest[fastify.constants.DIST_TAGS]);
});
fastify.post('/-/package/:packageName/dist-tags', async (request, reply) => {
fastify.post('/-/package/:packageName/dist-tags', async (request) => {
// @ts-ignore
const { packageName } = request.params;
// @ts-ignore
fastify.storage.mergeTags(packageName, request.body, function (err): void {
if (err) {
reply.send(err);
}
reply
.code(fastify.statusCode.CREATED)
.send({ ok: fastify.constants.API_MESSAGE.TAG_UPDATED });
});
await fastify.storage.mergeTags(packageName, request.body);
return { ok: fastify.constants.API_MESSAGE.TAG_UPDATED };
});
fastify.delete('/-/package/:packageName/dist-tags', async (request, reply) => {
// @ts-ignore
const { packageName } = request.params;
fastify.storage.getPackage({
name: packageName,
uplinksLook: true,
req: request.raw,
callback: function (err, info): void {
if (err) {
reply.send(err);
}
reply.send(info[fastify.constants.DIST_TAGS]);
},
});
// const { packageName } = request.params;
reply.code(fastify.statusCode.NOT_FOUND);
});
}

@ -14,6 +14,8 @@ async function manifestRoute(fastify: FastifyInstance) {
debug('pkg name %s ', packageName);
const data = await storage?.getPackageByOptions({
name: packageName,
// remove on refactor getPackageByOptions
// @ts-ignore
req: request.raw,
uplinksLook: true,
requestOptions: {
@ -32,6 +34,8 @@ async function manifestRoute(fastify: FastifyInstance) {
debug('pkg name %s, with version / tag: %s ', packageName, version);
const data = await storage?.getPackageByOptions({
name: packageName,
// remove on refactor getPackageByOptions
// @ts-ignore
req: request.raw,
version,
uplinksLook: true,

@ -20,7 +20,7 @@ async function searchRoute(fastify: FastifyInstance) {
const { url, query } = request.query;
const storage = fastify.storage;
const data = await storage.searchManager?.search({
const data = await storage.search({
query,
url,
abort,

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import buildDebug from 'debug';
import { FastifyInstance } from 'fastify';
@ -8,16 +9,16 @@ async function tarballRoute(fastify: FastifyInstance) {
// @ts-ignore
const { package: pkg, filename } = request.params;
debug('stream tarball for %s@%s', pkg, filename);
const stream = fastify.storage.getTarball(pkg, filename);
return reply.send(stream);
// const stream = fastify.storage.getTarball(pkg, filename);
// return reply.send(stream);
});
fastify.get('/:scopedPackage/-/:scope/:filename', async (request, reply) => {
// @ts-ignore
const { scopedPackage, filename } = request.params;
debug('stream scope tarball for %s@%s', scopedPackage, filename);
const stream = fastify.storage.getTarball(scopedPackage, filename);
return reply.send(stream);
// const stream = fastify.storage.getTarball(scopedPackage, filename);
// return reply.send(stream);
});
}

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