{"_id":"@astralibx/staff-engine","_rev":"4-6d940b7855c9fcbdf8a3816c0648a216","name":"@astralibx/staff-engine","dist-tags":{"latest":"0.2.3"},"versions":{"0.2.0":{"name":"@astralibx/staff-engine","version":"0.2.0","keywords":["staff","authentication","permissions","jwt"],"license":"MIT","_id":"@astralibx/staff-engine@0.2.0","maintainers":[{"name":"astralib","email":"1997hariprakash@gmail.com"}],"homepage":"https://github.com/Hariprakash1997/astralib#readme","bugs":{"url":"https://github.com/Hariprakash1997/astralib/issues"},"dist":{"shasum":"6c32317fadcdcbfdf7f5bae4370a9bc75df48151","tarball":"https://registry.npmjs.org/@astralibx/staff-engine/-/staff-engine-0.2.0.tgz","fileCount":8,"integrity":"sha512-mXUvvaaTO2H6jp4dxvbJBSr6rWpsw3xBa0eienXMOS6Tt/Li6kWv7Ev1X26eYwLk8XMH84DmEOJZh3QAWA80/A==","signatures":[{"sig":"MEQCIAd0uVEg/+DeaVSXQKBx+eRPV/4PY5iPslu3/lpqq5c7AiAm5KWMuMKuaSElMHRjDX8orMxQ1/kT/9yYs093Bmz6MA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":297742},"main":"dist/index.cjs","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.cjs"}}},"gitHead":"2469f8e7ec03a0b42cf5db266e50e3bfca011bcb","scripts":{"dev":"tsup --watch","lint":"eslint src/","test":"vitest run","build":"tsup","clean":"rm -rf dist","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"astralib","email":"1997hariprakash@gmail.com"},"repository":{"url":"git+https://github.com/Hariprakash1997/astralib.git","type":"git","directory":"packages/staff/staff-engine"},"_npmVersion":"10.9.4","description":"Staff management engine with JWT authentication, runtime-configurable permissions, and CRUD operations","directories":{},"_nodeVersion":"22.22.1","dependencies":{"zod":"^3.23.0","jsonwebtoken":"^9.0.0","@astralibx/core":"^1.2.1","@astralibx/staff-types":"^0.2.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^3.0.0","express":"^5.0.0","mongoose":"^8.12.1","typescript":"^5.8.2","@types/node":"^22.0.0","@types/express":"^5.0.0","@types/jsonwebtoken":"^9.0.0","@vitest/coverage-v8":"^3.0.0"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0","mongoose":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/staff-engine_0.2.0_1774187336317_0.8564304857917697","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@astralibx/staff-engine","version":"0.2.1","keywords":["staff","authentication","permissions","jwt"],"license":"MIT","_id":"@astralibx/staff-engine@0.2.1","maintainers":[{"name":"astralib","email":"1997hariprakash@gmail.com"}],"homepage":"https://github.com/Hariprakash1997/astralib#readme","bugs":{"url":"https://github.com/Hariprakash1997/astralib/issues"},"dist":{"shasum":"f04e60556b159611dc81baaf295595e4b0aa8daa","tarball":"https://registry.npmjs.org/@astralibx/staff-engine/-/staff-engine-0.2.1.tgz","fileCount":8,"integrity":"sha512-eWVReBz6TBbhF7hCisuo1/giNBoDUgEkl/zA0AnLC1oMN3+JNuIp6urYPdCTezvow56Zkrgtw8VCzO77EqNm8Q==","signatures":[{"sig":"MEUCIQCH0PblUlL7Z5Ia871P5qAM6oBABbptIwWgv7x7HfGJtAIgDspdJsVp1bRuS+o0+huZXLxryPGY/O6PnXw13K1rsDw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":321471},"main":"dist/index.cjs","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.cjs"}}},"gitHead":"aa24b21103d13c355fbf84fb143335febb4b0136","scripts":{"dev":"tsup --watch","lint":"eslint src/","test":"vitest run","build":"tsup","clean":"rm -rf dist","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"astralib","email":"1997hariprakash@gmail.com"},"repository":{"url":"git+https://github.com/Hariprakash1997/astralib.git","type":"git","directory":"packages/staff/staff-engine"},"_npmVersion":"10.9.4","description":"Staff management engine with JWT authentication, runtime-configurable permissions, and CRUD operations","directories":{},"_nodeVersion":"22.22.1","dependencies":{"zod":"^3.23.0","jsonwebtoken":"^9.0.0","@astralibx/core":"^1.2.1","@astralibx/staff-types":"^0.2.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^3.0.0","express":"^5.0.0","mongoose":"^8.12.1","typescript":"^5.8.2","@types/node":"^22.0.0","@types/express":"^5.0.0","@types/jsonwebtoken":"^9.0.0","@vitest/coverage-v8":"^3.0.0"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0","mongoose":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/staff-engine_0.2.1_1774200548355_0.0802934440689651","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@astralibx/staff-engine","version":"0.2.2","keywords":["staff","authentication","permissions","jwt"],"license":"MIT","_id":"@astralibx/staff-engine@0.2.2","maintainers":[{"name":"astralib","email":"1997hariprakash@gmail.com"}],"homepage":"https://github.com/Hariprakash1997/astralib#readme","bugs":{"url":"https://github.com/Hariprakash1997/astralib/issues"},"dist":{"shasum":"33438acc9da4eccfbbd7fd670bd958a3886c16c4","tarball":"https://registry.npmjs.org/@astralibx/staff-engine/-/staff-engine-0.2.2.tgz","fileCount":8,"integrity":"sha512-gXW7a9hA5vwJMdbRULbfbni0apS3zmby9ViX/tLqDbkwxA6Wis2dyM5Hqz98PLybv5zw7ogzA7nTHiOv9CMEXg==","signatures":[{"sig":"MEUCIQDWJwjf8YxFghvdX4VXagHhw8k5qxVCyMmt01CPUzuiQgIgY+Pm/1RVMOR7eTYAYIvTZFqLTJk616QWH2eFBZcoAQ4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":323072},"main":"dist/index.cjs","types":"dist/index.d.ts","module":"dist/index.mjs","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.cjs"}}},"gitHead":"0488f44a86c3dd1c854677012a7978c30f317df3","scripts":{"dev":"tsup --watch","lint":"eslint src/","test":"vitest run","build":"tsup","clean":"rm -rf dist","test:watch":"vitest","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"astralib","email":"1997hariprakash@gmail.com"},"repository":{"url":"git+https://github.com/Hariprakash1997/astralib.git","type":"git","directory":"packages/staff/staff-engine"},"_npmVersion":"10.9.4","description":"Staff management engine with JWT authentication, runtime-configurable permissions, and CRUD operations","directories":{},"_nodeVersion":"22.22.1","dependencies":{"zod":"^3.23.0","jsonwebtoken":"^9.0.0","@astralibx/core":"^1.2.1","@astralibx/staff-types":"^0.2.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^3.0.0","express":"^5.0.0","mongoose":"^8.12.1","typescript":"^5.8.2","@types/node":"^22.0.0","@types/express":"^5.0.0","@types/jsonwebtoken":"^9.0.0","@vitest/coverage-v8":"^3.0.0"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0","mongoose":"^7.0.0 || ^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/staff-engine_0.2.2_1774230935478_0.23962656030168228","host":"s3://npm-registry-packages-npm-production"}},"0.2.3":{"name":"@astralibx/staff-engine","version":"0.2.3","description":"Staff management engine with JWT authentication, runtime-configurable permissions, and CRUD operations","repository":{"type":"git","url":"git+https://github.com/Hariprakash1997/astralib.git","directory":"packages/staff/staff-engine"},"main":"dist/index.cjs","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.ts","default":"./dist/index.cjs"}}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"eslint src/","clean":"rm -rf dist"},"keywords":["staff","authentication","permissions","jwt"],"license":"MIT","dependencies":{"@astralibx/staff-types":"^0.2.0","@astralibx/core":"^1.2.1","jsonwebtoken":"^9.0.0","zod":"^3.23.0"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0","mongoose":"^7.0.0 || ^8.0.0"},"devDependencies":{"@types/express":"^5.0.0","@types/jsonwebtoken":"^9.0.0","@types/node":"^22.0.0","@vitest/coverage-v8":"^3.0.0","express":"^5.0.0","mongoose":"^8.12.1","tsup":"^8.0.0","typescript":"^5.8.2","vitest":"^3.0.0"},"_id":"@astralibx/staff-engine@0.2.3","gitHead":"18ae0348539bf3644ee008d3c122bd39abff0db4","bugs":{"url":"https://github.com/Hariprakash1997/astralib/issues"},"homepage":"https://github.com/Hariprakash1997/astralib#readme","_nodeVersion":"22.22.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-wvESbPW/x1GSEvCuuaMjoo6m7kkrzbQIhEyI76CIpQbu2gN4CbBSe8R8GqiZQqdRwH5zuxOrOygOcP70DTrHQw==","shasum":"0ea7afe7580f014172102d1918d22baffd1d217d","tarball":"https://registry.npmjs.org/@astralibx/staff-engine/-/staff-engine-0.2.3.tgz","fileCount":8,"unpackedSize":323376,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDBy+EdH2+14CvSMVOSKPQGXv9gLwJluaocihnMtG9iCAIhAJpdrDBG8aO7vZL7FPXtj0fvHnUU0fuY+cpmhbYGPAdW"}]},"_npmUser":{"name":"astralib","email":"1997hariprakash@gmail.com"},"directories":{},"maintainers":[{"name":"astralib","email":"1997hariprakash@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/staff-engine_0.2.3_1774259917140_0.8355347239522148"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-22T13:48:56.182Z","modified":"2026-03-23T09:58:37.393Z","0.2.0":"2026-03-22T13:48:56.461Z","0.2.1":"2026-03-22T17:29:08.502Z","0.2.2":"2026-03-23T01:55:35.633Z","0.2.3":"2026-03-23T09:58:37.276Z"},"bugs":{"url":"https://github.com/Hariprakash1997/astralib/issues"},"license":"MIT","homepage":"https://github.com/Hariprakash1997/astralib#readme","keywords":["staff","authentication","permissions","jwt"],"repository":{"type":"git","url":"git+https://github.com/Hariprakash1997/astralib.git","directory":"packages/staff/staff-engine"},"description":"Staff management engine with JWT authentication, runtime-configurable permissions, and CRUD operations","maintainers":[{"name":"astralib","email":"1997hariprakash@gmail.com"}],"readme":"# @astralibx/staff-engine\n\n[![npm version](https://img.shields.io/npm/v/@astralibx/staff-engine.svg)](https://www.npmjs.com/package/@astralibx/staff-engine)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n\nStaff 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.\n\n## Install\n\n```bash\nnpm install @astralibx/staff-engine\n```\n\n### Peer Dependencies\n\n| Package | Required |\n|---------|----------|\n| `express` | Yes |\n| `mongoose` | Yes |\n\n```bash\nnpm install express mongoose\n```\n\n## Quick Start\n\n```ts\nimport { createStaffEngine } from '@astralibx/staff-engine';\nimport mongoose from 'mongoose';\nimport bcrypt from 'bcryptjs';\nimport express from 'express';\n\nconst app = express();\napp.use(express.json());\n\nconst connection = mongoose.createConnection('mongodb://localhost:27017/myapp');\n\nconst engine = createStaffEngine({\n  db: { connection },\n  auth: {\n    jwtSecret: process.env.JWT_SECRET!,\n  },\n  adapters: {\n    hashPassword: (plain) => bcrypt.hash(plain, 12),\n    comparePassword: (plain, hash) => bcrypt.compare(plain, hash),\n  },\n});\n\napp.use('/api/staff', engine.routes);\napp.listen(3000);\n```\n\n## Features\n\n### Authentication\n\n- **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.\n- **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.\n- **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.\n- **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.\n\n### Staff Management\n\n- **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.\n- **Email uniqueness** -- duplicate email rejected with `STAFF_EMAIL_EXISTS`. Can be disabled via `requireEmailUniqueness: false`.\n- **Paginated list with filters** -- `GET /` supports `status`, `role`, `page`, and `limit` query params. Returns `data[]` + `pagination` with total counts.\n- **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.\n- **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.\n\n### Permissions\n\n- **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.\n\n> **Note:** Permission groups use `label` for display text and `groupId` for the unique identifier -- not `name`.\n\n```ts\nawait engine.permissions.createGroup({\n  groupId: 'chat-management',    // unique identifier, kebab-case\n  label: 'Chat Management',      // display text shown in UI\n  permissions: [\n    { key: 'chat:view', label: 'View chats', type: 'view' },\n    { key: 'chat:edit', label: 'Edit chats', type: 'edit' },\n  ],\n  sortOrder: 1,\n});\n```\n- **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`.\n- **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`.\n- **Owner bypasses all checks** -- `requirePermission` middleware skips the permission check entirely when `req.user.role === 'owner'`.\n\n### Security\n\n- **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.\n- **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.\n- **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.\n- **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.\n\n### Integration\n\n- **`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.\n- **`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.\n- **`requireRole` middleware** -- `engine.auth.requireRole('owner', 'staff')` checks the token's resolved role against the provided list.\n\n## Routes\n\n| Method | Path | Auth | Description |\n|--------|------|------|-------------|\n| `POST` | `/setup` | Public | Create initial owner account (auto-locks after first use) |\n| `POST` | `/login` | Public | Authenticate and receive JWT token |\n| `GET` | `/me` | Staff | Get current staff profile and resolved permissions |\n| `PUT` | `/me/password` | Staff | Change own password (only when `allowSelfPasswordChange: true`) |\n| `GET` | `/` | Owner | List staff with pagination and status/role filters |\n| `POST` | `/` | Owner | Create a new staff member |\n| `PUT` | `/:staffId` | Owner | Update staff name, email, metadata |\n| `PUT` | `/:staffId/permissions` | Owner | Replace staff permission set |\n| `PUT` | `/:staffId/status` | Owner | Activate or deactivate a staff member |\n| `PUT` | `/:staffId/password` | Owner | Reset a staff member's password |\n| `GET` | `/permission-groups` | Staff | List all permission groups |\n| `POST` | `/permission-groups` | Owner | Create a new permission group |\n| `PUT` | `/permission-groups/:groupId` | Owner | Update a permission group's entries or label |\n| `DELETE` | `/permission-groups/:groupId` | Owner | Delete a permission group |\n\n## Architecture\n\nThe factory function returns a single `StaffEngine` object:\n\n| Export | Purpose |\n|--------|---------|\n| `engine.routes` | Express router -- mount at `/api/staff` or similar |\n| `engine.auth.verifyToken` | Middleware to authenticate any route |\n| `engine.auth.requirePermission(...keys)` | Middleware for permission-gated routes |\n| `engine.auth.ownerOnly` | Middleware to restrict to owner role |\n| `engine.auth.resolveStaff(token)` | Programmatic token resolution (no HTTP response) |\n| `engine.staff` | Direct access to `StaffService` for programmatic use |\n| `engine.permissions` | Direct access to `PermissionService` |\n| `engine.models` | Mongoose models (`Staff`, `PermissionGroup`) |\n| `engine.destroy()` | Flush permission cache and clean up resources |\n\n## Seeding Data\n\nSchema factory functions are exported so you can seed data or run scripts without creating a full engine instance:\n\n```ts\nimport { createStaffModel, createPermissionGroupModel } from '@astralibx/staff-engine';\n\nconst Staff = createStaffModel(connection);\nconst PermissionGroup = createPermissionGroupModel(connection);\n\nawait PermissionGroup.create({\n  groupId: 'admin',\n  label: 'Admin',\n  permissions: [\n    { key: 'chat:view', label: 'View chats', type: 'view' },\n    { key: 'chat:edit', label: 'Edit chats', type: 'edit' },\n  ],\n  sortOrder: 1,\n});\n```\n\n## Redis Key Prefix (Required for Multi-Project Deployments)\n\n> **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.\n\n```ts\nconst engine = createStaffEngine({\n  redis: {\n    connection: redis,\n    keyPrefix: 'myproject:staff:', // REQUIRED if sharing Redis\n  },\n  // ...\n});\n```\n\n## Links\n\n- [GitHub](https://github.com/Hariprakash1997/astralib/tree/main/packages/staff/staff-engine)\n- [staff-types](https://github.com/Hariprakash1997/astralib/tree/main/packages/staff/staff-types)\n- [staff-ui](https://github.com/Hariprakash1997/astralib/tree/main/packages/staff/staff-ui)\n\n## License\n\nMIT\n","readmeFilename":"README.md"}