npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@majkapp/majk-plugin-server

v1.4.0

Published

Lightweight HTTP server for serving MAJK plugins in single-server architecture

Readme

@majkapp/majk-plugin-server

Lightweight HTTP server for serving MAJK plugins in single-server architecture.

Table of Contents

Features

  • Serves multiple plugins from a single Express server
  • Routes: /plugins/{org}/{id}/ui/* for static assets, /plugins/{org}/{id}/api/{functionName} for function calls
  • Configurable asset path prefix: Optionally serve assets via custom prefix (e.g., /plugin-screens/{pluginId}/*)
  • SPA routing support: Falls back to index.html for non-existent UI routes
  • Runtime plugin management: Register/unregister plugins without restarting server with full URL information
  • Token-based authentication: Optional auth with token validation and HTTP-only cookies
  • Function middleware: Pre/post-processing hooks for function invocations with authorization errors
  • CORS configuration: CORS disabled by default, opt-in when needed
  • Comprehensive request logging: Logs ALL requests including 404s with status codes and durations
  • Rich response objects: start() and registerPlugin() return complete URL information for each plugin
  • Event emission for monitoring and debugging
  • TypeScript with full type definitions
  • Single-file implementation for simplicity

Installation

npm install @majkapp/majk-plugin-server

Quick Start

Basic Server

import { createServer } from '@majkapp/majk-plugin-server';

const server = createServer({
  port: 3000,
  plugins: [
    {
      org: 'myorg',
      id: 'my-plugin',
      distPath: './plugins/my-plugin/dist',
      capabilities: {}
    }
  ],
  functionInvoker: async (org, id, functionName, params) => {
    console.log(`Invoking ${org}/${id}.${functionName}`);
    return { success: true, data: 'result' };
  }
});

const result = await server.start();
console.log(`Server running on ${result.baseUrl}`);
console.log('Plugins:', result.plugins);

Production-Ready Server

Complete example with all features enabled:

import {
  createServer,
  ForbiddenError,
  UnauthorizedError,
  InsufficientPermissionsError
} from '@majkapp/majk-plugin-server';
import jwt from 'jsonwebtoken';

const server = createServer({
  // Auto-find free port
  port: 0,

  // Configure plugins
  plugins: [
    {
      org: 'myorg',
      id: 'dashboard',
      distPath: './plugins/dashboard/dist',
      capabilities: { screens: ['main'], functions: ['getData'] }
    },
    {
      org: 'myorg',
      id: 'admin',
      distPath: './plugins/admin/dist',
      capabilities: { screens: ['settings'], functions: ['deleteData'] }
    }
  ],

  // Function invocation handler
  functionInvoker: async (org, id, functionName, params) => {
    // Route to actual plugin implementation
    const plugin = await import(`./plugins/${id}/index.js`);
    return await plugin[functionName](params);
  },

  // Authentication with JWT
  auth: {
    enabled: true,
    tokenValidator: async (token) => {
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        return decoded !== null;
      } catch (error) {
        return false;
      }
    },
    cookieHttpOnly: true,
    cookieSameSite: 'strict',
    cookieMaxAge: 24 * 60 * 60 * 1000 // 24 hours
  },

  // Function middleware for authorization
  functionMiddleware: {
    beforeInvoke: async (context) => {
      // Extract user from JWT in Authorization header
      const authHeader = context.request.headers.authorization;
      if (!authHeader?.startsWith('Bearer ')) {
        throw new UnauthorizedError('Missing authorization header');
      }

      const token = authHeader.substring(7);
      const user = jwt.verify(token, process.env.JWT_SECRET);

      // Check role-based permissions
      if (context.functionName.startsWith('delete') && user.role !== 'admin') {
        throw new ForbiddenError('Only admins can delete data');
      }

      // Add user context to params
      return {
        params: { ...context.params, userId: user.id, userRole: user.role },
        metadata: { user, startTime: Date.now() }
      };
    },

    afterInvoke: async (context, result, metadata) => {
      // Log execution time
      const duration = Date.now() - metadata.startTime;
      console.log(`${context.functionName} executed in ${duration}ms`);

      // Filter sensitive data for non-admins
      if (metadata.user.role !== 'admin' && result.data) {
        delete result.data.internalFields;
      }

      return result;
    }
  },

  // Event monitoring
  eventEmitter: async (event) => {
    // Log important events
    if (event.type.includes('error') || event.type.includes('unauthorized')) {
      console.error(`❌ ${event.type}:`, event.data);
    } else if (event.type.includes('success')) {
      console.log(`✅ ${event.type}:`, event.data);
    }

    // Send to monitoring service
    if (process.env.MONITORING_ENABLED) {
      await fetch(process.env.MONITORING_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ timestamp: new Date(), ...event })
      });
    }
  },

  // Request logging
  maxRequestLogSize: 1000,

  // CORS disabled for security
  cors: false
});

// Start server and get complete server information
const result = await server.start();
console.log(`🚀 Server running on ${result.baseUrl}`);
console.log(`   Serving ${result.plugins.length} plugin(s):`);
result.plugins.forEach(p => {
  console.log(`   - ${p.org}/${p.id}: ${p.urls.ui}`);
});

// Runtime plugin management
export async function hotReloadPlugin(org: string, id: string, distPath: string) {
  await server.unregisterPlugin(org, id);
  await server.registerPlugin({ org, id, distPath, capabilities: {} });
  console.log(`Plugin ${org}/${id} reloaded`);
}

// Access request logs
setInterval(() => {
  const logs = server.getRequestLog();
  console.log(`Recent requests: ${logs.length}`);
}, 60000);

Minimal Server (Development)

import { createServer } from '@majkapp/majk-plugin-server';

const server = createServer({
  plugins: [
    {
      org: 'dev',
      id: 'test-plugin',
      distPath: './dist',
      capabilities: {}
    }
  ],
  functionInvoker: async (org, id, fn, params) => {
    return { success: true, data: `Called ${fn}` };
  }
});

const result = await server.start();
console.log(`Dev server: ${result.plugins[0].urls.ui}`);

API

Server Methods

  • start(): Promise<ServerStartResult> - Start the server and get complete server information
  • registerPlugin(plugin: PluginConfig): Promise<PluginRegistrationResult> - Register a plugin at runtime and get its URLs
  • unregisterPlugin(org: string, id: string): Promise<void> - Unregister a plugin at runtime
  • getPlugins(): PluginConfig[] - Get all registered plugins
  • getPort(): number | undefined - Get the actual port the server is running on
  • getRequestLog(): RequestLogEntry[] - Get request log (includes ALL requests, even 404s)

Routes

  • GET /health - Server health check
  • GET /api/request-log - Get request log
  • POST /plugins/{org}/{id}/api/{functionName} - Invoke plugin function
  • GET /plugins/{org}/{id}/ui/* - Serve static assets (with SPA fallback to index.html)

Events

The server emits the following events via the eventEmitter:

  • server.start - Server started
  • plugin.registered - Plugin registered at runtime
  • plugin.unregistered - Plugin unregistered at runtime
  • plugin.register.duplicate - Attempted to register duplicate plugin
  • plugin.unregister.not_found - Attempted to unregister non-existent plugin
  • plugin.not_found - Plugin not found for request
  • function.invoke.start - Function invocation started
  • function.invoke.success - Function invocation succeeded
  • function.invoke.error - Function invocation failed
  • static.serve - Static asset served
  • static.serve.fallback - Fallback to index.html for SPA routing
  • static.not_found - Static asset not found
  • static.dist_missing - Plugin dist directory missing
  • auth.authorization.header.valid - Bearer token validated successfully
  • auth.authorization.header.invalid - Bearer token validation failed
  • auth.token.checking - Validating token from query parameter
  • auth.token.valid - Token validated successfully, cookie issued
  • auth.token.invalid - Token validation failed
  • auth.cookie.present - Request authenticated via cookie
  • auth.missing - No authentication credentials provided
  • auth.unauthorized - Unauthorized access attempt blocked
  • function.middleware.before - Before-invoke middleware executed
  • function.middleware.after - After-invoke middleware executed
  • function.authorization.error - Authorization error thrown from middleware

Function Middleware

The server supports middleware for pre/post-processing function invocations. Middleware runs after authentication checks but before/after the function invocation.

Middleware Interface

export interface FunctionMiddlewareContext {
  org: string;
  pluginId: string;
  functionName: string;
  params: any;
  request: express.Request;
}

export interface FunctionMiddlewareResult {
  params?: any;  // Modified params to pass to function
  metadata?: any; // Metadata to pass through to afterInvoke
}

export interface FunctionMiddleware {
  // Pre-process: Called before function invocation
  // Can modify params, throw authorization errors, or return modified params
  beforeInvoke?: (context: FunctionMiddlewareContext) => Promise<FunctionMiddlewareResult>;

  // Post-process: Called after function invocation
  // Can modify result, throw errors, or return modified result
  afterInvoke?: (context: FunctionMiddlewareContext, result: any, metadata?: any) => Promise<any>;
}

Authorization Errors

Middleware can throw specialized errors for authorization issues:

// Base class
export class AuthorizationError extends Error {
  constructor(message: string, public statusCode: number = 403);
}

// Specific error types
export class ForbiddenError extends AuthorizationError {
  constructor(message: string = 'Forbidden'); // statusCode = 403
}

export class UnauthorizedError extends AuthorizationError {
  constructor(message: string = 'Unauthorized'); // statusCode = 401
}

export class InsufficientPermissionsError extends AuthorizationError {
  constructor(message: string = 'Insufficient permissions'); // statusCode = 403
}

Middleware Example

import {
  createServer,
  ForbiddenError,
  UnauthorizedError,
  InsufficientPermissionsError
} from '@majkapp/majk-plugin-server';

const server = createServer({
  plugins: [...],
  functionInvoker: async (org, id, fn, params) => { /* ... */ },

  functionMiddleware: {
    // Pre-process: Add user context and validate permissions
    beforeInvoke: async (context) => {
      // Extract user from request (e.g., JWT token)
      const user = getUserFromRequest(context.request);

      if (!user) {
        throw new UnauthorizedError('Not authenticated');
      }

      // Check permissions
      if (!user.hasPermission(context.functionName)) {
        throw new InsufficientPermissionsError(`Need permission: ${context.functionName}`);
      }

      // Add user context to params
      return {
        params: {
          ...context.params,
          userId: user.id,
          userRole: user.role
        },
        metadata: {
          user,
          requestTime: Date.now()
        }
      };
    },

    // Post-process: Log and filter response
    afterInvoke: async (context, result, metadata) => {
      // Log invocation
      await logFunctionCall({
        user: metadata.user,
        function: context.functionName,
        duration: Date.now() - metadata.requestTime
      });

      // Filter sensitive data based on user role
      if (metadata.user.role !== 'admin') {
        delete result.data?.internalFields;
      }

      return result;
    }
  }
});

Middleware Flow

  1. Authentication: Token/cookie validation (if auth enabled)
  2. beforeInvoke: Modify params, validate permissions, add context
  3. Function Invocation: Call the actual plugin function
  4. afterInvoke: Modify result, log, filter data
  5. Response: Return final result to client

Error Handling

  • Authorization errors return appropriate HTTP status codes (401/403)
  • Errors are logged and emitted via function.authorization.error event
  • Other errors return 500 status code

Authentication

The server supports multiple authentication methods:

  1. Bearer Token Authentication (recommended for APIs and cross-origin requests)
  2. Cookie-based Authentication (for browser-based clients)
  3. Query Parameter Token (for initial authentication)

Authentication Priority

When a request is received, the server checks authentication in this order:

  1. Authorization Bearer Token - Checked first, ideal for API clients and CORS scenarios
  2. Cookie - Checked second, for browser-based sessions
  3. Query Parameter Token - Checked last, for initial authentication and cookie setup

Bearer Token Authentication

Perfect for API clients, mobile apps, and cross-origin requests:

// Server configuration
const server = createServer({
  cors: true, // Enable CORS for cross-origin requests
  auth: {
    enabled: true,
    tokenValidator: async (token) => {
      // Your validation logic (JWT verify, database lookup, etc.)
      const isValid = await validateToken(token);
      return isValid;
    }
  }
});

// Client usage with Bearer token:
// 1. Add Authorization header to every request:
fetch('/plugins/myorg/my-plugin/ui/', {
  headers: {
    'Authorization': 'Bearer your-token-here'
  }
});

// 2. No cookies are set or required
// 3. Token is validated on every request (stateless)

Cookie-based Authentication Flow

Traditional browser-based authentication with sessions:

// Server configuration
const server = createServer({
  auth: {
    enabled: true,
    tokenValidator: async (token) => {
      const isValid = await validateToken(token);
      return isValid;
    },
    cookieName: 'majk_auth',
    cookieMaxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
});

// Client usage:
// 1. First request with token:
//    GET /plugins/myorg/my-plugin/ui/?token=abc123
//    -> Server validates token, sets cookie, returns 200
//
// 2. Subsequent requests (cookie automatically sent):
//    GET /plugins/myorg/my-plugin/ui/dashboard
//    -> Server validates cookie, returns 200
//
// 3. Request without auth:
//    GET /plugins/myorg/my-plugin/ui/
//    -> Server returns 401 Unauthorized

Authentication Examples

API Client with Bearer Token:

const response = await fetch(
  'http://localhost:3000/plugins/myorg/my-plugin/api/getData',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
    },
    body: JSON.stringify({ filter: 'active' })
  }
);

Browser with Cookie:

// Initial authentication
window.location.href = '/plugins/myorg/my-plugin/ui/?token=abc123';

// Subsequent requests automatically include cookie
fetch('/plugins/myorg/my-plugin/api/getData', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ filter: 'active' })
});

Security Features

  • Bearer token support: Stateless authentication for APIs and CORS scenarios
  • HTTP-only cookies: Not accessible via JavaScript (XSS protection)
  • SameSite strict: Cookies only sent to same origin (CSRF protection)
  • Configurable expiration: Control cookie lifetime
  • Token validation: Custom logic for token verification
  • Priority-based checking: Bearer tokens checked first for optimal performance

CORS Configuration

CORS is disabled by default for security. Enable only when needed:

const server = createServer({
  cors: true, // Enable CORS for all routes
  // ... other config
});

Enhanced Server Response Objects

Starting with v1.2.0, the server returns rich response objects that include complete URL information for easy access to plugins.

Server Start Response

When you start the server, you get a ServerStartResult object:

const result = await server.start();
console.log(result);
// {
//   port: 56371,
//   baseUrl: "http://localhost:56371",
//   plugins: [
//     {
//       org: "myorg",
//       id: "my-plugin",
//       distPath: "/path/to/plugin/dist",
//       urls: {
//         ui: "http://localhost:56371/plugins/myorg/my-plugin/ui",
//         api: "http://localhost:56371/plugins/myorg/my-plugin/api",
//         assets: "http://localhost:56371/plugin-screens/my-plugin"  // if assetPathPrefix configured
//       }
//     }
//   ]
// }

// Access specific plugin URLs
const myPlugin = result.plugins.find(p => p.id === 'my-plugin');
console.log(`Open UI at: ${myPlugin.urls.ui}`);
console.log(`API endpoint: ${myPlugin.urls.api}/functionName`);

Plugin Registration Response

When you register a plugin at runtime, you get a PluginRegistrationResult:

const info = await server.registerPlugin({
  org: 'myorg',
  id: 'new-plugin',
  distPath: './plugins/new-plugin/dist',
  capabilities: {}
});

console.log(info);
// {
//   org: "myorg",
//   id: "new-plugin",
//   distPath: "./plugins/new-plugin/dist",
//   urls: {
//     ui: "http://localhost:56371/plugins/myorg/new-plugin/ui",
//     api: "http://localhost:56371/plugins/myorg/new-plugin/api",
//     assets: "http://localhost:56371/plugin-screens/new-plugin"
//   }
// }

// Immediately use the URLs
console.log(`Plugin registered! Access at: ${info.urls.ui}`);

Benefits:

  • No need to manually construct URLs
  • Assets URL included when assetPathPrefix is configured
  • Perfect for dynamic plugin loading
  • Easy integration with monitoring dashboards
  • Simplified testing and automation

Request Logging

The server automatically logs ALL requests, including 404s, with detailed information:

// After some requests have been made
const logs = server.getRequestLog();
console.log(logs);
// [
//   {
//     id: "1762896053497-6tdvzl1yf",
//     timestamp: "2025-11-11T21:20:53.497Z",
//     method: "POST",
//     url: "/plugins/wrong/wrong/api/test",
//     org: "wrong",
//     pluginId: "wrong",
//     statusCode: 404,
//     duration: 0,
//     error: "Plugin not found"
//   },
//   {
//     id: "1762896053495-vzyvs2nuv",
//     timestamp: "2025-11-11T21:20:53.495Z",
//     method: "POST",
//     url: "/plugins/test/test-plugin/api/hello",
//     org: "test",
//     pluginId: "test-plugin",
//     statusCode: 200,
//     duration: 0
//   }
// ]

Request Log Features:

  • Captures ALL requests (200s, 404s, 500s, etc.)
  • Includes status code, duration, and error messages
  • Configurable size limit via maxRequestLogSize
  • Useful for debugging, monitoring, and analytics
  • Accessible via getRequestLog() or /api/request-log endpoint

Example: Monitoring 404s

setInterval(() => {
  const logs = server.getRequestLog();
  const notFound = logs.filter(log => log.statusCode === 404);

  if (notFound.length > 0) {
    console.warn(`⚠️  Found ${notFound.length} 404 errors in recent requests`);
    notFound.forEach(log => {
      console.warn(`   ${log.method} ${log.url}`);
    });
  }
}, 60000);

Auto Port Selection

The server can automatically find and use a free port:

// Option 1: Omit port (will auto-find)
const server = createServer({
  // No port specified
  plugins: [...],
  functionInvoker: async () => { /* ... */ }
});

const result = await server.start();
console.log(`Server running on port ${result.port}`);
console.log(`Base URL: ${result.baseUrl}`);

// Option 2: Explicit port = 0 (auto-find)
const server = createServer({
  port: 0, // 0 = find free port
  plugins: [...]
});

const result = await server.start();
console.log(`Auto-selected port: ${result.port}`);

// Option 3: Specific port
const server = createServer({
  port: 3000, // Try this specific port
  plugins: [...]
});

const result = await server.start(); // result.port = 3000 (or error if taken)

Getting the Port

// Method 1: From start() result
const result = await server.start();
const port = result.port;

// Method 2: getPort() after starting
await server.start();
const port = server.getPort();

Cookie Configuration

Cookie options are configurable with secure defaults:

const server = createServer({
  auth: {
    enabled: true,
    tokenValidator: async (token) => validateToken(token),
    cookieHttpOnly: true,      // Default: true (XSS protection)
    cookieSameSite: 'strict',   // Default: 'strict' (CSRF protection)
    cookieName: 'majk_auth',    // Default: 'majk_auth'
    cookieMaxAge: 24 * 60 * 60 * 1000  // Default: 24 hours
  }
});

Asset Path Prefix

