Skip to content

FenkoHQ/foxymirror

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

foxymirror

A quarantining proxy for the npm and PyPI registries. It serves everything upstream serves, except releases younger than 7 days. The idea: if someone slips a malicious version into a popular package, you don't install it on day one — you install it on day eight, after the rest of the world has had a chance to find and yank it.

Runs as a single Cloudflare Worker. Upstream integrity hashes (dist.integrity, dist.shasum, PyPI hashes.sha256) are passed through unchanged, so your package manager still verifies what it downloaded against the original publisher's hash.

This repo is the source you run on your own Cloudflare account. There is no shared public instance — the whole point is to control the proxy your installs depend on.

What it does

  • npm: filters versions and time, rewrites dist-tags.latest to the newest version old enough to survive the quarantine window. Tarball URLs are rewritten to flow back through the worker so a client can't accidentally bypass the filter.
  • PyPI: speaks PEP 691 + PEP 700 simple-index JSON to read upload-time. Also serves the HTML simple index and the legacy /pypi/<pkg>/json endpoint for tools that still want them.
  • Files: streamed straight through. Every file request re-checks the quarantine against the upstream index, so guessing a URL for a quarantined version returns HTTP 451.

Deploying to your Cloudflare account

The whole thing is one Worker. Cloudflare's free tier covers a solo developer or small team comfortably: 100k requests/day, 10ms CPU per request, no egress charges. Larger CI fleets, or filtering massive metadata documents like @types/node, want the $5/month paid plan for the 30s CPU ceiling.

git clone https://github.com/FenkoHQ/foxymirror.git
cd foxymirror
npm install
npx wrangler login
npx wrangler deploy

Wrangler prints the workers.dev URL it deployed to. That URL is yours; everything that follows assumes you'll substitute it in.

Putting it on your own domain

In wrangler.toml:

routes = [
  { pattern = "mirror.example.com/*", zone_name = "example.com" }
]

Then npx wrangler deploy again.

Configurable settings

In wrangler.toml under [vars]:

Variable Default What it does
QUARANTINE_DAYS 7 How old a release must be before it's served
UPSTREAM_NPM https://registry.npmjs.org npm upstream
UPSTREAM_PYPI_SIMPLE https://pypi.org/simple PyPI simple index upstream
UPSTREAM_PYPI_JSON https://pypi.org/pypi PyPI legacy JSON upstream
UPSTREAM_PYPI_FILES https://files.pythonhosted.org PyPI file storage

Locking it down

Workers are public by default. If you don't want random people on the internet using your quarantine compute, gate the worker — Cloudflare Access on the route, an Authorization header check in src/index.ts, or an IP allowlist. The hashes are still upstream's, so an outsider can't poison anything by hitting your mirror; the concern is bill and rate-limit.

Pointing clients at your mirror

Substitute https://your-worker.workers.dev (or your custom domain) below.

npm / pnpm / Yarn classic

One install:

npm install --registry https://your-worker.workers.dev/npm/ <pkg>

Persistent for this user:

npm config set registry https://your-worker.workers.dev/npm/

Per-project — drop a .npmrc next to package.json:

registry=https://your-worker.workers.dev/npm/

Yarn Berry

yarn config set npmRegistryServer https://your-worker.workers.dev/npm/

pip

One install:

pip install --index-url https://your-worker.workers.dev/pypi/simple/ <pkg>

Persistent — ~/.config/pip/pip.conf:

[global]
index-url = https://your-worker.workers.dev/pypi/simple/

uv

export UV_INDEX_URL=https://your-worker.workers.dev/pypi/simple/

Or in pyproject.toml:

[[tool.uv.index]]
url = "https://your-worker.workers.dev/pypi/simple/"
default = true

Poetry

poetry source add --priority=default foxymirror https://your-worker.workers.dev/pypi/simple/

System-wide via /etc/environment

If you want every shell on the box to pick up the mirror without anyone configuring anything, drop the standard env vars into /etc/environment:

npm_config_registry=https://your-worker.workers.dev/npm/
YARN_NPM_REGISTRY_SERVER=https://your-worker.workers.dev/npm/
PIP_INDEX_URL=https://your-worker.workers.dev/pypi/simple/
UV_INDEX_URL=https://your-worker.workers.dev/pypi/simple/
UV_DEFAULT_INDEX=https://your-worker.workers.dev/pypi/simple/
PIPENV_PYPI_MIRROR=https://your-worker.workers.dev/pypi/simple/

This is the env-var matrix the major tools recognise:

Tool Env var Notes
npm npm_config_registry npm maps every npm_config_<key> to a config option
pnpm npm_config_registry reads npm's
Yarn classic npm_config_registry reads npm's
Yarn Berry YARN_NPM_REGISTRY_SERVER
pip PIP_INDEX_URL also PIP_EXTRA_INDEX_URL for fallbacks
uv UV_INDEX_URL, UV_DEFAULT_INDEX set both for forward/backward compat
pipenv PIPENV_PYPI_MIRROR
Poetry none clean needs poetry source add or poetry.toml

