> ## Documentation Index
> Fetch the complete documentation index at: https://x-preview-mintlify-d8d2882f.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat XDK リファレンス

> 対応する各言語で X Chat の鍵管理、暗号化、復号、署名を処理する暗号化 SDK である Chat XDK のリファレンス。

**Chat XDK** は X Chat の鍵管理、暗号化、復号、署名を処理します。X の HTTP API を呼び出すことは**ありません**——[Python](/xdks/python/overview) または [TypeScript](/xdks/typescript/overview) の **XDK**、あるいは HTTPS とユーザーアクセストークンと組み合わせてください。

アプリの解説:[はじめに](/ja/xchat/getting-started)。サンプルボット:[chat-xdk/examples](https://github.com/xdevplatform/chat-xdk/tree/main/examples)。

### インストール

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install chatxdk
    ```

    PyPI のパッケージ名は `chatxdk` で、`chat_xdk` としてインポートします。Python 3.10 以上が必要です。
  </Tab>

  <Tab title="TypeScript">
    ```bash theme={null}
    npm install @xdevplatform/chat-xdk
    npm install juicebox-sdk   # optional peer dependency — required for setup()/unlock() secure key backup
    ```

    コンパイル済みの WASM エンジンはパッケージに同梱されています——ビルドステップは不要です。Node.js 18 以上が必要です。
  </Tab>

  <Tab title="Rust">
    ```toml theme={null}
    [dependencies]
    # chat-xdk-core is not yet on crates.io — use the git dependency.
    # It exports both ChatCore and the async secure-key-backup Chat type.
    chat-xdk-core = { git = "https://github.com/xdevplatform/chat-xdk", tag = "v0.4.0" }

    # Required until thrift 0.24 is released on crates.io
    [patch.crates-io]
    thrift = { git = "https://github.com/apache/thrift.git", rev = "deb36fa409849de45973b04ffc3ce49d277ca90a" }
    ```
  </Tab>

  <Tab title="Go">
    ```bash theme={null}
    go get github.com/xdevplatform/chat-xdk/go/chatxdk
    ```

    プリコンパイル済みの静的ライブラリが同梱されています(macOS arm64/amd64、Linux amd64 glibc/musl)——C コンパイラは必要ですが Rust は不要です。Go 1.21 以上が必要です。
  </Tab>

  <Tab title="C#">
    ```bash theme={null}
    dotnet add package XDevPlatform.ChatXdk
    ```

    パッケージは自己完結型です:macOS(arm64, x64)、Linux(x64)、Windows(x64)向けのネイティブライブラリが内包されています。.NET 8 以上が必要です。
  </Tab>

  <Tab title="Java">
    ```xml theme={null}
    <dependency>
      <groupId>com.x</groupId>
      <artifactId>chatxdk</artifactId>
      <version>0.4.0</version>
    </dependency>
    ```

    Maven Central で提供されています。jar には macOS(arm64, x64)、Linux(x64)、Windows(x64)向けのネイティブライブラリが同梱されており、`jna.library.path` のセットアップは不要です。`com.x.chatxdk` からインポートします。JDK 17 以上が必要です。
  </Tab>
</Tabs>

***

## クイックスタート

鍵をロードし、アイデンティティを一度セットし、バックログを復号し、ライブイベントを 1 つ復号し、メッセージを暗号化します。送信ボディは、[はじめに](/ja/xchat/getting-started) と同じように [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message) に配線してください。

スニペットは 2 つの**オプション**セッションストアを使って最も短い呼び出し形式を使用します:`set_signing_keys` は他の参加者の公開鍵を保持([public-keys エンドポイント](/x-api/chat/get-user-public-keys) から取得)して、復号呼び出しが呼び出し単位の引数なしに送信者を検証できるようにし、`set_cache_keys(true)` は SDK が各会話の検証済み鍵を記憶できるようにして、暗号化呼び出しには会話 ID とテキストだけあれば済むようにします。どちらかをスキップして呼び出しごとに同じ値を渡すこともできます——どちらのスタイルでも検証は同じです。[復号](#decrypt) を参照してください。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from chat_xdk import Chat

    chat = Chat(juicebox_config_json)  # or Chat() + import_keys(blob, version)
    chat.unlock("YOUR_PASSCODE")

    # Session defaults: identity for signing, stored signing keys for
    # verification, opt-in cache for conversation keys
    chat.set_identity(my_user_id, signing_key_version)
    chat.set_signing_keys(signing_keys)  # all participants
    chat.set_cache_keys(True)

    # Batch-decrypt the backlog; senders verify against the stored keys
    result = chat.decrypt_events(raw_events)
    for dm in result["messages"]:
        ev = dm["event"]
        if ev["type"] == "Message":
            print(ev["sender_id"], ev["content"]["text"])

    # Decrypt one live event with the cached conversation key
    event = chat.decrypt_event(one_event_b64)

    # Encrypt and sign as the session identity, under the cached key
    payload = chat.encrypt_message(event["conversation_id"], "Hi!")
    message_id = payload.message_id  # SDK-generated — send as message_id
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { createChat } from '@xdevplatform/chat-xdk';

    const chat = await createChat({
      juiceboxConfig: juiceboxConfigJson,
      getAuthToken: async (realmId) => getRealmToken(realmId),
    });
    await chat.unlock('YOUR_PASSCODE');

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.setIdentity(myUserId, signingKeyVersion);
    chat.setSigningKeys(signingKeys); // all participants
    chat.setCacheKeys(true);

    // Batch-decrypt the backlog; senders verify against the stored keys
    const result = chat.decryptEvents(rawEvents);
    for (const dm of result.messages) {
      if (dm.event.type === 'message') {
        console.log(dm.event.senderId, dm.event.content?.text);
      }
    }

    // Decrypt one live event with the cached conversation key
    const event = chat.decryptEvent(oneEventB64);

    // Encrypt and sign as the session identity, under the cached key
    const payload = chat.encryptMessage({ conversationId: event.conversationId!, text: 'Hi!' });
    const messageId = payload.messageId; // SDK-generated — send as message_id
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // chat_xdk_core::Chat + unlock(b"…").await, or ChatCore + import_keys_with_version

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.set_identity(my_user_id, signing_key_version);
    chat.set_signing_keys(signing_keys); // all participants
    chat.set_cache_keys(true);

    // Batch-decrypt the backlog; senders verify against the stored keys
    let result = chat.decrypt_events(&raw_events, &[]);
    for dm in &result.messages {
        if let Event::Message(msg) = &dm.event {
            println!("{}: {}", msg.meta.sender_id.as_deref().unwrap_or("?"), msg.text().unwrap_or(""));
        }
    }

    // Decrypt one live event with the cached conversation key
    let event = chat.decrypt_event(one_event_b64, &Default::default(), &[])?;

    // Encrypt and sign as the session identity, under the cached key
    let payload = chat.encrypt_message(EncryptMessageParams::new(conversation_id, "Hi!"))?;
    let message_id = payload.message_id; // SDK-generated — send as message_id
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    chat := chatxdk.New()
    defer chat.Close()
    blob, _ := chatxdk.Base64ToBytes(privateKeysB64)
    _ = chat.ImportKeysWithVersion(blob, signingKeyVersion)

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.SetIdentity(myUserID, signingKeyVersion)
    _ = chat.SetSigningKeys(signingKeys) // all participants
    chat.SetCacheKeys(true)

    // Batch-decrypt the backlog; senders verify against the stored keys
    result, err := chat.DecryptEvents(rawEvents, nil)
    for _, dm := range result.Messages {
        if dm.Event.Type == "Message" {
            fmt.Println(dm.Event.AsMessage().Text())
        }
    }

    // Decrypt one live event with the cached conversation key
    event, err := chat.DecryptEvent(oneEventB64, nil, nil)
    msg := event.AsMessage() // nil unless event.Type == "Message"

    // Encrypt and sign as the session identity, under the cached key
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        ConversationID: *msg.ConversationID,
        Text:           "Hi!",
    })
    messageID := payload.MessageID // SDK-generated — send as message_id
    _ = messageID
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using var chat = new Chat();
    chat.ImportKeys(privateKeyBytes, signingKeyVersion);

    // Session defaults: identity for signing, stored signing keys for
    // verification, opt-in cache for conversation keys
    chat.SetIdentity(myUserId, signingKeyVersion);
    chat.SetSigningKeys(signingKeys); // all participants
    chat.SetCacheKeys(true);

    // Batch-decrypt the backlog; senders verify against the stored keys
    var result = chat.DecryptEvents(rawEvents);
    foreach (var dm in result.Messages)
    {
        if (dm.Event.GetProperty("type").GetString() == "Message")
            Console.WriteLine(dm.Event.GetProperty("content").GetProperty("text").GetString());
    }

    // Decrypt one live event with the cached conversation key
    var evt = chat.DecryptEvent(oneEventB64);
    var conversationId = evt.GetProperty("conversation_id").GetString()!;

    // Encrypt and sign as the session identity, under the cached key
    var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hi!"));
    var messageId = payload.MessageId; // SDK-generated — send as message_id
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    try (Chat chat = new Chat()) {
        chat.importKeys(privateKeyBytes, signingKeyVersion);

        // Session defaults: identity for signing, stored signing keys for
        // verification, opt-in cache for conversation keys
        chat.setIdentity(myUserId, signingKeyVersion);
        chat.setSigningKeys(signingKeys); // all participants
        chat.setCacheKeys(true);

        // Batch-decrypt the backlog; senders verify against the stored keys
        DecryptEventsResult result = chat.decryptEvents(rawEvents, null);
        for (DecryptedMessage dm : result.messages) {
            if ("Message".equals(dm.event.path("type").asText())) {
                System.out.println(dm.event.path("content").path("text").asText());
            }
        }

        // Decrypt one live event with the cached conversation key
        JsonNode event = chat.decryptEvent(oneEventB64, (Map<String, byte[]>) null, null);
        String conversationId = event.path("conversation_id").asText();

        // Encrypt and sign as the session identity, under the cached key
        SendPayload payload = chat.encryptMessage(new EncryptMessageParams(conversationId, "Hi!"));
        String messageId = payload.messageId; // SDK-generated — send as message_id
    }
    ```
  </Tab>
</Tabs>

***

## ライフサイクルと鍵

SDK を構築し、秘密鍵を保管し(パスコードで保護されるセキュアキーバックアップまたはローカル鍵ブロブ)、Chat API に**公開**鍵を登録し、アンロックまたはインポートの後に **`set_identity(user_id, signing_key_version)`** を呼び出します——これは、署名されるアクションが既定とする送信者と署名鍵バージョンを設定するので、encrypt および prepare メソッドが呼び出し単位のアイデンティティ引数なしで動作します。デバイス/アプリのアイデンティティごとに `generate_keypairs` を一度呼び出し、登録ペイロードを public-keys エンドポイントに POST してください。セキュアキーバックアップには全バインディングで `setup` / `unlock`(および関連するパスコードヘルパー)を使用します。`export_keys` / `import_keys`(ボットとサーバー向けの生の鍵ブロブ永続化)は**ネイティブバインディングでのみ**利用できます——Python、Go、.NET、JVM、Rust。JS/WASM バインディングは生の鍵のエクスポート/インポートを公開しません:ブラウザではインスタンスに到達する任意のスクリプトがアイデンティティを持ち出しうるため、JS は鍵をセキュアキーバックアップ内に保持します。リクエストごとのバックアップレルムのラウンドトリップを避けたい JS サーバーは、リクエスト間で 1 つのアンロック済み `Chat` インスタンスを再利用するか、鍵ブロブがサポートされるネイティブバインディングを実行してください。

SDK は登録済み公開鍵に対して X API が報告するバージョンも必要とします。これにより、他のバージョンを対象とする鍵変更エントリはスキップされます。`set_identity` はそれをユーザー ID と一緒に記録します。`import_keys` はそれをオプションの引数として直接受け付けます(Rust と Go では `import_keys_with_version` / `ImportKeysWithVersion` を使用)。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    from chat_xdk import Chat

    # Secure key backup (client)
    chat = Chat(juicebox_config_json)
    chat.setup("YOUR_PASSCODE")          # first time — generates keypairs
    # chat.unlock("YOUR_PASSCODE")        # later sessions
    chat.set_identity(user_id, version)  # version from add-public-key / get-public-keys response
    reg = chat.get_public_keys()     # or registration fields from generate_keypairs

    # Key blob (server / bot)
    chat2 = Chat()
    chat2.import_keys(secret_blob, version)
    chat2.set_identity(user_id, version)
    blob = chat2.export_keys()       # treat as a password
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import { createChat } from '@xdevplatform/chat-xdk';

    const chat = await createChat({
      juiceboxConfig: juiceboxConfigJson,
      getAuthToken: async (realmId) => getRealmToken(realmId),
    });
    await chat.setup('YOUR_PASSCODE');
    // await chat.unlock('YOUR_PASSCODE');
    chat.setIdentity(userId, version);
    const publics = chat.getPublicKeys();

    // JS/WASM stores keys only through secure key backup — there is no raw key
    // export/import here. For key-blob persistence, use a native binding.
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // chat_xdk_core::Chat — async secure key backup unlock, or ChatCore + import_keys
    chat.setup(b"YOUR_PASSCODE").await?;
    // chat.unlock(b"YOUR_PASSCODE").await?;
    chat.set_identity(user_id, version);
    let publics = chat.get_public_keys()?;
    let blob = chat.export_keys()?;
    chat.import_keys_with_version(&blob, version)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    chat := chatxdk.New()
    defer chat.Close()

    // Prefer ImportKeys for servers; secure key backup unlock where supported
    keyBlob, _ := chatxdk.Base64ToBytes(privateKeysB64)
    if err := chat.ImportKeysWithVersion(keyBlob, version); err != nil {
        log.Fatal(err)
    }
    chat.SetIdentity(userID, version)
    publics, err := chat.GetPublicKeys()
    blob, err := chat.ExportKeys()
    _ = publics
    _ = blob
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    using var chat = new Chat();
    chat.ImportKeys(privateKeyBytes, version);
    // or secure key backup setup / unlock when config is available
    chat.SetIdentity(userId, version);
    var publics = chat.GetPublicKeys();
    var blob = chat.ExportKeys();
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    try (Chat chat = new Chat()) {
        chat.importKeys(privateKeyBytes, version);
        chat.setIdentity(userId, version);
        var publics = chat.getPublicKeys();
        byte[] blob = chat.exportKeys();
    }
    ```
  </Tab>
</Tabs>

セキュアキーバックアップの構成は 3 つの形式を受け付けます:X API の `juicebox_config` オブジェクト(推奨——そのまま渡す)、完全な `sdk_config` ラッパー、または裸の `token_map`。

オプション:署名検証は**デフォルトでオン**です(`reject_unverified = true`)——無効にするには `set_reject_unverified(false)` を呼び出します(推奨されません)。バックアップレルム構成が変わった場合は `update_config` を使用します。UI の状態には `is_unlocked` / `has_identity_key` を使用します。完全なフィールドリストは [chat-xdk リポジトリ](https://github.com/xdevplatform/chat-xdk) のスタブにあります。

***

## 会話鍵

3 つの **prepare** メソッドはそれぞれ、鍵変更に必要な処理を 1 回の呼び出しで完了します:新しい会話鍵を生成し、渡された公開鍵からすべての参加者に対して暗号化し、変更に署名します。送信者のアイデンティティと署名鍵バージョンはセッションから来ます(`set_identity`)。パラメーターに `sender_id` / `signing_key_version` を設定すればオーバーライドできます。すべて同じ **`PreparedConversationChange`** 形状を返し、POST 準備が整っています——SDK のフィールド `encrypted_key` を `conversation_participant_keys` の **`encrypted_conversation_key`** にリネームし、アクション署名を必須の **`action_signatures`** ボディフィールドにマップしてください。

| シナリオ                                                              | メソッド                              | 返されるアクション署名 |
| :---------------------------------------------------------------- | :-------------------------------- | :---------- |
| 1:1 を開始する(会話 ID を省略——SDK が導出)または任意の会話の鍵をローテーションする(ID を渡す)         | `prepare_conversation_key_change` | 1           |
| グループを作成する(ID は `POST /2/chat/conversations/group/initialize` で発行) | `prepare_group_create`            | 2——両方送信     |
| グループにメンバーを追加する                                                    | `prepare_group_members_change`    | 2——両方送信     |

`encrypt_message` とメディア用に**生の**鍵バイトを保持してください。API の暗号化エンベロープを暗号化に渡さないでください。

<Warning>
  **ラップする前に取得した鍵を検証してください。** prepare メソッドは渡された任意の公開鍵に対して新しい会話鍵を暗号化します。渡す前に、取得した各レコードに対して `verify_key_binding(identity, signing, signature)` を呼び出してください——public-keys API からの `public_key`、`signing_public_key`、`identity_public_key_signature` の各フィールドで——差し替えられたアイデンティティ鍵が会話鍵を受け取れないようにします。
</Warning>

鍵変更イベントペイロードに対して `extract_conversation_keys` を使って `{ keys, latest_version }` を再構築します。`decrypt_conversation_key` は 1 つの ECIES ブロブをアンラップします。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    # One entry per participant public key, from the public-keys API:
    # participants = [
    #     {"user_id": "1215441834412953600", "public_key": "BASE64_IDENTITY_PUBLIC_KEY", "key_version": "1733889755256"},
    #     {"user_id": "1843439638876491776", "public_key": "BASE64_IDENTITY_PUBLIC_KEY", "key_version": "1766181805686"},
    # ]
    prepared = chat.prepare_conversation_key_change(participants)
    # prepared["conversation_key"]   — raw bytes for encrypt_message
    # prepared["participant_keys"]   — per-user wraps; rename encrypted_key → encrypted_conversation_key on POST
    # prepared["action_signatures"]  — required on the POST body

    extracted = chat.extract_conversation_keys(key_change_blobs)
    keys = extracted["keys"]
    latest = extracted["latest_version"]
    raw = keys[latest]

    one = chat.decrypt_conversation_key(encrypted_blob)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const prepared = chat.prepareConversationKeyChange({ publicKeys: participants });
    // prepared.conversationKey — Uint8Array for encryptMessage
    // prepared.participantKeys / prepared.actionSignatures — POST body fields

    const extracted = chat.extractConversationKeys(keyChangeBlobs);
    const raw = extracted.keys[extracted.latestVersion!];

    const one = chat.decryptConversationKey(encryptedBlob);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let prepared = chat.prepare_conversation_key_change(
        ConversationKeyChangeParams::new(participants),
    )?;
    let extracted = chat.extract_conversation_keys(&key_change_blobs);
    let latest = extracted.latest_version.as_deref().unwrap_or_default();
    let raw = &extracted.keys[latest];
    let one = chat.decrypt_conversation_key(&encrypted_blob)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    prepared, err := chat.PrepareConversationKeyChange(chatxdk.ConversationKeyChangeParams{
        PublicKeys: participants,
    })
    // prepared.ConversationKey feeds EncryptMessage
    // prepared.ParticipantKeys / prepared.ActionSignatures — POST body fields
    extracted, err := chat.ExtractConversationKeys(keyChangeBlobs)
    one, err := chat.DecryptConversationKey(encryptedBlob)
    _ = prepared
    _ = extracted
    _ = one
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var prepared = chat.PrepareConversationKeyChange(new ConversationKeyChangeParams(participants));
    var extracted = chat.ExtractConversationKeys(keyChangeBlobs);
    var raw = extracted.Keys[extracted.LatestVersion];
    var one = chat.DecryptConversationKey(encryptedBlob);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    PreparedConversationChange prepared =
            chat.prepareConversationKeyChange(new ConversationKeyChangeParams(participants));
    ConversationKeyBundle extracted = chat.extractConversationKeys(keyChangeBlobs);
    byte[] raw = extracted.keys.get(extracted.latestVersion);
    byte[] one = chat.decryptConversationKey(encryptedBlob);
    ```
  </Tab>
</Tabs>

グループ作成とメンバー追加については、各メソッドが必要とするパラメーター(`prepare_group_create` はメンバー/管理者 ID リスト、`prepare_group_members_change` は新規 + 現在の名簿)を渡してください——サンプルは[グループ](/ja/xchat/groups#create-the-group-and-establish-keys)を参照してください。どちらも**2 つ**のアクション署名を返します。POST には両方を含める必要があります。

***

## 復号

**`decrypt_events`** は履歴とバックログ用です:ストリームから会話鍵を取得し、復号済みメッセージを返し、バッチ全体を失敗させる代わりにイベントごとのエラーを**収集**します。**`decrypt_event`** は 1 つのライブイベント用です。失敗時に raise/throw します。

送信者を SDK が検証できるよう、**署名鍵**を渡してください。API の public-key フィールドを `SigningKeyEntry` にマップしてください:`public_key_version` → `public_key_version`(同じ名前)、`signing_public_key` → `public_key`、`public_key` → `identity_public_key`、および `identity_public_key_signature` と `user_id`。

呼び出し単位の鍵引数を省略できるようにする、2 つのオプトインなセッションストアがあります:

* **`set_signing_keys(entries)`** は参加者の署名鍵を保管します。復号呼び出しが署名鍵引数を省略(または空を渡す)した場合、代わりにストアが使われます。検証自体は変わりません——鍵はこの呼び出しを通じてのみストアに入り、復号対象のイベントからは決して入りません。呼び出しごとに以前のセットが置き換わります。
* **`set_cache_keys(true)`** は会話鍵キャッシュを有効にします(デフォルトはオフ)。有効な間、`decrypt_events` は会話ごとに、鍵変更が有効な署名を持っていた最新の鍵をキャッシュします。`decrypt_event` は会話鍵引数が省略された場合にそこにフォールバックし、暗号化ヘルパーは省略された会話鍵をそこから解決します。無効化するとキャッシュはクリアされます。

明示的で空でない引数は常にストアより優先されます。明示的な呼び出し単位の引数はファーストクラスのままです——リクエストがストアが空の新しいインスタンスに着地しうるサーバーレスや複数インスタンス構成では、これが正しい選択です。

検証はデフォルトで必須です:署名鍵を省略しても検証はスキップされません。何も渡されず何も保存されていない場合、署名付きイベントは失敗します(`decrypt_events` では `errors` に収集され、`decrypt_event` ではスローされます)。実際に検証をスキップするには、まず `set_reject_unverified(false)` を呼び出す必要があります(本番環境では推奨されません)。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    signing_keys = [{
        "user_id": uid,
        "public_key_version": row["public_key_version"],
        "public_key": row["signing_public_key"],
        "identity_public_key": row["public_key"],
        "identity_public_key_signature": row["identity_public_key_signature"],
    } for row in api_public_keys]

    result = chat.decrypt_events(raw_events, signing_keys)
    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["type"] == "Message":
            text = ev["content"].get("text")

    cached = result["conversation_keys"]["keys"]
    live = chat.decrypt_event(one_event_b64, cached, signing_keys)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const signingKeys = apiPublicKeys.map((row) => ({
      userId: uid,
      publicKeyVersion: row.public_key_version,
      publicKey: row.signing_public_key,
      identityPublicKey: row.public_key,
      identityPublicKeySignature: row.identity_public_key_signature,
    }));

    const result = chat.decryptEvents(rawEvents, signingKeys);
    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, signingKeys);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let result = chat.decrypt_events(&raw_events, &signing_keys);
    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)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    result, err := chat.DecryptEvents(rawEvents, signingKeys)
    for idx, msg := range result.Errors {
        log.Printf("event %s failed: %s", idx, msg)
    }
    cached := result.ConversationKeys.Keys
    live, err := chat.DecryptEvent(oneEventB64, cached, signingKeys)
    _ = live
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var result = chat.DecryptEvents(rawEvents, signingKeys);
    foreach (var kv in result.Errors) { /* kv.Key = event index, kv.Value = error */ }
    var cached = result.ConversationKeys.Keys;
    var live = chat.DecryptEvent(oneEventB64, cached, signingKeys);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    DecryptEventsResult result = chat.decryptEvents(rawEvents, signingKeys);
    Map<String, byte[]> cached = result.conversationKeys.keys;
    JsonNode live = chat.decryptEvent(oneEventB64, cached, signingKeys);
    ```
  </Tab>
</Tabs>

***

## 暗号化と送信ヘルパー

**`encrypt_message(conversation_id, text)`** はテキストメッセージ用に署名付き暗号文を構築します。オプションで `entities`、`attachments`(`media_hash_key` 経由)、`should_notify`、`ttl_msec`。送信者アイデンティティはセッション(`set_identity`)から、会話鍵はオプトインの鍵キャッシュ(`set_cache_keys`)から解決されます——または `sender_id` / `signing_key_version` と `conversation_key` + `conversation_key_version` を明示的に渡します。SDK は **`message_id`** を生成し(署名済みイベントに埋め込まれた UUID)、ペイロード上で返します——自分で発行しないでください。リトライ時は同じペイロードを再利用して、ID が二度発行されないようにしてください。ペイロードを send-message ボディにマップしてください:`message_id` → **`message_id`**、`encrypted_content` → **`encoded_message_create_event`**、`encoded_event_signature` → **`encoded_message_event_signature`**。

**返信はイベントベースです。** `encrypt_reply(conversation_id, text, reply_to_event)` は返信対象の base64 生イベントを受け取ります。SDK はそこから引用プレビュー(シーケンス ID、送信者、テキスト、エンティティ、添付)を導出し、署名済みオリジナルを送信メッセージに埋め込むので、受信者は引用を検証できます。オリジナルが返信より古い鍵バージョンで暗号化されている場合は、`reply_to_ckces`——生の鍵変更イベント——を渡してください。オリジナルが**編集**されている場合は、生の編集イベントを `reply_to_edit_event` として渡してください:プレビューはメッセージが現在示している内容を引用します(そのテキストとエンティティは編集から取られます)、そして受信者がチェックできるよう編集がオリジナルと一緒に伝わります。明示的な `reply_to_*` フィールドは、生のイベントをもはや保持していない呼び出し元向けのオーバーライドとして残されています。

**リアクションもイベントベースです。** `encrypt_add_reaction(target_event, emoji)` と `encrypt_remove_reaction(...)` は、リアクションの対象となる生のイベントから会話 ID と対象シーケンス ID を導出します。同じパラメーターでリアクションの追加と後の削除ができます。生のイベントを保持していないときにのみ、`conversation_id` と `target_message_sequence_id` を明示的に設定してください。

受信側では、返信を引用する復号済みメッセージは **`reply_preview_validation`**(`"Valid"` / `"Invalid"`。JS バインディングは `'valid'` / `'invalid'`)を持ちます:SDK は埋め込まれたオリジナルの署名をあなたの署名鍵に対して検証し(イベントに含まれる鍵ではなく)、復号し、引用されたコンテンツと作者をそれに対して比較しました。プレビューが編集イベントを埋め込んでいる場合、SDK は編集を同様に検証し(同じ会話、オリジナルと同じ作者)、引用テキストを編集前のテキストではなく編集後の内容に対してチェックします。メッセージがプレビューを持たない、またはプレビューがオリジナルを埋め込まない場合、このフィールドは存在しません。`Invalid` なプレビューは信頼できないものとして扱ってください:メッセージ自体は本物ですが、引用素材はそうではありません——引用は検証済みオリジナルからのみ描画してください。

**`encrypt` / `decrypt`** は会話鍵下の UTF-8 メタデータ用です(たとえば暗号化されたグループ名)——メッセージエンベロープ用ではありません。**`encrypt_stream` / `decrypt_stream`** は添付ファイルのバイト列を暗号化します。[メディア](/ja/xchat/media) を参照してください。低レベルの **`sign` / `verify` / `verify_key_binding`** は高度なフローをサポートします。会話鍵の変更、グループ作成、メンバー追加は [prepare メソッド](#conversation-keys) によって署名されます。

`encrypt_message` / `encrypt_reply` に渡される会話 ID は、保持している任意の形式で構いません——イベントからの `A:B`、リスティングや URL パスからの `A-B`(順不同)、または裸の受信者ユーザー ID——SDK は署名前に正規化します。グループ ID(`g` プレフィックス付き)はそのまま渡ります。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    payload = chat.encrypt_message(
        conversation_id, "Hello",
        # Optional keyword args: entities, attachments, should_notify, ttl_msec
    )
    body = {
        "message_id": payload.message_id,
        "encoded_message_create_event": payload.encrypted_content,
        "encoded_message_event_signature": payload.encoded_event_signature,
    }
    # POST body to /2/chat/conversations/{id}/messages

    # Preview derived from + embedded raw event so recipients can validate;
    # add reply_to_ckces=[...] when the original used an older key version
    reply = chat.encrypt_reply(conversation_id, "Sounds good", original_event_b64)

    # Conversation and target derived from the raw event
    add = chat.encrypt_add_reaction(original_event_b64, "👍")
    remove = chat.encrypt_remove_reaction(original_event_b64, "👍")

    name_ct = chat.encrypt("Group title", raw_conversation_key)
    title = chat.decrypt(name_ct, raw_conversation_key)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const payload = chat.encryptMessage({
      conversationId,
      text: 'Hello',
      // Optional: entities, attachments, shouldNotify, ttlMsec
    });
    const body = {
      message_id: payload.messageId,
      encoded_message_create_event: payload.encryptedContent,
      encoded_message_event_signature: payload.encodedEventSignature,
    };
    // POST body to /2/chat/conversations/{id}/messages

    // Preview derived from + embedded raw event so recipients can validate;
    // add replyToCkces: [...] when the original used an older key version
    const reply = chat.encryptReply({
      conversationId,
      text: 'Sounds good',
      replyToEvent: originalEventB64,
    });

    // Conversation and target derived from the raw event
    const add = chat.encryptAddReaction({ emoji: '👍', targetEvent: originalEventB64 });
    const remove = chat.encryptRemoveReaction({ emoji: '👍', targetEvent: originalEventB64 });

    const nameCt = chat.encrypt('Group title', rawConversationKey);
    const title = chat.decrypt(nameCt, rawConversationKey);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let payload = chat.encrypt_message(EncryptMessageParams::new(conversation_id, "Hello"))?;
    // Send body: payload.message_id → message_id,
    // payload.encrypted_content → encoded_message_create_event,
    // payload.encoded_event_signature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set params.reply_to_ckces when the original used an older key version
    let reply = chat.encrypt_reply(EncryptReplyParams::new(
        conversation_id, "Sounds good", original_event_b64,
    ))?;

    // Conversation and target derived from the raw event
    let reaction = EncryptReactionParams::new(original_event_b64, "👍");
    let add = chat.encrypt_add_reaction(&reaction)?;
    let remove = chat.encrypt_remove_reaction(&reaction)?;

    // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
    let name_ct = chat.encrypt("Group title", &conv_key)?;
    let title = chat.decrypt(&name_ct, &conv_key)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        ConversationID: conversationID,
        Text:           "Hello",
    })
    // Send body: payload.MessageID → message_id,
    // payload.EncryptedContent → encoded_message_create_event,
    // payload.EncodedEventSignature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set ReplyToCkces when the original used an older key version
    reply, err := chat.EncryptReply(chatxdk.EncryptReplyParams{
        ConversationID: conversationID,
        Text:           "Sounds good",
        ReplyToEvent:   originalEventB64,
    })

    // Conversation and target derived from the raw event
    reaction := chatxdk.EncryptReactionParams{Emoji: "👍", TargetEvent: originalEventB64}
    add, err := chat.EncryptAddReaction(reaction)
    remove, err := chat.EncryptRemoveReaction(reaction)

    nameCt, err := chat.Encrypt("Group title", rawKey)
    title, err := chat.Decrypt(nameCt, rawKey)
    _ = payload
    _ = reply
    _ = add
    _ = remove
    _ = title
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var payload = chat.EncryptMessage(new EncryptMessageParams(conversationId, "Hello"));
    // Send body: payload.MessageId → message_id,
    // payload.EncryptedContent → encoded_message_create_event,
    // payload.EncodedEventSignature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set ReplyToCkces when the original used an older key version
    var reply = chat.EncryptReply(new EncryptReplyParams(conversationId, "Sounds good", originalEventB64));

    // Conversation and target derived from the raw event
    var reaction = new EncryptReactionParams(originalEventB64, "👍");
    var add = chat.EncryptAddReaction(reaction);
    var remove = chat.EncryptRemoveReaction(reaction);

    var nameCt = chat.Encrypt("Group title", rawKey);
    var title = chat.Decrypt(nameCt, rawKey);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    SendPayload payload = chat.encryptMessage(new EncryptMessageParams(conversationId, "Hello"));
    // Send body: payload.messageId → message_id,
    // payload.encryptedContent → encoded_message_create_event,
    // payload.encodedEventSignature → encoded_message_event_signature

    // Preview derived from + embedded raw event so recipients can validate;
    // set replyToCkces when the original used an older key version
    SendPayload reply =
            chat.encryptReply(new EncryptReplyParams(conversationId, "Sounds good", originalEventB64));

    // Conversation and target derived from the raw event
    EncryptReactionParams reaction = new EncryptReactionParams(originalEventB64, "👍");
    SendPayload add = chat.encryptAddReaction(reaction);
    SendPayload remove = chat.encryptRemoveReaction(reaction);

    String nameCt = chat.encrypt("Group title", rawKey);
    String title = chat.decrypt(nameCt, rawKey);
    ```
  </Tab>
</Tabs>

***

## メディアストリーム

テキストと**同じ**会話鍵でファイルバイト列を暗号化し、Chat メディア API 経由でアップロードし、`encrypt_message` に **`media_hash_key`** を添付します。これは Posts のメディアモデル(`expansions=attachments.media_keys`)ではありません。完全なアップロード/ダウンロードのフロー:[メディア](/ja/xchat/media)。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    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, raw_conversation_key)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    const ciphertext = chat.encryptStream(fileBytes, rawConversationKey);
    // Upload `ciphertext`; mediaHashKey comes from the upload finalize step.
    const plain = chat.decryptStream(ciphertext, rawConversationKey);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // 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)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    ciphertext, err := chat.EncryptStream(fileBytes, rawKey)
    plain, err := chat.DecryptStream(ciphertext, rawKey)
    _ = plain
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var ciphertext = chat.EncryptStream(fileBytes, rawKey);
    var plain = chat.DecryptStream(ciphertext, rawKey);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    byte[] ciphertext = chat.encryptStream(fileBytes, rawKey);
    byte[] plain = chat.decryptStream(ciphertext, rawKey);
    ```
  </Tab>
</Tabs>

### 大きなメディア向けの増分ストリーミング

大きなファイルでは、ペイロード全体をメモリに保持することを避けてください:`stream_encryptor()` / `stream_decryptor()` は `push(chunk)` でチャンク(それぞれ約 1 MB)を送り込み、最後に一度 `finish()` を呼び出す `StreamEncryptor` / `StreamDecryptor` を返します。復号時、`finish()` は切り捨てられたストリームを検出します(最終フレームより前に入力が終わっている場合は失敗します)。したがって、成功するまで push されたプレーンテキストを完全と扱わないでください。

<Warning>
  **JS/WASM のみ:** `finish()` は基盤の WASM オブジェクトを消費して解放します——`finish()` の後に `free()` を呼び出さないでください(スローします)。`free()` は、finish 前にストリームを放棄する場合(たとえばエラーパス)にのみ呼び出してください。
</Warning>

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    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)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    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);
    ```
  </Tab>
</Tabs>

***

## ユーティリティ

Base64/hex ヘルパー、MIME スニッフィング、画像寸法は、モジュールレベルの関数(Python/JS/Rust/Go)または `ChatXdkUtilities`(C#/Java)として利用可能です——追加のライブラリを取り込まずに添付メタデータを構築するときに便利です。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    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)
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    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 dims = detectImageDimensions(fileBytes);
    const width = dims?.width ?? 0;
    const height = dims?.height ?? 0;
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    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 dims = chat_xdk_core::detect_image_dimensions(&file_bytes);
    let (w, h) = dims.map(|d| (d.width, d.height)).unwrap_or((0, 0));
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    b64, _ := chatxdk.BytesToBase64(raw)
    raw2, err := chatxdk.Base64ToBytes(b64)
    hexed, err := chatxdk.BytesToHex(raw)
    raw3, err := chatxdk.HexToBytes(hexed)
    mime, _ := chatxdk.DetectMimeType(fileBytes)
    dims, _ := chatxdk.DetectImageDimensions(fileBytes)
    w, h := dims.Width, dims.Height
    _ = b64
    _ = raw2
    _ = hexed
    _ = raw3
    _ = mime
    _ = w
    _ = h
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    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 dims = ChatXdkUtilities.DetectImageDimensions(fileBytes);
    var w = dims?.Width ?? 0;
    var h = dims?.Height ?? 0;
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    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);
    ImageDimensions wh = ChatXdkUtilities.detectImageDimensions(fileBytes);
    long width = wh.width, height = wh.height;
    ```
  </Tab>
</Tabs>

***

## 重要な型

これらの概念的な型は各言語で登場します(正確なフィールド名は異なります。JS では `message` のような camelCase のイベント識別子が使われることが多いです):

* **SendPayload** — `encrypt_message` および他の暗号化ヘルパーの戻り値:SDK 生成の **`message_id`**(署名済みイベントに埋め込まれた UUID——メッセージの `message_id` として送信し、重複排除用に保持)、`encrypted_content`、`encoded_event_signature`、署名メタデータ、`conversation_key_version`、`should_notify`。Chat API の送信ボディにマップしてください。
* **PublicKeyRegistrationPayload** — add-public-key API 用の `generate_keypairs` / public-key ゲッターの出力。
* **SigningKeyEntry** — 署名検証のために復号に渡されるか、`set_signing_keys` で保存される送信者の公開素材。
* **PreparedConversationChange** — 3 つの prepare メソッドの出力:導出または渡された `conversation_id`、生の `conversation_key` バイト、`conversation_key_version`、`participant_keys`(`user_id`、`encrypted_key`、`public_key_version`)、`action_signatures`(`message_id`、`encoded_message_event_detail`、`signature`、`signature_version`、`public_key_version`、オプションで `signature_payload`——鍵変更署名では省略されます。そのペイロードには平文の鍵が埋め込まれるためです)。
* **DecryptEventsResult** — メッセージ、オプションのエラー、抽出された `conversation_keys`。返信を引用する復号済みメッセージには `reply_preview_validation` が付きます([暗号化と送信ヘルパー](#encrypt-and-send-helpers) を参照)。

完全なフィールドリストは [chat-xdk リポジトリ](https://github.com/xdevplatform/chat-xdk) の言語スタブ(`docs/API.md`、`*.pyi`、`index.d.ts`)を使用してください。

***

## エラー

Python は通常、記述的なメッセージ付きの **`ValueError`** を送出します(たとえば無効なパスコード)。TypeScript/JavaScript は **`Error`** をスローします。Go は `(value, error)` を返します。1 つの不良イベントがバッチを中断しないよう、履歴には **`decrypt_events`** を優先してください。部分的な失敗については errors コレクションを検査してください。

一部の検証エラーは**恒久的**です。署名は不変であり、イベント自体から署名済みペイロードを再構築することで検証されます。そのため、`signature missing or no matching signing key` や ECDSA 不一致で失敗する古いイベントは、以後のロードでも毎回失敗します——リトライ、鍵の更新、API 呼び出しでは修復できません。これらは一時的なエラーではなく tombstone として扱ってください。会話鍵をローテーションすれば、そこから先はクリーンで検証可能な履歴が始まります。

***

## 次のステップ

<CardGroup cols={2}>
  <Card title="はじめに" icon="rocket" href="/ja/xchat/getting-started">
    Chat XDK を Chat API に配線
  </Card>

  <Card title="メディア" icon="image" href="/ja/xchat/media">
    ストリーム暗号化とメディア REST
  </Card>

  <Card title="リアルタイムイベント" icon="bolt" href="/ja/xchat/real-time-events">
    Webhook とアクティビティ配信
  </Card>

  <Card title="トラブルシューティング" icon="wrench" href="/ja/xchat/troubleshooting">
    よくある失敗
  </Card>
</CardGroup>
