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

@aria-framework/server

v0.14.0

Published

Aria App Framework — server module. Process lifecycle spine: graceful shutdown coordinator, the DB-worker death policy (exitCode + graceful SIGTERM), crash handlers, and an HTTPS-or-HTTP listen helper. Handlers register before the worker spawns; the serve

Readme

@aria-framework/server

Aria App Framework — the process lifecycle spine. Graceful shutdown, the DB-worker death policy, crash handlers, and an HTTPS-or-HTTP listen helper — the small, safety-critical code where a wrong move is a silent outage, now maintained once instead of forked per app.

This is deliberately not a middleware bootstrap (helmet/session/flash/csrf). That's where the apps diverge (CSP allowlists, cookie names, api-before-session ordering); it stays app-side.

createLifecycle

const { createLifecycle, workerDeathPolicy } = require('@aria-framework/server');

// At the TOP of server.js — BEFORE spawning the DB worker, because the death
// policy self-sends SIGTERM and a no-handler window would let a signal death
// read as a clean stop to systemd (no restart).
const lifecycle = createLifecycle({
  logger,
  teardown: [                                   // stop app services before draining HTTP
    () => require('./lib/backup').stopScheduler(),
    () => require('./lib/email-inbound').stop(),
    () => require('./lib/notifHub').closeAll(),  // end SSE so server.close can drain
  ],
  onClose: async () => {                         // final release after drain
    await dbClient.close();
    encryption.close();
  }
  // hardDeadlineMs: 10000, installCrashHandlers: true
});

async function start() {
  await encryption.init(/* ... */);
  await dbClient.init(/* ... */);   // its onWorkerDeath = workerDeathPolicy({ logger }) — see below
  // ... app-specific boot (caches, email, schedulers) ...
  const app = createApp(sessionSecret);
  await lifecycle.listen(app, {
    https: config.httpsEnabled ? { cert: fs.readFileSync(config.httpsCertPath), key: fs.readFileSync(config.httpsKeyPath) } : null,
    port: config.port, host: config.host,
    onListening: (proto) => logger.info(`running at ${proto}://${config.host}:${config.port}`)
  });
}
start().catch((err) => { logger.error(`Startup failed: ${err.message}`); process.exit(1); });

createLifecycle registers SIGTERM/SIGINT (and, unless installCrashHandlers:false, unhandledRejection/uncaughtException → log + exit 1) immediately, and owns the server reference internally — so the app never juggles a "server assigned later" variable, and a death during boot (before listen) still shuts down cleanly. Shutdown runs the teardown hooks in order (each error-swallowed so one can't block), drains the HTTP server, runs onClose, then exits with process.exitCode || 0 — honoring a failure code the death policy pre-set, so a supervised restart happens on a crash while a plain operator stop stays 0. A hardDeadlineMs timer force-exits if drain/close hangs.

The drain is bounded (0.1.2), so onClose reliably runs instead of being skipped by the deadline: idle keep-alive sockets are destroyed immediately (closeIdleConnections(), else they hold close() open for up to keepAliveTimeout), and anything still active at half the deadline is destroyed too (closeAllConnections()). Cutting a straggler request at hardDeadlineMs/2 is strictly better than force-exiting past your DB close at hardDeadlineMs. Long-lived streams (SSE/websockets) should still be ended in teardown — that's a clean shutdown, whereas this is the backstop.

HTTPS here is HTTP/1.1 (Express + Node's native http2 don't mix reliably). For real HTTP/2, front the app with a reverse proxy speaking h2 to the browser and HTTP/1.1 to this process.

workerDeathPolicy

// In your DbClient subclass (see @aria-framework/db-worker):
const { workerDeathPolicy } = require('@aria-framework/server');
new DbClient({ workerPath, logger, onWorkerDeath: workerDeathPolicy({ logger }) });

A non-deliberate worker death means every invoke() rejects forever — a silent outage. This turns it into a supervised restart: set process.exitCode = 1, then route through the app's graceful shutdown via SIGTERM (so schedulers, SSE, and the DB client all close cleanly). If no SIGTERM handler is registered (a boot-window death, or an app that didn't call createLifecycle), it exits directly so a bare signal death can't read as a clean stop. It never shares an object with createLifecycle — they communicate through the signal + process.exitCode — so it can be wired into a DbClient constructed long before the lifecycle exists.

Windows: process.kill(self,'SIGTERM') hard-terminates (no handler runs); the exit-code intent still holds. Production is Linux (systemd/pm2), where the graceful route runs. Pair with Restart=on-failure + StartLimitIntervalSec/StartLimitBurst so crash loops surface as a failed unit.

Flushing the logger before exit (0.2.0)

logger.error(...); process.exit(1) is only correct with a synchronous transport. A buffering one (winston File, pino to a stream) loses the very line that explains why the process is dying — it fails exactly when you need the log. Pass a flush hook and every exit this module owns drains first: the crash handlers, the hard-deadline force-exit, and the normal post-drain release.

