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

next-armor

v0.1.0

Published

Drop-in security for Next.js API routes. Rate limiting, bot detection, threat scoring, input validation, SSRF protection. Zero cloud dependencies.

Readme

next-armor

Drop-in security for Next.js API routes. One function. Zero cloud dependencies.

npm install next-armor

The Problem

Every Next.js API route needs rate limiting, input validation, bot protection. Most developers skip it because there's no frictionless option — existing solutions require cloud services, Redis, or stitching together 5 packages.

The Solution

import { armor } from "next-armor";
import { z } from "zod";

export const POST = armor(
  {
    rateLimit: { max: 10, window: "1m" },
    validate: z.object({
      email: z.string().email(),
      name: z.string().min(1).max(100),
    }),
    honeypot: true,
  },
  async (req, { data, ip }) => {
    // data is validated, sanitized, bot-checked, rate-limited
    console.log(data.email); // typed, safe
    return Response.json({ success: true });
  },
);

That's it. Rate limiting, input validation, bot detection, XSS sanitization — all handled before your code runs.

What You Get

| Feature | What It Does | | -------------------- | ------------------------------------------------------------------ | | Rate limiting | In-memory token bucket. No Redis required. | | Input validation | Zod schema validation with typed output. | | Bot detection | Invisible honeypot fields. No CAPTCHA. | | Threat scoring | Behavioral analysis — burst detection, path probing, malicious UA. | | XSS sanitization | Auto-escapes HTML in validated strings. | | SSRF protection | Blocks private IPs and cloud metadata endpoints. | | IP extraction | Works with Vercel, Cloudflare, standard proxies. |

How It Compares

| | next-armor | Arcjet | DIY | | ---------------------- | ---------- | ------------- | ------------------- | | Cloud service required | No | Yes ($23/mo+) | No | | Redis required | No | No | Usually | | Setup | 1 function | SDK + API key | 50+ lines per route | | Threat scoring | Built-in | Built-in | Build it yourself | | Honeypot bot detection | Built-in | No | Build it yourself | | SSRF protection | Built-in | No | Build it yourself | | Price | Free | $23/mo+ | Your time |

Usage

Quick Start — One Line Protection

// app/api/contact/route.ts
import { armor } from "next-armor";
import { z } from "zod";

export const POST = armor(
  {
    rateLimit: { max: 5, window: "1m" },
    validate: z.object({
      email: z.string().email(),
      message: z.string().min(1).max(5000),
    }),
    honeypot: true,
  },
  async (req, { data }) => {
    await sendEmail(data.email, data.message);
    return Response.json({ sent: true });
  },
);

Rate Limiting Only

export const GET = armor(
  {
    rateLimit: { max: 30, window: "1m" },
  },
  async (req) => {
    return Response.json({ data: await fetchData() });
  },
);

Full Threat Scoring

export const POST = armor(
  {
    rateLimit: { max: 10, window: "1m" },
    validate: schema,
    honeypot: true,
    threatScore: true,
    blockAt: ThreatLevel.SUSPICIOUS, // Block earlier than default
  },
  async (req, { data, ip, threat }) => {
    if (threat?.level === ThreatLevel.CAUTIOUS) {
      // Log but allow
      console.log(`Cautious request from ${ip}`);
    }
    return Response.json({ success: true });
  },
);

Honeypot Fields (Client Side)

Add hidden fields to your forms that bots auto-fill:

// In your form component
<form action="/api/contact" method="POST">
  {/* Hidden from real users, visible to bots */}
  <div
    aria-hidden="true"
    style={{
      position: "absolute",
      left: "-9999px",
      height: 0,
      width: 0,
      overflow: "hidden",
    }}
  >
    <input type="text" name="website_url" tabIndex={-1} autoComplete="off" />
    <input type="text" name="phone_number" tabIndex={-1} autoComplete="off" />
  </div>

  <input name="email" type="email" required />
  <textarea name="message" required />
  <button type="submit">Send</button>
</form>

When honeypot: true is set in armor(), it automatically checks these fields and silently rejects bot submissions with a fake 200 response (so bots don't learn they were caught).

SSRF Protection

Validate URLs before server-side fetching:

import { validateFetchUrl } from "next-armor";

const result = await validateFetchUrl(userProvidedUrl);
if (!result.valid) {
  return Response.json({ error: result.reason }, { status: 400 });
}
// Safe to fetch
const data = await fetch(userProvidedUrl);

Blocks: private IPs (127.x, 10.x, 192.168.x), cloud metadata (169.254.169.254), DNS rebinding attacks, non-HTTPS protocols.

Using Individual Modules

Don't want the wrapper? Use modules directly:

import {
  rateLimit,
  getClientIp,
  checkHoneypot,
  evaluateRequest,
  escapeHtml,
  validateFetchUrl,
  ThreatLevel,
} from "next-armor";

// Build your own flow
const limiter = rateLimit({ interval: 60_000, limit: 10 });

export async function POST(request: Request) {
  const ip = getClientIp(request);

  const { success } = await limiter.check(ip);
  if (!success) {
    /* handle */
  }

  const assessment = evaluateRequest(
    ip,
    "/api/submit",
    request.headers.get("user-agent") || "",
  );
  if (assessment.level === ThreatLevel.BLOCKED) {
    /* handle */
  }

  // ... your logic
}

Configure Threat Scoring

Customize detection rules at app startup:

import { configureThreatScore } from "next-armor";

configureThreatScore({
  // Add custom probe paths to detect
  probePaths: ["/api/admin", "/api/internal"],
  // Adjust thresholds
  thresholds: { cautious: 2, suspicious: 5, blocked: 8 },
  // Custom tracking window
  windowMs: 5 * 60 * 1000, // 5 minutes
});

Window Formats

Rate limit windows accept human-readable strings:

| Format | Duration | | ------- | ----------------- | | '30s' | 30 seconds | | '1m' | 1 minute | | '5m' | 5 minutes | | '1h' | 1 hour | | 60000 | 60,000ms (number) |

How Threat Scoring Works

The threat engine evaluates every request using red (threat) and blue (trust) signals:

Red signals (increase score):

  • Burst requests (8+ in 10 seconds)
  • Path probing (wp-admin, .env, .git, etc.)
  • Malicious user agents (sqlmap, nikto, curl, etc.)
  • Honeypot triggers
  • Failed auth attempts

Blue signals (decrease score):

  • Normal browsing pattern (2-10 req/min, no red flags)
  • Legitimate browser user agent
  • Clean history (60+ seconds, zero incidents)

Threat levels:

  • TRUSTED (0-2): Normal traffic
  • CAUTIOUS (3-5): Slightly suspicious, monitor
  • SUSPICIOUS (6-8): Likely malicious, consider blocking
  • BLOCKED (9+): Block the request

Requirements

  • Node.js 18+
  • Next.js 13+ (App Router)
  • zod (optional — only needed if using validate)

FAQ

Does it work on Vercel Edge? Yes. Everything uses standard Web APIs (Request/Response). No Node.js-specific APIs except validateFetchUrl which uses dns/promises.

What happens on cold starts? Rate limit and threat score data resets. This is acceptable for most deployments. If you need persistent rate limiting, use Upstash Redis behind the same rateLimit() interface.

Can I use it without the wrapper? Yes. Every module is independently importable. Use armor() for convenience or individual functions for control.

Is Zod required? Only if you use the validate option. It's a peer dependency marked as optional.

License

MIT

Author

Trent Jackson — Building Pureformance, an architecture practice for founders whose systems are straining.