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

fpyx

v1.4.0

Published

Hardened cross-runtime request fingerprinting for anonymous rate limiting. Zero dependencies. Zero I/O. Zero crypto APIs. Works in Node.js 18+, Bun, Deno, Cloudflare Workers, edge runtimes, and modern browsers. Fast FNV-1a 64-bit hashing with trusted IP d

Readme

fpyx

Why

Rate limiting requires a key. Getting that key right is harder than it looks.

Every rate limiter boils down to one operation: incrementing a counter for a key. But if your key is wrong, your rate limiter does absolutely nothing useful.

The industry standard is..lazy. Most developers just grab req.ip or naively split headers.get('x-forwarded-for').split(',')[0], and test their rate limiting on 127.0.0.1 or a static IPv4 address. They never see IPv6 privacy extensions rotating addresses in real-time, nor see an attacker injecting x-forwarded-for: 1.2.3.4, unknown, _hidden into a proxy chain.

So they don't even know their rate limiter is functionally useless until they get targeted by someone who knows what they're doing.

When you do that in production, you get hit by three things:

  1. Proxies: Behind a load balancer, req.ip is the proxy's IP. Every request lands in the same bucket.
  2. Spoofing: Blindly reading x-forwarded-for lets attackers inject arbitrary strings and rotate their "IP" on every request.
  3. IPv6: Modern OS privacy extensions rotate IPv6 interface identifiers constantly. Rate limiting on a full /128 means attackers bypass your limits just by waiting.

Cloudflare handles DDoS, but it doesn't know your app's business logic. That logic lives in your app, and it needs a correct key.

fpyx gives you that key and that key only. It takes a request, extracts the correct IP from headers your infrastructure actually controls, handles the IPv6 edge cases, and returns a stable FNV-1a fast hash.

Where

Zero dependencies. Works everywhere JavaScript runs with Fetch-standard Request/Headers (Node 18+, Bun, Deno, Cloudflare Workers, Vercel Edge).

[!IMPORTANT] This is not for identity or authentication.

What

Deep dive.

Read the docs/why document, along with the docs/security (attack vectors in IP-based rate limiting) and how fpyx handles them.

Comparison

How this differs from express-rate-limit, rate-limiter-flexible, Upstash Rate Limit, etc? See the full docs/comparison document.

Install

npm install fpyx
# or 
pnpm install fpyx
# or
bun add fpyx

How

The place fpyx earns its keep is pre-auth surfaces: login, signup, password reset, public endpoints, anything where a user hasn't proven who they are yet and IP is the only anchor you have.

For authenticated requests, pass actorId and fpyx uses it exclusively (hashed and scoped, more below), IP is ignored entirely.

You call one function. Pass the request. Get an object back. Hand the hash to whatever counting layer you use (Redis, Upstash, memory, etc).

Here is how you can use it with @upstash/ratelimit:

import { fingerprint } from 'fpyx';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '60 s'),
});

export async function POST(request: Request) {
  const { hash, traits } = fingerprint(request, {
    // 1. Primary Anchor: Use actorId if the user is logged in. 
    // This ignores IP entirely to prevent identity fragmentation.
    actorId: session?.userId, 

    // 2. Trust: Only check headers YOUR infrastructure controls.
    ipHeaders: ['cf-connecting-ip', 'x-forwarded-for'],

    // 3. Partitioning: Separate buckets for different endpoints.
    scope: 'api/v1/login'
  });

  // Security Check: If no actor and no valid IP could be found
  if (!traits.actorId && !traits.ip) {
    console.warn("Unresolved identity: Blocking or alerting recommended.");
    // return new Response('Unauthorized', { status: 401 });
  }

  const { success } = await ratelimit.limit(hash);
  if (!success) return new Response('Too Many Requests', { status: 429 });

  // ... proceed with logic
}

Extra entropy

If the defaults feel too rigid, extra is an escape hatch for anything that doesn't fit scope or actorId. You pass a string, the library folds it into the hash verbatim.

// Invalidate all buckets on a specific day
fingerprint(request, {
  extra: `day:${new Date().toISOString().slice(0, 10)}`,
});

// Kill-switch: rotate everyone's bucket on deploy
fingerprint(request, {
  extra: process.env.BUCKET_VERSION ?? 'v1',
});

extra is trimmed and ignored if empty, so passing undefined or '' has no effect on the hash.

The Output

If the user is unauthenticated (relying on IP):

{
  hash: 'c72a1d8e90b3f445', // The key. Hand this to your counter.

  parts: [
    'ip:2001:0db8:abcd:1200:0000:0000:0000:0000', // IPv6 masked to /56
    'scope:auth/login'
  ],

  traits: {
    actorId: null,
    ip: '2001:0db8:abcd:1200:0000:0000:0000:0000',
    scope: 'auth/login',
    extra: null
  }
}

If the user is authenticated (relying on actorId):

{
  hash: 'a3f1c8e2b4d09571',
  parts: ['actor:user_abc123', 'scope:auth/login'],
  traits: {
    actorId: 'user_abc123',
    ip: null, // Ignored entirely
    scope: 'auth/login',
    extra: null
  }
}

If extra is provided:

{
  hash: 'f3a1...',
  parts: ['ip:203.0.113.10', 'scope:views', 'extra:Something'],
  traits: {
    actorId: null,
    ip: '203.0.113.10',
    scope: 'views',
    extra: 'Something'
  }
}

API

fingerprint(source, options?)

| Option | Type | Default | Description | |---|---|---|---| | actorId | string \| null | null | Verified user, session, or API key. Takes full precedence over IP when present. | | ipHeaders | string[] | see below | Ordered list of headers to check for client IP. | | ipv6Subnet | number | 56 | Prefix length for IPv6 subnet masking. Integer 1-128. | | scope | string \| null | null | Opaque string for key space partitioning. | | extra | string \| null | null | Caller-controlled opaque string for additional entropy. You normalize it, the library just folds it in. | | hashFn | HashFunction | fnv1a64Hex | Custom hash function. |

Default ipHeaders precedence: cf-connecting-ip, fastly-client-ip, fly-client-ip, true-client-ip, forwarded, x-forwarded-for, x-real-ip.

Returns { hash, parts, traits }.

| Field | Type | Description | |---|---|---| | hash | string | 16-character hex string. Hand this to your counter. | | parts | string[] | The components that produced the hash, before joining and hashing. | | traits | FingerprintTraits | The resolved identity: actorId, ip, scope, extra. |

fnv1a64Hex(data)

The default hash function. FNV-1a 64-bit, returns a 16-character zero-padded lowercase hex string. Not cryptographic. Fast.

You can override it.

License

MIT (c) @rccyx