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

@novats/plugins

v1.0.0

Published

Nova framework — first-party plugins

Readme

@novats/plugins

First-party plugins for the Nova framework.

🚧 Pre-release. Public API is not stable yet.

Each plugin is a function that takes options and returns a Nova Plugin, ready to pass to app.register(...).

Install

pnpm add @novats/core @novats/plugins

@novats/plugins declares @novats/core as a peer dependency. Install both — they share the same Nova instance.

Plugins shipped

| Plugin | Status | Notes | | ------------------------- | ------ | -------------------------------------------------- | | cors | ✅ | Cross-Origin Resource Sharing per the Fetch spec. | | rateLimit | ✅ | Fixed-window in-memory; pluggable store for Redis. |

cors

import { Nova } from "@novats/core";
import { cors } from "@novats/plugins";

const app = new Nova();

// Wide-open default
await app.register(cors());

// Concrete origin
await app.register(cors({ origin: "https://app.example.com" }));

// Multiple allowed origins
await app.register(cors({ origin: ["https://a.example.com", "https://b.example.com"] }));

// Pattern match
await app.register(cors({ origin: /\.example\.com$/ }));

// Dynamic (sync or async)
await app.register(
  cors({
    origin: async (origin) => await isOriginAllowed(origin),
    credentials: true,
  }),
);

Options

| Option | Type | Default | Notes | | ---------------- | -------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------ | | origin | "*" \| string \| string[] \| RegExp \| (origin) => boolean \| Promise<boolean> | "*" | Combining "*" with credentials: true throws — spec violation. | | methods | string[] | standard 6 | GET, HEAD, PUT, PATCH, POST, DELETE. | | allowedHeaders | string[] | reflect request | When unset, the preflight echoes Access-Control-Request-Headers. | | exposedHeaders | string[] | none | Sets Access-Control-Expose-Headers on regular responses. | | credentials | boolean | false | Adds Access-Control-Allow-Credentials: true. | | maxAge | number (seconds) | unset (no cache) | Access-Control-Max-Age preflight cache duration. |

Behavior

  • Requests without an Origin header are treated as same-origin and pass through with no CORS headers.
  • Requests whose origin is denied also pass through with no CORS headers. The browser blocks the response client-side; the server does not leak the acceptance list via a 403.
  • An OPTIONS request that carries Access-Control-Request-Method is a preflight and is fully handled by the plugin (204, no downstream handler).
  • A regular cross-origin request gets the appropriate headers attached and is forwarded to the next middleware / handler.

Vary header

CORS responses that depend on the request's Origin (or Access-Control-Request-Headers in preflights) need to advertise that dependency via Vary so HTTP caches do not serve a stale response to a different origin. The plugin appends to any existing Vary value an upstream middleware may have set.

rateLimit

Fixed-window rate limiting. Counts requests per "key" (default: client IP) inside a window of windowMs milliseconds. The (N+1)-th request inside the window is rejected with 429.

import { Nova } from "@novats/core";
import { rateLimit } from "@novats/plugins";

const app = new Nova();

// 100 requests per minute, per IP
await app.register(rateLimit({ max: 100, windowMs: 60_000 }));

// Different limits for different paths — register multiple instances on
// dedicated routers, or use a custom keyGenerator that incorporates the path.

Options

| Option | Type | Default | Notes | | -------------- | -------------------------------------- | ---------------------- | ----------------------------------------------------------------------------------- | | max | number | (required) | Requests allowed per window, inclusive. Must be positive. | | windowMs | number | (required) | Window duration in milliseconds. Must be positive. | | keyGenerator | (ctx) => string | socket.remoteAddress | Customize when behind a proxy / CDN — read X-Forwarded-For after validating it. | | message | string | "Too Many Requests" | Sent as { "error": message } on rejection. | | status | number | 429 | Status code for rejection. | | headers | boolean | true | Emit RateLimit-Limit / -Remaining / -Reset (and Retry-After on 429). | | skip | (ctx) => boolean \| Promise<boolean> | undefined | Return true to bypass rate limiting for a request. | | onLimit | (ctx) => void \| Promise<void> | undefined | Fire-and-forget hook invoked when a request is rejected. Errors logged, not thrown. | | store | RateLimitStore | in-memory | Swap for Redis/Memcached by implementing the interface. |

Headers

The plugin emits the IETF draft RateLimit-* fields (no X- prefix):

| Header | Where | Meaning | | --------------------- | -------------- | --------------------------------------------- | | RateLimit-Limit | every response | The max configured. | | RateLimit-Remaining | every response | Remaining quota in the active window. | | RateLimit-Reset | every response | Seconds until the active window resets. | | Retry-After | only on 429 | Same value as RateLimit-Reset at rejection. |

Set headers: false to suppress all of them (e.g. when fronted by an API gateway that already adds them).

Behind a proxy / CDN

The default keyGenerator uses socket.remoteAddress which, behind a reverse proxy, will be the proxy itself — collapsing all clients to one counter. Pass a custom keyGenerator that reads a forwarded header only after validating the proxy chain:

rateLimit({
  max: 100,
  windowMs: 60_000,
  keyGenerator: (ctx) => {
    const fwd = ctx.raw.req.headers["x-forwarded-for"];
    return typeof fwd === "string" ? fwd.split(",")[0].trim() : "unknown";
  },
});

Custom store (Redis et al.)

Implement the RateLimitStore interface and pass it via store:

import type { RateLimitStore } from "@novats/plugins";

class RedisRateLimitStore implements RateLimitStore {
  async increment(key: string, windowMs: number) {
    // INCR + EXPIRE in a pipeline, return current count + TTL → resetAt
  }
}

await app.register(rateLimit({ max: 100, windowMs: 60_000, store: new RedisRateLimitStore() }));

The default in-memory store is fine for single-process apps. For multi-instance deployments where rate limits must be shared, a Redis (or similar) backend is required — counters in one process are invisible to the others.