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

strapi-security-suite

v0.4.0

Published

All-in-one authentication and session security plugin for Strapi v5

Readme

🛡️ Strapi Security Suite

The admin security plugin that takes your sessions personally — and now scales horizontally.

One plugin. Auto-logout. Single-session enforcement. Token revocation. Heartbeat. Multi-pod-safe. Built for Strapi v5. Backed by your existing database. Zero new infrastructure.


🤔 What Is This?

Imagine a bouncer at a nightclub. But the nightclub is your Strapi admin panel, the bouncer has a perfect memory, never sleeps, and will physically escort your idle admins out the door after 30 minutes of doing nothing.

That's this plugin. And in v0.4, the bouncer works across every door of every venue in the franchise — not just the one he's standing at.

  🔐 Admin logs in (Pod A)
   |
   |  👀 Activity tracked → DB (visible to all pods)
   |  🫀 Client heartbeat fires every 30s on mouse/keyboard
   |
   |  😴 Admin walks away from desk...
   |
   |  ⏰ 30 minutes pass...
   |
   |  👑 Watcher leader (could be Pod C) marks session revoked → DB
   |
   🚪 Next request to ANY pod → BOOM. Logged out. Cookies cleared. Token dead.
         No coin-flip. No "depends which pod you hit." Just security.

✨ Features at a Glance

| Feature | What It Does | Vibe | | -------------------------- | ---------------------------------------------------------- | -------------------- | | ⏰ Auto-Logout | Kicks idle admins after configurable minutes | "Use it or lose it" | | 🫀 Activity Heartbeat | Form-filling counts as activity (no spurious idle-logouts) | "We see you typing" | | 🚫 Single-Session Lock | One admin = one session. Across every pod. | "No shadow clones" | | 💀 Session Revocation | Per-sessionId. Cluster-wide. Instant. | "Ghosts get ghosted" | | 🌐 Multi-Pod Safe | DB-backed state, leader-elected watcher | "OpenShift-ready" | | 🔑 Password Policy | Expiry + non-reusable passwords (configurable) | "Rotate or regret" | | ⚙️ Admin UI | Settings panel right inside Strapi | "Click, don't code" | | 🛡️ Input Validation | Server-side validation on every settings save | "Trust nobody" |


🚀 Quick Start

Step 1: Install it

yarn add strapi-security-suite

Step 2: Enable it

Add this to your config/plugins.js (or .ts):

module.exports = ({ env }) => ({
  'strapi-security-suite': {
    enabled: true,
  },
});

Step 3: Restart Strapi

yarn develop

Three new tables — sss_admin_sessions, sss_login_locks, sss_watcher_leases — are auto-created by Strapi on first boot. No manual migration. No new dependencies. No config required.

Step 4: Find it

Go to SettingsGlobalSecurity Suite

That's it. You're done. Go get a coffee. ☕


🌐 Why Multi-Pod-Safe Matters

In v0.3 the plugin kept its session state — last-active timestamps, revoked emails, login locks — in per-pod in-memory Maps and Sets. On a single-pod deployment, fine. On a horizontally-scaled OpenShift / Kubernetes deployment with multiple replicas behind a load balancer, those data structures lived independently per pod and that broke every guarantee the plugin made:

  • An admin's requests round-robined across N pods → each pod saw only ~1/N of their activity → some pod's watcher decided they'd been idle 30 min and revoked them while they were actively typing.
  • Pod A revoked a session. The next request landed on Pod B → no entry → no force-reload signal → revocation became a 1/N-probability event.
  • A logged-out admin's bearer kept working on every pod that hadn't seen the logout, until the JWT expired.
  • Two concurrent logins for the same email hit different pods → both pods saw empty maps → both succeeded. Single-session enforcement only worked on a single pod, which is exactly when you don't need it.

v0.4 moves all of that state into the database. Revocation issued on any pod is visible to every other pod on the next request. The watcher is leader-elected so only one pod cluster-wide actually runs the 5-second tick. Login locks are atomic across pods via SELECT … FOR UPDATE. No Redis. No new infra. Your existing Postgres / MySQL / SQLite handles it.


🖼️ The Admin Panel

Once installed, you get a beautiful settings page with two panels:

┌───────────────────────────────────────────────────────┐
│            🛡️ Security & Session Settings              │
├───────────────────────────┬───────────────────────────┤
│                           │                           │
│  🕐 SESSION MANAGEMENT    │  🔑 PASSWORD MANAGEMENT    │
│                           │                           │
│  Auto Logout Time: [30]   │  Password Control: [ON]   │
│  (minutes)                │                           │
│                           │  Expiry Days: [30]        │
│  Multi-Session            │                           │
│  Control: [ON]            │  Non-Reusable: [ON]       │
│                           │                           │
├───────────────────────────┴───────────────────────────┤
│                               [ 💾 Save Settings ]    │
└───────────────────────────────────────────────────────┘

Settings are stored in a single-type DB record. Change a value, hit save, it takes effect immediately. No restarts. No config files.


🧠 How It Actually Works

🔗 The Middleware Pipeline

When any request hits your Strapi server, it passes through 5 security checkpoints (middlewares), in this exact order:

  🌐 Incoming Request
       │
       ▼
  1. 🐣 seedUserInfos
  │    "Decode the JWT. Pull userId AND sessionId. Hydrate ctx.state."
  │
  ▼
  2. 🔍 interceptRenewToken
  │    "Logging out? Mark this sessionId revoked in the DB. Cluster-wide."
  │
  ▼
  3. 👣 trackActivity
  │    "If this sessionId is revoked → 403 + clear cookies. Else stamp lastActiveAt
  │     to the DB (write-coalesced to once per 30s)."
  │
  ▼
  4. ☠️ rejectRevokedTokens
  │    "Belt-and-suspenders revocation check. Sets app.admin.tk header so the
  │     frontend force-reloads. Calls sessionManager.invalidateRefreshToken."
  │
  ▼
  5. 🚫 preventMultipleSessions  (on login only)
       "Acquire cross-pod login lock. Refuse with 409 if another active session
        for this email exists anywhere in the cluster."

⏱️ The Auto-Logout Watcher (Leader-Elected)

Every pod runs a setInterval every 5 seconds. Inside the tick:

  🔄 Every 5 seconds, every pod:
     │
     ├─→ acquireWatcherLease()  (atomic UPDATE on sss_watcher_leases)
     │     │
     │     ├─→ Got it? I'm the leader. Continue.
     │     └─→ Someone else has it? Skip the rest. (1 cheap DB query, done.)
     │
     │  (Only the leader runs the body below)
     │
     ├─→ pruneExpiredLocks()   (clean up sss_login_locks where lockedUntil < now)
     │
     ├─→ listIdleSessions({ idleThresholdMs })
     │     SELECT FROM sss_admin_sessions
     │     WHERE revoked_at IS NULL AND last_active_at < (now - threshold)
     │
     └─→ For each idle session:
          • UPDATE sss_admin_sessions SET revoked_at = NOW() WHERE id = ?
          • sessionManager('admin').invalidateRefreshToken(userId)
          • Log it

If the leader pod dies, its lease (15s TTL) expires and another pod claims it on the next tick. Worst-case revocation lag during failover: 15 seconds.

🫀 The Activity Heartbeat

A new admin-side hook listens for mousemove, keydown, scroll, click, touchstart (passive). On any event, throttled to once per 30 seconds, it fires POST /strapi-security-suite/heartbeat. The middleware chain treats it like any other authenticated request, so trackActivity updates lastActiveAt.

This means a user filling a long form for 25 minutes — generating zero other HTTP traffic — is not auto-logged-out. Form-filling is correctly recognized as activity.

🖥️ The Frontend Interceptor

window.fetch is patched to watch for the app.admin.tk response header:

  🌐 Admin makes any API call
       │
       ▼
  👀 Check response headers for 'app.admin.tk'
       │
       YES → 🚨 FORCED LOGOUT 🚨  window.location.reload()
       │
       NO  → ✅ Normal response. Continue working.

🗃️ DB Schema (auto-created on boot)

| Table | Purpose | Key columns | | -------------------- | ----------------------------- | -------------------------------------------------------------- | | sss_admin_sessions | One row per admin session | session_id (unique), email, last_active_at, revoked_at | | sss_login_locks | Cross-pod login lock | email (unique), locked_until | | sss_watcher_leases | Watcher leader-election lease | name (unique), holder, expires_at |

