@antzsoft/chat-core 1.2.4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -99,24 +99,40 @@ var init_chat_store = __esm({
99
99
  // src/index.ts
100
100
  var src_exports = {};
101
101
  __export(src_exports, {
102
+ AntzChatAuthError: () => AntzChatAuthError,
102
103
  AntzChatClient: () => AntzChatClient,
104
+ AntzChatError: () => AntzChatError,
105
+ AntzChatNetworkError: () => AntzChatNetworkError,
106
+ AntzChatPermissionError: () => AntzChatPermissionError,
107
+ AntzChatServerError: () => AntzChatServerError,
108
+ AntzChatValidationError: () => AntzChatValidationError,
103
109
  appConfigApi: () => appConfigApi,
104
110
  authApi: () => authApi,
105
111
  connectSocket: () => connectSocket,
106
112
  conversationsApi: () => conversationsApi,
107
113
  createAuthStore: () => createAuthStore,
114
+ createRestTransitSession: () => createRestTransitSession,
115
+ decryptPayload: () => decryptPayload,
108
116
  devicesApi: () => devicesApi,
109
117
  disconnectSocket: () => disconnectSocket,
118
+ encryptPayload: () => encryptPayload,
119
+ fetchServerKeys: () => fetchServerKeys,
120
+ generateEphemeralKey: () => generateEphemeralKey,
110
121
  getApiClient: () => getApiClient,
111
122
  getAuthStore: () => getAuthStore,
112
123
  getCompressionStrategy: () => getCompressionStrategy,
124
+ getSessionId: () => getSessionId,
125
+ getSessionKey: () => getSessionKey,
113
126
  getSocket: () => getSocket,
114
127
  getSocketStatus: () => getSocketStatus,
115
128
  initApiClient: () => initApiClient,
116
129
  initAuthStore: () => initAuthStore,
130
+ isTransitEnvelope: () => isTransitEnvelope,
117
131
  messagesApi: () => messagesApi,
132
+ normalizeAxiosError: () => normalizeAxiosError,
118
133
  normalizeConversation: () => normalizeConversation,
119
134
  onSocketStatus: () => onSocketStatus,
135
+ performHandshake: () => performHandshake,
120
136
  reconnectSocket: () => reconnectSocket,
121
137
  refreshSocketAuth: () => refreshSocketAuth,
122
138
  resetAuthStore: () => resetAuthStore,
@@ -256,7 +272,7 @@ async function compressFile(file, platformCompressFn, config) {
256
272
  }
257
273
 
258
274
  // src/api/client.ts
259
- var import_axios = __toESM(require("axios"), 1);
275
+ var import_axios2 = __toESM(require("axios"), 1);
260
276
 
261
277
  // src/crypto/transit.ts
262
278
  function hasWebCrypto() {
@@ -391,6 +407,9 @@ function setTransitSession(session) {
391
407
  s.readyResolve?.();
392
408
  s.readyResolve = null;
393
409
  }
410
+ function getTransitSession() {
411
+ return getState().session;
412
+ }
394
413
  function clearTransitSession() {
395
414
  const s = getState();
396
415
  s.session = null;
@@ -407,19 +426,270 @@ function getSessionId() {
407
426
  return getState().session?.sessionId ?? null;
408
427
  }
409
428
 
429
+ // src/crypto/detect.ts
430
+ var _cached = null;
431
+ async function detectTransitAlgo() {
432
+ if (_cached) return _cached;
433
+ try {
434
+ await globalThis.crypto.subtle.generateKey(
435
+ { name: "X25519" },
436
+ false,
437
+ ["deriveKey"]
438
+ );
439
+ _cached = "x25519";
440
+ } catch {
441
+ _cached = "p256";
442
+ }
443
+ return _cached;
444
+ }
445
+ function resetAlgoCache() {
446
+ _cached = null;
447
+ }
448
+
449
+ // src/crypto/handshake.ts
450
+ function hasWebCrypto2() {
451
+ return typeof globalThis.crypto?.subtle !== "undefined";
452
+ }
453
+ async function fetchServerKeys(apiUrl) {
454
+ const res = await fetch(`${apiUrl}/crypto/pubkey`);
455
+ if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
456
+ const body = await res.json();
457
+ return body?.data ?? body;
458
+ }
459
+ async function generateEphemeralKey(algo, serverKeys) {
460
+ if (hasWebCrypto2()) {
461
+ return generateWebCryptoEphemeralKey(algo, serverKeys);
462
+ }
463
+ return generateNobleEphemeralKey(serverKeys);
464
+ }
465
+ async function createRestTransitSession(apiUrl) {
466
+ try {
467
+ const serverKeys = await fetchServerKeys(apiUrl);
468
+ if (!serverKeys.enabled) return null;
469
+ const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
470
+ const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
471
+ const res = await fetch(`${apiUrl}/crypto/session`, {
472
+ method: "POST",
473
+ headers: { "Content-Type": "application/json" },
474
+ body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
475
+ });
476
+ if (!res.ok) return null;
477
+ const body = await res.json();
478
+ const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
479
+ if (!sessionId) return null;
480
+ const sessionKey = await deriveSessionKey(sessionId);
481
+ return { sessionId, sessionKey };
482
+ } catch {
483
+ return null;
484
+ }
485
+ }
486
+ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
487
+ const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
488
+ socketHandshakeAuth["transitEphemeralPub"] = ephemeralPubB64;
489
+ socketHandshakeAuth["transitAlgo"] = algo;
490
+ return deriveSessionKey;
491
+ }
492
+ async function generateWebCryptoEphemeralKey(algo, serverKeys) {
493
+ const ephemeral = await globalThis.crypto.subtle.generateKey(
494
+ algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
495
+ false,
496
+ ["deriveBits"]
497
+ );
498
+ const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
499
+ const ephemeralPriv = ephemeral.privateKey;
500
+ return {
501
+ ephemeralPubB64: bufToB642(pubRaw),
502
+ deriveSessionKey: (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId)
503
+ };
504
+ }
505
+ async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
506
+ const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
507
+ const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
508
+ const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
509
+ const sharedBits = await globalThis.crypto.subtle.deriveBits(
510
+ { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
511
+ ephemeralPriv,
512
+ 256
513
+ );
514
+ const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
515
+ const salt = new TextEncoder().encode(sessionId);
516
+ const info = new TextEncoder().encode("antz-transit-v1");
517
+ return globalThis.crypto.subtle.deriveKey(
518
+ { name: "HKDF", hash: "SHA-256", salt, info },
519
+ hkdfKey,
520
+ { name: "AES-GCM", length: 256 },
521
+ false,
522
+ ["encrypt", "decrypt"]
523
+ );
524
+ }
525
+ async function generateNobleEphemeralKey(serverKeys) {
526
+ const { x25519 } = await import("@noble/curves/ed25519");
527
+ const { hkdf } = await import("@noble/hashes/hkdf");
528
+ const { sha256 } = await import("@noble/hashes/sha256");
529
+ const { randomBytes } = await import("@noble/hashes/utils");
530
+ const ephemeralPriv = randomBytes(32);
531
+ const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
532
+ const serverPubBytes = base64ToUint82(serverKeys.x25519);
533
+ return {
534
+ ephemeralPubB64: uint8ToBase64(ephemeralPub),
535
+ deriveSessionKey: (sessionId) => {
536
+ const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
537
+ const salt = new TextEncoder().encode(sessionId);
538
+ const info = new TextEncoder().encode("antz-transit-v1");
539
+ return Promise.resolve(hkdf(sha256, sharedSecret, salt, info, 32));
540
+ }
541
+ };
542
+ }
543
+ function bufToB642(buf) {
544
+ const bytes = new Uint8Array(buf);
545
+ let str = "";
546
+ bytes.forEach((b) => {
547
+ str += String.fromCharCode(b);
548
+ });
549
+ return btoa(str);
550
+ }
551
+ function b64ToBuf2(b64) {
552
+ const bin = atob(b64);
553
+ const buf = new Uint8Array(bin.length);
554
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
555
+ return buf.buffer;
556
+ }
557
+ function uint8ToBase64(bytes) {
558
+ let str = "";
559
+ bytes.forEach((b) => {
560
+ str += String.fromCharCode(b);
561
+ });
562
+ return btoa(str);
563
+ }
564
+ function base64ToUint82(b64) {
565
+ const bin = atob(b64);
566
+ const buf = new Uint8Array(bin.length);
567
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
568
+ return buf;
569
+ }
570
+
571
+ // src/errors.ts
572
+ var import_axios = require("axios");
573
+ var AntzChatError = class extends Error {
574
+ constructor(code, message, retryable = false, context) {
575
+ super(message);
576
+ this.name = "AntzChatError";
577
+ this.code = code;
578
+ this.retryable = retryable;
579
+ this.context = context;
580
+ Object.setPrototypeOf(this, new.target.prototype);
581
+ }
582
+ };
583
+ var AntzChatAuthError = class extends AntzChatError {
584
+ constructor(message, code = "AUTH_FAILED", context) {
585
+ super(code, message, false, context);
586
+ this.name = "AntzChatAuthError";
587
+ Object.setPrototypeOf(this, new.target.prototype);
588
+ }
589
+ };
590
+ var AntzChatValidationError = class extends AntzChatError {
591
+ constructor(message, context) {
592
+ const msg = Array.isArray(message) ? message.join("; ") : message;
593
+ super("VALIDATION_ERROR", msg, false, context);
594
+ this.name = "AntzChatValidationError";
595
+ this.fields = Array.isArray(message) ? message : void 0;
596
+ Object.setPrototypeOf(this, new.target.prototype);
597
+ }
598
+ };
599
+ var AntzChatNetworkError = class extends AntzChatError {
600
+ constructor(message, code = "NETWORK_ERROR", context) {
601
+ super(code, message, true, context);
602
+ this.name = "AntzChatNetworkError";
603
+ Object.setPrototypeOf(this, new.target.prototype);
604
+ }
605
+ };
606
+ var AntzChatPermissionError = class extends AntzChatError {
607
+ constructor(message, context) {
608
+ super("PERMISSION_DENIED", message, false, context);
609
+ this.name = "AntzChatPermissionError";
610
+ Object.setPrototypeOf(this, new.target.prototype);
611
+ }
612
+ };
613
+ var AntzChatServerError = class extends AntzChatError {
614
+ constructor(message, httpStatus, context) {
615
+ super("SERVER_ERROR", message, true, context);
616
+ this.name = "AntzChatServerError";
617
+ this.httpStatus = httpStatus;
618
+ Object.setPrototypeOf(this, new.target.prototype);
619
+ }
620
+ };
621
+ function normalizeAxiosError(error) {
622
+ if (error instanceof AntzChatError) return error;
623
+ if ((0, import_axios.isAxiosError)(error)) {
624
+ const status = error.response?.status;
625
+ const body = error.response?.data;
626
+ const decryptionFailed = body != null && isTransitEnvelope(body);
627
+ const rawMessage = decryptionFailed ? void 0 : body?.message ?? void 0;
628
+ const message = (Array.isArray(rawMessage) ? rawMessage.join("; ") : rawMessage) || error.message || "Request failed";
629
+ const ctx = {
630
+ ...status != null && { httpStatus: status },
631
+ ...body?.path != null && { path: body.path },
632
+ ...body?.error != null && { serverError: body.error },
633
+ ...error.code != null && { axiosCode: error.code },
634
+ ...decryptionFailed && { decryptionFailed: true, note: "Transit decryption failed \u2014 server error body is an encrypted envelope" }
635
+ };
636
+ if (!error.response) {
637
+ return new AntzChatNetworkError(message || "Network error", "NETWORK_ERROR", ctx);
638
+ }
639
+ if (status === 401) {
640
+ const code = error.config?._retry ? "AUTH_FAILED" : "SESSION_EXPIRED";
641
+ return new AntzChatAuthError(message, code, ctx);
642
+ }
643
+ if (status === 403) return new AntzChatPermissionError(message, ctx);
644
+ if (status === 400 || status === 422) {
645
+ return new AntzChatValidationError(
646
+ Array.isArray(rawMessage) ? rawMessage : message,
647
+ ctx
648
+ );
649
+ }
650
+ if (status === 404) return new AntzChatServerError(message, 404, ctx);
651
+ if (status === 429) return new AntzChatNetworkError(message, "RATE_LIMITED", ctx);
652
+ if (status != null && status >= 500) return new AntzChatServerError(message, status, ctx);
653
+ return new AntzChatServerError(message, status, ctx);
654
+ }
655
+ const msg = error instanceof Error ? error.message : String(error);
656
+ return new AntzChatError("UNKNOWN_ERROR", msg, false);
657
+ }
658
+
410
659
  // src/api/client.ts
411
660
  var _tokenStore = null;
412
661
  var _config = null;
413
662
  var _avatarSent = false;
663
+ var _transitHandshakePromise = null;
664
+ function getTransitHandshakePromise() {
665
+ return _transitHandshakePromise;
666
+ }
414
667
  function initApiClient(config, tokenStore) {
415
668
  _config = config;
416
669
  _tokenStore = tokenStore;
417
670
  _avatarSent = false;
418
- const client = import_axios.default.create({
671
+ const client = import_axios2.default.create({
419
672
  baseURL: config.apiUrl,
420
673
  headers: { "Content-Type": "application/json" }
421
674
  });
422
675
  configureTransit(config.transitEncryption);
676
+ if (config.transitEncryption && !getTransitSession() && !_transitHandshakePromise) {
677
+ _transitHandshakePromise = (async () => {
678
+ try {
679
+ const session = await createRestTransitSession(config.apiUrl);
680
+ if (session && !getTransitSession()) {
681
+ const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
682
+ setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
683
+ } else if (!session) {
684
+ configureTransit(false);
685
+ }
686
+ } catch {
687
+ configureTransit(false);
688
+ } finally {
689
+ _transitHandshakePromise = null;
690
+ }
691
+ })();
692
+ }
423
693
  client.interceptors.request.use(async (req) => {
424
694
  const token = _tokenStore?.getAccessToken();
425
695
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
@@ -483,7 +753,7 @@ function initApiClient(config, tokenStore) {
483
753
  const refreshToken = _tokenStore?.getRefreshToken();
484
754
  if (!refreshToken) {
485
755
  _tokenStore?.clearTokens();
486
- return Promise.reject(error);
756
+ return Promise.reject(normalizeAxiosError(error));
487
757
  }
488
758
  if (isRefreshing) {
489
759
  return new Promise((resolve) => {
@@ -496,7 +766,7 @@ function initApiClient(config, tokenStore) {
496
766
  original._retry = true;
497
767
  isRefreshing = true;
498
768
  try {
499
- const { data } = await import_axios.default.post(
769
+ const { data } = await import_axios2.default.post(
500
770
  `${_config.apiUrl}/auth/refresh`,
501
771
  { refreshToken }
502
772
  );
@@ -508,12 +778,12 @@ function initApiClient(config, tokenStore) {
508
778
  return client(original);
509
779
  } catch {
510
780
  _tokenStore?.clearTokens();
511
- return Promise.reject(error);
781
+ return Promise.reject(normalizeAxiosError(error));
512
782
  } finally {
513
783
  isRefreshing = false;
514
784
  }
515
785
  }
516
- return Promise.reject(error);
786
+ return Promise.reject(normalizeAxiosError(error));
517
787
  }
518
788
  );
519
789
  return client;
@@ -827,7 +1097,8 @@ function normalizeLastMessage(lastMsg) {
827
1097
  isEdited: false,
828
1098
  sentAt: lastMsg.sentAt ?? "",
829
1099
  createdAt: lastMsg.sentAt ?? "",
830
- ...lastMsg.senderName && { senderName: lastMsg.senderName }
1100
+ ...lastMsg.senderName && { senderName: lastMsg.senderName },
1101
+ ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
831
1102
  };
832
1103
  }
833
1104
  function normalizeConversation(conv) {
@@ -940,6 +1211,9 @@ var conversationsApi = {
940
1211
  async removeIcon(conversationId) {
941
1212
  const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
942
1213
  return normalizeConversation(data);
1214
+ },
1215
+ async clearChat(conversationId) {
1216
+ await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
943
1217
  }
944
1218
  };
945
1219
 
@@ -1191,122 +1465,6 @@ var usersApi = {
1191
1465
 
1192
1466
  // src/socket/socket.ts
1193
1467
  var import_socket = require("socket.io-client");
1194
-
1195
- // src/crypto/detect.ts
1196
- var _cached = null;
1197
- async function detectTransitAlgo() {
1198
- if (_cached) return _cached;
1199
- try {
1200
- await globalThis.crypto.subtle.generateKey(
1201
- { name: "X25519" },
1202
- false,
1203
- ["deriveKey"]
1204
- );
1205
- _cached = "x25519";
1206
- } catch {
1207
- _cached = "p256";
1208
- }
1209
- return _cached;
1210
- }
1211
- function resetAlgoCache() {
1212
- _cached = null;
1213
- }
1214
-
1215
- // src/crypto/handshake.ts
1216
- function hasWebCrypto2() {
1217
- return typeof globalThis.crypto?.subtle !== "undefined";
1218
- }
1219
- async function fetchServerKeys(apiUrl) {
1220
- const res = await fetch(`${apiUrl}/crypto/pubkey`);
1221
- if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
1222
- const body = await res.json();
1223
- return body?.data ?? body;
1224
- }
1225
- async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
1226
- if (hasWebCrypto2()) {
1227
- return performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth);
1228
- }
1229
- return performNobleHandshake(serverKeys, socketHandshakeAuth);
1230
- }
1231
- async function performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth) {
1232
- const ephemeral = await globalThis.crypto.subtle.generateKey(
1233
- algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
1234
- false,
1235
- ["deriveBits"]
1236
- );
1237
- const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
1238
- socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
1239
- socketHandshakeAuth["transitAlgo"] = algo;
1240
- const ephemeralPriv = ephemeral.privateKey;
1241
- return (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
1242
- }
1243
- async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
1244
- const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
1245
- const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
1246
- const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
1247
- const sharedBits = await globalThis.crypto.subtle.deriveBits(
1248
- { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
1249
- ephemeralPriv,
1250
- 256
1251
- );
1252
- const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
1253
- const salt = new TextEncoder().encode(sessionId);
1254
- const info = new TextEncoder().encode("antz-transit-v1");
1255
- return globalThis.crypto.subtle.deriveKey(
1256
- { name: "HKDF", hash: "SHA-256", salt, info },
1257
- hkdfKey,
1258
- { name: "AES-GCM", length: 256 },
1259
- false,
1260
- ["encrypt", "decrypt"]
1261
- );
1262
- }
1263
- async function performNobleHandshake(serverKeys, socketHandshakeAuth) {
1264
- const { x25519 } = await import("@noble/curves/ed25519");
1265
- const { hkdf } = await import("@noble/hashes/hkdf");
1266
- const { sha256 } = await import("@noble/hashes/sha256");
1267
- const { randomBytes } = await import("@noble/hashes/utils");
1268
- const ephemeralPriv = randomBytes(32);
1269
- const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
1270
- const serverPubBytes = base64ToUint82(serverKeys.x25519);
1271
- socketHandshakeAuth["transitEphemeralPub"] = uint8ToBase64(ephemeralPub);
1272
- socketHandshakeAuth["transitAlgo"] = "x25519";
1273
- return (sessionId) => {
1274
- const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
1275
- const salt = new TextEncoder().encode(sessionId);
1276
- const info = new TextEncoder().encode("antz-transit-v1");
1277
- const sessionKey = hkdf(sha256, sharedSecret, salt, info, 32);
1278
- return Promise.resolve(sessionKey);
1279
- };
1280
- }
1281
- function bufToB642(buf) {
1282
- const bytes = new Uint8Array(buf);
1283
- let str = "";
1284
- bytes.forEach((b) => {
1285
- str += String.fromCharCode(b);
1286
- });
1287
- return btoa(str);
1288
- }
1289
- function b64ToBuf2(b64) {
1290
- const bin = atob(b64);
1291
- const buf = new Uint8Array(bin.length);
1292
- for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
1293
- return buf.buffer;
1294
- }
1295
- function uint8ToBase64(bytes) {
1296
- let str = "";
1297
- bytes.forEach((b) => {
1298
- str += String.fromCharCode(b);
1299
- });
1300
- return btoa(str);
1301
- }
1302
- function base64ToUint82(b64) {
1303
- const bin = atob(b64);
1304
- const buf = new Uint8Array(bin.length);
1305
- for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
1306
- return buf;
1307
- }
1308
-
1309
- // src/socket/socket.ts
1310
1468
  var _socket = null;
1311
1469
  var _socketProxy = null;
1312
1470
  var _connectingPromise = null;
@@ -1403,28 +1561,52 @@ async function _doConnect(config, getToken) {
1403
1561
  };
1404
1562
  let boundDeriveSessionKey = null;
1405
1563
  if (config.transitEncryption) {
1406
- try {
1407
- const serverKeys = await fetchServerKeys(config.apiUrl);
1408
- if (!serverKeys.enabled) {
1409
- throw new Error(
1410
- "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
1411
- );
1564
+ const inFlight = getTransitHandshakePromise();
1565
+ if (inFlight) await inFlight;
1566
+ const existingSession = getTransitSession();
1567
+ let httpsSession = null;
1568
+ if (existingSession?.enabled && existingSession.sessionId) {
1569
+ httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
1570
+ } else {
1571
+ httpsSession = await createRestTransitSession(config.apiUrl);
1572
+ if (httpsSession) {
1573
+ const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1574
+ setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
1575
+ }
1576
+ }
1577
+ if (httpsSession) {
1578
+ socketHandshakeAuth["transitSessionId"] = httpsSession.sessionId;
1579
+ boundDeriveSessionKey = null;
1580
+ } else {
1581
+ try {
1582
+ const serverKeys = await fetchServerKeys(config.apiUrl);
1583
+ if (!serverKeys.enabled) {
1584
+ throw new AntzChatError(
1585
+ "TRANSIT_MISMATCH",
1586
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides.",
1587
+ false,
1588
+ { sdkEnabled: true, serverEnabled: false }
1589
+ );
1590
+ }
1591
+ const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1592
+ boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
1593
+ } catch (err) {
1594
+ throw err;
1412
1595
  }
1413
- const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1414
- boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
1415
- } catch (err) {
1416
- throw err;
1417
1596
  }
1418
1597
  } else {
1419
1598
  try {
1420
1599
  const serverKeys = await fetchServerKeys(config.apiUrl);
1421
1600
  if (serverKeys.enabled) {
1422
- throw new Error(
1423
- "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides."
1601
+ throw new AntzChatError(
1602
+ "TRANSIT_MISMATCH",
1603
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides.",
1604
+ false,
1605
+ { sdkEnabled: false, serverEnabled: true }
1424
1606
  );
1425
1607
  }
1426
1608
  } catch (err) {
1427
- if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
1609
+ if (err?.code === "TRANSIT_MISMATCH") throw err;
1428
1610
  }
1429
1611
  }
1430
1612
  _socket = (0, import_socket.io)(`${config.socketOrigin}/chat`, {
@@ -1568,14 +1750,10 @@ async function drainSendQueue(conversationId) {
1568
1750
  while (queue.length > 0) {
1569
1751
  const entry = queue.shift();
1570
1752
  if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
1571
- entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
1753
+ entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
1572
1754
  continue;
1573
1755
  }
1574
- try {
1575
- entry.resolve(await entry.run());
1576
- } catch (e) {
1577
- entry.reject(e);
1578
- }
1756
+ entry.run().then(entry.resolve).catch(entry.reject);
1579
1757
  }
1580
1758
  sendQueues.delete(conversationId);
1581
1759
  sendQueueRunning.delete(conversationId);
@@ -1585,7 +1763,7 @@ function queueSendMessage(payload) {
1585
1763
  if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);
1586
1764
  const queue = sendQueues.get(conversationId);
1587
1765
  if (queue.length >= QUEUE_MAX_SIZE) {
1588
- return Promise.reject(new Error("[AntzChat] Send queue full: too many messages in flight"));
1766
+ return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1589
1767
  }
1590
1768
  return new Promise((resolve, reject) => {
1591
1769
  queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
@@ -1596,7 +1774,7 @@ function waitForReconnect() {
1596
1774
  return new Promise((resolve, reject) => {
1597
1775
  const timer = setTimeout(() => {
1598
1776
  unsubscribe();
1599
- reject(new Error("[AntzChat] Socket reconnect timeout"));
1777
+ reject(new AntzChatNetworkError("Socket reconnect timeout", "SOCKET_TIMEOUT"));
1600
1778
  }, RECONNECT_WAIT_TIMEOUT);
1601
1779
  const unsubscribe = onSocketStatus((status) => {
1602
1780
  if (status === "connected") {
@@ -1606,7 +1784,7 @@ function waitForReconnect() {
1606
1784
  } else if (status === "error") {
1607
1785
  clearTimeout(timer);
1608
1786
  unsubscribe();
1609
- reject(new Error("[AntzChat] Socket reconnect failed"));
1787
+ reject(new AntzChatNetworkError("Socket reconnect failed", "SOCKET_NOT_CONNECTED"));
1610
1788
  }
1611
1789
  });
1612
1790
  });
@@ -1617,13 +1795,15 @@ async function withAck(event, payload) {
1617
1795
  await waitForReconnect();
1618
1796
  socket = tryGetSocket();
1619
1797
  }
1620
- if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
1798
+ if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
1621
1799
  return new Promise((resolve, reject) => {
1622
- const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
1800
+ let timer;
1623
1801
  secureEmit(socket, event, payload, (response) => {
1624
1802
  clearTimeout(timer);
1625
1803
  resolve(response);
1626
- });
1804
+ }).then(() => {
1805
+ timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
1806
+ }).catch(reject);
1627
1807
  });
1628
1808
  }
1629
1809
  function fireAndForget(event, payload) {
@@ -1650,6 +1830,9 @@ var socketEmit = {
1650
1830
  deleteMessageForMe(messageId) {
1651
1831
  return withAck("delete_message_for_me", { messageId });
1652
1832
  },
1833
+ clearChat(conversationId) {
1834
+ return withAck("clear_chat_for_me", { conversationId });
1835
+ },
1653
1836
  addReaction(messageId, emoji) {
1654
1837
  return withAck("add_reaction", { messageId, emoji });
1655
1838
  },
@@ -1673,7 +1856,7 @@ var socketEmit = {
1673
1856
  const socket = tryGetSocket();
1674
1857
  if (!socket) return Promise.resolve([]);
1675
1858
  return new Promise((resolve, reject) => {
1676
- const timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
1859
+ let timer;
1677
1860
  secureEmit(socket, "get_online_users", { userIds }, (response) => {
1678
1861
  clearTimeout(timer);
1679
1862
  if (response && typeof response === "object" && "onlineStatus" in response) {
@@ -1684,7 +1867,9 @@ var socketEmit = {
1684
1867
  } else {
1685
1868
  resolve([]);
1686
1869
  }
1687
- });
1870
+ }).then(() => {
1871
+ timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
1872
+ }).catch(reject);
1688
1873
  });
1689
1874
  },
1690
1875
  getTypingUsers(conversationId) {
@@ -1746,24 +1931,40 @@ var AntzChatClient = class {
1746
1931
  };
1747
1932
  // Annotate the CommonJS export names for ESM import in node:
1748
1933
  0 && (module.exports = {
1934
+ AntzChatAuthError,
1749
1935
  AntzChatClient,
1936
+ AntzChatError,
1937
+ AntzChatNetworkError,
1938
+ AntzChatPermissionError,
1939
+ AntzChatServerError,
1940
+ AntzChatValidationError,
1750
1941
  appConfigApi,
1751
1942
  authApi,
1752
1943
  connectSocket,
1753
1944
  conversationsApi,
1754
1945
  createAuthStore,
1946
+ createRestTransitSession,
1947
+ decryptPayload,
1755
1948
  devicesApi,
1756
1949
  disconnectSocket,
1950
+ encryptPayload,
1951
+ fetchServerKeys,
1952
+ generateEphemeralKey,
1757
1953
  getApiClient,
1758
1954
  getAuthStore,
1759
1955
  getCompressionStrategy,
1956
+ getSessionId,
1957
+ getSessionKey,
1760
1958
  getSocket,
1761
1959
  getSocketStatus,
1762
1960
  initApiClient,
1763
1961
  initAuthStore,
1962
+ isTransitEnvelope,
1764
1963
  messagesApi,
1964
+ normalizeAxiosError,
1765
1965
  normalizeConversation,
1766
1966
  onSocketStatus,
1967
+ performHandshake,
1767
1968
  reconnectSocket,
1768
1969
  refreshSocketAuth,
1769
1970
  resetAuthStore,