Commit 9ed649f
authored
feat: add xurl chat — an end-to-end encrypted XChat client (#89)
* feat: add xurl chat — an end-to-end encrypted XChat client
Adds a chat command family built on the chat-xdk crypto library
(github.com/xdevplatform/chat-xdk/go/chatxdk): encryption, decryption,
and signature verification happen locally, so the server only ever sees
ciphertext.
Commands:
- chat keys status|restore|import — key management. xurl never generates
or registers encryption keys: the account must already have keys from
another XChat client, brought to this machine via Juicebox PIN
recovery (read-only; xurl never writes to Juicebox) or an exported
key blob. Restore/import reject keys that are not registered on the
account, and adopt the registered key version by matching the local
identity key with chat-xdk's MatchesRegisteredKey.
- chat conversations — inbox list with decrypted group names (encrypted
names render as a lock marker when no key is available).
- chat read / chat listen — decrypted history and a live poll loop.
Conversation keys come from the events endpoint's out-of-band
meta.conversation_key_events; key changes whose signers have left the
group fall back to ECIES-only extraction (message authorship is still
verified per message). Poll loops never follow pagination tokens,
which walk backward through history; they re-fetch the newest page
and dedup via a seen set.
- chat send — sends with the SDK's verified key cache, falling back to
extracted keys; a fresh 1:1 gets a conversation key automatically,
and rotating an existing conversation's key always requires explicit
confirmation.
Every chat command prints the acting account, and a missing-keys
session distinguishes 'wrong acting user' from 'new machine', offering
the PIN recovery inline on a TTY.
The crypto binding is cgo (darwin amd64/arm64, linux amd64), so chat
code is build-tagged and other platforms get a stub; goreleaser
binaries stay CGO_ENABLED=0 and ship the stub.
Storage: ~/.xurl becomes a directory — tokens in auth.yml, private
chat keys in keys.yml (0600, write-then-rename, corrupt files refuse
overwrite). A legacy single-file ~/.xurl migrates automatically with
crash recovery; the pre-v1.0 JSON conversion and .twurlrc import are
retained on top of the new layout.
* fix: don't auto-login unknown explicit usernames; honor -v in chat read
Two fixes from read-only exercise of the chat commands:
- GetOAuth2Header with an explicitly named user that has no stored
token now errors with re-auth instructions instead of silently
launching the interactive browser flow. The old behavior minted a
real token under whatever label was passed — typos included — and
invalidated the account's previous grant as a side effect.
- chat read now honors --verbose for non-message events (key changes,
read receipts, typing); the printer was hardcoded to non-verbose.
* feat(chat): add key rotation and adopt server-confirmed conversation ids
- New 'xurl chat rotate CONVERSATION|@USERNAME': generates a fresh
conversation key and wraps it to every current participant's newest
registered keys (group rosters come from conversation metadata, 1:1
rosters from the canonical id). Rotation is confirmed interactively
and refused non-interactively without --yes, since other
participants' clients see the key change. It protects future
messages only, and also grants forward access to members whose keys
were registered after the last rotation.
- Sending to someone new and rotating now share one primitive
(establishConversationKey): an empty conversation id derives a fresh
1:1, a set id rotates in place. Both adopt the canonical
conversation id returned by the keys endpoint instead of
reconstructing it client-side.
* feat(chat): media, replies, add-members, mark-read, typing
Round out the chat client with the remaining documented routes and SDK
capabilities:
- Media: 'chat send --file' encrypts a local file under the conversation
key (EncryptStream), uploads it via the three-step media routes, and
attaches the media_hash_key; 'chat download CONV HASH -o out' fetches
and decrypts an attachment, trying each held key version. Inbound
attachments now render the media_hash_key so it can be downloaded.
- Replies: 'chat send --reply-to SEQ' builds a threaded reply
(EncryptReply) from the referenced event; replies render with a ↩.
- Group membership: 'chat add-members GROUP @user...' rotates the
conversation key to the new roster (PrepareGroupMembersChange) so
added members read messages going forward. Confirmed interactively,
refused non-interactively without --yes.
- Read receipts: 'chat mark-read', plus a --mark-read flag on send.
- Typing: 'chat typing'.
Media/text share one key resolver (resolveSendKey) so an attachment is
always encrypted under the same key as its message. Send now accepts
text, --file, or both.
* feat(chat): mark read and typing automatically
Reading or replying to a conversation implies you have seen it, so:
- 'chat read' and 'chat listen' now mark the conversation read
automatically — up to the newest event, which the backend treats as a
watermark that also marks every earlier message read. 'listen'
advances the watermark as new messages arrive.
- 'chat send' marks the conversation read after sending and sends a
typing indicator before, mirroring how a person composes.
All are best-effort writes (a failure warns, never aborts the read or
send) and opt-out via --no-mark-read / --no-typing for lurking or
scripting. The standalone 'mark-read' and 'typing' commands remain for
explicit/scripted use.
* fix(auth): don't silently downgrade an explicit -u user to app-only
When a specific OAuth2 user is requested with -u/--username and that
user's token cannot be produced (e.g. an expired access token whose
refresh fails), getAuthHeader fell through to OAuth1 and then the
app-only bearer token. The request then went out as the wrong principal
and the server rejected it with a confusing 'OAuth 2.0 Application-Only
is forbidden' 403 instead of the real cause.
Now, when a username was explicitly requested, a failure to obtain that
user's OAuth2 header is returned to the caller (surfacing e.g. 'refresh
token invalid — re-authenticate'). The unspecified-user fallback is
unchanged, so raw passthrough behavior is preserved.
* docs: complete and tidy chat command coverage
- SKILL.md: restore the '### Media Upload' heading that was clobbered
when the chat section was inserted, and rewrite the chat walkthrough as
a numbered flow covering every action — read, send (+ --file,
--reply-to, --no-mark-read/--no-typing), download, listen, mark-read,
typing, rotate, add-members — so an agent can see how each is invoked.
- README.md: condense the chat section (72 to 46 lines) to a compact
command list plus the key-policy/storage/platform notes, leaving
SKILL.md as the detailed reference.1 parent 49ea3b8 commit 9ed649f
21 files changed
Lines changed: 3481 additions & 67 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
19 | 24 | | |
20 | 25 | | |
21 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
7 | 15 | | |
8 | 16 | | |
9 | 17 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
45 | 46 | | |
46 | 47 | | |
47 | 48 | | |
48 | | - | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
54 | | - | |
| 55 | + | |
55 | 56 | | |
56 | 57 | | |
57 | 58 | | |
| |||
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
68 | | - | |
| 69 | + | |
69 | 70 | | |
70 | 71 | | |
71 | 72 | | |
| |||
383 | 384 | | |
384 | 385 | | |
385 | 386 | | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
386 | 432 | | |
387 | 433 | | |
388 | | - | |
| 434 | + | |
389 | 435 | | |
390 | 436 | | |
391 | 437 | | |
| |||
407 | 453 | | |
408 | 454 | | |
409 | 455 | | |
410 | | - | |
| 456 | + | |
411 | 457 | | |
412 | 458 | | |
413 | 459 | | |
| |||
0 commit comments