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

@broberg/apikey

v0.1.1

Published

Framework-agnostic inbound API-key primitives for the broberg.ai fleet: mint prefixed keys, timing-safe verify (hashed or plaintext), sliding-window rate-limit over a pluggable store, a Cloudflare-style authorization cascade (permission × resource-filter

Readme

@broberg/apikey

Framework-agnostic inbound API-key primitives for the broberg.ai fleet. It owns the dangerous-to-get-wrong bits — minting, constant-time verification, rate-limiting, and a Cloudflare-style authorization cascade — and leaves storage, tenancy, and request-context resolution to you. Bring your own lookup.

Designed from a 9-repo fleet survey (trail · cardmem · cms · upmetrics · vn): the package never forces hashing, a tenancy model, a fixed prefix, or a rate-limit backend.

npm i @broberg/apikey      # exact-pin for prod-auth deps

Core (@broberg/apikey)

import { generateKey, hashKey, verifyKey, makeKeyPreview, hasScope } from "@broberg/apikey";

const raw = generateKey("trail");           // "trail_<64 hex>"  — show ONCE
const stored = hashKey(raw);                // sha256 — store this (hash-at-rest)
const preview = makeKeyPreview(raw);         // "trail_0a1b2c3d" — display/grep anchor

// On each request — YOU do the DB read; the package does the constant-time compare:
verifyKey(presented, stored);               // hashed (default): timingSafeEqual(sha256(presented), stored)
verifyKey(presented, stored, { hashed: false }); // plaintext-revealable (upmetrics-style)

hasScope(["content:*"], ["content:write"]); // true — exact / `*` / `area:*`

timingSafeEqual(a, b) is exported too — the length-checked constant-time compare that replaces unsafe a !== b token checks.

Rate limit — pluggable store

In-memory by default (single-machine). For a stateless multi-machine fleet, pass a shared store so the window doesn't leak per machine:

import { SlidingWindowRateLimiter, type RateLimitStore } from "@broberg/apikey";

const limiter = new SlidingWindowRateLimiter({ windowMs: 60_000, max: 100 });
const { allowed, remaining, resetAt } = await limiter.check(clientKey);

// One limiter, per-key caps — pass a per-check `max` override (v0.1.1):
await limiter.check(clientKey, { max: keyRecord.rateLimitPerHour });

// Shared backend (Turso/Redis): implement one method.
const turso: RateLimitStore = {
  async hit(key, now, windowMs) { /* … */ return { count, oldest }; },
};
new SlidingWindowRateLimiter({ windowMs: 60_000, max: 100, store: turso });

Authorization cascade (@broberg/apikey/authorize)

The optional rich tier — permission × resource-filter × CIDR × TTL (modelled on cms F134). Simple adopters skip this and use hasScope.

import { evaluateToken, type TokenGrant } from "@broberg/apikey/authorize";

const grant: TokenGrant = {
  permissions: ["deploy:trigger"],
  resources: [{ scope: "site", effect: "include", targets: ["fysiodk"] }],
  ipFilters: [{ mode: "in", cidrs: ["203.0.113.0/24"] }],
  notBefore: Date.parse("2026-01-01"),
  notAfter: Date.parse("2027-01-01"),
};

const decision = evaluateToken(grant, {
  permission: "deploy:trigger",
  resource: { scope: "site", target: "fysiodk" },
  ip: "203.0.113.5",
});
// → { allowed: true } | { allowed: false, reason: "expired" | "permission_denied" | "resource_denied" | "ip_denied" }

Cascade order: TTL → permission → resource (exclude wins) → CIDR (IPv4 + IPv6, zero-dep). A scope with no filter is unconstrained.

Tenant selector (trail's selector-not-grant)

import { selectTenant, TenantAccessError } from "@broberg/apikey/authorize";

// A `spansAll` key lets the owner pick any tenant they belong to via a header.
// A non-member slug is a HARD refuse — never a silent fall-back to home.
try {
  const tenant = selectTenant({ requestedSlug, homeTenant, spansAll: true, isMember });
} catch (e) {
  if (e instanceof TenantAccessError) return new Response(null, { status: 401 });
}

Adapters

// Stack B — Hono
import { honoApiKeyMiddleware, honoRateLimit } from "@broberg/apikey/hono";
app.use("/api/*", honoApiKeyMiddleware({ lookup, authorize }));   // 401/403; c.get("apiKey")
app.use("/api/*", honoRateLimit(limiter));                         // 429 + Retry-After

// Stack A — Next.js (Web-standard Request/Response, edge-safe, no `next` dep)
import { withApiKeyAuth, nextRateLimit } from "@broberg/apikey/next";
export const POST = withApiKeyAuth(async (req, record) => Response.json({ ok: true }), { lookup });

lookup(presented) => record | null is yours: hash + DB/filesystem read, your storage, your tenancy. The package never sees your store.

Boundaries (what it deliberately does NOT do)

  • No storage — no DB/CRUD layer; you own the schema (Drizzle / libSQL / JSON).
  • No request→tenant resolution — that's your proxy/router; feed the result into selectTenant.
  • No bundled Redis/Turso — ships the RateLimitStore interface + in-memory only.
  • Core crypto is Node/Bun (node:crypto). At the edge, hash via Web Crypto inside your lookup; the adapters themselves are edge-safe.

MIT · part of the broberg.ai shared inventory.