M mctl-telegram

How Local Bridge works

Short version of activation, proof of possession, and the read-only first token. Setup steps live in Quick start. Owner actions live in Owner controls.

Activation

mctl-telegram-local activate talks to:

Nothing is written to the server database until you approve. A brand-new Telegram id is created as mode=local with session_encrypted = NULL and send_enabled = false.

Proof of possession

After approval the CLI does not paste a token. It:

  1. POST /api/local-bridge/devices/{device_id}/nonce
  2. Signs device_id + "." + nonce with the local Ed25519 private key
  3. POST /api/local-bridge/devices/{device_id}/credential with the nonce and signature

The private key never leaves device_key.json. The server verifies a signature; it cannot forge one. Every later daemon connection repeats the same shape against /refresh instead of /credential.

A device-signed credential lasts hours, not months. There is no bearer secret sitting in a file that you have to rotate by hand — that is the legacy worker-token path, documented under Legacy connect.

Read-only first token

The first credential a newly registered device receives is read-only, regardless of the account's send_enabled value. Getting send capability is always the separate set_send_consent (or manage-page) step. A refresh after you grant consent is what adds telegram:messages:send and telegram:messages:pin.

This is why a just-activated daemon can list dialogs immediately and cannot send until you say so.

Device binding and revocation

device_key.json holds the private key, the public key, and — once issued — the device credential, in one record. Everything the daemon writes is owner-only (0600), including the session database and its SQLite sidecar files. On Windows, where NTFS ignores that mode, the same files and the folder holding them instead carry an explicit permission entry naming your account and nobody else — not even Administrators or SYSTEM, which means a daemon you install as a Windows service running as LocalSystem will not be able to read credentials you created as yourself. That entry stops the ordinary permission check and nothing more: an administrator of the machine can still read the files by other means, so the protection is against another ordinary account on the same computer, not against whoever administers it.

The daemon also repairs these permissions at startup on an installation created by an older version. That pass is best-effort: it refuses to touch a configuration directory owned by a different account rather than seizing it, and if it cannot apply a permission it logs a warning and carries on rather than refusing to start. A warning from it at startup means the files may still be readable by other accounts on the machine — worth acting on rather than ignoring.

revoke_local_bridge_device denylists the jti claimed at first issuance. Every later refresh carries that same jti forward, so denylisting it kills every credential the device has ever held. If the device is already connected, the same call evicts its live /bridge websocket.

What the server keeps

Depends on how the account became local:

Local accounts are excluded from the idle and absolute session sweepers, so nothing silently reverts to hosted after 30 days.

What the relay sees

Tool arguments and results pass through the relay in memory for the duration of a call. Audit rows stay metadata-only and record call_path=local when the daemon served the call. This is not end-to-end encryption.

Next

This page is rendered from docs/local-bridge/how-it-works.md in the repository, so what you read here is the same text the code is developed against.