By default, the server serves plugin static assets via /plugins/{org}/{id}/ui/*. You can optionally configure an additional asset path prefix to serve assets via a custom route (e.g., /plugin-screens/{pluginId}/*). This is useful when plugins are built with specific base paths (like Vite's base option).

const server = createServer({
  plugins: [{ org: 'myorg', id: 'my-plugin', distPath: './dist', capabilities: {} }],
  functionInvoker: async () => ({ success: true }),
  assetPathPrefix: '/plugin-screens'  // Enables /plugin-screens/{pluginId}/* routes
});

With this configuration, assets will be served from both:

  • /plugins/myorg/my-plugin/ui/* (default route)
  • /plugin-screens/my-plugin/* (custom prefix route)

CLI Usage:

majk-plugin-server -d ./my-plugin --asset-path-prefix /plugin-screens

CLI Tool

The server includes a CLI tool with two modes: Standalone Mode (single plugin development) and Auto-Discovery Mode (multiple plugins).

Installation

npm install -g @majkapp/majk-plugin-server

Standalone Mode (Development)

Serve a single plugin from its directory - perfect for development:

# Serve a plugin from its directory
majk-plugin-server --plugin-dir ./my-plugin

# With custom port
majk-plugin-server -d ./my-plugin -p 3456

# With verbose output
majk-plugin-server -d ./my-plugin -v

How it works:

  1. Reads package.json to get plugin name and configuration
  2. Extracts org/id from package name (e.g., @myorg/my-pluginmyorg/my-plugin)
  3. Serves UI from dist/ directory
  4. Accessible at: http://localhost:PORT/plugins/ORG/ID/ui/

Requirements:

  • Plugin directory must contain package.json
  • Plugin must be built (dist/ directory must exist)
  • Plugin will be served at /plugins/{org}/{id}/ui/

Example:

cd my-plugin
npm run build  # Build your plugin first
cd ..
majk-plugin-server --plugin-dir ./my-plugin

# Output:
# ╔═══════════════════════════════════════════════════╗
# ║       MAJK Plugin Server - Standalone Mode       ║
# ╚═══════════════════════════════════════════════════╝
#
# 🔍 Loading plugin from: ./my-plugin
#   ✅ Loaded: @myorg/my-plugin (myorg/my-plugin)
#   📂 Serving from: ./my-plugin/dist
#
# Plugin routes:
#   • http://localhost:3000/plugins/myorg/my-plugin/ui/

Auto-Discovery Mode (Multiple Plugins)

Discover and serve multiple plugins from directories:

# Serve installed plugins
majk-plugin-server --plugins-dir ~/.majk-dev/plugins

# Serve local development plugins
majk-plugin-server --local-plugins-dir ~/.majk-dev/local-plugins

# Serve both with custom port
majk-plugin-server \
  --plugins-dir ~/.majk-dev/plugins \
  --local-plugins-dir ~/.majk-dev/local-plugins \
  -p 3456

Options

  • -p, --port <port> - Port to listen on (default: 3000)
  • -d, --plugin-dir <path> - Standalone mode: single plugin directory
  • --plugins-dir <path> - Directory containing multiple plugins
  • --local-plugins-dir <path> - Directory containing local plugin links
  • --functions-handler <path> - JavaScript file with function handlers
  • --middleware <path> - JavaScript file with middleware
  • -v, --verbose - Verbose output
  • -h, --help - Show help

Note: Cannot mix standalone mode (-d) with auto-discovery mode (--plugins-dir/--local-plugins-dir)

Function Handlers

Enable function invocation by providing a JavaScript file that exports handlers:

Option 1: Object with named functions

// handlers.js
module.exports = {
  async getData(params) {
    return { success: true, data: params };
  },

  async saveData(params) {
    if (!params.data) {
      return { success: false, error: 'No data provided' };
    }
    return { success: true, id: Date.now() };
  },

  async deleteData(params) {
    return { success: true, deleted: true };
  }
};

Option 2: Single function with routing

// handlers-advanced.js
module.exports = async function(org, id, functionName, params) {
  console.log(`Calling ${org}/${id}.${functionName}`);

  switch (functionName) {
    case 'getData':
      return { success: true, data: params };
    case 'saveData':
      return { success: true, saved: true };
    default:
      return { success: false, error: 'Unknown function' };
  }
};

Usage:

# With object-based handlers
majk-plugin-server -d ./my-plugin --functions-handler ./handlers.js

# With routing-based handlers
majk-plugin-server -d ./my-plugin --functions-handler ./handlers-advanced.js -v

Example files available in examples/:

  • functions-handler.js - Object-based handlers
  • functions-handler-advanced.js - Routing-based handler

Middleware

Add pre/post processing to function invocations:

// middleware.js
const { UnauthorizedError, ForbiddenError } = require('@majkapp/plugin-server');

module.exports = {
  // Before function invocation
  async beforeInvoke(context) {
    // context: { org, pluginId, functionName, params, request }

    // Authentication check
    const user = getUserFromRequest(context.request);
    if (!user) {
      throw new UnauthorizedError('Authentication required');
    }

    // Authorization check
    if (context.functionName === 'deleteData' && user.role !== 'admin') {
      throw new ForbiddenError('Admin access required');
    }

    // Add user context to params
    return {
      params: { ...context.params, userId: user.id },
      metadata: { user, startTime: Date.now() }
    };
  },

  // After function invocation
  async afterInvoke(context, result, metadata) {
    const duration = Date.now() - metadata.startTime;
    console.log(`Function executed in ${duration}ms`);

    // Add metadata to response
    return {
      ...result,
      _metadata: { executionTime: duration }
    };
  }
};

Usage:

majk-plugin-server -d ./my-plugin \
  --functions-handler ./handlers.js \
  --middleware ./middleware.js

Example file available: examples/middleware-example.js

Authorization Errors:

  • UnauthorizedError - Returns 401
  • ForbiddenError - Returns 403
  • InsufficientPermissionsError - Returns 403

Plugin Discovery

Installed Plugins (--plugins-dir):

  • Scans for directories with package.json
  • Requires majk.type: "in-process" in package.json
  • Looks for dist directory for built assets
  • Parses org/id from package name

Local Plugins (--local-plugins-dir):

  • Scans for directories with package.json containing localPlugin config
  • Follows targetDirectory to actual plugin source
  • Supports development plugins via symbolic links
  • Example local plugin package.json:
    {
      "name": "@myorg/my-plugin-local",
      "localPlugin": {
        "targetDirectory": "/path/to/plugin/source",
        "packageName": "@myorg/my-plugin",
        "type": "in-process"
      }
    }

Error Handling

The CLI is error-tolerant:

  • Skips plugins with missing files/directories
  • Logs errors prominently in output
  • Emits error events for monitoring
  • Continues serving valid plugins despite individual failures

Limitations in CLI Mode

  • Function invocation disabled: API endpoints return "not enabled" error
  • Static assets only: UI routes work normally
  • No auth by default: Add auth config for production use

Practical Examples

Example 1: Multi-Tenant SaaS Application

import { createServer, UnauthorizedError, ForbiddenError } from '@majkapp/majk-plugin-server';
import { verifyJWT, getTenantForUser } from './auth';

const server = createServer({
  plugins: [
    {
      org: 'saas',
      id: 'crm',
      distPath: './plugins/crm/dist',
      capabilities: { screens: ['contacts', 'deals'], functions: ['getContacts', 'createDeal'] }
    },
    {
      org: 'saas',
      id: 'analytics',
      distPath: './plugins/analytics/dist',
      capabilities: { screens: ['dashboard'], functions: ['getMetrics'] }
    }
  ],

  functionInvoker: async (org, id, functionName, params) => {
    const plugin = await import(`./plugins/${id}/index.js`);
    return await plugin[functionName](params);
  },

  auth: {
    enabled: true,
    tokenValidator: async (token) => {
      try {
        await verifyJWT(token);
        return true;
      } catch {
        return false;
      }
    }
  },

  functionMiddleware: {
    beforeInvoke: async (context) => {
      const token = context.request.headers.authorization?.substring(7);
      const user = await verifyJWT(token);

      if (!user) {
        throw new UnauthorizedError('Invalid token');
      }

      const tenant = await getTenantForUser(user.id);

      if (!tenant.hasAccess(context.pluginId)) {
        throw new ForbiddenError(`Tenant does not have access to ${context.pluginId}`);
      }

      // Add tenant context to all function calls
      return {
        params: {
          ...context.params,
          tenantId: tenant.id,
          userId: user.id
        },
        metadata: { tenant, user }
      };
    },

    afterInvoke: async (context, result, metadata) => {
      // Log usage for billing
      await logUsage({
        tenantId: metadata.tenant.id,
        function: context.functionName,
        plugin: context.pluginId,
        timestamp: new Date()
      });

      return result;
    }
  },

  eventEmitter: async (event) => {
    // Real-time monitoring dashboard
    await publishToWebSocket('monitoring-dashboard', event);
  }
});

await server.start();

Example 2: Development Server with Hot Reload

import { createServer } from '@majkapp/majk-plugin-server';
import chokidar from 'chokidar';
import path from 'path';

const pluginRegistry = new Map();

const server = createServer({
  plugins: [],
  functionInvoker: async (org, id, functionName, params) => {
    const plugin = pluginRegistry.get(`${org}/${id}`);
    if (!plugin) throw new Error('Plugin not found');

    // Clear require cache for hot reload
    const modulePath = require.resolve(plugin.entryPoint);
    delete require.cache[modulePath];

    const pluginModule = require(modulePath);
    return await pluginModule[functionName](params);
  },
  cors: true // Enable CORS for local development
});

// Auto-discover plugins
async function discoverPlugins() {
  const pluginsDir = './plugins';
  const plugins = await fs.readdir(pluginsDir);

  for (const pluginName of plugins) {
    const distPath = path.join(pluginsDir, pluginName, 'dist');
    if (fs.existsSync(distPath)) {
      await server.registerPlugin({
        org: 'dev',
        id: pluginName,
        distPath,
        capabilities: {}
      });

      pluginRegistry.set(`dev/${pluginName}`, {
        entryPoint: path.join(pluginsDir, pluginName, 'index.js')
      });

      console.log(`📦 Registered: ${pluginName}`);
    }
  }
}

// Watch for changes and hot reload
chokidar.watch('./plugins/**/dist/**/*').on('change', async (filePath) => {
  const pluginName = filePath.split('/')[2];
  console.log(`🔄 Reloading: ${pluginName}`);

  await server.unregisterPlugin('dev', pluginName);
  await server.registerPlugin({
    org: 'dev',
    id: pluginName,
    distPath: `./plugins/${pluginName}/dist`,
    capabilities: {}
  });

  console.log(`✅ Reloaded: ${pluginName}`);
});

