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

ws-request-manager

v1.0.0

Published

WebSocket middleware for Node.js servers with pluggable auth, designed to work with http-request-manager

Downloads

188

Readme

ws-request-manager

WebSocket middleware for Node.js servers, designed to work with http-request-manager on the frontend.

Provides channels, pub/sub messaging, notifications, heartbeat, delivery tracking, and daily cleanup — all behind a consumer-provided auth function. The message protocol matches exactly what http-request-manager's WebSocketManagerService and WebSocketMessageService send, so the frontend connects seamlessly.

http-request-manager Support

This middleware implements the full WebSocket protocol that http-request-manager expects. Every message type the Angular frontend sends is handled:

Channel Management

| Message type | FE Service Method | What It Does | |---|---|---| | subscribe | openChannel(channel) | Adds client to channel, sends channel list | | unsubscribe | closeChannel(channel) | Removes client from channel | | createChannel | createChannel(name) | Creates a PUB- channel, broadcasts update | | deleteChannel | deleteChannel(name) | Removes channel (if empty), notifies subscribers | | getChannels | getChannels() | Returns list of available PUB- channels | | getUsers | getUsersInChannel(channel) | Returns users subscribed to a channel |

Messaging

| Message type | FE Service Method | What It Does | |---|---|---| | stateManagerMessage | sendStateMessage(channel, data) | Broadcasts state update to all channel subscribers (including sender) | | message | sendChannelMessage(channel, data) | Sends message to channel subscribers (excluding sender) | | broadcast | broadcastMessage(data) | Sends to ALL connected clients across all channels | | channelMessage | sendChannelMessageToChannels(channels, data) | Sends to multiple channels at once | | userMessage | sendUserMessage(userId, data) | Sends a direct message to a specific user's channel |

Notifications

| Message type | FE Service Method | What It Does | |---|---|---| | subscribeNotifications | subscribeNotifications(channel) | Subscribes to a MES- notification channel | | unsubscribeNotifications | unsubscribeNotifications(channel) | Unsubscribes from a notification channel | | notification | sendNotification(channel, data) | Sends notification to all subscribers of a MES- channel | | getNotificationChannels | getNotificationChannels() | Returns available notification channels | | getTodaysNotificationChannels | getTodaysNotificationChannels() | Returns today's notification channels | | createNotificationChannel | createNotificationChannel(name) | Creates a new MES- notification channel |

Message Tracking

| Message type | FE Service Method | What It Does | |---|---|---| | messageAckBatch | acknowledgeMessages(ids) | Acknowledges batch of delivered messages | | gapRequest | requestMessageGap(info) | Requests missing messages between IDs |

Server Responses

The server sends these response types back to the frontend:

| Response type | Trigger | Description | |---|---|---| | channelsList | On connect, after create/delete channel | Full list of available channels | | channelCreated | After createChannel | Confirms channel creation | | channelDeleted | After deleteChannel | Confirms channel deletion | | success | After subscribe | Confirms subscription with user data | | info | Re-subscribe to already-joined channel | Info message | | unsubscribed | After unsubscribe | Confirms unsubscription | | usersInChannel | After getUsers | List of users in channel(s) | | notificationSubscribed | After subscribeNotifications | Confirms notification subscription | | notificationUnsubscribed | After unsubscribeNotifications | Confirms notification unsubscription | | notificationChannelsList | After getNotificationChannels | List of notification channels | | todaysNotificationChannelsList | After getTodaysNotificationChannels | Today's notification channels | | gapResponse | After gapRequest | Response with any found messages | | error | On invalid operations | Error with description |

HTTP REST Routes

Mounted by register(app, auth):

| Route | Method | Description | |---|---|---| | /ws/channels | GET | List active PUB- channels | | /ws/connections | GET | Channel→user connection table | | /ws/broadcast | POST | Push messages to channel(s) from backend |

Heartbeat

The server sends WebSocket ping frames every 30 seconds (configurable via WS_HEARTBEAT_MS). Clients that don't respond with pong are terminated.

Installation

npm install ws-request-manager

Peer Dependencies

This middleware is designed to run alongside http-request-manager on the backend. The frontend connects using http-request-manager's WebSocketManagerService.

npm install ws-request-manager express ws
npm install http-request-manager  # frontend package

Quick Start

import express from 'express';
import { createServer } from 'http';
import { register, registerServer, destroy, noAuth } from 'ws-request-manager';

const app = express();
const server = createServer(app);

// Pass your auth function — noAuth() for dev, your own for production
await register(app, noAuth);
await registerServer(server, noAuth);

server.listen(3000, () => console.log('Server running on :3000'));

// Graceful shutdown
process.on('SIGTERM', () => { destroy(); server.close(); });

That's it. The library handles WebSocket upgrades, channels, messages, heartbeat, and cleanup. You provide the auth.

Lifecycle Hooks

| Hook | Purpose | |------|---------| | register(app, auth) | Mount HTTP routes (/ws/channels, /ws/broadcast, /ws/connections) | | registerServer(server, auth) | Attach WebSocket upgrade handler, start heartbeat + cleanup | | destroy() | Clean up timers (heartbeat, daily cleanup) |

Both register() and registerServer() require an auth: WsAuthFn parameter.

Pluggable Auth

The library does NOT implement any authentication strategy. Instead, it accepts a WsAuthFn — an async function you write that decides whether to allow or reject each WebSocket connection.

WsAuthFn

type WsAuthFn = (socket: WebSocket, request: IncomingMessage) => Promise<WsUser>;
  • On success: return a WsUser object (must have sub property)
  • On failure: throw a WsAuthError with a close code, or any Error (defaults to code 4001)
import { WsAuthFn, WsAuthError } from 'ws-request-manager';

const myAuth: WsAuthFn = async (socket, request) => {
  const params = parseQuery(request.url);
  if (params.token === 'my-secret') {
    return { sub: 'user-123', type: 'service' };  // ✅ allow
  }
  throw new WsAuthError('Invalid token', 4001);     // ❌ reject
};

WsUser

interface WsUser {
  sub: string;       // Required — unique user identifier
  [key: string]: any; // Optional — any additional properties
}

The returned WsUser is attached to socket.user and available in all message handlers.

WsAuthError

class WsAuthError extends Error {
  readonly code: number;  // WebSocket close code (default: 4001)
  constructor(message: string, code?: number);
}

Common close codes:

| Code | Meaning | |------|---------| | 4000 | No token provided | | 4001 | Invalid credentials (default) | | 4002 | IP blocked (rate limited) | | 4003 | Token expired |

noAuth

Development convenience — allows all connections:

import { noAuth } from 'ws-request-manager';

await register(app, noAuth);
await registerServer(server, noAuth);
// ⚠️  Logs "WS running without authentication" on each connection

Auth Helpers

The library exports utilities for writing auth functions:

import { parseQuery, parseCookies, getClientIp } from 'ws-request-manager';

| Helper | Signature | Description | |--------|-----------|-------------| | parseQuery(url) | (url: string \| undefined) => Record<string, string> | Parse URL query parameters | | parseCookies(header) | (header: string \| undefined) => Record<string, string> | Parse cookie header | | getClientIp(request) | (request: IncomingMessage) => string | Extract client IP (x-forwarded-for, x-real-ip, remoteAddress) |

Example Auth Functions

API Key Auth

import { WsAuthFn, WsAuthError, parseQuery } from 'ws-request-manager';

function createApiKeyAuth(apiKey: string): WsAuthFn {
  return async (_socket, request) => {
    const params = parseQuery(request.url);
    if (params.token !== apiKey) {
      throw new WsAuthError('Invalid API key', 4001);
    }
    return { sub: 'api-client', type: 'service' };
  };
}

// Usage:
await register(app, createApiKeyAuth(process.env.STATIC_API_KEY!));
await registerServer(server, createApiKeyAuth(process.env.STATIC_API_KEY!));

JWT Auth

import jwt from 'jsonwebtoken';
import { WsAuthFn, WsAuthError, parseQuery } from 'ws-request-manager';

