diff --git a/crates/core/src/chat.rs b/crates/core/src/chat.rs index 7ea8be9..33befbd 100644 --- a/crates/core/src/chat.rs +++ b/crates/core/src/chat.rs @@ -244,10 +244,15 @@ impl Chat { self.inner.import_keys(&secret)?; Ok(()) } + // Invalid PIN carries the remaining-attempt count from Juicebox so + // callers can warn the user before the guess budget runs out. RecoverResult::Failure { - reason, - guesses_remaining: _, - } => Err(SdkError::Juicebox(reason.into())), + reason: crate::keys::juicebox::RecoverFailureReason::InvalidPin, + guesses_remaining, + } => Err(SdkError::Juicebox(JuiceboxError::InvalidPin { + guesses_remaining, + })), + RecoverResult::Failure { reason, .. } => Err(SdkError::Juicebox(reason.into())), RecoverResult::KeyReconstructionFailed => Err(SdkError::Key( KeyError::ReconstructionFailed("Failed to reconstruct keys".into()), )), @@ -567,6 +572,100 @@ mod tests { assert!(!keys_after.identity.is_empty()); } + /// A Juicebox stub whose recover always fails with an invalid PIN, + /// reporting a fixed remaining-guess count. + struct InvalidPinJuicebox { + guesses_remaining: Option, + } + + #[async_trait::async_trait] + impl crate::keys::juicebox::JuiceboxApi for InvalidPinJuicebox { + async fn register_private_key( + &self, + _pin: &[u8], + _config: &JuiceboxConfig, + _secret: &[u8], + ) -> RegisterResult { + RegisterResult::Success + } + + async fn recover_private_key( + &self, + _pin: &[u8], + _config: &JuiceboxConfig, + ) -> crate::keys::juicebox::RecoverResult { + crate::keys::juicebox::RecoverResult::Failure { + reason: crate::keys::juicebox::RecoverFailureReason::InvalidPin, + guesses_remaining: self.guesses_remaining, + } + } + + async fn delete_keys( + &self, + _config: &JuiceboxConfig, + ) -> crate::keys::juicebox::DeleteResult { + crate::keys::juicebox::DeleteResult::Success + } + } + + #[tokio::test] + async fn test_unlock_invalid_pin_carries_guesses_remaining() { + let chat = Chat::with_juicebox( + test_config(), + Arc::new(InvalidPinJuicebox { + guesses_remaining: Some(3), + }), + ); + let err = chat.unlock(b"0000").await.unwrap_err(); + match &err { + SdkError::Juicebox(JuiceboxError::InvalidPin { guesses_remaining }) => { + assert_eq!(*guesses_remaining, Some(3)); + } + other => panic!("expected InvalidPin, got {other:?}"), + } + assert_eq!( + err.to_string(), + "Juicebox error: Invalid PIN: guesses_remaining=3" + ); + } + + #[tokio::test] + async fn test_unlock_invalid_pin_without_count() { + let chat = Chat::with_juicebox( + test_config(), + Arc::new(InvalidPinJuicebox { + guesses_remaining: None, + }), + ); + let err = chat.unlock(b"0000").await.unwrap_err(); + assert!(matches!( + err, + SdkError::Juicebox(JuiceboxError::InvalidPin { + guesses_remaining: None + }) + )); + assert_eq!(err.to_string(), "Juicebox error: Invalid PIN"); + } + + /// `change_pin` unlocks with the old PIN first, so a wrong old PIN + /// surfaces the same count-carrying error as `unlock`. + #[tokio::test] + async fn test_change_pin_invalid_old_pin_carries_guesses_remaining() { + let chat = Chat::with_juicebox( + test_config(), + Arc::new(InvalidPinJuicebox { + guesses_remaining: Some(0), + }), + ); + let err = chat.change_pin(b"0000", b"2580").await.unwrap_err(); + assert!(matches!( + err, + SdkError::Juicebox(JuiceboxError::InvalidPin { + guesses_remaining: Some(0) + }) + )); + } + #[tokio::test] async fn test_encrypt_decrypt_roundtrip() { let chat = Chat::with_juicebox(test_config(), Arc::new(MockJuiceboxApi::new())); diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 62cf137..b578815 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -93,9 +93,16 @@ pub enum KeyError { /// Errors from Juicebox SDK integration. #[derive(Debug, Error)] pub enum JuiceboxError { - /// Wrong PIN provided. - #[error("Invalid PIN")] - InvalidPin, + /// Wrong PIN provided. `guesses_remaining` is the attempt budget Juicebox + /// reports after the failure (0 = exhausted, keys locked); `None` when the + /// count is unavailable. The `guesses_remaining=N` message token is stable + /// — bindings parse it out of the error string, so the format must not + /// change (the bare `Invalid PIN` prefix likewise stays as-is). + #[error("Invalid PIN{}", .guesses_remaining.map(|n| format!(": guesses_remaining={n}")).unwrap_or_default())] + InvalidPin { + /// Remaining PIN attempts reported by Juicebox, if known. + guesses_remaining: Option, + }, /// Keys not registered yet. #[error("Keys not registered")] @@ -141,15 +148,17 @@ pub enum JuiceboxError { impl JuiceboxError { /// Returns true if this error is retryable. pub fn is_retryable(&self) -> bool { - matches!( - self, - JuiceboxError::InvalidPin - | JuiceboxError::NotRegistered - | JuiceboxError::InvalidAuth - | JuiceboxError::Transient - | JuiceboxError::RateLimitExceeded - | JuiceboxError::StorageFailed - ) + match self { + // A zero remaining budget means the stored keys are locked; + // another attempt cannot succeed. + JuiceboxError::InvalidPin { guesses_remaining } => *guesses_remaining != Some(0), + JuiceboxError::NotRegistered + | JuiceboxError::InvalidAuth + | JuiceboxError::Transient + | JuiceboxError::RateLimitExceeded + | JuiceboxError::StorageFailed => true, + _ => false, + } } } @@ -213,7 +222,9 @@ mod tests { #[test] fn test_sdk_error_from_juicebox() { - let jb = JuiceboxError::InvalidPin; + let jb = JuiceboxError::InvalidPin { + guesses_remaining: None, + }; let sdk: SdkError = jb.into(); assert!(matches!(sdk, SdkError::Juicebox(_))); } @@ -264,7 +275,20 @@ mod tests { #[test] fn test_juicebox_error_retryable() { - assert!(JuiceboxError::InvalidPin.is_retryable()); + assert!(JuiceboxError::InvalidPin { + guesses_remaining: None + } + .is_retryable()); + assert!(JuiceboxError::InvalidPin { + guesses_remaining: Some(1) + } + .is_retryable()); + // An exhausted guess budget locks the stored keys; retrying a PIN + // cannot succeed. + assert!(!JuiceboxError::InvalidPin { + guesses_remaining: Some(0) + } + .is_retryable()); assert!(JuiceboxError::NotRegistered.is_retryable()); assert!(JuiceboxError::InvalidAuth.is_retryable()); assert!(JuiceboxError::Transient.is_retryable()); @@ -279,7 +303,27 @@ mod tests { #[test] fn test_juicebox_error_display() { - assert!(JuiceboxError::InvalidPin.to_string().contains("PIN")); + assert_eq!( + JuiceboxError::InvalidPin { + guesses_remaining: None + } + .to_string(), + "Invalid PIN" + ); + assert_eq!( + JuiceboxError::InvalidPin { + guesses_remaining: Some(3) + } + .to_string(), + "Invalid PIN: guesses_remaining=3" + ); + assert_eq!( + JuiceboxError::InvalidPin { + guesses_remaining: Some(0) + } + .to_string(), + "Invalid PIN: guesses_remaining=0" + ); assert!(JuiceboxError::NoTokens.to_string().contains("token")); assert!(JuiceboxError::Other("custom".into()) .to_string() diff --git a/crates/core/src/keys/juicebox.rs b/crates/core/src/keys/juicebox.rs index 61b3ba2..8cbdc5c 100644 --- a/crates/core/src/keys/juicebox.rs +++ b/crates/core/src/keys/juicebox.rs @@ -579,7 +579,11 @@ impl JuiceboxApi for JuiceboxClient { impl From for JuiceboxError { fn from(reason: RecoverFailureReason) -> Self { match reason { - RecoverFailureReason::InvalidPin => JuiceboxError::InvalidPin, + // Conversion from the bare reason has no count; `Chat::unlock` + // builds the variant directly to carry `guesses_remaining`. + RecoverFailureReason::InvalidPin => JuiceboxError::InvalidPin { + guesses_remaining: None, + }, RecoverFailureReason::NotRegistered => JuiceboxError::NotRegistered, RecoverFailureReason::InvalidAuth => JuiceboxError::InvalidAuth, RecoverFailureReason::UpgradeRequired => JuiceboxError::UpgradeRequired, diff --git a/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs b/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs index 7c0fdd6..351e523 100644 --- a/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs +++ b/crates/dotnet/dotnet/ChatXdk.Tests/ChatTests.cs @@ -163,6 +163,29 @@ public void UpdateConfig_RejectsMalformedKeyStoreTokenMapJson() Assert.Contains("Invalid key_store_token_map_json", ex.Message); } + [Fact] + public void GuessesRemaining_ParsedFromInvalidPinMessage() + { + // The core's invalid-PIN unlock error carries the stable + // "guesses_remaining=N" token in the message; 0 means exhausted. + Assert.Equal(3, + new ChatXdkException("Juicebox error: Invalid PIN: guesses_remaining=3").GuessesRemaining); + Assert.Equal(0, + new ChatXdkException("Juicebox error: Invalid PIN: guesses_remaining=0").GuessesRemaining); + Assert.Null(new ChatXdkException("Juicebox error: Invalid PIN").GuessesRemaining); + // The count is read only from the invalid-PIN form, not from + // unrelated messages that happen to contain the token. + Assert.Null(new ChatXdkException("Delete failed: guesses_remaining=7").GuessesRemaining); + } + + [Fact] + public void GuessesRemaining_NullOnNonPinErrors() + { + using var chat = new Chat(); + var ex = Assert.Throws(() => chat.UpdateConfig("not json")); + Assert.Null(ex.GuessesRemaining); + } + // Key generation [Fact] diff --git a/crates/dotnet/dotnet/ChatXdk/Chat.cs b/crates/dotnet/dotnet/ChatXdk/Chat.cs index d7c267a..85a303b 100644 --- a/crates/dotnet/dotnet/ChatXdk/Chat.cs +++ b/crates/dotnet/dotnet/ChatXdk/Chat.cs @@ -1130,6 +1130,29 @@ private static object[][] SerialiseEntities(IReadOnlyList enti /// public sealed class ChatXdkException : Exception { - public ChatXdkException(string message) : base(message) { } + // Stable invalid-PIN message form the core emits + // ("Invalid PIN: guesses_remaining=N"). Anchored on the full form so a + // count embedded in an unrelated pass-through message is not misread. + private static readonly System.Text.RegularExpressions.Regex GuessesRemainingPattern = + new(@"\bInvalid PIN: guesses_remaining=(\d+)", System.Text.RegularExpressions.RegexOptions.Compiled); + + public ChatXdkException(string message) : base(message) + { + var match = GuessesRemainingPattern.Match(message ?? ""); + if (match.Success && int.TryParse(match.Groups[1].Value, out var n)) + { + GuessesRemaining = n; + } + } + + /// + /// Remaining PIN attempts reported by Juicebox, or when the + /// message carries no count. + /// + /// Present only on invalid-PIN / + /// failures. 0 means the guess budget is + /// exhausted and the stored keys are locked. + /// + public int? GuessesRemaining { get; } } } diff --git a/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkException.java b/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkException.java index c8bfc3d..bf9f01d 100644 --- a/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkException.java +++ b/crates/jvm/java/chatxdk/src/main/java/com/x/chatxdk/ChatXdkException.java @@ -1,8 +1,44 @@ package com.x.chatxdk; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** Thrown when the native chat-xdk library returns an error string. */ public class ChatXdkException extends RuntimeException { + + /** + * Stable invalid-PIN message form the core emits ("Invalid PIN: guesses_remaining=N"). + * Anchored on the full form so a count embedded in an unrelated pass-through message is + * not misread. + */ + private static final Pattern GUESSES_REMAINING = + Pattern.compile("\\bInvalid PIN: guesses_remaining=(\\d+)"); + public ChatXdkException(String message) { super(message); } + + /** + * Remaining PIN attempts reported by Juicebox, or {@code null} when the message carries no + * count. + * + *