await discoverPlugins();
const port = await server.start();
console.log(`🚀 Dev server running on http://localhost:${port}`);
console.log('   Hot reload enabled - changes will be reflected automatically');

Example 3: API Gateway with Rate Limiting

import { createServer, UnauthorizedError, ForbiddenError } from '@majkapp/majk-plugin-server';
import rateLimit from 'express-rate-limit';

// Rate limit tracking
const rateLimitMap = new Map();

function checkRateLimit(userId: string, functionName: string): boolean {
  const key = `${userId}:${functionName}`;
  const now = Date.now();
  const windowMs = 60000; // 1 minute
  const maxRequests = 100;

  const userLimits = rateLimitMap.get(key) || { count: 0, resetTime: now + windowMs };

  if (now > userLimits.resetTime) {
    userLimits.count = 0;
    userLimits.resetTime = now + windowMs;
  }

  userLimits.count++;
  rateLimitMap.set(key, userLimits);

  return userLimits.count <= maxRequests;
}

const server = createServer({
  plugins: [
    { org: 'api', id: 'users', distPath: './plugins/users/dist', capabilities: {} },
    { org: 'api', id: 'payments', distPath: './plugins/payments/dist', capabilities: {} },
    { org: 'api', id: 'notifications', distPath: './plugins/notifications/dist', capabilities: {} }
  ],

  functionInvoker: async (org, id, functionName, params) => {
    const plugin = await import(`./plugins/${id}/index.js`);
    return await plugin[functionName](params);
  },

  auth: {
    enabled: true,
    tokenValidator: async (token) => {
      return await validateApiKey(token);
    }
  },

  functionMiddleware: {
    beforeInvoke: async (context) => {
      const apiKey = context.request.headers.authorization?.substring(7);
      const user = await getUserFromApiKey(apiKey);

      if (!user) {
        throw new UnauthorizedError('Invalid API key');
      }

      // Check rate limits
      if (!checkRateLimit(user.id, context.functionName)) {
        throw new ForbiddenError('Rate limit exceeded - max 100 requests per minute');
      }

      // Track API usage for billing
      return {
        params: { ...context.params, apiKeyId: user.apiKeyId },
        metadata: { user, startTime: Date.now() }
      };
    },

    afterInvoke: async (context, result, metadata) => {
      const duration = Date.now() - metadata.startTime;

      // Log API usage
      await logApiCall({
        userId: metadata.user.id,
        apiKeyId: metadata.user.apiKeyId,
        function: context.functionName,
        plugin: context.pluginId,
        duration,
        timestamp: new Date(),
        statusCode: result.success ? 200 : 500
      });

      // Add rate limit headers to response
      return {
        ...result,
        headers: {
          'X-RateLimit-Limit': '100',
          'X-RateLimit-Remaining': getRemainingRequests(metadata.user.id, context.functionName)
        }
      };
    }
  },

  eventEmitter: async (event) => {
    // Alert on errors
    if (event.type.includes('error')) {
      await sendAlert({
        severity: 'error',
        message: `API Error: ${event.type}`,
        data: event.data
      });
    }
  }
});

