Security model
LinkBrain is single-user. Independent gates in the Worker’s fetch() entry
ensure only you can store or read private links.
Gate 1 — webhook secret (blocks the open internet)
Section titled “Gate 1 — webhook secret (blocks the open internet)”Every POST must carry an X-Telegram-Bot-Api-Secret-Token header equal to
your WEBHOOK_SECRET (a 128-bit random value). A request with a wrong or
missing secret is rejected with 403 before anything else runs. Only Telegram
sends that header, because you registered it via setWebhook. Forging a request
means guessing a 16-byte random secret — infeasible.
Gate 2 — chat-id allowlist (blocks other Telegram users)
Section titled “Gate 2 — chat-id allowlist (blocks other Telegram users)”Even a request that has the correct secret is ignored unless the message’s
chat.id matches your ALLOWED_CHAT_ID. Anything else is silently dropped — no
reply, nothing stored. So only your chat can actually use the bot.
Gate 3 — extension token (blocks browser-capture strangers)
Section titled “Gate 3 — extension token (blocks browser-capture strangers)”The Chrome extension does not use the Telegram webhook secret. It calls
POST /capture/api/link with Authorization: Bearer <EXTENSION_TOKEN>, and the
Worker rejects missing or wrong tokens with 401. This token is save-only: it
can submit a URL and note to the shared enrichment path, but it does not grant
dashboard access or expose private library reads.
What this does and doesn’t stop
Section titled “What this does and doesn’t stop”- ✅ A stranger cannot POST directly to the Worker (Gate 1).
- ✅ Another Telegram user cannot save or read links, and gets no response at all (Gate 2) — the bot appears dead to them.
- ✅ A browser without
EXTENSION_TOKENcannot use the extension capture API (Gate 3). - ⚠️ The bot is still messageable. Anyone who knows its
@usernamecan send it a message, and Telegram forwards it to the Worker; Gate 2 just makes it a no-op. You can’t make a Telegram bot un-messageable — the defense is to ignore outsiders, which is exactly what happens. Keeping the username private is the practical extra step.
Keeping it secure
Section titled “Keeping it secure”WEBHOOK_SECRETlives only as a Cloudflare secret and was passed once tosetWebhook— it is not in the repository. Don’t share it.- If it ever leaks, rotate:
wrangler secret put WEBHOOK_SECRETwith a new value, then callsetWebhookagain with the matchingsecret_token. EXTENSION_TOKENis a separate Cloudflare secret. If it leaks, rotate it withwrangler secret put EXTENSION_TOKEN, then update the value in the extension popup settings.- The Worker also serves these public docs on
GETrequests at every path except/webhook. That’s intentional and contains no secrets. The webhook itself —POST /webhook— stays behind both gates;run_worker_firstroutes that path to the Worker so a static asset can never shadow it. run_worker_firstalso routes/capture/api/linkto the Worker so the extension endpoint cannot be shadowed by a static asset.