Mashroom Logo

Mashroom Server

Version: 2.5.3

https://www.mashroom-server.com

(c) nonblocking.at gmbh

Table of contents

About

Mashroom Server is a Node.js based Microfrontend Integration Platform. It supports the integration of Express webapps on the server side and composing pages from multiple Single Page Applications on the client side (Browser). It also provides common infrastructure such as security, communication (publish/subscribe), theming, i18n, storage, and logging out of the box and supports custom middleware and services via plugins.

Mashroom Server allows it to implemented SPAs (and express webapps) completely independent and without a vendor lock-in, and to use it on arbitrary pages with different configurations and even multiple times on the same page. It also allows it to restrict the access to resources (Pages, Apps) based on user roles.

From a technical point of view the core of Mashroom Server is a plugin loader that scans npm packages for plugin definitions (package.json, mashroom.json) and loads them at runtime. Such a plugin could be an Express webapp or a SPA or more generally all kind of code it knows how to load, which is determined by the available plugin loaders. Plugin loaders itself are also just plugins, so it is possible to add any type of custom plugin type.

Key features

Portal

Core

Feature/Compatibility Matrix

Supported
Operating Systems Linux, MacOS, Windows
Node.js 18.x, 20.x
HTTP 1.0, 1.1, 2 + TLS 1.1, 1.2, 1.3
Authentication LDAP (Active Directory), OpenID Connect/OAuth2, local user database (JSON file)
Authorization Role based; ACL (URL and HTTP method, based on roles and/or IP address); Resource permissions (Page, App instance, Topic, …)
Security CSRF protection, Helmet integration
Storage MongoDB, Plain JSON Files
Memory Cache Local Memory, Redis
Messaging MQTT (3.1, 3.1.1/4.0, 5.0), AMQP (1.0)
Session Storage Local Memory (no Cluster support), shared Filesystem, Redis, MongoDB
API Proxy HTTP, HTTPS, SSE, WebSocket
CDN Any that can be configured as caching proxy
Clustering yes (tested with PM2)
Monitoring CPU, Heap, Requests + Plugin Metrics; Exporter for Prometheus
Desktop Browsers Chrome (latest), Firefox (latest), Safari (latest), Edge (latest)
Mobile Browsers Chrome (latest), Safari (latest)

Architecture

Mashroom Server Architecture

Key concepts

Plugin definition

A plugin definition consists of two parts:

  1. A plugin definition element, either in package.json or a separate mashroom.json file
  2. A loader script (bootstrap)

A package.json with a Mashroom Server plugin definition looks like this:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-packagejson-extension.json",
    "name": "my-webapp",
    "version": "1.0.0",
    "dependencies": {
        "express": "4.16.4"
    },
    "devDependencies": {
    },
    "scripts": {
        "build": "babel src -d dist"
    },
    "mashroom": {
        "devModeBuildScript": "build",
        "plugins": [
            {
                "name": "My Webapp",
                "type": "web-app",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "requires": [
                    "A special service plugin"
                ],
                "defaultConfig": {
                    "path": "/my/webapp"
                }
            }
        ]
    }
}

The same in a separate mashroom.json

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-plugins.json",
    "devModeBuildScript": "build",
    "plugins": [
        {
            "name": "My Webapp",
            "type": "web-app",
            "bootstrap": "./dist/mashroom-bootstrap.js",
            "requires": [
                "A special service plugin"
            ],
            "defaultConfig": {
                "path": "/my/webapp"
            }
        }
    ]
}

Multiple plugins can be defined within a single npm package.

The type element determines which plugin loader will be used to load the plugin. The optional requires defines plugins that must be loaded before this plugin can be loaded. The content defaultConfig differs per plugin type. It can be overwritten in the plugins section of the server configuration.

The devModeBuildScript property is optional. If present npm run <devModeBuildScript> is executed in devMode after every change in the package.

The bootstrap script for this case might look like this:

import webapp from './my-express-webapp';

import {MashroomWebAppPluginBootstrapFunction} from 'mashroom/type-definitions';

const bootstrap: MashroomWebAppPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
    return webapp;
};

export default bootstrap;

The context element allows access to the server configuration, the logger factory and all services.

Plugin context

The plugin context allows access to the logger factory und all services. The plugin context is available via:

Examples:

import type {MashroomLogger} from '@mashroom/mashroom/type-definitions';
import type {MashroomStorageService} from '@mashroom/mashroom-storage/type-definitions';

const bootstrap: MashroomWebAppPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {

    const pluginContext = pluginContextHolder.getPluginContext();
    const logger: MashroomLogger = pluginContext.loggerFactory('my.log.category');
    const storageService: MashroomStorageService = pluginContext.services.storage.service;

        //...
};
app.get('/', (req, res) => {

    const pluginContext = req.pluginContext;
    const logger: MashroomLogger = pluginContext.loggerFactory('my.log.category');
    const storageService: MashroomStorageService = pluginContext.services.storage.service;

    //...
});

NOTE: Never store the pluginContext outside a bootstrap or request handler because service references my change over time when plugins are reloaded. But it save to store the pluginContextHolder instance.

Setup

Minimum Requirements

Install

Just checkout the mashroom-portal-quickstart repo for a typical portal setup. Or mashroom-quickstart if you don't need the Portal plugin.

A single package.json is enough to set up a server instance. Plugins are just npm dependencies.

Configuration

The configuration files are expected in the folder where mashroom is executed. Alternatively you can pass the root folder as argument:

mashroom <path_to_config_files>

The following config files are loaded and merged together if present (in this order):

The typical configuration could look like this:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-server-config.json",
    "name": "Mashroom Test Server 1",
    "port": 8080,
    "indexPage": "/portal",
    "xPowerByHeader": "Mashroom Server",
    "tmpFolder": "/tmp",
    "pluginPackageFolders": [{
        "path": "./node_modules/@mashroom"
     }, {
        "path": "./my-plugin-packages",
        "watch": true,
        "devMode": true
    }],
    "ignorePlugins": [],
    "plugins": {
        "Mashroom Session Middleware": {
            "provider": "Mashroom Session Filestore Provider",
            "session": {
            }
        },
        "Mashroom Session Filestore Provider": {
            "path": "./data/sessions",
            "ttl": 1200
        },
        "Mashroom Security Services": {
            "provider": "Mashroom Security Simple Provider",
            "acl": "./acl.json"
        },
        "Mashroom Security Simple Provider": {
            "users": "./users.json",
            "loginPage": "/login"
        },
        "Mashroom Storage Services": {
            "provider": "Mashroom Storage Filestore Provider"
        },
        "Mashroom Storage Filestore Provider": {
            "dataFolder": "./data/storage"
        },
        "Mashroom Internationalization Services": {
            "availableLanguages": ["en", "de"],
            "defaultLanguage": "en"
        },
        "Mashroom Http Proxy Services": {
            "rejectUnauthorized": false,
            "poolMaxSockets": 10
        }
    }
}

The same as a Javascript file:

module.exports = {
    name: "Mashroom Test Server 1",
    port: 8080,
    indexPage: "/portal",
    xPowerByHeader: "Mashroom Server",
    tmpFolder: "/tmp",
    pluginPackageFolders: [{
         path: "./node_modules/@mashroom"
     }, {
        path: "./my-plugin-packages",
        devMode: true
    }],
    ignorePlugins: [],
    plugins: {

    }
}

Since version 1.3 the property values can also contain string templates and the environment variables are accessible via env object:

{
    "name": "${env.USER}'s Mashroom Server",
    "port": 5050
}

To enable HTTPS and/or HTTP2 you would add:

{
    "httpsPort": 5443,
    "tlsOptions": {
        "key": "./certs/key.pem",
        "cert": "./certs/cert.pem"
    },
    "enableHttp2": true
}

Properties

Security

To enable security you have to add the mashroom-security package and a provider package such as mashroom-security-provider-simple.

The security package provides access control lists based on URLs and a Service for managing and checking resource permissions manually.

NOTE: The security in the Portal and for Portal Apps (SPA) is described below in Mashroom Portal -> Security.

ACL

You can secure every URL in Mashroom with the ACL, based on user role or IP. For example:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-security-acl.json",
    "/portal/**": {
        "*": {
            "allow": {
                "roles": ["Authenticated"]
            }
        }
    },
    "/mashroom/**": {
        "*": {
            "allow": {
                "roles": ["Administrator"],
                "ips": ["127.0.0.1", "::1"]
            }
        }
    }
}

NOTE: For more details check the mashroom-security documentation below.

Resource permissions

The SecurityService allows it to define and check resource permissions based on a permission key and the user roles. For example:

import type {MashroomSecurityService} from '@mashroom/mashroom-security/type-definitions';

export default async (req: Request, res: Response) => {
    const securityService: MashroomSecurityService = req.pluginContext.services.security.service;

    // Create a permission
    await securityService.updateResourcePermission(req, {
        type: 'Page',
        key: pageId,
        permissions: [{
            permissions: ['View'],
            roles: ['Role1', 'Role2']
        }]
    });

    // Check a permission
    const mayAccess = await securityService.checkResourcePermission(req, 'Page', pageId, 'View');

    // ...
}

This mechanism is used by the Portal for Site, Page and Portal App permissions.

NOTE: For more details check the mashroom-security documentation below.

HTTP Security Headers

Use the mashroom-helmet plugin to add the Helmet middleware, which adds a bunch of HTTP headers to prevent XSS and other attacks.

CSRF

The mashroom-csrf plugin adds middleware that checks every POST, PUT and DELETE request for a CSRF token in the HTTP headers or the query. You need to use the MashroomCSRFService to get the current token.

NOTE: The default Portal theme automatically adds the current CSRF token in a meta tag with the name csrf-token.

Logging

The logger is currently backed by log4js.

The configuration is expected to be in the same folder as mashroom.cfg. Possible config files are:

The first config file found from this list will be used. A file logger would be configured like this:

    {
        "appenders": {
            "file1": {"type": "file", "filename": "log/mashroom.log", "maxLogSize": 10485760, "numBackups": 3},
            "file2": {
              "type": "file", "filename": "log/my-stuff.log", "maxLogSize": 10485760, "numBackups": 3,
              "layout":  {
                "type": "pattern",
                "pattern": "%d %p %X{sessionID} %X{clientIP} %X{username} %c - %m"
              }
            },
            "console": {"type": "console"}
        },
        "categories": {
            "default": {"appenders": ["file1", "console"], "level": "debug"},
            "my-stuff": {"appenders": ["file2"], "level": "info"}
        }
    }

The following built in context properties can be used with %X{} or a custom layout:

You can use logger.withContext() or logger.addContext() to add context information.

For configuration details and possible appenders see log4js-node Homepage.

Logstash

To push the logs to logstash you can use the logstash-http package:

    "dependencies": {
      "@log4js-node/logstash-http": "^1.0.0"
    }

And configure log4js like this:

{
  "appenders": {
    "logstash": {
      "type": "@log4js-node/logstash-http",
      "url": "http://elasticsearch:9200/_bulk",
      "application": "your-index"
    }
  },
  "categories": {
    "default": {
      "appenders": [ "logstash" ],
      "level": "info"
    }
  }
}

Internationalization

The mashroom-i18n plugin provides a simple service to lookup messages on the file system based on the current user language.

You can use it like this from where ever the pluginContext is available:

import type {MashroomI18NService} from '@mashroom/mashroom-i18n/type-definitions';

export default (req: Request, res: Response) => {
    const i18nService: MashroomI18NService = req.pluginContext.services.i18n.service;

    const currentLang = i18nService.getLanguage(req);
    const message =  i18nService.getMessage('username', 'de');
    // message will be 'Benutzernamen'

    // ...
}

One the client-side (in Portal Apps) you can use an arbitrary i18n framework (such as intl-messageformat). The current Portal locale will be passed to the Apps with the portalAppSetup:

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup, clientServices) => {
    const { lang } = portalAppSetup;
    // lang will be 'en' or 'fr' or whatever
    const { messageBus, portalAppService } = clientServices;
    // ...
};

Caching

Mashroom tries to automatically use the most efficient caching mechanisms. All you need to do is to add the appropriate plugins.

Server-side

Add mashroom-memory-cache plugin and optionally mashroom-memory-cache-provider-redis if you want to use Redis instead of the local memory. Many other plugins (such as mashroom-storage and mashroom-portal) will automatically detect it and use it (see their documentation for more details).

Browser

The mashroom-browser-cache plugin provides a service to set Cache-Control headers based on a policy. For example:

import type {MashroomCacheControlService} from '@mashroom/mashroom-browser-cache/type-definitions';

export default async (req: Request, res: Response) => {

    const cacheControlService: MashroomCacheControlService = req.pluginContext.services.browserCache.cacheControl;
    await cacheControlService.addCacheControlHeader('ONLY_FOR_ANONYMOUS_USERS', req, res);

    // ..
};

Other plugins (such as mashroom-portal) will automatically use it if present.

NOTE: This plugin will always set no-cache header in devMode.

CDN

Since v2 Mashroom ships a CDN plugin that will automatically be used by mashroom-portal and other plugins to deliver static assets.

Basically, mashroom-cdn consists of a list of CDN hosts and a service to get the next host to use:

import type {MashroomCDNService} from '@mashroom/mashroom-cdn/type-definitions';

export default async (req: Request, res: Response) => {

    const cdnService: MashroomCDNService = req.pluginContext.services.cdn.service;

    const cdnHost = cdnService.getCDNHost();
    const resourceUrl = `${cdnHost}/<the-actual-path>`;

    // ..
};

NOTE: The mashroom-cdn plugin requires a CDN that works like a transparent proxy, which forwards an identical request to the origin (in this case Mashroom) if does not exist yet.

Virtual Host Path Mapping

The mashroom-vhost-path-mapper plugin can be used to map frontend paths to internal paths, based on virtual hosts.

So, lets say you want to map the Mashroom Portal site site1, reachable under http://localhost:5050/portal/site1, to www.my-company.com. In that case you would configure the plugin like this:

{
  "plugins": {
       "Mashroom VHost Path Mapper Middleware": {
           "hosts": {
              "www.my-company.com": {
                 "mapping": {
                    "/login": "/login",
                    "/": "/portal/site1"
                 }
              }
          }
       }
    }
}

If your frontend base path is different, e.g. www.my-company.com/foo, you would also have to set the frontendBasePath in the configuration.

NOTE: All other plugins will only deal with the rewritten paths, keep that in mind especially when defining ACLs.

Clustering

If you're going to run Mashroom Server in a cluster you should keep in mind:

A cluster safe log configuration could look like this:

const NODE_ID = process.env.pm_id || process.pid;

module.exports = {
    appenders: {
        file: {'type': 'file', 'filename': `log/mashroom.${NODE_ID}.log`, 'maxLogSize': 10485760, 'numBackups': 3},
        console: {'type': 'console'}
    },
    categories: {
        default: {appenders: ['file', 'console'], 'level': 'info'}
    },
    disableClustering: true
};

Monitoring

Mashroom Server gathers a lot of internal metrics that can be exposed in different formats. Currently, there are two exporters available:

To enable the metrics, just add @mashroom/mashroom-monitoring-metrics-collector and one of the exporter plugins.

The Prometheus metrics will be available at /metrics. An example Grafana Dashboard can be found in the Mashroom repo.

Here how it looks:

Mashroom Monitoring Dashboard

Health checks

There are a few integrated health checks available that can be used as probes for monitoring tools or to check the readiness/liveness of Kubernetes pods.

An overview of the health checks is available under http://<host>:<port>/mashroom/health

Admin UI

The Mashroom Server Admin UI is available under http://<host>:<port>/mashroom/admin

Mashroom Admin UI

It contains:

Mashroom Portal

Architecture

Mashroom Portal Architecture

How does it work?

Every Portal App (SPA) has to expose a global bootstrap method. The Portal Client fetches the app metadata from the server, loads all required resources and calls then the bootstrap with the configuration object and the DOM element where the app should run in.

HTTP/Websocket Proxy

The built-in proxy allows the Portal App to access internal APIs. It supports HTTP, Websocket and SSE.

You have to define proxies like this in the plugin definition:

"defaultConfig": {
  "proxies": {
    "myApi": {
      "targetUri": "http://localhost:1234/api/v1"
    }
  }
}

To be able to use it in the boostrap of your App like this:

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup) => {
    const {lang, restProxyPaths} = portalAppSetup;

    fetch(`${restProxyPaths.myApi}/foo/bar?q=xxx`, {
        credentials: 'same-origin',
    }).then( /* ... */ );

    // ...
};

Proxy Interceptors

Plugins of type http-proxy-interceptor can be used to intercept all proxy calls and to check, enrich or manipulate requests or responses.

A typical use case is to add some security headers (e.g. Bearer) or do some redirect or caching.

Checkout out the mashroom-http-proxy documentation for details.

Remote Apps

Portal Apps (SPA) can reside on a remote server and automatically be registered. Currently, there are two different remote registries that can also be combined:

Here, how it works:

Remote app resources

Ad hoc register a remote app

Here for example with mashroom-portal-remote-app-registry.

Open /mashroom/admin/ext/remote-portal-apps, paste the URL into the input and lick Add:

Register Remote Portal App

After that you can add the new Portal App via Drag'n'Drop where ever you want:

Register remote portal app

Server Side Rendering

Since v2 the Portal can render the whole Portal page on the server-side and even include the initial HTML of Portal Apps, capable of server-side rendering.

NOTE: If an App support SSR it should provide its style in separate CSS file, because in that case the Portal will try to inline all styles so everything gets rendered correctly without any extra resources.

To enable SSR in an App you have to:

The ssrBootstrap could look like this in React:

import React from 'react';
import {renderToString} from 'react-dom/server';
import App from './App';

import type {MashroomPortalAppPluginSSRBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalAppPluginSSRBootstrapFunction = (portalAppSetup) => {
    const {appConfig, restProxyPaths, lang} = portalAppSetup;
    const dummyMessageBus: any = {};
    return renderToString(<App appConfig={appConfig} messageBus={dummyMessageBus}/>);
};

export default bootstrap;

NOTE: The server-side bootstrap will receive the same portalAppSetup like on the client-side, but no client services. If you need the messageBus or other services make sure the App renders without them or with a mock implementation.

On the client-side you would do:

import React from 'react';
import {render, hydrate, unmountComponentAtNode} from 'react-dom';
import App from './App';

import type {MashroomPortalAppPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (element, portalAppSetup, clientServices) => {
    const {appConfig, restProxyPaths, lang} = portalAppSetup;
    const {messageBus} = clientServices;

    const ssrHost = element.querySelector('[data-ssr-host="true"]');
    if (ssrHost) {
        hydrate(<App appConfig={appConfig} messageBus={messageBus}/>, ssrHost);
    } else {
        render(<App appConfig={appConfig} messageBus={messageBus}/>, element);
    }
};

global.startMyApp = bootstrap;

Remote Apps would basically work the same, but the server-side bootstrap needs to be exposed as route that receives a POST with the portalAppSetup in the content. Checkout the Mashroom Demo SSR Portal App.

All put together rendering a page works like this:

SSR Rendering

NOTE: Every App should support client-side rendering, because they can be added dynamically to a page. If you don't want to render on the client (e.g. for security reasons) at least render an error message on the client-side.

Performance/SEO hints

SSR improves page performance and SEO heavily. To improve it further we recommend:

SSR Performance

"SPA Mode"

The Portal supports dynamically replacing the content of a page (that's the layout + all Apps) by the content of another page. This can be used to avoid full page loads and let the Portal behave like an SPA itself, so it basically can switch to client-side rendering.

The client-side rendering needs to be implemented in the Theme, which needs to the following during page navigation:

<!-- in the template -->
<a class="nav-link active"
   href="{{@root.siteBasePath}}{{friendlyUrl}}"
   onclick="return replacePageContent('{{pageId}}', '{{@root.siteBasePath}}{{friendlyUrl}}')"
   data-mr-page-id="{{pageId}}">
    {{title}}
</a>
import type {MashroomPortalClientServices, MashroomPortalPageContent} from '@mashroom/mashroom-portal/type-definitions';

const clientServices: MashroomPortalClientServices | undefined = (global as any).MashroomPortalServices;
if (!clientServices) {
    return;
}

(global as any).replacePageContent = (pageId: string, pageUrl: string): boolean => {
    showPageLoadingIndicator(true);
    clientServices.portalPageService.getPageContent(pageId).then(
        (content: MashroomPortalPageContent) => {
            if (content.fullPageLoadRequired || !content.pageContent) {
                // Full page load required!
                document.location.replace(pageUrl);
            } else {
                contentEl.innerHTML = content.pageContent;
                // Execute page scripts
                eval(content.evalScript);
                highlightPageIdInNavigation(pageId);
                window.history.pushState({ pageId }, '', pageUrl);
                showPageLoadingIndicator(false);
            }
        },
        (error) => {
            // If an error occurs we do a full page load
            console.error('Dynamically replacing the page content failed!', error);
            document.location.replace(pageUrl);
        }
    );
    return false;
}

NOTE: The Portal will automatically detect if the requested page is not compatible to the initial loaded one (because the Theme oder Page Enhancements differ). In that case it will return fullPageLoadRequired: true.

Composite Apps

A Composite App is a higher order App, which uses other Portal Apps (SPAs) as building blocks. So, its App in App, or SPA in SPA, or picture in picture ;-) Such a Composite App can itself be used as building block for another Composite App, which can be continued infinite.

This approach takes advantage of Mashroom Portal's ability to load any registered App into any DOM element. Basically, you just render a div element in your SPA with a unique (random) ID and load the App like so:

// Get the portalAppService in the bootstrap
const bootstrap: MashroomPortalAppPluginBootstrapFunction = (element, portalAppSetup, clientServices) => {
    const {portalAppService} = clientServices;
    //...
}

// Load the App
const loadedApp = await portalAppService.loadApp(domElID, 'My App', null, /* position */ null, /* appConfig */ {
    someProp: 'foo',
});

And unload it like this:

await portalAppService.unloadApp(loadedApp.id);

Make sure you call unloadApp() if you want to remove/replace an App and not just remove the host DOM node, because like that the resources are not properly removed from the browser.

Here for example the mashroom-portal-demo-composite-app:

Mashroom Portal Composite App

NOTE: A Composite App only runs in Mashroom Portal and leads to a vendor lock-in. At very least other integration hosts need to provide a compatible implementation of MashroomPortalAppService.

Dynamic Cockpits

One key feature of Mashroom Portal is the possibility to create dynamic pages (or cockpits) based:

This is quite similar to a Composite App but in this case the page loads a single (static) App, which takes over the page and manages loading and unloading other Apps outside of itself. Typically, the names of the Apps that get loaded are not pre-defined, but determined from some metaInfo of the available Apps. So, such a cockpit can be dynamically extended by just adding new Apps with some capabilities (at runtime).

To find Apps you would use the portalAppService like this:

const availableApps = await portalAppService.getAvailableApps();
const appToShowWhatever = availableApps.filter(({metaInfo}) => metaInfo?.canShow === 'whatever');

Have a look at this Demo Dynamic Cockpit, it consist of a central search bar and tries to load found data with Apps with apropriate metaInfo. E.g. this App could load customers:

{
    "plugins": [
        {
            "name": "Mashroom Dynamic Cockpit Demo Customer Details App",
            // ...
            "defaultConfig": {
                "metaInfo": {
                    "demoCockpit": {
                        "viewType": "Details",
                        "entity": "Customer"
                    }
                },
                // ...
            }
        }
    ]
}

Theming

A Mashroom Portal Theme can be written with any template engine that works with Express.

You need to implement three templates:

NOTE: If you want to have a type-safe template we recommend using React. Have a look at mashroom-portal-demo-alternative-theme for an example.

Themable Portal Apps

Obviously, theming only works properly if all the Apps on a page regard the currently applied theme and in particular do not:

Here some best practices:

Messaging

The Portal comes with a client-side MessageBus that can be used by Portal Apps to communicate with each other locally.

If server-side messaging (mashroom-messaging) and Websocket support (mashroom-websocket) is installed, the MessageBus is automatically connected to the server-side messaging facility and like this Portal Apps can communicate with Apps in other browsers and even with 3rd party systems (when a external messaging system such as MQTT is connected).

Messaging

Page Enhancements

Page enhancement plugins allow to add some extra script or style to a page. Either to any page or based on some rules (e.g. page URL or the user agent).

Checkout the mashroom-portal documentation for details.

Portal App Enhancements

Portal app enhancement plugins can be used to modify the portalAppSetup of some (or any) Apps before loading. It can also be used to add custom services on the client side (passed with clientServices to every App).

A typical use case would be to add some extra data to the user or to augment the appConfig.

Checkout the mashroom-portal documentation for details.

Security

The Mashroom Portal uses the security plugin to control the access to pages and Portal Apps. It also introduced a concept of fine grain permissions (mapped to roles) which can be checked in Portal Apps and in backends (passed via HTTP header by the API Proxy).

Portal App Security

If a user requires specific roles to be able to load an App (dynamically) you have to set the defaultRestrictViewToRoles in the plugin definition. Otherwise, any user, which is able to access the Portal API (which can be controlled via ACL), will be able to load it.

NOTE: Users with the role Administrator are able to load all Apps, regardless of defaultRestrictViewToRoles. They are also able to override/redefine the view permission when adding Apps to a page.

Within Portal Apps you should not work with roles but with abstract permission keys such as "mayDeleteCustomer". In the plugin definition this keys can then be mapped to roles like this:

"defaultConfig": {
  "rolePermissions": {
    "mayDeleteCustomer": ["Role1", "Role2"]
  }
}

And the permission can be checked like this:

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup, clientServices) => {
    const {appConfig, user: {permissions, username, displayName, email}} = portalAppSetup;

    // True if user has role "Role1" OR "Role2"
    if (permissions.mayDeleteCustomer) {
        // ...
    }
}

Securing backend access

Proxy access can be restricted by adding a restrictToRole property in the plugin definition:

"defaultConfig": {
  "proxies": {
    "myApi": {
      "targetUri": "http://localhost:1234/api/v1",
      "sendPermissionsHeader": false,
      "restrictToRoles": ["Role1"]
    }
  }
}

NOTE: Not even users with the Administrator role can override that restriction. So, even if they can load a restricted App they will not be able to access the backend.

Furthermore, it is possible to pass the Portal App security context to the backend via HTTP headers. This is useful if you want to check some fine grain permissions there as well.

Headers that can be passed to the Backend:

Site and Page Security

Sites and Pages can be secured by:

Both approaches can be combined.

User Interface

Page layout

Mashroom Portal Layout

The theme is responsible for rendering the page. It defines where the main content is. The portal adds the selected layout to the main content and the configured Portal Apps within the app areas of this layout.

Default start page with demo apps

Mashroom Portal

Add a new Portal App

As an Administrator you can add Portal Apps via Admin Toolbar: Add Apps

Mashroom Portal Add App

Portal App configuration

After adding an App you can click on the Configure icon to edit the appConfig and the permissions:

Mashroom Portal App Settings

Custom App Config Editor

Instead of the default JSON editor you can define a custom editor App for you appConfig. The custom editor is itself a plain Portal App (SPA) which gets an extra appConfig property editorTarget of type MashroomPortalConfigEditorTarget that can be used to communicate back with the Admin Toolbar:

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (portalAppHostElement, portalAppSetup, clientServices) => {
    const {appConfig: {editorTarget}} = portalAppSetup;

    if (!editorTarget || !editorTarget.pluginName) {
        throw new Error('This app can only be started as App Config Editor!');
    }

    const currentAppConfig = editorTarget.appConfig;

    // ...

    // When the user is done:
    editorTarget.updateAppConfig(updatedAppConfig);
    editorTarget.close();

    //...
};

Here for example the mashroom-portal-demo-react-app2 plugin which has a custom editor:

Mashroom Portal Custom App Config Editor

Show Portal App versions

The default portal theme allows to show all Portal App versions by clicking on App versions. You can enable it like this in the Mashroom config file:

{
    "plugins": {
        "Mashroom Portal Default Theme": {
          "showEnvAndVersions": true
        }
    }
}

Mashroom Portal show versions

Adding a new page

As an Administrator you can add a new Page from the Admin Toolbar: Create -> Create New Page:

Mashroom Portal Page Settings

After that you can start to place Portal Apps via Add Apps.

Adding a new site

As an Administrator you can add a new Site from the Admin Toolbar: Create -> Create New Page:

Mashroom Portal Site Settings

After that you can start to add additional pages.

Core Documentation

Services

MashroomPluginService

Accessible through pluginContext.services.core.pluginService

Interface:

export interface MashroomPluginService {
    /**
     * The currently known plugin loaders
     */
    getPluginLoaders(): Readonly<MashroomPluginLoaderMap>;

    /**
     * Get all currently known plugins
     */
    getPlugins(): Readonly<Array<MashroomPlugin>>;

    /**
     * Get all currently known plugin packages
     */
    getPluginPackages(): Readonly<Array<MashroomPluginPackage>>;

    /**
     * Register for the next loaded event of given plugin (fired AFTER the plugin has been loaded).
     */
    onLoadedOnce(pluginName: string, listener: () => void): void;

    /**
     * Register for the next unload event of given plugin (fired BEFORE the plugin is going to be unloaded).
     */
    onUnloadOnce(pluginName: string, listener: () => void): void;
}

MashroomMiddlewareStackService

Accessible through pluginContext.services.core.middlewareStackService

Interface:

export interface MashroomMiddlewareStackService {
    /**
     * Check if the stack has given plugin
     */
    has(pluginName: string): boolean;

    /**
     * Execute the given middleware.
     * Throws an exception if it doesn't exists
     */
    apply(
        pluginName: string,
        req: Request,
        res: Response,
    ): Promise<void>;

    /**
     * Get the ordered list of middleware plugin (first in the list is executed first)
     */
    getStack(): Array<{pluginName: string; order: number}>;
}

MashroomHttpUpgradeService

Accessible through pluginContext.services.core.websocketUpgradeService

Interface:

/**
 * A services to add and remove HTTP/1 upgrade listeners
 */
export interface MashroomHttpUpgradeService {
    /**
     * Register an upgrade handler for given path
     */
    registerUpgradeHandler(handler: MashroomHttpUpgradeHandler, pathExpression: string | RegExp): void;
    /**
     * Unregister an upgrade handler
     */
    unregisterUpgradeHandler(handler: MashroomHttpUpgradeHandler): void;
}

MashroomHealthProbeService

A service that allows it plugins to register health probes. If a probe fails the server state goes to unready.

/**
 * A services to obtain all available health probes
 */
export interface MashroomHealthProbeService {
    /**
     * Register a new health probe for given plugin
     */
    registerProbe(forPlugin: string, probe: MashroomHealthProbe): void;
    /**
     * Unregister a health probe for given plugin
     */
    unregisterProbe(forPlugin: string): void;
    /**
     * Get all registered probes
     */
    getProbes(): Readonly<Array<MashroomHealthProbe>>;
}

You can use it like this in your plugin bootstrap:

const bootstrap: MashroomStoragePluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
    const {services: {core: {pluginService, healthProbeService}}} = pluginContextHolder.getPluginContext();

    healthProbeService.registerProbe(pluginName, healthProbe);

    pluginService.onUnloadOnce(pluginName, () => {
        healthProbeService.unregisterProbe(pluginName);
    });

    // ...
};

Plugin Types

plugin-loader

A plugin-loader plugin adds support for a custom plugin type.

To register a new plugin-loader add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Custom Plugin Loader",
                "type": "plugin-loader",
                "bootstrap": "./dist/mashroom-bootstrap",
                "loads": "my-custom-type",
                "defaultConfig": {
                   "myProperty": "foo"
                }
            }
        ]
    }
}

After that all plugins of type my-custom-type will be passed to your custom loader instantiated by the bootstrap script:

import type {
    MashroomPluginLoader, MashroomPlugin, MashroomPluginConfig, MashroomPluginContext,
    MashroomPluginLoaderPluginBootstrapFunction
} from 'mashroom/type-definitions';

class MyPluginLoader implements MashroomPluginLoader {

    get name(): string {
        return 'My Plugin Loader';
    }

    generateMinimumConfig(plugin: MashroomPlugin) {
        return {};
    }

    async load(plugin: MashroomPlugin, config: MashroomPluginConfig, context: MashroomPluginContext) {
        // TODO
    }

    async unload(plugin: MashroomPlugin) {
        // TODO
    }
}

const myPluginLoaderPlugin: MashroomPluginLoaderPluginBootstrapFunction = (pluginName, pluginConfig, pluginContextHolder) => {
    return new MyPluginLoader();
};

export default myPluginLoaderPlugin;

web-app

Registers a Express webapp that will be available at a given path.

To register a web-app plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
            {
                "name": "My Webapp",
                "type": "web-app",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                    "path": "/my/webapp",
                    "myProperty": "foo"
                }
            }
        ]
     }
}

And the bootstrap just returns the Express webapp:

import webapp from './webapp';

import type {MashroomWebAppPluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';

const bootstrap: MashroomWebAppPluginBootstrapFunction = async () => {
    return webapp;
};

export default bootstrap;

Additional handlers

It is also possible to return handlers in the bootstrap. Currently there is only one:

Example:

const bootstrap: MashroomWebAppPluginBootstrapFunction = async () => {
    return {
        expressApp: webapp,
        upgradeHandler: (request: IncomingMessageWithContext, socket: Socket, head: Buffer) => {
            // TODO
        },
    };
};

api

Registers a Express Router (a REST API) and makes it available at a given path.

To register a API plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
            {
                "name": "My REST API",
                "type": "api",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                    "path": "/my/api",
                    "myProperty": "foo"
                }
            }
        ]
    }
}

And the bootstrap just returns the Express router:

const express = require('express');
const router = express.Router();

router.get('/', (req, res) => {
  // ...
});

import type {MashroomApiPluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';

const bootstrap: MashroomApiPluginBootstrapFunction = async () => {
    return router;
};

export default bootstrap;

middleware

Registers a Express middleware and adds it to the global middleware stack.

To register a middleware plugin add this to package.json:

{
    "mashroom": {
        "plugins": [{
            "name": "My Middleware",
            "type": "middleware",
            "bootstrap": "./dist/mashroom-bootstrap.js",
            "defaultConfig": {
                "order": 500,
                "myProperty": "foo"
            }
        }]
    }
}

And the bootstrap just returns the Express middleware:

import MyMiddleware from './MyMiddleware';

import type {MashroomMiddlewarePluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';

const bootstrap: MashroomMiddlewarePluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
    const pluginContext = pluginContextHolder.getPluginContext();
    const middleware = new MyMiddleware(pluginConfig.myProperty, pluginContext.loggerFactory);
    return middleware.middleware();
};

export default bootstrap;

static

Registers some static resources and exposes it at a given path (via Express static).

To register a static plugin add this to package.json:

{
    "mashroom": {
        "plugins": [{
            "name": "My Documents",
            "type": "static",
            "documentRoot": "./my-documents",
            "defaultConfig": {
                "path": "/my/docs"
            }
        }]
    }
}

services

Used to load arbitrary shared code that can be loaded via pluginContext.

To register a service plugin add this to package.json:

{
    "mashroom": {
        "plugins": [{
            "name": "My Services",
            "type": "services",
            "namespace": "myNamespace",
            "bootstrap": "./dist/mashroom-bootstrap.js",
            "defaultConfig": {
            }
        }]
    }
}

The bootstrap will just return an object with a bunch of services:

import MyService from './MyService';

import type {MashroomServicesPluginBootstrapFunction} from '@mashroom/mashroom/type-definitions';

const bootstrap: MashroomServicesPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
    const pluginContext = pluginContextHolder.getPluginContext();
    const service = new MyService(pluginContext.loggerFactory);

    return {
        service,
    };
};

export default bootstrap;

admin-ui-integration

A simple plugin to register an arbitrary web-app or static plugin as panel in the Admin UI.

To register an admin-ui-integration plugin add this to package.json:

{
    "mashroom": {
        "plugins": [{
            "name": "My Admin Panel Integration",
            "type": "admin-ui-integration",
            "requires": [
                "My Admin Panel"
            ],
            "target": "My Admin Panel",
            "defaultConfig": {
                "menuTitle": "My Admin Panel",
                "path": "/my-admin-panel",
                "height": "80vh",
                "weight": 10000
            }
        }]
    }
}
    parent.postMessage({ height: contentHeight + 20 }, "*");

Plugin Documentation

Mashroom Security

This plugin adds role based security to the Mashroom Server.

It comes with the following mechanisms:

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Security Services": {
            "provider": "Mashroom Security Simple Provider",
            "forwardQueryHintsToProvider": [],
            "acl": "./acl.json"
        }
    }
}
ACL

A typical ACL configuration looks like this:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-security-acl.json",
    "/portal/**": {
        "*": {
            "allow": {
                "roles": ["Authenticated"]
            }
        }
    },
    "/mashroom/**": {
        "*": {
            "allow": {
                "roles": ["Administrator"],
                "ips": ["127.0.0.1", "::1"]
            }
        }
    }
}

The general structure is:

    "/my/path/**": {
        "*|GET|POST|PUT|DELETE|PATCH|OPTIONS": {
            "allow": "any"|<array of roles>|<object with optional properties roles and ips>
            "deny": "any"|<array of roles>|<object with optional properties roles and ips>
        }
    }

Example: Allow all users except the ones that come from an IP address starting with 12:

{
    "/my-app/**": {
        "*": {
            "allow": {
                "roles": ["Authenticated"]
            },
            "deny": {
                "ips": ["12.**"]
            }
        }
    }
}

Example: Restrict the Portal to authenticated users but make a specific site public:

{
    "/portal/public-site/**": {
        "*": {
            "allow": "any"
        }
    },
    "/portal/**": {
        "*": {
            "allow": {
                "roles": ["Authenticated"]
            }
        }
    }
}
Security Service

Adding and checking a resource permission (e.g. for a Page) works like this:

import type {MashroomSecurityService} from '@mashroom/mashroom-security/type-definitions';

