0
Fork 0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-30 22:34:10 -05:00

feat(plugin): improve plugin loader (#3370)

* feat(plugin): implement scope package support plugins

* feat(plugin): improve plugin loader

* chore: fix build

* chore: cover config path case

* chore: async ui thene plugin

* chore: store async plugin

* chore: refactor plugin loader auth

* feat: filter refactoring

* chore: remove old plugin loader

* chore: add changeset

* chore: add docs

* chore: refactor relative plugin loader

* Update user.jwt.spec.ts

* Update user.jwt.spec.ts
This commit is contained in:
Juan Picado 2022-09-16 08:02:08 +02:00 committed by GitHub
parent 98afd48378
commit 9fc2e79611
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 807 additions and 525 deletions

View file

@ -14,6 +14,5 @@
],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": []
"updateInternalDependencies": "patch"
}

View file

@ -0,0 +1,76 @@
---
'@verdaccio/api': major
'@verdaccio/auth': major
'@verdaccio/config': major
'@verdaccio/types': major
'@verdaccio/loaders': major
'@verdaccio/node-api': major
'verdaccio-audit': major
'verdaccio-auth-memory': major
'verdaccio-htpasswd': major
'@verdaccio/local-storage': major
'verdaccio-memory': major
'@verdaccio/server': major
'@verdaccio/server-fastify': major
'@verdaccio/store': major
'@verdaccio/test-helper': major
'customprefix-auth': major
'verdaccio': major
'@verdaccio/web': major
---
feat(plugins): improve plugin loader
### Changes
- Add scope plugin support to 6.x https://github.com/verdaccio/verdaccio/pull/3227
- Avoid config collisions https://github.com/verdaccio/verdaccio/issues/928
- https://github.com/verdaccio/verdaccio/issues/1394
- `config.plugins` plugin path validations
- Updated algorithm for plugin loader.
- improved documentation (included dev)
## Features
- Add scope plugin support to 6.x https://github.com/verdaccio/verdaccio/pull/3227
- Custom prefix:
```
// config.yaml
server:
pluginPrefix: mycompany
middleware:
audit:
foo: 1
```
This configuration will look up for `mycompany-audit` instead `Verdaccio-audit`.
## Breaking Changes
### sinopia plugins
- `sinopia` fallback support is removed, but can be restored using `pluginPrefix`
### plugin filter
- method rename `filter_metadata`->`filterMetadata`
### Plugin constructor does not merge configs anymore https://github.com/verdaccio/verdaccio/issues/928
The plugin receives as first argument `config`, which represents the config of the plugin. Example:
```
// config.yaml
auth:
plugin:
foo: 1
bar: 2
export class Plugin<T> {
public constructor(config: T, options: PluginOptions) {
console.log(config);
// {foo:1, bar: 2}
}
}
```

View file

