feat: add xurl chat — an end-to-end encrypted XChat client#89
Merged
Conversation
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.
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.
- 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.
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.
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.
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.
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
chatcommand family: a full XChat (encrypted DM) client where encryption, decryption, and signature verification happen locally via chat-xdk (go/chatxdkv0.4.1) — the server only ever sees ciphertext.Design decisions
public_key_versionis adopted by matching the local identity key with the SDK'sMatchesRegisteredKey.meta.conversation_key_events. Key changes signed by members who have since left the group (whose keys the API no longer serves) fall back to ECIES-only extraction — safe because only keys encrypted to this identity can decrypt, and message authorship is still verified per message.listennever follows them: each poll re-fetches the newest page and dedups via a seen set seeded from the backlog.Storage change
~/.xurlbecomes a directory: tokens inauth.yml, private chat keys inkeys.yml(0600, write-then-rename; a corrupt key file refuses overwrite instead of qualifying for restore). A legacy single-file~/.xurlmigrates automatically — rename-based, non-destructive, with crash recovery for an interrupted migration — and the pre-v1.0 JSON conversion and.twurlrcimport still work on top of the new layout. Note: older binaries cannot read the new layout once a machine migrates.Testing
api(httptest-backed route executors),cli(conversation resolution incl. usernames starting withg, key-encoding matching against real generated keypairs, request-shape mapping), andstore(key-store round-trip/permissions/corruption, migration incl. interrupted-migration recovery) — full suite green, plusgofmt/go vetand aGOOS=windows CGO_ENABLED=0stub build.listen, and multi-account switching.Docs
README ("Encrypted Chat" section), SKILL.md (agent guidance incl. secret-safety rules for
~/.xurl/and PIN handling), CHANGELOG (Added + Changed entries).