const lifecycle = createLifecycle({
  logger,
  // winston has no universal flush — end the logger and wait for 'finish':
  flush: () => new Promise((res) => { logger.on('finish', res); logger.end(); }),
  flushMs: 2000,     // upper bound; crash state is untrusted, so never unbounded
  teardown: [/* ... */],
  onClose: async () => { /* ... */ }
});

// Give your OWN fatal paths the same durability — fatal() is the same bounded
// exit, so a boot failure's reason reaches the log file instead of racing it:
start().catch((err) => {
  logger.error(`Startup failed: ${err.message}`, { stack: err.stack });
  lifecycle.fatal(1);            // NOT process.exit(1)
});

Omit flush and behavior is identical to 0.1.2. Worst-case stop time becomes hardDeadlineMs + flushMs — still far inside systemd's 90s TimeoutStopSec.

The middleware baseline (0.3.0)

const { applyTransport, applySession } = require('@aria-framework/server');

applyTransport(app, { trustProxy, csp, bodyLimit, staticDirs, rateLimiter },
               { express, helmet, cookieParser });

app.use('/api/v1', apiRouter);        // optional: stateless, session-free

const { csrfUtil } = applySession(app,
  { sessionStore, secret, cookieName, secure, flash, csrf, viewEngine },
  { session, doubleCsrf });

TWO PHASES, and that is the design. One consumer mounts a stateless Bearer-JWT API between them, so it gets no session cookie and the global CSRF guard never touches it. A single applyBaseline(app) would force that API into the session stack or make the app opt out of half the baseline. An app with no such API calls the two back to back.

Dependencies stay at ZERO. Callers pass their own express/helmet/session instances, so the package can never introduce a second copy of express-session that silently fails to share state.

flash.skipWhen keeps a background request from eating the message. Flash is read-and-clear, which is right for a navigation and wrong for anything else. A page that refetches itself — a live-updating detail view, a poll — sends the session cookie and renders nothing the user sees; consuming there deletes the message while the navigation that would have shown it is still in flight, so a confirmation for the user's own action intermittently never appears. Only the app knows which of its requests are page views, so it says:

applySession(app, {
  …,
  flash: { skipWhen: (req) => req.get('X-Requested-With') === 'fetch' }
}, { session });

Skipped requests still get a working res.flash and an empty res.locals.flash; the message stays in the session for the next real page load. flash: false still disables the feature outright, and flash: {} (or omitting it) behaves exactly as before.

csp MERGES over the defaults, never replaces. A replace-style option is exactly how a directive goes missing — one consumer was missing objectSrc, baseUri and frameAncestors for months because each app hand-maintained its own policy. An app adding one origin keeps every hardening it did not mention. csp: false disables.

csrf.util — bring your own instance. An app whose route modules re-apply the guard after their own body parser must share ONE doubleCsrf instance with the global gate. Two built from the same secret and options are interchangeable today (the tokens are stateless HMACs), but that is a coincidence, not a contract — change the cookie name in one place and the other silently diverges. Pass csrf: { util } and the package applies your instance instead of building one; { doubleCsrf } is then not required in the deps.

csrf.skipWhen(req) must be PATH-SCOPED. One consumer exempts known upload routes at the global layer and re-applies the guard after multer — hence the returned csrfUtil. A bare content-type test ("skip multipart") would let an attacker POST enctype=multipart/form-data to any route and bypass CSRF entirely. Default is () => false, so doing nothing means global protection.

Order is a security property, not a style choice: helmet before anything can respond, body parsing before CSRF (the token arrives in the body), CSRF after session (it keys on the session identifier). The test suite asserts the order explicitly.

What stays in the app: the rate limiters themselves (limits are policy), how secure is derived, the session sweep interval, and everything after the baseline.

