Cloudflare Worker that proxies Chrome Web Store requests for the CWS Tracker extension.
Chrome blocks extensions from accessing CWS domains directly (CORS, content script injection, scripting API all restricted). This proxy fetches CWS pages server-side and returns content to the extension.
Deploy this proxy to your own free Cloudflare account (100,000 requests/day — far more than the extension needs). No CLI required.
-
Click the button. Cloudflare asks you to sign in (or create a free account) and to connect GitHub or GitLab and grant repository access — it clones this repo into your account so future pushes auto-redeploy.
-
Keep every build setting at its default and continue. For reference, here is what each field means:
Field Default — keep it Why Environment productionJust a label the Worker reads. Build command (empty) No build step — Wrangler bundles on deploy. Deploy command npm run deployRuns wrangler deploy.Non-production branch deploy command npx wrangler versions uploadOnly used for other branches — ignore it. Path /The repo root is the Worker. API token (pre-filled) Cloudflare auto-creates this token scoped to your own account to run the deploy. It's yours, not the repo author's — keep it. -
Set your API key. When prompted for the
API_KEYSsecret, enter a long random string you invent (e.g. runopenssl rand -hex 32). This is a shared password between your proxy and your extension — save it. -
Wait for the deploy to finish, then copy your Worker URL — it looks like
https://cws-tracker-proxy.<your-subdomain>.workers.dev. -
Configure the extension → open Settings and paste:
- Proxy URL = your Worker URL
- Proxy API Key = the same
API_KEYSvalue from step 3
Run a scan — rankings should come back through your own proxy. Prefer the CLI? See Manual deployment below.
- A Cloudflare account (free plan is sufficient)
- Node.js 18+ installed locally
- npm installed
Prefer the CLI over the one-click button above? Follow these steps.
- Go to https://dash.cloudflare.com/sign-up
- Sign up with email and password
- No domain or credit card is required for Workers free tier
Free tier limits:
- 100,000 requests per day
- 10ms CPU time per request
- Unlimited Workers
This is more than enough for the CWS Tracker extension.
cd proxy
npm installThis installs:
- wrangler - Cloudflare's CLI tool for developing, testing, and deploying Workers
- @cloudflare/workers-types - TypeScript types for the Workers runtime
- vitest + @cloudflare/vitest-pool-workers - Testing framework
npx wrangler loginThis opens your browser and asks you to authorize Wrangler to access your Cloudflare account. Click Allow.
After authorization, Wrangler stores a token locally at ~/.wrangler/config/default.toml. You won't need to login again on this machine.
To verify it worked:
npx wrangler whoamiThis should print your Cloudflare account name and ID.
The proxy requires an API key to prevent unauthorized use. You create this key yourself - it's not provided by Cloudflare. It's a shared secret between your extension and your proxy.
Generate a random key:
# Option A: Using openssl
openssl rand -hex 32
# Option B: Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Option C: Using Python
python3 -c "import secrets; print(secrets.token_hex(32))"This produces something like:
a3f8b2c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1
Save this key. You'll need it in two places:
- As a secret in Cloudflare Workers (Step 5)
- In the extension's settings as the proxy API key (later)
Secrets are encrypted environment variables that your Worker can read but that never appear in your code or dashboard in plain text.
npx wrangler secret put API_KEYSWhen prompted, paste your API key and press Enter. If you want multiple keys (e.g., one for development, one for production), separate them with commas:
a3f8b2c4d5e6f7...key1,b4c9d0e1f2a3...key2
npm run deployWrangler will:
- Bundle your TypeScript code
- Upload it to Cloudflare's edge network
- Print the URL of your deployed Worker
Output looks like:
Published cws-tracker-proxy (X.XX sec)
https://cws-tracker-proxy.<your-subdomain>.workers.dev
Save this URL. This is your proxy endpoint.
Test the health endpoint (no auth needed):
curl https://cws-tracker-proxy.<your-subdomain>.workers.dev/healthExpected response:
{"status":"ok","timestamp":"2026-02-05T...","version":"0.1.0"}Test fetching an extension (auth required):
curl -H "X-API-Key: YOUR_API_KEY_HERE" \
"https://cws-tracker-proxy.<your-subdomain>.workers.dev/detail?id=cjpalhdlnbpafiamejdnhcphjbkeiagm"Expected: JSON response with html field containing the CWS page HTML.
npm run devStarts a local dev server at http://localhost:8787. For local dev, copy .dev.vars.example to .dev.vars and set your key there — .dev.vars is gitignored, so it never gets committed:
# .dev.vars
API_KEYS=dev-test-key
Wrangler loads .dev.vars automatically in npm run dev. For production the key is a Worker secret (npx wrangler secret put API_KEYS) or is entered during the one-click deploy — never put it in wrangler.toml.
npm test24 tests covering auth, CORS, validation, routing, and error handling.
# Health check
curl http://localhost:8787/health
# Fetch extension detail
curl -H "X-API-Key: dev-test-key" \
"http://localhost:8787/detail?id=cjpalhdlnbpafiamejdnhcphjbkeiagm"
# Fetch search results
curl -H "X-API-Key: dev-test-key" \
"http://localhost:8787/search?q=ad+blocker"
# Fetch with locale
curl -H "X-API-Key: dev-test-key" \
"http://localhost:8787/detail?id=cjpalhdlnbpafiamejdnhcphjbkeiagm&hl=ja"You don't need a Cloudflare account to run this proxy. The Worker is built almost entirely on web-standard APIs (fetch, Request/Response, URL, AbortController); the only Cloudflare-specific dependency is the Cache API (caches.default), and caching is a best-effort optimization, not required for correctness.
Three ways to run it off Cloudflare:
| Option | Code changes | Caching | Best for | Production-grade |
|---|---|---|---|---|
A. workerd |
none | off (needs cache stub) | faithful self-host, same runtime | ✅ |
B. wrangler dev |
none | in-memory (works) | quick personal use / testing | |
| C. Node / Deno | one entry file | in-memory polyfill | a normal server process you operate | ✅ |
Two things every self-host needs that Cloudflare gives you for free:
- HTTPS. The extension calls the proxy over
https://. Run a reverse proxy in front of the HTTP port — Caddy is easiest (automatic Let's Encrypt TLS). Plainhttp://works forlocalhosttesting only.- A process manager (systemd, pm2, Docker) so it restarts on crash/reboot.
One upside of single-process self-hosting: the in-memory rate limiter and cache actually persist across requests, instead of resetting per-isolate like on Cloudflare's edge.
workerd is the same runtime that powers Cloudflare Workers, released as a standalone open-source binary (Apache-2.0). It runs the exact same src/index.ts — no source changes.
# 1. Bundle the TypeScript Worker to a single JS module
npx wrangler deploy --dry-run --outdir dist # -> dist/index.js
# 2. Install the runtime
npm install -D workerdThe Worker uses the Cache API (caches.default), and standalone workerd throws No Cache was configured unless you give it a cache backend — so you must wire one up or every authed request 500s. The simplest backend is a tiny "null cache": reads always miss, writes are discarded (caching is effectively off, but the Worker runs correctly). Create nullcache.js:
// Minimal Cache API backend: every read misses, every write is discarded.
export default { async fetch() { return new Response(null, { status: 504 }); } };Create workerd.capnp (keep it in the same directory as nullcache.js and dist/ — embed paths resolve relative to the .capnp file):
using Workerd = import "/workerd/workerd.capnp";
const config :Workerd.Config = (
services = [
(name = "main", worker = .proxy),
(name = "nullcache", worker = .nullcache),
],
sockets = [ (name = "http", address = "*:8787", http = (), service = "main") ],
);
const proxy :Workerd.Worker = (
modules = [ (name = "index.js", esModule = embed "dist/index.js") ],
bindings = [
(name = "API_KEYS", fromEnvironment = "API_KEYS"),
(name = "ENVIRONMENT", text = "production"),
],
cacheApiOutbound = "nullcache", # required, else caches.default throws
compatibilityDate = "2024-12-30",
compatibilityFlags = ["nodejs_compat"],
);
const nullcache :Workerd.Worker = (
modules = [ (name = "nullcache.js", esModule = embed "nullcache.js") ],
compatibilityDate = "2024-12-30",
);Run it:
API_KEYS="$(openssl rand -hex 32)" npx workerd serve workerd.capnpOutbound fetch to Chrome Web Store works out of the box. Want real caching instead of the stub? Point cacheApiOutbound at a proper cache backend — see the workerd config samples.
The same dev server you use locally can listen on all interfaces. Zero code changes, and caching works (Miniflare implements the Cache API in memory).
# Put your key in .dev.vars (gitignored): API_KEYS=your-long-random-secret
npx wrangler dev --ip 0.0.0.0 --port 8787Convenient for personal use, but wrangler dev is a development server — no graceful restarts, not hardened for hostile traffic. Keep it behind Caddy/nginx + a firewall, or prefer Option A/C for anything serious.
For a conventional server you already operate. The Worker's handler is already (request, env, ctx) => Response, so you only add a thin entry file that supplies env from process.env, a ctx shim, and a tiny caches.default polyfill (the one API Node lacks).
npm install @hono/node-server tsx # web-standard HTTP server + TypeScript runner (Node 18+)Create server.node.ts:
import { serve } from '@hono/node-server';
import worker from './src/index';
// Workers' `caches.default` isn't in Node — minimal in-memory polyfill.
// (Ignores TTL; swap for an LRU/Redis if you want real expiry.)
const store = new Map<string, Response>();
(globalThis as any).caches = {
default: {
async match(req: Request | string) {
const hit = store.get(typeof req === 'string' ? req : req.url);
return hit ? hit.clone() : undefined;
},
async put(req: Request | string, res: Response) {
store.set(typeof req === 'string' ? req : req.url, res.clone());
},
},
};
const env = {
API_KEYS: process.env.API_KEYS ?? '',
ENVIRONMENT: process.env.ENVIRONMENT ?? 'production',
};
const ctx = {
waitUntil: (p: Promise<unknown>) => void Promise.resolve(p).catch(() => {}),
passThroughOnException() {},
};
const port = Number(process.env.PORT ?? 8787);
serve({ fetch: (request) => worker.fetch(request, env as any, ctx as any), port });
console.log(`CWS proxy listening on http://localhost:${port}`);Run it (no build step — tsx runs TypeScript directly):
API_KEYS="$(openssl rand -hex 32)" npx tsx server.node.tsDeno works the same way: keep the same polyfill and env/ctx shims, then Deno.serve(req => worker.fetch(req, env, ctx)) and run with deno run --allow-net --allow-env.
Everything else — CWS fetching, batchexecute pagination, the in-memory rate limiter, CORS, auth, input validation — is runtime-agnostic and runs unchanged.
Whichever option you pick, point the extension's Proxy URL at your server's public HTTPS address and set Proxy API Key to your API_KEYS value, exactly as in the Cloudflare setup.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /detail?id={extensionId}&hl={locale} |
Required | Fetch extension detail page |
| GET | /search?q={query}&hl={locale} |
Required | Fetch search results page |
| GET | /autocomplete?q={query}&hl={locale} |
Required | Fetch search autocomplete suggestions |
| GET | /reviews?id={extensionId}&limit={n}&hl={locale} |
Required | Fetch up to limit newest reviews (1–500, default 50) |
| GET | /health |
None | Health check |
Pass API key via either method:
# Header (recommended for extension)
X-API-Key: your-api-key
# Query parameter (useful for quick testing)
/detail?id=...&key=your-api-key
Successful response (200):
{
"url": "https://chromewebstore.google.com/detail/cjpalhdlnbpafiamejdnhcphjbkeiagm?hl=en",
"status": 200,
"html": "<!doctype html>...",
"htmlLength": 650000,
"fetchedAt": "2026-02-05T03:00:00.000Z"
}Error response:
{
"error": "Description of what went wrong"
}/detail endpoint:
| Parameter | Required | Default | Description |
|---|---|---|---|
id |
Yes | - | Extension ID (32 lowercase letters) |
hl |
No | en |
Locale code (e.g., ja, es, zh-CN) |
/search endpoint:
| Parameter | Required | Default | Description |
|---|---|---|---|
q |
Yes | - | Search query (max 200 chars) |
hl |
No | en |
Locale code |
| Status | Meaning | Example |
|---|---|---|
| 400 | Bad request | Missing id, invalid extension ID format, query too long |
| 401 | No API key | Missing X-API-Key header and key param |
| 403 | Bad API key | Key not in the configured API_KEYS list |
| 404 | Bad route | Called an endpoint that doesn't exist |
| 405 | Bad method | Used POST/PUT/etc instead of GET |
| 429 | Rate limited | Exceeded 30 requests/minute for this API key |
| 502 | CWS error | Proxy couldn't fetch from Chrome Web Store |
| 504 | Timeout | CWS didn't respond within 15 seconds |
Extension Cloudflare Worker Chrome Web Store
| | |
| GET /detail?id=xxx&key=yyy | |
|----------------------------->| |
| | GET /detail/xxx?hl=en |
| |----------------------------->|
| | |
| | 200 OK (HTML page) |
| |<-----------------------------|
| | |
| 200 OK { html: "..." } | |
|<-----------------------------| |
- Extension sends request to proxy with API key
- Proxy validates API key and checks rate limits
- Proxy fetches the CWS page server-side (no CORS issues)
- Proxy wraps the HTML in JSON and returns it with CORS headers
- Extension parses the HTML to extract extension data
- API key auth: Prevents unauthorized use of your proxy
- CORS restriction: Only
chrome-extension://origins andlocalhostcan call the proxy - Rate limiting: 30 req/min per API key (in-memory sliding window)
- Caching: 5-minute TTL reduces load on CWS and speeds up responses
- Input validation: Extension IDs must be exactly 32 lowercase letters
Successful responses are cached for 5 minutes using Cloudflare's Cache API. The X-Cache header indicates cache status:
X-Cache: HIT- Served from cacheX-Cache: MISS- Fresh fetch from CWS
After deployment, you can manage the Worker at:
https://dash.cloudflare.com > Workers & Pages > cws-tracker-proxy
From the dashboard you can:
- View request analytics (requests/day, errors, latency)
- View real-time logs (Logs tab > Begin log stream)
- Update environment variables and secrets (Settings > Variables)
- Set up custom domains (Settings > Triggers > Custom Domains)
- Configure additional rate limiting rules (Security > WAF)
By default your Worker is at cws-tracker-proxy.<subdomain>.workers.dev. To use a custom domain:
- Add a domain to Cloudflare (free plan works)
- Go to Worker > Settings > Triggers > Custom Domains
- Add your domain (e.g.,
api.cws-tracker.com)
After making code changes:
npm test # Verify tests pass
npm run deploy # Deploy updated codeThe update is instant and zero-downtime.
Free tier covers all expected usage:
| Limit | Free Tier | Expected Usage |
|---|---|---|
| Requests/day | 100,000 | ~100-500 (10 extensions x 20 keywords x daily scan + retries) |
| CPU time/request | 10ms | ~2-5ms (simple fetch relay) |
| Workers | Unlimited | 1 |
You would need the paid plan ($5/month) only if you exceed 100K requests/day, which would require ~200 active users running full scans simultaneously.