A few things to know before you commit to the /etc/environment approach:

  • It's not a shell file. PAM parses it as plain KEY=value lines. No export, no shell expansion, no $VAR references. The block above is fine as-is; just don't try to be clever inside it.
  • systemd services don't read it. Login sessions, sshd, su, and GUI sessions do (via PAM), so interactive shells and the tools they launch pick it up. Anything running as a systemd service — self-hosted CI runners, the Docker daemon, sidecar agents — does not. For those, either add EnvironmentFile=/etc/environment to the unit (under /etc/systemd/system/<service>.d/foxymirror.conf) and systemctl daemon-reload && systemctl restart <service>, or set the vars in /etc/systemd/system.conf under DefaultEnvironment= for system-wide coverage.
  • Containers don't inherit it. Docker/Podman containers start with their own env. Either --env-file /etc/environment at run time, bake the vars into the image, or set them in your compose.yaml.
  • Per-install escape hatch. If you ever need to go straight to upstream for one command without un-setting anything globally:
    env npm_config_registry=https://registry.npmjs.org/ npm install <pkg>
    env PIP_INDEX_URL=https://pypi.org/simple/ pip install <pkg>
    

Verifying your deployment

Substitute your worker URL for https://your-worker.workers.dev below. Each test takes a few seconds and proves a different invariant.

1. The mirror is reachable and filtering something. Any active package will show non-zero drops:

curl -sI https://your-worker.workers.dev/npm/react | grep -i x-foxymirror
# x-foxymirror-dropped: <some number>
# x-foxymirror-kept:    <larger number>

2. The mirror serves an older latest than upstream when upstream's latest was published in the last 7 days. Without installing anything:

# pip
pip index versions --index-url https://your-worker.workers.dev/pypi/simple/ anthropic | head -2
pip index versions --index-url https://pypi.org/simple/ anthropic | head -2

# npm
npm view --registry https://your-worker.workers.dev/npm/ playwright version
npm view --registry https://registry.npmjs.org/ playwright version

If upstream's headline release is older than a week, both lines match — that's correct too.

3. File bytes are untouched. Pull the same tarball through the mirror and direct, compare hashes:

curl -s -o /tmp/m.tgz https://your-worker.workers.dev/npm/react/-/react-19.2.5.tgz
curl -s -o /tmp/u.tgz https://registry.npmjs.org/react/-/react-19.2.5.tgz
sha256sum /tmp/m.tgz /tmp/u.tgz

Both checksums must be identical. If they aren't, something is wrong — bail and investigate before installing anything.

4. Quarantined files refuse direct access. Pick a release upstream published in the last few days and hit its tarball URL on your mirror directly:

curl -sI https://your-worker.workers.dev/npm/react/-/react-<recent-version>.tgz | head -1
# HTTP/2 451

Same idea for PyPI:

curl -sI https://your-worker.workers.dev/pypi/files/anthropic/anthropic-<recent-version>-py3-none-any.whl | head -1
# HTTP/2 451

5. A pinned-but-quarantined version fails the resolver. Inside a throwaway directory or venv:

# npm — pin a version younger than the window
mkdir -p /tmp/foxy-npm && cd /tmp/foxy-npm && npm init -y >/dev/null
npm install --registry https://your-worker.workers.dev/npm/ react@<recent-version>
# → npm error notarget No matching version found

# pip — same test
python -m venv /tmp/foxy-pip && /tmp/foxy-pip/bin/pip install \
  --index-url https://your-worker.workers.dev/pypi/simple/ "anthropic==<recent-version>"
# → Could not find a version that satisfies the requirement

The pinned-version test is the most important one — it's the actual scenario you're defending against, where a malicious release sits at the top of upstream and you would otherwise pin to it.

What you'll see when quarantine kicks in

A version inside the 7-day window simply doesn't appear in the metadata. The install will either pick an older version that satisfies the range, or fail with "no matching version found". If you genuinely need a release that fresh, fall back to upstream for that one install:

npm install --registry https://registry.npmjs.org/ <pkg>
pip install --index-url https://pypi.org/simple/ <pkg>

You can check the response headers to see how many versions were dropped:

curl -sI https://your-worker.workers.dev/npm/react | grep -i x-foxymirror

Local development

npm install
npx wrangler dev          # serves on http://127.0.0.1:8787
npm test                  # vitest
npx tsc --noEmit          # typecheck

What this doesn't protect you against

  • A package that was already malicious 7 days ago. Quarantine buys detection time; it doesn't detect anything itself. Pair it with normal advisories.
  • Postinstall scripts. If you npm install a quarantined-but-clean version that runs an evil postinstall script three months later, this mirror doesn't help. Use --ignore-scripts or pnpm's safer defaults.
  • Typosquats. The mirror serves whatever you ask for; if you ask for the wrong package it'll happily quarantine that too and then hand it over.
  • Yanked-and-re-uploaded versions. Both registries permit this in some cases. If a poisoned version was yanked and replaced under the same number, the new copy still has to age 7 days before this mirror serves it.

License

MIT.

About

Quarantining npm + PyPI proxy on Cloudflare Workers. Hides releases under 7 days old to mitigate supply-chain attacks.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages