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

cf-access-mock

v0.1.0

Published

Local mock of Cloudflare Access for offline development: serves a JWKS at /cdn-cgi/access/certs and mints RS256 CF_Authorization JWTs. No WARP, no internet, no real Cloudflare needed.

Readme

cf-access-mock

Local mock of Cloudflare Access for offline development. It serves a JWKS at Cloudflare's literal /cdn-cgi/access/certs path and mints RS256 CF_Authorization JWTs with the exact claim shape of a real Access application token — so your app's existing JWT verification (signature via JWKS, iss, aud, email) passes unchanged, with no WARP, no VPN, no internet, and no real Cloudflare involved.

The problem

Apps deployed behind Cloudflare Access validate the Cf-Access-Jwt-Assertion header / CF_Authorization cookie against https://<team>.cloudflareaccess.com/cdn-cgi/access/certs. For local development the usual workaround is copying CF_Authorization out of a real, logged-in staging session in devtools — which dies whenever the token expires, staging is down, the VPN/WARP client is broken, or you're offline.

cf-access-mock replaces the Cloudflare side entirely: your own local issuer, your own keys, tokens for any email you ask for.

Requirements

  • Node.js ≥ 18.17 (native fetch and jose v6).
  • openssl on PATH — impersonate mode only, to generate the local CA and leaf certificate on first run. Preinstalled on macOS (LibreSSL works fine) and virtually every Linux distro; the default http mode never touches it.
  • macOS or Linux for impersonate mode (it reads /etc/hosts and binds :443; on Linux the bind needs a one-time setcap, see below). The default http mode has no OS-specific behavior. Windows is untested.
  • sudo once for the impersonate-mode /etc/hosts entry — the tool never runs sudo itself, it prints the one-liner for you.

Quick start

npm install -g cf-access-mock

cf-access-mock --email [email protected]
cf-access-mock is running
  issuer   http://localhost:8788

Point your app's issuer/JWKS override at the mock (.env.local):
  CF_ACCESS_ISSUER_URL_OVERRIDE=http://localhost:8788
  CF_ACCESS_AUDIENCE=b0d0...64-hex...c4f1

CF_Authorization ([email protected], expires in 6h):
  eyJhbGciOiJSUzI1NiIsImtpZCI6...

Browser options:
  a) open http://localhost:8788/[email protected]   <- sets the cookie, no extension needed
  b) ModHeader: cf-access-jwt-assertion = <token above>
  c) console:  document.cookie = "CF_Authorization=<token above>; path=/; max-age=21600"

Fresh token any time:  curl 'http://localhost:8788/[email protected]'

The /login trick works because cookies are host-scoped but port-blind (RFC 6265): a cookie set from localhost:8788 is also sent to your app on localhost:3000.

Two integration shapes

1. Your app can point its issuer/JWKS URL at anything → default http mode

If your app builds its verification from a configurable issuer or JWKS URL, just point it at the mock. jose's createRemoteJWKSet (and most JWKS clients) happily fetch plain http:// URLs, so no TLS is involved.

A typical consumer, unchanged from what you'd run in production:

import { createRemoteJWKSet, jwtVerify } from "jose";

