Skip to content

Clean up & inspect zombie sessions in Express: prunes cookie-only sessions to keep Prisma/Redis stores lean.

License

Notifications You must be signed in to change notification settings

CodeCornTech/empty-session-reaper

Repository files navigation

@codecorn/empty-session-reaper

Tiny middleware to prune or debug empty sessions (cookie-only + your rules) and keep Prisma/Redis stores lean.
Agnostic core — no app-specific keys inside. Bring your own policy via options.

npm downloads stars issues CI coverage umbrella license


Features

  • 🧹 Prune sessions that are “empty” under your rules.
  • 🔎 Debug with dry-run + logger; optional session mutation logger.
  • 🧩 Agnostic: pass allowlists, predicates, denylists — no hardcoded keys.
  • 🧪 Store-agnostic: Prisma/SQL, Redis, etc.
  • ⚙️ Composable predicates (emptyObject, equals, oneOf, and, or, flashEmptyOrOneOf).
  • 🧰 Optional preset: cookieFlash() — beginner-friendly.
  • 🧯 Safe by design: no env access, tiny footprint.

Install

npm i @codecorn/empty-session-reaper

Import styles

// ✅ CommonJS (require)
const {
  wireEmptySessionReaper,
  predicates,
  buildAllowedKeys,
  wireSessionMutationLogger, // optional: logs added/removed keys
} = require("@codecorn/empty-session-reaper");
const { cookieFlash } = require("@codecorn/empty-session-reaper/presets");
// ✅ ESM / TypeScript
import {
  wireEmptySessionReaper,
  predicates as P,
  buildAllowedKeys,
  wireSessionMutationLogger, // optional
} from "@codecorn/empty-session-reaper";
import { cookieFlash } from "@codecorn/empty-session-reaper/presets";

Usage A — minimal “cookie + empty flash”

// Meaning of buildAllowedKeys(input, expandBase, base):
// - base: starting list (default: ['cookie'])
// - input: extra keys to allow (e.g., ['flash'])
// - expandBase:
//     true  => merge base + input  (e.g., ['cookie'] + ['flash'] -> ['cookie','flash'])
//     false => use input only      (e.g., ['flash'])
wireEmptySessionReaper(app, {
  logger: (m, meta) => console.debug(m, meta),

  allowedKeys: buildAllowedKeys(["flash"], true, ["cookie"]),
  // ↑ allowlist = merge base ['cookie'] + ['flash'] → ['cookie','flash']
  maxKeys: 2,

  keyPredicates: {
    // flash is harmless if it's an empty object {}
    flash: P.emptyObject,
  },
});

Usage B — advanced custom policy (full control)

const isLoginFlash = (flash: any) => {
  if (!flash || typeof flash !== "object") return false;
  const ks = Object.keys(flash);
  if (ks.length !== 1) return false;
  const arr = Array.isArray((flash as any)[ks[0]]) ? (flash as any)[ks[0]] : [];
  return arr.length === 1 && /^please sign in\.?$/i.test(String(arr[0] || ""));
};

wireEmptySessionReaper(app, {
  logger: (m, meta) => console.debug(m, meta),

  allowedKeys: ["cookie", "flash", "url", "flag"],
  maxKeys: 4,

  disallowedKeyPatterns: [/^csrf/i, /^token/i, /^user/i],

  keyPredicates: {
    flash: (v) => P.emptyObject(v) || isLoginFlash(v),
    url: (v) => ["/", "/login", "/signin"].includes(String(v || "")),
    flag: P.oneOf([false, "auto"]),
  },

  isSessionPrunable: (s) => {
    const url = String((s as any).url || "");
    return !(/\.(env|git)\b/i.test(url) || /\/upload\/\./i.test(url));
  },
});

Usage C — optional preset cookieFlash

const preset = cookieFlash({
  // flashKey: 'flash',
  // flashField: 'error',
  // loginMessages: [/^please sign in\.?$/i, /^access denied$/i],
  // extraAllowedKeys: ['url'],
  // maxKeys: 3,
  // disallowedKeyPatterns: [/^csrf/i, /^token/i],
  // extraPredicates: { url: (v) => ['/', '/login'].includes(String(v || '')) },
  // finalCheck: (s) => !/\.env\b/i.test(String((s as any).url || '')),
});
wireEmptySessionReaper(app, { logger: console.debug, ...preset });

Bonus: Session mutation logger (discover origins)

// Place AFTER session(...) and BEFORE the reaper:
wireSessionMutationLogger(app, {
  logger: (label, meta) => console.debug(label, meta),
  includeValues: false, // true to also log shallow values (use redact to mask)
  redact: (k, v) => (/(token|secret|pass)/i.test(k) ? "[redacted]" : v),
  label: "session mutation",
});

Logs { path, added: [...], removed: [...] } on each response where the session keys changed.


API (core)

createEmptySessionReaper(opts) -> (req, res, next) => void
  Create the middleware with your pruning policy.

wireEmptySessionReaper(app, opts) -> middleware
  Mounts the middleware on the app and returns it.

buildAllowedKeys(input?: string[], expandBase?: boolean, base?: string[]) -> string[]
  Helper to compose allowlists.
  - base: starting list (default: ["cookie"])
  - input: extra allowed keys (e.g., ["flash"])
  - expandBase:
      true  -> merge base + input
      false -> use input only

predicates:
  - emptyObject(v)
  - equals(x)
  - oneOf([a,b,c])
  - and(p1,p2,...)
  - or(p1,p2,...)
  - flashEmptyOrOneOf(field='error', messages=[/^please sign in\.?$/i])

createSessionMutationLogger(opts) -> middleware
wireSessionMutationLogger(app, opts) -> middleware
lookUpSessMutation(app, opts) -> alias of wireSessionMutationLogger

📝 License

MIT © CodeCorn™

Distributed under the MIT license.

Credits

  • Cousìn (co-author & review)PYTORCHIA FOR LIFE
  • Federico Girolami (CodeCorn) — Maintainer

👤 Maintainer

👨‍💻 Federico Girolami

Full Stack Developer | System Integrator | Digital Solution Architect 🚀

📫 Get in Touch

🌐 Website: codecorn.it *(Under Construction)*

📧 Email: f.girolami@codecorn.it

🐙 GitHub: github.com/fgirolami29


🤝 Contribute

Pull requests are welcome. For major changes, please open an issue first to discuss what you’d like to change.

Powered by CodeCorn™ 🚀


About

Clean up & inspect zombie sessions in Express: prunes cookie-only sessions to keep Prisma/Redis stores lean.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published