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

@astralibx/staff-engine

v0.2.3

Published

Staff management engine with JWT authentication, runtime-configurable permissions, and CRUD operations

Readme

@astralibx/staff-engine

npm version License: MIT

Staff management backend with JWT authentication, role-based token expiry, IP-based rate limiting, runtime-configurable permission groups, and a REST admin API. No hardcoded permissions -- all groups and entries are defined at runtime via API.

Install

npm install @astralibx/staff-engine

Peer Dependencies

| Package | Required | |---------|----------| | express | Yes | | mongoose | Yes |

npm install express mongoose

Quick Start

import { createStaffEngine } from '@astralibx/staff-engine';
import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
import express from 'express';

const app = express();
app.use(express.json());

const connection = mongoose.createConnection('mongodb://localhost:27017/myapp');

const engine = createStaffEngine({
  db: { connection },
  auth: {
    jwtSecret: process.env.JWT_SECRET!,
  },
  adapters: {
    hashPassword: (plain) => bcrypt.hash(plain, 12),
    comparePassword: (plain, hash) => bcrypt.compare(plain, hash),
  },
});

app.use('/api/staff', engine.routes);
app.listen(3000);

Features

Authentication

  • JWT with role-based expiry -- login returns a JWT signed with jwtSecret. Owners get ownerTokenExpiry (default 30d); staff members get staffTokenExpiry (default 24h). Both are configurable.
  • Login with IP rate limiting -- POST /login tracks failed attempts per IP using Redis sorted sets or in-memory fallback. Locks out after maxAttempts failures within windowMs (default: 5 attempts / 15 min). Returns STAFF_RATE_LIMITED on lockout.
  • Setup route auto-locks -- POST /setup creates the initial owner account and then permanently locks itself. Any subsequent call returns STAFF_SETUP_ALREADY_COMPLETE. This route is always public and never requires a token.
  • Token distinguishes expired vs invalid -- verifyToken middleware checks TokenExpiredError separately from all other JWT errors and returns STAFF_TOKEN_EXPIRED vs STAFF_TOKEN_INVALID so clients can show the correct message.

Staff Management

  • Create staff with permissions -- owner creates staff with name, email, password hash (via adapter), optional initial permissions, and optional externalUserId for linking to external identity systems.
  • Email uniqueness -- duplicate email rejected with STAFF_EMAIL_EXISTS. Can be disabled via requireEmailUniqueness: false.
  • Paginated list with filters -- GET / supports status, role, page, and limit query params. Returns data[] + pagination with total counts.
  • Owner-only access -- all staff CRUD routes (GET /, POST /, PUT /:id, PUT /:id/permissions, PUT /:id/status, PUT /:id/password) require owner role. GET /me and PUT /me/password are staff-accessible.
  • No-delete policy -- staff records are never hard-deleted. Deactivate to revoke access. Inactive staff tokens are rejected on every authenticated request even before JWT expiry.

Permissions

  • Runtime-configurable groups via API -- POST /permission-groups creates a named group with permission entries. Groups have groupId, label, sortOrder, and an array of entries (key, label, type). No redeploy required to define new permissions.

Note: Permission groups use label for display text and groupId for the unique identifier -- not name.

await engine.permissions.createGroup({
  groupId: 'chat-management',    // unique identifier, kebab-case
  label: 'Chat Management',      // display text shown in UI
  permissions: [
    { key: 'chat:view', label: 'View chats', type: 'view' },
    { key: 'chat:edit', label: 'Edit chats', type: 'edit' },
  ],
  sortOrder: 1,
});
  • Edit-to-view cascade -- when granting a permission ending in .edit, the corresponding .view key is automatically required. The engine validates this on PUT /:id/permissions.
  • Permission cache (Redis or in-memory) -- each staff member's resolved permission list is cached after the first lookup. TTL is permissionCacheTtlMs (default 5 min). Cache is invalidated immediately on updatePermissions or updateStatus.
  • Owner bypasses all checks -- requirePermission middleware skips the permission check entirely when req.user.role === 'owner'.

Security

  • Rate limiting (configurable window/max) -- windowMs and maxAttempts are configurable at engine creation time. Uses Redis ZADD/ZREMRANGEBYSCORE for accurate per-IP tracking across multiple processes.
  • Last-owner guard -- PUT /:id/status with status: 'inactive' checks that at least one other active owner exists before allowing the change. Returns STAFF_LAST_OWNER_GUARD if blocked.
  • Inactive token rejection -- every call through verifyToken re-reads staff status from MongoDB (with optional tenant filter). Inactive or pending accounts are rejected with STAFF_TOKEN_INVALID even with an otherwise valid JWT.
  • Status checks on every request -- verifyToken loads status and role fresh on each request. There is no session store; the database is the source of truth.

Integration

  • resolveStaff for programmatic token resolution -- engine.auth.resolveStaff(token) returns { staffId, role, permissions } or null without sending an HTTP response. Useful for WebSocket authentication or programmatic access in other modules.
  • requirePermission middleware for consumer routes -- engine.auth.requirePermission('contacts.view', 'contacts.edit') returns an Express middleware that checks the current user's permissions. Owners always pass. Non-owners are rejected with STAFF_INSUFFICIENT_PERMISSIONS and a list of missing keys.
  • requireRole middleware -- engine.auth.requireRole('owner', 'staff') checks the token's resolved role against the provided list.

Routes

| Method | Path | Auth | Description | |--------|------|------|-------------| | POST | /setup | Public | Create initial owner account (auto-locks after first use) | | POST | /login | Public | Authenticate and receive JWT token | | GET | /me | Staff | Get current staff profile and resolved permissions | | PUT | /me/password | Staff | Change own password (only when allowSelfPasswordChange: true) | | GET | / | Owner | List staff with pagination and status/role filters | | POST | / | Owner | Create a new staff member | | PUT | /:staffId | Owner | Update staff name, email, metadata | | PUT | /:staffId/permissions | Owner | Replace staff permission set | | PUT | /:staffId/status | Owner | Activate or deactivate a staff member | | PUT | /:staffId/password | Owner | Reset a staff member's password | | GET | /permission-groups | Staff | List all permission groups | | POST | /permission-groups | Owner | Create a new permission group | | PUT | /permission-groups/:groupId | Owner | Update a permission group's entries or label | | DELETE | /permission-groups/:groupId | Owner | Delete a permission group |

Architecture

The factory function returns a single StaffEngine object:

| Export | Purpose | |--------|---------| | engine.routes | Express router -- mount at /api/staff or similar | | engine.auth.verifyToken | Middleware to authenticate any route | | engine.auth.requirePermission(...keys) | Middleware for permission-gated routes | | engine.auth.ownerOnly | Middleware to restrict to owner role | | engine.auth.resolveStaff(token) | Programmatic token resolution (no HTTP response) | | engine.staff | Direct access to StaffService for programmatic use | | engine.permissions | Direct access to PermissionService | | engine.models | Mongoose models (Staff, PermissionGroup) | | engine.destroy() | Flush permission cache and clean up resources |

Seeding Data

Schema factory functions are exported so you can seed data or run scripts without creating a full engine instance:

import { createStaffModel, createPermissionGroupModel } from '@astralibx/staff-engine';

const Staff = createStaffModel(connection);
const PermissionGroup = createPermissionGroupModel(connection);

await PermissionGroup.create({
  groupId: 'admin',
  label: 'Admin',
  permissions: [
    { key: 'chat:view', label: 'View chats', type: 'view' },
    { key: 'chat:edit', label: 'Edit chats', type: 'edit' },
  ],
  sortOrder: 1,
});

Redis Key Prefix (Required for Multi-Project Deployments)

WARNING: If multiple projects share the same Redis server, you MUST set a unique keyPrefix per project. Without this, rate limiter state and permission cache entries will collide across projects.

const engine = createStaffEngine({
  redis: {
    connection: redis,
    keyPrefix: 'myproject:staff:', // REQUIRED if sharing Redis
  },
  // ...
});

Links

License

MIT