const ISSUER = process.env.CF_ACCESS_ISSUER_URL_OVERRIDE ?? `https://${process.env.CF_ACCESS_TEAM_DOMAIN}.cloudflareaccess.com`;
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/cdn-cgi/access/certs`));

const { payload } = await jwtVerify(token, JWKS, {
  issuer: ISSUER,
  audience: process.env.CF_ACCESS_AUDIENCE,
});

2. Your app hardcodes https://<team>.cloudflareaccess.com → impersonate mode

Many codebases (reasonably) refuse non-Cloudflare issuers and build the URL from a bare team name. For those, the mock can become a fake team:

cf-access-mock --mode impersonate --team local-mock --email [email protected]

This serves the same issuer over https on port 443 for the hostname local-mock.cloudflareaccess.com, using a locally generated CA. Two one-time steps, both printed by the tool:

# 1. Resolve the fake team locally (the only sudo ever needed; the name is fake, so
#    the entry never conflicts with your real team and can stay forever):
echo "127.0.0.1 local-mock.cloudflareaccess.com" | sudo tee -a /etc/hosts

# 2. Make Node processes trust the mock CA (Node ignores the OS keychain;
#    set it in the shell that starts each service):
export NODE_EXTRA_CA_CERTS="$HOME/.cf-access-mock/ca.pem"

Then configure the app exactly like production, no code changes:

CF_ACCESS_TEAM_DOMAIN=local-mock
CF_ACCESS_AUDIENCE=<printed by the tool>

Port 443 is required because apps build the certs URL without a port. On macOS this needs no sudo (unprivileged processes may bind low ports on the wildcard address); on Linux grant the capability once: sudo setcap cap_net_bind_service=+ep "$(command -v node)".

On every impersonate-mode start the tool self-checks the setup end to end, three layers with a ✓/✗ line each and the exact fix on failure:

  1. DNS — the fake hostname must resolve to this machine through the system resolver (without the hosts entry, *.cloudflareaccess.com resolves to real Cloudflare — which later fails as a confusing signature/kid mismatch, not a DNS error). Includes a DNS-flush hint when /etc/hosts has the entry but it hasn't taken effect yet.
  2. Issuer — the JWKS must answer over https:443 with the mock CA pinned.
  3. CA trust — the same request without pinning, through Node's default trust store: it succeeds only if NODE_EXTRA_CA_CERTS was exported before the process started. This catches "exported in the wrong shell", "set after startup", and "points at the wrong file" (each gets its own message). It reflects the shell the mock was started from — start it from the same environment your services use (a global export in your shell profile makes this a non-issue).

First-run checklist (impersonate mode)

  1. Start the mock — if your app doesn't run on port 3000, set the login redirect target now:
    cf-access-mock --mode impersonate --email [email protected] --redirect http://localhost:3100
  2. One-time: hosts entry. If the startup report says Self-check: FAILED, run the printed sudo one-liner and restart the mock until you see Self-check: OK. The entry is permanent and harmless — the team name is fake, so it never collides with a real Cloudflare team.
  3. One-time: trust the CA in every shell that starts a JWT-verifying service, before starting it — Node reads this at process start, so putting it in a runtime-loaded .env file does nothing:
    export NODE_EXTRA_CA_CERTS="$HOME/.cf-access-mock/ca.pem"
    Add it to the same place you keep per-project shell setup (direnv, profile, Makefile) — a permanent global export in ~/.zshrc/~/.bashrc is safe, it only adds one CA for Node processes. The startup self-check verifies this took effect (✓ CA trust); if it prints ✗, fix the export and restart the mock from a shell that has it.
  4. Point your services at the mock with the two values printed at startup, then restart them:
    CF_ACCESS_TEAM_DOMAIN=local-mock
    CF_ACCESS_AUDIENCE=<printed by the tool>
  5. Authentication is not authorization. The mock signs any email you ask for, but if your app maps emails to roles in its own database, seed the minted email there too — otherwise you'll be authenticated and still hit the app's "no access" state.
  6. Log the browser in:
    open "http://localhost:8788/[email protected]"
    The cookie is set for localhost (ports don't matter for cookies), and you land on your app authenticated. Alternatives: a header-injection extension (cf-access-jwt-assertion: <token>) or document.cookie in the console — both printed at startup.
  7. Verify independently (optional):
    curl --cacert ~/.cf-access-mock/ca.pem https://local-mock.cloudflareaccess.com/cdn-cgi/access/certs

Endpoints

| Endpoint | Purpose | | --- | --- | | GET /cdn-cgi/access/certs | JWKS — same path as real Cloudflare Access | | GET /mint?email=<addr>&ttl=<30m\|6h\|1d> | Returns a fresh signed JWT as plain text | | GET /login?email=<addr>&redirect=<url> | Sets the CF_Authorization cookie and redirects (default http://localhost:3000) | | GET / | Service info |

Minted claims

{
  "aud": ["<your audience>"],
  "email": "[email protected]",
  "iss": "http://localhost:8788",
  "type": "app",
  "identity_nonce": "6ei69kawdKzMIAPF",
  "sub": "7335d417-61da-459d-899c-0a01c76a2f94",
  "country": "US",
  "exp": 1659474457,
  "iat": 1659474397,
  "nbf": 1659474397
}

Fidelity notes: aud is an array (as in real Access tokens), iss has no trailing slash, type is "app", and the JWT header carries kid + RS256. The JWKS response serves the standard keys array; Cloudflare's extra public_cert/public_certs PEM fields are not replicated (standards-based validators, including jose, read only keys).

Flags

| Flag | Default | Purpose | | --- | --- | --- | | --email <addr> | [email protected] | email claim of minted tokens | | --audience <str> | generated once, persisted | aud claim; print-matched to CF_ACCESS_AUDIENCE | | --ttl <dur> | 6h | token lifetime (30m, 6h, 1d) — your choice, unlike a real session | | --mode <http\|impersonate> | http | see integration shapes above | | --team <name> | local-mock | fake team name (impersonate mode) | | --port <n> | 8788 | plain-http listener | | --redirect <url> | http://localhost:3000 | default /login redirect target | | --rotate-keys | — | replace the persisted signing keypair |

State

Everything persists in ~/.cf-access-mock/: the RS256 signing key (so tokens survive restarts and consumers' JWKS caches stay valid), the generated audience, and the impersonation CA. Delete the directory or run --rotate-keys to start fresh.

Security

This tool authenticates nobody — it mints a valid-looking token for any email it is asked for. That is the point, locally.

  • Never expose it beyond localhost.
  • Never let a deployed environment resolve or trust it: keep NODE_EXTRA_CA_CERTS and the fake team name out of production config. (A deployed app pointed at a fake team fails closed anyway — the real *.cloudflareaccess.com resolves to Cloudflare, which won't serve your mock's keys.)
  • Treat ~/.cf-access-mock/ as dev-machine material; the keys sign identity tokens your local apps trust.

This project is not affiliated with or endorsed by Cloudflare. "Cloudflare" is a trademark of Cloudflare, Inc., used here only to describe compatibility.

License

MIT