Middleware Plugin
What's a Middleware Plugin?
Middleware plugins have the capability to modify the API (web and cli) layer, either adding new endpoints or intercepting requests.
API
The interface lives in @verdaccio/core, under the pluginUtils namespace:
import { pluginUtils } from '@verdaccio/core';
interface ExpressMiddleware<PluginConfig, Storage, Auth> extends Plugin<PluginConfig> {
register_middlewares(app: Express, auth: Auth, storage: Storage): void;
}
The type parameters are declared in the order <PluginConfig, Storage, Auth>, but
register_middlewares receives auth before storage. The compiler will not catch
the two being swapped when both are any.
Storage and Auth are the types of the instances Verdaccio injects. A plugin that only
needs auth can declare {} for Storage, as the generator template does.
register_middlewares
app is the Express application, so the method can mount any router or middleware on it.
auth and storage are the live instances and can be extended, though we don't recommend
it unless well founded.
import { pluginUtils } from '@verdaccio/core';
import express, { type Express } from 'express';
/**
* Whatever your plugin accepts under its own key in `middlewares:` of `config.yaml`.
*
* ```yaml
* middlewares:
* audit:
* enabled: true # keep it: defining this block replaces the default list
* my-endpoint:
* greeting: hola
* ```
*/
type CustomConfig = { greeting?: string };
/**
* A middleware plugin mounts routes on the Express application Verdaccio built.
*
* Two things about the signature that the compiler will not warn you about:
*
* - The type parameters are `<PluginConfig, Storage, Auth>`, but `register_middlewares`
* receives **`auth` before `storage`**. Declare `unknown` for the ones you do not use.
* - `this.config` is typed `PluginConfig | unknown` on the base class, hence the cast below.
*
* @see https://verdaccio.org/dev/plugin-middleware
*/
export default class CustomEndpoint
extends pluginUtils.Plugin<CustomConfig>
implements pluginUtils.ExpressMiddleware<CustomConfig, unknown, pluginUtils.IBasicAuth>
{
/**
* Called once at startup, in the order the plugins appear in `middlewares:`.
*
* Your routes are registered **before** the registry API, so a matching route answers and
* the built-in handler never runs — mount on a prefix unless overriding is the point.
*
* By the time a request reaches this router, Verdaccio has already:
*
* - parsed the JSON body, so `req.body` is an object and reading the raw stream fails
* (7.x; on 6.x there is no parser and `req.body` is `undefined`);
* - run the JWT middleware, so `req.remote_user` is set — except under `/-/verdaccio/`,
* the web UI namespace, which is skipped on purpose.
*
* @param app the Express application; mount routers on it
* @param _auth the live auth instance, for permission checks
*/
public register_middlewares(app: Express, _auth: pluginUtils.IBasicAuth): void {
const router = express.Router();
router.get('/hello', (req, res) => {
this.options.logger.info({ url: req.url }, 'custom-endpoint: incoming request');
res.json({ hello: (this.config as CustomConfig).greeting ?? 'world' });
});
// Prefer a prefix. A bare `app.use(router)` sees every request to the registry.
app.use('/-/npm/v2/my-endpoint', router);
}
}
This is the same shape the plugin generator scaffolds, so running it is the quickest way to get a compiling starting point.
The built-in
verdaccio-auditis a real middleware plugin and the shortest one to read.
Where your middleware runs
Verdaccio builds one Express application and registers everything in this order
(packages/server/express/src/server.ts). Since Express runs middleware in registration
order, this is also the order a request travels:
Four consequences are worth spelling out, because they are the ones people get wrong.
You run before the built-in API, so you can intercept it
Your routes are registered at step 7 and the registry API at step 8. If your route matches,
you answer and the built-in handler never runs. Mounting PUT /:package in a plugin
overrides publishing.
That is easy to do by accident: a broad app.use() with no path prefix sees every request.
Mount on a prefix — app.use('/-/npm/v2/my-endpoint', router) — unless overriding is what
you want.
req.body is already parsed
The parser at step 4 is express.json({ strict: false, limit: config.max_body_size }), so:
req.bodyis a parsed object by the time your plugin runs, and reading the raw stream yourself fails withstream is not readable.max_body_sizefromconfig.yamlis your limit too (default10mb). A plugin accepting larger payloads needs that raised.- Registering
express.json()in your own plugin is harmless — it sees the body already parsed and skips — but it cannot replace Verdaccio's, which always runs first.
On 6.x this holds since 6.10.4 (#6249),
which moved the parser ahead of the plugins there too. On earlier 6.x releases req.body is
undefined and the plugin has to parse the stream itself.
req.remote_user is populated, except under /-/verdaccio/
The JWT middleware at step 6 exists so plugins can read the authenticated user. On 7.x it is
deliberately skipped for the web UI namespace (/-/verdaccio/), which has its own token
handling — a plugin mounted under that prefix will not see req.remote_user. On 6.x the same
middleware is mounted globally, so it runs for that namespace as well.
Defining middlewares: replaces the default list
verdaccio-audit, which backs npm audit, is an ordinary middleware plugin declared in the
default configuration. If you replace the middlewares: block with only your own plugin,
audit is gone. Keep it listed:
middlewares:
audit:
enabled: true
my-plugin:
option: value
On 7.x, audit is loaded automatically only when no middleware plugin is configured at all.
Errors
Call next(err) and let the error handler at step 10 format the response; it understands
the errors from errorUtils in @verdaccio/core. Throwing asynchronously outside a request
handler takes the process down, as it would in any Express app.
Express 5 route syntax
7.x runs Express 5 (path-to-regexp 8); 6.x runs Express 4.
Route patterns that were valid in Express 4 now throw, and the throw happens while your
plugin is registering — which is during startup.
Nothing catches it. register_middlewares is called in a plain loop, so a plugin with an
old-style route stops Verdaccio from booting. Unlike a theme plugin, which falls back to
the default, a middleware plugin cannot fail quietly here.
Measured against Express 5.2.1:
| Pattern | Express 4 | Express 5 |
|---|---|---|
/foo/* | ok | ✗ Missing parameter name at index 6 |
/foo/:id? | ok | ✗ Unexpected ? at index 8, expected end |
/foo/:id(\d+) | ok | ✗ Unexpected ( at index 8, expected end |
/foo/{*rest} | — | ok, the replacement for * |
/foo/*rest | — | ok, a named wildcard |
/foo{/:id} | — | ok, the replacement for :id? |
So: wildcards must be named, optional segments move into braces, and inline regular
expressions are gone — a pattern like /:id(\d+) has to become a plain parameter plus a
check inside the handler.
Verdaccio's own routes are the shortest reference for the new spelling: /-/static/{*all},
/-/user/token/{*subject}, /{*any}.
The Express 5 migration guide covers the
rest, including res.sendFile, which the 7.x release notes also flag for plugin
authors.
Overwriting HTTP Security Headers {#overwrite-http-security-headers]
By default, Verdaccio sets the following HTTP headers. If you have other security requirements, you can overwrite these settings using a middleware plugin (6.2.5 or newer).
| Header | Verdaccio Setting |
|---|---|
| Content-Security-Policy | connect-src 'self' |
| X-Content-Type-Options | nosniff |
| X-Frame-Options | deny |
| X-XSS-Protection | 1; mode=block |
Generate a middleware plugin
Run yo verdaccio-plugin and pick middleware when asked for the plugin type; the
plugin generator page covers installation and the full prompt list.
The scaffold it produces is the example shown above, already compiling against the current
@verdaccio/core.
List Community Middleware Plugins
-
verdaccio-audit: verdaccio plugin for npm audit cli support (built-in) (compatible since 3.x)
-
verdaccio-profile-api: verdaccio plugin for npm profile cli support and npm profile set password for verdaccio-htpasswd based authentificaton
-
verdaccio-https Verdaccio middleware plugin to redirect to https if x-forwarded-proto header is set
-
verdaccio-badges A verdaccio plugin to provide a version badge generator endpoint
-
verdaccio-openmetrics Verdaccio plugin exposing an OpenMetrics/Prometheus endpoint with health and traffic metrics
-
verdaccio-sentry sentry loggin errors
-
verdaccio-pacman Verdaccio Middleware Plugin to manage tags and versions of packages