Repository navigation
chat-xdk: sign, check replies and retry feature reads as the native clients do: v1 chats interoperate with X clients - #76
santiagomed wants to merge 30 commits into
Conversation
…every binding The core keeps the whole policy: a read that fails, times out or answers anything but a 200 features object is retried after 1 s, then after twice the previous wait, never more than 5 minutes, at most 10 times, and the first answer that is a features object holds for the chat's lifetime. Chats created while a read is in flight or waiting for its retry share it. Each binding performs the GET and every retry the core hands back after its delay_ms; Rust and C ABI callers hand over a later answer after a failed one. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…signed null When the native clients remove a group's title, description or avatar they send the change without the value and sign it as the string null. The verifier rebuilt an empty string, so decrypt_event rejected those changes under the default policy. It now rebuilds null for an absent value and keeps an empty one empty, and the decrypted change carries an empty value. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…versions as integers As the native clients do, a signature that carries no version, or one that is not an integer, no longer verifies (it was read as version 7), and a message's conversation-key version signs as its integer value, or null, at every version, not only from 8. A key change's version signs as its integer value too, and a legacy key change signs only the participant fields it carries. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
… and leaves included While v1_send is on, key changes, group creates, member adds, removals and leaves, deletes, read receipts, settings, invites, admin, join and metadata changes all sign at signature version 10, as messages do, in the layout of version 10: a group create signs its group type and TTL, and a member add its description slot. While it is off each signs at its legacy version: 7, or 8 for a member add that carries a description, a member removal and a leave included. Every action is signed over the payload the verifier rebuilds from its event, so the signer and the verifier share one layout per version. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…ng them A reply still opens its original with the key changes passed in reply_to_ckces, but no longer embeds them as raw_event_ckces, and a received preview's original opens only with the keys the receiver holds or the message key the reply carries, as the native clients do. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…ed text and its exact entities A preview's text must match the first 140 characters of the quoted text, counted in UTF-16 code units as the native clients count them, an absent text counting as empty, and its entity list must be the quoted message's exactly. A shorter prefix or a subset of the entities no longer passes. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…ttachments An attachment without an id derives its key from its position, and the native clients leave payment receipts out of a message's attachments before numbering them, so a file after a receipt takes the receipt's position. Other attachments the SDK does not surface keep theirs. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…s it quotes As the native clients do, a v1 send's signature carries message_signing_key_info_list: the author and signing-key version of a reply's quoted original and edit, then the sender's own, each once, for the X API to fill in the keys a recipient needs to verify the quoted events. v0 sends carry no list, as 0.5.x sent them. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…ents do While sends are v1, a reaction asks for a notification only in a one-to-one conversation, removing a reaction never does, and an edit never does: the native clients notify on an edit only behind a switch of their own, which the SDK does not have. v0 reactions and edits keep should_notify true, as 0.5.x sent them. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…mat's version The binding suites expect a member removal and a group leave at signature version 7 while sends are v0, and each checks a leave signs its removal and conversation delete at version 10 once v1_send is on. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
A valid answer with every switch off holds like any other; only a failed or late response is retried or leaves a chat open to a later answer. Go's constructors no longer say the switches are read once. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
PREVIEW_TEXT_UNITS counts UTF-16 code units, the unit the comparison takes the first 140 of. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…r send-format version A removal names the new key version without signing it, a leave's conversation_key_version is named on its removal, a member add signs at 10 while sends are v1, and Rust callers retry a feature read that fails. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…hecks into the 0.7 guides Every guide and why.md say where each topic already lives: actions sign at the send format's version (removals and leaves included, so only a member add with a description needs readers upgraded first while sends are v0), a failed feature read is retried with bounded backoff, a v1 payload lists its signing keys and asks for notifications as X clients' do, a reply preview must quote its message exactly and carries no key change, and a change that removes a title, description or avatar decrypts to an empty value. Install lines and snippet markers pin 0.7.2. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
…signing and retried feature reads API.md and CRYPTO.md give every action's version (7 while sends are v0, 10 while they are v1), the removal's unsigned key version, integer key versions and null for a removed group value at every version, payment receipts' place in attachment numbering, and the exact reply-preview check without embedded key changes. ARCHITECTURE.md, the READMEs and AGENTS.md describe the bounded retry of a failed feature-switch read. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
… parity fixes The changes since 0.7.1 go under Unreleased; the entries that shipped in 0.7.1 move under its own heading. Co-authored-by: Santiago Medina <santiagomed@users.noreply.github.com>
|
|
|
I have read the CLA Document and I hereby sign the CLA 1 out of 2 committers have signed the CLA. |
… explicit fields A v1 send the caller builds without the original event can now say what X clients need, in every binding: - EncryptReplyParams.reply_to_message_id names the quoted message by its signed message_id. X clients mark a v1 reply that embeds no message-key original and names no original invalid; with reply_to_event the id comes from the event and the field overrides it. - EncryptEditParams.target_message_id names the edited message. A v1 edit names the message it edits; with target_event the id comes from the event and the field overrides it. A v0 edit names none, as in 0.5.x. - A media AttachmentDescriptor takes plaintext_sha256, the base64 SHA-256 of the file's plaintext bytes, which the message carries and signs. The message is built before its files are encrypted, so the caller hashes the file. One that is not the base64 of 32 bytes fails the send with an invalid-input error. Decrypted media attachments return it. The JVM and .NET add an eight-argument media factory so existing callers keep compiling. Rust's AttachmentDescriptor::Media gains a field, so a struct literal of it adds plaintext_sha256.
…target_message_id and plaintext_sha256
…reply by sequence id can name the original's id
- features: a chat that joins after a GET's deadline starts a GET that also answers the chats still waiting on the expired one, which used to be dropped with their retries. A GET a binding could not start (`bindings::abandon_fetch`, used by Python when its thread will not start) schedules no retry, and the next chat's GET answers its chats; before, a phantom retry took every chat created in the next 3 s. - signatures: a conversation-key change never signs below the highest version of a key change the chat signed or verified in that conversation. A chat sends v0 until its feature read answers, so it could sign a key change at 7 after one at 10, which X clients that enforce a key-change signature-version floor reject as a downgrade. - replies, edits: a `reply_to_message_id` or `target_message_id` that is not the message id the reply's or edit's event carries fails with InvalidInput; recipients reject a preview whose id is not its embedded original's. Every binding tests the error. - reply validation compares the preview's entities with the quoted message's without copying them. - comments state the rule instead of pointing at other clients. - docs/API.md types the descriptor's `plaintext_sha256` as `string | null`, as index.d.ts does.
…t; audit comments and docs Feature switches - A chat reads GET /2/chat/features again 5 minutes after each answer, so a change to v1_send reaches running chats without a restart or a new chat. Each valid answer replaces the one before; a failed read keeps the current answer and is retried after 1 s, doubling, capped at 5 minutes, while a chat waits. Reads stop once no chat sharing them is alive. - With a session identity the request carries ?bucket=NNNN, the last four digits of the user id, so the gateway buckets a sender the same way wherever it runs; without one it buckets by address as before. Every binding's set_identity starts the read for the new URL. Chats that read the same URL share one read, and a chat that joins takes its latest answer at once. A repeated start rejoins a read its binding abandoned. - Rust and the C ABI add feature_switches_url / chat_xdk_feature_switches_url and FEATURES_REFRESH_MS / CHAT_XDK_FEATURES_REFRESH_MS; each answer handed to set_feature_switches replaces the one before. The internal configure hook takes refresh_ms. The Rust example bot reads its sender's URL and refreshes and retries on that schedule. - A prepare call that signs several actions reads the send format once, so a refresh between its signatures can't split them across versions. Audit - Comments describe local behavior, keep siblings symmetric and match the code; stale text about one-time reads, ten retries and query-free requests is gone from code, READMEs, guides, API, ARCHITECTURE, CRYPTO, AGENTS.md and the CHANGELOG. - Guides cover the key-change signature floor, the edit-side target_message_id, and the invalid-input error for an explicit id that differs from the event's.
Summary
Brings the SDK's v1 behavior in line with the native clients in every binding (Rust, Python, JS/WASM, Go, JVM, .NET, C ABI). Every fix lands once in
crates/core. The bindings gain the retry loop for the feature-switch read, three optional fields a v1 send needs when the caller holds no original event (see New optional fields), and their parallel tests. Nothing else in the public API changes.nullin the value's place.raw_event_ckces), and decrypt never reads them from a reply.message_signing_key_info_list): the sender's, plus the quoted message's and its edit's for a reply.set_feature_switches/chat_xdk_set_feature_switchesaccept a later answer after a failed one.why.md, with install pins moved to 0.7.2. API.md (including how to hash a file in each binding), CRYPTO.md, ARCHITECTURE.md, the READMEs, AGENTS.md (the bounded retry replaces the fetch-once rule) and the CHANGELOG are updated too. The guides now build a reply from explicit fields withreply_to_message_id.Before / after
New optional fields
Each one is optional in every binding, and callers that don't set it build the same sends as before.
reply_to_message_idonEncryptReplyParams(replyToMessageIdin JS and on the JVM,ReplyToMessageIDin Go,ReplyToMessageIdin .NET). It names the quoted message by its signedmessage_id. X clients mark a v1 reply that embeds no message-key original and names no original invalid, so a reply built from the explicit preview fields needs it. Withreply_to_event, the id comes from the event and the field overrides it.target_message_idonEncryptEditParams(targetMessageIdin JS and on the JVM,TargetMessageIDin Go,TargetMessageIdin .NET). It names the edited message. Withtarget_event, the id comes from the event and the field overrides it. Withouttarget_event, the SDK has only the target's sequence id, so a v1 edit built fromconversation_idandtarget_message_sequence_idneeds it. A v0 edit names none, as in 0.5.x.plaintext_sha256on the media attachment descriptor. It is the base64 SHA-256 of the file's plaintext bytes, which the message carries and signs. The message is built beforeencrypt_filesees the file, so the caller hashes the file. A value that is not the base64 of 32 bytes fails the send with an invalid-input error that names it. Decrypted media attachments return it asplaintext_sha256(plaintextSha256in JS), ornullwhen the sender sent none. The JVM and .NET get an eight-argumentmedia/Mediafactory, so existing calls keep compiling. Go getsAttachmentDescriptor.PlaintextSHA256.Rust source change:
AttachmentDescriptor::Mediagains theplaintext_sha256: Option<String>field. Code that builds the variant with a struct literal must addplaintext_sha256: None, and a pattern that lists every field must add it or end in... The CHANGELOG notes this.Notes for review
Testing
make cipasses: fmt, clippy-D warnings, cargo-deny licenses, workspace tests and the bindings gate.The fuzz targets compile.
Locally, the Python, JS/WASM, Go, JVM and C ABI suites pass, each with a new test that a leave signs at 7, then at 10 once v1 is on. The .NET suite (updated the same way) couldn't restore packages here, so CI runs it.
The migration-guide snippet checks pass for all seven languages.
The 0.5.0 / 0.5.2 compat check passes unchanged.
New core tests:
The new fields have core tests for v0 and v1 sends, overrides of the derived ids, the decrypted hash, and invalid hashes in a message, a reply and a reply preview. There is a C ABI invalid-input case, and every binding has a round trip that reads each field back from the decrypted send.
For that change,
cargo test --workspace, clippy-D warnings, fmt, the Go and Python suites, and the Rust, Python, TypeScript, Go and C guide snippet checks pass locally. The JS, JVM and .NET suites run in CI.