export default async (req: Request, res: Response) => {
    const securityService: MashroomSecurityService = req.pluginContext.services.security.service;

    // Create a permission
    await securityService.updateResourcePermission(req, {
        type: 'Page',
        key: pageId,
        permissions: [{
            permissions: ['View'],
            roles: ['Role1', 'Role2']
        }]
    });

    // Check a permission
    const mayAccess = await securityService.checkResourcePermission(req, 'Page', pageId, 'View');

    // ...
}

Services

MashroomSecurityService

The exposed service is accessible through pluginContext.services.security.service

Interface:

export interface MashroomSecurityService {
    /**
     * Get the current user or null if the user is not authenticated
     */
    getUser(request: Request): MashroomSecurityUser | null | undefined;

    /**
     * Checks if user != null
     */
    isAuthenticated(request: Request): boolean;

    /**
     * Check if the currently authenticated user has given role
     */
    isInRole(request: Request, roleName: string): boolean;

    /**
     * Check if the currently authenticated user is an admin (has the role Administrator)
     */
    isAdmin(request: Request): boolean;

    /**
     * Check the request against the ACL
     */
    checkACL(request: Request): Promise<boolean>;

    /**
     * Check if given abstract "resource" is permitted for currently authenticated user.
     * The permission has to be defined with updateResourcePermission() first, otherwise the allowIfNoResourceDefinitionFound flag defines the outcome.
     */
    checkResourcePermission(request: Request, resourceType: MashroomSecurityResourceType, resourceKey: string, permission: MashroomSecurityPermission, allowIfNoResourceDefinitionFound?: boolean): Promise<boolean>;

    /**
     * Set a resource permission for given abstract resource.
     * A resource could be: {type: 'Page', key: 'home', permissions: [{ roles: ['User'], permissions: ['VIEW'] }]}
     *
     * If you pass a permission with an empty roles array it actually gets removed from the storage.
     */
    updateResourcePermission(request: Request, resource: MashroomSecurityProtectedResource): Promise<void>;

    /**
     * Get the permission definition for given resource, if any.
     */
    getResourcePermissions(request: Request, resourceType: MashroomSecurityResourceType, resourceKey: string): Promise<MashroomSecurityProtectedResource | null | undefined>;

    /**
     * Add a role definition
     */
    addRoleDefinition(request: Request, roleDefinition: MashroomSecurityRoleDefinition): Promise<void>;

    /**
     * Get all known roles. Returns all roles added with addRoleDefinition() or implicitly added bei updateResourcePermission().
     */
    getExistingRoles(request: Request): Promise<Array<MashroomSecurityRoleDefinition>>;

    /**
     * Check if an auto login would be possible.
     */
    canAuthenticateWithoutUserInteraction(request: Request): Promise<boolean>;

    /**
     * Start authentication process
     */
    authenticate(request: Request, response: Response): Promise<MashroomSecurityAuthenticationResult>;

    /**
     * Check the existing authentication (if any)
     */
    checkAuthentication(request: Request): Promise<void>;

    /**
     * Get the authentication expiration time in unix time ms
     */
    getAuthenticationExpiration(request: Request): number | null | undefined;

    /**
     * Revoke the current authentication
     */
    revokeAuthentication(request: Request): Promise<void>;

    /**
     * Login user with given credentials (for form login).
     */
    login(request: Request, username: string, password: string): Promise<MashroomSecurityLoginResult>;

    /**
     * Find a security provider by name.
     * Useful if you want to dispatch the authentication to a different provider.
     */
    getSecurityProvider(name: string): MashroomSecurityProvider | null | undefined;
}

Plugin Types

security-provider

This plugin type is responsible for the actual authentication and for creating a user object with a list of roles.

To register your custom security-provider plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Custom Security Provider",
                "type": "security-provider",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                   "myProperty": "foo"
                }
            }
        ]
    }
}

The bootstrap returns the provider:

import type {MashroomSecurityProviderPluginBootstrapFunction} from '@mashroom/mashroom-security/type-definitions';

const bootstrap: MashroomSecurityProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {

    return new MySecurityProvider(/* ... */);
};

export default bootstrap;

The provider has to implement the following interface:

export interface MashroomSecurityProvider {
    /**
     * Check if an auto login would be possible.
     * This is used for public pages when an authentication is optional but nevertheless desirable.
     * It is safe to always return false.
     */
    canAuthenticateWithoutUserInteraction(request: Request): Promise<boolean>;
    /**
     * Start authentication process.
     * This typically means to redirect to the login page, then you should return status: 'deferred'.
     * This method could also automatically login the user, then you should return status: 'authenticated'.
     */
    authenticate(request: Request, response: Response, authenticationHints?: any): Promise<MashroomSecurityAuthenticationResult>;

    /**
     * Check the existing authentication (if any).
     * Use this to extend the authentication expiration or to periodically refresh the access token.
     *
     * This method gets called for almost every request, so do nothing expensive here.
     */
    checkAuthentication(request: Request): Promise<void>;

    /**
     * Get the authentication expiration time in unix time ms. Return null/undefined if there is no authentication.
     */
    getAuthenticationExpiration(request: Request): number | null | undefined;

    /**
     * Revoke the current authentication.
     * That typically means to remove the user object from the session.
     */
    revokeAuthentication(request: Request): Promise<void>;

    /**
     * Programmatically login user with given credentials (optional, but necessary if you use the default login page)
     */
    login(request: Request, username: string, password: string): Promise<MashroomSecurityLoginResult>;

    /**
     * Get the current user or null if the user is not authenticated
     */
    getUser(request: Request): MashroomSecurityUser | null | undefined;
}

Mashroom Security Simple Provider

This plugin adds a simple, JSON file based security provider.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-simple as dependency.

To activate this provider configure the Mashroom Security plugin like this:

{
    "plugins": {
        "Mashroom Security Services": {
            "provider": "Mashroom Security Simple Provider"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Security Simple Provider": {
            "users": "./users.json",
            "loginPage": "/login",
            "authenticationTimeoutSec": 1200
        }
    }
}
{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-security-simple-provider-users.json",
    "users": [
        {
            "username": "admin",
            "displayName": "Administrator",
            "email": "xxxxx@xxxx.com",
            "pictureUrl": "xxxx",
            "passwordHash": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918",
            "extraData": {
                "firstName": "John",
                "lastName": "Do"
            },
            "roles": [
                "Administrator"
            ],
            "secrets": {
                "token": "xxxxxxx"
            }
        },
        {
            "username": "john",
            "displayName": "John Do",
            "passwordHash": "96d9632f363564cc3032521409cf22a852f2032eec099ed5967c0d000cec607a",
            "roles": [
                "User",
                "PowerUser"
            ]
        }
    ]
}

The passwordHash is the SHA256 hash of the password. displayName, email and pictureUrl are optional. extraData will be mapped to user.extraData and secrets will be mapped to user.secrets.

Mashroom LDAP Security Provider

This plugin adds a LDAP security provider.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-ldap as dependency.

To activate this provider, configure the Mashroom Security plugin like this:

{
    "plugins": {
        "Mashroom Security Services": {
            "provider": "Mashroom LDAP Security Provider"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
    "plugins": {
        "Mashroom LDAP Security Provider": {
            "loginPage": "/login",
            "serverUrl": "ldap://my-ldap-server:636",
            "ldapConnectTimeout": 3000,
            "ldapTimeout": 5000,
            "bindDN": "uid=mashroom,dc=nonblocking,dc=at",
            "bindCredentials": "secret",
            "baseDN": "ou=users,dc=nonblocking,dc=at",
            "userSearchFilter": "(&(objectClass=person)(uid=@username@))",
            "groupSearchFilter": "(objectClass=group)",
            "extraDataMapping": {
                "mobile": "mobile",
                "address": "postalAddress"
            },
            "secretsMapping": {
                "internalUserId": "uid"
            },
            "groupToRoleMapping": "./groupToRoleMapping.json",
            "userToRoleMapping": "./userToRoleMapping.json",
            "authenticationTimeoutSec": 1200
        }
    }
}

For a server that requires TLS you have to provide a tlsOptions object:

{
    "plugins": {
        "Mashroom LDAP Security Provider": {
            "serverUrl": "ldaps://my-ldap-server:636",
            "tlsOptions": {
              "cert": "./server-cert.pem",

              // Necessary only if the server requires client certificate authentication.
              //"key": "./client-key.pem",

              // Necessary only if the server uses a self-signed certificate.
              // "rejectUnauthorized": false,
              // "ca": [ "./server-cert.pem" ],
            }
        }
    }
}

The groupToRoleMapping file has to following simple structure:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-security-ldap-provider-group-to-role-mapping.json",
    "LDAP_GROUP1": [
        "ROLE1",
        "ROLE2"
    ]
}

And the userToRoleMapping file:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-security-ldap-provider-user-to-role-mapping.json",
    "username": [
        "ROLE1",
        "ROLE2"
    ]
}

Mashroom OpenID Connect Security Provider

This plugin adds an OpenID Connect/OAuth2 security provider that can be used to integrate Mashroom Server with almost all Identity Providers or Identity Platforms.

Tested with:

Should work with (among others):

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-ldap as dependency.

To activate this provider, configure the Mashroom Security plugin like this:

{
    "plugins": {
        "Mashroom Security Services": {
            "provider": "Mashroom OpenID Connect Security Provider"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
    "plugins": {
        "Mashroom OpenID Connect Security Provider": {
            "mode": "OIDC",
            "issuerDiscoveryUrl": "http://localhost:8080/.well-known/openid-configuration",
            "issuerMetadata": null,
            "scope": "openid email profile",
            "clientId": "mashroom",
            "clientSecret": "your-client-secret",
            "redirectUrl": "http://localhost:5050/openid-connect-cb",
            "responseType": "code",
            "usePKCE": false,
            "extraAuthParams": {},
            "extraDataMapping": {
                "phone": "phone",
                "birthdate": "birthdate",
                "updatedAt": "updated_at"
            },
            "rolesClaimName": "roles",
            "adminRoles": [
                "mashroom-admin"
            ],
            "httpRequestTimeoutMs": 3500
        },
        "Mashroom OpenID Connect Security Provider Callback": {
           "path": "/openid-connect-cb"
        }
    }
}
Roles

Since the authorization mechanism relies on user roles it is necessary to configure your identity provider to map the user roles to a scope (which means we can get it as claim). See Example Configurations below.

Secrets

The plugin maps the ID/JWT Token to user.secrets.idToken so it can for example be used in a Http Proxy Interceptor to set the Bearer for backend calls.

Authentication Expiration

The implementation automatically extends the authentication via refresh token every view seconds (as long as the user is active). So, if the authentication session gets revoked in the identity provider the user is signed out almost immediately.

The expiration time of the access token defines after which time the user is automatically signed out due to inactivity. And the expiration time of the refresh token defines how long the user can work without signing in again.

Example Configurations

Keycloak

Setup:

You'll find more details about the configuration here: https://www.keycloak.org/documentation.html

If your Keycloak runs on localhost, the Realm name is test amd the client name mashroom, then the config would look like this:

{
    "plugins": {
        "Mashroom OpenID Connect Security Provider": {
            "issuerDiscoveryUrl": "http://localhost:8080/auth/realms/test/.well-known/uma2-configuration",
            "clientId": "mashroom",
            "clientSecret": "xxxxxxxxxxxxxxxx",
            "redirectUrl": "http://localhost:5050/openid-connect-cb",
            "rolesClaim": "roles",
            "adminRoles": [
                "mashroom-admin"
            ]
        }
    }
}
OpenAM

Setup:

You'll find more details about the configuration here: https://backstage.forgerock.com/docs/openam/13.5/admin-guide

If your OpenAM server runs on localhost, the Realm name is Test amd the client name mashroom, then the config would look like this:

{
    "plugins": {
        "Mashroom OpenID Connect Security Provider": {
            "issuerDiscoveryUrl": "http://localhost:8080/openam/oauth2/Test/.well-known/openid-configuration",
            "scope": "openid email profile",
            "clientId": "mashroom",
            "clientSecret": "mashroom",
            "redirectUrl": "http://localhost:5050/openid-connect-cb",
            "rolesClaim": "roles",
            "adminRoles": [
                "mashroom-admin"
            ]
        }
    }
}
Google Identity Platform

Setup:

Possible config:

{
    "plugins": {
        "Mashroom OpenID Connect Security Provider": {
            "issuerDiscoveryUrl": "https://accounts.google.com/.well-known/openid-configuration",
            "scope": "openid email profile",
            "clientId": "xxxxxxxxxxxxxxxx.apps.googleusercontent.com",
            "clientSecret": "xxxxxxxxxxxxxxxx",
            "redirectUrl": "http://localhost:5050/openid-connect-cb",
            "extraAuthParams": {
                "access_type": "offline"
            },
            "usePKCE": true
        }
    }
}

The access_type=offline parameter is necessary to get a refresh token.

Since Google users don't have authorization roles there is no way to make some users Administrator.

GitHub OAuth2

Setup:

Possible config:

{
    "plugins": {
        "Mashroom OpenID Connect Security Provider": {
            "mode": "OAuth2",
            "issuerMetadata": {
                "issuer": "GitHub",
                "authorization_endpoint": "https://github.com/login/oauth/authorize",
                "token_endpoint": "https://github.com/login/oauth/access_token",
                "userinfo_endpoint": "https://api.github.com/user",
                "end_session_endpoint": null
            },
            "scope": "openid email profile",
            "clientId": "xxxxxxxxxxxxxxxx",
            "clientSecret": "xxxxxxxxxxxxxxxx",
            "redirectUrl": "http://localhost:5050/openid-connect-cb"
        }
    }
}

Since GitHub uses pure OAuth2 the users don't have permission roles and there is no way to make some users Administrator. It also supports no userinfo endpoint, so it actually makes not much sense to use it with Mashroom.

Mashroom Basic Authentication Wrapper Security Provider

This plugin adds support for Basic authentication to any other security provider that implements login() properly. This can be useful when you need to access some APIs on the server from an external system or for test purposes.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-provider-basic-wrapper as dependency.

To activate this provider configure the Mashroom Security plugin like this:

{
    "plugins": {
        "Mashroom Security Services": {
            "provider": "Mashroom Basic Wrapper Security Provider"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Basic Wrapper Security Provider": {
             "targetSecurityProvider": "Mashroom Security Simple Provider",
             "onlyPreemptive": true,
             "realm": "mashroom"
        }
    }
}

Mashroom Security Default Login Webapp

This plugin adds a default login webapp which can be used for security providers that require a login page.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-security-default-login-webapp as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Security Default Login Webapp": {
            "path": "/login",
            "pageTitle": "My fancy website",
            "loginFormTitle": "Login",
            "styleFile": "./login_style.css"
        }
    }
}

Mashroom CSRF Protection

If you add this plugin all updating HTTP methods (such as POST, PUT and DELETE) must contain a CSRF token automatically generated for the session. Otherwise, the request will be rejected.

There are two ways to pass the token:

You can use the MashroomCSRFService to get the current token.

Mashroom Portal automatically uses this plugin to secure all requests if available.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-csrf-protection as dependency.

After that you can use the service like this:

import type {MashroomCacheControlService} from '@mashroom/mashroom-csrf-protection/type-definitions';

export default (req: Request, res: Response) => {

    const csrfService: MashroomCacheControlService = req.pluginContext.services.csrf.service;
    const token = csrfService.getCSRFToken(req);

    // ...
}

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom CSRF Middleware": {
            "safeMethods": ["GET", "HEAD", "OPTIONS"]
        },
        "Mashroom CSRF Services": {
            "saltLength": 8,
            "secretLength": 18
        }
    }
}

Services

MashroomCSRFService

The exposed service is accessible through pluginContext.services.csrf.service

Interface:

export interface MashroomCSRFService {

    /**
     * Get the current CSRF token for this session
     */
    getCSRFToken(request: Request): string;

    /**
     * Check if the given token is valid
     */
    isValidCSRFToken(request: Request, token: string): boolean;
}

Mashroom Helmet

This plugin adds the Helmet middleware which sets a bunch of protective HTTP headers on each response.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-helmet as dependency.

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Helmet Middleware": {
            "helmet": {
                "contentSecurityPolicy": false,
                "crossOriginEmbedderPolicy": false,
                "crossOriginOpenerPolicy": {
                    "policy": "same-origin"
                },
                "crossOriginResourcePolicy": {
                    "policy": "same-site"
                },
                "expectCt": false,
                "referrerPolicy": false,
                "hsts": {
                    "maxAge": 31536000
                },
                "noSniff": true,
                "originAgentCluster": false,
                "dnsPrefetchControl": {
                    "allow": false
                },
                "frameguard": {
                    "action": "sameorigin"
                },
                "permittedCrossDomainPolicies": {
                    "permittedPolicies": "none"
                },
                "hidePoweredBy": false,
                "xssFilter": true
            }
        }
    }
}

NOTE: You shouldn't enable the noCache module because this would significantly decrease the performance of the Mashroom Portal.

Mashroom Error Pages

This plugin allows it to show proper HTML pages for arbitrary HTTP status codes. It delivers error pages only if the request accept header contains text/html. So, typically not for AJAX requests.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-error-pages as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Error Pages Middleware": {
            "mapping": {
                "404": "./pages/404.html",
                "403": "./pages/403.html",
                "400": "http://my.server-com/bad_request.html",
                "500": "/some/server/path/500.html",
                "default": "./pages/default.html"
            }
        }
    }
}
HTML Files

Mashroom Storage

This plugin adds a storage service abstraction that delegates to a provider plugin.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-storage as dependency.

Then use the storage service like this:

import type {MashroomStorageService} from '@mashroom/mashroom-storage/type-definitions';

export default async (req: Request, res: ExpressResponse) => {
    const storageService: MashroomStorageService = req.pluginContext.services.storage.service;

    const customerCollection = await storageService.getCollection('my-collection');

    const customer = await customerCollection.findOne({customerNr: 1234567});
    const customers = await customerCollection.find({ $and: [{ name: { $regex: 'jo.*' } }, { visits: { $gt: 10 } }], 20, 10, { visits: 'desc' });

    // ...
}

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
       "Mashroom Storage Services": {
           "provider": "Mashroom Storage Filestore Provider",
           "memoryCache": {
               "enabled": false,
               "ttlSec": 120,
               "invalidateOnUpdate": true,
               "collections": {
                   "mashroom-portal-pages": {
                      "enabled": true,
                      "ttlSec": 300
                   }
               }
           }
       }
    }
}

Services

MashroomStorageService

The exposed service is accessible through pluginContext.services.storage.service

Interface:

export interface MashroomStorageService {
    /**
     * Get (or create) the MashroomStorageCollection with given name.
     */
    getCollection<T extends StorageRecord>(name: string): Promise<MashroomStorageCollection<T>>;
}

export interface MashroomStorageCollection<T extends MashroomStorageRecord> {
    /**
     * Find all items that match given filter. The filter supports a subset of Mongo's filter operations (like $gt, $regex, ...).
     */
    find(filter?: MashroomStorageObjectFilter<T>, limit?: number, skip?: number, sort?: MashroomStorageSort<T>): Promise<MashroomStorageSearchResult<T>>;

    /**
     * Return the first item that matches the given filter or null otherwise.
     */
    findOne(filter: MashroomStorageObjectFilter<T>): Promise<MashroomStorageObject<T> | null | undefined>;

