Skip to content

qc: qrypt.chat in the terminal (hqtui client, CLI, MCP, browser login) - #271

Merged
ralyodio merged 1 commit into
masterfrom
feat/qc-tui
Oct 6, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/qc-tui

Conversation

@ralyodio

@ralyodio ralyodio commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Adds qc, qrypt.chat's terminal client. The CLI that was there before couldn't do any of this: it talked to a WebSocket server the Next app no longer runs, and it sent plaintext.

What you get

  • qc opens a full-screen chat client built on hqtui 0.8.0:
    • chats with unread counts, and a transcript grouped by sender, newest at the bottom
    • typing indicators and a live/connecting/offline badge
    • the OpenEmoji picker on Ctrl+E, plus :rocket:-style shortcodes turned into emoji on send
    • mouse support: one click opens a chat, the wheel scrolls
  • qc chats | read <chat> | send <chat> <text|-> | listen for scripts, with --json.
  • qc mcp runs an MCP server on stdio with the tools list_chats, read_chat and send_message.

Encryption

  • It uses the web app's own ML-KEM-1024 code (post-quantum-encryption.js), run in Node/Bun with the keys passed in.
  • Each message is encrypted per participant on the machine running qc, so the server only ever sees ciphertext.
  • A test sends a message and opens each copy with the recipient's keys.

qc login: OAuth 2.1, authorization code + PKCE, started from the CLI

  1. qc listens on a loopback port and opens /cli/authorize. The request carries an S256 challenge and a one-time ML-KEM-1024 public key, and qc prints a confirmation code.
  2. The signed-in web app shows the same code. On Approve, it seals the account keypair (exportUserKeys) to the one-time key. POST /api/cli/authorize then stores a 5-minute, single-use code in cli_auth_codes; only the code's hash is stored.
  3. qc calls POST /api/cli/token with the code and verifier. It gets back its own Supabase session, minted via generateLink → verifyOtp, plus the sealed keys.
    • qc never receives a copy of the browser's session, because refresh tokens rotate and two holders would revoke each other.
    • grant_type=refresh_token rotates the session.
    • Phone-only and anonymous accounts get a confirmed synthetic address for the magic-link bridge, the same way Moshpit names do.
  4. Over SSH, --oob shows the code in the browser for you to paste into the terminal.

Server changes

  • /api/events now uses authenticateRequest, so it accepts a Bearer token or the cookie. A CLI can now follow the live stream.
  • Migration 20261006120000_cli_auth_codes: RLS on, no policies, service_role only. It is already applied on dev2, and I verified anon/authenticated have no access and service_role can insert.

Package

  • packages/qryptchat → @profullstack/qryptchat on npm, with bins qc and qryptchat.
  • bun scripts/build-qc.js bundles src/cli and the crypto it uses into about 70 KB, with the TUI and MCP as lazy chunks. The runtime dependencies are @profullstack/hqtui, mlkem and @noble/ciphers.
  • public/skill.md and llms.txt describe qc. skill.md used to say there was no API.

Tests

24 new tests in tests/cli/:

  • the key handoff: it seals and opens, and a wrong key can't open it
  • the confirmation code
  • per-participant encryption and decryption
  • refresh on a 401 that saves the rotated session
  • the SSE reader
  • argument parsing and chat matching
  • MCP
  • PKCE: the RFC 7636 vector, single use, no burn on a wrong verifier, expiry, the synthetic address, redirect allow-listing
  • the TUI layout through renderToScreen, including the picker, a click that opens a chat, and wheel scroll

Results:

  • bun run test:ci: 593/593.
  • next build passes when Supabase env is set; without it, it fails at /api/auth/upload-avatar exactly as it did before this change.

Not in this PR

  • /api/messages/load sorts ascending and then applies the limit, so a chat with more than 100 messages shows its first 100, not its latest. The web app has the same behaviour. This is Preshy's messaging area, so I left it alone.
  • The old bin/qryptchat-cli.js (blessed/WebSocket) is still in the repo but nothing points at it any more.

🤖 Generated with Claude Code

Bare `qc` opens a full-screen end-to-end encrypted chat client on hqtui:
chats with unread counts, a bottom-anchored transcript grouped by sender,
typing indicators, a live/connecting/offline badge, the OpenEmoji picker
on Ctrl+E, :shortcode: emojify on send, and the mouse (one click opens a
chat, the wheel scrolls). Scripts get `qc chats|read|send|listen`
(--json), agents get `qc mcp` (list_chats, read_chat, send_message).

Encryption is the web app's own ML-KEM-1024 code run in Node/Bun with the
keys handed in: a message is encrypted per participant here, and the
server only ever sees ciphertext.

`qc login` is OAuth 2.1 authorization code + PKCE, started from the CLI:
- /cli/authorize (web): the signed-in app approves, seals the account
  keypair to a one-time ML-KEM-1024 key qc generated, and stores a
  5-minute single-use code (hash only) in cli_auth_codes.
- /api/cli/token: code + verifier -> a fresh Supabase session of qc's own
  (never the browser's: refresh tokens rotate) + the sealed keys;
  grant_type=refresh_token rotates. Phone-only/anon accounts get a
  confirmed synthetic address for the magic-link bridge, like names do.
- Loopback redirect, or --oob to paste a code over SSH. Both sides show
  a confirmation code derived from the challenge and the one-time key.

/api/events now authenticates with authenticateRequest (Bearer or
cookie), so a CLI can follow the live stream.

Migration 20261006120000_cli_auth_codes: RLS on, no policies,
service_role only. Applied on dev2.

Publishable package: packages/qryptchat (@profullstack/qryptchat, bins
qc + qryptchat), built by scripts/build-qc.js. The root `qryptchat` bin
and `cli` script now point at qc; the old WebSocket-era blessed CLI is
left in place, unused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

13 finding(s)

MEDIUM: 9 | LOW: 4

Severity Rule Location
MEDIUM redos-nested-quantifier src/app/api/profile/update/route.js:73
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:38
MEDIUM js-unescaped-html-sink src/app/blog/[slug]/page.jsx:66
MEDIUM js-unescaped-html-sink src/app/faq/page.jsx:57
MEDIUM js-unescaped-html-sink src/app/layout.jsx:137
MEDIUM js-unescaped-html-sink src/app/layout.jsx:141
MEDIUM js-unescaped-html-sink src/app/page.jsx:47
MEDIUM redos-nested-quantifier src/lib/auth/dns-name.js:88
MEDIUM js-unescaped-html-sink src/lib/components/chat/MessageItem.jsx:63
LOW secret-generic-credential src/app/api/auth/register-anon/route.test.js:32
LOW secret-jwt tests/debug-sms.js:10
LOW secret-generic-credential tests/private-key-import-export.test.js:252
LOW secret-generic-credential tests/private-key-import-export.test.js:264

Snippets are redacted; ThreatCrush never prints matched credential material.

@socket-security

Copy link
Copy Markdown
Contributor

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​profullstack/​hqtui@​0.8.08210010097100

View full report

@ralyodio
ralyodio merged commit 27822bb into master Oct 6, 2026
11 checks passed
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