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

@c9up/visa

v0.1.3

Published

Visa — OAuth 2.1 authorization server for the Ream framework: authorization code with PKCE, refresh rotation, revocation and introspection, on a store contract

Readme

@c9up/visa

An OAuth 2.1 authorization server for the Ream ecosystem. It issues tokens to other applications on a user's behalf.

It does not authenticate anyone — that stays warden's job. Visa asks your application who is signed in, and everything else is its business.

No dependencies: node:crypto only. A package in the path that mints and compares credentials is a package that can replace them.

ream configure @c9up/visa

What it implements

| | | |---|---| | Grants | authorization_code (PKCE required), refresh_token, client_credentials | | Endpoints | /oauth/token, /oauth/revoke, /oauth/introspect, /.well-known/oauth-authorization-server | | Removed | implicit and password — gone from OAuth 2.1, and not available under any option |

/authorize is not mounted, deliberately: it needs a signed-in user and a consent screen, and both belong to your application. See below.

Security decisions, and why

  • PKCE on every client, confidential ones included — a code on the front channel is interceptable whoever asked for it.
  • S256 only. plain puts the verifier in the authorization request, so anything that can read that request can complete the exchange, which is what PKCE exists to stop. allowPlainChallenge: true brings it back; don't.
  • Exact redirect URI matching, with the one exception the spec names (a loopback port). A prefix match is how an open redirect on the client's own domain becomes a stolen code.
  • An error is never redirected to an unverified URI. A bad redirect_uri is shown to the user, not bounced.
  • Everything is hashed at rest — codes, access tokens, refresh tokens, client secrets. A database dump is not a set of working credentials.
  • Refresh rotation with replay detection. Every use mints a new token; a spent one coming back proves a leak, and since there is no telling the thief from the victim, the whole family goes.
  • One sentence per failure. "No such code", "expired", "already used" and "wrong client" are invalid_grant: The code is not valid. — telling them apart is how a code space gets probed.

Wiring /authorize

The decision is visa's; the page is yours.

// start/routes.ts
import visa from '@c9up/visa/services/main'

router.get('/oauth/authorize', async (ctx) => {
  const outcome = await visa.authorize(ctx.request.qs(), ctx.auth.user?.id)

  if (outcome.type === 'redirect') return ctx.response.redirect(outcome.url)
  if (outcome.type === 'error') {
    // NOT a redirect: the redirect URI is what failed validation.
    return ctx.view.render('oauth/error', { error: outcome.error.toResponse() })
  }
  if (!ctx.auth.user) return ctx.response.redirect('/login?next=' + encodeURIComponent(ctx.request.url(true)))

  return ctx.view.render('oauth/consent', {
    client: outcome.request.client,
    scopes: outcome.request.scopes,
  })
})

router.post('/oauth/consent', async (ctx) => {
  const outcome = await visa.authorize(ctx.request.all(), ctx.auth.user.id)
  if (outcome.type !== 'consent') return ctx.response.redirect('/')
  const url = ctx.request.input('approve')
    ? await visa.grant(outcome.request, ctx.auth.user.id)
    : visa.deny(outcome.request)
  return ctx.response.redirect(url)
})

Registering a client

const { client, secret } = await visa.registerClient({
  id: 'invoices',
  name: 'Invoices',
  redirectUris: ['https://invoices.example.com/callback'],
  scopes: ['profile', 'invoices:read'],
})
// `secret` is shown once and never stored in plaintext. A client that loses
// it gets a new one.

A public client — a SPA, a native app — registers with tokenEndpointAuthMethod: 'none' and gets no secret. It may not use client_credentials: "the client itself" means nothing when anyone can read its id out of a browser.

Protecting a resource

const grant = await visa.verify(bearerToken)
if (!grant) return ctx.response.unauthorized({ error: 'invalid_token' })
if (!grant.scopes.includes('invoices:read')) {
  return ctx.response.forbidden({ error: 'insufficient_scope' })
}

Testing

@c9up/visa/testing hands back the REAL server on a memory store, with a client already registered:

import { testVisa } from '@c9up/visa/testing'

const t = await testVisa({ scopes: ['profile', 'invoices:read'] })
const tokens = await t.tokensFor('user-7', 'invoices:read')
const grant = await t.visa.verify(tokens.access_token)

tokensFor() walks authorize → consent → code → exchange rather than minting a token directly. Not a lenient double on purpose: a helper that granted whatever was asked would teach applications to ship a consent screen nobody has seen refuse.

The issuer

It identifies the SERVER, and that is all it does here. An access token is opaque — a random string with no claims inside — so nothing is "bound" to an issuer cryptographically; the only thing that can vouch for such a token is the server that minted it. That is why /oauth/introspect answers with iss, and why the metadata document names it.

It is validated at boot rather than at the first request: it must be a URL, https (localhost excepted), with no query string and no fragment, and a trailing slash is dropped — every endpoint is built by concatenation, so https://auth.test/ would otherwise produce https://auth.test//oauth/token.

The store

MemoryStore is for tests and a single development process. Everything else implements VisaStore — twelve methods. Two of them, consumeAuthorizationCode and consumeRefreshToken, must be atomic: two requests racing with the same code must not both succeed, and that single-use guarantee is what replay detection is built on.

What is not here yet

OpenID Connect — id_token, discovery, JWKS, /userinfo. The nonce is already carried through the authorization code for it.

License

MIT