Present only on invalid-PIN {@link Chat#unlock} / {@link Chat#changePin} failures. + * {@code 0} means the guess budget is exhausted and the stored keys are locked. + */ + public Integer getGuessesRemaining() { + String message = getMessage(); + if (message == null) { + return null; + } + Matcher m = GUESSES_REMAINING.matcher(message); + if (!m.find()) { + return null; + } + try { + return Integer.valueOf(m.group(1)); + } catch (NumberFormatException e) { + // Digits too large for an int — treat as no usable count. + return null; + } + } } diff --git a/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java b/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java index 890265b..07451b1 100644 --- a/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java +++ b/crates/jvm/java/chatxdk/src/test/java/com/x/chatxdk/ChatTest.java @@ -123,6 +123,36 @@ void updateConfigRejectsMalformedKeyStoreTokenMapJson() { } } + @Test + void guessesRemainingParsedFromInvalidPinMessage() { + // The core's invalid-PIN unlock error carries the stable + // "guesses_remaining=N" token in the message; 0 means exhausted. + assertEquals( + 3, + new ChatXdkException("Juicebox error: Invalid PIN: guesses_remaining=3") + .getGuessesRemaining()); + assertEquals( + 0, + new ChatXdkException("Juicebox error: Invalid PIN: guesses_remaining=0") + .getGuessesRemaining()); + assertNull( + new ChatXdkException("Juicebox error: Invalid PIN").getGuessesRemaining()); + // The count is read only from the invalid-PIN form, not from + // unrelated messages that happen to contain the token. + assertNull( + new ChatXdkException("Delete failed: guesses_remaining=7") + .getGuessesRemaining()); + } + + @Test + void guessesRemainingNullOnNonPinErrors() { + try (Chat chat = new Chat()) { + ChatXdkException ex = + assertThrows(ChatXdkException.class, () -> chat.updateConfig("not json")); + assertNull(ex.getGuessesRemaining()); + } + } + @Test void generateKeypairsReturnsValidPayload() throws Exception { try (Chat chat = new Chat()) { diff --git a/crates/pyo3/python/chat_xdk/__init__.py b/crates/pyo3/python/chat_xdk/__init__.py index 40c7ece..5f349bc 100644 --- a/crates/pyo3/python/chat_xdk/__init__.py +++ b/crates/pyo3/python/chat_xdk/__init__.py @@ -23,6 +23,8 @@ __version__ = "0.1.0" +import re as _re + from chat_xdk._native import ( Chat, PublicKeyRegistration, @@ -40,6 +42,23 @@ hex_to_bytes, ) +# Stable invalid-PIN message form the core emits +# ("Invalid PIN: guesses_remaining=N"). Anchored on the full form so a count +# embedded in an unrelated pass-through message is not misread. +_GUESSES_REMAINING = _re.compile(r"\bInvalid PIN: guesses_remaining=(\d+)") + + +def guesses_remaining(exc): + """Remaining PIN attempts from an invalid-PIN unlock failure, or None. + + Present only on the exception raised by ``Chat.unlock`` / + ``Chat.change_pin`` for a wrong PIN; 0 means the guess budget is + exhausted and the stored keys are locked. + """ + match = _GUESSES_REMAINING.search(str(exc)) + return int(match.group(1)) if match else None + + __all__ = [ "Chat", "PublicKeyRegistration", @@ -54,5 +73,6 @@ "bytes_to_hex", "detect_image_dimensions", "detect_mime_type", + "guesses_remaining", "hex_to_bytes", ] diff --git a/crates/pyo3/python/chat_xdk/__init__.pyi b/crates/pyo3/python/chat_xdk/__init__.pyi index d74e0a1..b366963 100644 --- a/crates/pyo3/python/chat_xdk/__init__.pyi +++ b/crates/pyo3/python/chat_xdk/__init__.pyi @@ -274,3 +274,4 @@ def bytes_to_hex(data: bytes) -> str: ... def hex_to_bytes(hex: str) -> Optional[bytes]: ... def detect_mime_type(data: bytes) -> Optional[str]: ... def detect_image_dimensions(data: bytes) -> Optional[tuple[int, int]]: ... +def guesses_remaining(exc: BaseException) -> Optional[int]: ... diff --git a/crates/pyo3/python/tests/test_api.py b/crates/pyo3/python/tests/test_api.py index f21b405..9c5a058 100644 --- a/crates/pyo3/python/tests/test_api.py +++ b/crates/pyo3/python/tests/test_api.py @@ -183,6 +183,32 @@ def test_x_api_juicebox_config_shape(self): chat.update_config(bad_config) self.assertIn("Invalid key_store_token_map_json", str(ctx.exception)) + def test_guesses_remaining_parses_invalid_pin_message(self): + from chat_xdk import guesses_remaining + + # The core's invalid-PIN unlock error carries the stable + # "guesses_remaining=N" token in the message; 0 means exhausted. + self.assertEqual( + guesses_remaining(ValueError("Juicebox error: Invalid PIN: guesses_remaining=3")), + 3, + ) + self.assertEqual( + guesses_remaining(ValueError("Juicebox error: Invalid PIN: guesses_remaining=0")), + 0, + ) + self.assertIsNone(guesses_remaining(ValueError("Juicebox error: Invalid PIN"))) + # The count is read only from the invalid-PIN form, not from unrelated + # messages that happen to contain the token. + self.assertIsNone(guesses_remaining(ValueError("Delete failed: guesses_remaining=7"))) + + def test_guesses_remaining_none_on_non_pin_errors(self): + from chat_xdk import Chat, guesses_remaining + + chat = Chat() + with self.assertRaises(ValueError) as ctx: + chat.update_config("not json") + self.assertIsNone(guesses_remaining(ctx.exception)) + def test_pin_accepts_str_bytes_and_bytearray(self): from chat_xdk import Chat diff --git a/crates/wasm/js/index.d.ts b/crates/wasm/js/index.d.ts index 13482b4..caae779 100644 --- a/crates/wasm/js/index.d.ts +++ b/crates/wasm/js/index.d.ts @@ -1072,6 +1072,13 @@ export declare function createChat(options: CreateChatOptions): Promise "stub-token", + juiceboxModule: { Client: InvalidPinClient, Configuration: StubJuiceboxConfiguration }, + }); + let unlockErr; + try { + await chat.unlock("2580"); + } catch (err) { + unlockErr = err; + } + assert.equal(guessesRemaining(unlockErr), 3); + + // No count on non-PIN failures, unrelated messages that happen to carry + // the token, or non-errors. + assert.equal(guessesRemaining(new Error("Juicebox recovery failed: reason=Transient")), null); + assert.equal(guessesRemaining(new Error("delete failed: guesses_remaining=7")), null); + assert.equal(guessesRemaining(undefined), null); +} + async function main() { await delegationTests(); await guessBudgetTests(); await firstBootTests(); + await guessesRemainingTests(); console.log("wrapper.test.mjs: all assertions passed"); } diff --git a/docs/API.md b/docs/API.md index 27fe6f3..6639685 100644 --- a/docs/API.md +++ b/docs/API.md @@ -116,6 +116,18 @@ Rust core takes PINs as `&[u8]` and Go takes `[]byte` so callers can zeroize their buffers; the JS wrapper additionally accepts `Uint8Array` PINs for the same reason. +A wrong PIN fails `unlock` (and `change_pin`, which unlocks with the old PIN +first) with an invalid-PIN error whose message carries the stable token +`guesses_remaining=N` — the attempt budget Juicebox reports after the failure. +`0` means the budget is exhausted and the stored keys are locked; the token is +absent when no count is available. Read it structurally instead of parsing the +message: Rust `JuiceboxError::InvalidPin { guesses_remaining: Option }`, +JVM `ChatXdkException.getGuessesRemaining()` (`Integer`, `null` when absent), +.NET `ChatXdkException.GuessesRemaining` (`int?`), Go +`GuessesRemaining(err) (int, bool)`, Python `chat_xdk.guesses_remaining(exc)` +(`int | None`), JS `guessesRemaining(err)` (`number | null`, reading the +wrapper's `reason=InvalidPin guesses_remaining=N` error form). + ### Conversation Keys | # | Method | Rust | JS | Python | Go | JVM | .NET | diff --git a/go/chatxdk/chat_juicebox.go b/go/chatxdk/chat_juicebox.go index 8f1c5e1..8d0fdd8 100644 --- a/go/chatxdk/chat_juicebox.go +++ b/go/chatxdk/chat_juicebox.go @@ -4,7 +4,9 @@ package chatxdk import ( "encoding/json" + "regexp" "runtime" + "strconv" ) // UpdateConfig updates the Juicebox configuration (e.g., to refresh auth tokens). @@ -55,3 +57,27 @@ func (c *Chat) ChangePin(oldPin, newPin []byte) error { _, err := ffiChangePin(c.h, oldPin, newPin) return err } + +// Stable invalid-PIN message form the core emits +// ("Invalid PIN: guesses_remaining=N"). Anchored on the full form so a count +// embedded in an unrelated pass-through message is not misread. +var guessesRemainingPattern = regexp.MustCompile(`\bInvalid PIN: guesses_remaining=(\d+)`) + +// GuessesRemaining extracts the remaining PIN-attempt count Juicebox reports +// on an invalid-PIN [Chat.Unlock] / [Chat.ChangePin] failure. It returns +// ok=false when the error carries no count (any non-PIN failure). A count of +// 0 means the guess budget is exhausted and the stored keys are locked. +func GuessesRemaining(err error) (n int, ok bool) { + if err == nil { + return 0, false + } + m := guessesRemainingPattern.FindStringSubmatch(err.Error()) + if m == nil { + return 0, false + } + n, convErr := strconv.Atoi(m[1]) + if convErr != nil { + return 0, false + } + return n, true +} diff --git a/go/chatxdk/chatxdk_test.go b/go/chatxdk/chatxdk_test.go index 1e03433..11e6f0b 100644 --- a/go/chatxdk/chatxdk_test.go +++ b/go/chatxdk/chatxdk_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/binary" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -1477,6 +1478,40 @@ func TestUpdateConfigInvalid(t *testing.T) { } } +// TestGuessesRemaining pins the parsing of the stable "guesses_remaining=N" +// token the core emits on invalid-PIN unlock failures; 0 means the guess +// budget is exhausted. Errors without the token report ok=false. +func TestGuessesRemaining(t *testing.T) { + if n, ok := GuessesRemaining(errors.New("Juicebox error: Invalid PIN: guesses_remaining=3")); !ok || n != 3 { + t.Errorf("expected (3, true), got (%d, %v)", n, ok) + } + if n, ok := GuessesRemaining(errors.New("Juicebox error: Invalid PIN: guesses_remaining=0")); !ok || n != 0 { + t.Errorf("expected (0, true), got (%d, %v)", n, ok) + } + if _, ok := GuessesRemaining(errors.New("Juicebox error: Invalid PIN")); ok { + t.Error("expected ok=false without the token") + } + // The count is read only from the invalid-PIN form, not from unrelated + // messages that happen to contain the token. + if _, ok := GuessesRemaining(errors.New("Delete failed: guesses_remaining=7")); ok { + t.Error("expected ok=false for a non-PIN message carrying the token") + } + if _, ok := GuessesRemaining(nil); ok { + t.Error("expected ok=false for nil error") + } + + // A real error from the binding's own path carries no count. + chat := New() + defer chat.Close() + err := chat.UpdateConfig("not-valid-json") + if err == nil { + t.Fatal("expected error for invalid JSON config") + } + if _, ok := GuessesRemaining(err); ok { + t.Error("expected ok=false for a non-PIN error") + } +} + func TestUpdateConfigXAPIJuiceboxConfigShape(t *testing.T) { chat := New() defer chat.Close() diff --git a/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a b/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a index 3443c3a..fd12640 100644 Binary files a/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a and b/go/chatxdk/libs/darwin_amd64/libchat_xdk_go.a differ diff --git a/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a b/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a index 158014e..561336c 100644 Binary files a/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a and b/go/chatxdk/libs/darwin_arm64/libchat_xdk_go.a differ diff --git a/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a b/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a index 9a6d018..b374e5b 100644 Binary files a/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a and b/go/chatxdk/libs/linux_amd64/libchat_xdk_go.a differ diff --git a/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a b/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a index 625c634..2313f0b 100644 Binary files a/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a and b/go/chatxdk/libs/linux_amd64_musl/libchat_xdk_go.a differ