@ -1,116 +0,0 @@
## Development notes
The `5.x` still under development, key points:
Ensure you have `nvm` installed or the latest Node.js (check `.nvmrc`
for mode details).
```bash
nvm install
```
Verdaccio uses **pnpm** as monorepo management. To install
```bash
npm i -g pnpm@latest-6
```
Install all needed packages
```bash
pnpm install
```
For building the application:
```bash
pnpm build
```
Running the test
```
pnpm test
```
### Running the application (with UI hot reloading)
```bash
pnpm start
```
with hot reloading (server and UI), `nodemon` will restart the server and `babel` runs
in watch mode.
```bash
pnpm start:watch
```
Running with `ts-node`
```
pnpm start:ts
```
### Running the Website
We use _Gatsbyjs_ as development stack for website,
please [for more information check their official guidelines.](https://www.gatsbyjs.com/docs/quick-start/)
```
pnpm website:develop
```
### Running E2E
For running the CLI test
```
pnpm test:e2e:cli
```
For running the UI test
```
pnpm test:e2e:ui
```
### Linting
Linting the code.
```bash
pnpm lint
```
For website runs
```bash
pnpm website:lint
```
Formatting the code with prettier
```bash
pnpm prettier
```
### Debugging
Run the server in debug mode (it does not include UI hot reload)
with `--inspect` support.
```
pnpm debug
pnpm debug:break
```
> requires `pnpm build` previously
#### debug internal output
Each verdaccio module uses `debug`, use the namespaces in combination with filters to get a verbose output about each action, for example:
```
DEBUG=verdaccio:* pnpm start
```

View file

@ -5,6 +5,7 @@ module.exports = {
},
verbose: false,
collectCoverage: true,
coverageReporters: ['text', 'html'],
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!**/partials/**', '!**/fixture/**'],
coveragePathIgnorePatterns: ['node_modules', 'fixtures'],
coverageThreshold: {

View file

@ -27,7 +27,8 @@ export const getConf = (conf) => {
};
export async function initializeServer(configName): Promise<Application> {
return initializeServerHelper(getConf(configName), [apiMiddleware], Storage);
const config = getConf(configName);
return initializeServerHelper(config, [apiMiddleware], Storage);
}
export function createUser(app, name: string, password: string): supertest.Test {

View file

@ -22,6 +22,9 @@ jest.mock('@verdaccio/auth', () => ({
apiJWTmiddleware() {
return mockApiJWTmiddleware();
}
init() {
return Promise.resolve();
}
allow_access(_d, f_, cb) {
cb(null, true);
}

View file

@ -7,6 +7,8 @@ import { createUser, getPackage, initializeServer } from './_helper';
const FORBIDDEN_VUE = 'authorization required to access package vue';
jest.setTimeout(20000);
describe('token', () => {
describe('basics', () => {
const FAKE_TOKEN: string = buildToken(TOKEN_BEARER, 'fake');

View file

@ -35,6 +35,9 @@ jest.mock('@verdaccio/auth', () => ({
apiJWTmiddleware() {
return mockApiJWTmiddleware();
}
init() {
return Promise.resolve();
}
allow_access(_d, f_, cb) {
cb(null, true);
}

View file

@ -1,7 +1,7 @@
import buildDebug from 'debug';
import { NextFunction, Request, Response } from 'express';
import _ from 'lodash';
import { HTPasswd, HTPasswdConfig } from 'verdaccio-htpasswd';
import { HTPasswd } from 'verdaccio-htpasswd';
import { createAnonymousRemoteUser, createRemoteUser } from '@verdaccio/config';
import {
@ -12,7 +12,7 @@ import {
VerdaccioError,
errorUtils,
} from '@verdaccio/core';
import { loadPlugin } from '@verdaccio/loaders';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import {
AllowAccess,
AuthPluginPackage,
@ -82,6 +82,7 @@ export interface IAuth extends IBasicAuth<Config>, IAuthMiddleware, TokenEncrypt
plugins: any[];
allow_unpublish(pkg: AuthPluginPackage, user: RemoteUser, callback: Callback): void;
invalidateToken(token: string): Promise<void>;
init(): Promise<void>;
}
class Auth implements IAuth {
@ -94,24 +95,32 @@ class Auth implements IAuth {
this.config = config;
this.logger = LoggerApi.logger.child({ sub: 'auth' });
this.secret = config.secret;
this.plugins = [];
if (!this.secret) {
throw new TypeError('secret it is required value on initialize the auth class');
}
}
public async init() {
let plugins = await this.loadPlugin();
debug('auth plugins found %s', plugins.length);
if (!plugins || plugins.length === 0) {
plugins = this.loadDefaultPlugin();
}
this.plugins = plugins;
this.plugins =
_.isNil(config?.auth) === false ? this._loadPlugin(config) : this.loadDefaultPlugin(config);
this._applyDefaultPlugins();
}
private loadDefaultPlugin(config: Config) {
const plugingConf: HTPasswdConfig = { ...config, file: './htpasswd' };
const pluginOptions: PluginOptions<{}> = {
config,
private loadDefaultPlugin() {
debug('load default auth plugin');
const pluginOptions: PluginOptions = {
config: this.config,
logger: this.logger,
};
let authPlugin;
try {
authPlugin = new HTPasswd(plugingConf, pluginOptions as any as PluginOptions<HTPasswdConfig>);
authPlugin = new HTPasswd({ file: './htpasswd' }, pluginOptions as any as PluginOptions);
} catch (error: any) {
debug('error on loading auth htpasswd plugin stack: %o', error);
return [];
@ -120,22 +129,20 @@ class Auth implements IAuth {
return [authPlugin];
}
private _loadPlugin(config: Config): IPluginAuth<Config>[] {
const pluginOptions = {
config,
logger: this.logger,
};
return loadPlugin<IPluginAuth<Config>>(
config,
config.auth,
pluginOptions,
private async loadPlugin(): Promise<IPluginAuth<Config>[]> {
return asyncLoadPlugin<IPluginAuth<Config>>(
this.config.auth,
{
config: this.config,
logger: this.logger,
},
(plugin: IPluginAuth<Config>): boolean => {
const { authenticate, allow_access, allow_publish } = plugin;
// @ts-ignore
return authenticate || allow_access || allow_publish;
}
},
this.config?.server?.pluginPrefix
);
}

View file

@ -71,6 +71,7 @@ describe('Auth utilities', () => {
): Promise<string> {
const config: Config = getConfig(configFileName, secret);
const auth: IAuth = new Auth(config);
await auth.init();
// @ts-ignore
const spy = jest.spyOn(auth, methodToSpy);
// @ts-ignore
@ -409,6 +410,7 @@ describe('Auth utilities', () => {
const secret = 'b2df428b9929d3ace7c598bbf4e496b2';
const config: Config = getConfig('security-legacy', secret);
const auth: IAuth = new Auth(config);
await auth.init();
const token = auth.aesEncrypt(null);
const security: Security = config.security;
const credentials = getMiddlewareCredentials(

View file

@ -1,7 +1,7 @@
import _ from 'lodash';
import path from 'path';
import { IAuth } from '@verdaccio/auth';
import { Config as AppConfig, ROLES } from '@verdaccio/config';
import { Config as AppConfig, ROLES, getDefaultConfig } from '@verdaccio/config';
import { errorUtils } from '@verdaccio/core';
import { setup } from '@verdaccio/logger';
import { Config } from '@verdaccio/types';
@ -12,22 +12,31 @@ import { authPluginFailureConf, authPluginPassThrougConf, authProfileConf } from
setup([]);
describe('AuthTest', () => {
test('should be defined', () => {
const config: Config = new AppConfig(_.cloneDeep(authProfileConf));
test('should init correctly', async () => {
const config: Config = new AppConfig({ ...authProfileConf });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
});
test('should load default auth plugin', async () => {
const config: Config = new AppConfig({ ...authProfileConf, auth: undefined });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
});
describe('test authenticate method', () => {
describe('test authenticate states', () => {
test('should be a success login', () => {
const config: Config = new AppConfig(_.cloneDeep(authProfileConf));
test('should be a success login', async () => {
const config: Config = new AppConfig({ ...authProfileConf });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
const callback = jest.fn();
@ -50,11 +59,11 @@ describe('AuthTest', () => {
});
});
test('should be a fail on login', () => {
test('should be a fail on login', async () => {
const config: Config = new AppConfig(authPluginFailureConf);
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
const callback = jest.fn();
@ -69,11 +78,11 @@ describe('AuthTest', () => {
// that might make break the request
// the @ts-ignore below are intended
describe('test authenticate out of control inputs from plugins', () => {
test('should skip falsy values', () => {
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
test('should skip falsy values', async () => {
const config: Config = new AppConfig({ ...authPluginPassThrougConf });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
const callback = jest.fn();
@ -89,11 +98,11 @@ describe('AuthTest', () => {
}
});
test('should error truthy non-array', () => {
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
test('should error truthy non-array', async () => {
const config: Config = new AppConfig({ ...authPluginPassThrougConf });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
const callback = jest.fn();
@ -107,11 +116,11 @@ describe('AuthTest', () => {
}
});
test('should skip empty array', () => {
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
test('should skip empty array', async () => {
const config: Config = new AppConfig({ ...authPluginPassThrougConf });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
const callback = jest.fn();
@ -124,11 +133,11 @@ describe('AuthTest', () => {
expect(callback.mock.calls[0][1]).toBeUndefined();
});
test('should accept valid array', () => {
const config: Config = new AppConfig(_.cloneDeep(authPluginPassThrougConf));
test('should accept valid array', async () => {
const config: Config = new AppConfig({ ...authPluginPassThrougConf });
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
expect(auth).toBeDefined();
const callback = jest.fn();
@ -144,4 +153,31 @@ describe('AuthTest', () => {
});
});
});
describe('test multiple authenticate methods', () => {
test('should skip falsy values', async () => {
const config: Config = new AppConfig({
...getDefaultConfig(),
plugins: path.join(__dirname, './partials/plugin'),
auth: {
success: {},
'fail-invalid-method': {},
},
});
config.checkSecretKey('12345');
const auth: IAuth = new Auth(config);
await auth.init();
return new Promise((resolve) => {
auth.authenticate('foo', 'bar', (err, value) => {
expect(value).toEqual({
name: 'foo',
groups: ['test', '$all', '$authenticated', '@all', '@authenticated', 'all'],
real_groups: ['test'],
});
resolve(value);
});
});
});
});
});

View file

@ -4,21 +4,32 @@ import { getDefaultConfig } from '@verdaccio/config';
export const authProfileConf = {
...getDefaultConfig(),
plugins: path.join(__dirname, '../partials/plugin'),
auth: {
[`${path.join(__dirname, '../partials/plugin/authenticate.success')}`]: {},
success: {},
},
};
export const authPluginFailureConf = {
...getDefaultConfig(),
plugins: path.join(__dirname, '../partials/plugin'),
auth: {
[`${path.join(__dirname, '../partials/plugin/authenticate.fail.js')}`]: {},
fail: {},
},
};
export const authPluginPassThrougConf = {
...getDefaultConfig(),
plugins: path.join(__dirname, '../partials/plugin'),
auth: {
[`${path.join(__dirname, '../partials/plugin/authenticate.passthroug')}`]: {},
passthroug: {},
},
};
export const authFailInvalidMethod = {
...getDefaultConfig(),
plugins: path.join(__dirname, '../partials/plugin'),
auth: {
'fail-invalid-method': {},
},
};

View file

@ -1,11 +0,0 @@
import { errorUtils } from '@verdaccio/core';
module.exports = function () {
return {
authenticate(user, pass, callback) {
// we return an 500 error, the second argument must be false.
// https://verdaccio.org/docs/en/dev-plugins#onerror
callback(errorUtils.getInternalError(), false);
},
};
};

View file

@ -0,0 +1,11 @@
const { errorUtils } = require('@verdaccio/core');
module.exports = function () {
return {
authenticateFake(user, pass, callback) {
/* user and pass are used here to forward errors
and success types respectively for testing purposes */
callback(errorUtils.getInternalError(), false);
},
};
};

View file

@ -0,0 +1,5 @@
{
"name": "verdaccio-fail",
"main": "fail.js",
"version": "1.0.0"
}

View file