    /**
     * Insert one item
     */
    insertOne(item: T): Promise<MashroomStorageObject<T>>;

    /**
     * Update the first item that matches the given filter.
     */
    updateOne(filter: MashroomStorageObjectFilter<T>, propertiesToUpdate: Partial<MashroomStorageObject<T>>): Promise<MashroomStorageUpdateResult>;

    /**
     * Update multiple entries
     */
    updateMany(filter: MashroomStorageObjectFilter<T>, propertiesToUpdate: Partial<MashroomStorageObject<T>>): Promise<MashroomStorageUpdateResult>;

    /**
     * Replace the first item that matches the given filter.
     */
    replaceOne(filter: MashroomStorageObjectFilter<T>, newItem: T): Promise<MashroomStorageUpdateResult>;

    /**
     * Delete the first item that matches the given filter.
     */
    deleteOne(filter: MashroomStorageObjectFilter<T>): Promise<MashroomStorageDeleteResult>;

    /**
     * Delete all items that match the given filter.
     */
    deleteMany(filter: MashroomStorageObjectFilter<T>): Promise<MashroomStorageDeleteResult>;
}

Plugin type

storage-provider

This plugin type adds a a new storage implementation that can be used by this plugin.

To register a new storage-provider plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Storage Provider",
                "type": "storage-provider",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                    "myProperty": "test"
                }
            }
        ]
    }
}

The bootstrap returns the provider:

import MyStorage from './MyStorage';

import type {MashroomStoragePluginBootstrapFunction} from '@mashroom/mashroom-storage/type-definitions';

const bootstrap: MashroomStoragePluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {

    return new MyStorage(/* .... */);
};

export default bootstrap;

The plugin has to implement the following interfaces:

export interface MashroomStorage {
    /**
     * Get (or create) the MashroomStorageCollection with given name.
     */
    getCollection<T extends StorageRecord>(
        name: string,
    ): Promise<MashroomStorageCollection<T>>;
}

Mashroom Storage Filestore Provider

This plugin adds a simple but cluster-safe, JSON based storage provider.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-storage-provider-filestore as dependency.

To activate this provider configure the Mashroom Security plugin like this:

{
    "plugins": {
         "Mashroom Storage Services": {
              "provider": "Mashroom Storage Filestore Provider"
          }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Storage Filestore Provider": {
            "dataFolder": "/var/mashroom/data/storage",
            "checkExternalChangePeriodMs": 100,
            "prettyPrintJson": true
        }
    }
}

Mashroom Storage MongoDB Provider

This plugin adds a MongoDB based storage provider.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-storage-provider-mongodb as dependency.

To activate this provider, configure the Mashroom Security plugin like this:

{
    "plugins": {
         "Mashroom Storage Services": {
              "provider": "Mashroom Storage MongoDB Provider"
          }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Storage MongoDB Provider": {
            "uri": "mongodb://user:xxxxx@localhost:27017/mashroom_storage_db",
            "connectionOptions": {
                "poolSize": 5,
                "useUnifiedTopology": true,
                "useNewUrlParser": true
            }
        }
    }
}

Mashroom Session

This plugin adds Express session as middleware.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session as dependency.

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Session Middleware": {
            "order": -100,
            "provider": "Mashroom Session Filestore Provider",
            "session": {
                "secret": "EWhQ5hvETGkqvPDA",
                "resave": false,
                "saveUninitialized": false,
                "cookie": {
                    "maxAge": 7200000,
                    "httpOnly": true,
                    "secure": false,
                    "sameSite": false
                }
            }
        }
    }
}

Security hints:

Plugin Types

session-store-provider

This plugin type adds a session store that can be used by this plugin.

To register a custom session-store-provider plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Session Provider",
                "type": "session-store-provider",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                    "myProperty": "test"
                }
            }
        ]
    }
}

The bootstrap returns the express session store (here for example the file store):

import sessionFileStore from 'session-file-store';

import type {MashroomSessionStoreProviderPluginBootstrapFunction} from '@mashroom/mashroom-session/type-definitions';

const bootstrap: MashroomSessionStoreProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder, expressSession) => {
    const options = {...pluginConfig};
    const FileStore = sessionFileStore(expressSession);
    return new FileStore(options);
};

export default bootstrap;

Mashroom Session Filestore Provider

This plugin adds a file based session store that can be used by Mashroom Session. Actually this is just a wrapper for the session-file-store package.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session-provider-filestore as dependency.

Activate this session provider in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Session Middleware": {
            "provider": "Mashroom Session Filestore Provider"
        }
    }
}

And to change the default config of this plugin add:

{
  "plugins": {
        "Mashroom Session Filestore Provider": {
            "path": "../../data/sessions"
        }
    }
}

NOTE: The base for relative paths is the Mashroom config file.

All config options are passed to the session-file-store. See session-file-store for available options.

Mashroom Session Redis Provider

This plugin adds a Redis session store that can be used by Mashroom Session. Actually this is just a wrapper for the connect-redis package.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session-provider-redis as dependency.

Activate this session provider in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Session Redis Provider": {
            "provider": "Mashroom Session MongoDB Provider"
        }
    }
}

And to change the default config of this plugin add:

{
  "plugins": {
      "Mashroom Session Redis Provider": {
          "client": {
              "redisOptions": {
                  "host": "localhost",
                  "port": "6379",
                  "maxRetriesPerRequest": 3,
                  "enableOfflineQueue": false
              },
              "cluster": false,
              "clusterNodes": null,
              "clusterOptions": null
          },
          "prefix": "mashroom:sess:"
          "ttl": 86400
      }
  }
}

NOTE: Don't set client.redisOptions.keyPrefix because then the session metrics will not work properly.

Usage with Sentinel

For a high availability cluster with Sentinel the configuration would look like this:

{
  "plugins": {
      "Mashroom Session Redis Provider": {
         "client": {
             "redisOptions": {
                 "sentinels": [
                     { "host": "localhost", "port": 26379 },
                     { "host": "localhost", "port": 26380 }
                 ],
                 "name": "myMaster",
                 "keyPrefix": "mashroom:sess:"
             }
         }
      }
  }
}

Checkout out the Sentinel section of the ioredis documentation for all available options.

Usage with a cluster

For a sharding cluster configure the plugin like this:

{
  "plugins": {
      "Mashroom Session Redis Provider": {
         "client": {
             "cluster": true,
             "clusterNodes": [
                 {
                     "host": "redis-node1",
                     "port": "6379"
                 },
                 {
                     "host": "redis-node2",
                     "port": "6379"
                 }
             ],
             "clusterOptions": {
                 "maxRedirections": 3
             },
             "redisOptions": {
                 "keyPrefix": "mashroom:sess:"
             }
         }
      }
  }
}

Checkout out the Cluster section of the ioredis documentation for all available options.

Mashroom Session MongoDB Provider

This plugin adds a mongoDB session store that can be used by Mashroom Session. Actually this is just a wrapper for the connect-mongo package.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-session-provider-mongodb as dependency.

Activate this session provider in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Session Middleware": {
            "provider": "Mashroom Session MongoDB Provider"
        }
    }
}

And to change the default config of this plugin add:

{
  "plugins": {
      "Mashroom Session MongoDB Provider": {
          "client": {
              "uri": "mongodb://localhost:27017/mashroom_session_db?connectTimeoutMS=1000&socketTimeoutMS=2500",
              "connectionOptions": {
                  "minPoolSize": 5,
                  "serverSelectionTimeoutMS": 3000
              }
          },
          "collectionName": "mashroom-sessions",
          "ttl": 86400,
          "autoRemove": "native",
          "autoRemoveInterval": 10,
          "touchAfter": 0,
          "crypto": {
              "secret": false
          }
      }
    }
}

Mashroom HTTP proxy

This plugin adds a service for forwarding requests to a target URI. It supports HTTP, HTTPS and WebSockets (only the default/nodeHttpProxy implementation, see below).

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-http-proxy as dependency.

After that you can use the service like this:

import type {MashroomHttpProxyService} from '@mashroom/mashroom-http-proxy/type-definitions';

export default async (req: Request, res: Response) => {
    const httpProxyService: MashroomHttpProxyService = req.pluginContext.services.proxy.service;

    const targetURI = 'http://foo.bar/api/test';
    const additionalHeaders = {};
    await httpProxyService.forward(req, res, targetURI, additionalHeaders);
}

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Http Proxy Services": {
            "forwardMethods": [
                "GET",
                "POST",
                "PUT",
                "DELETE"
            ],
            "forwardHeaders": [
              "accept",
              "accept-*",
              "range",
              "expires",
              "cache-control",
              "last-modified",
              "content-*",
              "x-forwarded-*",
              "uber-trace-id",
              "uberctx-",
              "b3",
              "x-b3-*",
              "trace*",
              "sec-websocket-*"
            ],
            "rejectUnauthorized": true,
            "poolMaxTotalSockets": null,
            "poolMaxSocketsPerHost": 10,
            "poolMaxWaitingRequestsPerHost": null,
            "socketTimeoutMs": 30000,
            "keepAlive": true,
            "retryOnReset": true,
            "wsMaxConnectionsTotal": 2000,
            "wsMaxConnectionsPerHost": null,
            "proxyImpl": "default"
        }
    }
}

Services

MashroomHttpProxyService

The exposed service is accessible through pluginContext.services.proxy.service

Interface:

export interface MashroomHttpProxyService {

    /**
     * Forwards the given request to the targetUri and passes the response from the target to the response object.
     * The Promise will always resolve, you have to check response.statusCode to see if the transfer was successful or not.
     * The Promise will resolve as soon as the whole response was sent to the client.
     */
    forward(req: Request, res: Response, targetUri: string, additionalHeaders?: HttpHeaders): Promise<void>;

    /**
     * Forwards a WebSocket request (ws or wss).
     * The passed additional headers are only available at the upgrade/handshake request (most WS frameworks allow you to access it).
     */
    forwardWs(req: IncomingMessageWithContext, socket: Socket, head: Buffer, targetUri: string, additionalHeaders?: HttpHeaders): Promise<void>;

}

Plugin Types

http-proxy-interceptor

This plugin type can be used to intercept http proxy calls and to add for example authentication headers to backend calls.

To register your custom http-proxy-interceptor plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Custom Http Proxy Interceptor",
                "type": "http-proxy-interceptor",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                    "order": 500,
                    "myProperty": "foo"
                }
            }
        ]
    }
}

The bootstrap returns the interceptor:

import type {MashroomHttpProxyInterceptorPluginBootstrapFunction} from '@mashroom/mashroom-http-proxy/type-definitions';

const bootstrap: MashroomHttpProxyInterceptorPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {

    return new MyInterceptor(/* ... */);
};

export default bootstrap;

The provider has to implement the following interface:

interface MashroomHttpProxyInterceptor {


    /**
     * Intercept request to given targetUri.
     *
     * The existingHeaders contain the original request headers, headers added by the MashroomHttpProxyService client and the ones already added by other interceptors.
     * The existingQueryParams contain query parameters from the request and the ones already added by other interceptors.
     *
     * clientRequest is the request that shall be forwarded. DO NOT MANIPULATE IT. Just use it to access "method" and "pluginContext".
     *
     * Return null or undefined if you don't want to interfere with a call.
     */
    interceptRequest?(targetUri: string, existingHeaders: Readonly<HttpHeaders>, existingQueryParams: Readonly<QueryParams>,
                      clientRequest: Request, clientResponse: Response):
        Promise<MashroomHttpProxyRequestInterceptorResult | undefined | null>;

    /**
     * Intercept WebSocket request to given targetUri.
     *
     * The existingHeaders contain the original request headers, headers added by the MashroomHttpProxyService client and the ones already added by other interceptors.
     *
     * The changes are ONLY applied to the upgrade request, not to WebSocket messages.
     *
     * Return null or undefined if you don't want to interfere with a call.
     */
    interceptWsRequest?(targetUri: string, existingHeaders: Readonly<HttpHeaders>, clientRequest: IncomingMessageWithContext):
        Promise<MashroomWsProxyRequestInterceptorResult | undefined | null>;

    /**
     * Intercept response from given targetUri.
     *
     * The existingHeaders contain the original request header and the ones already added by other interceptors.
     * targetResponse is the response that shall be forwarded to the client. DO NOT MANIPULATE IT. Just use it to access "statusCode" an such.
     *
     * Return null or undefined if you don't want to interfere with a call.
     */
    interceptResponse?(targetUri: string, existingHeaders: Readonly<HttpHeaders>, targetResponse: IncomingMessage,
                       clientRequest: Request, clientResponse: Response):
        Promise<MashroomHttpProxyResponseInterceptorResult | undefined | null>;

}

As an example you could add a Bearer token to each request like this (implemented like this in the mashroom-http-proxy-add-id-token module):

export default class MyInterceptor implements MashroomHttpProxyInterceptor {

    async interceptRequest(targetUri: string, existingHeaders: Readonly<HttpHeaders>, existingQueryParams: Readonly<QueryParams>,
                         clientRequest: Request, clientResponse: Response) {
        const logger = clientRequest.pluginContext.loggerFactory('test.http.interceptor');
        const securityService = clientRequest.pluginContext.services.security && clientRequest.pluginContext.services.security.service;

        const user = securityService.getUser(clientRequest);
        if (!user) {
           return;
        }

        return {
           addHeaders: {
              Authorization: `Bearer ${user.secrets.idToken}`
           }
        };
    }
}

Or return forbidden for some reason:

export default class MyInterceptor implements MashroomHttpProxyInterceptor {

    async interceptRequest(targetUri: string, existingHeaders: Readonly<HttpHeaders>, existingQueryParams: Readonly<QueryParams>,
                         clientRequest: Request, clientResponse: Response) {

        clientResponse.sendStatus(403);

        return {
          responseHandled: true
        };
    }
}

Or even manipulate the response:

export default class MyInterceptor implements MashroomHttpProxyInterceptor {

    async interceptResponse(targetUri: string, existingHeaders: Readonly<HttpHeaders>, targetResponse: IncomingMessage, clientRequest: ExpressRequest, clientResponse: ExpressResponse) {
        let body = [];
        targetResponse.on('data', function (chunk) {
            body.push(chunk);
        });
        targetResponse.on('end', function () {
            body = Buffer.concat(body).toString();
            console.log("Response from proxied server:", body);
            clientResponse.json({ success: true });
        });

        // NOTE: if you "await" the end event you have to call targetResponse.resume() here
        //  because the interceptor pauses the stream from the target until all interceptors are done

        return {
            responseHandled: true
        };
    }
}

Mashroom Add User Header Http Proxy Interceptor

If you add this plugin it will add HTTP headers with user information to all proxy backend calls. By default, it adds:

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-csrf-protection as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Http Proxy Add User Headers Interceptor": {
            "userNameHeader": "X-USER-NAME",
            "displayNameHeader": "X-USER-DISPLAY-NAME",
            "emailHeader": "X-USER-EMAIL",
            "extraDataHeaders": {},
            "targetUris": [".*"]
        }
    }
}

Mashroom Add Access Token Http Proxy Interceptor

If you add this plugin it will add the access token from the OpenId Connect plugin to every backend call.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-csrf-protection as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Http Proxy Add Access Token Interceptor": {
            "addBearer": false,
            "accessTokenHeader": "X-USER-ACCESS-TOKEN",
            "targetUris": [".*"]
        }
    }
}

Mashroom WebSocket

This plugin adds WebSocket support to the Mashroom Server. It exposes a new service that can be used to interact with clients that connect at /websocket/*.

NOTE: This implementation only allows authenticated users to connect.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-websocket as dependency.

And you can use the security service like this:

import type {MashroomWebSocketService} from '@mashroom/mashroom-websocket/type-definitions';

export default async (req: Request, res: Response) => {
    const webSocketService: MashroomWebSocketService = req.pluginContext.services.websocket.service;

    webSocketService.addMessageListener((path) => path === '/whatever', async (message, client) => {

        // ...

        await webSocketService.sendMessage(client, {
            message: 'Hello there'
        });
    });
}

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom WebSocket Webapp": {
            "path": "/websocket",
            "reconnectMessageBufferFolder": null,
            "reconnectTimeoutSec": 5,
            "restrictToRoles": ["WebSocketRole"],
            "enableKeepAlive": true,
            "keepAliveIntervalSec": 15,
            "maxConnections": 2000
        }
    }
}

There will also be a test page available under: /websocket/test

Reconnect to a previous session

When you connect with a client you will receive a message with your clientId from the server:

{
    "type": "setClientId",
    "payload": "abcdef"
}

When you get disconnected you should reconnect with the query parameter ?clientId=abcdef to get all messages you missed meanwhile.

This only works if reconnectMessageBufferFolder is set properly.

Services

MashroomWebSocketService

The exposed service is accessible through pluginContext.services.websocket.service

Interface:

export interface MashroomWebSocketService {
    /**
     * Add a listener for message.
     * The matcher defines which messages the listener receives. The match can be based on the connect path
     * (which is the sub path where the client connected, e.g. if it connected on /websocket/test the connect path would be /test)
     * or be based on the message content or both.
     */
    addMessageListener(
        matcher: MashroomWebSocketMatcher,
        listener: MashroomWebSocketMessageListener,
    ): void;

    /**
     * Remove a message listener
     */
    removeMessageListener(
        matcher: MashroomWebSocketMatcher,
        listener: MashroomWebSocketMessageListener,
    ): void;

    /**
     * Add a listener for disconnects
     */
    addDisconnectListener(listener: MashroomWebSocketDisconnectListener): void;

    /**
     * Remove a disconnect listener
     */
    removeDisconnectListener(
        listener: MashroomWebSocketDisconnectListener,
    ): void;

    /**
     * Send a (JSON) message to given client.
     */
    sendMessage(client: MashroomWebSocketClient, message: any): Promise<void>;

    /**
     * Get all clients on given connect path
     */
    getClientsOnPath(connectPath: string): Array<MashroomWebSocketClient>;

    /**
     * Get all clients for a specific username
     */
    getClientsOfUser(username: string): Array<MashroomWebSocketClient>;

    /**
     * Get the number of connected clients
     */
    getClientCount(): number;

    /**
     * Close client connection (this will also trigger disconnect listeners)
     */
    close(client: MashroomWebSocketClient): void;

    /**
     * The base path where clients can connect
     */
    readonly basePath: string;
}

Mashroom Messaging

This plugin adds server side messaging support to Mashroom Server. If an external provider plugin (e.g. MQTT) is configured the messages can also be sent across multiple nodes (cluster support!) and to 3rd party systems.

Optionally it supports sending and receiving messages via WebSocket (Requires mashroom-websocket).

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging as dependency.

And you can use the messaging service like this:

import type {MashroomMessagingService} from '@mashroom/mashroom-messaging/type-definitions';

export default async (req: Request, res: Response) => {
    const messagingService: MashroomMessagingService = req.pluginContext.services.messaging.service;

    // Subscribe
    await messagingService.subscribe(req, 'my/topic', (data) => {
        // Do something with data
    });

    // Publish
    await messagingService.publish(req, 'other/topic', {
        item: 'Beer',
        quantity: 1,
    });

    // ...
}

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Messaging Services": {
            "externalProvider": null,
            "externalTopics": [],
            "userPrivateBaseTopic": "user",
            "enableWebSockets": true,
            "topicACL": "./topicACL.json"
        }
    }
}

