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:
parent
98afd48378
commit
9fc2e79611
67 changed files with 807 additions and 525 deletions
|
@ -14,6 +14,5 @@
|
|||
],
|
||||
"access": "public",
|
||||
"baseBranch": "master",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
"updateInternalDependencies": "patch"
|
||||
}
|
||||
|
|
76
.changeset/early-jokes-nail.md
Normal file
76
.changeset/early-jokes-nail.md
Normal 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}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -22,6 +22,9 @@ jest.mock('@verdaccio/auth', () => ({
|
|||
apiJWTmiddleware() {
|
||||
return mockApiJWTmiddleware();
|
||||
}
|
||||
init() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
allow_access(_d, f_, cb) {
|
||||
cb(null, true);
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -35,6 +35,9 @@ jest.mock('@verdaccio/auth', () => ({
|
|||
apiJWTmiddleware() {
|
||||
return mockApiJWTmiddleware();
|
||||
}
|
||||
init() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
allow_access(_d, f_, cb) {
|
||||
cb(null, true);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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': {},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "verdaccio-fail",
|
||||
"main": "fail.js",
|
||||
"version": "1.0.0"
|
||||
}
|
11
packages/auth/test/partials/plugin/verdaccio-fail/fail.js
Normal file
11
packages/auth/test/partials/plugin/verdaccio-fail/fail.js
Normal 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);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "verdaccio-fail",
|
||||
"main": "fail.js",
|
||||
"version": "1.0.0"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "verdaccio-passthroug",
|
||||
"main": "passthroug.js",
|
||||
"version": "1.0.0"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "verdaccio-success",
|
||||
"main": "success.js",
|
||||
"version": "1.0.0"
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from './plugin-loader';
|
||||
export { asyncLoadPlugin } from './plugin-async-loader';
|
||||
|
|
132
packages/loaders/src/plugin-async-loader.ts
Normal file
132
packages/loaders/src/plugin-async-loader.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
40
packages/loaders/src/utils.ts
Normal file
40
packages/loaders/src/utils.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
auth:
|
||||
'@verdaccio-scope/verdaccio-auth-foo':
|
||||
enabled: true
|
|
@ -0,0 +1,3 @@
|
|||
auth:
|
||||
auth-memory:
|
||||
enabled: true
|
|
@ -0,0 +1,3 @@
|
|||
auth:
|
||||
not-found:
|
||||
enabled: true
|
|
@ -0,0 +1,4 @@
|
|||
auth:
|
||||
something:
|
||||
enabled: true
|
||||
plugins: /roo/does-not-exist
|
|
@ -0,0 +1,4 @@
|
|||
plugins: '../test-plugin-storage'
|
||||
auth:
|
||||
plugin:
|
||||
enabled: true
|
3
packages/loaders/test/partials/config/scope-auth.yaml
Normal file
3
packages/loaders/test/partials/config/scope-auth.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
auth:
|
||||
'@verdaccio-scope/verdaccio-auth-foo':
|
||||
enabled: true
|
3
packages/loaders/test/partials/config/valid-plugin.yaml
Normal file
3
packages/loaders/test/partials/config/valid-plugin.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
auth:
|
||||
plugin:
|
||||
enabled: true
|
|
@ -0,0 +1,7 @@
|
|||
function ValidScopedVerdaccioPlugin() {
|
||||
return {
|
||||
authenticate: function () {},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ValidVerdaccioPlugin;
|
|
@ -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"
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
168
packages/loaders/test/plugin_loader_async.spec.ts
Normal file
168
packages/loaders/test/plugin_loader_async.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -20,7 +20,7 @@ jest.mock('../src/fs', () => ({
|
|||
setup();
|
||||
|
||||
// @ts-expect-error
|
||||
const optionsPlugin: PluginOptions<{}> = {
|
||||
const optionsPlugin: PluginOptions = {
|
||||
logger,
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -4,7 +4,7 @@ module.exports = Object.assign({}, config, {
|
|||
coverageThreshold: {
|
||||
global: {
|
||||
// FIXME: increase to 90
|
||||
lines: 50,
|
||||
lines: 48,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
7
packages/tools/verdaccio-prefix-fake-plugin/index.js
Normal file
7
packages/tools/verdaccio-prefix-fake-plugin/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
function ValidVerdaccioPlugin() {
|
||||
return {
|
||||
authenticate: function () {},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = ValidVerdaccioPlugin;
|
13
packages/tools/verdaccio-prefix-fake-plugin/package.json
Normal file
13
packages/tools/verdaccio-prefix-fake-plugin/package.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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==}
|
||||
|
|
Loading…
Reference in a new issue