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

@codefusion-cc/cloudflare-access

v0.1.0

Published

Cloudflare Access as an app's sign-in: the application token of each request verified in the Worker (signature against the team's cached keys, audience, issuer, expiry), a fetch for the page that tells a session that ended from a lost connection, and a fa

Readme

@codefusion-cc/cloudflare-access

Cloudflare Access as an app's sign-in. Access stands in front of some paths of the app, signs people in and adds a signed token to every request it lets through. This package is the app's side of that:

  • in the Worker, the token of each request verified against the team's published keys, so a request that reached the Worker around Access is refused;
  • in the page, a fetch on which a session that ended is a 401 and not a lost connection;
  • for tests, a fake Access team whose keys the test holds.

Access says only who someone is. What they may do stays the app's.

WebCrypto and fetch only. The key cache is remoteKeys of @codefusion-cc/google-sign-in, which this package depends on for it.

npm install @codefusion-cc/cloudflare-access

In the Worker

import { accessApplication, accessSignIn } from '@codefusion-cc/cloudflare-access'

// Neither value is a secret: plain `vars`. Null while either is unset.
const application = accessApplication(env.ACCESS_TEAM_DOMAIN, env.ACCESS_AUD)

const signIn = await accessSignIn(request, application)
if (!signIn.ok) return error(signIn.status, signIn.reason)   // 401 signed-out, 503 not-configured or keys-unavailable
const { email, subject } = signIn.identity

accessSignIn is the whole decision: without an application nobody is let in (503 not-configured, whatever token a request carries); no token or one that does not verify is 401 signed-out, with the problem for the log and never for the client; keys that could not be read are 503 keys-unavailable with retryAfterSeconds. The answer is kept for the request, so a guard in front of the routes and a route can both ask and the token is verified once (another application, or other options, is another question and is answered anew). verifyAccessRequest and verifyAccessToken underneath give the bare check.

  • What is checked: the token in Cf-Access-Jwt-Assertion (never the cookie) is a JWT of at most 16 000 characters; its algorithm is RS256, fixed and never read from the token, so alg: none and HMAC-with-the-public-key fail; iss is exactly https://<team domain>; aud holds the application's audience tag; type is app (the team-wide session token is not for origins); exp is in the future and nbf and iat, when there, are not, each with 60 s of allowance for clocks (clockSkewSeconds); email is an address (a service token names no person and is refused); and last, the signature holds under the published key the token's kid names. The claims come first only so a token that could never pass costs no key lookup; nothing is accepted before the signature holds.
  • Never the header's presence or the hostname. On an address Access does not cover (workers.dev, a preview, a path the application misses) anyone can send the header; only a token the team signed for this application passes. A request with no token is missing, wherever it arrived.
  • Results, not exceptions: { ok: true, identity } or { ok: false, problem }. keys-unavailable means the team's keys could not be read: answer 503, it is not a forgery. Only a programming mistake rejects with a TypeError: an application that is none, or a now, clockSkewSeconds or maxAgeSeconds that is not a finite number from 0 (Number(env.MAX_AGE) of an unset variable is NaN, which would switch the limit off). A failure of the runtime's own WebCrypto rejects with its error.
  • maxAgeSeconds refuses a token issued longer ago, whatever its own expiry says: a ceiling on the session length that holds when the application's is set longer by mistake. Keep it at or above the application's session duration, or people are refused while Access still lets them through.

The team's keys

https://<team domain>/cdn-cgi/access/certs, through one cache per isolate and team (accessKeys):

  • used for 5 minutes, then read again; a key the team withdraws stops working within that time;
  • a token naming a key id the cache has not seen reads the keys again at once (Access rotates them every six weeks and keeps the old one for seven days), at most once per 30 seconds, so made-up key ids cannot make the Worker hammer the endpoint;
  • while the endpoint cannot be reached, the last good keys serve for up to 24 hours; a key id they do not hold, and everything after those 24 hours, is keys-unavailable.

In the page

Once the session ends, Access answers a request with a redirect to its sign-in page on the team's domain. fetch follows it into a CORS failure and rejects exactly as it does for a lost connection. Cloudflare documents the way out (Cloudflare One → Access settings → Session management): Access answers 401 instead for requests that carry X-Requested-With: XMLHttpRequest.

import { ACCESS_LOGOUT_PATH, accessFetch } from '@codefusion-cc/cloudflare-access/browser'

const send = accessFetch()
const response = await send('/api/panel/orders', { method: 'POST', body })
if (response.status === 401) showSignInAgain()   // keep what the person typed

accessFetch() is fetch for the page's own origin with that header, credentials: 'same-origin' and redirect: 'manual'; a redirect Access still answers with comes back as an empty 401. So 401 always means "not signed in" (Access's, the Worker's, or the redirect), and a rejection is still a lost connection or an abort. An API behind it must never redirect. It refuses another origin, and an init that asks for other credentials or redirect.

Signing in again is any navigation of a tab to a path of the application: a reload, or a new tab when a form must keep its values (window.open('/panel'), then retry once the person comes back). A request cannot do it. A link to ACCESS_LOGOUT_PATH signs out.

Session length and revocation

The Worker sees a token, not the session behind it. It cannot learn that someone was taken off a policy or that their session was revoked: an issued token verifies until its exp.

  • On a hostname the application covers, Access refuses revoked sessions itself, before the Worker (Cloudflare: within about a minute). Taking someone off the policy alone does not revoke: their token lasts until the application's session duration ends. So remove the person and revoke their session (Zero Trust → Team & Resources → Users → Revoke), and keep the application's session duration short enough for the day that is forgotten.
  • A short session costs little: when the application's token ends and the team-wide session still holds, the next navigation gets a new one without a prompt.
  • What the Worker can do: cap the age it accepts (maxAgeSeconds), and keep its own list of who may do what.

The Access application

One self-hosted application per app, with a destination for every path the Worker serves behind the sign-in (the pages and their API, e.g. /panel and /api/panel), so they share one audience tag. A policy that allows the staff's addresses, a session duration, and one login method. The team domain and the application's audience tag (its Additional settings) are the two values the Worker needs.

In tests

import { testAccessTeam } from '@codefusion-cc/cloudflare-access/testing'

const access = await testAccessTeam()
vi.stubGlobal('fetch', access.fetch)                       // the certs endpoint, as the Worker's fetch; what else
                                                            // the Worker fetches: testAccessTeam({ passThrough })
const env = { ACCESS_TEAM_DOMAIN: access.teamDomain, ACCESS_AUD: access.audience }

await worker.fetch(await access.request('https://shop.test/api/panel/me', '[email protected]'), env)
await access.token('[email protected]', { aud: ['another-application'] })
await access.token('[email protected]', {}, { signedWith: 'stranger' })   // or 'none', 'hmac-public-key'
access.answerKeys('error')                                  // 'network', 'malformed', 'empty', 'hang'
await access.rotate()

Each team gets a domain of its own by default, so two tests never share cached keys. It runs in Node, in workerd and in a browser.