With a config like that you can place a file topicacl.json_ in your server config with a content like this:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-security-topic-acl.json",
    "my/topic": {
        "allow": ["Role1"]
    },
    "foo/bar/#": {
        "allow": "any"
        "deny": ["NotSoTrustedRole"]
    }
}

The general structure is:

    "/my/+/topic/#": {
        "allow": "any"|<array of roles>
        "deny": "any"|<array of roles>
    }

You can use here + or * as a wildcard for a single level and # for multiple levels.

WebSocket interface

If enableWebSockets is true you can connect to the messaging system on /messaging which is by default /websocket/messaging. The server expects and sends serialized JSON.

After a successful connection you can use the following commands:

Subscribe

{
  messageId: 'ABCD',
  command: 'subscribe',
  topic: 'foo/bar',
}

The messageId should be unique. You will get a response message like this when the operation succeeds:

{
  messageId: 'ABCD',
  success: true,
}

Otherwise a error message like this:

{
  messageId: 'ABCD',
  error: true,
  message: 'The error message'
}

Unsubscribe

{
  messageId: 'ABCD',
  command: 'unsubscribe',
  topic: 'foo/bar',
}

Success and error response messages are the same as above.

Publish

{
  messageId: 'ABCD',
  command: 'publish',
  topic: 'foo/bar',
  message: {
     foo: 'bar'
  }
}

Success and error response messages are the same as above.

And the server will push the following if a message for a subscribed topic arrives:

{
  remoteMessage: true,
  topic: 'foo/bar',
  message: {
     what: 'ever'
  }
}

Services

MashroomMessagingService

The exposed service is accessible through pluginContext.services.messaging.service

Interface:

export interface MashroomMessagingService {
    /**
     * Subscribe to given topic.
     * Topics can be hierarchical and also can contain wildcards. Supported wildcards are + for a single level
     * and # for multiple levels. E.g. foo/+/bar or foo/#
     *
     * Throws an exception if there is no authenticated user
     */
    subscribe(req: Request, topic: string, callback: MashroomMessagingSubscriberCallback): Promise<void>;

    /**
     * Unsubscribe from topic
     */
    unsubscribe(topic: string, callback: MashroomMessagingSubscriberCallback): Promise<void>;

    /**
     * Publish to a specific topic
     *
     * Throws an exception if there is no authenticated user
     */
    publish(req: Request, topic: string, data: any): Promise<void>;

    /**
     * The private topic only the current user can access.
     * E.g. if the value is user/john the user john can access to user/john/whatever
     * but not to user/otheruser/foo
     *
     * Throws an exception if there is no authenticated user
     */
    getUserPrivateTopic(req: Request): string;

    /**
     * The connect path to send publish or subscribe via WebSocket.
     * Only available if enableWebSockets is true and mashroom-websocket is preset.
     */
    getWebSocketConnectPath(req: Request): string | null | undefined;
}

Plugin Types

external-messaging-provider

This plugin type connects the messaging system to an external message broker. It also adds cluster support to the messaging system.

To register your custom external-messaging-provider plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Custom External Messaging Provider",
                "type": "external-messaging-provider",
                "bootstrap": "./dist/mashroom-bootstrap",
                "defaultConfig": {
                   "myProperty": "foo"
                }
            }
        ]
    }
}

The bootstrap returns the provider:

import type {MashroomExternalMessagingProviderPluginBootstrapFunction} from '@mashroom/mashroom-messaging/type-definitions';

const bootstrap: MashroomExternalMessagingProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {

    return new MyExternalMessagingProvider(/* ... */);
};

export default bootstrap;

The provider has to implement the following interface:

export interface MashroomMessagingExternalProvider {
    /**
     * Add a message listener
     * The message must be a JSON object.
     */
    addMessageListener(listener: MashroomExternalMessageListener): void;

    /**
     * Remove an existing listener
     */
    removeMessageListener(listener: MashroomExternalMessageListener): void;

    /**
     * Send a message to given internal topic.
     * Used to broadcast message between Mashroom instances.
     *
     * The passed topic must be prefixed with the topic the provider is listening to.
     * E.g. if the passed topic is foo/bar and the provider is listening to mashroom/# the message must be
     * sent to mashroom/foo/bars.
     *
     * The message will be a JSON object.
     */
    sendInternalMessage(topic: string, message: any): Promise<void>;

    /**
     * Send a message to given external topic.
     * Used to send messages to 3rd party systems.
     *
     * The message will be a JSON object.
     */
    sendExternalMessage(topic: string, message: any): Promise<void>;
}

Mashroom Messaging External Provider AMQP

This plugin allows to use an AMQP 1.0 compliant broker as external messaging provider for server side messaging. This enables cluster support for server side messaging and also allows communication with 3rd party systems.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging-external-provider-amqp as dependency.

To activate this provider configure the Mashroom Messaging plugin like this:

{
    "plugins": {
        "Mashroom Messaging Services": {
            "externalProvider": "Mashroom Messaging External Provider AMQP"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Messaging External Provider MQTT": {
              "internalRoutingKey": "mashroom",
              "brokerTopicExchangePrefix": "/topic/",
              "brokerTopicMatchAny": "#",
              "brokerHost": "localhost",
              "brokerPort": 5672,
              "brokerUsername": null,
              "brokerPassword": null
        }
    }
}
Broker specific configuration

RabbitMQ

 "brokerTopicExchangePrefix": "/topic/",
 "brokerTopicMatchAny": "#",

ActiveMQ

 "brokerTopicExchangePrefix": "topic://",
 "brokerTopicMatchAny": ">",

Qpid Broker

 "brokerTopicExchangePrefix": "amq.topic/",
 "brokerTopicMatchAny": "#",

Mashroom Messaging External Provider MQTT

This plugin allows to use a MQTT server as external messaging provider for server side messaging. This enables cluster support for server side messaging and also allows communication with 3rd party systems.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging-external-provider-mqtt as dependency.

To activate this provider configure the Mashroom Messaging plugin like this:

{
    "plugins": {
        "Mashroom Messaging Services": {
            "externalProvider": "Mashroom Messaging External Provider MQTT"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Messaging External Provider MQTT": {
            "internalTopic": "mashroom",
            "mqttConnectUrl": "mqtt://localhost:1883",
            "mqttProtocolVersion": 4,
            "mqttQoS": 1,
            "mqttUser": null,
            "mqttPassword": null,
            "rejectUnauthorized": true
        }
    }
}

Mashroom Messaging External Provider Redis

This plugin allows to use a Redis server as external messaging provider for server side messaging. This enables cluster support for server side messaging and also allows communication with 3rd party systems.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-messaging-external-provider-redis as dependency.

To activate this provider configure the Mashroom Messaging plugin like this:

{
    "plugins": {
        "Mashroom Messaging Services": {
            "externalProvider": "Mashroom Messaging External Provider Redis"
        }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Messaging External Provider Redjs": {
            "internalTopic": "mashroom",
            "client": {
                "redisOptions": {
                    "host": "localhost",
                    "port": "6379",
                    "maxRetriesPerRequest": 3,
                    "enableOfflineQueue": false
                },
                "cluster": false,
                "clusterNodes": null,
                "clusterOptions": null
            }
        }
    }
}

Mashroom Memory Cache

This plugin adds a general purpose memory cache service. Some other plugins will automatically use it if present, for example mashroom-storage.

The cache service provides multiple regions with the possibility to clear single regions. It comes with a built-in provider that uses the local Node.js memory, which is not ideal for clusters. But it can also be configured to use another provider, e.g. an implementation based on Redis.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-memory-cache as dependency.

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Memory Cache Services": {
            "provider": "local",
            "defaultTTLSec": 300
        }
    }
}

Services

MashroomMemoryCacheService

The exposed service is accessible through pluginContext.services.cache.service

Interface:

export interface MashroomMemoryCacheService {
    /**
     * Get a cache entry from given region
     */
    get(region: string, key: CacheKey): Promise<CacheValue | undefined>;
    /**
     * Set a cache entry in given region
     */
    set(region: string, key: CacheKey, value: CacheValue, ttlSec?: number): Promise<void>;
    /**
     * Delete an entry in given region
     */
    del(region: string, key: CacheKey): Promise<void>;
    /**
     * Clear the entire region
     * This might be an expensive operation, depending on the provider
     */
    clear(region: string): Promise<void>;
    /**
     * Get the number of entries in this region (if possible)
     * This might be an expensive operation, depending on the provider
     */
    getEntryCount(region: string): Promise<number | undefined>;
}

Plugin Types

memory-cache-provider

This plugin type adds a memory cache provider that can be used by this plugin.

To register a custom memory-cache-provider plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Cache Store Provider",
                "type": "memory-cache-provider",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                    "myProperty": "test"
                }
            }
        ]
    }
}

The bootstrap returns the provider:

import type {MashroomMemoryCacheProviderPluginBootstrapFunction} from '@mashroom/mashroom-memory-cache/type-definitions';

const bootstrap: MashroomMemoryCacheProviderPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
    return new MyCacheStore();
};

export default bootstrap;

And the provider has to implement the following interface:

export interface MashroomMemoryCacheProvider {
    /**
     * Get a cache entry from given region
     */
    get(region: string, key: CacheKey): Promise<CacheValue | undefined>;
    /**
     * Set a cache entry in given region
     */
    set(region: string, key: CacheKey, value: CacheValue, ttlSec: number): Promise<void>;
    /**
     * Delete an entry in given region
     */
    del(region: string, key: CacheKey): Promise<void>;
    /**
     * Clear the entire region
     */
    clear(region: string): Promise<void>;
    /**
     * Get the number of entries in this region (if possible)
     */
    getEntryCount(region: string): Promise<number | undefined>;
}

Mashroom Memory Cache Redis Provider

This plugin adds a Redis based provider for the mashroom-memory-cache.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-memory-cache-provider-redis as dependency.

To activate this provider configure the Mashroom Memory Cache plugin like this:

{
    "plugins": {
          "Mashroom Memory Cache Redis Provider": {
              "provider": "Mashroom Memory Cache Redis Provider",
              "defaultTTLSec": 10
          }
    }
}

And configure this plugin like this in the Mashroom config file:

{
  "plugins": {
        "Mashroom Memory Cache Redis Provider": {
            "redisOptions": {
                "host": "localhost",
                "port": "6379",
                "keyPrefix": "mashroom:cache:"
           }
        }
    }
}

Checkout out the ioredis documentation for all available options.

Usage with Sentinel

For a high availability cluster with Sentinel the configuration would look like this:

{
  "plugins": {
      "Mashroom Memory Cache Redis Provider": {
          "redisOptions": {
              "sentinels": [
                  { "host": "localhost", "port": 26379 },
                  { "host": "localhost", "port": 26380 }
               ],
              "name": "myMaster",
              "keyPrefix": "mashroom:cache:"
          }
      }
  }
}

Checkout out the Sentinel section of the ioredis documentation for all available options.

Usage with a cluster

For a sharding cluster configure the plugin like this:

{
  "plugins": {
      "Mashroom Memory Cache Redis Provider": {
          "cluster": true,
          "clusterNodes": [
              {
                  "host": "redis-node1",
                  "port": "6379"
              },
              {
                  "host": "redis-node2",
                  "port": "6379"
              }
          ],
          "clusterOptions": {
              "maxRedirections": 3
          },
          "redisOptions": {
              "keyPrefix": "mashroom:cache:"
          }
      }
  }
}

Checkout out the Cluster section of the ioredis documentation for all available options.

Mashroom I18N

This plugin adds a service for internationalization. It determines the language from the HTTP headers and supports translation of messages.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-i18n as dependency.

After that you can use the service like this:

import type {MashroomI18NService} from '@mashroom/mashroom-i18n/type-definitions';

export default (req: Request, res: Response) => {
    const i18nService: MashroomI18NService = req.pluginContext.services.i18n.service;

    const currentLang = i18nService.getLanguage(req);
    const message =  i18nService.getMessage('username', 'de');
    // message will be 'Benutzernamen'

    // ...
}

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Internationalization Services": {
            "availableLanguages": ["en", "de", "fr"],
            "defaultLanguage": "en",
            "messages": "./messages"
        }
    }
}

The lookup for message files works like this:

And a messages file (e.g. messages.de.json) looks like this:

{
    "message_key": "Die Nachricht"
}

Services

MashroomI18NService

The exposed service is accessible through pluginContext.services.i18n.service

Interface:

export interface MashroomI18NService {
    /**
     * Get the currently set language (for current session)
     */
    getLanguage(req: Request): string;

    /**
     * Set session language
     */
    setLanguage(language: string, req: Request): void;

    /**
     * Get the message for given key and language
     */
    getMessage(key: string, language: string): string;

    /**
     * Get plain string in the current users language from a I18NString
     */
    translate(req: Request, str: I18NString): string;

    /**
     * Get available languages
     */
    readonly availableLanguages: Readonly<Array<string>>;

    /**
     * Get the default languages
     */
    readonly defaultLanguage: string;
}

Mashroom Background Jobs

This plugin adds a background job scheduler to the Mashroom Server that supports cron expressions. It is possible to add background jobs via service or as custom plugin.

If you don't provide a cron expression the job is only executed now (immediately), this is useful if you need some code that should be executed once during server startup.

This plugin also comes with an Admin UI extension (/mashroom/admin/ext/background-jobs) that can be used to check the jobs.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-background-jobs as dependency.

After that you can use the service like this:

import type {MashroomBackgroundJobService} from '@mashroom/mashroom-background-jobs/type-definitions';

export default async (req: Request, res: Response) => {
    const backgroundJobsService: MashroomBackgroundJobService = req.pluginContext.services.backgroundJobs.service;

    backgroundJobsService.schedule('Test Job', '0/5 * * * *', () => {
       // Job implementation
    });

    // ...
}

NOTE: Despite its name a job is started in the main thread and therefore blocks the event loop. So, if you do CPU intensive work you need to spawn a Worker Thread yourself.

Services

MashroomBackgroundJobService

The exposed service is accessible through pluginContext.services.backgroundJobs.service

Interface:

export interface MashroomBackgroundJobService {

    /**
     * Schedule a job.
     * If cronSchedule is not defined the job is executed once (immediately).
     * Throws an error if the cron expression is invalid.
     */
    scheduleJob(name: string, cronSchedule: string | undefined | null, callback: MashroomBackgroundJobCallback): MashroomBackgroundJob;

    /**
     * Unschedule an existing job
     */
    unscheduleJob(name: string): void;

    readonly jobs: Readonly<Array<MashroomBackgroundJob>>;
}

Plugin Types

background-job

This plugin type allows it to schedule a background job.

To register your custom background-job plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My background job",
                "type": "background-job",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "defaultConfig": {
                   "cronSchedule": "0/1 * * * *",
                   "invokeImmediately": false,
                   "yourConfigProp": "whatever"
                }
            }
        ]
    }
}

The bootstrap returns the job callback:

import type {MashroomBackgroundJobPluginBootstrapFunction} from '@mashroom/mashroom-background-jobs/type-definitions';

const bootstrap: MashroomBackgroundJobPluginBootstrapFunction = async (pluginName, pluginConfig, pluginContextHolder) => {
    const {yourConfigProp} = pluginConfig;
    return (pluginContect) => {
        // Job impl
    };
};

export default bootstrap;

Mashroom Browser Cache

This plugin adds a Service to manage cache control headers. It also allows to disable the cache globally.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-browser-cache as dependency.

After that you can use the service like this:

import type {MashroomCacheControlService} from '@mashroom/mashroom-browser-cache/type-definitions';

export default async (req: Request, res: Response) => {

    const cacheControlService: MashroomCacheControlService = req.pluginContext.services.browserCache.cacheControl;
    await cacheControlService.addCacheControlHeader('ONLY_FOR_ANONYMOUS_USERS', req, res);

    // ..
};

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Cache Control Services": {
            "disabled": false,
            "maxAgeSec": 31536000
        }
    }
}

Services

MashroomCacheControlService

The Cache Control service is accessible through pluginContext.services.browserCache.cacheControl

Interface:

export interface MashroomCacheControlService {
    /**
     * Add the Cache-Control header based on the policy and authentication status.
     */
     addCacheControlHeader(cachingPolicy: CachingPolicy, request: Request, response: Response): void;

    /**
     * Remove a previously set Cache-Control header
     */
     removeCacheControlHeader(response: Response): void;
}

Caching Policies are:

export type CachingPolicy =  'SHARED' | 'PRIVATE_IF_AUTHENTICATED' | 'NEVER' | 'ONLY_FOR_ANONYMOUS_USERS';

Mashroom CDN

This plugin adds a Service to manage CDN hosts. It basically just returns a host from a configurable list, which can be used to access an asset via CDN.

NOTE: The mashroom-cdn plugin requires a CDN that works like a transparent proxy, which forwards an identical request to the origin (in this case Mashroom) if does not exist yet.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-cdn as dependency.

After that you can use the service like this:

import type {MashroomCDNService} from '@mashroom/mashroom-cdn/type-definitions';

export default async (req: Request, res: Response) => {

    const cdnService: MashroomCDNService = req.pluginContext.services.cdn.service;

    const cdnHost = cdnService.getCDNHost();
    const resourceUrl = `${cdnHost}/<the-actual-path>`;

    // ..
};

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom CDN Services": {
            "cdnHosts": [
                "//cdn1.myhost.com",
                "//cdn2.myhost.com"
            ]
        }
    }
}

Services

MashroomCDNService

The CDN service is accessible through pluginContext.services.cdn.cacheControl

Interface:

export interface MashroomCDNService {
    /**
     * Return a CDN host or null if there is none configured.
     */
    getCDNHost(): string | null;
}

Mashroom Robots

This plugin adds a middleware that exposes a robots.txt file for search engines.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-robots as dependency.

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Robots Middleware": {
            "robots.txt": "./robots.txt"
        }
    }
}

Mashroom Virtual Host Path Mapper