Hidden from the content-manager and content-type-builder via pluginOptions. Strapi creates them on first boot the same way the existing security-settings singleType is created — no manual migration step.


📂 Project Structure

strapi-security-suite/
📁 admin/src/                    ← Admin panel (React)
│   📄 index.js                    Plugin entry + fetch interceptor + heartbeat install
│   📄 heartbeat.js                Throttled activity-heartbeat client hook
│   📄 constants.js                API paths, header names, heartbeat throttle
│   📄 pluginId.js                 Plugin ID constant
│   📁 components/Initializer.jsx  Plugin lifecycle init
│   📁 pages/
│   │   📄 App.jsx                 Router
│   │   📄 HomePage.jsx            Settings UI
│   📁 translations/en.json        i18n strings
│
📁 server/src/                   ← Server-side (Node.js)
│   📄 index.js                    Plugin entry point
│   📄 register.js                 Middleware registration phase
│   📄 bootstrap.js                Permissions + settings seeding + watcher start
│   📄 destroy.js                  Releases watcher lease, stops interval
│   📄 constants.js                ⭐ ALL magic values live here
│   │
│   📁 controllers/
│   │   📄 adminSecurityController.js   GET/POST settings + POST heartbeat
│   │
│   📁 services/
│   │   📄 state.js                The DB-backed state core (replaces the in-memory globals)
│   │   📄 autoLogoutChecker.js    Leader-elected background watcher
│   │
│   📁 middlewares/
│   │   📄 seedUserInfos.js        Decode JWT, extract userId + sessionId
│   │   📄 interceptRenewToken.js  Revoke session on logout (DB-backed)
│   │   📄 trackActivity.js        Persist lastActiveAt (write-coalesced)
│   │   📄 rejectRevokedTokens.js  Force-reload signal + cookie clear
│   │   📄 preventMultipleSessions.js  Cross-pod login lock + active-session check
│   │
│   📁 policies/has-admin-permission.js
│   │
│   📁 utils/
│   │   📄 errors.js               PluginError, ValidationError, AuthorizationError
│   │   📄 clearSessionCookies.js  Clears koa.sess, koa.sess.sig, refresh + JWT cookies
│   │
│   📁 content-types/
│   │   📁 security-settings/      Plugin config (singleType)
│   │   📁 admin-session/          Per-session activity + revocation
│   │   📁 login-lock/             Cross-pod login lock
│   │   📁 watcher-lease/          Watcher leader-election lease
│   │
│   📁 routes/index.js             Admin-typed routes with policies
│
📁 tests/                        ← Vitest test suite (66 tests)
│   📁 helpers/
│   │   📄 strapi-fake.js          sqlite :memory: + Knex harness
│   │   📄 mock-strapi.js          Mock-based ctx + state helpers
│   📁 server/
│   │   📄 state.test.js                  15 tests — touch, revocation, listIdle, hasActiveSession
│   │   📄 state.concurrency.test.js      9 tests — multi-pod login lock + watcher lease
│   │   📄 seedUserInfos.test.js          6 tests — JWT decode, ctx hydration
│   │   📄 trackActivity.test.js          4 tests — touch, revocation rejection
│   │   📄 rejectRevokedTokens.test.js    4 tests — header signal, sessionManager
│   │   📄 preventMultipleSessions.test.js  8 tests — login lock flow
│   │   📄 interceptRenewToken.test.js    3 tests — logout revocation
│   │   📄 autoLogoutChecker.test.js      8 tests — leader election + idle revocation
│   │   📄 adminSecurityController.test.js  9 tests — heartbeat + settings validation

🔧 Configuration Schema

All settings live in a single-type content-type in the database:

{
  "autoLogoutTime": 30,
  "multipleSessionsControl": true,
  "passwordExpiryDays": 30,
  "nonReusablePassword": true,
  "enablePasswordManagement": true
}

| Field | Type | Default | What It Does | | -------------------------- | --------- | ------- | ---------------------------------------- | | autoLogoutTime | integer | 30 | Minutes of inactivity before auto-logout | | multipleSessionsControl | boolean | true | Block concurrent sessions for same admin | | passwordExpiryDays | integer | 30 | Days before password must be changed | | nonReusablePassword | boolean | true | Prevent reuse of previous passwords | | enablePasswordManagement | boolean | true | Master switch for password features |


