Add a cache purge page and API with proof-of-work - #1405
Conversation
Add a /purge page (like jsDelivr's purge tool) plus a POST /purge API that drops every cached artifact of a package/version - built modules, type declarations, build metadata, the local npm store and the npm resolution caches - so the next request rebuilds it from scratch. For non-exact specifiers (bare names / dist-tags / semver ranges) the cached latest resolution is invalidated first, so a stale resolution cannot survive the purge. Every purge must solve a hashcash-style SHA-256 proof-of-work challenge (GET /purge/challenge, single-use, short TTL) so mass purge-and-rebuild attacks are not free, and is rate-limited per client IP. The page is fully self-contained and solves the challenge in the browser with Web Crypto; the README documents a curl flow for scripts.
9000e3d to
a209d0a
Compare
- Drop the redundant dist-tag refresh: purgePackageCache already clears the \latest\ resolution entry, so the pre-parse refresh duplicated it - Drop ExpiresAt from the challenge response (no client reads it) and the per-challenge difficulty (a package-level constant) - crypto/rand.Read never fails since Go 1.24: drop the error handling - Garbage collect pending challenges only once the store runs full - Drop the empty id/nonce guard in powVerify (a map miss covers it) and use slices.DeleteFunc in the rate limiter - Initialize the purge response slices so empty results serialize as [] instead of null (the page then drops its defensive fallbacks) - Fix the purgeCache config: a JSON \alse\ was silently re-enabled when the PURGE_CACHE env var was unset; mirror the Compress mode - Purge page: inline single-use variables, share the monospace font stack via a CSS variable, clearer element names
\parseEsmPath\ already reports whether the request pinned an exact version; pass it through so a fixed-version purge leaves the unrelated \pkg@latest\ resolution cache alone (it would only cause an extra registry query on the next bare-name request). A purge of a bare name, dist-tag or semver range still drops the dist-tag entry so the target gets re-resolved.
|
a, i think we need an github oauth for the purge api |
- Add an optional GitHub OAuth gate on POST /purge (githubClientId / githubClientSecret or GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET). The proof-of-work challenge is still required on top of the login, so the two form a double gate; the per-client rate limit is keyed by GitHub account once signed in. - Add /purge/login, /purge/callback, /purge/logout and /purge/auth.json, with a signed (HttpOnly, SameSite=Lax) session cookie; the page shows a Sign in with GitHub button and solves the challenge as before. - Add optional Cloudflare edge-cache purging (cloudflareZoneId / cloudflareApiToken or CLOUDFLARE_ZONE_ID / CLOUDFLARE_API_TOKEN): after a purge the public URLs derived from the removed storage keys are evicted via the purge_cache API in batches of 30. - Docs in the README; tests for the session, the OAuth flow and the Cloudflare batching.
|
Implemented both in the latest commit ( a) GitHub OAuth
One design question worth your call: this authenticates who someone is but doesn't authorize what they may purge — any GitHub account can still purge anything (now attributable/bannable). Restricting to the package owner isn't generally possible: npm ownership can't be verified through GitHub OAuth, and for b) Cloudflare
Tests: Happy to adjust any of these choices — any-account vs owner-only, exact-URL vs purge-everything, or anything else — or feel free to push to the branch directly. |
|
nice! can you add these env vars in the config as well? |
|
Done in |
|
Addressed both points from the review in Generic proof-of-work endpoint. The PoW machinery moved out of
Shared page assets. The existing
Tests: |
- drop the cached resolution by package prefix instead of a single guessed version, so ranges, dist-tags, date specifiers and git refs are all re-resolved after a purge - parse the purge input with net/url instead of hand-rolled string surgery - reuse the shared TTL cache for the purge rate limit, so idle clients are reclaimed - drop the unused /purge/challenge alias and dedupe the GitHub OAuth requests
The * external-all variant is stored under a normalized .../ea/ segment that no longer nests under the plain package id for scoped/gh/pr packages, so its files and metadata survived a purge. Delete both namespaces and recover the un-normalized build path (the meta key) from the storage key.
A bare name, dist-tag, range, date or git ref only asks to re-check which version is current, so a purge now stops at invalidating the resolution cache and keeps the existing build; the next request rebuilds it only if the resolved version/commit actually moved. Purging the build outputs, types, npm store and CDN entries stays reserved for an explicit exact version.
ije
left a comment
There was a problem hiding this comment.
please remove the shared js and css, then i will merge! thanks for your great work!
react pulls a large dependency graph, so purging it triggers an expensive rebuild for what is only a demo. The remaining examples stay lightweight.
Fixes #1222 — adds a cache purge page and API, the esm.sh equivalent of jsDelivr's purge tool.
What it does
GET /purge— a landing page where you enter an esm.sh URL or a bare specifier (pkg,pkg@version,@scope/pkg,gh/user/repo@ref).POST /purge— drops every cached artifact of the resolved package/version: built modules & source maps, type declarations, build metadata (and its in-memory copy), the local npm store copy and the npm resolution/404 caches. The next request rebuilds it from scratch. The JSON response lists what was purged plus a URL to trigger the rebuild.latestresolution is invalidated first, so a stalelatestcannot survive the purge — this complements the existinginvalidateDistTagCacheIfNewer(Invalidate stale latest resolution when a newer explicit version is requested #1398).GET /purge/challenge(single-use, 2 min TTL, difficulty 4). Mass purge-and-rebuild attacks are not free; the page solves it automatically in the browser with Web Crypto, and the README shows a curl one-liner for scripts. A per-IP rate limit (5/min) is stacked on top.purgeCache(envPURGE_CACHE, defaulttrue) can disable the whole feature for self-hosted deployments.The page is fully self-contained — no third-party code is loaded. The example chips only fill the input with specifiers like
@steve02081504/async-evaland@steve02081504/virtual-consoleto try.Tests
TestParsePurgeInput,TestPurgeRefreshDistTag,TestPowChallenge,TestPurgePackageCache(plain + scoped). Verified locally on Windows (go test ./server/, all green) and cross-compiled for Linux.