This plugin adds the possibility to map external paths to internal ones based on virtual host. This is required for web-apps that need to know the actual "base path" to generate URLs (in that case rewriting via reverse proxy won't work).

For example Mashroom Portal can use this to move Sites to different paths but keep the ability to generate absolute paths for resources and API calls. Which is useful if you want to expose specific Sites via a virtual host.

NOTE: All other plugins will only deal with the rewritten paths, keep that in mind especially when defining ACLs.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-vhost-path-mapper as dependency.

To map for example a portal site to www.my-company.com/web configure the reverse proxy like this:

www.my-company.com/web -> <portal-host>:<portal-port>/

and the plugin like this:

{
  "plugins": {
       "Mashroom VHost Path Mapper Middleware": {
           "considerHttpHeaders": ["x-my-custom-host-header", "x-forwarded-host"],
           "hosts": {
              "www.my-company.com": {
                 "frontendBasePath": "/web",
                 "mapping": {
                    "/login": "/login",
                    "/": "/portal/public-site"
                 }
              },
              "localhost:8080": {
                 "mapping": {
                     "/": "/local-test"
                 }
              }
          }
       }
    }
}

That means if someone accesses Mashroom Server via https://www.my-company.com/web/test the request will hit the path /portal/public-site/test.

It also works the other way round. If the server redirects to /login it would be changed to /web/login (in this example).

Port based virtual hosts (like localhost:8080) are also possible but only if the request still contains the original host header (and no X-Forwarded-Host different from the host header).

The mapping rules do not support regular expressions.

The frontendBasePath is optional and / by default.

The considerHttpHeaders property is also optional and can be used to detect the host based on some custom header. The first header that is present will be used (so the order in the list specifies the priority).

Services

MashroomVHostPathMapperService

The exposed service is accessible through pluginContext.services.vhostPathMapper.service

Interface:

export interface MashroomVHostPathMapperService {
    /**
     * Reverse map the given server url to the url as seen by the user (browser).
     * The given URL must not contain host, only path with query params and so on.
     */
    getFrontendUrl(req: Request, url: string): string;

    /**
     * Get the details if the url of the current path has been rewritten
     */
    getMappingInfo(req: Request): RequestVHostMappingInfo | undefined;
}

Mashroom Monitoring Metrics Collector

This plugin provides a service to add metrics to the monitoring system that can be used by plugins. It also adds a middleware that collects request metrics like duration and HTTP status.

It uses internally the OpenTelemetry SDK.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-monitoring-stats-collector as dependency.

You can change the default configuration in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Monitoring Metrics Collector Services": {
            "disableMetrics": [],
            "defaultHistogramBuckets": [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
            "customHistogramBucketConfig": {
              "mashroom_http_request_duration_seconds": [0.1, 1, 10]
            }
        }
    }
}
Synchronous example:
    const collectorService: MashroomMonitoringMetricsCollectorService = req.pluginContext.services.metrics.service;

    collectorService.counter('http_request_counter', 'HTTP Request Counter').inc();
Asynchronous example:
    const collectorService: MashroomMonitoringMetricsCollectorService = pluginContext.services.metrics.service;

    const ref = await collectorService.addObservableCallback((asyncCollectorService) => {
        // ... somehow get the value to measure
        asyncCollectorService.gauge('http_pool_active_connections', 'HTTP Pool Active Connections').set(theValue);
    });
Using directly the OpenTelemetry API

If you prefer the OpenTelemetry API the above examples would look like:

    const collectorService: MashroomMonitoringMetricsCollectorService = req.pluginContext.services.metrics.service;

    const meterProvider = collectorService.getOpenTelemetryMeterProvider();
    const meter = meterProvider.getMeter('my_meter');
    const counter = meter.createCounter('http_request_counter', {
        description: 'HTTP Request Counter',
    });

    counter.add(1);

and this:

    const collectorService: MashroomMonitoringMetricsCollectorService = pluginContext.services.metrics.service;

    const meterProvider = collectorService.getOpenTelemetryMeterProvider();
    const meter = meterProvider.getMeter('my_meter');
    const observableGauge = meter.createObservableGauge('http_request_counter', {
        description: 'HTTP Request Counter',
    });
    
    meter.addBatchObservableCallback((observableResult) => {
        // ... somehow get the value to measure
        observableResult.observe(observableGauge, theValue);
    }, [observableGauge]);

Services

MashroomMonitoringMetricsCollectorService

The exposed service is accessible through pluginContext.services.metrics.service

Interface:

/**
 * Mashroom Monitoring Metrics Collector Service
 *
 * An abstraction that uses currently OpenTelemetry Metrics under the hood, see https://opentelemetry.io/docs/specs/otel/metrics/
 */
export interface MashroomMonitoringMetricsCollectorService {
    /**
     * A counter is a cumulative metric that represents a single monotonically increasing counter
     * whose value can only increase.
     * Even though the returned Counter has a set() method, the new value must always be higher than the current.
     */
    counter(name: string, help: string): MashroomMonitoringMetricsCounter;
    /**
     * A histogram samples observations (usually things like request durations or response sizes)
     * and counts them in configurable buckets.It also provides a sum of all observed values.
     */
    histogram(name: string, help: string, buckets?: number[]): MashroomMonitoringMetricsHistogram;
    /**
     * Add a callback for asynchronous measuring of values.
     * Gauges and counters where you can set the value directly are only available like this!
     */
    addObservableCallback(cb: MashroomMonitoringMetricsObservableCallback): Promise<MashroomMonitoringMetricsObservableCallbackRef>;
    /**
     * Get OpenTelemetry resource metrics for export
     */
    getOpenTelemetryResourceMetrics(): Promise<ResourceMetrics>;
    /**
     * The underlying MeterProvider which can be used if you prefer directly using the OpenTelemetry API instead.
     * All metrics created will be automatically exported as well.
     */
    getOpenTelemetryMeterProvider(): MeterProvider;
}

NOTE: Don't keep a global reference to the returned metric objects.

Mashroom Monitoring Prometheus Exporter

This plugin exports the following metrics to the Prometheus monitoring system:

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-monitoring-prometheus-exporter as dependency.

You can change the default configuration in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Monitoring Prometheus Exporter Webapp": {
            "path": "/myMetricsPath"
        }
    }
}
Example Queries

The examples assume the Prometheus job to scrape the metrics adds the label service:Mashroom

Request rate:

sum(rate(mashroom_http_requests_total{service="Mashroom"}[5m]))

Requests with HTTP 500 response rate:

sum(rate(mashroom_http_requests_total{status="500", service="Mashroom"}[5m]))

95% of requests served within seconds:

histogram_quantile(0.95, sum(rate(mashroom_http_request_duration_seconds_bucket{service="Mashroom"}[5m])) by (le)) * 1000

Heap total in MB:

nodejs_heap_size_total_bytes{service="Mashroom"} / 1024 / 1024

Heap used in MB:

nodejs_heap_size_used_bytes{service="Mashroom"} / 1024 / 1024

CPU usage total in %:

avg(irate(process_cpu_seconds_total{service="Mashroom"}[5m])) * 100

GC pauses 95% quantile

histogram_quantile(0.95, sum(rate(nodejs_gc_duration_seconds_bucket[5m])) by (le))

User sessions:

mashroom_sessions_total{service="Mashroom"}

Active HTTP proxy connections (e.g. Portal App REST calls):

mashroom_http_proxy_http_pool_connections_active_total{service="Mashroom"}
mashroom_http_proxy_https_pool_connections_active_total{service="Mashroom"}
mashroom_http_proxy_ws_connections_active_total{service="Mashroom"}

Idle HTTP proxy connections:

mashroom_http_proxy_http_pool_connections_idle_total{service="Mashroom"}
mashroom_http_proxy_https_pool_connections_idle_total{service="Mashroom"}

Plugins total:

mashroom_plugins_total{service="Mashroom"}

Plugins loaded:

mashroom_plugins_loaded_total{service="Mashroom"}

Plugins in error state:

mashroom_plugins_error_total{service="Mashroom"}

Remote Portal App endpoints total:

mashroom_remote_app_endpoints_total{service="Mashroom"}

Remote Portal App endpoints in error state:

mashroom_remote_app_endpoints_error_total{service="Mashroom"}

Kubernetes remote Portal App services total:

mashroom_remote_app_k8s_services_total{service="Mashroom"}

Kubernetes remote Portal App services in error state:

mashroom_remote_app_k8s_services_error_total{service="Mashroom"}

Memory cache hit ratio:

mashroom_memory_cache_hit_ratio{service="Mashroom"}

Redis session store provider connected:

mashroom_sessions_redis_nodes_connected{service="Mashroom"}

Redis memory cache provider connected:

mashroom_memory_cache_redis_nodes_connected{service="Mashroom"}

MongoDB storage provider connected:

mashroom_storage_mongodb_connected{service="Mashroom"}

MQTT messaging system connected:

mashroom_messaging_mqtt_connected{service="Mashroom"}
Kubernetes Hints

On Kubernetes the metrics are scraped separately for each container. So, you have to do the aggregation in the query.

For example, the overall request rate would still be:

sum(rate(mashroom_http_requests_total{namespace="my-namespace"}[5m]))

But the request rate per pod:

sum by (kubernetes_pod_name) (rate(mashroom_http_requests_total{namespace="my-namespace"}[5m]))

Or the Session count per pod:

mashroom_sessions_total{namespace="my-namespace"} by (kubernetes_pod_name)

In the last two examples you typically would use {{kubernetespodname}} in the legend.

Demo Grafana Dashboard

You can find a demo Grafana Dashboard here: https://github.com/nonblocking/mashroom/tree/master/packages/plugin-packages/mashroom-monitoring-prometheus-exporter/test/grafana-test/grafana/provisioning/dashboards/Mashroom%20Dashboard.json

Mashroom Monitoring PM2 Exporter

This plugin exports metrics to the PM2 via pm2/io. Which is useful if you use the PM2 process manager to run Mashroom Server.

It activates the pm2/io default metrics like v8, runtime, network, http (configurable). And it exports Mashroom plugin metrics like session count, memory cache stats, MongoDB/Redis connection stats, …

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-monitoring-pm2-exporter as dependency.

You can change the default configuration in your Mashroom config file like this:

     "plugins": {
         "Mashroom Monitoring PM2 Exporter": {
              "pmxMetrics": {
                  "v8": true,
                  "runtime": true,
                  "network": {
                      "upload": true,
                      "download": true
                  },
                  "http": true,
                  "eventLoop": true
              },
              "mashroomMetrics": [
                  "mashroom_plugins_total",
                  "mashroom_plugins_loaded_total",
                  "mashroom_plugins_error_total",
                  "mashroom_remote_app_endpoints_total",
                  "mashroom_remote_app_endpoints_error_total",
                  "mashroom_sessions_total",
                  "mashroom_websocket_connections_total",
                  "mashroom_https_proxy_active_connections_total",
                  "mashroom_https_proxy_idle_connections_total",
                  "mashroom_https_proxy_waiting_requests_total",
                  "mashroom_sessions_mongodb_connected",
                  "mashroom_sessions_redis_nodes_connected",
                  "mashroom_storage_mongodb_connected",
                  "mashroom_memory_cache_entries_added_total",
                  "mashroom_memory_cache_hit_ratio",
                  "mashroom_memory_cache_redis_nodes_connected",
                  "mashroom_messaging_amqp_connected",
                  "mashroom_messaging_mqtt_connected"
              ]
         }
    }
}

NOTE: Currently only counter and gauge metrics can be exported! For a full list install the mashroom-monitoring-prometheus-exporter and check the output of /metrics

After starting the server with pm2 you can see the metrics in the "Custom metrics" pane when you start:

pm2 monit

Or you can get it as JSON (axm_monitor property) if you execute

pm2 prettylist
Fetching all metrics via inter-process communication

If you want to gather all metrics in OpenTelemetry format you can use inter-process communication.

Here as an example how to export the metrics for each worker and make it available in Prometheus format:

Create a script metrics.js with a simple webserver:

const pm2 = require('pm2');
const { PrometheusSerializer } = require('@opentelemetry/exporter-prometheus');
const Express = require('express');
const metricsServer = Express();

const metrics = {}; // <pid> -> OpenTelemetry ResourceMetrics
const metricsServerPort = 15050;
const prometheusSerializer = new PrometheusSerializer();

metricsServer.get('/metrics/:id', async (req, res) => {
    const id = req.params.id;
    const slice = metrics[id];
    if (!slice) {
        console.error(`No metrics found for ID ${id}. Known node IDs:`, Object.keys(metrics));
        res.sendStatus(404);
        return;
    }
    res.set('Content-Type', 'text/plain');
    res.end(prometheusSerializer.serialize(slice));
});

metricsServer.listen(metricsServerPort, '0.0.0.0', () => {
    console.debug(`Prometheus cluster metrics are available at http://localhost:${metricsServerPort}/metrics`);
});

setInterval(() => {
    pm2.connect(() => {
        pm2.describe('mashroom', (describeError, processInfo) => {
            if (!describeError) {
                Promise.all(processInfo.map((processData) => {
                    console.debug(`Asking process ${processData.pm_id} for metrics`);
                    return new Promise((resolve) => {
                        pm2.sendDataToProcessId(
                            processData.pm_id,
                            {
                                data: null,
                                topic: 'getMetrics',
                                from: process.env.pm_id,
                            },
                            (err, res) => {
                                if (err) {
                                    console.error('Error sending data via PM2 intercom', err);
                                }
                                resolve();
                            },
                        );
                    });
                })).finally(() => {
                    pm2.disconnect();
                });
            } else {
                pm2.disconnect();
            }
        });
    });
}, 10000);

process.on('message', (msg) => {
    if (msg.from !== process.env.pm_id && msg.topic === 'returnMetrics') {
        console.debug(`Received metrics from process ${msg.from}`);
        metrics[msg.from] = msg.data;
    }
});

And a pm2 config like this:

{
    "apps": [
        {
            "name": "mashroom",
            "instances": 4,
            "max_restarts": 3,
            "exec_mode": "cluster",
            "script": "starter.js",
            "env": {
                "NODE_ENV": "production"
            }
        },
        {
            "name": "metrics_collector",
            "instances": 1,
            "exec_mode": "fork",
            "script": "metrics.js"
        }
    ]
}

Now the worker metrics will be available under http:/localhost:15050/metrics/.

A Prometheus scrape config could look like this:

    - job_name: 'mashroom'
      static_configs:
        - targets:
          - localhost:15050/metrics/0
          - localhost:15050/metrics/2
          - localhost:15050/metrics/3
          - localhost:15050/metrics/4
          labels:
            service: 'Mashroom'
      relabel_configs:
        - source_labels: [__address__]
          regex:  '[^/]+(/.*)'
          target_label: __metrics_path__
        - source_labels: [__address__]
          regex:  '[^/]+/[^/]+/(.*)'
          target_label: node
        - source_labels: [__address__]
          regex:  '([^/]+)/.*'
          target_label: __address__

Mashroom Portal

This plugin adds a Portal component which allows composing pages from Single Page Applications (SPAs).

Registered SPAs (Portal Apps) can be placed on arbitrary pages via Drag'n'Drop. Each instance receives a config object during startup and a bunch of client services, which for example allow access to the message bus. The config is basically an arbitrary JSON object, which can be edited via Admin Toolbar. A Portal App can also bring its own config editor App, which again is just a simple SPA.

One of the provided client services allow Portal Apps to load any other App (known by name) into any existing DOM node. This can be used to:

The Portal supports hybrid rendering for both the Portal pages and SPAs. So, if an SPA supports server side rendering the initial HTML can be incorporated into the initial HTML page. Navigating to another page dynamically replaces the SPAs in the content area via client side rendering (needs to be supported by the Theme).

The Portal also supports i18n, theming, role based security, a client-side message bus which can be connected to a server-side broker and a registry for Remote Apps on a separate server or container.

Usage

Since this plugin requires a lot of other plugins the easiest way to use it is to clone this quickstart repository: mashroom-portal-quickstart

You can find a full documentation of Mashroom Server and this portal plugin with a setup and configuration guide here: https://www.mashroom-server.com/documentation

The plugin allows the following configuration properties:

{
  "plugins": {
        "Mashroom Portal WebApp": {
            "path": "/portal",
            "adminApp": "Mashroom Portal Admin App",
            "defaultTheme": "Mashroom Portal Default Theme",
            "defaultLayout": "Mashroom Portal Default Layouts 1 Column",
            "warnBeforeAuthenticationExpiresSec": 60,
            "autoExtendAuthentication": false,
            "ignoreMissingAppsOnPages": false,
            "versionHashSalt": null,
            "resourceFetchConfig": {
                "fetchTimeoutMs": 3000,
                "httpMaxSocketsPerHost": 3,
                "httpRejectUnauthorized": true
            },
            "defaultProxyConfig": {
                "sendPermissionsHeader": false,
                "restrictToRoles": ["ROLE_X"]
            },
            "ssrConfig": {
                "ssrEnable": true,
                "renderTimoutMs": 2000,
                "cacheEnable": true,
                "cacheTTLSec": 300,
                "inlineStyles": true
            },
            "addDemoPages": true
        }
    }
}

Browser support

The Portal supports only modern Browsers and requires ES6.

Services

MashroomPortalService

The exposed service is accessible through pluginContext.services.portal.service

Interface:

export interface MashroomPortalService {
    /**
     * Get all registered portal apps
     */
    getPortalApps(): Readonly<Array<MashroomPortalApp>>;

    /**
     * Get all registered theme plugins
     */
    getThemes(): Readonly<Array<MashroomPortalTheme>>;

    /**
     * Get all registered layout plugins
     */
    getLayouts(): Readonly<Array<MashroomPortalLayout>>;

    /**
     * Get all registered page enhancement plugins
     */
    getPortalPageEnhancements(): Readonly<Array<MashroomPortalPageEnhancement>>;

    /**
     * Get all registered app enhancement plugins
     */
    getPortalAppEnhancements(): Readonly<Array<MashroomPortalAppEnhancement>>;

    /**
     * Get all sites
     */
    getSites(limit?: number): Promise<Array<MashroomPortalSite>>;

    /**
     * Get the site with the given id
     */
    getSite(siteId: string): Promise<MashroomPortalSite | null | undefined>;

    /**
     * Find the site with given path
     */
    findSiteByPath(path: string): Promise<MashroomPortalSite | null | undefined>;

    /**
     * Insert new site
     */
    insertSite(site: MashroomPortalSite): Promise<void>;

    /**
     * Update site
     */
    updateSite(site: MashroomPortalSite): Promise<void>;

    /**
     * Delete site
     */
    deleteSite(req: Request, siteId: string): Promise<void>;

    /**
     * Get page with given id
     */
    getPage(pageId: string): Promise<MashroomPortalPage | null | undefined>;

    /**
     * Find the page ref within a site with given friendly URL
     */
    findPageRefByFriendlyUrl(site: MashroomPortalSite, friendlyUrl: string): Promise<MashroomPortalPageRef | null | undefined>;

    /**
     * Find the page ref within a site by the given pageId
     */
    findPageRefByPageId(site: MashroomPortalSite, pageId: string): Promise<MashroomPortalPageRef | null | undefined>;

    /**
     * Insert new page
     */
    insertPage(page: MashroomPortalPage): Promise<void>;

    /**
     * Update page
     */
    updatePage(page: MashroomPortalPage): Promise<void>;

    /**
     * Insert new page
     */
    deletePage(req: Request, pageId: string): Promise<void>;

    /**
     * GetPortal App instance
     */
    getPortalAppInstance(pluginName: string, instanceId: string | null | undefined): Promise<MashroomPortalAppInstance | null | undefined>;

    /**
     * Insert a new Portal App instance
     */
    insertPortalAppInstance(portalAppInstance: MashroomPortalAppInstance): Promise<void>;

    /**
     * Update given Portal App instance
     */
    updatePortalAppInstance(portalAppInstance: MashroomPortalAppInstance): Promise<void>;

    /**
     * Delete given portal Portal App instance
     */
    deletePortalAppInstance(req: Request, pluginName: string, instanceId: string | null | undefined): Promise<void>;
}

Plugin Types

portal-app

Deprecated since Mashroom v2, please use portal-app2.

portal-app2

This plugin type makes a Single Page Application (SPA) available in the Portal.