@ -0,0 +1,11 @@
const { errorUtils } = require('@verdaccio/core');
module.exports = function () {
return {
authenticate(user, pass, callback) {
/* user and pass are used here to forward errors
and success types respectively for testing purposes */
callback(errorUtils.getInternalError(), false);
},
};
};

View file

@ -0,0 +1,5 @@
{
"name": "verdaccio-fail",
"main": "fail.js",
"version": "1.0.0"
}

View file

@ -0,0 +1,5 @@
{
"name": "verdaccio-passthroug",
"main": "passthroug.js",
"version": "1.0.0"
}

View file

@ -0,0 +1,5 @@
{
"name": "verdaccio-success",
"main": "success.js",
"version": "1.0.0"
}

View file

@ -11,8 +11,9 @@
# path to a directory with all packages
storage: ./storage
# path to a directory with plugins to include
plugins: ./plugins
# path to a directory with plugins to include, the plugins folder has the higher priority for loading plugins
# disable this folder to avoid warnings if is not used
# plugins: ./plugins
# https://verdaccio.org/docs/webui
web:
@ -99,6 +100,9 @@ packages:
# WORKAROUND: Through given configuration you can workaround following issue https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
server:
keepAliveTimeout: 60
# The pluginPrefix replaces the default plugins prefix which is `verdaccio`, please don't include `-`. If `something` is provided
# the resolve package will be `something-xxxx`.
# pluginPrefix: something
# https://verdaccio.org/docs/configuration#offline-publish
# publish:

View file

@ -14,7 +14,8 @@
# path to a directory with all packages
storage: /verdaccio/storage/data
# path to a directory with plugins to include
# path to a directory with plugins to include, the plugins folder has the higher priority for loading plugins
# disable this folder to avoid warnings if is not used
plugins: /verdaccio/plugins
# https://verdaccio.org/docs/webui
@ -105,6 +106,9 @@ packages:
# https://github.com/verdaccio/verdaccio/issues/301. Set to 0 in case 60 is not enough.
server:
keepAliveTimeout: 60
# The pluginPrefix replaces the default plugins prefix which is `verdaccio`, please don't include `-`. If `something` is provided
# the resolve package will be `something-xxxx`.
# pluginPrefix: something
# https://verdaccio.org/docs/configuration#offline-publish
# publish:

View file

@ -43,7 +43,7 @@ class Config implements AppConfig {
public configPath: string;
public storage: string | void;
public plugins: string | void;
public plugins: string | void | null;
public security: Security;
public serverSettings: ServerSettingsConf;
// @ts-ignore

View file

@ -226,6 +226,13 @@ export type ServerSettingsConf = {
// express-rate-limit settings
rateLimit: RateLimit;
keepAliveTimeout?: number;
/**
* Plugins should be prefixed verdaccio-XXXXXX by default.
* To override the default prefix, use this property without `-`
* If you set pluginPrefix: acme, the packages to resolve will be
* acme-XXXXXX
*/
pluginPrefix?: string;
};
/**
@ -245,7 +252,7 @@ export interface ConfigYaml {
listen?: ListenAddress;
https?: HttpsConf;
http_proxy?: string;
plugins?: string | void;
plugins?: string | void | null;
https_proxy?: string;
no_proxy?: string;
max_body_size?: string;
@ -264,7 +271,7 @@ export interface ConfigYaml {
}
/**
* Configuration object with additonal methods for configuration, includes yaml and internal medatada.
* Configuration object with additional methods for configuration, includes yaml and internal medatada.
* @interface Config
* @extends {ConfigYaml}
*/

View file

@ -1,7 +1,7 @@
import { Config, Logger } from '../configuration';
export class Plugin<T> {
public constructor(config: T, options: PluginOptions<T>) {}
public constructor(config: T, options: PluginOptions) {}
}
export interface IPlugin<T> {
@ -9,7 +9,7 @@ export interface IPlugin<T> {
version?: string;
}
export interface PluginOptions<T> {
config: T & Config;
export interface PluginOptions {
config: Config;
logger: Logger;
}

View file

@ -2,5 +2,5 @@ import { Manifest } from '../manifest';
import { IPlugin } from './commons';
export interface IPluginStorageFilter<T> extends IPlugin<T> {
filter_metadata(packageInfo: Manifest): Promise<Manifest>;
filterMetadata(packageInfo: Manifest): Promise<Manifest>;
}

View file

@ -84,7 +84,7 @@ export type IPackageStorageManager = ILocalPackageManager;
*/
interface ILocalData<T> extends IPlugin<T>, ITokenActions {
logger: Logger;
config: T & Config;
config: T;
add(name: string): Promise<void>;
remove(name: string): Promise<void>;
get(): Promise<any>;

View file

@ -20,7 +20,10 @@
"devDependencies": {
"@verdaccio/core": "workspace:6.0.0-6-next.47",
"@verdaccio/config": "workspace:6.0.0-6-next.47",
"@verdaccio/types": "workspace:11.0.0-6-next.16"
"@verdaccio/types": "workspace:11.0.0-6-next.16",
"@verdaccio-scope/verdaccio-auth-foo": "0.0.2",
"verdaccio-auth-memory": "workspace:*",
"customprefix-auth": "0.0.1"
},
"homepage": "https://verdaccio.org",
"keywords": [

View file

@ -1 +1 @@
export * from './plugin-loader';
export { asyncLoadPlugin } from './plugin-async-loader';

View file

@ -0,0 +1,132 @@
import buildDebug from 'debug';
import { lstat } from 'fs/promises';
import { dirname, isAbsolute, join, resolve } from 'path';
import { logger } from '@verdaccio/logger';
import { Config, IPlugin, Logger } from '@verdaccio/types';
import { isES6, isValid, tryLoad } from './utils';
const debug = buildDebug('verdaccio:plugin:loader:async');
async function isDirectory(pathFolder) {
const stat = await lstat(pathFolder);
return stat.isDirectory();
}
export type Params = { config: Config; logger: Logger };
/**
* The plugin loader find recursively plugins, if one plugin fails is ignored and report the error to the logger.
*
* The loader follows the order:
* - If the at the `config.yaml` file the `plugins: ./plugins` is defined
* - If is absolute will use the provided path
* - If is relative, will use the base path of the config file. eg: /root/config.yaml the plugins folder should be
* hosted at /root/plugins
* - The next step is find at the node_modules or global based on the `require` native algorithm.
* - If the package is scoped eg: @scope/foo, try to load the package `@scope/foo`
* - If the package is not scoped, will use the default prefix: verdaccio-foo.
* - If a custom prefix is provided, the verdaccio- is replaced by the config.server.pluginPrefix.
*
* The `sanityCheck` is the validation for the required methods to load the plugin, if the validation fails the plugin won't be loaded.
* The `params` is an object that contains the global configuration and the logger.
*
* @param {*} pluginConfigs the custom plugin section
* @param {*} params a set of params to initialize the plugin
* @param {*} sanityCheck callback that check the shape that should fulfill the plugin
* @param {*} prefix by default is verdaccio but can be override with config.server.pluginPrefix
* @return {Array} list of plugins
*/
export async function asyncLoadPlugin<T extends IPlugin<T>>(
pluginConfigs: any = {},
params: Params,
sanityCheck: any,
prefix: string = 'verdaccio'
): Promise<any> {
const pluginsIds = Object.keys(pluginConfigs);
const { config } = params;
let plugins: any[] = [];
for (let pluginId of pluginsIds) {
debug('plugin %s', pluginId);
if (typeof config.plugins === 'string') {
let pluginsPath = config.plugins;
debug('plugin path %s', pluginsPath);
if (!isAbsolute(pluginsPath)) {
if (typeof config.config_path === 'string' && !config.configPath) {
logger.error(
'configPath is missing and the legacy config.config_path is not available for loading plugins'
);
}
if (!config.configPath) {
logger.error('config path property is required for loading plugins');
continue;
}
pluginsPath = resolve(join(dirname(config.configPath), pluginsPath));
}
logger.debug({ path: pluginsPath }, 'plugins folder defined, loading plugins from @{path} ');
// throws if is nto a directory
try {
await isDirectory(pluginsPath);
const pluginDir = pluginsPath;
const externalFilePlugin = resolve(pluginDir, `${prefix}-${pluginId}`);
let plugin = tryLoad(externalFilePlugin);
if (plugin && isValid(plugin)) {
plugin = executePlugin(plugin, pluginConfigs[pluginId], params);
if (!sanityCheck(plugin)) {
logger.error(
{ content: externalFilePlugin },
"@{content} doesn't look like a valid plugin"
);
continue;
}
plugins.push(plugin);
continue;
}
} catch (err: any) {
logger.warn(
{ err: err.message, pluginsPath, pluginId },
'@{err} on loading plugins at @{pluginsPath} for @{pluginId}'
);
}
}
if (typeof pluginId === 'string') {
const isScoped: boolean = pluginId.startsWith('@') && pluginId.includes('/');
debug('is scoped plugin %s', isScoped);
const pluginName = isScoped ? pluginId : `${prefix}-${pluginId}`;
debug('plugin pkg name %s', pluginName);
let plugin = tryLoad(pluginName);
if (plugin && isValid(plugin)) {
plugin = executePlugin(plugin, pluginConfigs[pluginId], params);
if (!sanityCheck(plugin)) {
logger.error({ content: pluginName }, "@{content} doesn't look like a valid plugin");
continue;
}
plugins.push(plugin);
continue;
} else {
logger.error(
{ pluginName },
'package not found, try to install @{pluginName} with a package manager'
);
continue;
}
}
}
debug('plugin found %s', plugins.length);
return plugins;
}
export function executePlugin(plugin, pluginConfig, params: Params) {
if (isES6(plugin)) {
debug('plugin is ES6');
// eslint-disable-next-line new-cap
return new plugin.default(pluginConfig, params);
} else {
debug('plugin is commonJS');
return plugin(pluginConfig, params);
}
}

View file

@ -1,143 +0,0 @@
import buildDebug from 'debug';
import _ from 'lodash';
import Path from 'path';
import { logger } from '@verdaccio/logger';
import { Config, IPlugin } from '@verdaccio/types';
const debug = buildDebug('verdaccio:plugin:loader');
export const MODULE_NOT_FOUND = 'MODULE_NOT_FOUND';
/**
* Requires a module.
* @param {*} path the module's path
* @return {Object}
*/
function tryLoad(path: string): any {
try {
return require(path);
} catch (err: any) {
if (err.code === MODULE_NOT_FOUND) {
return null;
}
throw err;
}
}
function mergeConfig(appConfig, pluginConfig): Config {
return _.merge(appConfig, pluginConfig);
}
function isValid(plugin): boolean {
return _.isFunction(plugin) || _.isFunction(plugin.default);
}
function isES6(plugin): boolean {
return Object.keys(plugin).includes('default');
}
// export type PluginGeneric<R, T extends IPlugin<R> = ;
/**
* Load a plugin following the rules
* - First try to load from the internal directory plugins (which will disappear soon or later).
* - A second attempt from the external plugin directory
* - A third attempt from node_modules, in case to have multiple match as for instance
* verdaccio-ldap
* and sinopia-ldap. All verdaccio prefix will have preferences.
* @param {*} config a reference of the configuration settings
* @param {*} pluginConfigs
* @param {*} params a set of params to initialize the plugin
* @param {*} sanityCheck callback that check the shape that should fulfill the plugin
* @return {Array} list of plugins
*/
export function loadPlugin<T extends IPlugin<T>>(
config: Config,
pluginConfigs: any = {},
params: any,
sanityCheck: any,
prefix: string = 'verdaccio'
): any[] {
return Object.keys(pluginConfigs).map((pluginId: string): IPlugin<T> => {
let plugin;
const localPlugin = Path.resolve(__dirname + '/../plugins', pluginId);
// try local plugins first
plugin = tryLoad(localPlugin);
// try the external plugin directory
if (plugin === null && config.plugins) {
const pluginDir = config.plugins;
const externalFilePlugin = Path.resolve(pluginDir, pluginId);
plugin = tryLoad(externalFilePlugin);
// npm package
if (plugin === null && pluginId.match(/^[^\.\/]/)) {
plugin = tryLoad(Path.resolve(pluginDir, `${prefix}-${pluginId}`));
// compatibility for old sinopia plugins
if (!plugin) {
plugin = tryLoad(Path.resolve(pluginDir, `sinopia-${pluginId}`));
}
}
}
// npm package
if (plugin === null && pluginId.match(/^[^\.\/]/)) {
plugin = tryLoad(`${prefix}-${pluginId}`);
// compatibility for old sinopia plugins
if (!plugin) {
plugin = tryLoad(`sinopia-${pluginId}`);
}
}
if (plugin === null) {
plugin = tryLoad(pluginId);
}
// relative to config path
debug('config path: %s', config.configPath);
if (plugin === null && pluginId.match(/^\.\.?($|\/)/) && config.configPath) {
plugin = tryLoad(Path.resolve(Path.dirname(config.configPath), pluginId));
}
if (plugin === null) {
logger.error(
{ content: pluginId, prefix },
'plugin not found. try npm install @{prefix}-@{content}'
);
throw Error(`
${prefix}-${pluginId} plugin not found. try "npm install ${prefix}-${pluginId}"`);
}
if (!isValid(plugin)) {
logger.error(
{ content: pluginId },
'@{prefix}-@{content} plugin does not have the right code structure'
);
throw Error(`"${pluginId}" plugin does not have the right code structure`);
}
/* eslint new-cap:off */
try {
plugin = isES6(plugin)
? new plugin.default(mergeConfig(config, pluginConfigs[pluginId]), params)
: new plugin(pluginConfigs[pluginId], params);
} catch (error: any) {
plugin = null;
logger.error({ error, pluginId }, 'error loading a plugin @{pluginId}: @{error}');
}
/* eslint new-cap:off */
if (plugin === null || !sanityCheck(plugin)) {
logger.error(
{ content: pluginId, prefix },
"@{prefix}-@{content} doesn't look like a valid plugin"
);
throw Error(`sanity check has failed, "${pluginId}" is not a valid plugin`);
}
debug('Plugin successfully loaded: %o-%o', pluginId, prefix);
return plugin;
});
}

View file

@ -0,0 +1,40 @@
import buildDebug from 'debug';
import _ from 'lodash';
import { logger } from '@verdaccio/logger';
import { Config } from '@verdaccio/types';
const debug = buildDebug('verdaccio:plugin:loader:utils');
const MODULE_NOT_FOUND = 'MODULE_NOT_FOUND';
export function mergeConfig(appConfig, pluginConfig): Config {
return _.merge(appConfig, pluginConfig);
}
export function isValid(plugin): boolean {
return _.isFunction(plugin) || _.isFunction(plugin.default);
}
export function isES6(plugin): boolean {
return Object.keys(plugin).includes('default');
}
/**
* Requires a module.
* @param {*} path the module's path
* @return {Object}
*/
export function tryLoad(path: string): any {
try {
debug('loading plugin %s', path);
return require(path);
} catch (err: any) {
if (err.code === MODULE_NOT_FOUND) {
debug('plugin %s not found', path);
return null;
}
logger.error({ err: err.msg }, 'error loading plugin @{err}');
throw err;
}
}

View file

@ -0,0 +1,3 @@
auth:
'@verdaccio-scope/verdaccio-auth-foo':
enabled: true

View file

@ -0,0 +1,3 @@
auth:
auth-memory:
enabled: true

View file

@ -0,0 +1,3 @@
auth:
not-found:
enabled: true

View file

@ -0,0 +1,4 @@
auth:
something:
enabled: true
plugins: /roo/does-not-exist

View file

@ -0,0 +1,4 @@
plugins: '../test-plugin-storage'
auth:
plugin:
enabled: true

View file

@ -0,0 +1,3 @@
auth:
'@verdaccio-scope/verdaccio-auth-foo':
enabled: true

View file

@ -0,0 +1,3 @@
auth:
plugin:
enabled: true

View file

@ -0,0 +1,7 @@
function ValidScopedVerdaccioPlugin() {
return {
authenticate: function () {},
};
}
module.exports = ValidVerdaccioPlugin;

View file

@ -0,0 +1,11 @@
{
"name": "@verdaccio-scoped/verdaccio-plugin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View file

@ -1,88 +0,0 @@
import path from 'path';
import { setup } from '@verdaccio/logger';
import { loadPlugin } from '../src/plugin-loader';
setup([]);
describe('plugin loader', () => {
const relativePath = path.join(__dirname, './partials/test-plugin-storage');
const buildConf = (name) => {
return {
config_path: path.join(__dirname, './'),
max_users: 0,
auth: {
[`${relativePath}/${name}`]: {},
},
};
};
describe('auth plugins', () => {
test('testing auth valid plugin loader', () => {
const _config = buildConf('verdaccio-plugin');
// @ts-ignore
const plugins = loadPlugin(_config, _config.auth, {}, function (plugin) {
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
});
expect(plugins).toHaveLength(1);
});
test('testing storage valid plugin loader', () => {
const _config = buildConf('verdaccio-es6-plugin');
// @ts-ignore
const plugins = loadPlugin(_config, _config.auth, {}, function (p) {
return p.getPackageStorage;
});
expect(plugins).toHaveLength(1);
});
test('testing auth plugin invalid plugin', () => {
const _config = buildConf('invalid-plugin');
try {
// @ts-ignore
loadPlugin(_config, _config.auth, {}, function (p) {
return p.authenticate || p.allow_access || p.allow_publish;
});
} catch (e: any) {
expect(e.message).toEqual(
`"${relativePath}/invalid-plugin" plugin does not have the right code structure`
);
}
});
test('testing auth plugin invalid plugin sanityCheck', () => {
const _config = buildConf('invalid-plugin-sanity');
try {
// @ts-ignore
loadPlugin(_config, _config.auth, {}, function (plugin) {
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
});
} catch (err: any) {
expect(err.message).toEqual(
`sanity check has failed, "${relativePath}/invalid-plugin-sanity" is not a valid plugin`
);
}
});
test('testing auth plugin no plugins', () => {
const _config = buildConf('invalid-package');
try {
// @ts-ignore
loadPlugin(_config, _config.auth, {}, function (plugin) {
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
});
} catch (e: any) {
expect(e.message).toMatch('plugin not found');
expect(e.message.replace(/\\/g, '/')).toMatch(
'/partials/test-plugin-storage/invalid-package'
);
}
});
test.todo('test middleware plugins');
test.todo('test storage plugins');
});
});

View file

@ -0,0 +1,168 @@
import path from 'path';
import { Config, parseConfigFile } from '@verdaccio/config';
import { logger, setup } from '@verdaccio/logger';
import { asyncLoadPlugin } from '../src/plugin-async-loader';
function getConfig(file: string) {
const conPath = path.join(__dirname, './partials/config', file);
return new Config(parseConfigFile(conPath));
}
const authSanitize = function (plugin) {
return plugin.authenticate || plugin.allow_access || plugin.allow_publish;
};
const pluginsPartialsFolder = path.join(__dirname, './partials/test-plugin-storage');
setup();
describe('plugin loader', () => {
describe('file plugins', () => {
describe('absolute path', () => {
test('testing auth valid plugin loader', async () => {
const config = getConfig('valid-plugin.yaml');
config.plugins = pluginsPartialsFolder;
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, authSanitize);
expect(plugins).toHaveLength(1);
});
test('should handle does not exist plugin folder', async () => {
const config = getConfig('plugins-folder-fake.yaml');
const plugins = await asyncLoadPlugin(
config.auth,
{ logger: logger, config: config },
authSanitize
);
expect(plugins).toHaveLength(0);
});
test('testing load auth npm package invalid method check', async () => {
const config = getConfig('valid-plugin.yaml');
config.plugins = pluginsPartialsFolder;
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, (p) => p.anyMethod);
expect(plugins).toHaveLength(0);
});
test('should fails if plugins folder is not a directory', async () => {
const config = getConfig('plugins-folder-fake.yaml');
// force file instead a folder.
config.plugins = path.join(__dirname, 'just-a-file.js');
const plugins = await asyncLoadPlugin(
config.auth,
{ logger: logger, config: config },
authSanitize
);
expect(plugins).toHaveLength(0);
});
});
describe('relative path', () => {
test('should resolve plugin based on relative path', async () => {
const config = getConfig('relative-plugins.yaml');
// force file instead a folder.
const plugins = await asyncLoadPlugin(
config.auth,
{ logger: logger, config: config },
authSanitize
);
expect(plugins).toHaveLength(1);
});
test('should fails if config path is missing', async () => {
const config = getConfig('relative-plugins.yaml');
// @ts-expect-error
config.configPath = undefined;
// @ts-expect-error
config.config_path = undefined;
// force file instead a folder.
const plugins = await asyncLoadPlugin(
config.auth,
{ logger: logger, config: config },
authSanitize
);
expect(plugins).toHaveLength(0);
});
// config.config_path is not considered for loading plugins due legacy support
test('should fails if config path is missing (only config_path)', async () => {
const config = getConfig('relative-plugins.yaml');
// @ts-expect-error
config.configPath = undefined;
// force file instead a folder.
const plugins = await asyncLoadPlugin(
config.auth,
{ logger: logger, config: config },
authSanitize
);
expect(plugins).toHaveLength(0);
});
});
});
describe('npm plugins', () => {
test('testing load auth npm package', async () => {
const config = getConfig('npm-plugin-auth.yaml');
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, authSanitize);
expect(plugins).toHaveLength(1);
});
test('should handle not found installed package', async () => {
const config = getConfig('npm-plugin-not-found.yaml');
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, (p) => p.anyMethod);
expect(plugins).toHaveLength(0);
});
test('testing load auth npm package invalid method check', async () => {
const config = getConfig('npm-plugin-auth.yaml');
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, (p) => p.anyMethod);
expect(plugins).toHaveLength(0);
});
test('testing load auth npm package custom prefix', async () => {
const config = getConfig('custom-prefix-auth.yaml');
const plugins = await asyncLoadPlugin(
config.auth,
{ config, logger },
authSanitize,
'customprefix'
);
expect(plugins).toHaveLength(1);
});
test('testing load auth scope npm package', async () => {
const config = getConfig('scope-auth.yaml');
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, authSanitize);
expect(plugins).toHaveLength(1);
});
});
describe('fallback plugins', () => {
test('should fallback to npm package if does not find on plugins folder', async () => {
const config = getConfig('npm-plugin-auth.yaml');
config.plugins = pluginsPartialsFolder;
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, authSanitize);
expect(plugins).toHaveLength(1);
});
test('should fallback to npm package if plugins folder does not exist', async () => {
const config = getConfig('npm-plugin-auth.yaml');
config.plugins = '/does-not-exist';
const plugins = await asyncLoadPlugin(config.auth, { config, logger }, authSanitize);
expect(plugins).toHaveLength(1);
});
});
});

