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

@keyring-dev/next

v0.1.1

Published

Next.js App Router adapter for Keyring. Wraps a route handler on the Node runtime; verification is CPU-only and makes no network call.

Readme

@keyring-dev/next

Next.js App Router adapter for Keyring, on the Node runtime. MIT.

# .env.local
KEYRING_SECRET_KEY=krsk_live_...   # your control-plane key
KEYRING_PROJECT_ID=...             # the project whose policy this process caches
KEYRING_BASE_URL=https://...       # your control plane; no default
// app/api/orders/route.ts
import { getKeyring, withKeyring } from '@keyring-dev/next';

export const runtime = 'nodejs';

const keyring = getKeyring({
  resources: { orders: { live: liveOrders, test: testOrders } },
});

export const GET = withKeyring(
  (_request, { keyring: context }) =>
    Response.json({
      tenant: context?.tenantId,
      env: context?.env,
      orders: context?.orders,
    }),
  { instance: keyring },
);
curl -H "Authorization: Bearer kr_test_..." http://localhost:3000/api/orders

This block is compiled the way a customer compiles it — tsc against the published dist/, with no project references and no path mapping — and that check pins the context property's exact type. A runnable version of it is executed against a stub control plane in the source repository's own test suite, which also runs next build over this exact block in a real Next.js project, because Next.js type-checks a route handler's signature at build time and never in next dev.

context is KeyringContext | null, the same shape req.keyring has in @keyring-dev/express and @keyring-dev/fastify — one row of @keyring-dev/sdk's cross-language contract table, so the ?. above is not defensive style. It is null on a route configured skip: true and on a request the wrapper refused; a refused request never reaches your handler at all. Unlike Express's property there is no "from here onward" caveat: the wrapper builds the object it hands you, so the field is always present.

Everything the wrapper does is in @keyring-dev/sdk — read its README for the three onUnavailable modes, the three ergonomic levels, rate limiting, idempotency and the telemetry buffer. This package is translation.

One client per process, and what the dev server does to that

getKeyring() returns a process-wide client held on globalThis, the same pattern a Next.js codebase already uses for globalThis.prisma, and for the same reason. The client owns a poller on an interval, an LRU that has to outlive a request, a disk snapshot and a telemetry buffer. next dev re-evaluates a route module on every save, so a client constructed at module scope without this would be a new poller on every edit, with the previous ones still holding their timers — nothing calls close() on a module the bundler dropped.

The options are read by whichever call constructs the instance; a later call with different options gets the instance that already exists rather than a second one. So configure it in one place — a lib/keyring.ts that exports getKeyring({ ... }) — and pass it as instance from each route, as the quickstart does. A call that hands it options after the client exists is reported through that call's onError, so an edit that did not take says so instead of looking like one that did.

The edge runtime, middleware.ts and proxy.ts

They cannot run this SDK, and this package does not pretend otherwise.

Verification is a cache lookup in your own process, not a call to us: a poller on an interval, an LRU of the project's keys, a disk snapshot that makes the next boot survive an outage of ours, and AsyncLocalStorage for currentEnv(). The edge runtime has no long-lived process to hold any of that and no filesystem to persist it, and the only edge-shaped alternative — asking us over the network on every request — is the single thing this architecture exists not to do. There is no degraded edge mode here to reach for.

So every route that uses withKeyring needs export const runtime = 'nodejs', and middleware.ts (and Next.js 16's proxy.ts) is out, because Next.js runs it on the edge runtime.

Under the edge-light and worker export conditions — the ones the edge runtime resolves, under webpack and under Turbopack alike — @keyring-dev/next is a module that throws that instruction the moment it is evaluated, exports the same names the real entry does (each one a function that throws too), and never imports the SDK. The exports matter as much as the throw: Turbopack checks an import against the target module's exports statically, before anything runs, so a module with none of them failed that check with "Export withKeyring doesn't exist in target module" instead of ever reaching the message below. An export const runtime = 'edge' route therefore fails next build naming runtime = 'nodejs' and middleware.ts, rather than on webpack's UnhandledSchemeError: Reading from "node:crypto" — what it produced before these export conditions existed — or on Turbopack's static export error. The source repository's own test suite builds such a route under both bundlers and asserts which message comes out.

Pages Router

Not supported. withKeyring wraps an App Router route handler — a Request in, a Response out — and a pages/api/* handler is the Node (req, res) pair instead. Wrapping both would be two code paths in a package whose whole job is to be thin, so this one does App Router only.

Serverless, and why the default may be the wrong one there

A process that starts cold and serves one request never gets a policy snapshot in time. Under stale-then-open — the SDK default — a store that has never loaded admits any well-formed key, unverified, for as long as it stays that way, and the key format's checksum is CRC-32, so producing one costs an attacker nothing. That is the third row of @keyring-dev/sdk's failure-mode table and it is the row a fresh serverless invocation sits in.

If you deploy this to a platform that gives you a fresh process per request or close to it, set onUnavailable: 'stale-then-closed' and accept the other cost — during an outage of ours a cold process refuses every request until its first successful poll. On a long-lived Node server (next start, a container, a VM) the default is the default for the usual reasons, and persist (on by default) makes a restart that keeps its filesystem serve from the snapshot it left.

Per-route options

The wrapper takes the same rule the routes table takes, for the route this file serves:

export const POST = withKeyring(handler, { onUnavailable: 'closed' }); // money
export const GET = withKeyring(handler, { skip: true }); // no verification, no event

A rule passed here replaces the routes table's rule for this request; it does not merge with it. That matters on a money route. With routes: { 'POST /api/payments': { onUnavailable: 'closed' } } on the client, withKeyring(handler, { scopes: ['write:payments'] }) discards the closed and puts the route back on the default, stale-then-open — where a key the cache has never seen is admitted unverified and, by design, not scope-checked. The scopes you added to tighten the route protect nothing there and cost it the closed mode. Put every setting a route needs in one place: all of it on the wrapper, or all of it in routes. @keyring-dev/sdk's own test suite pins the replacement.

skip: true leaves context.keyring null, runs your handler and records no usage event. scopes and maxStalenessMs are accepted too. A dynamic route can pass route: '/api/orders/[id]' so the request log and any routes rule see the pattern rather than a different concrete path on every request.

next build needs the same environment as the server

next build evaluates each route module to collect page data, and the quickstart builds its client at module scope — so the build environment needs KEYRING_SECRET_KEY, KEYRING_PROJECT_ID and KEYRING_BASE_URL too, or the build fails on the SDK's "missing configuration" the moment getKeyring() constructs the client. Construction alone makes no network call and starts no poller — getKeyring() detects next build's NEXT_PHASE and defers start() to the process that actually serves requests, so a build worker never presents your live secret key to the control plane. The source repository's own test suite builds the quickstart against a counting stub and asserts it receives zero requests.

Rate limits and idempotency

Both are the SDK's, unchanged. Rate-limit headers are set on whatever Response your handler returned — including one it proxied from fetch(), whose headers are immutable, which is rebuilt rather than left unlabelled. A request carrying Idempotency-Key has its body read from a clone before your handler runs (your handler still gets an unconsumed body) and its response buffered and stored before the bytes reach the caller, which is what makes a replay correct for a process that dies mid-stream.