To register a new portal-app plugin add this to package.json:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Single Page App",
                "type": "portal-app2",
                "clientBootstrap": "startMyApp",
                "resources": {
                    "js": [
                        "bundle.js"
                    ]
                },
                "local": {
                    "resourcesRoot": "./dist",
                    "ssrBootstrap": "./dist/renderToString.js"
                },
                "defaultConfig": {
                    "appConfig": {
                        "myProperty": "foo"
                    }
                }
            }
        ]
    }
}

A full config with all optional properties would look like this:

{
    "mashroom": {
        "plugins": [
            {
                "name": "My Single Page App",
                "type": "portal-app2",
                "clientBootstrap": "startMyApp",
                "resources": {
                    "js": [
                        "bundle.js"
                    ],
                    "css": []
                },
                "sharedResources": {
                    "js": []
                },
                "screenshots": [
                    "screenshot1.png"
                ],
                "local": {
                    "resourcesRoot": "./dist",
                    "ssrBootstrap": "/dist/renderToString.js"
                },
                "remote": {
                    "resourcesRoot": "/public",
                    "ssrInitialHtmlPath": "/ssr"
                },
                "defaultConfig": {
                    "title": {
                        "en": "My Single Page App",
                        "de": "Meine Single Page App"
                    },
                    "category": "My Category",
                    "tags": ["my", "stuff"],
                    "description": {
                        "en": "Here the english description",
                        "de": "Hier die deutsche Beschreibung"
                    },
                    "metaInfo": {
                        "capabilities": ["foo"]
                    },
                    "defaultRestrictViewToRoles": ["Role1"],
                    "rolePermissions": {
                        "doSomethingSpecial": ["Role2", "Role3"]
                    },
                    "caching": {
                        "ssrHtml": "same-config-and-user"
                    },
                    "editor": {
                        "editorPortalApp": "Demo Config Editor",
                        "position": "in-place",
                        "appConfig": {
                        }
                    },
                    "proxies": {
                        "spaceXApi": {
                            "targetUri": "https://api.spacexdata.com/v3",
                            "sendPermissionsHeader": false,
                            "restrictToRoles": ["Role1"]
                        }
                    },
                    "appConfig": {
                        "myProperty": "foo"
                    }
                }
            }
        ]
    }
}

The clientBootstrap is in this case a global function that starts the App within the given host element. Here for example a React app:

import React from 'react';
import {render, hydrate, unmountComponentAtNode} from 'react-dom';
import App from './App';

import type {MashroomPortalAppPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (element, portalAppSetup, clientServices) => {
    const {appConfig, restProxyPaths, lang} = portalAppSetup;
    const {messageBus} = clientServices;

    // Check if the Apps has been rendered in the server-side, if this is a Hybrid App and a ssrBootstrap is configured
    //const ssrHost = element.querySelector('[data-ssr-host="true"]');
    //if (ssrHost) {
    //    hydrate(<App appConfig={appConfig} messageBus={messageBus}/>, ssrHost);
    //} else {
        // CSR
        render(<App appConfig={appConfig} messageBus={messageBus}/>, element);
    //}

    return {
        willBeRemoved: () => {
            unmountComponentAtNode(portalAppHostElement);
        },
        updateAppConfig: (appConfig) => {
            // Implement if dynamic app config should be possible
        }
    };
};

global.startMyApp = bootstrap;

And for an Angular app:

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {MashroomPortalAppPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';
import {AppModule} from './app/app.module';

const bootstrap: MashroomPortalAppPluginBootstrapFunction = (hostElement, portalAppSetup, portalClientServices) => {

    return platformBrowserDynamic([
        {provide: 'host.element', useValue: hostElement },
        {provide: 'app.setup', useValue: portalAppSetup},
        {provide: 'client.services', useValue: portalClientServices}
    ]).bootstrapModule(AppModule).then(
        (module) => {
            return {
                willBeRemoved: () => {
                    console.info('Destroying Angular module');
                    module.destroy();
                }
            };
        }
    );
};

global.startAngularDemoApp = bootstrap;

In case of a Hybrid App which supports Server Side Rendering (SSR) the server side bootstrap would look like this:

import React from 'react';
import {renderToString} from 'react-dom/server';
import App from './App';

import type {MashroomPortalAppPluginSSRBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalAppPluginSSRBootstrapFunction = (portalAppSetup, req) => {
    const {appConfig, restProxyPaths, lang} = portalAppSetup;
    const dummyMessageBus: any = {};
    const html = renderToString(<App appConfig={appConfig} messageBus={dummyMessageBus}/>);

    return html;
    // Alternatively (supports Composite Apps)
    /*
    return {
        html,
        embeddedApps: [],
    }
    */
};

export default bootstrap;

The portalAppSetup has the following structure:

export type MashroomPortalAppSetup = {
    readonly appId: string;
    readonly title: string | null | undefined;
    readonly proxyPaths: MashroomPortalProxyPaths;
    // Legacy, will be removed in Mashroom v3
    readonly restProxyPaths: MashroomPortalProxyPaths;
    readonly resourcesBasePath: string;
    readonly globalLaunchFunction: string;
    readonly lang: string;
    readonly user: MashroomPortalAppUser;
    readonly appConfig: MashroomPluginConfig;
}
    export type MashroomPortalAppUser = {
        readonly guest: boolean;
        readonly username: string;
        readonly displayName: string;
        readonly email: string | null;
        readonly permissions: MashroomPortalAppUserPermissions;
        readonly [customProp: string]: any;
    }

The clientServices argument contains the client services, see below.

Client Services

The following client side services are available for all portal apps:

MashroomPortalMessageBus

export interface MashroomPortalMessageBus {
    /**
     * Subscribe to given topic.
     * Topics starting with getRemotePrefix() will be subscribed server side via WebSocket (if available).
     * Remote topics can also contain wildcards: # for multiple levels and + or * for a single level
     * (e.g. remote:/foo/+/bar)
     */
    subscribe(topic: string, callback: MashroomPortalMessageBusSubscriberCallback): Promise<void>;

    /**
     * Subscribe once to given topic. The handler will be removed after the first message has been received.
     * Remote topics are accepted.
     */
    subscribeOnce(topic: string, callback: MashroomPortalMessageBusSubscriberCallback): Promise<void>;

    /**
     * Unsubscribe from given topic.
     * Remote topics are accepted.
     */
    unsubscribe(topic: string, callback: MashroomPortalMessageBusSubscriberCallback): Promise<void>;

    /**
     * Publish to given topic.
     * Remote topics are accepted.
     */
    publish(topic: string, data: any): Promise<void>;

    /**
     * Get the private user topic for the given user or the currently authenticated user if no argument given.
     * You can subscribe to "sub" topics as well, e.g. <private_topic>/foo
     */
    getRemoteUserPrivateTopic(username?: string): string | null | undefined;

    /**
     * The prefix for remote topics
     */
    getRemotePrefix(): string;

    /**
     * Register a message interceptor.
     * An interceptor can be useful for debugging or to manipulate the messages.
     * It can change the data of an event by return a different value or block messages
     * by calling cancelMessage() from the interceptor arguments.
     */
    registerMessageInterceptor(interceptor: MashroomPortalMessageBusInterceptor): void;

    /**
     * Unregister a message interceptor.
     */
    unregisterMessageInterceptor(interceptor: MashroomPortalMessageBusInterceptor): void;
}

MashroomPortalStateService

export interface MashroomPortalStateService {
    /**
     * Get a property from state.
     * It will be looked up in the URL (query param or encoded) and in the local and session storage
     */
    getStateProperty(key: string): any | null | undefined;

    /**
     * Add given key value pair into the URL (encoded)
     */
    setUrlStateProperty(key: string, value: any | null | undefined): void;

    /**
     * Add given key value pair to the session storage
     */
    setSessionStateProperty(key: string, value: any): void;

    /**
     * Add given key value pair to the local storage
     */
    setLocalStoreStateProperty(key: string, value: any): void;
}

MashroomPortalAppService

export interface MashroomPortalAppService {
    /**
     * Get all existing apps
     */
    getAvailableApps(): Promise<Array<MashroomAvailablePortalApp>>;

    /**
     * Load portal app to given host element at given position (or at the end if position is not set)
     *
     * The returned promise will always resolve! If there was a loading error the MashroomPortalLoadedPortalApp.error property will be true.
     */
    loadApp(appAreaId: string, pluginName: string, instanceId: string | null | undefined, position?: number | null | undefined, overrideAppConfig?: any | null | undefined): Promise<MashroomPortalLoadedPortalApp>;

    /**
     * Load portal app into a modal overlay.
     *
     * The returned promise will always resolve! If there was a loading error the MashroomPortalLoadedPortalApp.error property will be true.
     */
    loadAppModal(pluginName: string, title?: string | null | undefined, overrideAppConfig?: any | null | undefined, onClose?: ModalAppCloseCallback | null | undefined): Promise<MashroomPortalLoadedPortalApp>;

    /**
     * Reload given portal app
     *
     * The returned promise will always resolve!
     * If there was a loading error the MashroomPortalLoadedPortalApp.error property will be true.
     */
    reloadApp(id: string, overrideAppConfig?: any | null | undefined): Promise<MashroomPortalLoadedPortalApp>;

    /**
     * Unload given portal app
     */
    unloadApp(id: string): void;

    /**
     * Move a loaded app to another area (to another host element within the DOM)
     */
    moveApp(id: string, newAppAreaId: string, newPosition?: number): void;

    /**
     * Show the name and version for all currently loaded apps in a overlay (for debug purposes)
     */
    showAppInfos(customize?: (portalApp: MashroomPortalLoadedPortalApp, overlay: HTMLDivElement) => void): void;

    /**
     * Hide all app info overlays
     */
    hideAppInfos(): void;

    /**
     * Add listener for load events (fired after an app has been loaded an attached to the page)
     */
    registerAppLoadedListener(listener: MashroomPortalAppLoadListener): void;

    /**
     * Remove listener for load events
     */
    unregisterAppLoadedListener(listener: MashroomPortalAppLoadListener): void;

    /**
     * Add listener for unload events (fired before an app will been detached from the page)
     */
    registerAppAboutToUnloadListener(listener: MashroomPortalAppLoadListener,): void;

    /**
     * Remove listener for unload events
     */
    unregisterAppAboutToUnloadListener(listener: MashroomPortalAppLoadListener,): void;

    /**
     * Load the setup for given app/plugin name on the current page
     */
    loadAppSetup(pluginName: string, instanceId: string | null | undefined): Promise<MashroomPortalAppSetup>;

    /**
     * Get some stats about a loaded App
     */
    getAppStats(pluginName: string): MashroomPortalLoadedPortalAppStats | null;

    /**
     * Check if some loaded Portal Apps have been update (and have a different version on the server).
     * This can be used to check if the user should refresh the current page.
     *
     * Returns the list of upgraded Apps.
     */
    checkLoadedPortalAppsUpdated(): Promise<Array<string>>;

    /**
     * Prefetch resources of given app/plugin. This is useful if you know which apps you will have to load
     * in the future and want to minimize the loading time.
     */
    prefetchResources(pluginName: string): Promise<void>;

    readonly loadedPortalApps: Array<MashroomPortalLoadedPortalApp>;
}

MashroomPortalUserService

export interface MashroomPortalUserService {
    /**
     * Get the authentication expiration time in unix time ms.
     * Returns null if the check fails and "0" if the check returns 403.
     */
    getAuthenticationExpiration(): Promise<number | null>;

    /**
     * Get the unix ms left until authentication expiration.
     * Returns null if the check fails and "0" if the check returns 403.
     */
    getTimeToAuthenticationExpiration(): Promise<number | null>;

    /**
     * Extend the authentication.
     * Can be used to update the authentication when no server interaction has occurred for a while and the authentication is about to expire.
     */
    extendAuthentication(): void;

    /**
     * Logout the current user
     */
    logout(): Promise<void>;

    /**
     * Get the current user's language
     */
    getUserLanguage(): string;

    /**
     * Set the new user language
     */
    setUserLanguage(lang: string): Promise<void>;

    /**
     * Get all available languages (e.g. en, de)
     */
    getAvailableLanguages(): Promise<Array<string>>;

    /**
     * Get the configured default language
     */
    getDefaultLanguage(): Promise<string>;
}

MashroomPortalSiteService

export interface MashroomPortalSiteService {
    /**
     * Get the base url for the current site
     */
        getCurrentSiteUrl(): string;

    /**
     * Get a list with all sites
     */
        getSites(): Promise<Array<MashroomPortalSiteLinkLocalized>>;

    /**
     * Get the page tree for given site
     */
        getPageTree(siteId: string): Promise<Array<MashroomPortalPageRefLocalized>>;
}

MashroomPortalPageService

export interface MashroomPortalPageService {
    /**
     * Get current pageId
     */
    getCurrentPageId(): string;
    /**
     * Get the page friendlyUrl from given URL (e.g. /portal/web/test?x=1 -> /test)
     */
    getPageFriendlyUrl(pageUrl: string): string;
    /**
     * Find the pageId for given URL (can be a page friendlyUrl or a full URL as seen by the client).
     */
    getPageId(pageUrl: string): Promise<string | undefined>;
    /**
     * Get the content for given pageId.
     * It also calculates if the correct theme and all necessary page enhancements for the requested page
     * are already loaded. Otherwise fullPageLoadRequired is going to be true and no content returned.
     */
    getPageContent(pageId: string): Promise<MashroomPortalPageContent>;
}

MashroomPortalRemoteLogger

export interface MashroomPortalRemoteLogger {
    /**
     * Send a client error to the server log
     */
    error(msg: string, error?: Error): void;

    /**
     * Send a client warning to the server log
     */
    warn(msg: string, error?: Error): void;

    /**
     * Send a client info to the server log
     */
    info(msg: string): void;
}

MashroomPortalAdminService

export interface MashroomPortalAdminService {
    /**
     * Get all existing themes
     */
    getAvailableThemes(): Promise<Array<MashroomAvailablePortalTheme>>;

    /**
     * Get all existing layouts
     */
    getAvailableLayouts(): Promise<Array<MashroomAvailablePortalLayout>>;

    /**
     * Get all currently existing roles
     */
    getExistingRoles(): Promise<Array<RoleDefinition>>;

    /**
     * Get all app instances on current page
     */
    getAppInstances(): Promise<Array<MashroomPagePortalAppInstance>>;

    /**
     * Add an app to the current page.
     */
    addAppInstance(pluginName: string, areaId: string, position?: number, appConfig?: any): Promise<MashroomPagePortalAppInstance>;

    /**
     * Update given app instance config or position
     */
    updateAppInstance(pluginName: string, instanceId: string, areaId: string | null | undefined, position: number | null | undefined, appConfig: any | null | undefined): Promise<void>;

    /**
     * Remove given app instance from page
     */
    removeAppInstance(pluginName: string, instanceId: string): Promise<void>;

    /**
     * Get roles that are permitted to view the app (no roles means everyone is permitted)
     */
    getAppInstancePermittedRoles(pluginName: string, instanceId: string): Promise<string[] | null | undefined>;

    /**
     * Update roles that are permitted to view the app (undefined or null means everyone is permitted)
     */
    updateAppInstancePermittedRoles(pluginName: string, instanceId: string, roles: string[] | null | undefined): Promise<void>;

    /**
     * Get current pageId
     */
    getCurrentPageId(): string;

    /**
     * Get page data
     */
    getPage(pageId: string): Promise<MashroomPortalPage>;

    /**
     * Add new page
     */
    addPage(page: MashroomPortalPage): Promise<MashroomPortalPage>;

    /**
     * Update an existing page
     */
    updatePage(page: MashroomPortalPage): Promise<void>;

    /**
     * Delete the given page
     */
    deletePage(pageId: string): Promise<void>;

    /**
     * Get roles that are permitted to view the page (no roles means everyone is permitted)
     */
    getPagePermittedRoles(pageId: string): Promise<string[] | null | undefined>;

    /**
     * Update roles that are permitted to view the page (undefined or null means everyone is permitted)
     */
    updatePagePermittedRoles(pageId: string, roles: string[] | null | undefined): Promise<void>;

    /**
     * Get current siteId
     */
    getCurrentSiteId(): string;

    /**
     * Get site with given id
     */
    getSite(siteId: string): Promise<MashroomPortalSite>;

    /**
     * Add new site
     */
    addSite(site: MashroomPortalSite): Promise<MashroomPortalSite>;

    /**
     * Update existing site
     */
    updateSite(site: MashroomPortalSite): Promise<void>;

    /**
     * Delete the given site
     */
    deleteSite(siteId: string): Promise<void>;

    /**
     * Get roles that are permitted to view the site (no roles means everyone is permitted)
     */
    getSitePermittedRoles(siteId: string): Promise<string[] | null | undefined>;

    /**
     * Update roles that are permitted to view the site (undefined or null means everyone is permitted)
     */
    updateSitePermittedRoles(siteId: string, roles: string[] | null | undefined): Promise<void>;
}
portal-theme

This plugin types adds a theme to the Portal.

To register a new portal-theme plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
           {
                "name": "My Theme",
                "type": "portal-theme",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "resourcesRoot": "./dist",
                "views": "./views",
                "defaultConfig": {
                    "param1": true
                 }
            }
        ]
     }
}

Since Mashroom Portal uses the Express render mechanism all Template Engines supported by Express can be used to define the template. The bootstrap returns the template engine and the engine name like so:

import {engine} from 'express-handlebars';
import path from 'path';

