Skip to content

feat(connections): scan-to-connect — QR channel pairing (Telegram + Discord)#9

Merged
ogarciarevett merged 1 commit into
mainfrom
feat/scan-to-connect
Jun 4, 2026
Merged

feat(connections): scan-to-connect — QR channel pairing (Telegram + Discord)#9
ogarciarevett merged 1 commit into
mainfrom
feat/scan-to-connect

Conversation

@ogarciarevett

Copy link
Copy Markdown
Owner

Summary

Connect a messaging channel by scanning a QR — from Vesper World or the terminal — with the chat captured automatically (no copying chat ids). Dependency-free; bot tokens stay on the CLI (stdin -> Keychain), never the browser.

The flagship is the Telegram flow: a QR of https://t.me/<bot>?start=<nonce> — scan, tap Start, and the bot's long-poll receives /start <nonce>, so Vesper captures and persists your chat id automatically. Discord is the analogue (OAuth2 invite QR + a first pair <nonce> message captures the target channel).

What's included

  • Core — an optional Pairable capability a ChannelHandler may also implement (startPairing -> PairingSession streaming awaiting/linked/error/expired); a portable, dependency-free QR encoder (Nayuki port, cross-checked byte-for-byte vs upstream) + a half-block ANSI terminal renderer (media/qr); a pairable channel-state flag; pairing audit kinds with nonce/qr redaction.
  • Telegram — deep-link QR auto-captures chatId.
  • Discord — OAuth2 invite QR (state=<nonce>) + first pair <nonce> message captures the channel.
  • Daemon — a PairingCoordinator that multiplexes the single inbound long-poll into the chat sink + active pairing sessions (a transient receiver covers not-yet-running channels), and persists params.defaultChatId on link.
  • UI + CLIPOST /api/connections/:id/pair (ndjson stream) + GET /api/qr; a Vesper World "Connect" canvas-QR card with live status; vesper connections pair <id> rendering the QR in the terminal.

Design notes

  • One inbound consumer: pairing never opens a second getUpdates (Telegram allows only one).
  • Tokens never transit the pairing path — only nonces/links/QRs + the resulting chat id; UI token entry intentionally stays CLI (secure browser entry would need the out-of-band approval gate, which undercuts ease).
  • Bring-your-own-CLI intact; no LLM provider SDKs; zero new dependencies in the lockfile.

Deferred (follow-up)

WhatsApp-Web via Baileys (isolated opt-in package + contract amendment), Signal (signal-cli), optional browser token entry behind the approval gate.

Test plan

  • bun test — 854 / 0 fail (QR encoder/renderer, pairing abstraction, Telegram + Discord pairing, coordinator, ndjson endpoint + /api/qr, CLI render).
  • biome ci clean.
  • tsc --noEmit adds no new errors (manual; CI skips tsc).
  • No new dependencies; lockfile has no Baileys/provider SDK.
  • Live round-trip against a real Telegram/Discord bot token (not exercised in CI — no tokens; all network/inbound seams mocked).

…+ Discord

Connect a channel by scanning a QR — from Vesper World or the terminal — with
the chat captured automatically (no copying ids). Dependency-free; bot tokens
stay CLI/stdin -> Keychain.

- core: optional Pairable capability (startPairing -> streamed PairingUpdates);
  portable Nayuki QR encoder + half-block terminal renderer (media/qr); pairable
  channel-state flag; pairing audit kinds with nonce/qr redaction
- telegram: t.me/<bot>?start=<nonce> deep-link QR auto-captures chatId via the
  bot long-poll
- discord: OAuth2 invite QR (state=<nonce>) + first `pair <nonce>` message
  captures the channel
- daemon: PairingCoordinator multiplexes the single inbound long-poll into chat
  sink + pairing sessions (transient receiver for not-yet-running channels);
  persists params.defaultChatId on link
- ui/cli: POST /api/connections/:id/pair ndjson + GET /api/qr; Vesper World
  "Connect" canvas QR card; `vesper connections pair <id>` terminal QR

854 tests / 0 fail; biome clean; no new dependencies. WhatsApp-Web (Baileys) +
Signal deferred to a follow-up. Record per Rule 11 (issue cap): cycle-log.md.
@ogarciarevett ogarciarevett self-assigned this Jun 4, 2026
@ogarciarevett ogarciarevett merged commit c1db395 into main Jun 4, 2026
1 check passed
@ogarciarevett ogarciarevett deleted the feat/scan-to-connect branch June 4, 2026 22:51
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