Changelog

  • 0.3.2 — three fixes from an xhigh review of 0.3.0/0.3.1, all found by reading the module against its own comments rather than by a failing test.

    • helmetOptions could silently override the CSP. It is the last argument to Object.assign, so a consumer passing helmetOptions.contentSecurityPolicy would replace the merged directives wholesale — dropping the object-src / base-uri / frame-ancestors hardening this module exists to hold, or quietly defeating csp: false. Now throws: there is one way to configure CSP, and it is csp. The guard is narrow — unrelated helmet options still pass through.
    • __Host- prefix and cookie attributes are now cross-validated. The prefix was chosen from opts.secure while csrf.cookieOptions could be overridden independently. A domain, a non-/ path, or secure: false alongside the prefix makes the browser silently refuse to store the cookie, disabling CSRF on every state-changing request with no error raised anywhere in this stack. That contradiction now fails at boot instead of in production.
    • peerDependencies actually declared. The header comment promised "peer dependencies, not dependencies", but the manifest listed express / helmet / cookie-parser / express-session / csrf-csrf only under devDependencies — so npm had no way to warn a consumer about an incompatible version, and the sibling convention (db-worker declares better-sqlite3 as a peer) was not followed. All five are declared optional: the lifecycle half of this package (createLifecycle, its original 0.1.x purpose) needs none of them, so the range exists to catch a version mismatch, not to force an install.

    Known and deliberately NOT changed here: applySession hardcodes saveUninitialized: false while the CSRF token HMAC binds to req.session.id. A GET that never touches the session emits no session cookie, so the following POST mints a new id and the form token fails to validate. Both consumers already work around this per-route (req.session.csrfBootstrap = 1 before rendering an anonymous form) — which is exactly the repeated hand-wiring this package exists to remove. But fixing it centrally changes session-issuance behaviour and wants its own version.

  • 0.3.1csrf.util lets a consumer supply its own doubleCsrf instance rather than have one built. Surfaced by the second consumer: Support101 keeps its instance in lib/csrf.js so routes/public.js and routes/tickets.js can re-apply the same guard after multer, and a package-built second instance would have been a latent divergence in a security primitive.

  • 0.3.0 — adds the Express middleware baseline (applyTransport / applySession), ~120-140 lines of near-identical wiring per consumer. Additive: createLifecycle and workerDeathPolicy are unchanged, and the package still has zero runtime dependencies. This was previously deferred because the two apps diverged on CSP, cookie names and api-before-session; all three were resolved rather than worked around — see index.js.

  • 0.2.0flush before exit. New flush / flushMs options and a returned fatal(code). All four exits this module owns (two crash handlers, the hard-deadline force-exit, the post-drain release) now drain the logger first, bounded by flushMs so a wedged transport can't stop the exit; fatal() extends the same guarantee to app-side paths like start().catch. Backward compatible — without flush, behavior is byte-identical to 0.1.2. Found because BOTH consumers log to a winston File transport and both had log(); process.exit() on their startup-failure path, silently truncating the reason. Apps that set installCrashHandlers:false purely to get their own flush-then-exit can now drop that override.

  • 0.1.2 — the drain is now bounded, so a hung request can no longer cost you onClose. server.close() waits for existing connections, including ones sitting idle in a keep-alive pool, so a drain could outlive hardDeadlineMs and force-exit before the DB/encryption release ever ran. Shutdown now calls closeIdleConnections() immediately and closeAllConnections() at half the deadline (both Node >=18.2, feature-detected — older runtimes keep the previous wait-only behavior). Found while adopting 0.1.1 in a second app, whose hand-rolled shutdown had deliberately not gated its resource release on the drain callback for exactly this reason.

  • 0.1.1 — review fixes. (1) The force-exit hard-deadline is now armed at the TOP of shutdown (before teardown/onClose run), so a hanging async teardown hook can't prevent the safety net from arming. (2) listen() builds the server explicitly and attaches an error listener before binding, so a bind failure (EADDRINUSE/EACCES) REJECTS the returned promise instead of crashing the process — callers can catch it. (3) Documented that the crash handlers hard-exit WITHOUT graceful teardown by design (corrupt state → fast restart).

  • 0.1.0 — first release. The lifecycle spine extracted from Support101/ Acc101 (identical after both converged on the hardened shutdown): createLifecycle (shutdown coordinator + listen helper + crash handlers) and workerDeathPolicy (the DbClient onWorkerDeath hook). The middleware baseline is intentionally out of scope.

Diagnostics (0.9.0)

versionRoute answers "which build is this?" in one string. createDiagnostics() answers the same question with the detail you need when that string turns out to be a lie:

const diag = server.createDiagnostics({
  root: __dirname,                              // where package.json and node_modules live
  native: ['better-sqlite3', 'keytar'],         // the packages that differ per host
  env: ['NODE_ENV', 'KEYSTORE_FILE_ONLY'],      // NAMES only — reported as set / not set
  probes: [                                     // the app's half; the package cannot know these
    { id: 'backup', label: 'Backup & restore', check: async () => ({ ok: true, detail: '2h ago' }) }
  ]
});
const snapshot = await diag.collect();
<%- include('ui/diagnostics', { d: snapshot }) %>

DECLARED versus RESOLVED is the point. An alias installs a different package under the requested name — one consumer runs a SQLCipher fork as better-sqlite3 — so "better-sqlite3 13.0.3" can be a true statement about the wrong package. Both names appear. A row is flagged only where the two certainly disagree: a wrong badge is worse than no badge, because it sends someone to investigate a non-problem and the next real one gets ignored.

Environment variables report names and set/not-set by default, never values — a debug page is exactly where a session secret leaks to whoever holds one permission, a lower bar than a credential store which is separately gated and audited.

A variable can opt IN, one at a time: { name: 'NODE_ENV', show: true } renders its value. That exists because "set" answers a question next to the one that matters for a flag — NODE_ENV=staging satisfies it as well as production, and CREDENTIALS_DB_ENCRYPTED=false as well as true, while those decide whether cookies are secure and whether a credential store is encrypted at all. Marking is a decision the app makes about a variable it named itself; there is deliberately no heuristic guessing which values look secret, because a heuristic that is wrong once has published a credential.

A probe that throws is a red row carrying its message, never a 500, and never stops the probes after it. The page exists to be readable when something is broken.

Package resolution reads each package.json directly rather than require.resolve(), which resolves an entry point and so reports a package with no main as missing.