mirror of
https://github.com/verdaccio/verdaccio.git
synced 2024-12-30 22:34:10 -05:00
refactor: html render middleware improvements (#3603)
* refactor: render middleware * refactor: render middleware
This commit is contained in:
parent
1b38fb2d30
commit
45c03819e2
64 changed files with 576 additions and 783 deletions
15
.changeset/weak-mangos-taste.md
Normal file
15
.changeset/weak-mangos-taste.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
'@verdaccio/api': minor
|
||||||
|
'@verdaccio/config': minor
|
||||||
|
'@verdaccio/types': minor
|
||||||
|
'@verdaccio/hooks': minor
|
||||||
|
'@verdaccio/middleware': minor
|
||||||
|
'verdaccio-audit': minor
|
||||||
|
'@verdaccio/proxy': minor
|
||||||
|
'@verdaccio/server': minor
|
||||||
|
'@verdaccio/store': minor
|
||||||
|
'@verdaccio/web': minor
|
||||||
|
'@verdaccio/ui-theme': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
refactor: render html middleware
|
|
@ -13,6 +13,7 @@ import {
|
||||||
validatioUtils,
|
validatioUtils,
|
||||||
} from '@verdaccio/core';
|
} from '@verdaccio/core';
|
||||||
import { logger } from '@verdaccio/logger';
|
import { logger } from '@verdaccio/logger';
|
||||||
|
import { rateLimit } from '@verdaccio/middleware';
|
||||||
import { Config, RemoteUser } from '@verdaccio/types';
|
import { Config, RemoteUser } from '@verdaccio/types';
|
||||||
import { getAuthenticatedMessage, mask } from '@verdaccio/utils';
|
import { getAuthenticatedMessage, mask } from '@verdaccio/utils';
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ const debug = buildDebug('verdaccio:api:user');
|
||||||
export default function (route: Router, auth: Auth, config: Config): void {
|
export default function (route: Router, auth: Auth, config: Config): void {
|
||||||
route.get(
|
route.get(
|
||||||
'/-/user/:org_couchdb_user',
|
'/-/user/:org_couchdb_user',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||||
debug('verifying user');
|
debug('verifying user');
|
||||||
const message = getAuthenticatedMessage(req.remote_user.name);
|
const message = getAuthenticatedMessage(req.remote_user.name);
|
||||||
|
@ -53,6 +55,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
|
||||||
*/
|
*/
|
||||||
route.put(
|
route.put(
|
||||||
'/-/user/:org_couchdb_user/:_rev?/:revision?',
|
'/-/user/:org_couchdb_user/:_rev?/:revision?',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||||
const { name, password } = req.body;
|
const { name, password } = req.body;
|
||||||
debug('login or adduser');
|
debug('login or adduser');
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
errorUtils,
|
errorUtils,
|
||||||
validatioUtils,
|
validatioUtils,
|
||||||
} from '@verdaccio/core';
|
} from '@verdaccio/core';
|
||||||
|
import { rateLimit } from '@verdaccio/middleware';
|
||||||
import { Config } from '@verdaccio/types';
|
import { Config } from '@verdaccio/types';
|
||||||
|
|
||||||
import { $NextFunctionVer, $RequestExtend } from '../../types/custom';
|
import { $NextFunctionVer, $RequestExtend } from '../../types/custom';
|
||||||
|
@ -41,6 +42,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/-/npm/v1/user',
|
'/-/npm/v1/user',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||||
if (_.isNil(req.remote_user.name) === false) {
|
if (_.isNil(req.remote_user.name) === false) {
|
||||||
return next(buildProfile(req.remote_user.name));
|
return next(buildProfile(req.remote_user.name));
|
||||||
|
@ -55,6 +57,7 @@ export default function (route: Router, auth: Auth, config: Config): void {
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/-/npm/v1/user',
|
'/-/npm/v1/user',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
function (req: $RequestExtend, res: Response, next: $NextFunctionVer): void {
|
||||||
if (_.isNil(req.remote_user.name)) {
|
if (_.isNil(req.remote_user.name)) {
|
||||||
res.status(HTTP_STATUS.UNAUTHORIZED);
|
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { getApiToken } from '@verdaccio/auth';
|
||||||
import { Auth } from '@verdaccio/auth';
|
import { Auth } from '@verdaccio/auth';
|
||||||
import { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, errorUtils } from '@verdaccio/core';
|
import { HEADERS, HTTP_STATUS, SUPPORT_ERRORS, errorUtils } from '@verdaccio/core';
|
||||||
import { logger } from '@verdaccio/logger';
|
import { logger } from '@verdaccio/logger';
|
||||||
|
import { rateLimit } from '@verdaccio/middleware';
|
||||||
import { Storage } from '@verdaccio/store';
|
import { Storage } from '@verdaccio/store';
|
||||||
import { Config, RemoteUser, Token } from '@verdaccio/types';
|
import { Config, RemoteUser, Token } from '@verdaccio/types';
|
||||||
import { mask, stringToMD5 } from '@verdaccio/utils';
|
import { mask, stringToMD5 } from '@verdaccio/utils';
|
||||||
|
@ -26,6 +27,7 @@ function normalizeToken(token: Token): NormalizeToken {
|
||||||
export default function (route: Router, auth: Auth, storage: Storage, config: Config): void {
|
export default function (route: Router, auth: Auth, storage: Storage, config: Config): void {
|
||||||
route.get(
|
route.get(
|
||||||
'/-/npm/v1/tokens',
|
'/-/npm/v1/tokens',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
async function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||||
const { name } = req.remote_user;
|
const { name } = req.remote_user;
|
||||||
|
|
||||||
|
@ -53,6 +55,7 @@ export default function (route: Router, auth: Auth, storage: Storage, config: Co
|
||||||
|
|
||||||
route.post(
|
route.post(
|
||||||
'/-/npm/v1/tokens',
|
'/-/npm/v1/tokens',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
function (req: $RequestExtend, res: Response, next: $NextFunctionVer) {
|
||||||
const { password, readonly, cidr_whitelist } = req.body;
|
const { password, readonly, cidr_whitelist } = req.body;
|
||||||
const { name } = req.remote_user;
|
const { name } = req.remote_user;
|
||||||
|
@ -123,6 +126,7 @@ export default function (route: Router, auth: Auth, storage: Storage, config: Co
|
||||||
|
|
||||||
route.delete(
|
route.delete(
|
||||||
'/-/npm/v1/tokens/token/:tokenKey',
|
'/-/npm/v1/tokens/token/:tokenKey',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
|
async (req: $RequestExtend, res: Response, next: $NextFunctionVer) => {
|
||||||
const {
|
const {
|
||||||
params: { tokenKey },
|
params: { tokenKey },
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
const pkgVersion = require('../package.json').version;
|
import _ from 'lodash';
|
||||||
|
|
||||||
export function getUserAgent(): string {
|
export function getUserAgent(
|
||||||
return `verdaccio/${pkgVersion}`;
|
customUserAgent?: boolean | string,
|
||||||
|
version?: string,
|
||||||
|
name?: string
|
||||||
|
): string {
|
||||||
|
if (customUserAgent === true) {
|
||||||
|
return `${name}/${version}`;
|
||||||
|
} else if (_.isString(customUserAgent) && _.isEmpty(customUserAgent) === false) {
|
||||||
|
return customUserAgent;
|
||||||
|
} else if (customUserAgent === false) {
|
||||||
|
return 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${name}/${version}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
FlagsConfig,
|
FlagsConfig,
|
||||||
PackageAccess,
|
PackageAccess,
|
||||||
PackageList,
|
PackageList,
|
||||||
|
RateLimit,
|
||||||
Security,
|
Security,
|
||||||
ServerSettingsConf,
|
ServerSettingsConf,
|
||||||
} from '@verdaccio/types';
|
} from '@verdaccio/types';
|
||||||
|
@ -28,11 +29,17 @@ const debug = buildDebug('verdaccio:config');
|
||||||
|
|
||||||
export const WEB_TITLE = 'Verdaccio';
|
export const WEB_TITLE = 'Verdaccio';
|
||||||
|
|
||||||
|
// we limit max 1000 request per 15 minutes on user endpoints
|
||||||
|
export const defaultUserRateLimiting = {
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinates the application configuration
|
* Coordinates the application configuration
|
||||||
*/
|
*/
|
||||||
class Config implements AppConfig {
|
class Config implements AppConfig {
|
||||||
public user_agent: string;
|
public user_agent: string | undefined;
|
||||||
public uplinks: any;
|
public uplinks: any;
|
||||||
public packages: PackageList;
|
public packages: PackageList;
|
||||||
public users: any;
|
public users: any;
|
||||||
|
@ -49,7 +56,7 @@ class Config implements AppConfig {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public secret: string;
|
public secret: string;
|
||||||
public flags: FlagsConfig;
|
public flags: FlagsConfig;
|
||||||
|
public userRateLimit: RateLimit;
|
||||||
public constructor(config: ConfigYaml & { config_path: string }) {
|
public constructor(config: ConfigYaml & { config_path: string }) {
|
||||||
const self = this;
|
const self = this;
|
||||||
this.storage = process.env.VERDACCIO_STORAGE_PATH || config.storage;
|
this.storage = process.env.VERDACCIO_STORAGE_PATH || config.storage;
|
||||||
|
@ -65,6 +72,7 @@ class Config implements AppConfig {
|
||||||
this.flags = {
|
this.flags = {
|
||||||
searchRemote: config.flags?.searchRemote ?? true,
|
searchRemote: config.flags?.searchRemote ?? true,
|
||||||
};
|
};
|
||||||
|
this.user_agent = config.user_agent;
|
||||||
|
|
||||||
for (const configProp in config) {
|
for (const configProp in config) {
|
||||||
if (self[configProp] == null) {
|
if (self[configProp] == null) {
|
||||||
|
@ -72,11 +80,14 @@ class Config implements AppConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
if (typeof this.user_agent === 'undefined') {
|
||||||
if (_.isNil(this.user_agent)) {
|
// by default user agent is hidden
|
||||||
this.user_agent = getUserAgent();
|
debug('set default user agent');
|
||||||
|
this.user_agent = getUserAgent(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.userRateLimit = { ...defaultUserRateLimiting, ...config?.userRateLimit };
|
||||||
|
|
||||||
// some weird shell scripts are valid yaml files parsed as string
|
// some weird shell scripts are valid yaml files parsed as string
|
||||||
assert(_.isObject(config), APP_ERROR.CONFIG_NOT_VALID);
|
assert(_.isObject(config), APP_ERROR.CONFIG_NOT_VALID);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ export * from './package-access';
|
||||||
export { fromJStoYAML, parseConfigFile } from './parse';
|
export { fromJStoYAML, parseConfigFile } from './parse';
|
||||||
export * from './uplinks';
|
export * from './uplinks';
|
||||||
export * from './security';
|
export * from './security';
|
||||||
|
export * from './agent';
|
||||||
export * from './user';
|
export * from './user';
|
||||||
export { default as ConfigBuilder } from './builder';
|
export { default as ConfigBuilder } from './builder';
|
||||||
export { getDefaultConfig } from './conf';
|
export { getDefaultConfig } from './conf';
|
||||||
|
|
|
@ -252,6 +252,7 @@ export interface ConfigYaml {
|
||||||
store?: any;
|
store?: any;
|
||||||
listen?: ListenAddress;
|
listen?: ListenAddress;
|
||||||
https?: HttpsConf;
|
https?: HttpsConf;
|
||||||
|
user_agent?: string;
|
||||||
http_proxy?: string;
|
http_proxy?: string;
|
||||||
plugins?: string | void | null;
|
plugins?: string | void | null;
|
||||||
https_proxy?: string;
|
https_proxy?: string;
|
||||||
|
@ -264,6 +265,7 @@ export interface ConfigYaml {
|
||||||
url_prefix?: string;
|
url_prefix?: string;
|
||||||
server?: ServerSettingsConf;
|
server?: ServerSettingsConf;
|
||||||
flags?: FlagsConfig;
|
flags?: FlagsConfig;
|
||||||
|
userRateLimit?: RateLimit;
|
||||||
// internal objects, added by internal yaml to JS config parser
|
// internal objects, added by internal yaml to JS config parser
|
||||||
// @deprecated use configPath instead
|
// @deprecated use configPath instead
|
||||||
config_path?: string;
|
config_path?: string;
|
||||||
|
@ -277,7 +279,6 @@ export interface ConfigYaml {
|
||||||
* @extends {ConfigYaml}
|
* @extends {ConfigYaml}
|
||||||
*/
|
*/
|
||||||
export interface Config extends Omit<ConfigYaml, 'packages' | 'security' | 'configPath'> {
|
export interface Config extends Omit<ConfigYaml, 'packages' | 'security' | 'configPath'> {
|
||||||
user_agent: string;
|
|
||||||
server_id: string;
|
server_id: string;
|
||||||
secret: string;
|
secret: string;
|
||||||
// save the configuration file path, it's fails without thi configPath
|
// save the configuration file path, it's fails without thi configPath
|
||||||
|
|
|
@ -15,7 +15,7 @@ const singleHeaderNotificationConfig = parseConfigFile(
|
||||||
);
|
);
|
||||||
const multiNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('multiple.notify'));
|
const multiNotificationConfig = parseConfigFile(parseConfigurationNotifyFile('multiple.notify'));
|
||||||
|
|
||||||
setup([]);
|
setup({});
|
||||||
|
|
||||||
const domain = 'http://slack-service';
|
const domain = 'http://slack-service';
|
||||||
|
|
||||||
|
|
|
@ -40,9 +40,14 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@verdaccio/core": "workspace:6.0.0-6-next.59",
|
"@verdaccio/core": "workspace:6.0.0-6-next.59",
|
||||||
"@verdaccio/utils": "workspace:6.0.0-6-next.27",
|
"@verdaccio/utils": "workspace:6.0.0-6-next.27",
|
||||||
|
"@verdaccio/config": "workspace:6.0.0-6-next.59",
|
||||||
|
"@verdaccio/url": "workspace:11.0.0-6-next.25",
|
||||||
"debug": "4.3.4",
|
"debug": "4.3.4",
|
||||||
|
"lru-cache": "7.14.1",
|
||||||
|
"express": "4.18.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mime": "2.6.0"
|
"mime": "2.6.0",
|
||||||
|
"express-rate-limit": "5.5.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
|
@ -7,6 +7,9 @@ export { expectJson } from './middlewares/json';
|
||||||
export { antiLoop } from './middlewares/antiLoop';
|
export { antiLoop } from './middlewares/antiLoop';
|
||||||
export { final } from './middlewares/final';
|
export { final } from './middlewares/final';
|
||||||
export { allow } from './middlewares/allow';
|
export { allow } from './middlewares/allow';
|
||||||
|
export { rateLimit } from './middlewares/rate-limit';
|
||||||
|
export { userAgent } from './middlewares/user-agent';
|
||||||
|
export { webMiddleware } from './middlewares/web';
|
||||||
export { errorReportingMiddleware, handleError } from './middlewares/error';
|
export { errorReportingMiddleware, handleError } from './middlewares/error';
|
||||||
export {
|
export {
|
||||||
log,
|
log,
|
||||||
|
|
8
packages/middleware/src/middlewares/rate-limit.ts
Normal file
8
packages/middleware/src/middlewares/rate-limit.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import RateLimit from 'express-rate-limit';
|
||||||
|
|
||||||
|
import { RateLimit as RateLimitType } from '@verdaccio/types';
|
||||||
|
|
||||||
|
export function rateLimit(rateLimitOptions?: RateLimitType) {
|
||||||
|
const limiter = new RateLimit(rateLimitOptions);
|
||||||
|
return limiter;
|
||||||
|
}
|
10
packages/middleware/src/middlewares/user-agent.ts
Normal file
10
packages/middleware/src/middlewares/user-agent.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { getUserAgent } from '@verdaccio/config';
|
||||||
|
|
||||||
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
||||||
|
|
||||||
|
export function userAgent(config) {
|
||||||
|
return function (_req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
||||||
|
res.setHeader('x-powered-by', getUserAgent(config?.user_agent));
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
|
@ -4,15 +4,7 @@ import {
|
||||||
validatePackage as utilValidatePackage,
|
validatePackage as utilValidatePackage,
|
||||||
} from '@verdaccio/utils';
|
} from '@verdaccio/utils';
|
||||||
|
|
||||||
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types';
|
export function validateName(_req, _res, next, value: string, name: string) {
|
||||||
|
|
||||||
export function validateName(
|
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string,
|
|
||||||
name: string
|
|
||||||
): void {
|
|
||||||
if (value === '-') {
|
if (value === '-') {
|
||||||
// special case in couchdb usually
|
// special case in couchdb usually
|
||||||
next('route');
|
next('route');
|
||||||
|
@ -23,13 +15,7 @@ export function validateName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validatePackage(
|
export function validatePackage(_req, _res, next, value: string, name: string) {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer,
|
|
||||||
value: string,
|
|
||||||
name: string
|
|
||||||
): void {
|
|
||||||
if (value === '-') {
|
if (value === '-') {
|
||||||
// special case in couchdb usually
|
// special case in couchdb usually
|
||||||
next('route');
|
next('route');
|
||||||
|
|
1
packages/middleware/src/middlewares/web/index.ts
Normal file
1
packages/middleware/src/middlewares/web/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as webMiddleware } from './web-middleware';
|
|
@ -1,39 +1,16 @@
|
||||||
import buildDebug from 'debug';
|
import buildDebug from 'debug';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import _ from 'lodash';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { HTTP_STATUS } from '@verdaccio/core';
|
import { HTTP_STATUS } from '@verdaccio/core';
|
||||||
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
|
||||||
import { logger } from '@verdaccio/logger';
|
|
||||||
import { isURLhasValidProtocol } from '@verdaccio/url';
|
import { isURLhasValidProtocol } from '@verdaccio/url';
|
||||||
|
|
||||||
import renderHTML from '../renderHTML';
|
|
||||||
import { setSecurityWebHeaders } from './security';
|
import { setSecurityWebHeaders } from './security';
|
||||||
|
import renderHTML, { isHTTPProtocol } from './utils/renderHTML';
|
||||||
|
|
||||||
const debug = buildDebug('verdaccio:web:render');
|
const debug = buildDebug('verdaccio:web:render');
|
||||||
|
|
||||||
export async function loadTheme(config: any) {
|
|
||||||
if (_.isNil(config.theme) === false) {
|
|
||||||
const plugin = await asyncLoadPlugin(
|
|
||||||
config.theme,
|
|
||||||
{ config, logger },
|
|
||||||
// TODO: add types { staticPath: string; manifest: unknown; manifestFiles: unknown }
|
|
||||||
function (plugin: any) {
|
|
||||||
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
|
|
||||||
},
|
|
||||||
config?.serverSettings?.pluginPrefix ?? 'verdaccio-theme'
|
|
||||||
);
|
|
||||||
if (plugin.length > 1) {
|
|
||||||
logger.warn(
|
|
||||||
'multiple ui themes has been detected and is not supported, only the first one will be used'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.head(plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendFileCallback = (next) => (err) => {
|
const sendFileCallback = (next) => (err) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
return;
|
return;
|
||||||
|
@ -45,14 +22,15 @@ const sendFileCallback = (next) => (err) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function renderWebMiddleware(config, auth): Promise<any> {
|
export function renderWebMiddleware(config, tokenMiddleware, pluginOptions) {
|
||||||
const { staticPath, manifest, manifestFiles } =
|
const { staticPath, manifest, manifestFiles } = pluginOptions;
|
||||||
(await loadTheme(config)) || require('@verdaccio/ui-theme')();
|
|
||||||
debug('static path %o', staticPath);
|
debug('static path %o', staticPath);
|
||||||
|
|
||||||
/* eslint new-cap:off */
|
/* eslint new-cap:off */
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(auth.webUIJWTmiddleware());
|
if (typeof tokenMiddleware === 'function') {
|
||||||
|
router.use(tokenMiddleware);
|
||||||
|
}
|
||||||
router.use(setSecurityWebHeaders);
|
router.use(setSecurityWebHeaders);
|
||||||
|
|
||||||
// Logo
|
// Logo
|
||||||
|
@ -77,6 +55,36 @@ export async function renderWebMiddleware(config, auth): Promise<any> {
|
||||||
res.sendFile(file, sendFileCallback(next));
|
res.sendFile(file, sendFileCallback(next));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// logo
|
||||||
|
if (config?.web?.logo && !isHTTPProtocol(config?.web?.logo)) {
|
||||||
|
// URI related to a local file
|
||||||
|
const absoluteLocalFile = path.posix.resolve(config.web.logo);
|
||||||
|
debug('serve local logo %s', absoluteLocalFile);
|
||||||
|
try {
|
||||||
|
// TODO: remove existsSync by async alternative
|
||||||
|
if (
|
||||||
|
fs.existsSync(absoluteLocalFile) &&
|
||||||
|
typeof fs.accessSync(absoluteLocalFile, fs.constants.R_OK) === 'undefined'
|
||||||
|
) {
|
||||||
|
// Note: `path.join` will break on Windows, because it transforms `/` to `\`
|
||||||
|
// Use POSIX version `path.posix.join` instead.
|
||||||
|
config.web.logo = path.posix.join('/-/static/', path.basename(config.web.logo));
|
||||||
|
router.get(config.web.logo, function (_req, res, next) {
|
||||||
|
// @ts-ignore
|
||||||
|
debug('serve custom logo web:%s - local:%s', config.web.logo, absoluteLocalFile);
|
||||||
|
res.sendFile(absoluteLocalFile, sendFileCallback(next));
|
||||||
|
});
|
||||||
|
debug('enabled custom logo %s', config.web.logo);
|
||||||
|
} else {
|
||||||
|
config.web.logo = undefined;
|
||||||
|
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
config.web.logo = undefined;
|
||||||
|
debug(`web logo is wrong, path ${absoluteLocalFile} does not exist or is not readable`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
router.get('/-/web/:section/*', function (req, res) {
|
router.get('/-/web/:section/*', function (req, res) {
|
||||||
renderHTML(config, manifest, manifestFiles, req, res);
|
renderHTML(config, manifest, manifestFiles, req, res);
|
||||||
debug('render html section');
|
debug('render html section');
|
|
@ -1,11 +1,6 @@
|
||||||
import { HEADERS } from '@verdaccio/core';
|
import { HEADERS } from '@verdaccio/core';
|
||||||
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '@verdaccio/middleware';
|
|
||||||
|
|
||||||
export function setSecurityWebHeaders(
|
export function setSecurityWebHeaders(_req, res, next): void {
|
||||||
req: $RequestExtend,
|
|
||||||
res: $ResponseExtend,
|
|
||||||
next: $NextFunctionVer
|
|
||||||
): void {
|
|
||||||
// disable loading in frames (clickjacking, etc.)
|
// disable loading in frames (clickjacking, etc.)
|
||||||
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
res.header(HEADERS.FRAMES_OPTIONS, 'deny');
|
||||||
// avoid stablish connections outside of domain
|
// avoid stablish connections outside of domain
|
|
@ -1,5 +1,6 @@
|
||||||
import buildDebug from 'debug';
|
import buildDebug from 'debug';
|
||||||
import LRU from 'lru-cache';
|
import LRU from 'lru-cache';
|
||||||
|
import path from 'path';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
|
||||||
import { WEB_TITLE } from '@verdaccio/config';
|
import { WEB_TITLE } from '@verdaccio/config';
|
||||||
|
@ -8,9 +9,8 @@ import { TemplateUIOptions } from '@verdaccio/types';
|
||||||
import { getPublicUrl } from '@verdaccio/url';
|
import { getPublicUrl } from '@verdaccio/url';
|
||||||
|
|
||||||
import renderTemplate from './template';
|
import renderTemplate from './template';
|
||||||
import { hasLogin, validatePrimaryColor } from './utils/web-utils';
|
import { hasLogin, validatePrimaryColor } from './web-utils';
|
||||||
|
|
||||||
const pkgJSON = require('../package.json');
|
|
||||||
const DEFAULT_LANGUAGE = 'es-US';
|
const DEFAULT_LANGUAGE = 'es-US';
|
||||||
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 60 });
|
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 60 });
|
||||||
|
|
||||||
|
@ -21,6 +21,26 @@ const defaultManifestFiles = {
|
||||||
ico: 'favicon.ico',
|
ico: 'favicon.ico',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if URI is starting with "http://", "https://" or "//"
|
||||||
|
* @param {string} uri
|
||||||
|
*/
|
||||||
|
export function isHTTPProtocol(uri: string): boolean {
|
||||||
|
return /^(https?:)?\/\//.test(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveLogo(config, req) {
|
||||||
|
const isLocalFile = config?.web?.logo && !isHTTPProtocol(config?.web?.logo);
|
||||||
|
|
||||||
|
if (isLocalFile) {
|
||||||
|
return `${getPublicUrl(config?.url_prefix, req)}-/static/${path.basename(config?.web?.logo)}`;
|
||||||
|
} else if (isHTTPProtocol(config?.web?.logo)) {
|
||||||
|
return config?.web?.logo;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function renderHTML(config, manifest, manifestFiles, req, res) {
|
export default function renderHTML(config, manifest, manifestFiles, req, res) {
|
||||||
const { url_prefix } = config;
|
const { url_prefix } = config;
|
||||||
const base = getPublicUrl(config?.url_prefix, req);
|
const base = getPublicUrl(config?.url_prefix, req);
|
||||||
|
@ -33,11 +53,13 @@ export default function renderHTML(config, manifest, manifestFiles, req, res) {
|
||||||
const title = config?.web?.title ?? WEB_TITLE;
|
const title = config?.web?.title ?? WEB_TITLE;
|
||||||
const login = hasLogin(config);
|
const login = hasLogin(config);
|
||||||
const scope = config?.web?.scope ?? '';
|
const scope = config?.web?.scope ?? '';
|
||||||
const logoURI = config?.web?.logo ?? '';
|
const logoURI = resolveLogo(config, req);
|
||||||
const pkgManagers = config?.web?.pkgManagers ?? ['yarn', 'pnpm', 'npm'];
|
const pkgManagers = config?.web?.pkgManagers ?? ['yarn', 'pnpm', 'npm'];
|
||||||
const version = pkgJSON.version;
|
const version = config?.web?.version;
|
||||||
const flags = {
|
const flags = {
|
||||||
...config.flags,
|
...config.flags,
|
||||||
|
// legacy from 5.x
|
||||||
|
...config.experiments,
|
||||||
};
|
};
|
||||||
const primaryColor = validatePrimaryColor(config?.web?.primary_color) ?? '#4b5e40';
|
const primaryColor = validatePrimaryColor(config?.web?.primary_color) ?? '#4b5e40';
|
||||||
const {
|
const {
|
|
@ -2,7 +2,7 @@ import buildDebug from 'debug';
|
||||||
|
|
||||||
import { TemplateUIOptions } from '@verdaccio/types';
|
import { TemplateUIOptions } from '@verdaccio/types';
|
||||||
|
|
||||||
import { Manifest, getManifestValue } from './utils/manifest';
|
import { Manifest, getManifestValue } from './manifest';
|
||||||
|
|
||||||
const debug = buildDebug('verdaccio:web:render:template');
|
const debug = buildDebug('verdaccio:web:render:template');
|
||||||
|
|
18
packages/middleware/src/middlewares/web/utils/web-utils.ts
Normal file
18
packages/middleware/src/middlewares/web/utils/web-utils.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import buildDebug from 'debug';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const debug = buildDebug('verdaccio:web:middlwares');
|
||||||
|
|
||||||
|
export function validatePrimaryColor(primaryColor) {
|
||||||
|
const isHex = /^#([0-9A-F]{3}){1,2}$/i.test(primaryColor);
|
||||||
|
if (!isHex) {
|
||||||
|
debug('invalid primary color %o', primaryColor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return primaryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasLogin(config: any) {
|
||||||
|
return _.isNil(config?.web?.login) || config?.web?.login === true;
|
||||||
|
}
|
27
packages/middleware/src/middlewares/web/web-api.ts
Normal file
27
packages/middleware/src/middlewares/web/web-api.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import { validateName, validatePackage } from '../validation';
|
||||||
|
import { setSecurityWebHeaders } from './security';
|
||||||
|
|
||||||
|
export function webMiddleware(tokenMiddleware, webEndpointsApi) {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const route = Router();
|
||||||
|
// validate all of these params as a package name
|
||||||
|
// this might be too harsh, so ask if it causes trouble=
|
||||||
|
route.param('package', validatePackage);
|
||||||
|
route.param('filename', validateName);
|
||||||
|
route.param('version', validateName);
|
||||||
|
route.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
if (typeof tokenMiddleware === 'function') {
|
||||||
|
route.use(tokenMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
route.use(setSecurityWebHeaders);
|
||||||
|
|
||||||
|
if (webEndpointsApi) {
|
||||||
|
route.use(webEndpointsApi);
|
||||||
|
}
|
||||||
|
return route;
|
||||||
|
}
|
15
packages/middleware/src/middlewares/web/web-middleware.ts
Normal file
15
packages/middleware/src/middlewares/web/web-middleware.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { renderWebMiddleware } from './render-web';
|
||||||
|
import { webMiddleware } from './web-api';
|
||||||
|
|
||||||
|
export default (config, middlewares, pluginOptions): any => {
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const router = express.Router();
|
||||||
|
const { tokenMiddleware, webEndpointsApi } = middlewares;
|
||||||
|
// render web
|
||||||
|
router.use('/', renderWebMiddleware(config, tokenMiddleware, pluginOptions));
|
||||||
|
// web endpoints, search, packages, etc
|
||||||
|
router.use('/-/verdaccio/', webMiddleware(tokenMiddleware, webEndpointsApi));
|
||||||
|
return router;
|
||||||
|
};
|
8
packages/middleware/test/_helper.ts
Normal file
8
packages/middleware/test/_helper.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { parseConfigFile } from '@verdaccio/config';
|
||||||
|
|
||||||
|
export const getConf = (configName: string) => {
|
||||||
|
const configPath = path.join(__dirname, 'config', configName);
|
||||||
|
return parseConfigFile(configPath);
|
||||||
|
};
|
28
packages/middleware/test/config/default-test.yaml
Normal file
28
packages/middleware/test/config/default-test.yaml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
auth:
|
||||||
|
auth-memory:
|
||||||
|
users:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
password: test
|
||||||
|
|
||||||
|
web:
|
||||||
|
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
|
||||||
|
|
||||||
|
flags:
|
||||||
|
changePassword: true
|
29
packages/middleware/test/config/login-disabled.yaml
Normal file
29
packages/middleware/test/config/login-disabled.yaml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
auth:
|
||||||
|
auth-memory:
|
||||||
|
users:
|
||||||
|
test:
|
||||||
|
name: test
|
||||||
|
password: test
|
||||||
|
|
||||||
|
web:
|
||||||
|
title: verdaccio
|
||||||
|
login: false
|
||||||
|
|
||||||
|
publish:
|
||||||
|
allow_offline: false
|
||||||
|
|
||||||
|
uplinks:
|
||||||
|
|
||||||
|
log: { type: stdout, format: pretty, level: trace }
|
||||||
|
|
||||||
|
packages:
|
||||||
|
'@*/*':
|
||||||
|
access: $anonymous
|
||||||
|
publish: $anonymous
|
||||||
|
'**':
|
||||||
|
access: $anonymous
|
||||||
|
publish: $anonymous
|
||||||
|
_debug: true
|
||||||
|
|
||||||
|
flags:
|
||||||
|
changePassword: true
|
23
packages/middleware/test/config/web.yaml
Normal file
23
packages/middleware/test/config/web.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
web:
|
||||||
|
title: verdaccio web
|
||||||
|
login: true
|
||||||
|
scope: '@scope'
|
||||||
|
pkgManagers:
|
||||||
|
- pnpm
|
||||||
|
- yarn
|
||||||
|
showInfo: true
|
||||||
|
showSettings: true
|
||||||
|
showSearch: true
|
||||||
|
showFooter: true
|
||||||
|
showThemeSwitch: true
|
||||||
|
showDownloadTarball: true
|
||||||
|
showRaw: true
|
||||||
|
primary_color: '#ffffff'
|
||||||
|
logoURI: 'http://logo.org/logo.png'
|
||||||
|
|
||||||
|
url_prefix: /prefix
|
||||||
|
|
||||||
|
log: { type: stdout, format: pretty, level: trace }
|
||||||
|
|
||||||
|
flags:
|
||||||
|
changePassword: true
|
|
@ -1,4 +1,4 @@
|
||||||
import { getManifestValue } from '../src/utils/manifest';
|
import { getManifestValue } from '../src/middlewares/web/utils/manifest';
|
||||||
|
|
||||||
const manifest = require('./partials/manifest/manifest.json');
|
const manifest = require('./partials/manifest/manifest.json');
|
||||||
|
|
1
packages/middleware/test/partials/htmlParser.ts
Normal file
1
packages/middleware/test/partials/htmlParser.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const parseHtml = (html) => require('node-html-parser').parse(html);
|
64
packages/middleware/test/partials/manifest/manifest.json
Normal file
64
packages/middleware/test/partials/manifest/manifest.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"main.js": "/-/static/main.6126058572f989c948b1.js",
|
||||||
|
"main.css": "/-/static/main.6f2f2cccce0c813b509f.css",
|
||||||
|
"main.woff2": "/-/static/fonts/roboto-latin-900italic.woff2",
|
||||||
|
"main.woff": "/-/static/fonts/roboto-latin-900italic.woff",
|
||||||
|
"main.svg": "/-/static/93df1ce974e744e7d98f5d842da74ba0.svg",
|
||||||
|
"runtime.js": "/-/static/runtime.6126058572f989c948b1.js",
|
||||||
|
"NotFound.js": "/-/static/NotFound.6126058572f989c948b1.js",
|
||||||
|
"NotFound.svg": "/-/static/4743f1431b042843890a8644e89bb852.svg",
|
||||||
|
"Provider.js": "/-/static/Provider.6126058572f989c948b1.js",
|
||||||
|
"Version.css": "/-/static/454.97490e2b7f0dca05ddf3.css",
|
||||||
|
"Home.js": "/-/static/Home.6126058572f989c948b1.js",
|
||||||
|
"Home.css": "/-/static/268.97490e2b7f0dca05ddf3.css",
|
||||||
|
"Versions.js": "/-/static/Versions.6126058572f989c948b1.js",
|
||||||
|
"UpLinks.js": "/-/static/UpLinks.6126058572f989c948b1.js",
|
||||||
|
"Dependencies.js": "/-/static/Dependencies.6126058572f989c948b1.js",
|
||||||
|
"Engines.js": "/-/static/Engines.6126058572f989c948b1.js",
|
||||||
|
"Engines.svg": "/-/static/737531cc93ceb77b82b1c2e074a2557a.svg",
|
||||||
|
"Engines.png": "/-/static/2939f26c293bff8f35ba87194742aea8.png",
|
||||||
|
"Dist.js": "/-/static/Dist.6126058572f989c948b1.js",
|
||||||
|
"Install.js": "/-/static/Install.6126058572f989c948b1.js",
|
||||||
|
"Install.svg": "/-/static/1f07aa4bad48cd09088966736d1ed121.svg",
|
||||||
|
"Repository.js": "/-/static/Repository.6126058572f989c948b1.js",
|
||||||
|
"Repository.png": "/-/static/728ff5a8e44d74cd0f2359ef0a9ec88a.png",
|
||||||
|
"vendors.js": "/-/static/vendors.6126058572f989c948b1.js",
|
||||||
|
"38.6126058572f989c948b1.js": "/-/static/38.6126058572f989c948b1.js",
|
||||||
|
"26.6126058572f989c948b1.js": "/-/static/26.6126058572f989c948b1.js",
|
||||||
|
"761.6126058572f989c948b1.js": "/-/static/761.6126058572f989c948b1.js",
|
||||||
|
"4743f1431b042843890a8644e89bb852.svg": "/-/static/4743f1431b042843890a8644e89bb852.svg",
|
||||||
|
"node.png": "/-/static/2939f26c293bff8f35ba87194742aea8.png",
|
||||||
|
"fonts/roboto-latin-900italic.woff": "/-/static/fonts/roboto-latin-900italic.woff",
|
||||||
|
"fonts/roboto-latin-300italic.woff": "/-/static/fonts/roboto-latin-300italic.woff",
|
||||||
|
"fonts/roboto-latin-500italic.woff": "/-/static/fonts/roboto-latin-500italic.woff",
|
||||||
|
"fonts/roboto-latin-400italic.woff": "/-/static/fonts/roboto-latin-400italic.woff",
|
||||||
|
"fonts/roboto-latin-100italic.woff": "/-/static/fonts/roboto-latin-100italic.woff",
|
||||||
|
"fonts/roboto-latin-700italic.woff": "/-/static/fonts/roboto-latin-700italic.woff",
|
||||||
|
"fonts/roboto-latin-500.woff": "/-/static/fonts/roboto-latin-500.woff",
|
||||||
|
"fonts/roboto-latin-900.woff": "/-/static/fonts/roboto-latin-900.woff",
|
||||||
|
"fonts/roboto-latin-100.woff": "/-/static/fonts/roboto-latin-100.woff",
|
||||||
|
"fonts/roboto-latin-700.woff": "/-/static/fonts/roboto-latin-700.woff",
|
||||||
|
"fonts/roboto-latin-300.woff": "/-/static/fonts/roboto-latin-300.woff",
|
||||||
|
"fonts/roboto-latin-400.woff": "/-/static/fonts/roboto-latin-400.woff",
|
||||||
|
"fonts/roboto-latin-900italic.woff2": "/-/static/fonts/roboto-latin-900italic.woff2",
|
||||||
|
"fonts/roboto-latin-300italic.woff2": "/-/static/fonts/roboto-latin-300italic.woff2",
|
||||||
|
"fonts/roboto-latin-400italic.woff2": "/-/static/fonts/roboto-latin-400italic.woff2",
|
||||||
|
"fonts/roboto-latin-500italic.woff2": "/-/static/fonts/roboto-latin-500italic.woff2",
|
||||||
|
"fonts/roboto-latin-700italic.woff2": "/-/static/fonts/roboto-latin-700italic.woff2",
|
||||||
|
"fonts/roboto-latin-100italic.woff2": "/-/static/fonts/roboto-latin-100italic.woff2",
|
||||||
|
"fonts/roboto-latin-500.woff2": "/-/static/fonts/roboto-latin-500.woff2",
|
||||||
|
"fonts/roboto-latin-700.woff2": "/-/static/fonts/roboto-latin-700.woff2",
|
||||||
|
"fonts/roboto-latin-100.woff2": "/-/static/fonts/roboto-latin-100.woff2",
|
||||||
|
"fonts/roboto-latin-300.woff2": "/-/static/fonts/roboto-latin-300.woff2",
|
||||||
|
"fonts/roboto-latin-400.woff2": "/-/static/fonts/roboto-latin-400.woff2",
|
||||||
|
"fonts/roboto-latin-900.woff2": "/-/static/fonts/roboto-latin-900.woff2",
|
||||||
|
"favicon.ico": "/-/static/favicon.ico",
|
||||||
|
"git.png": "/-/static/728ff5a8e44d74cd0f2359ef0a9ec88a.png",
|
||||||
|
"logo.svg": "/-/static/93df1ce974e744e7d98f5d842da74ba0.svg",
|
||||||
|
"pnpm.svg": "/-/static/81ca2d852b9bc86713fe993bf5c7104c.svg",
|
||||||
|
"yarn.svg": "/-/static/1f07aa4bad48cd09088966736d1ed121.svg",
|
||||||
|
"logo-black-and-white.svg": "/-/static/983328eca26f265748c004651ca0e6c8.svg",
|
||||||
|
"npm.svg": "/-/static/737531cc93ceb77b82b1c2e074a2557a.svg",
|
||||||
|
"index.html": "/-/static/index.html",
|
||||||
|
"package.svg": "/-/static/4743f1431b042843890a8644e89bb852.svg"
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import express from 'express';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
|
@ -5,33 +6,30 @@ import supertest from 'supertest';
|
||||||
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
|
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
|
||||||
import { setup } from '@verdaccio/logger';
|
import { setup } from '@verdaccio/logger';
|
||||||
|
|
||||||
import { initializeServer } from './helper';
|
import { webMiddleware } from '../src';
|
||||||
|
import { getConf } from './_helper';
|
||||||
|
|
||||||
setup([]);
|
const pluginOptions = {
|
||||||
|
|
||||||
const mockManifest = jest.fn();
|
|
||||||
jest.mock('@verdaccio/ui-theme', () => mockManifest());
|
|
||||||
|
|
||||||
describe('test web server', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
mockManifest.mockReturnValue(() => ({
|
|
||||||
manifestFiles: {
|
manifestFiles: {
|
||||||
js: ['runtime.js', 'vendors.js', 'main.js'],
|
js: ['runtime.js', 'vendors.js', 'main.js'],
|
||||||
},
|
},
|
||||||
staticPath: path.join(__dirname, 'static'),
|
staticPath: path.join(__dirname, 'static'),
|
||||||
manifest: require('./partials/manifest/manifest.json'),
|
manifest: require('./partials/manifest/manifest.json'),
|
||||||
}));
|
};
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
const initializeServer = (configName: string, middlewares = {}) => {
|
||||||
jest.clearAllMocks();
|
const app = express();
|
||||||
mockManifest.mockClear();
|
app.use(webMiddleware(getConf(configName), middlewares, pluginOptions));
|
||||||
});
|
return app;
|
||||||
|
};
|
||||||
|
|
||||||
|
setup({});
|
||||||
|
|
||||||
|
describe('test web server', () => {
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
describe('output', () => {
|
describe('output', () => {
|
||||||
const render = async (config = 'default-test.yaml') => {
|
const render = async (config = 'default-test.yaml') => {
|
||||||
const response = await supertest(await initializeServer(config))
|
const response = await supertest(initializeServer(config))
|
||||||
.get('/')
|
.get('/')
|
||||||
.set('Accept', HEADERS.TEXT_HTML)
|
.set('Accept', HEADERS.TEXT_HTML)
|
||||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_HTML_UTF8)
|
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_HTML_UTF8)
|
||||||
|
@ -59,7 +57,7 @@ describe('test web server', () => {
|
||||||
// base: 'http://127.0.0.1:60864/prefix/',
|
// base: 'http://127.0.0.1:60864/prefix/',
|
||||||
// version: '6.0.0-6-next.28',
|
// version: '6.0.0-6-next.28',
|
||||||
logoURI: '',
|
logoURI: '',
|
||||||
flags: { searchRemote: true },
|
flags: { changePassword: true },
|
||||||
login: true,
|
login: true,
|
||||||
pkgManagers: ['pnpm', 'yarn'],
|
pkgManagers: ['pnpm', 'yarn'],
|
||||||
title: 'verdaccio web',
|
title: 'verdaccio web',
|
3
packages/middleware/test/static/main.js
Normal file
3
packages/middleware/test/static/main.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
main: '';
|
||||||
|
}
|
3
packages/middleware/test/static/vendor.js
Normal file
3
packages/middleware/test/static/vendor.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
'vendors': '';
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import template from '../src/template';
|
import template from '../src/middlewares/web/utils/template';
|
||||||
|
|
||||||
const manifest = require('./partials/manifest/manifest.json');
|
const manifest = require('./partials/manifest/manifest.json');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { validatePrimaryColor } from '../src/utils/web-utils';
|
import { validatePrimaryColor } from '../src/middlewares/web/utils/web-utils';
|
||||||
|
|
||||||
describe('Utilities', () => {
|
describe('Utilities', () => {
|
||||||
describe('validatePrimaryColor', () => {
|
describe('validatePrimaryColor', () => {
|
|
@ -10,6 +10,12 @@
|
||||||
{
|
{
|
||||||
"path": "../auth"
|
"path": "../auth"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "../core/url"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../core/core"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "../logger/logger"
|
"path": "../logger/logger"
|
||||||
},
|
},
|
||||||
|
|
|
@ -27,21 +27,19 @@
|
||||||
"main": "build/index.js",
|
"main": "build/index.js",
|
||||||
"types": "build/index.d.ts",
|
"types": "build/index.d.ts",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14",
|
"node": ">=12"
|
||||||
"npm": ">=6"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@verdaccio/core": "workspace:6.0.0-6-next.59",
|
"@verdaccio/core": "workspace:6.0.0-6-next.59",
|
||||||
"@verdaccio/config": "workspace:6.0.0-6-next.59",
|
"@verdaccio/config": "workspace:6.0.0-6-next.59",
|
||||||
"@verdaccio/logger": "workspace:6.0.0-6-next.27",
|
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"body-parser": "1.20.1",
|
|
||||||
"https-proxy-agent": "5.0.1",
|
"https-proxy-agent": "5.0.1",
|
||||||
"node-fetch": "cjs"
|
"node-fetch": "cjs"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@verdaccio/types": "workspace:11.0.0-6-next.19",
|
"@verdaccio/types": "workspace:11.0.0-6-next.19",
|
||||||
"@verdaccio/auth": "workspace:6.0.0-6-next.38",
|
"@verdaccio/auth": "workspace:6.0.0-6-next.38",
|
||||||
|
"@verdaccio/logger": "workspace:6.0.0-6-next.27",
|
||||||
"nock": "13.2.9",
|
"nock": "13.2.9",
|
||||||
"supertest": "6.3.3"
|
"supertest": "6.3.3"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { json as jsonParser } from 'body-parser';
|
|
||||||
import express, { Express, Request, Response } from 'express';
|
import express, { Express, Request, Response } from 'express';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import createHttpsProxyAgent from 'https-proxy-agent';
|
import createHttpsProxyAgent from 'https-proxy-agent';
|
||||||
|
@ -84,10 +83,10 @@ export default class ProxyAudit
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
/* eslint new-cap:off */
|
/* eslint new-cap:off */
|
||||||
|
|
||||||
router.post('/audits', jsonParser({ limit: '10mb' }), handleAudit);
|
router.post('/audits', express.json({ limit: '10mb' }), handleAudit);
|
||||||
router.post('/audits/quick', jsonParser({ limit: '10mb' }), handleAudit);
|
router.post('/audits/quick', express.json({ limit: '10mb' }), handleAudit);
|
||||||
|
|
||||||
router.post('/advisories/bulk', jsonParser({ limit: '10mb' }), handleAudit);
|
router.post('/advisories/bulk', express.json({ limit: '10mb' }), handleAudit);
|
||||||
|
|
||||||
app.use('/-/npm/v1/security', router);
|
app.use('/-/npm/v1/security', router);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export interface ConfigAudit {
|
export interface ConfigAudit {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
strict_ssl?: boolean | void;
|
max_body?: string;
|
||||||
|
strict_ssl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { logger, setup } from '@verdaccio/logger';
|
||||||
import { HTTP_STATUS } from '../../local-storage/node_modules/@verdaccio/core/build';
|
import { HTTP_STATUS } from '../../local-storage/node_modules/@verdaccio/core/build';
|
||||||
import ProxyAudit, { ConfigAudit } from '../src/index';
|
import ProxyAudit, { ConfigAudit } from '../src/index';
|
||||||
|
|
||||||
setup();
|
setup({});
|
||||||
|
|
||||||
const auditConfig: ConfigAudit = {
|
const auditConfig: ConfigAudit = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
@ -70,8 +70,8 @@ export interface IProxy {
|
||||||
fail_timeout: number;
|
fail_timeout: number;
|
||||||
upname: string;
|
upname: string;
|
||||||
search(options: ProxySearchParams): Promise<Stream.Readable>;
|
search(options: ProxySearchParams): Promise<Stream.Readable>;
|
||||||
getRemoteMetadataNext(name: string, options: ISyncUplinksOptions): Promise<[Manifest, string]>;
|
getRemoteMetadata(name: string, options: ISyncUplinksOptions): Promise<[Manifest, string]>;
|
||||||
fetchTarballNext(
|
fetchTarball(
|
||||||
url: string,
|
url: string,
|
||||||
options: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
|
options: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
|
||||||
): PassThrough;
|
): PassThrough;
|
||||||
|
@ -116,7 +116,7 @@ class ProxyStorage implements IProxy {
|
||||||
public constructor(config: UpLinkConfLocal, mainConfig: Config, agent?: Agents) {
|
public constructor(config: UpLinkConfLocal, mainConfig: Config, agent?: Agents) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.failed_requests = 0;
|
this.failed_requests = 0;
|
||||||
this.userAgent = mainConfig.user_agent;
|
this.userAgent = mainConfig.user_agent ?? 'hidden';
|
||||||
this.ca = config.ca;
|
this.ca = config.ca;
|
||||||
this.logger = LoggerApi.logger.child({ sub: 'out' });
|
this.logger = LoggerApi.logger.child({ sub: 'out' });
|
||||||
this.server_id = mainConfig.server_id;
|
this.server_id = mainConfig.server_id;
|
||||||
|
@ -294,7 +294,7 @@ class ProxyStorage implements IProxy {
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRemoteMetadataNext(
|
public async getRemoteMetadata(
|
||||||
name: string,
|
name: string,
|
||||||
options: ISyncUplinksOptions
|
options: ISyncUplinksOptions
|
||||||
): Promise<[Manifest, string]> {
|
): Promise<[Manifest, string]> {
|
||||||
|
@ -443,7 +443,7 @@ class ProxyStorage implements IProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: handle stream and retry
|
// FIXME: handle stream and retry
|
||||||
public fetchTarballNext(
|
public fetchTarball(
|
||||||
url: string,
|
url: string,
|
||||||
overrideOptions: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
|
overrideOptions: Pick<ISyncUplinksOptions, 'remoteAddress' | 'etag' | 'retry'>
|
||||||
): any {
|
): any {
|
||||||
|
|
|
@ -59,7 +59,7 @@ describe('proxy', () => {
|
||||||
const proxyPath = getConf('proxy1.yaml');
|
const proxyPath = getConf('proxy1.yaml');
|
||||||
const conf = new Config(parseConfigFile(proxyPath));
|
const conf = new Config(parseConfigFile(proxyPath));
|
||||||
|
|
||||||
describe('getRemoteMetadataNext', () => {
|
describe('getRemoteMetadata', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nock.cleanAll();
|
nock.cleanAll();
|
||||||
nock.abortPendingRequests();
|
nock.abortPendingRequests();
|
||||||
|
@ -78,7 +78,7 @@ describe('proxy', () => {
|
||||||
.get('/jquery')
|
.get('/jquery')
|
||||||
.reply(200, { body: 'test' });
|
.reply(200, { body: 'test' });
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
|
const [manifest] = await prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
});
|
});
|
||||||
expect(manifest).toEqual({ body: 'test' });
|
expect(manifest).toEqual({ body: 'test' });
|
||||||
|
@ -104,7 +104,7 @@ describe('proxy', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
|
const [manifest, etag] = await prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
});
|
});
|
||||||
expect(etag).toEqual('_ref_4444');
|
expect(etag).toEqual('_ref_4444');
|
||||||
|
@ -131,7 +131,7 @@ describe('proxy', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
const [manifest, etag] = await prox1.getRemoteMetadataNext('jquery', {
|
const [manifest, etag] = await prox1.getRemoteMetadata('jquery', {
|
||||||
etag: 'foo',
|
etag: 'foo',
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,7 @@ describe('proxy', () => {
|
||||||
.get('/jquery')
|
.get('/jquery')
|
||||||
.reply(200, { body: { name: 'foo', version: '1.0.0' } }, {});
|
.reply(200, { body: { name: 'foo', version: '1.0.0' } }, {});
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await prox1.getRemoteMetadataNext('jquery', {
|
await prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
});
|
});
|
||||||
expect(mockHttp).toHaveBeenCalledTimes(2);
|
expect(mockHttp).toHaveBeenCalledTimes(2);
|
||||||
|
@ -175,7 +175,7 @@ describe('proxy', () => {
|
||||||
test('proxy call with 304', async () => {
|
test('proxy call with 304', async () => {
|
||||||
nock(domain).get('/jquery').reply(304);
|
nock(domain).get('/jquery').reply(304);
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(prox1.getRemoteMetadataNext('jquery', { etag: 'rev_3333' })).rejects.toThrow(
|
await expect(prox1.getRemoteMetadata('jquery', { etag: 'rev_3333' })).rejects.toThrow(
|
||||||
'no data'
|
'no data'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -184,7 +184,7 @@ describe('proxy', () => {
|
||||||
nock(domain).get('/jquery').replyWithError('something awful happened');
|
nock(domain).get('/jquery').replyWithError('something awful happened');
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(new Error('something awful happened'));
|
).rejects.toThrowError(new Error('something awful happened'));
|
||||||
|
@ -193,7 +193,7 @@ describe('proxy', () => {
|
||||||
test('reply with 409 error', async () => {
|
test('reply with 409 error', async () => {
|
||||||
nock(domain).get('/jquery').reply(409);
|
nock(domain).get('/jquery').reply(409);
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(prox1.getRemoteMetadataNext('jquery', { retry: 0 })).rejects.toThrow(
|
await expect(prox1.getRemoteMetadata('jquery', { retry: 0 })).rejects.toThrow(
|
||||||
new Error('bad status code: 409')
|
new Error('bad status code: 409')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -202,7 +202,7 @@ describe('proxy', () => {
|
||||||
nock(domain).get('/jquery').reply(200, 'some-text');
|
nock(domain).get('/jquery').reply(200, 'some-text');
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(
|
).rejects.toThrowError(
|
||||||
|
@ -216,7 +216,7 @@ describe('proxy', () => {
|
||||||
nock(domain).get('/jquery').reply(409);
|
nock(domain).get('/jquery').reply(409);
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(
|
).rejects.toThrowError(
|
||||||
|
@ -228,7 +228,7 @@ describe('proxy', () => {
|
||||||
nock(domain).get('/jquery').reply(404);
|
nock(domain).get('/jquery').reply(404);
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
})
|
})
|
||||||
).rejects.toThrowError(errorUtils.getNotFound(API_ERROR.NOT_PACKAGE_UPLINK));
|
).rejects.toThrowError(errorUtils.getNotFound(API_ERROR.NOT_PACKAGE_UPLINK));
|
||||||
|
@ -254,7 +254,7 @@ describe('proxy', () => {
|
||||||
.reply(200, { body: { name: 'foo', version: '1.0.0' } });
|
.reply(200, { body: { name: 'foo', version: '1.0.0' } });
|
||||||
|
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
|
const [manifest] = await prox1.getRemoteMetadata('jquery', {
|
||||||
retry: { limit: 2 },
|
retry: { limit: 2 },
|
||||||
});
|
});
|
||||||
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
|
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
|
||||||
|
@ -274,13 +274,13 @@ describe('proxy', () => {
|
||||||
|
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
retry: { limit: 2 },
|
retry: { limit: 2 },
|
||||||
})
|
})
|
||||||
).rejects.toThrowError();
|
).rejects.toThrowError();
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
retry: { limit: 2 },
|
retry: { limit: 2 },
|
||||||
})
|
})
|
||||||
|
@ -311,14 +311,14 @@ describe('proxy', () => {
|
||||||
);
|
);
|
||||||
// force retry
|
// force retry
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
retry: { limit: 2 },
|
retry: { limit: 2 },
|
||||||
})
|
})
|
||||||
).rejects.toThrowError();
|
).rejects.toThrowError();
|
||||||
// display offline error on exausted retry
|
// display offline error on exausted retry
|
||||||
await expect(
|
await expect(
|
||||||
prox1.getRemoteMetadataNext('jquery', {
|
prox1.getRemoteMetadata('jquery', {
|
||||||
remoteAddress: '127.0.0.1',
|
remoteAddress: '127.0.0.1',
|
||||||
retry: { limit: 2 },
|
retry: { limit: 2 },
|
||||||
})
|
})
|
||||||
|
@ -338,7 +338,7 @@ describe('proxy', () => {
|
||||||
);
|
);
|
||||||
// this is based on max_fails, if change that also change here acordingly
|
// this is based on max_fails, if change that also change here acordingly
|
||||||
await setTimeout(3000);
|
await setTimeout(3000);
|
||||||
const [manifest] = await prox1.getRemoteMetadataNext('jquery', {
|
const [manifest] = await prox1.getRemoteMetadata('jquery', {
|
||||||
retry: { limit: 2 },
|
retry: { limit: 2 },
|
||||||
});
|
});
|
||||||
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
|
expect(manifest).toEqual({ body: { name: 'foo', version: '1.0.0' } });
|
||||||
|
|
|
@ -39,13 +39,13 @@ describe('tarball proxy', () => {
|
||||||
const proxyPath = getConf('proxy1.yaml');
|
const proxyPath = getConf('proxy1.yaml');
|
||||||
const conf = new Config(parseConfigFile(proxyPath));
|
const conf = new Config(parseConfigFile(proxyPath));
|
||||||
|
|
||||||
describe('fetchTarballNext', () => {
|
describe('fetchTarball', () => {
|
||||||
test('get file tarball fetch', (done) => {
|
test('get file tarball fetch', (done) => {
|
||||||
nock('https://registry.verdaccio.org')
|
nock('https://registry.verdaccio.org')
|
||||||
.get('/jquery/-/jquery-0.0.1.tgz')
|
.get('/jquery/-/jquery-0.0.1.tgz')
|
||||||
.replyWithFile(201, path.join(__dirname, 'partials/jquery-0.0.1.tgz'));
|
.replyWithFile(201, path.join(__dirname, 'partials/jquery-0.0.1.tgz'));
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
const stream = prox1.fetchTarballNext(
|
const stream = prox1.fetchTarball(
|
||||||
'https://registry.verdaccio.org/jquery/-/jquery-0.0.1.tgz',
|
'https://registry.verdaccio.org/jquery/-/jquery-0.0.1.tgz',
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
@ -66,7 +66,7 @@ describe('tarball proxy', () => {
|
||||||
.once()
|
.once()
|
||||||
.replyWithFile(201, path.join(__dirname, 'partials/jquery-0.0.1.tgz'));
|
.replyWithFile(201, path.join(__dirname, 'partials/jquery-0.0.1.tgz'));
|
||||||
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
const prox1 = new ProxyStorage(defaultRequestOptions, conf);
|
||||||
const stream = prox1.fetchTarballNext(
|
const stream = prox1.fetchTarball(
|
||||||
'https://registry.verdaccio.org/jquery/-/jquery-0.0.1.tgz',
|
'https://registry.verdaccio.org/jquery/-/jquery-0.0.1.tgz',
|
||||||
{ retry: { limit: 2 } }
|
{ retry: { limit: 2 } }
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,7 +45,6 @@
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"debug": "4.3.4",
|
"debug": "4.3.4",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"express-rate-limit": "5.5.1",
|
|
||||||
"lodash": "4.17.21"
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import compression from 'compression';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import buildDebug from 'debug';
|
import buildDebug from 'debug';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import RateLimit from 'express-rate-limit';
|
|
||||||
import { HttpError } from 'http-errors';
|
import { HttpError } from 'http-errors';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import AuditMiddleware from 'verdaccio-audit';
|
import AuditMiddleware from 'verdaccio-audit';
|
||||||
|
@ -13,7 +12,7 @@ import { Config as AppConfig } from '@verdaccio/config';
|
||||||
import { API_ERROR, HTTP_STATUS, errorUtils, pluginUtils } from '@verdaccio/core';
|
import { API_ERROR, HTTP_STATUS, errorUtils, pluginUtils } from '@verdaccio/core';
|
||||||
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
||||||
import { logger } from '@verdaccio/logger';
|
import { logger } from '@verdaccio/logger';
|
||||||
import { errorReportingMiddleware, final, log } from '@verdaccio/middleware';
|
import { errorReportingMiddleware, final, log, rateLimit, userAgent } from '@verdaccio/middleware';
|
||||||
import { Storage } from '@verdaccio/store';
|
import { Storage } from '@verdaccio/store';
|
||||||
import { ConfigYaml } from '@verdaccio/types';
|
import { ConfigYaml } from '@verdaccio/types';
|
||||||
import { Config as IConfig } from '@verdaccio/types';
|
import { Config as IConfig } from '@verdaccio/types';
|
||||||
|
@ -21,7 +20,6 @@ import webMiddleware from '@verdaccio/web';
|
||||||
|
|
||||||
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
|
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
|
||||||
import hookDebug from './debug';
|
import hookDebug from './debug';
|
||||||
import { getUserAgent } from './utils';
|
|
||||||
|
|
||||||
const debug = buildDebug('verdaccio:server');
|
const debug = buildDebug('verdaccio:server');
|
||||||
|
|
||||||
|
@ -29,23 +27,18 @@ const defineAPI = async function (config: IConfig, storage: Storage): Promise<an
|
||||||
const auth: Auth = new Auth(config);
|
const auth: Auth = new Auth(config);
|
||||||
await auth.init();
|
await auth.init();
|
||||||
const app = express();
|
const app = express();
|
||||||
const limiter = new RateLimit(config.serverSettings.rateLimit);
|
|
||||||
// run in production mode by default, just in case
|
// run in production mode by default, just in case
|
||||||
// it shouldn't make any difference anyway
|
// it shouldn't make any difference anyway
|
||||||
app.set('env', process.env.NODE_ENV || 'production');
|
app.set('env', process.env.NODE_ENV || 'production');
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(limiter);
|
app.use(rateLimit(config.serverSettings.rateLimit));
|
||||||
|
|
||||||
const errorReportingMiddlewareWrap = errorReportingMiddleware(logger);
|
const errorReportingMiddlewareWrap = errorReportingMiddleware(logger);
|
||||||
|
|
||||||
// Router setup
|
// Router setup
|
||||||
app.use(log(logger));
|
app.use(log(logger));
|
||||||
app.use(errorReportingMiddlewareWrap);
|
app.use(errorReportingMiddlewareWrap);
|
||||||
app.use(function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer): void {
|
app.use(userAgent(config));
|
||||||
res.setHeader('x-powered-by', getUserAgent(config.user_agent));
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
const pkgVersion = require('../package.json').version;
|
|
||||||
|
|
||||||
export function getUserAgent(userAgent: string): string {
|
|
||||||
if (typeof userAgent === 'string') {
|
|
||||||
return userAgent;
|
|
||||||
} else if (userAgent === false) {
|
|
||||||
return 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `verdaccio/${pkgVersion}`;
|
|
||||||
}
|
|
|
@ -55,15 +55,15 @@ test('should contains etag', async () => {
|
||||||
expect(typeof etag === 'string').toBeTruthy();
|
expect(typeof etag === 'string').toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should contains powered by header', async () => {
|
test('should be hidden by default', async () => {
|
||||||
const app = await initializeServer('conf.yaml');
|
const app = await initializeServer('conf.yaml');
|
||||||
const response = await supertest(app)
|
const response = await supertest(app)
|
||||||
.get('/')
|
.get('/')
|
||||||
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_HTML_UTF8)
|
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_HTML_UTF8)
|
||||||
.expect(HTTP_STATUS.OK);
|
.expect(HTTP_STATUS.OK);
|
||||||
const powered = response.get('x-powered-by');
|
const powered = response.get('x-powered-by');
|
||||||
expect(powered).toMatch('verdaccio/6');
|
expect(powered).toMatch('hidden');
|
||||||
});
|
}, 40000);
|
||||||
|
|
||||||
test('should not contains powered header', async () => {
|
test('should not contains powered header', async () => {
|
||||||
const app = await initializeServer('powered-disabled.yaml');
|
const app = await initializeServer('powered-disabled.yaml');
|
||||||
|
|
|
@ -298,7 +298,7 @@ class Storage {
|
||||||
let expected_length;
|
let expected_length;
|
||||||
const passThroughRemoteStream = new PassThrough();
|
const passThroughRemoteStream = new PassThrough();
|
||||||
const proxy = this.getUpLinkForDistFile(name, distFile);
|
const proxy = this.getUpLinkForDistFile(name, distFile);
|
||||||
const remoteStream = proxy.fetchTarballNext(distFile.url, {});
|
const remoteStream = proxy.fetchTarball(distFile.url, {});
|
||||||
|
|
||||||
remoteStream.on('request', async () => {
|
remoteStream.on('request', async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -392,7 +392,7 @@ class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxy = this.getUpLinkForDistFile(name, distFile);
|
const proxy = this.getUpLinkForDistFile(name, distFile);
|
||||||
const remoteStream = proxy.fetchTarballNext(distFile.url, {});
|
const remoteStream = proxy.fetchTarball(distFile.url, {});
|
||||||
remoteStream.on('response', async () => {
|
remoteStream.on('response', async () => {
|
||||||
try {
|
try {
|
||||||
const storage = this.getPrivatePackageStorage(name);
|
const storage = this.getPrivatePackageStorage(name);
|
||||||
|
@ -1732,7 +1732,7 @@ class Storage {
|
||||||
});
|
});
|
||||||
|
|
||||||
// get the latest metadata from the uplink
|
// get the latest metadata from the uplink
|
||||||
const [remoteManifest, etag] = await uplink.getRemoteMetadataNext(
|
const [remoteManifest, etag] = await uplink.getRemoteMetadata(
|
||||||
_cacheManifest.name,
|
_cacheManifest.name,
|
||||||
remoteOptions
|
remoteOptions
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
|
||||||
coverageThreshold: {
|
coverageThreshold: {
|
||||||
global: {
|
global: {
|
||||||
// FIXME: increase to 90
|
// FIXME: increase to 90
|
||||||
lines: 79,
|
lines: 72,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,11 +35,9 @@
|
||||||
"@verdaccio/tarball": "workspace:11.0.0-6-next.28",
|
"@verdaccio/tarball": "workspace:11.0.0-6-next.28",
|
||||||
"@verdaccio/url": "workspace:11.0.0-6-next.25",
|
"@verdaccio/url": "workspace:11.0.0-6-next.25",
|
||||||
"@verdaccio/utils": "workspace:6.0.0-6-next.27",
|
"@verdaccio/utils": "workspace:6.0.0-6-next.27",
|
||||||
"body-parser": "1.20.1",
|
|
||||||
"debug": "4.3.4",
|
"debug": "4.3.4",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21"
|
||||||
"lru-cache": "7.14.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "16.18.10",
|
"@types/node": "16.18.10",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
|
||||||
import { hasLogin } from '../utils/web-utils';
|
import { rateLimit } from '@verdaccio/middleware';
|
||||||
|
|
||||||
|
import { hasLogin } from '../web-utils';
|
||||||
import packageApi from './package';
|
import packageApi from './package';
|
||||||
import readme from './readme';
|
import readme from './readme';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
|
@ -9,6 +11,14 @@ import user from './user';
|
||||||
|
|
||||||
export default (auth, storage, config) => {
|
export default (auth, storage, config) => {
|
||||||
const route = Router(); /* eslint new-cap: 0 */
|
const route = Router(); /* eslint new-cap: 0 */
|
||||||
|
route.use(
|
||||||
|
'/data/',
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 2 * 60 * 1000, // 2 minutes
|
||||||
|
max: 5000, // limit each IP to 1000 requests per windowMs
|
||||||
|
...config?.web?.rateLimit,
|
||||||
|
})
|
||||||
|
);
|
||||||
route.use('/data/', packageApi(storage, auth, config));
|
route.use('/data/', packageApi(storage, auth, config));
|
||||||
route.use('/data/', search(storage, auth));
|
route.use('/data/', search(storage, auth));
|
||||||
route.use('/data/', sidebar(config, storage, auth));
|
route.use('/data/', sidebar(config, storage, auth));
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { getLocalRegistryTarballUri } from '@verdaccio/tarball';
|
||||||
import { Config, RemoteUser, Version } from '@verdaccio/types';
|
import { Config, RemoteUser, Version } from '@verdaccio/types';
|
||||||
import { formatAuthor, generateGravatarUrl } from '@verdaccio/utils';
|
import { formatAuthor, generateGravatarUrl } from '@verdaccio/utils';
|
||||||
|
|
||||||
import { sortByName } from '../utils/web-utils';
|
import { sortByName } from '../web-utils';
|
||||||
|
|
||||||
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages
|
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { $NextFunctionVer, $RequestExtend, $ResponseExtend, allow } from '@verda
|
||||||
import { Storage } from '@verdaccio/store';
|
import { Storage } from '@verdaccio/store';
|
||||||
import { Manifest } from '@verdaccio/types';
|
import { Manifest } from '@verdaccio/types';
|
||||||
|
|
||||||
import { AuthorAvatar, addScope } from '../utils/web-utils';
|
import { AuthorAvatar, addScope } from '../web-utils';
|
||||||
|
|
||||||
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages
|
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { convertDistRemoteToLocalTarballUrls } from '@verdaccio/tarball';
|
||||||
import { Config, Manifest, Version } from '@verdaccio/types';
|
import { Config, Manifest, Version } from '@verdaccio/types';
|
||||||
import { addGravatarSupport, formatAuthor, isVersionValid } from '@verdaccio/utils';
|
import { addGravatarSupport, formatAuthor, isVersionValid } from '@verdaccio/utils';
|
||||||
|
|
||||||
import { AuthorAvatar, addScope, deleteProperties } from '../utils/web-utils';
|
import { AuthorAvatar, addScope, deleteProperties } from '../web-utils';
|
||||||
|
|
||||||
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages
|
export { $RequestExtend, $ResponseExtend, $NextFunctionVer }; // Was required by other packages
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
errorUtils,
|
errorUtils,
|
||||||
validatioUtils,
|
validatioUtils,
|
||||||
} from '@verdaccio/core';
|
} from '@verdaccio/core';
|
||||||
|
import { rateLimit } from '@verdaccio/middleware';
|
||||||
import { Config, JWTSignOptions, RemoteUser } from '@verdaccio/types';
|
import { Config, JWTSignOptions, RemoteUser } from '@verdaccio/types';
|
||||||
|
|
||||||
import { $NextFunctionVer } from './package';
|
import { $NextFunctionVer } from './package';
|
||||||
|
@ -20,7 +21,10 @@ const debug = buildDebug('verdaccio:web:api:user');
|
||||||
|
|
||||||
function addUserAuthApi(auth: Auth, config: Config): Router {
|
function addUserAuthApi(auth: Auth, config: Config): Router {
|
||||||
const route = Router(); /* eslint new-cap: 0 */
|
const route = Router(); /* eslint new-cap: 0 */
|
||||||
route.post('/login', function (req: Request, res: Response, next: $NextFunctionVer): void {
|
route.post(
|
||||||
|
'/login',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
|
function (req: Request, res: Response, next: $NextFunctionVer): void {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
debug('authenticate %o', username);
|
debug('authenticate %o', username);
|
||||||
auth.authenticate(
|
auth.authenticate(
|
||||||
|
@ -42,11 +46,13 @@ function addUserAuthApi(auth: Auth, config: Config): Router {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (config?.flags?.changePassword === true) {
|
if (config?.flags?.changePassword === true) {
|
||||||
route.put(
|
route.put(
|
||||||
'/reset_password',
|
'/reset_password',
|
||||||
|
rateLimit(config?.userRateLimit),
|
||||||
function (req: Request, res: Response, next: $NextFunctionVer): void {
|
function (req: Request, res: Response, next: $NextFunctionVer): void {
|
||||||
if (_.isNil(req.remote_user.name)) {
|
if (_.isNil(req.remote_user.name)) {
|
||||||
res.status(HTTP_STATUS.UNAUTHORIZED);
|
res.status(HTTP_STATUS.UNAUTHORIZED);
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export { default } from './web-middleware';
|
export { default } from './middleware';
|
||||||
|
|
46
packages/web/src/middleware.ts
Normal file
46
packages/web/src/middleware.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import express from 'express';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { asyncLoadPlugin } from '@verdaccio/loaders';
|
||||||
|
import { logger } from '@verdaccio/logger';
|
||||||
|
import { webMiddleware } from '@verdaccio/middleware';
|
||||||
|
|
||||||
|
import webEndpointsApi from './api';
|
||||||
|
|
||||||
|
export async function loadTheme(config: any) {
|
||||||
|
if (_.isNil(config.theme) === false) {
|
||||||
|
const plugin = await asyncLoadPlugin(
|
||||||
|
config.theme,
|
||||||
|
{ config, logger },
|
||||||
|
// TODO: add types { staticPath: string; manifest: unknown; manifestFiles: unknown }
|
||||||
|
function (plugin: any) {
|
||||||
|
return plugin.staticPath && plugin.manifest && plugin.manifestFiles;
|
||||||
|
},
|
||||||
|
config?.serverSettings?.pluginPrefix ?? 'verdaccio-theme'
|
||||||
|
);
|
||||||
|
if (plugin.length > 1) {
|
||||||
|
logger.warn('multiple ui themes are not supported , only the first plugin is used used');
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.head(plugin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (config, auth, storage) => {
|
||||||
|
const pluginOptions = (await loadTheme(config)) || require('@verdaccio/ui-theme')();
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
const router = express.Router();
|
||||||
|
// load application
|
||||||
|
router.use(
|
||||||
|
webMiddleware(
|
||||||
|
config,
|
||||||
|
{
|
||||||
|
tokenMiddleware: auth.webUIJWTmiddleware(),
|
||||||
|
webEndpointsApi: webEndpointsApi(auth, storage, config),
|
||||||
|
},
|
||||||
|
pluginOptions
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
};
|
|
@ -1,25 +0,0 @@
|
||||||
import bodyParser from 'body-parser';
|
|
||||||
import { Router } from 'express';
|
|
||||||
|
|
||||||
import { Auth } from '@verdaccio/auth';
|
|
||||||
import { validateName, validatePackage } from '@verdaccio/middleware';
|
|
||||||
import { Storage } from '@verdaccio/store';
|
|
||||||
import { Config } from '@verdaccio/types';
|
|
||||||
|
|
||||||
import webEndpointsApi from '../api';
|
|
||||||
import { setSecurityWebHeaders } from './security';
|
|
||||||
|
|
||||||
export function webAPI(config: Config, auth: Auth, storage: Storage): Router {
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const route = Router();
|
|
||||||
// validate all of these params as a package name
|
|
||||||
// this might be too harsh, so ask if it causes trouble=
|
|
||||||
route.param('package', validatePackage);
|
|
||||||
route.param('filename', validateName);
|
|
||||||
route.param('version', validateName);
|
|
||||||
route.use(bodyParser.urlencoded({ extended: false }));
|
|
||||||
route.use(auth.webUIJWTmiddleware());
|
|
||||||
route.use(setSecurityWebHeaders);
|
|
||||||
route.use(webEndpointsApi(auth, storage, config));
|
|
||||||
return route;
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
import { renderWebMiddleware } from './middleware/render-web';
|
|
||||||
import { webAPI } from './middleware/web-api';
|
|
||||||
|
|
||||||
export default async (config, auth, storage) => {
|
|
||||||
// eslint-disable-next-line new-cap
|
|
||||||
const app = express.Router();
|
|
||||||
// load application
|
|
||||||
app.use('/', await renderWebMiddleware(config, auth));
|
|
||||||
// web endpoints, search, packages, etc
|
|
||||||
app.use('/-/verdaccio/', webAPI(config, auth, storage));
|
|
||||||
return app;
|
|
||||||
};
|
|
|
@ -1,34 +1,9 @@
|
||||||
import buildDebug from 'debug';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
// import { normalizeContributors } from '@verdaccio/store';
|
|
||||||
import { Author, ConfigYaml } from '@verdaccio/types';
|
import { Author, ConfigYaml } from '@verdaccio/types';
|
||||||
|
|
||||||
export type AuthorAvatar = Author & { avatar?: string };
|
export function hasLogin(config: ConfigYaml) {
|
||||||
|
return _.isNil(config?.web?.login) || config?.web?.login === true;
|
||||||
const debug = buildDebug('verdaccio:web:utils');
|
|
||||||
|
|
||||||
export function validatePrimaryColor(primaryColor) {
|
|
||||||
const isHex = /^#([0-9A-F]{3}){1,2}$/i.test(primaryColor);
|
|
||||||
if (!isHex) {
|
|
||||||
debug('invalid primary color %o', primaryColor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return primaryColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deleteProperties(propertiesToDelete: string[], objectItem: any): any {
|
|
||||||
debug('deleted unused version properties');
|
|
||||||
_.forEach(propertiesToDelete, (property): any => {
|
|
||||||
delete objectItem[property];
|
|
||||||
});
|
|
||||||
|
|
||||||
return objectItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addScope(scope: string, packageName: string): string {
|
|
||||||
return `@${scope}/${packageName}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortByName(packages: any[], orderAscending: boolean | void = true): string[] {
|
export function sortByName(packages: any[], orderAscending: boolean | void = true): string[] {
|
||||||
|
@ -38,6 +13,16 @@ export function sortByName(packages: any[], orderAscending: boolean | void = tru
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasLogin(config: ConfigYaml) {
|
export type AuthorAvatar = Author & { avatar?: string };
|
||||||
return _.isNil(config?.web?.login) || config?.web?.login === true;
|
|
||||||
|
export function addScope(scope: string, packageName: string): string {
|
||||||
|
return `@${scope}/${packageName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProperties(propertiesToDelete: string[], objectItem: any): any {
|
||||||
|
_.forEach(propertiesToDelete, (property): any => {
|
||||||
|
delete objectItem[property];
|
||||||
|
});
|
||||||
|
|
||||||
|
return objectItem;
|
||||||
}
|
}
|
|
@ -9,7 +9,7 @@ import { initializeServer as initializeServerHelper } from '@verdaccio/test-help
|
||||||
|
|
||||||
import routes from '../src';
|
import routes from '../src';
|
||||||
|
|
||||||
setup([]);
|
setup({});
|
||||||
|
|
||||||
export const getConf = (configName: string) => {
|
export const getConf = (configName: string) => {
|
||||||
const configPath = path.join(__dirname, 'config', configName);
|
const configPath = path.join(__dirname, 'config', configName);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { sortByName } from '../src/utils/web-utils';
|
import { sortByName } from '../src/web-utils';
|
||||||
|
|
||||||
describe('Utilities', () => {
|
describe('Utilities', () => {
|
||||||
describe('Sort packages', () => {
|
describe('Sort packages', () => {
|
||||||
|
|
553
pnpm-lock.yaml
553
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue