From 23a4a87b81ad5714325eff165e524cfc1904f15e Mon Sep 17 00:00:00 2001 From: Santiago Medina Rolong Date: Wed, 15 Jul 2026 16:36:11 -0700 Subject: [PATCH 1/3] docs(xchat): correct chat-xdk API surface across guides Fix API-accuracy bugs against the current SDK: streaming is a two-arg raw-bytes call (media_hash_key comes from upload finalize, not encrypt_stream); prepare methods take a single params object/struct in every binding except Python (and the params types dropped the Prepare prefix); decrypt_events .errors is a map, not a list; Go conversation keys are raw bytes, not base64; verification is fail-closed by default. Add the incremental streaming API, the keyVersion-per-event media key selection contract, the three Juicebox config shapes, and the hex utility helpers. --- xchat/cryptography-primer.mdx | 2 +- xchat/getting-started.mdx | 55 +++++++----- xchat/groups.mdx | 31 +++---- xchat/media.mdx | 28 +++++-- xchat/troubleshooting.mdx | 12 ++- xchat/xchat-xdk.mdx | 154 ++++++++++++++++++++++++---------- 6 files changed, 193 insertions(+), 89 deletions(-) diff --git a/xchat/cryptography-primer.mdx b/xchat/cryptography-primer.mdx index 16b2ce788..db9f295b0 100644 --- a/xchat/cryptography-primer.mdx +++ b/xchat/cryptography-primer.mdx @@ -224,7 +224,7 @@ If anything in the signed material changes, verification fails. Only someone wit ### In your app -The Chat XDK signs when you encrypt outbound messages and verifies when you decrypt inbound ones—**if** you provide the sender’s public key material (from the public-key APIs). Results typically include a **verified** flag; you can treat unverified messages as suspicious or configure the SDK to reject them. Details are in the [Chat XDK](/xchat/xchat-xdk) reference. +The Chat XDK signs when you encrypt outbound messages and verifies when you decrypt inbound ones against the sender’s public key material (from the public-key APIs). Verification is **mandatory by default**: the SDK rejects unverified signed events unless you explicitly disable the check (not recommended). Details are in the [Chat XDK](/xchat/xchat-xdk) reference. ### Signed state changes (action signatures) diff --git a/xchat/getting-started.mdx b/xchat/getting-started.mdx index 953489434..9f647d09f 100644 --- a/xchat/getting-started.mdx +++ b/xchat/getting-started.mdx @@ -408,10 +408,14 @@ The response returns the canonical conversation id (`data.conversation_id`—the } // Omit conversationId for a new 1:1; pass the id to rotate later - const prepared = chat.prepareConversationKeyChange('YOUR_USER_ID', signingKeyVersion, [ - await publicKeyInput('YOUR_USER_ID'), - await publicKeyInput('RECIPIENT_USER_ID'), - ]); + const prepared = chat.prepareConversationKeyChange({ + senderId: 'YOUR_USER_ID', + signingKeyVersion, + publicKeys: [ + await publicKeyInput('YOUR_USER_ID'), + await publicKeyInput('RECIPIENT_USER_ID'), + ], + }); const resp = await client.chat.addConversationKeys('RECIPIENT_USER_ID', { conversation_key_version: prepared.conversationKeyVersion, conversation_participant_keys: prepared.participantKeys.map((pk) => ({ @@ -439,11 +443,9 @@ The response returns the canonical conversation id (`data.conversation_id`—the ```rust // public_key_inputs: Vec from GET public keys // (user_id, public_key, key_version ← public_key_version) + // new 1:1; set params.conversation_id = Some(id) to rotate later let prepared = chat.prepare_conversation_key_change( - &sender_id, - &signing_key_version, - &public_key_inputs, - None, // new 1:1; pass Some(id) to rotate later + ConversationKeyChangeParams::new(&sender_id, &signing_key_version, public_key_inputs), )?; let participant_keys: Vec<_> = prepared .participant_keys @@ -491,11 +493,15 @@ The response returns the canonical conversation id (`data.conversation_id`—the ```go // KeyVersion comes from the public_key_version field on each record - prepared, err := chat.PrepareConversationKeyChange(myUserID, signingKeyVersion, - []chatxdk.PublicKeyInput{ + prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{ + SenderID: myUserID, + SigningKeyVersion: signingKeyVersion, + PublicKeys: []chatxdk.PublicKeyInput{ {UserID: myUserID, PublicKey: myIdentityPubB64, KeyVersion: myKeyVersion}, {UserID: recipientID, PublicKey: theirIdentityPubB64, KeyVersion: theirKeyVersion}, - }, "") // empty id for a new 1:1; pass the id to rotate later + }, + // ConversationID empty for a new 1:1; pass the id to rotate later + }) var parts []map[string]string for _, pk := range prepared.ParticipantKeys { parts = append(parts, map[string]string{ @@ -528,17 +534,21 @@ The response returns the canonical conversation id (`data.conversation_id`—the req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) // Response data.conversation_id is the canonical id for later requests - // prepared.ConversationKey is base64(raw key) — pass to EncryptMessage + // prepared.ConversationKey is the raw []byte key — pass to EncryptMessage _ = resp ``` ```csharp // KeyVersion comes from the public_key_version field on each record - var prepared = chat.PrepareConversationKeyChange(myUserId, signingKeyVersion, new[] { - new PublicKeyInput { UserId = myUserId, PublicKey = myPk, KeyVersion = myVer }, - new PublicKeyInput { UserId = recipientId, PublicKey = theirPk, KeyVersion = theirVer }, - }); // conversationId: null for a new 1:1; pass the id to rotate later + var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams { + SenderId = myUserId, + SigningKeyVersion = signingKeyVersion, + PublicKeys = new[] { + new PublicKeyInput { UserId = myUserId, PublicKey = myPk, KeyVersion = myVer }, + new PublicKeyInput { UserId = recipientId, PublicKey = theirPk, KeyVersion = theirVer }, + }, + }); // ConversationId null for a new 1:1; pass the id to rotate later var keysBody = new { conversation_key_version = prepared.ConversationKeyVersion, conversation_participant_keys = prepared.ParticipantKeys.Select(pk => new { @@ -577,9 +587,12 @@ The response returns the canonical conversation id (`data.conversation_id`—the PublicKeyInput theirs = new PublicKeyInput(); theirs.userId = recipientId; theirs.publicKey = theirIdentityPubB64; theirs.keyVersion = theirKeyVersion; - PreparedConversationChange prepared = chat.prepareConversationKeyChange( - myUserId, signingKeyVersion, List.of(mine, theirs), - null); // null for a new 1:1; pass the id to rotate later + ConversationKeyChangeParams keyParams = new ConversationKeyChangeParams(); + keyParams.senderId = myUserId; + keyParams.signingKeyVersion = signingKeyVersion; + keyParams.publicKeys = List.of(mine, theirs); + // keyParams.conversationId null for a new 1:1; set the id to rotate later + PreparedConversationChange prepared = chat.prepareConversationKeyChange(keyParams); List> parts = new ArrayList<>(); for (var pk : prepared.participantKeys) { @@ -622,7 +635,7 @@ The response returns the canonical conversation id (`data.conversation_id`—the ## 5. Send a message -Encrypt with the **raw** conversation key (in Go, pass that key as **base64**). On the send request, map: +Encrypt with the **raw** conversation key (raw bytes in every binding, including Go). On the send request, map: | Chat XDK field | Request body field | |:---------------|:-------------------| @@ -712,7 +725,7 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (` MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKeyB64, + ConversationKey: convKey, // raw []byte from prepared.ConversationKey Text: "Hello!", ConversationKeyVersion: convKeyVersion, SigningKeyVersion: signingKeyVersion, diff --git a/xchat/groups.mdx b/xchat/groups.mdx index 44039b830..48383a3a6 100644 --- a/xchat/groups.mdx +++ b/xchat/groups.mdx @@ -50,27 +50,28 @@ The `title` and `avatar_url` you pass to `prepare_group_create` are signed and e ```typescript - const prepared = chat.prepareGroupCreate( - myUserId, signingKeyVersion, memberPublicKeys, - groupId, // g-prefixed id from POST /2/chat/conversations/group/initialize - memberIds, adminIds, 'Project team', - ); + const prepared = chat.prepareGroupCreate({ + senderId: myUserId, signingKeyVersion, publicKeys: memberPublicKeys, + conversationId: groupId, // g-prefixed id from POST /2/chat/conversations/group/initialize + memberIds, adminIds, title: 'Project team', + }); // prepared.actionSignatures has two entries — send both ``` ```rust - let prepared = chat.prepare_group_create( - &sender_id, &signing_key_version, &member_public_keys, - &group_id, &member_ids, &admin_ids, - Some("Project team"), None, None, - )?; + let mut params = GroupCreateParams::new( + &sender_id, &signing_key_version, member_public_keys, + &group_id, member_ids, admin_ids, + ); + params.title = Some("Project team".into()); + let prepared = chat.prepare_group_create(params)?; // prepared.action_signatures has two entries — send both ``` ```go - prepared, err := chat.PrepareGroupCreate(chatxdk.PrepareGroupCreateParams{ + prepared, err := chat.PrepareGroupCreate(chatxdk.GroupCreateParams{ SenderID: myUserID, SigningKeyVersion: signingKeyVersion, PublicKeys: memberPublicKeys, ConversationID: groupID, MemberIDs: memberIDs, AdminIDs: adminIDs, Title: "Project team", @@ -82,7 +83,7 @@ The `title` and `avatar_url` you pass to `prepare_group_create` are signed and e ```csharp - var prepared = chat.PrepareGroupCreate(new PrepareGroupCreateParams { + var prepared = chat.PrepareGroupCreate(new GroupCreateParams { SenderId = myUserId, SigningKeyVersion = signingKeyVersion, PublicKeys = memberPublicKeys, ConversationId = groupId, MemberIds = memberIds, AdminIds = adminIds, Title = "Project team", @@ -92,7 +93,7 @@ The `title` and `avatar_url` you pass to `prepare_group_create` are signed and e ```java - PrepareGroupCreateParams params = new PrepareGroupCreateParams(); + GroupCreateParams params = new GroupCreateParams(); params.senderId = myUserId; params.signingKeyVersion = signingKeyVersion; params.publicKeys = memberPublicKeys; @@ -144,8 +145,8 @@ Whether a given field is stored encrypted is decided by the client that writes i ```go - groupName, err := chat.Decrypt(conversationGroupNameB64, convKeyB64) - encryptedName, err := chat.Encrypt("Project team", convKeyB64) + groupName, err := chat.Decrypt(conversationGroupNameB64, rawConvKey) // rawConvKey is raw []byte + encryptedName, err := chat.Encrypt("Project team", rawConvKey) _ = groupName _ = encryptedName ``` diff --git a/xchat/media.mdx b/xchat/media.mdx index 9074661b6..693e9acf8 100644 --- a/xchat/media.mdx +++ b/xchat/media.mdx @@ -71,10 +71,9 @@ flowchart LR mime, _ := chatxdk.DetectMimeType(plaintext) dims, _ := chatxdk.DetectImageDimensions(plaintext) _ = mime - ptB64, _ := chatxdk.BytesToBase64(plaintext) - encryptedB64, err := chat.EncryptStream(ptB64, convKeyB64) + encrypted, err := chat.EncryptStream(plaintext, rawConvKey) // both raw []byte _ = dims - _ = encryptedB64 + _ = encrypted ``` @@ -206,7 +205,7 @@ Encrypt with a media attachment, then POST the send-message body (same field map ```go payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKeyB64, Text: caption, + ConversationKey: rawConvKey, Text: caption, // rawConvKey is raw []byte ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion, Attachments: []chatxdk.AttachmentDescriptor{{ AttachmentType: "media", @@ -259,6 +258,25 @@ Encrypt with a media attachment, then POST the send-message body (same field map Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/download-chat-media). Response body is ciphertext. On inbound messages, read `media_hash_key` from decrypted attachments / `media_hashes`. +**Pick the key by the event's key version.** Each decrypted message event carries the `keyVersion` (JS; `key_version` in the other bindings) its content was encrypted under. Decrypt an attachment with the conversation key for **that** version—`conversationKeys.keys[event.keyVersion]`—not the latest. After a key rotation (for example a member add), the latest key cannot decrypt media attached to older messages. + + + + ```python + keys = result["conversation_keys"]["keys"] + key_for_media = keys[event["key_version"]] # not the latest version + plaintext = chat.decrypt_stream(encrypted_blob, key_for_media) + ``` + + + ```typescript + const keys = result.conversationKeys.keys; + const keyForMedia = keys[event.keyVersion]; // not the latest version + const plaintext = chat.decryptStream(encryptedBlob, keyForMedia); + ``` + + + ```python @@ -306,7 +324,7 @@ Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl req, _ := http.NewRequest(http.MethodGet, url, nil) req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := http.DefaultClient.Do(req) - // read body → BytesToBase64 → chat.DecryptStream(encryptedB64, convKeyB64) + // read body into []byte → chat.DecryptStream(encryptedBlob, rawConvKey) — both raw []byte _ = resp _ = err ``` diff --git a/xchat/troubleshooting.mdx b/xchat/troubleshooting.mdx index 17aa35ef7..3c6cba332 100644 --- a/xchat/troubleshooting.mdx +++ b/xchat/troubleshooting.mdx @@ -128,9 +128,13 @@ They may not have finished onboarding. After they register, load `public_key`, ` ### Signature does not verify -- Supply a full signing-key entry for the **sender** (all fields required by the Chat XDK—see the [Chat XDK](/xchat/xchat-xdk) reference) -- Re-fetch their public keys if they rotated versions -- To fail closed when verification fails: +Verification is **fail-closed by default** (`reject_unverified = true`): the SDK already rejects unverified signed events, so a failure here means the verification inputs are wrong, not that you need to turn checking on. Common causes: + +- Missing or incomplete signing-key entry for the **sender** (all fields required by the Chat XDK—see the [Chat XDK](/xchat/xchat-xdk) reference) +- The sender rotated versions—re-fetch their public keys +- A key version below the accepted floor never verifies + +The `set_reject_unverified` setter exists to opt **out** of this default (`false`, not recommended). If you disabled it earlier, restore the fail-closed default: @@ -177,7 +181,7 @@ These mistakes are specific to X Chat encryption (not general HTTP errors): | Issue | Fix | |:------|:----| -| Wrong key bytes | Pass the **raw** conversation key into the Chat XDK (Go: base64 encoding of those bytes), not the encrypted key string from the API | +| Wrong key bytes | Pass the **raw** conversation key bytes into the Chat XDK (raw bytes in every binding, including Go), not the encrypted key string from the API | | Wrong JSON field names | Map `encrypted_content` → `encoded_message_create_event` and `encoded_event_signature` → `encoded_message_event_signature` | | Missing message id | Generate `message_id` yourself and send the same value in the request body | | Version mismatch | Align `conversation_key_version` with the key you use; align signing key version with `set_key_version` / your public-key record | diff --git a/xchat/xchat-xdk.mdx b/xchat/xchat-xdk.mdx index 723bb3b27..c25a60d23 100644 --- a/xchat/xchat-xdk.mdx +++ b/xchat/xchat-xdk.mdx @@ -129,9 +129,10 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send result, err := chat.DecryptEvents(rawEvents, signingKeys) cached := result.ConversationKeys.Keys event, err := chat.DecryptEvent(oneEventB64, cached, senderSigningKeys) + rawKey := cached[*result.ConversationKeys.LatestVersion] // raw []byte payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKeyB64, Text: "Hi!", + ConversationKey: rawKey, Text: "Hi!", ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion, }) _ = event @@ -183,7 +184,7 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send ## Lifecycle and keys -Construct the SDK, store private keys (Juicebox PIN or a local key blob), register **public** keys with the Chat API, and set your registered **public-key version** after unlock or import. Call `generate_keypairs` once per device/app identity; post the registration payload to the public-keys endpoint. Use `export_keys` / `import_keys` for bots and servers; use `setup` / `unlock` (and related PIN helpers) for client apps with Juicebox. +Construct the SDK, store private keys (Juicebox PIN or a local key blob), register **public** keys with the Chat API, and set your registered **public-key version** after unlock or import. Call `generate_keypairs` once per device/app identity; post the registration payload to the public-keys endpoint. Use `setup` / `unlock` (and related PIN helpers) for Juicebox PIN storage on every binding. `export_keys` / `import_keys` (raw key-blob persistence for bots and servers) are available on the **native bindings only**—Python, Go, .NET, JVM, and Rust. The JS/WASM binding does not expose raw key export or import: in a browser any script that reaches the instance could exfiltrate the identity, so JS keeps keys inside Juicebox. A JS server that wants to avoid a Juicebox round-trip per request should reuse one unlocked `Chat` instance across requests, or run a native binding where key blobs are supported. @@ -217,7 +218,8 @@ Construct the SDK, store private keys (Juicebox PIN or a local key blob), regist chat.setKeyVersion(version); const publics = chat.getPublicKeys(); - // Key blob path: create without Juicebox, then importKeys / exportKeys + // JS/WASM stores keys only through Juicebox — there is no raw key + // export/import here. For key-blob persistence, use a native binding. ``` @@ -270,7 +272,9 @@ Construct the SDK, store private keys (Juicebox PIN or a local key blob), regist -Optional: `set_reject_unverified` so decrypt fails when a signature cannot be verified; `update_config` if Juicebox realm config changes; `is_unlocked` / `has_identity_key` for UI state. Full field lists live in the [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) stubs. +The Juicebox config you pass at construction accepts three shapes: the X API `juicebox_config` object (recommended—passed verbatim), a full `sdk_config` wrapper, or a bare `token_map`. + +Optional: signature verification is **on by default** (`reject_unverified = true`)—call `set_reject_unverified(false)` to disable it (not recommended); `update_config` if Juicebox realm config changes; `is_unlocked` / `has_identity_key` for UI state. Full field lists live in the [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) stubs. --- @@ -284,7 +288,7 @@ Three **prepare** methods each make one call do everything a key change needs: g | Create a group (id minted by `POST /2/chat/conversations/group/initialize`) | `prepare_group_create` | 2—send both | | Add members to a group | `prepare_group_members_change` | 2—send both | -Keep the **raw** key bytes (Go: base64 of those bytes) for `encrypt_message` and media; never pass the API’s encrypted envelope into encrypt. +Keep the **raw** key bytes for `encrypt_message` and media; never pass the API’s encrypted envelope into encrypt. **Verify fetched keys before wrapping.** The prepare methods encrypt the fresh conversation key to whatever public keys you pass. Before passing them, call `verify_key_binding(identity, signing, signature)` on each fetched record—its `public_key`, `signing_public_key`, and `identity_public_key_signature` fields from the public-keys API—so a substituted identity key cannot receive the conversation key. @@ -315,7 +319,9 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, ```typescript - const prepared = chat.prepareConversationKeyChange(myUserId, signingKeyVersion, participants); + const prepared = chat.prepareConversationKeyChange({ + senderId: myUserId, signingKeyVersion, publicKeys: participants, + }); // prepared.conversationKey — Uint8Array for encryptMessage // prepared.participantKeys / prepared.actionSignatures — POST body fields @@ -328,7 +334,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, ```rust let prepared = chat.prepare_conversation_key_change( - &my_user_id, &signing_key_version, &participants, None, + ConversationKeyChangeParams::new(&my_user_id, &signing_key_version, participants), )?; let extracted = chat.extract_conversation_keys(&key_change_blobs); let latest = extracted.latest_version.as_deref().unwrap_or_default(); @@ -338,8 +344,10 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, ```go - prepared, err := chat.PrepareConversationKeyChange(myUserID, signingKeyVersion, participants, "") - // prepared.ConversationKey is base64 of the raw key for later EncryptMessage + prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{ + SenderID: myUserID, SigningKeyVersion: signingKeyVersion, PublicKeys: participants, + }) + // prepared.ConversationKey is the raw []byte key for later EncryptMessage // prepared.ParticipantKeys / prepared.ActionSignatures — POST body fields extracted, err := chat.ExtractConversationKeys(keyChangeBlobs) one, err := chat.DecryptConversationKey(encryptedBlob) @@ -351,7 +359,9 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, ```csharp - var prepared = chat.PrepareConversationKeyChange(myUserId, signingKeyVersion, participants); + var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams { + SenderId = myUserId, SigningKeyVersion = signingKeyVersion, PublicKeys = participants, + }); var extracted = chat.ExtractConversationKeys(keyChangeBlobs); var raw = extracted.Keys[extracted.LatestVersion]; var one = chat.DecryptConversationKey(encryptedBlob); @@ -359,8 +369,11 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, ```java - PreparedConversationChange prepared = chat.prepareConversationKeyChange( - myUserId, signingKeyVersion, participants, null); + ConversationKeyChangeParams keyParams = new ConversationKeyChangeParams(); + keyParams.senderId = myUserId; + keyParams.signingKeyVersion = signingKeyVersion; + keyParams.publicKeys = participants; + PreparedConversationChange prepared = chat.prepareConversationKeyChange(keyParams); ConversationKeyBundle extracted = chat.extractConversationKeys(keyChangeBlobs); byte[] raw = extracted.keys.get(extracted.latestVersion); byte[] one = chat.decryptConversationKey(encryptedBlob); @@ -376,7 +389,7 @@ For group create and member adds, pass the params each method needs (member/admi **`decrypt_events`** is for history and backlog: it pulls conversation keys from the stream, returns decrypted messages, and **collects** per-event errors instead of failing the whole batch. **`decrypt_event`** is for a single live event when you already have a key cache; it raises/throws on failure. -Pass **signing keys** so the SDK can verify senders. Map API public-key fields into `SigningKeyEntry`: `public_key_version` → `public_key_version` (same name), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, plus `identity_public_key_signature` and `user_id`. Omit or pass an empty list to skip verification (not recommended in production). +Pass **signing keys** so the SDK can verify senders. Map API public-key fields into `SigningKeyEntry`: `public_key_version` → `public_key_version` (same name), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, plus `identity_public_key_signature` and `user_id`. Verification is mandatory by default: omitting or passing an empty signing-key list does **not** skip it—signed events fail (collected in `errors` for `decrypt_events`, thrown for `decrypt_event`). To actually skip verification you must first call `set_reject_unverified(false)` (not recommended in production). @@ -390,8 +403,8 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i } for row in api_public_keys] result = chat.decrypt_events(raw_events, signing_keys) - for err in result.get("errors") or []: - log.warning("decrypt error: %s", err) + for idx, msg in (result.get("errors") or {}).items(): + log.warning("event %s failed: %s", idx, msg) for dm in result["messages"]: ev = dm["event"] if ev.get("type") == "Message": @@ -412,8 +425,8 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i })); const result = chat.decryptEvents(rawEvents, signingKeys); - for (const err of result.errors ?? []) { - console.warn('decrypt error', err); + for (const [idx, msg] of Object.entries(result.errors ?? {})) { + console.warn(`event ${idx} failed: ${msg}`); } const cached = result.conversationKeys.keys; const live = chat.decryptEvent(oneEventB64, cached, signingKeysForSender); @@ -422,8 +435,8 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i ```rust let result = chat.decrypt_events(&raw_events, &signing_keys); - for err in &result.errors { - eprintln!("decrypt error: {err:?}"); + for (idx, msg) in &result.errors { + eprintln!("event {idx} failed: {msg}"); } let cached = &result.conversation_keys.keys; let live = chat.decrypt_event(one_event_b64, cached, &signing_keys_for_sender)?; @@ -432,8 +445,8 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i ```go result, err := chat.DecryptEvents(rawEvents, signingKeys) - for _, e := range result.Errors { - log.Printf("decrypt error: %v", e) + for idx, msg := range result.Errors { + log.Printf("event %s failed: %s", idx, msg) } cached := result.ConversationKeys.Keys live, err := chat.DecryptEvent(oneEventB64, cached, signingKeysForSender) @@ -444,7 +457,7 @@ Pass **signing keys** so the SDK can verify senders. Map API public-key fields i ```csharp var result = chat.DecryptEvents(rawEvents, signingKeys); - foreach (var e in result.Errors) { /* log */ } + foreach (var kv in result.Errors) { /* kv.Key = event index, kv.Value = error */ } var cached = result.ConversationKeys.Keys; var live = chat.DecryptEvent(oneEventB64, cached, signingKeysForSender); ``` @@ -528,12 +541,12 @@ The conversation id passed to `encrypt_message` / `encrypt_reply` can be any for ```go payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKeyB64, Text: "Hello", + ConversationKey: rawKey, Text: "Hello", // rawKey is raw []byte ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion, }) // body: message_id, encoded_message_create_event, encoded_message_event_signature - nameCt, err := chat.Encrypt("Group title", convKeyB64) - title, err := chat.Decrypt(nameCt, convKeyB64) + nameCt, err := chat.Encrypt("Group title", rawKey) + title, err := chat.Decrypt(nameCt, rawKey) _ = payload _ = title _ = err @@ -579,44 +592,82 @@ Encrypt file bytes with the **same** conversation key used for text, upload via ```python - enc = chat.encrypt_stream(file_bytes, raw_conversation_key) - # enc["ciphertext"], enc["media_hash_key"] — upload ciphertext; pass media_hash_key into encrypt_message + ciphertext = chat.encrypt_stream(file_bytes, raw_conversation_key) + # Upload `ciphertext`; the `media_hash_key` you attach on encrypt_message + # comes from the media-upload finalize step, not from encrypt_stream. - plain = chat.decrypt_stream(ciphertext_bytes, raw_conversation_key, media_hash_key) + plain = chat.decrypt_stream(ciphertext, raw_conversation_key) ``` ```typescript - const enc = chat.encryptStream(fileBytes, rawConversationKey); - // enc.ciphertext, enc.mediaHashKey - const plain = chat.decryptStream(ciphertextBytes, rawConversationKey, mediaHashKey); + const ciphertext = chat.encryptStream(fileBytes, rawConversationKey); + // Upload `ciphertext`; mediaHashKey comes from the upload finalize step. + const plain = chat.decryptStream(ciphertext, rawConversationKey); ``` ```rust - let enc = chat.encrypt_stream(&file_bytes, &raw_key)?; - let plain = chat.decrypt_stream(&ciphertext, &raw_key, &media_hash_key)?; + let ciphertext = chat.encrypt_stream(&file_bytes, &raw_key)?; + let plain = chat.decrypt_stream(&ciphertext, &raw_key)?; ``` ```go - enc, err := chat.EncryptStream(fileBytes, convKeyB64) - plain, err := chat.DecryptStream(ciphertext, convKeyB64, mediaHashKey) - _ = enc + ciphertext, err := chat.EncryptStream(fileBytes, rawKey) // rawKey is raw []byte + plain, err := chat.DecryptStream(ciphertext, rawKey) _ = plain _ = err ``` ```csharp - var enc = chat.EncryptStream(fileBytes, rawKey); - var plain = chat.DecryptStream(ciphertext, rawKey, mediaHashKey); + var ciphertext = chat.EncryptStream(fileBytes, rawKey); + var plain = chat.DecryptStream(ciphertext, rawKey); ``` ```java - EncryptStreamResult enc = chat.encryptStream(fileBytes, rawKey); - byte[] plain = chat.decryptStream(ciphertext, rawKey, mediaHashKey); + byte[] ciphertext = chat.encryptStream(fileBytes, rawKey); + byte[] plain = chat.decryptStream(ciphertext, rawKey); + ``` + + + +### Incremental streaming for large media + +For large files, avoid holding the whole payload in memory: `stream_encryptor()` / `stream_decryptor()` return a `StreamEncryptor` / `StreamDecryptor` you feed in chunks (about 1 MB each) with `push(chunk)`, then call `finish()` once at the end. On decrypt, `finish()` detects a truncated stream (it fails if input ended before the final frame), so don't treat pushed plaintext as complete until it succeeds. + + +**JS/WASM only:** `finish()` consumes and frees the underlying WASM object—never call `free()` after `finish()` (it throws). Call `free()` only to abandon a stream *before* finishing (e.g. on an error path). + + + + + ```python + enc = chat.stream_encryptor(raw_conversation_key) + chunks = [enc.push(chunk) for chunk in read_in_chunks(file_bytes, 1 << 20)] + chunks.append(enc.finish()) + ciphertext = b"".join(chunks) + + dec = chat.stream_decryptor(raw_conversation_key) + out = [dec.push(chunk) for chunk in read_in_chunks(ciphertext, 1 << 20)] + out.append(dec.finish()) # raises on truncation + plain = b"".join(out) + ``` + + + ```typescript + const enc = chat.streamEncryptor(rawConversationKey); + const parts: Uint8Array[] = []; + try { + for (const chunk of readInChunks(fileBytes, 1 << 20)) parts.push(enc.push(chunk)); + parts.push(enc.finish()); // consumes + frees enc — do not call enc.free() after this + } catch (e) { + enc.free(); // only when abandoning before finish() + throw e; + } + const ciphertext = concat(parts); ``` @@ -630,20 +681,27 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- ```python - from chat_xdk import bytes_to_base64, base64_to_bytes, detect_mime_type, detect_image_dimensions + from chat_xdk import ( + bytes_to_base64, base64_to_bytes, bytes_to_hex, hex_to_bytes, + detect_mime_type, detect_image_dimensions, + ) b64 = bytes_to_base64(raw) raw2 = base64_to_bytes(b64) + hexed = bytes_to_hex(raw) + raw3 = hex_to_bytes(hexed) mime = detect_mime_type(file_bytes) w, h = detect_image_dimensions(file_bytes) ``` ```typescript - import { bytesToBase64, base64ToBytes, detectMimeType, detectImageDimensions } from '@xdevplatform/chat-xdk'; + import { bytesToBase64, base64ToBytes, bytesToHex, hexToBytes, detectMimeType, detectImageDimensions } from '@xdevplatform/chat-xdk'; const b64 = bytesToBase64(raw); const raw2 = base64ToBytes(b64); + const hexed = bytesToHex(raw); + const raw3 = hexToBytes(hexed); const mime = detectMimeType(fileBytes); const { width, height } = detectImageDimensions(fileBytes); ``` @@ -652,6 +710,8 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- ```rust let b64 = chat_xdk_core::bytes_to_base64(&raw); let raw2 = chat_xdk_core::base64_to_bytes(&b64)?; + let hexed = chat_xdk_core::bytes_to_hex(&raw); + let raw3 = chat_xdk_core::hex_to_bytes(&hexed); let mime = chat_xdk_core::detect_mime_type(&file_bytes); let (w, h) = chat_xdk_core::detect_image_dimensions(&file_bytes)?; ``` @@ -660,9 +720,13 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- ```go b64 := chatxdk.BytesToBase64(raw) raw2, err := chatxdk.Base64ToBytes(b64) + hexed, err := chatxdk.BytesToHex(raw) + raw3, err := chatxdk.HexToBytes(hexed) mime := chatxdk.DetectMimeType(fileBytes) w, h, err := chatxdk.DetectImageDimensions(fileBytes) _ = raw2 + _ = hexed + _ = raw3 _ = mime _ = w _ = h @@ -673,6 +737,8 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- ```csharp var b64 = ChatXdkUtilities.BytesToBase64(raw); var raw2 = ChatXdkUtilities.Base64ToBytes(b64); + var hexed = ChatXdkUtilities.BytesToHex(raw); + var raw3 = ChatXdkUtilities.HexToBytes(hexed); var mime = ChatXdkUtilities.DetectMimeType(fileBytes); var (w, h) = ChatXdkUtilities.DetectImageDimensions(fileBytes); ``` @@ -681,6 +747,8 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- ```java String b64 = ChatXdkUtilities.bytesToBase64(raw); byte[] raw2 = ChatXdkUtilities.base64ToBytes(b64); + String hexed = ChatXdkUtilities.bytesToHex(raw); + byte[] raw3 = ChatXdkUtilities.hexToBytes(hexed); String mime = ChatXdkUtilities.detectMimeType(fileBytes); int[] wh = ChatXdkUtilities.detectImageDimensions(fileBytes); ``` @@ -696,7 +764,7 @@ These conceptual types show up across languages (exact field names differ; JS of - **SendPayload** — return value of `encrypt_message` and related encrypt helpers; map into the Chat API send body. - **PublicKeyRegistrationPayload** — output of `generate_keypairs` / public-key getters for the add-public-key API. - **SigningKeyEntry** — sender public material passed into decrypt for signature verification. -- **PreparedConversationChange** — output of the three prepare methods: the derived or passed `conversation_id`, the raw `conversation_key` (Go: base64 string), `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`), and `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, optional `signature_payload`—omitted on key-change signatures because that payload embeds the plaintext key). +- **PreparedConversationChange** — output of the three prepare methods: the derived or passed `conversation_id`, the raw `conversation_key` bytes, `conversation_key_version`, `participant_keys` (`user_id`, `encrypted_key`, `public_key_version`), and `action_signatures` (`message_id`, `encoded_message_event_detail`, `signature`, `signature_version`, `public_key_version`, optional `signature_payload`—omitted on key-change signatures because that payload embeds the plaintext key). - **DecryptEventsResult** — messages, optional errors, and extracted `conversation_keys`. For complete field lists, use language stubs in the [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) (`docs/API.md`, `*.pyi`, `index.d.ts`). From 627b044bf23b496b579b4cebf265e0a1149b3b9a Mon Sep 17 00:00:00 2001 From: Santiago Medina Rolong Date: Wed, 15 Jul 2026 16:47:42 -0700 Subject: [PATCH 2/3] docs(xchat): fix review findings in edited API examples Address code-review findings on the API-accuracy pass: TypeScript encrypt_message/encrypt_reply/reactions take a single params object; Go import_keys takes raw bytes (decode the base64 env var first); Go utility calls honor their (value, error) arities; Java reads the public conversationKey field, not a getter; C#/Java/Rust image-dimension and conversation-key types match the SDK; width/height casts added where narrowing. --- xchat/getting-started.mdx | 27 ++++++++------- xchat/groups.mdx | 5 +-- xchat/media.mdx | 25 +++++++------- xchat/troubleshooting.mdx | 3 +- xchat/xchat-xdk.mdx | 69 ++++++++++++++++++++++++--------------- 5 files changed, 76 insertions(+), 53 deletions(-) diff --git a/xchat/getting-started.mdx b/xchat/getting-started.mdx index 9f647d09f..c9006fa69 100644 --- a/xchat/getting-started.mdx +++ b/xchat/getting-started.mdx @@ -178,7 +178,11 @@ Load private keys with **Juicebox** (PIN + `juicebox_config` from the API) or a chat := chatxdk.New() defer chat.Close() - if err := chat.ImportKeys(os.Getenv("PRIVATE_KEYS_B64")); err != nil { + blob, err := chatxdk.Base64ToBytes(os.Getenv("PRIVATE_KEYS_B64")) + if err != nil { + log.Fatal(err) + } + if err := chat.ImportKeys(blob); err != nil { log.Fatal(err) } signingKeyVersion := os.Getenv("SIGNING_KEY_VERSION") @@ -486,7 +490,8 @@ The response returns the canonical conversation id (`data.conversation_id`—the .json()?; // Canonical id for later requests let conversation_id = resp["data"]["conversation_id"].as_str().unwrap().to_string(); - let conv_key = prepared.conversation_key; + // conversation_key is Option; encrypt_message wants owned bytes + let conv_key = prepared.conversation_key.expect("key present").to_bytes(); let conv_key_version = prepared.conversation_key_version; ``` @@ -625,7 +630,7 @@ The response returns the canonical conversation id (`data.conversation_id`—the HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString()); JsonNode data = mapper.readTree(resp.body()).path("data"); String conversationId = data.path("conversation_id").asText(); // canonical id - byte[] convKey = prepared.getConversationKey(); + byte[] convKey = prepared.conversationKey; String convKeyVersion = prepared.conversationKeyVersion; ``` @@ -676,15 +681,15 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (` import { randomUUID } from 'crypto'; const messageId = randomUUID(); - const payload = chat.encryptMessage( + const payload = chat.encryptMessage({ messageId, - 'YOUR_USER_ID', - 'CONVERSATION_ID', - convKey, - 'Hello!', - convKeyVersion, + senderId: 'YOUR_USER_ID', + conversationId: 'CONVERSATION_ID', + conversationKey: convKey, + text: 'Hello!', + conversationKeyVersion: convKeyVersion, signingKeyVersion, - ); + }); await client.chat.sendMessage('RECIPIENT_USER_ID', { message_id: messageId, encoded_message_create_event: payload.encryptedContent, @@ -701,7 +706,7 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (` &message_id, &sender_id, &conversation_id, - &conv_key, + conv_key, "Hello!", &conv_key_version, &signing_key_version, diff --git a/xchat/groups.mdx b/xchat/groups.mdx index 48383a3a6..bf291f95f 100644 --- a/xchat/groups.mdx +++ b/xchat/groups.mdx @@ -139,8 +139,9 @@ Whether a given field is stored encrypted is decided by the client that writes i ```rust - let group_name = chat.decrypt(&conversation_group_name_b64, &raw_conv_key)?; - let encrypted_name = chat.encrypt("Project team", &raw_conv_key)?; + // conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key + let group_name = chat.decrypt(&conversation_group_name_b64, &conv_key)?; + let encrypted_name = chat.encrypt("Project team", &conv_key)?; ``` diff --git a/xchat/media.mdx b/xchat/media.mdx index 693e9acf8..8274eca23 100644 --- a/xchat/media.mdx +++ b/xchat/media.mdx @@ -62,7 +62,8 @@ flowchart LR let _mime = detect_mime_type(&plaintext); let dims = detect_image_dimensions(&plaintext); let (width, height) = dims.map(|d| (d.width as i64, d.height as i64)).unwrap_or((0, 0)); - let encrypted_blob = chat.encrypt_stream(&plaintext, &raw_conv_key)?; + // conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key + let encrypted_blob = chat.encrypt_stream(&plaintext, &conv_key)?; ``` @@ -83,8 +84,8 @@ flowchart LR byte[] plaintext = await File.ReadAllBytesAsync("photo.jpg"); string? mime = ChatXdkUtilities.DetectMimeType(plaintext); var dims = ChatXdkUtilities.DetectImageDimensions(plaintext); - int width = dims?.Width ?? 0; - int height = dims?.Height ?? 0; + int width = (int)(dims?.Width ?? 0); + int height = (int)(dims?.Height ?? 0); byte[] encryptedBlob = chat.EncryptStream(plaintext, rawConvKey); ``` @@ -96,8 +97,8 @@ flowchart LR byte[] plaintext = Files.readAllBytes(Path.of("photo.jpg")); String mime = ChatXdkUtilities.detectMimeType(plaintext); ImageDimensions dims = ChatXdkUtilities.detectImageDimensions(plaintext); - int width = dims != null ? dims.width : 0; - int height = dims != null ? dims.height : 0; + int width = dims != null ? (int) dims.width : 0; + int height = dims != null ? (int) dims.height : 0; byte[] encryptedBlob = chat.encryptStream(plaintext, rawConvKey); ``` @@ -160,16 +161,15 @@ Encrypt with a media attachment, then POST the send-message body (same field map ```typescript const messageId = crypto.randomUUID(); - const payload = chat.encryptMessage( + const payload = chat.encryptMessage({ messageId, senderId, conversationId, - rawConvKey, - caption || '', + conversationKey: rawConvKey, + text: caption || '', conversationKeyVersion, signingKeyVersion, - null, - [{ + attachments: [{ attachmentType: 'media', mediaHashKey: mediaHashKey, width, @@ -177,7 +177,7 @@ Encrypt with a media attachment, then POST the send-message body (same field map filesizeBytes: plaintext.byteLength, filename: 'photo.jpg', }], - ); + }); await client.chat.sendMessage(conversationId.replace(/:/g, '-'), { message_id: messageId, encoded_message_create_event: payload.encryptedContent, @@ -314,7 +314,8 @@ Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl .header("Authorization", &auth) .send()? .bytes()?; - let plaintext = chat.decrypt_stream(&encrypted_blob, &raw_conv_key)?; + // conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key + let plaintext = chat.decrypt_stream(&encrypted_blob, &conv_key)?; ``` diff --git a/xchat/troubleshooting.mdx b/xchat/troubleshooting.mdx index 3c6cba332..4d1520e55 100644 --- a/xchat/troubleshooting.mdx +++ b/xchat/troubleshooting.mdx @@ -87,7 +87,8 @@ Load private keys first, then set the public-key **version** from your record on ```go - _ = chat.ImportKeys(privateKeysB64) + blob, _ := chatxdk.Base64ToBytes(privateKeysB64) + _ = chat.ImportKeys(blob) chat.SetKeyVersion(signingKeyVersion) ``` diff --git a/xchat/xchat-xdk.mdx b/xchat/xchat-xdk.mdx index c25a60d23..308d57e75 100644 --- a/xchat/xchat-xdk.mdx +++ b/xchat/xchat-xdk.mdx @@ -101,10 +101,10 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send const event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys); const rawKey = cached[result.conversationKeys.latestVersion!]; - const payload = chat.encryptMessage( - messageId, senderId, conversationId, rawKey, 'Hi!', + const payload = chat.encryptMessage({ + messageId, senderId, conversationId, conversationKey: rawKey, text: 'Hi!', conversationKeyVersion, signingKeyVersion, - ); + }); ``` @@ -113,8 +113,11 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send let result = chat.decrypt_events(&raw_events, &signing_keys); let cached = &result.conversation_keys.keys; let event = chat.decrypt_event(one_event_b64, cached, &sender_signing_keys)?; + // cached values are XChatConversationKey; encrypt_message wants owned bytes + let latest = result.conversation_keys.latest_version.as_deref().unwrap_or_default(); + let conv_key = cached[latest].to_bytes(); let payload = chat.encrypt_message(EncryptMessageParams::new( - &message_id, &sender_id, &conversation_id, &raw_key, "Hi!", + &message_id, &sender_id, &conversation_id, conv_key, "Hi!", &conversation_key_version, &signing_key_version, ))?; ``` @@ -123,7 +126,8 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send ```go chat := chatxdk.New() defer chat.Close() - _ = chat.ImportKeys(privateKeysB64) + blob, _ := chatxdk.Base64ToBytes(privateKeysB64) + _ = chat.ImportKeys(blob) chat.SetKeyVersion(signingKeyVersion) result, err := chat.DecryptEvents(rawEvents, signingKeys) @@ -239,7 +243,8 @@ Construct the SDK, store private keys (Juicebox PIN or a local key blob), regist defer chat.Close() // Prefer ImportKeys for servers; Juicebox unlock where supported - if err := chat.ImportKeys(privateKeysB64); err != nil { + keyBlob, _ := chatxdk.Base64ToBytes(privateKeysB64) + if err := chat.ImportKeys(keyBlob); err != nil { log.Fatal(err) } chat.SetKeyVersion(version) @@ -272,7 +277,7 @@ Construct the SDK, store private keys (Juicebox PIN or a local key blob), regist -The Juicebox config you pass at construction accepts three shapes: the X API `juicebox_config` object (recommended—passed verbatim), a full `sdk_config` wrapper, or a bare `token_map`. +The Juicebox config accepts three shapes: the X API `juicebox_config` object (recommended—passed verbatim), a full `sdk_config` wrapper, or a bare `token_map`. Optional: signature verification is **on by default** (`reject_unverified = true`)—call `set_reject_unverified(false)` to disable it (not recommended); `update_config` if Juicebox realm config changes; `is_unlocked` / `has_identity_key` for UI state. Full field lists live in the [chat-xdk repo](https://github.com/xdevplatform/chat-xdk) stubs. @@ -507,34 +512,35 @@ The conversation id passed to `encrypt_message` / `encrypt_reply` can be any for ```typescript - const payload = chat.encryptMessage( - messageId, senderId, conversationId, rawConversationKey, 'Hello', + const payload = chat.encryptMessage({ + messageId, senderId, conversationId, conversationKey: rawConversationKey, text: 'Hello', conversationKeyVersion, signingKeyVersion, - ); + }); const body = { message_id: messageId, encoded_message_create_event: payload.encryptedContent, encoded_message_event_signature: payload.encodedEventSignature, }; - const reply = chat.encryptReply( - replyMessageId, senderId, conversationId, rawConversationKey, - 'Sounds good', conversationKeyVersion, signingKeyVersion, - parentSequenceId, // replyToSequenceId — the message being replied to - ); + const reply = chat.encryptReply({ + messageId: replyMessageId, senderId, conversationId, conversationKey: rawConversationKey, + text: 'Sounds good', conversationKeyVersion, signingKeyVersion, + replyToSequenceId: parentSequenceId, // the message being replied to + }); const nameCt = chat.encrypt('Group title', rawConversationKey); const title = chat.decrypt(nameCt, rawConversationKey); ``` ```rust + // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key let payload = chat.encrypt_message(EncryptMessageParams::new( - &message_id, &sender_id, &conversation_id, &raw_key, "Hello", + &message_id, &sender_id, &conversation_id, conv_key.to_bytes(), "Hello", &conversation_key_version, &signing_key_version, ))?; // Map payload fields into the send-message JSON body as above - let name_ct = chat.encrypt("Group title", &raw_key)?; - let title = chat.decrypt(&name_ct, &raw_key)?; + let name_ct = chat.encrypt("Group title", &conv_key)?; + let title = chat.decrypt(&name_ct, &conv_key)?; ``` @@ -608,8 +614,9 @@ Encrypt file bytes with the **same** conversation key used for text, upload via ```rust - let ciphertext = chat.encrypt_stream(&file_bytes, &raw_key)?; - let plain = chat.decrypt_stream(&ciphertext, &raw_key)?; + // conv_key: &XChatConversationKey from extract_conversation_keys / decrypt_conversation_key + let ciphertext = chat.encrypt_stream(&file_bytes, &conv_key)?; + let plain = chat.decrypt_stream(&ciphertext, &conv_key)?; ``` @@ -703,7 +710,9 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- const hexed = bytesToHex(raw); const raw3 = hexToBytes(hexed); const mime = detectMimeType(fileBytes); - const { width, height } = detectImageDimensions(fileBytes); + const dims = detectImageDimensions(fileBytes); + const width = dims?.width ?? 0; + const height = dims?.height ?? 0; ``` @@ -713,17 +722,20 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- let hexed = chat_xdk_core::bytes_to_hex(&raw); let raw3 = chat_xdk_core::hex_to_bytes(&hexed); let mime = chat_xdk_core::detect_mime_type(&file_bytes); - let (w, h) = chat_xdk_core::detect_image_dimensions(&file_bytes)?; + let dims = chat_xdk_core::detect_image_dimensions(&file_bytes); + let (w, h) = dims.map(|d| (d.width, d.height)).unwrap_or((0, 0)); ``` ```go - b64 := chatxdk.BytesToBase64(raw) + b64, _ := chatxdk.BytesToBase64(raw) raw2, err := chatxdk.Base64ToBytes(b64) hexed, err := chatxdk.BytesToHex(raw) raw3, err := chatxdk.HexToBytes(hexed) - mime := chatxdk.DetectMimeType(fileBytes) - w, h, err := chatxdk.DetectImageDimensions(fileBytes) + mime, _ := chatxdk.DetectMimeType(fileBytes) + dims, _ := chatxdk.DetectImageDimensions(fileBytes) + w, h := dims.Width, dims.Height + _ = b64 _ = raw2 _ = hexed _ = raw3 @@ -740,7 +752,9 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- var hexed = ChatXdkUtilities.BytesToHex(raw); var raw3 = ChatXdkUtilities.HexToBytes(hexed); var mime = ChatXdkUtilities.DetectMimeType(fileBytes); - var (w, h) = ChatXdkUtilities.DetectImageDimensions(fileBytes); + var dims = ChatXdkUtilities.DetectImageDimensions(fileBytes); + var w = dims?.Width ?? 0; + var h = dims?.Height ?? 0; ``` @@ -750,7 +764,8 @@ Base64/hex helpers, MIME sniffing, and image dimensions are available as module- String hexed = ChatXdkUtilities.bytesToHex(raw); byte[] raw3 = ChatXdkUtilities.hexToBytes(hexed); String mime = ChatXdkUtilities.detectMimeType(fileBytes); - int[] wh = ChatXdkUtilities.detectImageDimensions(fileBytes); + ImageDimensions wh = ChatXdkUtilities.detectImageDimensions(fileBytes); + long width = wh.width, height = wh.height; ``` From 7c8f76728174a7e942e69192899a1f2b5d2bb851 Mon Sep 17 00:00:00 2001 From: Santiago Medina Rolong Date: Wed, 15 Jul 2026 17:07:01 -0700 Subject: [PATCH 3/3] docs(xchat): drop history-leaking asides from key examples State the conversation key as plain raw bytes without phrasing that implies a prior encoding, and remove code comments that only restate a variable's type. --- xchat/getting-started.mdx | 6 +++--- xchat/groups.mdx | 2 +- xchat/media.mdx | 6 +++--- xchat/troubleshooting.mdx | 2 +- xchat/xchat-xdk.mdx | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/xchat/getting-started.mdx b/xchat/getting-started.mdx index c9006fa69..464cb4852 100644 --- a/xchat/getting-started.mdx +++ b/xchat/getting-started.mdx @@ -539,7 +539,7 @@ The response returns the canonical conversation id (`data.conversation_id`—the req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) // Response data.conversation_id is the canonical id for later requests - // prepared.ConversationKey is the raw []byte key — pass to EncryptMessage + // prepared.ConversationKey feeds EncryptMessage _ = resp ``` @@ -640,7 +640,7 @@ The response returns the canonical conversation id (`data.conversation_id`—the ## 5. Send a message -Encrypt with the **raw** conversation key (raw bytes in every binding, including Go). On the send request, map: +Encrypt with the **raw** conversation key bytes. On the send request, map: | Chat XDK field | Request body field | |:---------------|:-------------------| @@ -730,7 +730,7 @@ Use a **hyphenated** conversation id in the URL path when the API requires it (` MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: convKey, // raw []byte from prepared.ConversationKey + ConversationKey: convKey, Text: "Hello!", ConversationKeyVersion: convKeyVersion, SigningKeyVersion: signingKeyVersion, diff --git a/xchat/groups.mdx b/xchat/groups.mdx index bf291f95f..7b882a4d5 100644 --- a/xchat/groups.mdx +++ b/xchat/groups.mdx @@ -146,7 +146,7 @@ Whether a given field is stored encrypted is decided by the client that writes i ```go - groupName, err := chat.Decrypt(conversationGroupNameB64, rawConvKey) // rawConvKey is raw []byte + groupName, err := chat.Decrypt(conversationGroupNameB64, rawConvKey) encryptedName, err := chat.Encrypt("Project team", rawConvKey) _ = groupName _ = encryptedName diff --git a/xchat/media.mdx b/xchat/media.mdx index 8274eca23..b074e065f 100644 --- a/xchat/media.mdx +++ b/xchat/media.mdx @@ -72,7 +72,7 @@ flowchart LR mime, _ := chatxdk.DetectMimeType(plaintext) dims, _ := chatxdk.DetectImageDimensions(plaintext) _ = mime - encrypted, err := chat.EncryptStream(plaintext, rawConvKey) // both raw []byte + encrypted, err := chat.EncryptStream(plaintext, rawConvKey) _ = dims _ = encrypted ``` @@ -205,7 +205,7 @@ Encrypt with a media attachment, then POST the send-message body (same field map ```go payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: rawConvKey, Text: caption, // rawConvKey is raw []byte + ConversationKey: rawConvKey, Text: caption, ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion, Attachments: []chatxdk.AttachmentDescriptor{{ AttachmentType: "media", @@ -325,7 +325,7 @@ Path: [`GET /2/chat/media/{conversation_id}/{media_hash_key}`](/x-api/chat/downl req, _ := http.NewRequest(http.MethodGet, url, nil) req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := http.DefaultClient.Do(req) - // read body into []byte → chat.DecryptStream(encryptedBlob, rawConvKey) — both raw []byte + // read body into []byte → chat.DecryptStream(encryptedBlob, rawConvKey) _ = resp _ = err ``` diff --git a/xchat/troubleshooting.mdx b/xchat/troubleshooting.mdx index 4d1520e55..75f9e99f7 100644 --- a/xchat/troubleshooting.mdx +++ b/xchat/troubleshooting.mdx @@ -182,7 +182,7 @@ These mistakes are specific to X Chat encryption (not general HTTP errors): | Issue | Fix | |:------|:----| -| Wrong key bytes | Pass the **raw** conversation key bytes into the Chat XDK (raw bytes in every binding, including Go), not the encrypted key string from the API | +| Wrong key bytes | Pass the **raw** conversation key bytes into the Chat XDK, not the encrypted key string from the API | | Wrong JSON field names | Map `encrypted_content` → `encoded_message_create_event` and `encoded_event_signature` → `encoded_message_event_signature` | | Missing message id | Generate `message_id` yourself and send the same value in the request body | | Version mismatch | Align `conversation_key_version` with the key you use; align signing key version with `set_key_version` / your public-key record | diff --git a/xchat/xchat-xdk.mdx b/xchat/xchat-xdk.mdx index 308d57e75..dafcd4297 100644 --- a/xchat/xchat-xdk.mdx +++ b/xchat/xchat-xdk.mdx @@ -133,7 +133,7 @@ Decrypt a backlog, cache keys, decrypt one event, encrypt a reply. Wire the send result, err := chat.DecryptEvents(rawEvents, signingKeys) cached := result.ConversationKeys.Keys event, err := chat.DecryptEvent(oneEventB64, cached, senderSigningKeys) - rawKey := cached[*result.ConversationKeys.LatestVersion] // raw []byte + rawKey := cached[*result.ConversationKeys.LatestVersion] payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ MessageID: messageID, SenderID: senderID, ConversationID: conversationID, ConversationKey: rawKey, Text: "Hi!", @@ -352,7 +352,7 @@ Use `extract_conversation_keys` on key-change event payloads to rebuild `{ keys, prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{ SenderID: myUserID, SigningKeyVersion: signingKeyVersion, PublicKeys: participants, }) - // prepared.ConversationKey is the raw []byte key for later EncryptMessage + // prepared.ConversationKey feeds EncryptMessage // prepared.ParticipantKeys / prepared.ActionSignatures — POST body fields extracted, err := chat.ExtractConversationKeys(keyChangeBlobs) one, err := chat.DecryptConversationKey(encryptedBlob) @@ -547,7 +547,7 @@ The conversation id passed to `encrypt_message` / `encrypt_reply` can be any for ```go payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{ MessageID: messageID, SenderID: senderID, ConversationID: conversationID, - ConversationKey: rawKey, Text: "Hello", // rawKey is raw []byte + ConversationKey: rawKey, Text: "Hello", ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion, }) // body: message_id, encoded_message_create_event, encoded_message_event_signature @@ -621,7 +621,7 @@ Encrypt file bytes with the **same** conversation key used for text, upload via ```go - ciphertext, err := chat.EncryptStream(fileBytes, rawKey) // rawKey is raw []byte + ciphertext, err := chat.EncryptStream(fileBytes, rawKey) plain, err := chat.DecryptStream(ciphertext, rawKey) _ = plain _ = err