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

@japikey/japikey

v0.4.0

Published

Library to create API keys powered by JWT and JWKS

Readme

japikey_js

Server-side Javascript library for API Key management

API Keys are a common pattern to give users, scripts, and AI Agents access to a resource without requiring them to log in. An API key needs to reference the user, have scopes limiting the access allowed, and be secure.

This repository implements a core library & web route extensions to accomplish this in a secure, manner. With japikey there are:

  • Zero secrets stored in the database. All information is public
  • API keys cannot be tampered once created, even by the server
  • API keys can be revoked
  • Clients can validate API keys with caching (using JWKS)
  • Clients can attest that the api key was emitted by a particular server

Interested in learning more? Check out the How It Works section

Getting Started

For express applications, see the Express quickstart

For cloudflare workers, see Cloudflare quickstart

@japikey ecosystem

The packages (and their dependencies) are split out over several packages for composability and readibility reasons. The current set of packages are as follows:

  • @japikey/japikey - Core package for creating an API key
  • @japikey/authenticate - Package for decoding an API key and determining if it is valid (can also be run on the client-side)
  • @japikey/shared - Common interfaces and errors used through the ecosystem
  • @japikey/experss - Express routes that implement the full japikey endpoints
  • @japikey/cloudflare - Cloudflare Worker routes that implement the full japikey endpoints
  • @japikey/sqlite - Sqlite adapter for saving your apikey information in sql

@japikey/japikey

The @japikey/japikey is the core package in the ecosystem. It contains the main createApiKey function for building an API key. Additionally it re-exports many types from @japikey/shared for convenience. To validate an existing API key, check out the @japikey/authenticate library

Usage:

import { createApiKey } from '@japikey/japikey;

// Add any app-defined data you want to be encoded in the api key
const claims = {
    scopes: ["read", "write"],
}

// Required data
const options = {
  sub: 'my-user-id',
  iss: new URL("https://example.com"),
  aud: 'api-key,
  expiresAt: new Date(), // Pick some time in the future for the key to expire

}
const { jwt, jwks, kid} = await createApiKey(claims, options)
// The jwt is the api key
// The jwks needs to be stored & made available to be able to verify the key
// the kid is the key id encoded within the jwt. This is also needed for verification

Technology Refresher

japikey relies on the standards of public/private key cryptography, via the JWT and JWKS format. Because each of these core pieces are important, let's spend a moment to recap the core features that are used.

Public/Private Key cryptography

Using a math formula, we can generate two sets of numbers A (public), and B(private), such that:

  • Everyone, including any malicious person can know what the Public Key number is
  • It is not computationally feasible to figure out what the Private Key number is, even if you can change any data
  • Given a message M, you can create a digest D with the Private key
  • The digest is a hash, so changing any part of the message will change the digest, and it's just as likely that any bit in the digest may change from modification
  • This also implies it's not computationally feasible to generate two different messages that share the same digest

Signing Process: The private key and message are used to create a digest, which becomes the digital signature.

graph LR
    A[Message] --> C[Digest]
    B[Private Key] --> C
    C --> D[Digital Signature]

Verification Process: The message, public key, and digest are used to validate the signature.

graph LR
    A[Message] --> D[Validation]
    B[Public Key] --> D
    C[Digest] --> D
    D --> E[Valid/Invalid]

This process ensures:

  • Authenticity: Only the private key holder could have created the signature
  • Integrity: Any modification to the message will result in a different hash
  • Non-repudiation: The sender cannot deny creating the signature

JWT

JWTs are a base64 encoded string with three parts, separated by periods. The three parts are header, payload, and signature. Each part, once base64 decoded, is a JSON object, containing both standardized and user-defined fields. The Header contains information about the type of cryptography used. For our scenario, the JWT's will always be signed with public/private key cryptography, so the alg key in the header will be RS256. The payload contains standard information such as the user id (sub field), the expiration time of the token (exp), which server issued it (iss) and more. Additionally, you can put any other JSON information you want in this part. Lastly, the signature part is the cryptography digest of the first two pieces, and then base64 encoded

In practice, we can use the header to confirm the type of cryptography used, and then use the digest to ensure the message has been signed by the server and not tampered with.

If you'll recall from the public/private key section, to verify a message we need the message, digest, and the public key. We can see that the JWT contains the message and the digest. Where does the public key come from? That's what JWKS is for

JWKS

JWKS is a json file, located at some_url/.well-known/jwks.json. Essentially, it contains multiple public keys. Each public key has an id, called a kid. This kid matches up with the key id in the JWT header. Putting it all together, the logic to verify a JWT is this:

  1. Inspect the iss (issuer) field to see who generated the token. Make sure this is one of the issuers you expect to receive a token from
  2. Download the issuer_url/.well-known/jwks.json file to list all the currently-active public keys
  3. Inspect the kid field in the header of the JWT to see which public key should be used. Select the corresponding public key from the jwks.json file
  4. Validate the digest signature, using the message and public key

There is one additional benefit of using JWKS in this manner, instead of e.g. hardcoding the public key in any of the client's code: It is easy to revoke any key signed with a particular key, just by removing it from the jwks.json file

How it works

