Add a URL rewrite setting for the downloads a per-tool mirror cannot name
Summary
tools.<name>.mirror (pnpm/pnpm#15035 follow-up) lets a workspace say where pnpm downloads Node.js, Bun and a Python interpreter from. It cannot cover everything pnpm fetches, and the gaps are not incidental:
| what pnpm fetches | from | can a mirror cover it? |
|---|---|---|
| Node.js | nodejs.org/download/<channel>/ |
yes |
| Bun | github.com/oven-sh/bun/releases/download/bun-v<v>/ |
yes |
| Python interpreter | github.com/astral-sh/python-build-standalone/releases |
yes |
| Deno | api.github.com/repos/denoland/deno/releases/tags/v<v> |
no |
| Yarn (zpm) | api.github.com/repos/yarnpkg/zpm/releases |
no |
| npm packages | registry / registries |
separate setting |
| PyPI distributions | python.indexUrl |
separate setting |
| crates | cargo.indexUrl |
separate setting |
| git dependencies | github.com, gitlab.com, bitbucket.org, codeload | nothing |
Deno and Yarn read the GitHub API for release metadata and take asset URLs out of the response. A base URL cannot stand in for that without also pinning the response shape. So the two runtimes most in need of a mirror are the two a mirror setting cannot reach, and a user behind a proxy still has four other settings to find.
Proposal
A URL rewrite map, applied to every URL pnpm is about to fetch:
urlRewrites:
'https://github.com/': 'https://gh-mirror.corp.example/'
'https://api.github.com/': 'https://gh-api-mirror.corp.example/'One rule then covers runtimes, interpreters, package managers, registries, indexes and git hosts at once, including the GitHub API calls no declarative mirror can express.
This is not speculative. Both tools that manage many runtimes converged on it, and both ship it alongside per-tool settings rather than instead of them:
- mise:
url_replacements, "map of URL patterns to replacement URLs… for download mirroring and custom registries", next tonode.mirror_urlandgo.download_mirror - proto:
settings.url-rewrites, a regex→replacement map covering tool download, checksum, plugin and archive URLs, next to[tools.*]config
The two are complementary. tools.<name>.mirror is declarative: pnpm knows the default, so the user names only the replacement. A rewrite is the escape hatch for every URL pnpm did not model.
Questions to settle
- Prefix or regex? proto and mise both use regex. A prefix match is far easier to reason about and to make safe, and covers the mirror case. Regex buys path rewriting.
- Which URLs are in scope? proto excludes git repository URLs and its own telemetry. pnpm would need to decide about git remotes, the pnpr server, audit/advisory endpoints, and self-update.
- How does it interact with authentication? Rewriting a URL moves it to a different origin, and pnpm resolves credentials by origin.
python_indexalready refuses to let repository-selected indexes pick up user-level npm credentials (crates/python-installer/src/settings.rs). A rewrite must not become a way to redirect a request to a host that then receives a token meant for another. This is the part that needs the most care. - Where can it be set? A repository-controlled
pnpm-workspace.yamlredirecting downloads is a different trust proposition from a user-level config doing so.
Question 3 is the reason this is filed rather than built: it is a security design question, not a configuration one.
Out of scope
Anything a tools.<name>.mirror already covers. This is for the URLs that setting cannot name.
Written by an agent (Claude Code, claude-opus-5).
Source: pnpm/pnpm