Skip to content

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.

  • ✅ 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_TOKEN cannot use the extension capture API (Gate 3).
  • ⚠️ The bot is still messageable. Anyone who knows its @username can 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.
  • WEBHOOK_SECRET lives only as a Cloudflare secret and was passed once to setWebhook — it is not in the repository. Don’t share it.
  • If it ever leaks, rotate: wrangler secret put WEBHOOK_SECRET with a new value, then call setWebhook again with the matching secret_token.
  • EXTENSION_TOKEN is a separate Cloudflare secret. If it leaks, rotate it with wrangler secret put EXTENSION_TOKEN, then update the value in the extension popup settings.
  • The Worker also serves these public docs on GET requests at every path except /webhook. That’s intentional and contains no secrets. The webhook itself — POST /webhook — stays behind both gates; run_worker_first routes that path to the Worker so a static asset can never shadow it.
  • run_worker_first also routes /capture/api/link to the Worker so the extension endpoint cannot be shadowed by a static asset.