await server.start();

Example 4: Microservices Architecture

import { createServer } from '@majkapp/majk-plugin-server';
import { connectToServiceBus } from './messaging';

const serviceBus = await connectToServiceBus();

const server = createServer({
  plugins: [
    { org: 'services', id: 'orders', distPath: './services/orders/dist', capabilities: {} },
    { org: 'services', id: 'inventory', distPath: './services/inventory/dist', capabilities: {} },
    { org: 'services', id: 'shipping', distPath: './services/shipping/dist', capabilities: {} }
  ],

  functionInvoker: async (org, id, functionName, params) => {
    // Route function calls through service bus for distributed processing
    return await serviceBus.request(`${id}.${functionName}`, params, {
      timeout: 30000
    });
  },

  functionMiddleware: {
    beforeInvoke: async (context) => {
      // Add distributed tracing
      const traceId = generateTraceId();
      const spanId = generateSpanId();

      return {
        params: {
          ...context.params,
          tracing: { traceId, spanId, parentSpanId: null }
        },
        metadata: { traceId, spanId, startTime: Date.now() }
      };
    },

    afterInvoke: async (context, result, metadata) => {
      const duration = Date.now() - metadata.startTime;

      // Record span in distributed tracing
      await recordSpan({
        traceId: metadata.traceId,
        spanId: metadata.spanId,
        service: context.pluginId,
        operation: context.functionName,
        duration,
        status: result.success ? 'ok' : 'error'
      });

      return result;
    }
  },

  eventEmitter: async (event) => {
    // Publish events to service bus for other microservices
    await serviceBus.publish('plugin-server.events', event);
  }
});

await server.start();

Example 5: Testing & Mock Server

import { createServer } from '@majkapp/majk-plugin-server';

// Mock data store
const mockData = {
  users: [
    { id: '1', name: 'Alice', role: 'admin' },
    { id: '2', name: 'Bob', role: 'user' }
  ],
  products: [
    { id: '1', name: 'Widget', price: 29.99 },
    { id: '2', name: 'Gadget', price: 49.99 }
  ]
};

const mockServer = createServer({
  plugins: [
    { org: 'test', id: 'api', distPath: './test-fixtures/dist', capabilities: {} }
  ],

  functionInvoker: async (org, id, functionName, params) => {
    console.log(`[MOCK] ${functionName}(${JSON.stringify(params)})`);

    // Simulate different responses based on function name
    if (functionName === 'getUsers') {
      return { success: true, data: mockData.users };
    }

    if (functionName === 'getUser') {
      const user = mockData.users.find(u => u.id === params.id);
      return user
        ? { success: true, data: user }
        : { success: false, error: 'User not found' };
    }

    if (functionName === 'createUser') {
      const newUser = { id: String(mockData.users.length + 1), ...params };
      mockData.users.push(newUser);
      return { success: true, data: newUser };
    }

    // Simulate slow responses
    if (params.slow) {
      await new Promise(resolve => setTimeout(resolve, 2000));
    }

    // Simulate errors
    if (params.error) {
      throw new Error('Simulated error');
    }

    return { success: true, data: 'Mock response' };
  },

  eventEmitter: async (event) => {
    console.log(`[EVENT] ${event.type}`, event.data);
  },

  cors: true // Enable CORS for testing from browser
});

const port = await server.start();
console.log(`🧪 Mock server running on http://localhost:${port}`);
console.log('   Available mock functions: getUsers, getUser, createUser');
console.log('   Add ?slow=true to simulate slow responses');
console.log('   Add ?error=true to simulate errors');

export default mockServer;

Client-Side Usage Examples

JavaScript/TypeScript Client

// Calling plugin functions
async function callPluginFunction(org: string, pluginId: string, functionName: string, params: any) {
  const response = await fetch(
    `http://localhost:3000/plugins/${org}/${pluginId}/api/${functionName}`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(params)
    }
  );

  return await response.json();
}

// Usage
const result = await callPluginFunction('myorg', 'dashboard', 'getData', {
  filter: 'active',
  limit: 10
});

console.log(result.data);

React Hook

import { useState, useEffect } from 'react';

function usePluginFunction(org: string, pluginId: string, functionName: string) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const execute = async (params: any) => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(
        `http://localhost:3000/plugins/${org}/${pluginId}/api/${functionName}`,
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${localStorage.getItem('token')}`
          },
          body: JSON.stringify(params)
        }
      );

      const result = await response.json();

      if (!result.success) {
        throw new Error(result.error);
      }

      setData(result.data);
      return result.data;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return { data, loading, error, execute };
}

// Usage in component
function DashboardComponent() {
  const { data, loading, error, execute } = usePluginFunction(
    'myorg',
    'dashboard',
    'getData'
  );

  useEffect(() => {
    execute({ filter: 'active' });
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{JSON.stringify(data)}</div>;
}

cURL Examples

# Health check
curl http://localhost:3000/health

# Get request logs
curl http://localhost:3000/api/request-log

# Access plugin UI (with token)
curl "http://localhost:3000/plugins/myorg/my-plugin/ui/?token=abc123"

# Call plugin function
curl -X POST http://localhost:3000/plugins/myorg/my-plugin/api/getData \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-token-here" \
  -d '{"filter": "active", "limit": 10}'

# With cookie authentication (after initial token)
curl http://localhost:3000/plugins/myorg/my-plugin/ui/dashboard \
  -H "Cookie: majk_auth=authenticated"

Troubleshooting

Common Issues

Issue: 401 Unauthorized on all requests

// Solution: Check that auth is properly configured
const server = createServer({
  auth: {
    enabled: true,
    tokenValidator: async (token) => {
      console.log('Validating token:', token); // Debug log
      return await yourValidationLogic(token);
    }
  }
});

// Or disable auth for testing
const server = createServer({
  auth: { enabled: false } // or omit auth entirely
});

Issue: Plugin not found (404)

// Check plugin is registered
const plugins = server.getPlugins();
console.log('Registered plugins:', plugins);

// Ensure distPath exists and contains index.html
const distPath = './plugins/my-plugin/dist';
console.log('Files in distPath:', fs.readdirSync(distPath));

Issue: CORS errors in browser

// Enable CORS for development
const server = createServer({
  cors: true,
  // ... other config
});

Issue: Function middleware errors not caught

// Always import error classes
import {
  UnauthorizedError,
  ForbiddenError,
  InsufficientPermissionsError
} from '@majkapp/majk-plugin-server';

// Throw proper error types
throw new UnauthorizedError('Custom message');

TypeScript Types

Full TypeScript type definitions:

export interface PluginConfig {
  org: string;
  id: string;
  distPath: string;
  capabilities: any;
}

export interface AuthConfig {
  enabled: boolean;
  tokenValidator: (token: string) => Promise<boolean>;
  cookieName?: string;
  cookieMaxAge?: number;
  cookieHttpOnly?: boolean;
  cookieSameSite?: 'strict' | 'lax' | 'none';
}

export interface FunctionMiddlewareContext {
  org: string;
  pluginId: string;
  functionName: string;
  params: any;
  request: express.Request;
}

export interface FunctionMiddlewareResult {
  params?: any;
  metadata?: any;
}

export interface FunctionMiddleware {
  beforeInvoke?: (context: FunctionMiddlewareContext) => Promise<FunctionMiddlewareResult>;
  afterInvoke?: (context: FunctionMiddlewareContext, result: any, metadata?: any) => Promise<any>;
}

export interface PluginServerConfig {
  port?: number;
  plugins: PluginConfig[];
  functionInvoker: (org: string, id: string, functionName: string, params: any) => Promise<any>;
  eventEmitter?: (event: ServerEvent) => Promise<void>;
  maxRequestLogSize?: number;
  cors?: boolean;
  auth?: AuthConfig;
  functionMiddleware?: FunctionMiddleware;
  assetPathPrefix?: string;  // Optional asset path prefix (e.g., '/plugin-screens')
}

export interface RequestLogEntry {
  id: string;
  timestamp: string;
  method: string;
  url: string;
  org: string;
  pluginId: string;
  statusCode?: number;
  duration?: number;
  error?: string;
}

export interface ServerEvent {
  type: string;
  timestamp: string;
  data: any;
}

export interface PluginUrls {
  ui: string;
  api: string;
  assets?: string;
}

export interface PluginInfo {
  org: string;
  id: string;
  distPath: string;
  urls: PluginUrls;
}

export interface ServerStartResult {
  port: number;
  baseUrl: string;
  plugins: PluginInfo[];
}

export interface PluginRegistrationResult {
  org: string;
  id: string;
  distPath: string;
  urls: PluginUrls;
}

export class AuthorizationError extends Error {
  constructor(message: string, public statusCode: number = 403);
}

export class ForbiddenError extends AuthorizationError {}
export class UnauthorizedError extends AuthorizationError {}
export class InsufficientPermissionsError extends AuthorizationError {}

export interface PluginServer {
  start(): Promise<ServerStartResult>;
  getPort(): number | undefined;
  registerPlugin(plugin: PluginConfig): Promise<PluginRegistrationResult>;
  unregisterPlugin(org: string, id: string): Promise<void>;
  getPlugins(): PluginConfig[];
  getRequestLog(): RequestLogEntry[];
  stop(): Promise<void>;
}

export function createServer(config: PluginServerConfig): PluginServer;

Development

Building

npm run build

Examples

Working code examples are available in the examples/ directory:

  • example-full.js / example-full.ts - Complete server with all features
  • example-auto-port.js - Auto port selection demonstration
  • example-middleware.js - Function middleware with authorization
  • example.ts - Basic TypeScript server setup

Run examples with:

node examples/example-full.js

Testing

The project uses Jest with TypeScript for testing:

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

Test Structure:

  • tests/auth.test.ts - Authentication and CORS tests (13 tests)
  • tests/middleware.test.ts - Function middleware tests (6 tests)
  • tests/auto-port.test.ts - Auto port selection tests (7 tests)
  • tests/runtime-plugin-management.test.ts - Plugin management tests (8 tests)

Total: 34 tests, all passing ✅

Test Coverage

Tests cover:

  • Bearer token authentication (Authorization header)
  • Token-based authentication with cookies
  • Query parameter token authentication
  • Authentication priority (bearer > cookie > query)
  • Function middleware (beforeInvoke, afterInvoke)
  • Authorization error handling
  • Auto port selection
  • Runtime plugin registration/unregistration
  • CORS configuration
  • Cookie security (HttpOnly, SameSite)

License

MIT