Skip to content

Telegram is optional: a key you make up is the door - #31

Merged
thankywal merged 1 commit into
mainfrom
telegram-is-optional
Sep 3, 2026
Merged

thankywal merged 1 commit into
mainfrom
telegram-is-optional

Conversation

@thankywal

Copy link
Copy Markdown
Owner

A deployment refused to finish setting up without ADMIN_BOT_TOKEN and OWNER_TELEGRAM_ID, and the web console could only be signed into with a pairing code that the Telegram console bot was the only thing able to issue. Between them, somebody with no Telegram account could not use their own copy of this at all — the first wall on the path a judge or a new owner walks, and one nothing on the product needs.

There are two doors now, and either one alone is a finished deployment.

The key

CONSOLE_KEY is a string the owner makes up, at least sixteen characters, and POST /admin/claim trades it for the same thirty-day session the pairing code has always ended in. One place mints a token, so the two doors cannot drift in how long somebody stays signed in or in what is written down about them. The key is compared in constant time and never stored — only a hash of it, beside a hash of the token.

Length is the whole of the rule, and there is deliberately no attempt limit: sixteen characters is far past what can be guessed, so a limit would defend against an attack nobody can mount while adding a number for the owner to wonder about. The setup page offers a generated key rather than leaving the owner to invent one.

Changing the key takes it back

A session opened with a key remembers which key, and ends the moment that is no longer the key. Without it, "change CONSOLE_KEY" stopped the next sign-in and nothing else — every browser already holding a token kept working, which is exactly the case a leaked key is changed for. A session paired from Telegram carries no such mark: it was the console bot that vouched for that person, not a setting.

One box

The deploy form reads .dev.vars.example, and that file now lists CONSOLE_KEY and nothing else. A row of empty boxes at the first step reads as work to do before anything happens, and there is none. A console bot is added afterwards from the Worker's own settings, and the setup page says how.

Two things found on the way

  • WEB_OWNER_ID is 0 — a legal row in a table keyed by Telegram account id, and an account Telegram has never heard of. findOwner now excludes it, because both of its callers use what comes back as a chat id, and a deployment with both doors would otherwise have stopped alerting the moment the console row happened to be the older of the two.
  • A short key stops a deployment only when it is the only door. Adding a weak one to a working Telegram console is trying something, not breaking something; the page says it is set and not being used rather than taking the deployment down.

The rest follows the same fact

The five READMEs ask for three things and offer Telegram as the extra; scripts/deploy.mjs decides on the record /health publishes rather than by matching words in a rendered page (a keyword heuristic that was already wrong); scripts/smoke.mjs walks the door the form actually asks for, which nothing had ever exercised; the installer can set the key; and muxel init no longer makes --owner-telegram-id a required flag.

Verified against a real Worker, not only in tests

On an empty database with a console key and no Telegram at all: /healthawaiting_setup, the first screen → 200 "Your console is connected", /healthready, wrong key → 401, right key → a token, console API with that token → 200, without it → 401.

And live: sign in, change CONSOLE_KEY in the Worker's settings, restart — the same browser is refused, the new key works, the old one is refused.

904 tests; the new ones each checked by putting the bug back.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FMFyy3oXb9u2AxoYQKRXHu

A deployment refused to finish setting up without ADMIN_BOT_TOKEN and
OWNER_TELEGRAM_ID, and the web console could only be signed into with a
pairing code that the Telegram console bot was the only thing able to
issue. Between them, somebody with no Telegram account could not use
their own copy of this at all — the first wall on the path a judge or a
new owner walks, and one nothing on the product needs.

There are two doors now and either one alone is a finished deployment.

CONSOLE_KEY is a string the owner makes up, at least sixteen characters,
and POST /admin/claim trades it for the same thirty day session the
pairing code has always ended in. One place mints a token, so the two
doors cannot drift in how long somebody stays signed in or in what is
written down about them. The key is compared in constant time and never
stored: only a hash of it, beside a hash of the token.

Length is the whole of the rule, and there is no attempt limit, because
sixteen characters is far past what can be guessed — a limit would
defend against an attack nobody can mount while adding a number for the
owner to wonder about. The setup page offers a generated key rather than
leaving the owner to invent one.

Changing the key takes it back. A session opened with a key remembers
which key, and ends the moment that is no longer the key: without that,
"change CONSOLE_KEY" stopped the next sign in and nothing else, and
every browser already holding a token kept working — which is exactly
the case a leaked key is changed for. A session paired from Telegram
carries no such mark. It was the console bot that vouched for that
person, not a setting.

WEB_OWNER_ID is 0: a legal row in a table keyed by Telegram account id,
and an account Telegram has never heard of. findOwner now excludes it,
because both of its callers use what comes back as a chat id, and a
deployment with both doors would otherwise have stopped alerting the
moment the console row happened to be the older of the two.

The deploy form asks for one box. It reads .dev.vars.example, and that
file now lists CONSOLE_KEY and nothing else: a row of empty boxes at the
first step reads as work to do before anything happens, and there is
none. A console bot is added afterwards from the Worker's own settings,
and the setup page says how.

A short key stops a deployment only when it is the only door. Adding a
weak one to a working Telegram console is trying something, not breaking
something, and the page says it is set and not being used rather than
taking the deployment down.

The rest follows the same fact. The five READMEs ask for three things
and offer Telegram as the extra; the deploy script decides on the record
/health publishes rather than by matching words in a rendered page; the
smoke test walks the door the form actually asks for, which nothing had
ever exercised; the installer can set the key; and `muxel init` no
longer makes --owner-telegram-id a required flag.

Verified against a real Worker on an empty database, twice: a console
key alone reaches "ready", the wrong key is refused, the right one mints
a token, and the console API answers it. And live: sign in, change the
key in the Worker's settings, and the same browser is refused while the
new key works.

904 tests, the new ones each checked by putting the bug back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMFyy3oXb9u2AxoYQKRXHu
@thankywal
thankywal merged commit cd69820 into main Sep 3, 2026
1 check passed
@thankywal
thankywal deleted the telegram-is-optional branch September 3, 2026 11:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant