> ## Documentation Index
> Fetch the complete documentation index at: https://docs.x.com/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와 함께 사용하세요.

앱 안내: [시작하기](/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.2.1" }

    # 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.2.1</version>
    </dependency>
    ```

    Maven Central에서 제공됩니다. jar에 macOS (arm64, x64), Linux (x64), Windows (x64)용 네이티브 라이브러리가 번들되어 있어 `jna.library.path` 설정이 필요 없습니다. `com.x.chatxdk`에서 임포트하세요. JDK 17+ 필요.
  </Tab>
</Tabs>

***

## 빠른 시작

백로그를 복호화하고, 키를 캐시하고, 이벤트 하나를 복호화하고, 회신을 암호화합니다. [시작하기](/xchat/getting-started)에서와 같이 전송 본문을 [`POST /2/chat/conversations/{id}/messages`](/x-api/chat/send-chat-message)에 연결하세요.

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

    chat = Chat(juicebox_config_json)  # or Chat() + import_keys(blob)
    chat.unlock("YOUR_PASSCODE")
    chat.set_key_version(signing_key_version)

    result = chat.decrypt_events(raw_events, signing_keys)
    for dm in result["messages"]:
        ev = dm["event"]
        if ev.get("type") == "Message":
            print(ev.get("sender_id"), ev.get("content", {}).get("text"))

    cached = result["conversation_keys"]["keys"]
    event = chat.decrypt_event(one_event_b64, cached, sender_signing_keys)

    raw_key = cached[result["conversation_keys"]["latest_version"]]
    payload = chat.encrypt_message(
        message_id, sender_id, conversation_id, raw_key, "Hi!",
        conversation_key_version, signing_key_version,
    )
    ```
  </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');
    chat.setKeyVersion(signingKeyVersion);

    const result = chat.decryptEvents(rawEvents, signingKeys);
    for (const dm of result.messages) {
      if (dm.event.type === 'message') {
        console.log(dm.event.senderId, dm.event.content?.text);
      }
    }

    const cached = result.conversationKeys.keys;
    const event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys);

    const rawKey = cached[result.conversationKeys.latestVersion!];
    const payload = chat.encryptMessage({
      messageId, senderId, conversationId, conversationKey: rawKey, text: 'Hi!',
      conversationKeyVersion, signingKeyVersion,
    });
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // ChatCore + import_keys, or chat_xdk_core::Chat + unlock().await
    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, conv_key, "Hi!",
        &conversation_key_version, &signing_key_version,
    ))?;
    ```
  </Tab>

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

    result, err := chat.DecryptEvents(rawEvents, signingKeys)
    cached := result.ConversationKeys.Keys
    event, err := chat.DecryptEvent(oneEventB64, cached, senderSigningKeys)
    rawKey := cached[*result.ConversationKeys.LatestVersion]
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
        ConversationKey: rawKey, Text: "Hi!",
        ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
    })
    _ = event
    _ = payload
    _ = err
    ```
  </Tab>

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

    var result = chat.DecryptEvents(rawEvents, signingKeys);
    var cached = result.ConversationKeys.Keys;
    var evt = chat.DecryptEvent(oneEventB64, cached, senderSigningKeys);
    var payload = chat.EncryptMessage(new EncryptMessageParams {
        MessageId = messageId, SenderId = senderId, ConversationId = conversationId,
        ConversationKey = rawKey, Text = "Hi!",
        ConversationKeyVersion = conversationKeyVersion, SigningKeyVersion = signingKeyVersion,
    });
    ```
  </Tab>

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

        DecryptEventsResult result = chat.decryptEvents(rawEvents, signingKeys);
        Map<String, byte[]> cached = result.conversationKeys.keys;
        JsonNode event = chat.decryptEvent(oneEventB64, cached, senderSigningKeys);

        EncryptMessageParams params = new EncryptMessageParams();
        params.messageId = messageId;
        params.senderId = senderId;
        params.conversationId = conversationId;
        params.conversationKey = rawKey;
        params.text = "Hi!";
        params.conversationKeyVersion = conversationKeyVersion;
        params.signingKeyVersion = signingKeyVersion;
        SendPayload payload = chat.encryptMessage(params);
    }
    ```
  </Tab>
</Tabs>

***

## 라이프사이클 및 키

SDK를 구성하고, 개인 키를 저장하며(패스코드로 보호되는 보안 키 백업 또는 로컬 키 blob), Chat API에 **공개** 키를 등록하고, 잠금 해제 또는 가져오기 후 등록된 **공개 키 버전**을 설정하세요. 보안 키 백업은 **Juicebox**로 구현되어 있으며, 관련 구성 필드가 그 이름을 갖는 것도 그 때문입니다. 기기/앱 신원당 한 번 `generate_keypairs`를 호출하고, 등록 페이로드를 공개 키 엔드포인트에 POST하세요. 모든 바인딩에서 보안 키 백업을 위해 `setup` / `unlock`(및 관련 패스코드 헬퍼)을 사용하세요. `export_keys` / `import_keys`(봇 및 서버용 원시 키 blob 지속)는 **네이티브 바인딩에서만** 사용 가능합니다—Python, Go, .NET, JVM, Rust. JS/WASM 바인딩은 원시 키 내보내기 또는 가져오기를 노출하지 않습니다: 브라우저에서 인스턴스에 접근하는 모든 스크립트가 신원을 유출할 수 있으므로, JS는 키를 보안 키 백업 내부에 유지합니다. 요청당 백업 realm 왕복을 피하고자 하는 JS 서버는 요청 전반에 걸쳐 잠금 해제된 하나의 `Chat` 인스턴스를 재사용하거나, 키 blob이 지원되는 네이티브 바인딩을 실행해야 합니다.

<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_key_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)
    chat2.set_key_version(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.setKeyVersion(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("YOUR_PASSCODE").await?;
    // chat.unlock("YOUR_PASSCODE").await?;
    chat.set_key_version(&version);
    let publics = chat.get_public_keys()?;
    let blob = chat.export_keys()?;
    chat.import_keys(&blob)?;
    ```
  </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.ImportKeys(keyBlob); err != nil {
        log.Fatal(err)
    }
    chat.SetKeyVersion(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);
    // or secure key backup setup / unlock when config is available
    chat.SetKeyVersion(version);
    var publics = chat.GetPublicKeys();
    var blob = chat.ExportKeys();
    ```
  </Tab>

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

보안 키 백업 구성은 세 가지 형태를 허용합니다: X API의 `juicebox_config` 객체(권장—그대로 전달), 전체 `sdk_config` 래퍼, 또는 순수한 `token_map`.

선택 사항: 서명 검증은 **기본적으로 켜져 있습니다**(`reject_unverified = true`)—비활성화하려면 `set_reject_unverified(false)`를 호출하세요(권장하지 않음); 백업 realm 구성이 변경되면 `update_config`; UI 상태를 위해 `is_unlocked` / `has_identity_key`. 전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk)의 스텁에 있습니다.

***

## 대화 키

세 가지 **prepare** 메서드는 각각 하나의 호출로 키 변경에 필요한 모든 것을 수행합니다: 새 대화 키를 생성하고, 각 참가자(전달한 공개 키에서)에 대해 암호화하고, 변경 사항에 서명합니다. 모두 동일한 **`PreparedConversationChange`** 형태를 반환하며, POST할 준비가 되어 있습니다—`conversation_participant_keys`에서 SDK 필드 `encrypted_key`를 \*\*`encrypted_conversation_key`\*\*로 이름을 바꾸고, action signature를 필수 **`action_signatures`** 본문 필드로 매핑하세요.

| 시나리오                                                          | 메서드                               | 반환되는 action signature |
| :------------------------------------------------------------ | :-------------------------------- | :-------------------- |
| 1:1 시작(대화 ID 생략—SDK가 파생함) 또는 어떤 대화의 키든 교체(ID 전달)              | `prepare_conversation_key_change` | 1                     |
| 그룹 생성 (`POST /2/chat/conversations/group/initialize`로 발급된 ID) | `prepare_group_create`            | 2—둘 다 전송              |
| 그룹에 멤버 추가                                                     | `prepare_group_members_change`    | 2—둘 다 전송              |

`encrypt_message`와 미디어를 위해 **원시** 키 바이트를 보관하세요; API의 암호화된 봉투를 암호화에 전달하지 마세요.

<Warning>
  **래핑하기 전에 가져온 키를 검증하세요.** prepare 메서드는 전달된 공개 키로 새 대화 키를 암호화합니다. 대체된 신원 키가 대화 키를 받지 못하도록, 전달하기 전에 각 가져온 레코드—공개 키 API의 `public_key`, `signing_public_key`, `identity_public_key_signature` 필드—에 대해 `verify_key_binding(identity, signing, signature)`를 호출하세요.
</Warning>

`{ keys, latest_version }`를 재구축하려면 키 변경 이벤트 페이로드에 `extract_conversation_keys`를 사용하세요. `decrypt_conversation_key`는 단일 ECIES blob을 언래핑합니다.

<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(my_user_id, signing_key_version, 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({
      senderId: myUserId, signingKeyVersion, 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(&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();
    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{
        SenderID: myUserID, SigningKeyVersion: signingKeyVersion, 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 {
        SenderId = myUserId, SigningKeyVersion = signingKeyVersion, PublicKeys = participants,
    });
    var extracted = chat.ExtractConversationKeys(keyChangeBlobs);
    var raw = extracted.Keys[extracted.LatestVersion];
    var one = chat.DecryptConversationKey(encryptedBlob);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={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);
    ```
  </Tab>
</Tabs>

그룹 생성 및 멤버 추가의 경우, 각 메서드가 필요로 하는 매개변수를 전달하세요(`prepare_group_create`에는 멤버/관리자 ID 목록; `prepare_group_members_change`에는 새 로스터와 현재 로스터)—샘플은 [그룹](/xchat/groups#create-the-group-and-establish-keys)을 참조하세요. 둘 다 **두 개**의 action signature를 반환합니다; POST는 둘 다 포함해야 합니다.

***

## 복호화

\*\*`decrypt_events`\*\*는 이력과 백로그용입니다: 스트림에서 대화 키를 가져오고, 복호화된 메시지를 반환하며, 전체 배치가 실패하는 대신 이벤트별 오류를 **수집**합니다. \*\*`decrypt_event`\*\*는 이미 키 캐시가 있을 때 단일 실시간 이벤트용입니다; 실패 시 예외를 발생/던집니다.

SDK가 발신자를 검증할 수 있도록 **서명 키**를 전달하세요. API 공개 키 필드를 `SigningKeyEntry`에 매핑합니다: `public_key_version` → `public_key_version`(같은 이름), `signing_public_key` → `public_key`, `public_key` → `identity_public_key`, 그리고 `identity_public_key_signature`와 `user_id`. 검증은 기본적으로 필수입니다: 서명 키 목록을 생략하거나 빈 목록을 전달해도 이를 건너뛰지 **않습니다**—서명된 이벤트는 실패합니다(`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.get("type") == "Message":
            text = ev.get("content", {}).get("text")

    cached = result["conversation_keys"]["keys"]
    live = chat.decrypt_event(one_event_b64, cached, signing_keys_for_sender)
    ```
  </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, signingKeysForSender);
    ```
  </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_for_sender)?;
    ```
  </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, signingKeysForSender)
    _ = 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, signingKeysForSender);
    ```
  </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, signingKeysForSender);
    ```
  </Tab>
</Tabs>

***

## 암호화 및 전송 헬퍼

\*\*`encrypt_message`\*\*는 텍스트 메시지를 위한 서명된 암호문을 만듭니다(선택적 엔티티, `media_hash_key`를 통한 첨부, TTL, 알림 플래그). 반환된 페이로드를 메시지 전송 본문에 매핑하세요: `encrypted_content` → **`encoded_message_create_event`**, `encoded_event_signature` → **`encoded_message_event_signature`**, 그리고 **`message_id`**.

회신과 반응에는 **`encrypt_reply`**, **`encrypt_add_reaction`**, \*\*`encrypt_remove_reaction`\*\*을 사용하세요(`sequence_id`는 부모를 대상으로 함). \*\*`encrypt` / `decrypt`\*\*는 대화 키 아래의 UTF-8 메타데이터용입니다(예: 암호화된 그룹 이름)—메시지 봉투용이 아닙니다. \*\*`encrypt_stream` / `decrypt_stream`\*\*은 첨부 바이트를 암호화합니다; [미디어](/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(
        message_id, sender_id, conversation_id, raw_conversation_key, "Hello",
        conversation_key_version, signing_key_version,
        # Optional keyword args: entities, attachments, should_notify, ttl_msec
    )
    body = {
        "message_id": 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

    reply = chat.encrypt_reply(
        reply_message_id, sender_id, conversation_id, raw_conversation_key,
        "Sounds good", conversation_key_version, signing_key_version,
        parent_sequence_id,  # reply_to_sequence_id — the message being replied to
    )
    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({
      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({
      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);
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    // conv_key: XChatConversationKey from extract_conversation_keys / decrypt_conversation_key
    let payload = chat.encrypt_message(EncryptMessageParams::new(
        &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", &conv_key)?;
    let title = chat.decrypt(&name_ct, &conv_key)?;
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    payload, err := chat.EncryptMessage(chatxdk.EncryptMessageParams{
        MessageID: messageID, SenderID: senderID, ConversationID: conversationID,
        ConversationKey: rawKey, Text: "Hello",
        ConversationKeyVersion: conversationKeyVersion, SigningKeyVersion: signingKeyVersion,
    })
    // body: message_id, encoded_message_create_event, encoded_message_event_signature
    nameCt, err := chat.Encrypt("Group title", rawKey)
    title, err := chat.Decrypt(nameCt, rawKey)
    _ = payload
    _ = title
    _ = err
    ```
  </Tab>

  <Tab title="C#">
    ```csharp theme={null}
    var payload = chat.EncryptMessage(new EncryptMessageParams {
        MessageId = messageId, SenderId = senderId, ConversationId = conversationId,
        ConversationKey = rawKey, Text = "Hello",
        ConversationKeyVersion = conversationKeyVersion, SigningKeyVersion = signingKeyVersion,
    });
    // Map EncryptedContent / EncodedEventSignature into the send-message body
    var nameCt = chat.Encrypt("Group title", rawKey);
    var title = chat.Decrypt(nameCt, rawKey);
    ```
  </Tab>

  <Tab title="Java">
    ```java theme={null}
    EncryptMessageParams params = new EncryptMessageParams();
    params.messageId = messageId;
    params.senderId = senderId;
    params.conversationId = conversationId;
    params.conversationKey = rawKey;
    params.text = "Hello";
    params.conversationKeyVersion = conversationKeyVersion;
    params.signingKeyVersion = signingKeyVersion;
    SendPayload payload = chat.encryptMessage(params);
    // Map to encoded_message_create_event / encoded_message_event_signature on POST

    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`)이 아닙니다. 전체 업로드/다운로드 흐름: [미디어](/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()`는 청크(각 약 1 MB)로 `push(chunk)`를 통해 공급한 다음 마지막에 `finish()`를 한 번 호출하는 `StreamEncryptor` / `StreamDecryptor`를 반환합니다. 복호화 시 `finish()`는 잘린 스트림을 감지합니다(마지막 프레임 전에 입력이 끝났으면 실패), 그러므로 성공하기 전까지 푸시된 평문을 완료된 것으로 취급하지 마세요.

<Warning>
  **JS/WASM 전용:** `finish()`는 기본 WASM 객체를 소비하고 해제합니다—`finish()` 후에는 절대 `free()`를 호출하지 마세요(예외를 던집니다). `finish()` *이전*에 스트림을 포기하는 경우에만 `free()`를 호출하세요(예: 오류 경로에서).
</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` 및 관련 암호화 헬퍼의 반환값; Chat API 전송 본문에 매핑.
* **PublicKeyRegistrationPayload** — 공개 키 추가 API를 위한 `generate_keypairs` / 공개 키 게터의 출력.
* **SigningKeyEntry** — 서명 검증을 위해 복호화에 전달되는 발신자 공개 자료.
* **PreparedConversationChange** — 세 가지 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`.

전체 필드 목록은 [chat-xdk 저장소](https://github.com/xdevplatform/chat-xdk)의 언어 스텁(`docs/API.md`, `*.pyi`, `index.d.ts`)을 사용하세요.

***

## 오류

Python은 일반적으로 설명적인 메시지와 함께 \*\*`ValueError`\*\*를 발생시킵니다(예: 잘못된 패스코드). TypeScript/JavaScript는 \*\*`Error`\*\*를 던집니다. Go는 `(value, error)`를 반환합니다. 이력의 경우 하나의 잘못된 이벤트가 배치를 중단시키지 않도록 \*\*`decrypt_events`\*\*를 선호하세요; 부분적 실패에 대해서는 errors 컬렉션을 검사하세요.

일부 검증 오류는 **영구적**입니다. 서명은 불변이며 이벤트 자체에서 서명된 페이로드를 재구축하여 검증되므로, `signature missing or no matching signing key` 또는 ECDSA 불일치로 실패하는 오래된 이벤트는 이후 로드 시마다 실패합니다—재시도, 키 갱신, 또는 API 호출로도 치유할 수 없습니다. 이러한 것을 일시적 오류가 아닌 툼스톤으로 취급하세요. 대화 키를 교체하면 그 시점 이후부터 깨끗하고 검증 가능한 이력이 시작됩니다.

***

## 다음 단계

<CardGroup cols={2}>
  <Card title="시작하기" icon="rocket" href="/xchat/getting-started">
    Chat XDK를 Chat API에 연결하기
  </Card>

  <Card title="미디어" icon="image" href="/xchat/media">
    스트림 암호화 및 미디어 REST
  </Card>

  <Card title="실시간 이벤트" icon="bolt" href="/xchat/real-time-events">
    웹훅 및 활동 전달
  </Card>

  <Card title="문제 해결" icon="wrench" href="/xchat/troubleshooting">
    일반적인 실패
  </Card>
</CardGroup>