🧪 API Endpoints

All routes are admin-typed (Strapi handles auth automatically):

| Method | Path | Auth | Permission | Description | | ------ | --------------------------------------- | -------- | ---------------- | --------------------------------- | | POST | /strapi-security-suite/heartbeat | 🔒 Admin | — | Activity keep-alive (returns 204) | | GET | /strapi-security-suite/admin/settings | 🔒 Admin | view-configs | Read security settings | | POST | /strapi-security-suite/admin/settings | 🔒 Admin | manage-configs | Update security settings |

🔐 Permissions

| Permission | What It Allows | | ---------------------------------------------- | ------------------------ | | plugin::strapi-security-suite.access | Access the settings page | | plugin::strapi-security-suite.view-configs | Read security settings | | plugin::strapi-security-suite.manage-configs | Modify security settings |


💡 Recommended Host-App Configuration

The plugin works out of the box with Strapi defaults, but for tighter revocation latency consider lowering the access-token TTL in your host app's config/admin.js:

module.exports = ({ env }) => ({
  auth: {
    secret: env('ADMIN_JWT_SECRET'),
    options: {
      expiresIn: '2m', // ← short access tokens, refreshed transparently by the admin frontend
    },
  },
});

With a 2-minute access-token TTL, a revoked admin loses access within ~2 minutes even if no other request is made (next refresh attempt fails because the refresh token is also invalidated). With the default 30-minute TTL, revocation is enforced on the next request the admin makes (via the app.admin.tk force-reload signal) — instant for active admins, up to 30 min for an idle one whose tab is open.


🛠️ Development

yarn install          # Install dependencies
yarn build            # Build the plugin
yarn watch            # Auto-rebuild on changes
yarn lint             # ESLint
yarn lint:fix         # ESLint --fix
yarn format           # Prettier
yarn format:check     # Check formatting
yarn verify           # Verify plugin exports
yarn test             # Run the full test suite (66 tests)
yarn test:watch       # Vitest in watch mode
yarn test:coverage    # Coverage report

The state-service tests run against a real sqlite :memory: DB via Knex — they exercise the actual SQL the plugin issues, including the SELECT … FOR UPDATE paths and ON CONFLICT behaviors. Two simulated pods cover the cross-pod concurrency cases.


🔮 Roadmap

| Feature | Status | | ----------------------------- | ----------------- | | ⏰ Auto-Logout | ✅ Shipped (v0.1) | | 🚫 Single-Session Enforcement | ✅ Shipped (v0.1) | | 💀 Session Revocation | ✅ Shipped (v0.1) | | ⚙️ Admin Settings UI | ✅ Shipped (v0.1) | | 🌐 Multi-Pod-Safe State | ✅ Shipped (v0.4) | | 🫀 Activity Heartbeat | ✅ Shipped (v0.4) | | 🧪 Test Suite (66 tests) | ✅ Shipped (v0.4) | | 🔑 Password Expiry | 🚧 In Development | | 🔄 Non-Reusable Passwords | 🚧 In Development | | 📝 Admin Activity Logs | 🔜 Planned | | 📊 Security Dashboard | 🔜 Planned | | 👊 Brute Force Detection | 🔜 Planned |


🗣️ Real Talk

"We installed this and now our interns can't share logins anymore." — A CTO, probably

"Our admin panel feels like it judges us now. I love it." — That one developer who actually cares

"I left my desk for coffee and came back logged out. Respect." — Someone who now understands security

"We scaled to 8 pods on OpenShift and the plugin… just kept working. Sessions, revocations, locks — all consistent." — A platform engineer in v0.4


👥 Author

LPIX-11 — Orange / Sonatel


⚖️ License

MIT — Do whatever you want. Just don't blame us if you turn off all the features and get breached. That's on you.


💡 Philosophy

Security should be:

  • Correct under load — Multi-pod deployments shouldn't degrade the security model into a coin flip.
  • Cheap to operate — DB-backed state with write-coalescing. No Redis. No new infra.
  • Unforgiving — Idle? Gone. Revoked? Dead. Duplicated? Blocked.
  • Mildly judgmental — This plugin will side-eye your stale sessions.

"The meta-principle: make the right thing the default thing. Discipline compounds. Shortcuts compound too, just in the wrong direction."