import type {MashroomPortalThemePluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalThemePluginBootstrapFunction = async () => {
    return {
        engineName: 'handlebars',
        engineFactory: () => {
            return engine({
                partialsDir: path.resolve(__dirname, '../views/partials/'),
            });
        },
    };
};

export default bootstrap;

NOTE: Even if Express.js could automatically load the template engine (like for Pug) you have to provide the engineFactory here, otherwise plugin local modules can not be loaded. In that case define the engineFactory like this:
engineFactory: () => require('pug').__express

The theme can contain the following views:

A typical portal view with Handlebars might look like this:

<!doctype html>
<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <meta name="description" content="{{model.page.description}}">
    <meta name="keywords" content="{{model.page.keywords}}">
    {{#if csrfToken}}
        <meta name="csrf-token" content="{{csrfToken}}">
    {{/if}}

    <title>{{site.title}} - {{page.title}}</title>

    <link rel="stylesheet" type="text/css" href="{{resourcesBasePath}}/style.css">

    {{{portalResourcesHeader}}}

    {{#if page.extraCss}}
        <style>
            {{{page.extraCss}}}
        </style>
    {{/if}}
</head>
<body>
    <div id="mashroom-portal-admin-app-container">
        <!-- Admin app goes here -->
    </div>

    <header>
        <div class="site-name">
            <h1>{{site.title}}</h1>
        </div>
    </header>

    <main>
        {{> navigation}}

        <div id="portal-page-content" class="mashroom-portal-apps-container container-fluid">
            {{{pageContent}}}
        </div>
    </main>

    <div id="mashroom-portal-modal-overlay">
        <div class="mashroom-portal-modal-overlay-wrapper">
            <div class="mashroom-portal-modal-overlay-header">
                <div id="mashroom-portal-modal-overlay-title">Title</div>
                <div id="mashroom-portal-modal-overlay-close" class="close-button"></div>
            </div>
            <div class="mashroom-portal-modal-overlay-content">
                <div id="mashroom-portal-modal-overlay-app">
                    <!-- Modal apps go here -->
                </div>
            </div>
        </div>
    </div>

    <div id="mashroom-portal-auth-expires-warning">
        <div class="mashroom-portal-auth-expires-warning-message">
            {{{__ messages "authenticationExpiresWarning"}}}
        </div>
    </div>

    {{{portalResourcesFooter}}}
</body>
</html>

The pageContent variable contains the actual content with the Portal layout (see below) and the Apps.

Here all available variables:

export type MashroomPortalPageRenderModel = {
    readonly portalName: string;
    readonly siteBasePath: string;
    readonly apiBasePath: string;
    readonly resourcesBasePath: string | null | undefined;
    readonly site: MashroomPortalSiteLocalized;
    readonly page: MashroomPortalPage & MashroomPortalPageRefLocalized;
    readonly portalResourcesHeader: string;
    readonly portalResourcesFooter: string;
    readonly pageContent: string;
    // @Deprecated, use pageContent; will be removed in 3.0
    readonly portalLayout: string;
    readonly lang: string;
    readonly availableLanguages: Readonly<Array<string>>;
    readonly messages: (key: string) => string;
    readonly user: MashroomPortalUser;
    readonly csrfToken: string | null | undefined;
    readonly userAgent: UserAgent;
    readonly lastThemeReloadTs: number;
    readonly themeVersionHash: string;
}
portal-layouts

This plugin type adds portal layouts to the portal. A layout defines a areas where portal-apps can be placed.

To register a new portal-layouts plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
           {
                "name": "My Layouts",
                "type": "portal-layouts",
                "layouts": {
                    "1 Column": "./layouts/1column.html",
                    "2 Columns": "./layouts/2columns.html",
                    "2 Columns 70/30": "./layouts/2columns_70_30.html",
                    "2 Columns with 1 Column Header": "./layouts/2columnsWith1columnHeader.html"
                }
            }
        ]
     }
}

A layout looks like this:

<div class="row">
    <div class="col-md-8 mashroom-portal-app-area" id="app-area1">
        <!-- Portal apps go here -->
    </div>
    <div class="col-md-4 mashroom-portal-app-area" id="app-area2">
        <!-- Portal apps go here -->
    </div>
</div>

Important is the class mashroom-portal-app-area and a unique id element.

remote-portal-app-registry

This plugin type adds a registry for remote portal-apps to the Portal.

To register a new remote-portal-app-registry plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
           {
                "name": "Mashroom Portal Remote App Registry",
                "type": "remote-portal-app-registry",
                "bootstrap": "./dist/registry/mashroom-bootstrap-remote-portal-app-registry.js",
                "defaultConfig": {
                    "priority": 100
                }
            }
        ]
     }
}

And the bootstrap must return an implementation of RemotePortalAppRegistry:

import {MyRegistry} from './MyRegistry';

import type {MashroomRemotePortalAppRegistryBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomRemotePortalAppRegistryBootstrapFunction = async (pluginName, pluginConfig, pluginContext) => {
    return new MyRegistry();
};

export default bootstrap;

The plugin must implement the following interface:

export interface MashroomRemotePortalAppRegistry {
    readonly portalApps: Readonly<Array<MashroomPortalApp>>;
}

h3.

portal-page-enhancement

This plugin type allows it to add extra resources (JavaScript and CSS) to a Portal page based on some (optional) rules. This can be used to add polyfills or some analytics stuff without the need to change a theme.

To register a new portal-page-enhancement plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
           {
                "name": "My Portal Page Enhancement",
                "type": "portal-page-enhancement",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "pageResources": {
                    "js": [{
                        "path": "my-extra-scripts.js",
                        "rule": "includeExtraScript",
                        "location": "header",
                        "inline": false
                    }, {
                        "dynamicResource": "myScript",
                        "location": "header"
                    }],
                    "css": []
                },
                "defaultConfig": {
                    "order": 100,
                    "resourcesRoot": "./dist/public"
                }
            }
        ]
     }
}

The bootstrap returns a map of rules and could look like this:

import type {MashroomPortalPageEnhancementPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalPageEnhancementPluginBootstrapFunction = () => {
    return {
        dynamicResources: {
            myScript: () => `console.info('My Script loaded');`,
        },
        rules: {
            // Example rule: Show only for IE
            includeExtraScript: (sitePath, pageFriendlyUrl, lang, userAgent) => userAgent.browser.name === 'IE',
        }
    }
};

export default bootstrap;

The JavaScript or CSS resource can also be generated dynamically by the plugin. In that case it will always be inlined. To use this state a dynamicResource name instead of a path and include the function that actually generates the content to the object returned by the bootstrap:

{
     "mashroom": {
        "plugins": [
           {
                "name": "My Portal Page Enhancement",
                "type": "portal-page-enhancement",
                "bootstrap": "./dist/mashroom-bootstrap.js",
                "pageResources": {
                    "js": [{
                        "dynamicResource": "extraScript",
                        "location": "header"
                    }],
                    "css": []
                }
            }
        ]
     }
}
import type {MashroomPortalPageEnhancementPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalPageEnhancementPluginBootstrapFunction = () => {
    return {
        dynamicResources: {
            extraScript: (sitePath, pageFriendlyUrl, lang, userAgent) => `console.info('test');`,
        }
    }
};

export default bootstrap;
portal-app-enhancement

This plugin type allows it to update or rewrite the portalAppSetup that is passed to Portal Apps at startup. This can be used to add extra config or user properties from a context. Additionally, this plugin allows it to pass extra clientServices to Portal Apps or replace one of the default ones.

To register a new portal-app-enhancement plugin add this to package.json:

{
     "mashroom": {
        "plugins": [
           {
              "name": "My Portal App Enhancement",
              "type": "portal-app-enhancement",
              "bootstrap": "./dist/mashroom-bootstrap.js",
              "portalCustomClientServices": {
                  "customService": "MY_CUSTOM_SERVICE"
              }
           }
        ]
     }
}

The bootstrap returns the actual enhancer plugin:

import MyPortalAppEnhancementPlugin from './MyPortalAppEnhancementPlugin';
import type {MashroomPortalAppEnhancementPluginBootstrapFunction} from '@mashroom/mashroom-portal/type-definitions';

const bootstrap: MashroomPortalAppEnhancementPluginBootstrapFunction = () => {
    return new MyPortalAppEnhancementPlugin();
};

export default bootstrap;

The plugin has to implement the following interface:

export interface MashroomPortalAppEnhancementPlugin {
    /**
     * Enhance the portalAppSetup object passed as the first argument (if necessary)
     */
    enhancePortalAppSetup: (portalAppSetup: MashroomPortalAppSetup, portalApp: MashroomPortalApp, request: Request) => Promise<MashroomPortalAppSetup>;
}

Mashroom Portal Default Layouts

This plugin adds some default layouts to the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-default-layouts as dependency.

Mashroom Portal App User ExtraData

This plugin copies the property *extraData" from the server user object to the Portal App user. This is useful if the security provider adds some extra information (such as the phone number) and you want to use it in a Portal App.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-app-user-extradata as dependency.

Mashroom Portal Default Theme

This plugins adds the default theme for the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-default-theme as dependency.

You can override the default config in your Mashroom config file like this:

{
    "plugins": {
        "Mashroom Portal Default Theme": {
            "spaMode": true,
            "showPortalAppHeaders": true,
            "showEnvAndVersions": true
        }
    }
}

Mashroom Portal Admin App

This plugin contains the default Admin Toolbar for the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-admin-app as dependency.

To enable it add the following to the Mashroom Portal config:

{
  "plugins": {
        "Plugin: Mashroom Portal WebApp": {
            "adminApp": "Mashroom Portal Admin App"
        }
    }
}

Mashroom Portal Tabify App

This Mashroom Portal App turns any app area where it is placed automatically into a tabbed container.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-tabify-app as dependency.

After placing it on a page use the Portal Admin Toolbar to set the following properties:

Updates via MessageBus

It is possible to change the App/Title mapping and to show a specific Tab via MessageBus. This is especially useful for dynamic cockpits where you load Apps programmatically via MashroomPortalAppService.

Available topics:

tabify-add-plugin-name-title-mapping expect a message like this:

{
    pluginName: 'My App',
    title: 'Another title'
}

tabify-add-app-id-title-mapping expect a message like this:

{
    appId: '1234123',
    title: 'Another title'
}

And tabify-focus-app expects just an id:

{
    appId: '1234123'
}

Mashroom Portal iFrame App

Adds a (responsive) iFrame Portal App to the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-iframe-app as dependency.

After placing it on a page use the Portal Admin Toolbar to set the following properties:

To make the iFrame responsive, the embedded page has to post its height via message like so:

var lastContentHeight = null;

function sendHeight() {
    var contentHeight = document.getElementById('content-wrapper').offsetHeight;
    if (lastContentHeight !== contentHeight) {
        parent.postMessage({
            height: contentHeight + /* margin */ 20
        }, "*");
        lastContentHeight = contentHeight;
    }
}

setInterval(sendHeight, 1000);

Mashroom Portal Remote App Registry

This plugin adds a remote app registry to Mashroom Portal, which scans periodically a list of remote servers for Portal Apps. It expects the package.json and optionally an external plugin config file (default mashroom.json) to be exposed at /. It also expects a remote config in the plugin definition, like this:

 {
    "name": "My Single Page App",
    "remote": {
        "resourcesRoot": "/public",
         "ssrInitialHtmlPath": "/ssr"
    }
 }

You can find an example remote app here: Mashroom Demo Remote Portal App.

This plugin also comes with an Admin UI extension (/mashroom/admin/ext/remote-portal-apps) and a REST API to add and remote URL's. The Admin UI allows adding a URL temporary only for the current session.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-remote-app-registry as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
      "Mashroom Portal Remote App Background Job": {
          "cronSchedule": "0/1 * * * *",
          "socketTimeoutSec": 3,
          "registrationRefreshIntervalSec": 600,
          "unregisterAppsAfterScanErrors": -1
      },
      "Mashroom Portal Remote App Registry": {
          "remotePortalAppUrls": "./remotePortalApps.json"
      },
      "Mashroom Portal Remote App Registry Admin Webapp": {
          "showAddRemoteAppForm": true
      }
  }
}

The config file contains just a list of URLs:

{
    "$schema": "https://www.mashroom-server.com/schemas/mashroom-portal-remote-apps.json",
    "remotePortalApps": [
        "http://demo-remote-app.mashroom-server.com"
    ]
}

The Service can be used like this:

import type {MashroomPortalRemoteAppEndpointService} from '@mashroom/mashroom-portal-remote-app-registry/type-definitions';

export default async (req: Request, res: Response) => {
    const remoteAppService: MashroomPortalRemoteAppEndpointService = req.pluginContext.services.remotePortalAppEndpoint.service;

    const remoteApps = await remoteAppService.findAll();

    // ...
}

The REST API can be used like this:

Available at /portal-remote-app-registry/api. Methods:

Services

MashroomPortalRemoteAppEndpointService

The exposed service is accessible through pluginContext.services.remotePortalAppEndpoint.service

Interface:

export interface MashroomPortalRemoteAppEndpointService {
    /**
     * Register a new Remote App URL
     */
    registerRemoteAppUrl(url: string): Promise<void>;

    /**
     * Register a Remote App URL only for the current session (useful for testing)
     */
    synchronousRegisterRemoteAppUrlInSession(
        url: string,
        request: Request,
    ): Promise<void>;

    /**
     * Unregister a Remote App
     */
    unregisterRemoteAppUrl(url: string): Promise<void>;

    /**
     * Find Remote App by URL
     */
    findRemotePortalAppByUrl(
        url: string,
    ): Promise<RemotePortalAppEndpoint | null | undefined>;

    /**
     * Return all known Remote App endpoints
     */
    findAll(): Promise<Readonly<Array<RemotePortalAppEndpoint>>>;

    /**
     * Update an existing Remote App endpoint
     */
    updateRemotePortalAppEndpoint(
        remotePortalAppEndpoint: RemotePortalAppEndpoint,
    ): Promise<void>;

    /**
     * Refresh (fetch new metadata) from given endpoint
     */
    refreshEndpointRegistration(
        remotePortalAppEndpoint: RemotePortalAppEndpoint,
    ): Promise<void>;
}

Mashroom Portal Remote App Registry for Kubernetes

Adds a remote app registry to Mashroom Portal which periodically scans Kubernetes services that expose Remote Portal Apps. It expects the package.json and optionally an external plugin config file (default mashroom.json) to be exposed at /. It also expects a remote config in the plugin definition, like this:

 {
    "name": "My Single Page App",
    "remote": {
        "resourcesRoot": "/public",
         "ssrInitialHtmlPath": "/ssr"
    }
 }

You can find an example remote app here: Mashroom Demo Remote Portal App.

This plugin also comes with an Admin UI extension (/mashroom/admin/ext/remote-portal-apps-k8s) that can be used to check all registered Apps.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-remote-app-registry-k8s as dependency.

You can override the default config in your Mashroom config file like this:

{
  "plugins": {
      "Mashroom Portal Remote App Kubernetes Background Job": {
          "cronSchedule": "0/1 * * * *",
          "k8sNamespacesLabelSelector": null,
          "k8sNamespaces": ["default"],
          "k8sServiceLabelSelector": null,
          "serviceNameFilter": "(microfrontend-|widget-)",
          "socketTimeoutSec": 3,
          "refreshIntervalSec": 600,
          "unregisterAppsAfterScanErrors": -1,
          "accessViaClusterIP": false,
          "serviceProcessingBatchSize": 20
        }
    }
}

The list of successful registered services will be available on http://<host>:<port>/portal-remote-app-registry-kubernetes

A more complex example

Select all services with label microfrontend=true and not label channel=alpha in all namespaces with label environment=development and tier=frontend:

{
  "plugins": {
      "Mashroom Portal Remote App Kubernetes Background Job": {
          "k8sNamespacesLabelSelector": ["environment=development,tier=frontend"],
          "k8sNamespaces": null,
          "k8sServiceLabelSelector": ["microfrontend=true,channel!=alpha"]
        }
    }
}
Priority

In case of duplicate Portal Apps the one that appears first in the list of namespaces is taken. For a configuration like this:

{
  "k8sNamespacesLabelSelector": ["environment=hotfix", "environment=prod"],
  "k8sNamespaces": ["namespace2"]
}

the order is:

Setup Kubernetes access

In order to allow Mashroom to fetch services for given namespaces you need to attach a Kubernetes Service Account with the correct permissions to the deployment.

Create a role with the required permissions like this:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: list-namespaces-services-cluster-role
rules:
  - apiGroups:
      - ""
    resources:
      - services
      - namespaces
    verbs:
      - get
      - list

And then create the Service Account and bind the role (we use a ClusterRoleBinding here so the account can read services in all namespaces in the cluster, if you don't want that, you have to create a RoleBinding per allowed namespace):

apiVersion: v1
kind: ServiceAccount
metadata:
  name: mashroom-portal
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: mashroom-portal-role-binding
subjects:
  - kind: ServiceAccount
    name: mashroom-portal
    namespace: default
roleRef:
  kind: ClusterRole
  name: list-namespaces-services-cluster-role
  apiGroup: rbac.authorization.k8s.io

And in your deployment resource just state the Service Account name:

apiVersion: apps/v1
kind: Deployment
metadata:
    name: mashroom-portal
    namespace: default
spec:
    # ...
    template:
        # ...
        spec:
            containers:
                - name: mashroom-portal
                # ...
            serviceAccountName: mashroom-portal

Mashroom Portal Sandbox App

This Mashroom Portal App can be used to load and test any Portal App with a specific configuration and to interact with the App via Message Bus. It can also be used for end-2-end testing with tools such as Selenium.

Usage

The app supports the following query parameters:

btoa(JSON.stringify({
    permissionA: true
}))

For an example how to use the sandbox in an end-2-end test see: https://github.com/nonblocking/mashroom-portal-quickstart/tree/master/plugin-packages/example-react-app/test-e2e/example.test.js

Mashroom Portal Remote Messaging App

This Mashroom Portal App can be used to test remote messaging in the Mashroom Portal. This App requires the mashroom-messaging and mashroom-websocket plugins to be installed.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-remote-messaging as dependency.

After adding the app to a page you can send a message to another user (or another browser tab) by using the (remote) topic user/<other-username>/. And the app will automatically subscribe the topic user//# to receive all user messages.

Demo Plugin Documentation

Mashroom Demo Webapp

This a simple demo Express webapp which can be developed and run standalone, but also be integrated into Mashroom Server on an arbitrary (configurable) path.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-demo-webapp as dependency.

After that the webapp will be available at /demo/webapp

You can change the path by overriding it in your Mashroom config file like this:

{
  "plugins": {
        "Mashroom Demo Webapp": {
            "path": '/my/path'
        }
    }
}

Mashroom Portal Demo Alternative Theme

This is an alternative demo theme for the Mashroom Portal.

This theme demonstrates how to create a type safe theme with express-react-views and TypeScript.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-alternative-theme as dependency.

Mashroom Portal Demo React App 2

This is another simple React based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.

This SPA also supports server side rendering and demonstrates how a custom editor can be used for the Portal App configuration (instead of the default JSON editor).

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-react-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo React App

This is a simple React based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-react-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo Angular App

This is a simple Angular based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-angular-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Implementation hints

Angular was clearly designed for monolithic web applications that use the whole browser window. So, to make it more behave like a Microfrontend you have to change a few things in the template generated by the Angular CLI:

Mashroom Portal Demo Vue App

This is a simple Vue based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-vue-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo Svelte App

This is a simple Svelte based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-svelte-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo SolidJS App

This is a simple SolidJS based SPA which can be developed and run standalone, but can also act as a building block in the Mashroom Portal.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-solidjs-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo Rest Proxy App

This is a simple SPA that demonstrates how the Mashroom Portal proxy could be used to connect to a REST API that cannot be reached directly by the client.

It fetches data rocketlaunch.live, but connects through the Portal. So the actual endpoint will not be visible in the browser.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-rest-proxy-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo WebSocket Proxy App

This is a simple SPA that demonstrates how the Mashroom Portal proxy can be used to connect to a WebSocket server that cannot be reached directly by the client.

By default, it connects to an echo server on ws://ws.ifelse.io/, but that server might go down any time. If you can't connect, you can always launch a local WebSocket server (like https://github.com/pmuellr/ws-echo) and change the targetUri in package.json accordingly.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-rest-proxy-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo Load Dynamically App

This is a simple SPA that demonstrates how an App registered to the Mashroom Portal can load and unload other Portal Apps on a page with a specific config.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-load-dynamically-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

Mashroom Portal Demo Composite App

This is a simple SPA that uses other SPAs (which are registered to the Mashroom Portal) as building blocks. We call this a *Composite App, and it could again be a building block for other Composite Apps.

The SPA itself is written in React but is uses other ones implemented with Vue.js, Angular and Svelte to build a dialog. It is capable of server-side rendering, which includes the embedded Apps.

Usage

If node_modules/@mashroom is configured as plugin path just add @mashroom/mashroom-portal-demo-composite-app as dependency.

Then you can place it on any page via Portal Admin Toolbar.

3rd Party Plugins