Member Junction
    Preparing search index...

    Module @memberjunction/server-extensions-core

    @memberjunction/server-extensions-core

    Plugin architecture for MJServer that enables auto-discovery and lifecycle management of server extension modules. Extensions register Express routes, background workers, or WebSocket services, handle their own or MJ-managed authentication, publish and consume typed services, and participate in health checks and graceful shutdown — all without modifying MJServer source code.

    For complete architectural patterns and guidelines, see the Server Extensions Guide.

    This package provides the core contracts and loader for server extensions:

    • BaseServerExtension — Abstract base class that all extensions implement. Defines DefaultPhase, Initialize, OnAllExtensionsMounted, Shutdown, and HealthCheck lifecycle methods.
    • ServerExtensionLoader — Discovers registered extension classes via MJ's ClassFactory, matches them to config entries, enforces phased mounting (pre-auth vs. post-auth), maintains the shared ServerExtensionServiceRegistry, and manages extension lifecycle.
    • ServerExtensionServiceRegistry — Typed registry facilitating decoupled cross-extension service discovery and telemetry sharing.
    • ServerExtensionInitContext — Unified context passed to extension Initialize and OnAllExtensionsMounted methods.

    Extensions are discovered automatically using MemberJunction's standard @RegisterClass + ClassFactory pattern.

    Open App packages listed in the host mj.config.cjs dynamicPackages.server[] declare the extensions they need:

    1. Named export MJ_SERVER_EXTENSIONS on the server package (preferred; read after the package is imported).
    2. Fallback: package.json → memberjunction.serverExtensions.

    @memberjunction/server-bootstrap collects those declarations and serve() merges them with the host serverExtensions[]. Host DriverClass wins (Enabled, RootPath, per-key Settings, Phase, OwnsCORS). A host entry with Enabled: false disables a discovered extension.

    Extensions declare their execution phase or inherit their class default:

    • 'pre-auth' (Default): Mounted before MJServer authentication middleware. Used for external webhooks that carry provider signatures (Slack HMAC, Teams Bot Framework JWT, Twilio signatures, WebRTC SDP brokers).
    • 'post-auth': Mounted after MJServer authentication middleware (createUnifiedAuthMiddleware). Guaranteed to have authenticated req.user context.

    MJServer mounts one global CORS middleware ahead of every extension. By default it reflects any allowed origin onto an extension's responses (refusals included) and answers every preflight (OPTIONS) with 204 itself, so an extension's own OPTIONS handler never runs.

    An extension with its own origin policy — e.g. a public pre-auth edge whose allowed origins are set per resource — sets OwnsCORS: true in its config (Open App MJ_SERVER_EXTENSIONS entry or host serverExtensions[]). The global CORS middleware then skips every request under that RootPath (case-insensitive, segment boundary: /ballot owns /ballot/x, not /ballots), and the extension answers its own preflights and sets its own Access-Control-* headers. A path that sets none is simply not readable cross-origin, so a forgotten handler fails closed. The unified auth middleware passes OPTIONS through, so the flag also works for post-auth extensions (their 401s then carry no CORS headers unless the extension adds them). Disabled entries own nothing; a non-boolean value is ignored with a warning. Helpers: GetCorsOwningServerExtensionRoots, IsPathUnderServerExtensionRoot, SkipForServerExtensionRoots.

    export const MJ_SERVER_EXTENSIONS = [
    { Enabled: true, DriverClass: 'BallotEdgeExtension', RootPath: '/ballot', Phase: 'pre-auth', OwnsCORS: true, Settings: {} },
    ];

    MJServer also disables Express's X-Powered-By header app-wide.

    Core system paths cannot be claimed by server extensions: /, /graphql, /auth, /oauth, /health, /magic-link, /schema, /media, /mcp. Configured roots matching these reserved paths fail closed and are dropped with an error.

    npm install @memberjunction/server-extensions-core
    
    import { Router } from 'express';
    import { RegisterClass } from '@memberjunction/global';
    import {
    BaseServerExtension,
    ServerExtensionInitContext,
    ServerExtensionPhase,
    ExtensionInitResult,
    ExtensionHealthResult
    } from '@memberjunction/server-extensions-core';

    @RegisterClass(BaseServerExtension, 'MyCustomExtension')
    export class MyCustomExtension extends BaseServerExtension {
    /** Declare lifecycle phase */
    public override get DefaultPhase(): ServerExtensionPhase {
    return 'pre-auth';
    }

    async Initialize(context: ServerExtensionInitContext): Promise<ExtensionInitResult> {
    const { app, config, services } = context;

    const router = Router();
    router.get('/hello', (_req, res) => {
    res.json({ message: 'Hello from my extension!' });
    });

    app.use(config.RootPath, router);

    return {
    Success: true,
    Message: 'Custom extension loaded',
    RegisteredRoutes: [`GET ${config.RootPath}/hello`],
    Service: { ping: () => 'pong' } // Auto-registered into ServerExtensionServiceRegistry
    };
    }

    async OnAllExtensionsMounted(context: ServerExtensionInitContext): Promise<void> {
    // Cross-extension service wiring
    const telephony = context.services.GetService('TwilioTelephonyService');
    }

    async Shutdown(): Promise<void> {
    // Clean up connections, drain requests, release resources
    }

    async HealthCheck(): Promise<ExtensionHealthResult> {
    return { Healthy: true, Name: 'MyCustomExtension' };
    }
    }
    module.exports = {
    serverExtensions: [
    {
    Name: 'MyExtension',
    Enabled: true,
    DriverClass: 'MyCustomExtension',
    RootPath: '/api/my-extension',
    Phase: 'pre-auth',
    Settings: {
    apiKey: process.env.MY_EXTENSION_API_KEY,
    }
    }
    ]
    };
    Member Description
    DefaultPhase Getter returning default lifecycle phase ('pre-auth' or 'post-auth'). Default is 'pre-auth'.
    Initialize(context) Called once at MJServer startup. Receives ServerExtensionInitContext.
    OnAllExtensionsMounted?(context) Optional hook called after all extensions across both phases have mounted. Ideal for service wiring.
    Shutdown() Called during graceful shutdown (SIGTERM/SIGINT).
    HealthCheck() Called periodically. Return health status quickly (< 100ms).
    OnConfigurationChange?(config) Optional. Called when configuration changes at runtime.
    Member Description
    Services Access the unified ServerExtensionServiceRegistry.
    LoadExtensions(app, configs, options?) Discover and initialize extensions filtered by phase (pre-auth or post-auth).
    NotifyAllExtensionsMounted(options?) Invoke OnAllExtensionsMounted on all loaded extensions.
    HealthCheckAll() Run health checks across all loaded extensions.
    ShutdownAll() Shut down all extensions in reverse order (LIFO).
    Extensions Read-only array of loaded extension instances.
    export type ServerExtensionPhase = 'pre-auth' | 'post-auth';

    export interface ServerExtensionConfig {
    Name?: string;
    DriverClass: string;
    RootPath: string;
    Enabled: boolean;
    Phase?: ServerExtensionPhase;
    OwnsCORS?: boolean; // true = global CORS skips RootPath; the extension handles CORS + preflights
    Settings?: Record<string, unknown>;
    PackagePath?: string;
    }

    export interface ServerExtensionInitContext {
    app: Application;
    httpServer?: Server;
    config: ServerExtensionConfig;
    publicUrl?: string;
    services: ServerExtensionServiceRegistry;
    phase: ServerExtensionPhase;
    }

    export interface ExtensionInitResult {
    Success: boolean;
    Message: string;
    RegisteredRoutes?: string[];
    Service?: object;
    Skipped?: boolean;
    }

    Classes

    BaseServerExtension
    DefaultServerExtensionServiceRegistry
    ServerExtensionLoader

    Interfaces

    ExtensionHealthResult
    ExtensionInitResult
    IWebSocketUpgradeTarget
    LoadExtensionsOptions
    NormalizeServerExtensionOptions
    PrepareServerExtensionOptions
    ServerExtensionConfig
    ServerExtensionInitContext
    ServerExtensionServiceRegistry

    Type Aliases

    ServerExtensionPhase

    Variables

    MAX_SERVER_EXTENSION_ROOT_PATH_LENGTH
    MJ_SERVER_EXTENSIONS_EXPORT
    MJ_SERVER_EXTENSIONS_PACKAGE_JSON_PATH
    RESERVED_SERVER_EXTENSION_ROOT_PREFIXES
    RESERVED_SERVER_EXTENSION_ROOTS

    Functions

    ComposeServerExtensionMountPath
    describeServerExtensionMount
    DescribeServerExtensionMount
    extractServerExtensionsFromModule
    ExtractServerExtensionsFromModule
    extractServerExtensionsFromPackageJson
    ExtractServerExtensionsFromPackageJson
    GetCorsOwningServerExtensionRoots
    InstallMediaUpgradeDispatcher
    IsGraphQLWsPath
    IsPathUnderServerExtensionRoot
    mergeServerExtensionConfigs
    MergeServerExtensionConfigs
    normalizeServerExtensionConfigs
    NormalizeServerExtensionConfigs
    prepareServerExtensionConfigs
    PrepareServerExtensionConfigs
    RegisterMediaUpgradeRoute
    serverExtensionRootsOverlap
    ServerExtensionRootsOverlap
    SkipForServerExtensionRoots
    validateServerExtensionRootPath
    ValidateServerExtensionRootPath