Cloudflare Worker that receives feedback POSTs from HDRezkaSpeeds / VideoSpeeds browser extensions and forwards them to a Telegram bot (developer’s personal inbox).
@BotFather./newbot, follow the prompts. Pick a name like
Speeds Feedback Bot and a username like speeds_feedback_bot.:)./start.https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
(replace <YOUR_TOKEN>)."chat":{"id":<your-id>}. Copy the
numeric id (positive for personal chats, negative for groups).cd cloudflare-worker
npm install
npx wrangler login # opens browser, authorise Cloudflare
npx wrangler kv namespace create RATE_LIMIT
It prints an id, e.g.
🌀 Creating namespace with title "speeds-feedback-RATE_LIMIT"
✨ Success!
Add the following to your wrangler.toml:
[[kv_namespaces]]
binding = "RATE_LIMIT"
id = "abcd1234ef5678"
Open wrangler.toml, replace REPLACE_WITH_KV_ID with that id.
npx wrangler secret put TELEGRAM_BOT_TOKEN
# paste the token, press Enter
npx wrangler secret put TELEGRAM_CHAT_ID
# paste the chat id, press Enter
# Random 32-byte hex key. The Worker HMACs each incoming IP with this
# secret before using it as a KV rate-limit key, so the rate-limit
# table can never be reverse-mapped to a real IP.
openssl rand -hex 32 | npx wrangler secret put IP_HASH_SECRET
npm run deploy
Output ends with the public URL, e.g.
https://speeds-feedback.<your-subdomain>.workers.dev
Save this URL — both extensions will POST to <URL>/feedback.
The endpoint requires an Origin header from one of the two extension
schemes (chrome-extension://* or moz-extension://*). Real browser
fetches send this automatically; for curl you have to spoof it:
curl -X POST https://speeds-feedback.<sub>.workers.dev/feedback \
-H "Content-Type: application/json" \
-H "Origin: chrome-extension://smoke-test" \
-d '{"app":"hdrezka","message":"hello from curl","rating":"positive"}'
Without -H "Origin: ..." the Worker returns 403 forbidden_origin
(intended — that’s the abuse gate).
You should:
{"ok":true} from curlPOST /feedbackBody (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
app |
"hdrezka" | "videospeeds" |
yes | Which extension is sending |
version |
string | no | Extension version, e.g. "0.2.0" |
rating |
"positive" | "neutral" | "negative" |
no | User’s mood |
message |
string | yes | The feedback text (max 4 KB) |
email |
string | no | Reply-to address (validated) |
diagnostics |
string | no | JSON-stringified diagnostic report (max 16 KB) |
userAgent |
string | no | UA string for context |
url |
string | no | Page URL (origin only — content scripts strip the rest) |
Required headers:
Content-Type: application/jsonOrigin: chrome-extension://... or Origin: moz-extension://.... Any
other origin (or missing header) gets 403 forbidden_origin. Browsers
set this automatically from extension HTML pages and MV3 content-script
fetches; non-browser tooling has to spoof it.Response:
200 {"ok": true} on success400 {"error": "validation_failed", "fields": [...]} on bad input403 {"error": "forbidden_origin"} from a non-extension origin429 {"error": "rate_limited", "retry_after_minutes": 60} after 5/hour per IP502 {"error": "send_failed"} if Telegram returns 4xx/5xx404 for any other path / methodGET /healthReturns plain ok for liveness probes.
npm run dev # local server on http://localhost:8787
npm run typecheck # tsc --noEmit
npm run logs # tail production logs (wrangler tail)
Local dev needs the same secrets — put them in .dev.vars (gitignored):
TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...
IP_HASH_SECRET=...
npx wrangler secret put TELEGRAM_BOT_TOKEN and
re-deploy. KV state survives.npm run logs. The Worker logs Telegram
errors with HTTP status + body so you can debug delivery problems.RATE_LIMIT_PER_HOUR in src/index.ts
and re-deploy. Existing limits in KV expire on their own.rl:<HMAC-SHA256 hex> rather than
rl:<dotted-quad> since the IP-hashing change. To pre-seed a block
for a known IP without writing the secret to disk, run a one-shot
Worker that computes the hash for you, or temporarily lower
RATE_LIMIT_PER_HOUR and let the natural counter trip.IP_HASH_SECRET: npx wrangler secret put IP_HASH_SECRET
with a fresh random value and re-deploy. The existing
rl:<old-hash> entries in KV will expire on their own (1-hour TTL),
so the rate-limit window resets at most one hour after rotation.GPL-3.0-or-later — same as the extensions it serves.