Now that we have a good understanding of the core technologies, we can talk about how japikeys works. When a user wants to create an API key, japikeys generates a brand-new public/private keypair.

Then, the following claims are added to the JWT:

  • header: {"alg": "RS256", "kid":"key-123"}
  • claims: {"sub": "user-id-123", "iss": "https://mydomain/jwks/key-123", "exp": someDateInTheFuture...}

and then the JWT is signed with the private key. Following signing, the private key is discarded, never to be saved or used again. This way, once generated, nobody, not even the server can modify the API key. An attacker also can't use the private key to change any of the claims because it no longer exists.

Once generated, the last step is to store the public key, along with some metadata (which user is the API key for, expiry, etc. in a database or other durable storage).

graph TD
    A[Database] --> B[Key-123]
    B --> C[Public Key]
    B --> D[Metadata]

    F[JWT Token] --> G[Header]
    F --> H[Payload]
    F --> I[Signature]

    G --> J[alg: RS256]
    G --> K[kid: key-123]

    H --> L[sub: user-id-123]
    H --> M[exp: expiration]
    H --> N[iss: https://example.com/jwks/key-123/]

The final thing the server needs to do is host a route for https://example.com/jwks/{:key_id}/.well-known/jwks.json The handler should take the key_id, look it up in the database, and return the matching public key in JWKS format. And that's it!

With this implementation, anyone (the client, the server, or anyone on the internet) can validate the API key with code like this: (error handling ommitted for brevity)

import * as jose from 'jose';
const unverified = jose.decodeJwt(token);
if (!/^https:\/\/example\.com\/jwks\/[0-9a-f-]{36}\/$/.test(unverified.iss)) return false; // Reject unexpected issuers
const jwks = jwks.createRemoteJWKSet(unverified.iss);
jose.jwtVerify(token, jwks);

It is extremely important that the clients have a strict allowlist for the issuer here. Otherwise, an attacker could create their own key, but pointing to a different issuer that they control. Since the issuer is used, before validation, this is explicitly something that has to be checked for.

And that's it! To query the list of active API keys, the server can use the database to view the keys and their metadata. Expired keys can optionally be reaped if desired. Keys will expire automatically without any active action. Once the exp field is in the past, then jwtVerify will automatically fail. Revoking a key is as simple as removing the key from the database.

Thoughts on jwks.json

Typically, the jwks can be cached for some period of time, in order to save an extra network lookup to grab the public key. We could keep the same behavior by having one global jwks.json file. This file would need to contain a new public key for every active API key. So, if there are 1,000 outstanding API keys, then this JSON file would have 1,000 entries in it. Additionally, even if that were the case, you still couldn't cache it for too long a time, because it's constantly being updated with new API keys as they are created.

Instead, we play around with the issuer. By changing the base url, we're able to self-encode that extra bit of information the server needs to return just a subset of information. With this trick, we know the key id, and the JWKS will always have zero or one hits, since the key id's are unique. On the caller side, this also has the benefit that the jwks payload, once you get a result, is (mostly) immutable. You don't have to worry about the key changing. However, you do need to refresh in order to catch API key revocation. That tradeoff is left as an security consideration to the client. You can check the jwks.json every time with the cost of some latency, or you can e.g. have an LRU cache of jwks and expire them as your policy finds reasonable (5 minutes, 1 hour etc).

S3 bucket

An alternative to dynamically querying the database for the .well-known/jwks.json route would be to just create the .json payload once during API key creation time, and upload that to e.g. S3 or some other static hosting. As mentioned, the only time this would need to be modified is in case of API key revocation, making this something that can be easily hosted without worrying about caching, or compute.

No secret data

The private key is the only secure piece of information in this whole process. As mentioned, we're purposefully discarding it, so there's no worry of it leaking. The public key in jwks.json is always meant to be public, so that's not an issue. If someone were to enumerate your website for all of the jwks.json files, they could only figure out the total number of API keys, but not any other information. You can also prevent this by detecting brute-force attacks (since it's a UUID) and blocking the crawler.

The only data not already exposed to the world is the metadata in the database. However, this isn't any particularly sensitive information. Presumably you already have e.g. the userId elsewhere in your database. Even with the metadata, you can't tweak the API keys, since you need the private key

Security considerations

Most of the primary security challenges are outside the scope of this library. The API key server, as well as the database need to be secured. Compromise of either would allow an attacker to insert their own public key into the database, granting access.

Authorization of all the endpoints is also outside the scope of this library. Any user-confusion or other attacks could allow an attacker to create an API key they don't have access to.

As mentioned, validating the API key comes with one hard requirement - that the issuer is validated from an allowlist. This has to be done every place an API key is used, potentially increasing the risk of improper validation.

API key revocation is also dependent on verifiers refreshing the JWKS public key info. If a client incorrectly caches the data, the revocation won't be detected. This can be an easy mistake to make because many JWKS implementations have caching built-in.

Performance considerations

Public/Private Key validation is slower than symmetric algorithms. If you are shaving nanoseconds, then this probably isn't the right implementation for you. Even more expensive is generating a public/private key (which also requires enough entropy in the system). This is only done when creating an api key (and is presumably a less-common and less time-sensitive operation).

In general, a JWT is also a larger header than other authentication schemes. If every byte matters, then consider something else.