View file

@ -10,7 +10,7 @@ import url from 'url';
import { findConfigFile, parseConfigFile } from '@verdaccio/config';
import { API_ERROR } from '@verdaccio/core';
import { setup } from '@verdaccio/logger';
import server from '@verdaccio/server';
import expressServer from '@verdaccio/server';
import fastifyServer from '@verdaccio/server-fastify';
import { ConfigYaml, HttpsConfKeyCert, HttpsConfPfx } from '@verdaccio/types';
@ -123,7 +123,7 @@ export async function initServer(
}
});
} else {
app = await server(config);
app = await expressServer(config);
const serverFactory = createServerFactory(config, addr, app);
serverFactory
.listen(addr.port || addr.path, addr.host, (): void => {
@ -196,6 +196,6 @@ export async function runServer(config?: string | ConfigYaml): Promise<any> {
displayExperimentsInfoBox(configurationParsed.flags);
// FIXME: get only the first match, the multiple address will be removed
const [addr] = getListListenAddresses(undefined, configurationParsed.listen);
const app = await server(configurationParsed);
const app = await expressServer(configurationParsed);
return createServerFactory(configurationParsed, addr, app);
}

View file

@ -16,7 +16,7 @@ export default class ProxyAudit implements IPluginMiddleware<{}, {}> {
public logger: Logger;
public strict_ssl: boolean;
public constructor(config: ConfigAudit, options: PluginOptions<{}>) {
public constructor(config: ConfigAudit, options: PluginOptions) {
this.enabled = config.enabled || false;
this.strict_ssl = config.strict_ssl !== undefined ? config.strict_ssl : true;
this.logger = options.logger;

View file

@ -1,7 +1,4 @@
// Temporary solution for requiring types will not cause the error.
import { Config } from '@verdaccio/types';
export interface ConfigAudit extends Config {
export interface ConfigAudit {
enabled: boolean;
strict_ssl?: boolean | void;
}

View file

@ -3,6 +3,7 @@ import buildDebug from 'debug';
import { API_ERROR, errorUtils } from '@verdaccio/core';
import {
Callback,
Config,
IPluginAuth,
Logger,
PackageAccess,
@ -18,12 +19,9 @@ export default class Memory implements IPluginAuth<VerdaccioMemoryConfig> {
public _logger: Logger;
public _users: Users;
public _config: {};
public _app_config: VerdaccioMemoryConfig;
public _app_config: Config;
public constructor(
config: VerdaccioMemoryConfig,
appOptions: PluginOptions<VerdaccioMemoryConfig>
) {
public constructor(config: VerdaccioMemoryConfig, appOptions: PluginOptions) {
this._users = config.users || {};
this._config = config;
this._logger = appOptions.logger;

View file

@ -1,5 +1,3 @@
import { Config } from '@verdaccio/types';
export interface UserMemory {
name: string;
password: string;
@ -9,7 +7,7 @@ export interface Users {
[key: string]: UserMemory;
}
export interface VerdaccioMemoryConfig extends Config {
export interface VerdaccioMemoryConfig {
max_users?: number;
users: Users;
}

View file

@ -3,7 +3,7 @@ import fs from 'fs';
import { dirname, join, resolve } from 'path';
import { unlockFile } from '@verdaccio/file-locking';
import { Callback, Config, IPluginAuth, Logger, PluginOptions } from '@verdaccio/types';
import { Callback, IPluginAuth, Logger, PluginOptions } from '@verdaccio/types';
import {
HtpasswdHashAlgorithm,
@ -24,7 +24,7 @@ export type HTPasswdConfig = {
rounds?: number;
max_users?: number;
slow_verify_ms?: number;
} & Config;
};
export const DEFAULT_BCRYPT_ROUNDS = 10;
export const DEFAULT_SLOW_VERIFY_MS = 200;
@ -46,7 +46,7 @@ export default class HTPasswd implements IPluginAuth<HTPasswdConfig> {
private logger: Logger;
private lastTime: any;
// constructor
public constructor(config: HTPasswdConfig, options: PluginOptions<HTPasswdConfig>) {
public constructor(config: HTPasswdConfig, options: PluginOptions) {
this.users = {};
// verdaccio logger

View file

@ -1,7 +1,9 @@
import { PluginOptions } from '@verdaccio/types';
import HTPasswd, { HTPasswdConfig } from './htpasswd';
export default function (config: HTPasswdConfig, stuff): HTPasswd {
return new HTPasswd(config, stuff);
export default function (config: HTPasswdConfig, params: PluginOptions): HTPasswd {
return new HTPasswd(config, params);
}
export { HTPasswd, HTPasswdConfig };

View file

@ -97,10 +97,10 @@ export default class LocalFS implements ILocalFSPackageManager {
if (locked) {
debug('unlock %s', packageJSONFileName);
await this._unlockJSON(packageJSONFileName);
this.logger.debug({ packageName }, 'the package @{packageName} has been updated');
this.logger.debug({ packageName }, 'the package @{packageName} has been updated');
return manifestUpdated;
} else {
this.logger.debug({ packageName }, 'the package @{packageName} has been updated');
this.logger.debug({ packageName }, 'the package @{packageName} has been updated');
return manifestUpdated;
}
} catch (err: any) {

View file

@ -20,7 +20,7 @@ jest.mock('../src/fs', () => ({
setup();
// @ts-expect-error
const optionsPlugin: PluginOptions<{}> = {
const optionsPlugin: PluginOptions = {
logger,
};

View file

@ -1,11 +1,11 @@
import buildDebug from 'debug';
import { errorUtils } from '@verdaccio/core';
import { Callback, Config, IPluginStorage, Logger, PluginOptions, Token } from '@verdaccio/types';
import { Callback, IPluginStorage, Logger, PluginOptions, Token } from '@verdaccio/types';
import MemoryHandler, { DataHandler } from './memory-handler';
export type ConfigMemory = Config & { limit?: number };
export type ConfigMemory = { limit?: number };
export interface MemoryLocalStorage {
secret: string;
list: string[];
@ -22,7 +22,7 @@ class LocalMemory implements IPluginStorage<ConfigMemory> {
private data: MemoryLocalStorage;
public config: ConfigMemory;
public constructor(config: ConfigMemory, options: PluginOptions<ConfigMemory>) {
public constructor(config: ConfigMemory, options: PluginOptions) {
this.config = config;
this.limit = config.limit || DEFAULT_LIMIT;
this.logger = options.logger;

View file

@ -11,12 +11,12 @@ import apiEndpoint from '@verdaccio/api';
import { Auth, IBasicAuth } from '@verdaccio/auth';
import { Config as AppConfig } from '@verdaccio/config';
import { API_ERROR, HTTP_STATUS, errorUtils } from '@verdaccio/core';
import { loadPlugin } from '@verdaccio/loaders';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { errorReportingMiddleware, final, log } from '@verdaccio/middleware';
import { Storage } from '@verdaccio/store';
import { ConfigYaml } from '@verdaccio/types';
import { Config as IConfig, IPlugin, IPluginStorageFilter } from '@verdaccio/types';
import { Config as IConfig, IPlugin } from '@verdaccio/types';
import webMiddleware from '@verdaccio/web';
import { $NextFunctionVer, $RequestExtend, $ResponseExtend } from '../types/custom';
@ -29,8 +29,9 @@ export interface IPluginMiddleware<T> extends IPlugin<T> {
const debug = buildDebug('verdaccio:server');
const defineAPI = function (config: IConfig, storage: Storage): any {
const defineAPI = async function (config: IConfig, storage: Storage): Promise<any> {
const auth: Auth = new Auth(config);
await auth.init();
const app: Application = express();
const limiter = new RateLimit(config.serverSettings.rateLimit);
// run in production mode by default, just in case
@ -62,27 +63,21 @@ const defineAPI = function (config: IConfig, storage: Storage): any {
hookDebug(app, config.configPath);
}
// register middleware plugins
const plugin_params = {
config: config,
logger: logger,
};
const plugins: IPluginMiddleware<IConfig>[] = loadPlugin(
config,
const plugins: IPluginMiddleware<IConfig>[] = await asyncLoadPlugin(
config.middlewares,
plugin_params,
{
config,
logger,
},
function (plugin: IPluginMiddleware<IConfig>) {
return plugin.register_middlewares;
}
);
if (_.isEmpty(plugins)) {
if (plugins.length === 0) {
logger.info('none middleware plugins has been defined, adding audit middleware by default');
plugins.push(
new AuditMiddleware(
{ ...config, enabled: true, strict_ssl: true },
{ config, logger: logger }
)
new AuditMiddleware({ ...config, enabled: true, strict_ssl: true }, { config, logger })
);
}
@ -96,7 +91,7 @@ const defineAPI = function (config: IConfig, storage: Storage): any {
// For WebUI & WebUI API
if (_.get(config, 'web.enable', true)) {
app.use(webMiddleware(config, auth, storage));
app.use(await webMiddleware(config, auth, storage));
} else {
app.get('/', function (req: $RequestExtend, res: $ResponseExtend, next: $NextFunctionVer) {
next(errorUtils.getNotFound(API_ERROR.WEB_DISABLED));
@ -135,31 +130,21 @@ const defineAPI = function (config: IConfig, storage: Storage): any {
return app;
};
export default (async function (configHash: ConfigYaml): Promise<any> {
export default (async function startServer(configHash: ConfigYaml): Promise<any> {
debug('start server');
const config: IConfig = new AppConfig(_.cloneDeep(configHash) as any);
const config: IConfig = new AppConfig({ ...configHash } as any);
// register middleware plugins
const plugin_params = {
config: config,
logger,
};
const filters = loadPlugin(
config,
config.filters || {},
plugin_params,
(plugin: IPluginStorageFilter<IConfig>) => plugin.filter_metadata
);
debug('loaded filter plugin');
// @ts-ignore
const storage: Storage = new Storage(config);
try {
// waits until init calls have been initialized
debug('storage init start');
await storage.init(config, filters);
await storage.init(config);
debug('storage init end');
} catch (err: any) {
logger.error({ error: err.msg }, 'storage has failed: @{error}');
throw new Error(err);
}
return defineAPI(config, storage);
return await defineAPI(config, storage);
});

View file

@ -8,6 +8,7 @@ export default fp(
async function (fastify: FastifyInstance, opts: { config: IConfig; filters?: unknown }) {
const { config } = opts;
const auth: IAuth = new Auth(config);
await auth.init();
fastify.decorate('auth', auth);
},
{

View file

@ -3,7 +3,7 @@ import buildDebug from 'debug';
import _ from 'lodash';
import { errorUtils, pluginUtils } from '@verdaccio/core';
import { loadPlugin } from '@verdaccio/loaders';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import LocalDatabase from '@verdaccio/local-storage';
import { Config, Logger } from '@verdaccio/types';
@ -31,7 +31,7 @@ class LocalStorage {
public async init() {
if (this.storagePlugin === null) {
this.storagePlugin = this._loadStorage(this.config, this.logger);
this.storagePlugin = await this.loadStorage(this.config, this.logger);
debug('storage plugin init');
await this.storagePlugin.init();
debug('storage plugin initialized');
@ -55,31 +55,35 @@ class LocalStorage {
return this.storagePlugin.setSecret(config.checkSecretKey(secretKey));
}
private _loadStorage(config: Config, logger: Logger): IPluginStorage {
const Storage = this._loadStorePlugin();
private async loadStorage(config: Config, logger: Logger): Promise<IPluginStorage> {
const Storage = await this.loadStorePlugin();
if (_.isNil(Storage)) {
assert(this.config.storage, 'CONFIG: storage path not defined');
debug('no custom storage found, loading default storage @verdaccio/local-storage');
return new LocalDatabase(config, logger);
}
return Storage as IPluginStorage;
}
private _loadStorePlugin(): IPluginStorage | void {
const plugin_params = {
config: this.config,
logger: this.logger,
};
const plugins: IPluginStorage[] = loadPlugin<IPluginStorage>(
this.config,
private async loadStorePlugin(): Promise<IPluginStorage | undefined> {
const plugins: IPluginStorage[] = await asyncLoadPlugin<IPluginStorage>(
this.config.store,
plugin_params,
{
config: this.config,
logger: this.logger,
},
(plugin): IPluginStorage => {
return plugin.getPackageStorage;
}
},
this.config?.server?.pluginPrefix
);
if (plugins.length > 1) {
this.logger.warn(
'more than one storage plugins has been detected, multiple storage are not supported, one will be selected automatically'
);
}
return _.head(plugins);
}
}

View file

@ -19,6 +19,7 @@ import {
searchUtils,
validatioUtils,
} from '@verdaccio/core';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { IProxy, ISyncUplinksOptions, ProxySearchParams, ProxyStorage } from '@verdaccio/proxy';
import {
@ -33,6 +34,7 @@ import {
DistFile,
GenericBody,
IPackageStorage,
IPluginStorageFilter,
Logger,
Manifest,
MergeTags,
@ -80,7 +82,7 @@ export const PROTO_NAME = '__proto__';
class Storage {
public localStorage: LocalStorage;
public filters: IPluginFilters;
public filters: IPluginFilters | null;
public readonly config: Config;
public readonly logger: Logger;
public readonly uplinks: ProxyInstanceList;
@ -88,7 +90,7 @@ class Storage {
this.config = config;
this.uplinks = setupUpLinks(config);
this.logger = logger.child({ module: 'storage' });
this.filters = [];
this.filters = null;
// @ts-ignore
this.localStorage = null;
debug('uplinks available %o', Object.keys(this.uplinks));
@ -655,10 +657,8 @@ class Storage {
* @param filters IPluginFilters
* @returns Storage instance
*/
public async init(config: Config, filters: IPluginFilters = []): Promise<void> {
public async init(config: Config): Promise<void> {
if (this.localStorage === null) {
this.filters = filters || [];
debug('filters available %o', filters);
this.localStorage = new LocalStorage(this.config, logger);
await this.localStorage.init();
debug('local init storage initialized');
@ -667,6 +667,20 @@ class Storage {
} else {
debug('storage has been already initialized');
}
if (!this.filters) {
this.filters = await asyncLoadPlugin<IPluginStorageFilter<any>>(
this.config.filters,
{
config: this.config,
logger: this.logger,
},
(plugin) => {
return plugin.filterMetadata;
},
this.config?.server?.pluginPrefix
);
debug('filters available %o', this.filters);
}
return;
}
@ -1728,7 +1742,7 @@ class Storage {
* @returns
*/
public async applyFilters(manifest: Manifest): Promise<[Manifest, any]> {
if (this.filters.length === 0) {
if (this.filters === null || this.filters.length === 0) {
return [manifest, []];
}
@ -1739,7 +1753,7 @@ class Storage {
// and return it directly for
// performance (i.e. need not be pure)
try {
filteredManifest = await filter.filter_metadata(manifest);
filteredManifest = await filter.filterMetadata(manifest);
} catch (err: any) {
this.logger.error({ err: err.message }, 'filter has failed @{err}');
filterPluginErrors.push(err);

View file

@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
lines: 50,
lines: 48,
},
},
});

View file

@ -27,14 +27,19 @@ export async function initializeServer(
const storage = new Storage(config);
await storage.init(config, []);
const auth: IAuth = new Auth(config);
await auth.init();
// 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
routesMiddleware.map((route: any) => {
app.use(route(config, auth, storage));
});
for (let route of routesMiddleware) {
if (route.async) {
const middleware = await route.routes(config, auth, storage);
app.use(middleware);
} else {
app.use(route(config, auth, storage));
}
}
// catch 404
app.get('/*', function (req, res, next) {

View file

@ -0,0 +1,7 @@
function ValidVerdaccioPlugin() {
return {
authenticate: function () {},
};
}
module.exports = ValidVerdaccioPlugin;

View file

@ -0,0 +1,13 @@
{
"name": "customprefix-auth",
"version": "0.0.1",
"private": true,
"description": "fake plugin for test",
"author": "Juan Picado <juanpicado19@gmail.com>",
"license": "MIT",
"homepage": "https://verdaccio.org",
"main": "index.js",
"scripts": {
"build": "echo 0"
}
}

View file

@ -16,7 +16,7 @@ class ExampleAuthPlugin implements IPluginAuth<{}> {
config: AppConfig;
logger: Logger;
constructor(config: AppConfig, options: PluginOptions<{}>) {
constructor(config: AppConfig, options: PluginOptions) {
this.config = config;
this.logger = options.logger;
}
@ -50,7 +50,7 @@ class ExampleAuthCustomPlugin implements IPluginAuth<{}> {
config: AppConfig;
logger: Logger;
constructor(config: AppConfig, options: PluginOptions<{}>) {
constructor(config: AppConfig, options: PluginOptions) {
this.config = config;
this.logger = options.logger;
}
@ -81,7 +81,7 @@ const config1: AppConfig = new Config({
config_path: '/home/sotrage',
});
const options: PluginOptions<{}> = {
const options: PluginOptions = {
config: config1,
logger: logger.child(),
};

View file

@ -4,7 +4,8 @@ import _ from 'lodash';
import path from 'path';
import { HTTP_STATUS } from '@verdaccio/core';
import { loadPlugin } from '@verdaccio/loaders';
import { asyncLoadPlugin } from '@verdaccio/loaders';
import { logger } from '@verdaccio/logger';
import { isURLhasValidProtocol } from '@verdaccio/url';
import renderHTML from '../renderHTML';
@ -12,19 +13,24 @@ import { setSecurityWebHeaders } from './security';
const debug = buildDebug('verdaccio:web:render');
export function loadTheme(config) {
export async function loadTheme(config: any) {
if (_.isNil(config.theme) === false) {
return _.head(
loadPlugin(
config,
config.theme,
{},
function (plugin) {
return _.isString(plugin);
},
'verdaccio-theme'
)
const plugin = await asyncLoadPlugin(
config.theme,
// @ts-ignore
{ config, logger },
function (plugin: string) {
return typeof plugin === 'string';
},
config?.server?.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);
}
}
@ -39,8 +45,9 @@ const sendFileCallback = (next) => (err) => {
}
};
export function renderWebMiddleware(config, auth): any {
const { staticPath, manifest, manifestFiles } = require('@verdaccio/ui-theme')();
export async function renderWebMiddleware(config, auth): Promise<any> {
const { staticPath, manifest, manifestFiles } =
(await loadTheme(config)) || require('@verdaccio/ui-theme')();
debug('static path %o', staticPath);
/* eslint new-cap:off */

View file

@ -3,11 +3,11 @@ import express from 'express';
import { renderWebMiddleware } from './middleware/render-web';
import { webAPI } from './middleware/web-api';
export default (config, auth, storage) => {
export default async (config, auth, storage) => {
// eslint-disable-next-line new-cap
const app = express.Router();
// load application
app.use('/', renderWebMiddleware(config, auth));
app.use('/', await renderWebMiddleware(config, auth));
// web endpoints, search, packages, etc
app.use('/-/verdaccio/', webAPI(config, auth, storage));
return app;

View file

@ -18,5 +18,9 @@ export const getConf = (configName: string) => {
// @deprecated
export async function initializeServer(configName): Promise<Application> {
return initializeServerHelper(getConf(configName), [apiMiddleware, routes], Storage);
return initializeServerHelper(
getConf(configName),
[apiMiddleware, { async: true, routes }],
Storage
);
}

View file

@ -426,20 +426,26 @@ importers:
packages/loaders:
specifiers:
'@verdaccio-scope/verdaccio-auth-foo': 0.0.2
'@verdaccio/config': workspace:6.0.0-6-next.47
'@verdaccio/core': workspace:6.0.0-6-next.47
'@verdaccio/logger': workspace:6.0.0-6-next.15
'@verdaccio/types': workspace:11.0.0-6-next.16
customprefix-auth: 0.0.1
debug: 4.3.4
lodash: 4.17.21
verdaccio-auth-memory: workspace:*
dependencies:
'@verdaccio/logger': link:../logger
debug: 4.3.4
lodash: 4.17.21
devDependencies:
'@verdaccio-scope/verdaccio-auth-foo': 0.0.2
'@verdaccio/config': link:../config
'@verdaccio/core': link:../core/core
'@verdaccio/types': link:../core/types
customprefix-auth: link:../tools/verdaccio-prefix-fake-plugin
verdaccio-auth-memory: link:../plugins/auth-memory
packages/logger:
specifiers:
@ -1092,6 +1098,9 @@ importers:
ts-node: 10.9.1_03bba433ca420ee50bf31a7f62506788
verdaccio: link:../../verdaccio
packages/tools/verdaccio-prefix-fake-plugin:
specifiers: {}
packages/types:
specifiers:
lunr-mutable-indexes: 2.3.2
@ -9340,6 +9349,21 @@ packages:
'@typescript-eslint/types': 5.37.0
eslint-visitor-keys: 3.3.0
/@verdaccio-scope/verdaccio-auth-foo/0.0.2:
resolution: {integrity: sha512-BqeDqLcYcm3CRYlrQnAueKg8vabBtqwgZ4jRLZJtig+JWzSX50sKdWrzbhf6waQT/HjRO+ADUs/2gjl1tdOTMQ==}
engines: {node: '>=12'}
dependencies:
'@verdaccio/commons-api': 10.2.0
dev: true
/@verdaccio/commons-api/10.2.0:
resolution: {integrity: sha512-F/YZANu4DmpcEV0jronzI7v2fGVWkQ5Mwi+bVmV+ACJ+EzR0c9Jbhtbe5QyLUuzR97t8R5E/Xe53O0cc2LukdQ==}
engines: {node: '>=8'}
dependencies:
http-errors: 2.0.0
http-status-codes: 2.2.0
dev: true
/@vue/compiler-core/3.0.11:
resolution: {integrity: sha512-6sFj6TBac1y2cWCvYCA8YzHJEbsVkX7zdRs/3yK/n1ilvRqcn983XvpBbnN3v4mZ1UiQycTvOiajJmOgN9EVgw==}
dependencies:
@ -15119,7 +15143,6 @@ packages:
/http-status-codes/2.2.0:
resolution: {integrity: sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==}
dev: false
/http2-wrapper/1.0.3:
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}