function createJwtAuth(secret: string): WsAuthFn {
  return async (_socket, request) => {
    const params = parseQuery(request.url);
    const token = params.token;
    if (!token) throw new WsAuthError('No token provided', 4000);
    return new Promise((resolve, reject) => {
      jwt.verify(token, secret, (err, decoded) => {
        if (err) reject(new WsAuthError(err.message, 4001));
        else resolve(decoded as WsUser);
      });
    });
  };
}

Cookie-Based Auth (BFF Session)

import { WsAuthFn, WsAuthError, parseCookies } from 'ws-request-manager';

function createCookieAuth(validateSession: (sid: string) => Promise<WsUser | null>): WsAuthFn {
  return async (_socket, request) => {
    const cookies = parseCookies(request.headers.cookie);
    const sessionId = cookies['bff_0'];
    if (!sessionId) throw new WsAuthError('No session cookie', 4000);
    const user = await validateSession(sessionId);
    if (!user) throw new WsAuthError('Invalid session', 4001);
    return user;
  };
}

Composable Auth Patterns

Auth functions are composable — wrap, chain, and combine them without modifying the library.

Rate Limiting

Rate limiting wraps your auth function. It's NOT separate middleware — the consumer owns the full auth pipeline.

import { WsAuthFn, WsAuthError, getClientIp } from 'ws-request-manager';

function createRateLimitedAuth(
  authFn: WsAuthFn,
  options: { maxFailures?: number; blockDurationMs?: number } = {}
): WsAuthFn {
  const { maxFailures = 10, blockDurationMs = 300_000 } = options;
  const failures = new Map<string, { count: number; lastLog: number; blockedAt: number | null }>();

  return async (socket, request) => {
    const ip = getClientIp(request);
    const info = failures.get(ip);

    // Check if IP is blocked
    if (info?.blockedAt) {
      const elapsed = Date.now() - info.blockedAt;
      if (elapsed < blockDurationMs) {
        const retryAfter = Math.ceil((blockDurationMs - elapsed) / 1000);
        throw new WsAuthError(`IP blocked. Retry after ${retryAfter}s`, 4002);
      }
      failures.delete(ip); // Block expired
    }

    try {
      const user = await authFn(socket, request);
      failures.delete(ip); // Success — reset
      return user;
    } catch (authErr) {
      // Track failure
      const now = Date.now();
      const entry = failures.get(ip) || { count: 0, lastLog: 0, blockedAt: null };
      entry.count++;
      if (entry.count >= maxFailures && !entry.blockedAt) {
        entry.blockedAt = now;
      }
      failures.set(ip, entry);
      throw authErr;
    }
  };
}

Usage:

const auth = createRateLimitedAuth(
  createApiKeyAuth(process.env.STATIC_API_KEY!),
  { maxFailures: 5, blockDurationMs: 60_000 }
);
await register(app, auth);
await registerServer(server, auth);

Multi-Auth (Fallback)

Try multiple auth methods in order. First success wins.

function createMultiAuth(...authFns: WsAuthFn[]): WsAuthFn {
  return async (socket, request) => {
    let lastError: Error | null = null;
    for (const fn of authFns) {
      try {
        return await fn(socket, request);
      } catch (err) {
        lastError = err as Error;
      }
    }
    throw lastError || new WsAuthError('All auth methods failed', 4001);
  };
}

Usage:

const auth = createMultiAuth(
  createApiKeyAuth(process.env.STATIC_API_KEY!),
  createJwtAuth(process.env.JWT_SECRET_KEY!)
);

Full Production: Rate-Limited Multi-Auth

const auth = createRateLimitedAuth(
  createMultiAuth(
    createApiKeyAuth(process.env.STATIC_API_KEY!),
    createJwtAuth(process.env.JWT_SECRET_KEY!)
  ),
  { maxFailures: 10, blockDurationMs: 300_000 }
);

await register(app, auth);
await registerServer(server, auth);

This tries API key first, then JWT, and blocks IPs after 10 failures for 5 minutes.

Integration with http-request-manager

ws-request-manager is designed to work alongside http-request-manager in a BFF (Backend-For-Frontend) server. The BFF server handles HTTP requests via http-request-manager and WebSocket connections via ws-request-manager.

Setup

// bff-server.ts
import express from 'express';
import { createServer } from 'http';
import { register, registerServer, destroy } from 'ws-request-manager';
import { WsAuthFn, WsAuthError, parseQuery, getClientIp } from 'ws-request-manager';

// 1. Create your auth function
const authFn: WsAuthFn = createRateLimitedAuth(
  createMultiAuth(
    createApiKeyAuth(process.env.STATIC_API_KEY!),
    createJwtAuth(process.env.JWT_SECRET_KEY!)
  )
);

// 2. Create Express app and HTTP server
const app = express();
app.use(express.json());

// 3. Mount WS HTTP routes (adds /ws/channels, /ws/broadcast, /ws/connections)
await register(app, authFn);

// 4. Create HTTP server and attach WebSocket upgrade handler
const server = createServer(app);
await registerServer(server, authFn);

// 5. Listen
server.listen(3000, () => {
  console.log('BFF server running on :3000');
});

// 6. Graceful shutdown
process.on('SIGTERM', () => { destroy(); server.close(); });

How It Works

┌─────────────────────────────────────────────────────────┐
│                    BFF Server (Express)                  │
│                                                          │
│  ┌──────────────────┐  ┌──────────────────────────────┐ │
│  │ http-request-mgr │  │     ws-request-manager        │ │
│  │                  │  │                                │ │
│  │  HTTP routes:    │  │  register(app, authFn) ──────►│ │
│  │  /api/*          │  │    → mounts /ws/* HTTP routes  │ │
│  │                  │  │                                │ │
│  │                  │  │  registerServer(srv, authFn) ─►│ │
│  │                  │  │    → handles WS upgrade        │ │
│  │                  │  │    → calls authFn(socket, req) │ │
│  │                  │  │    → sets socket.user on auth  │ │
│  │                  │  │    → starts heartbeat/cleanup  │ │
│  └──────────────────┘  └──────────────────────────────┘ │
│                                                          │
│  Auth function (provided by consumer):                    │
│  ┌────────────────────────────────────────────────────┐  │
│  │ authFn(socket, request) → Promise<WsUser>          │  │
│  │                                                    │  │
│  │  ✅ Return WsUser  → connection allowed            │  │
│  │  ❌ Throw Error    → connection rejected (4001)    │  │
│  │  ❌ Throw WsAuthError → rejected (custom code)      │  │
│  └────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Auth Flow

  1. Client initiates WebSocket upgrade to /ws?token=abc123
  2. registerServer intercepts the upgrade request
  3. Library calls authFn(socket, request) — your auth function
  4. On success: socket.user is set, connection is established, channel list is sent
  5. On failure: socket is closed with the error code, client receives AUTH_FAILED message

Frontend Connection (http-request-manager)

The frontend connects via http-request-manager's WebSocket service:

// Angular frontend
import { WSOptions } from 'http-request-manager';

const wsOptions: WSOptions = {
  wsServer: 'ws://localhost:3000/ws',
  jwtToken: 'my-jwt-token',  // Passed as ?token= query parameter
  channels: ['PUB-general', 'PUB-notifications']
};

// The token value is opaque to http-request-manager —
// the backend decides what to do with it.

The jwtToken is sent as the ?token= query parameter during the WebSocket upgrade. Your WsAuthFn on the backend decides how to validate it — JWT, API key, session cookie, or anything else.

Environment Variables

| Variable | Default | Description | |----------|---------|-------------| | WS_ENABLED | true | Enable/disable WebSocket functionality | | WS_PATH | /ws | WebSocket upgrade path | | WS_PORT | 4000 | Dedicated port for WS (falls back to PORT) | | WS_HEARTBEAT_MS | 30000 | Heartbeat interval in milliseconds | | WS_LOG_PAYLOADS | false | Log full message payloads (verbose) | | WS_LOG_PAYLOADS_MAX_CHARS | 8000 | Max chars for payload logging | | WS_MESSAGES_DB_PATH | ./data/ws-messages.db | Path to messages SQLite database | | WS_NOTIFICATIONS_DB_PATH | ./data/ws-notifications.db | Path to notifications SQLite database | | WS_MESSAGE_TTL_DAYS | 30 | Days to retain messages (0 = forever) |

Exports

// Lifecycle
export { register, registerServer, destroy } from './index';

// Auth types
export { WsUser, WsAuthFn, WsAuthError } from './auth/types';

// Auth utilities
export { noAuth } from './auth/noAuth';
export { parseQuery, parseCookies, getClientIp } from './auth/helpers';

Node.js 22+ Requirement

This library requires Node.js 22 or later. The sample server.js enforces this at startup:

const REQUIRED_NODE_MAJOR = 22;
if (parseInt(process.version.slice(1).split('.')[0]) < REQUIRED_NODE_MAJOR) {
  console.error(`❌ Node.js ${REQUIRED_NODE_MAJOR}+ required`);
  process.exit(1);
}

Sample Server

A complete standalone sample server is available at server.js in the repository root. It demonstrates:

  • noAuth — development mode (no authentication)
  • createApiKeyAuth(key) — static API key validation
  • createJwtAuth(secret) — JWT token verification
  • createCookieAuth(validate) — BFF session cookie validation
  • createRateLimitedAuth(authFn, opts) — IP-based rate limiting wrapper
  • createMultiAuth(...fns) — try multiple auth methods in order
  • createUpgradeHandler(wss, authFn) — WebSocket upgrade handler with auth
# Run the sample server
node server.js

# Test it
node test/sample-test-client.js --test=noauth
node test/sample-test-client.js --test=apikey --key=my-secret-key

See the Pluggable Auth section above for auth function examples.

Server Protocol Handlers

The server.js sample implements the full ws-request-manager message protocol, matching what the frontend WebSocketManagerService and WebSocketMessageService send. Each message from the FE includes a type field that the server dispatches:

| Message type | FE Service Method | Server Handler | |---|---|---| | subscribe | openChannel(channel) | Adds client to channel, sends channel list | | unsubscribe | closeChannel(channel) | Removes client from channel | | createChannel | createChannel(name) | Creates channel, adds client, broadcasts update | | deleteChannel | deleteChannel(name) | Removes channel, notifies subscribers | | getChannels | getChannels() | Returns available channels | | getUsers | getUsers(channel) | Returns users in a channel | | stateManagerMessage | sendStateMessage(channel, data) | Broadcasts state to channel subscribers (including sender) | | message | sendChannelMessage(channel, data) | Sends message to channel subscribers (excluding sender) | | broadcast | broadcastMessage(data) | Sends to ALL connected clients | | channelMessage | sendChannelMessage(channel, data) | Sends to specific channel | | userMessage | sendUserMessage(userId, data) | Sends to specific user | | subscribeNotifications | subscribeNotifications(channel) | Subscribes to notification channel | | unsubscribeNotifications | unsubscribeNotifications(channel) | Unsubscribes from notification channel | | notification | sendNotification(channel, data) | Sends notification to channel subscribers | | getNotificationChannels | getNotificationChannels() | Returns available notification channels | | getTodaysNotificationChannels | getTodaysNotificationChannels() | Returns today's notification channels | | createNotificationChannel | createNotificationChannel(name) | Creates notification channel | | messageAckBatch | acknowledgeMessages(ids) | Acknowledges message delivery | | gapRequest | requestMessageGap(info) | Requests missing messages |

Middleware Test Client

A comprehensive test client at test/ws-middleware-test.js tests all FE service functions against the server:

# Start the server first
node server.js

# Run middleware tests (no-auth mode)
node test/ws-middleware-test.js

# Run with API key auth
node test/ws-middleware-test.js --auth=apikey --key=my-secret-key

# Run with JWT auth
node test/ws-middleware-test.js --auth=jwt --secret=my-jwt-secret

The test client mirrors the exact message types and payloads that the Angular WebSocketManagerService and WebSocketMessageService send, ensuring the server middleware handles the full FE protocol correctly.