@antzsoft/chat-core 1.2.4 → 1.2.5

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
@@ -105,18 +105,27 @@ __export(src_exports, {
105
105
  connectSocket: () => connectSocket,
106
106
  conversationsApi: () => conversationsApi,
107
107
  createAuthStore: () => createAuthStore,
108
+ createRestTransitSession: () => createRestTransitSession,
109
+ decryptPayload: () => decryptPayload,
108
110
  devicesApi: () => devicesApi,
109
111
  disconnectSocket: () => disconnectSocket,
112
+ encryptPayload: () => encryptPayload,
113
+ fetchServerKeys: () => fetchServerKeys,
114
+ generateEphemeralKey: () => generateEphemeralKey,
110
115
  getApiClient: () => getApiClient,
111
116
  getAuthStore: () => getAuthStore,
112
117
  getCompressionStrategy: () => getCompressionStrategy,
118
+ getSessionId: () => getSessionId,
119
+ getSessionKey: () => getSessionKey,
113
120
  getSocket: () => getSocket,
114
121
  getSocketStatus: () => getSocketStatus,
115
122
  initApiClient: () => initApiClient,
116
123
  initAuthStore: () => initAuthStore,
124
+ isTransitEnvelope: () => isTransitEnvelope,
117
125
  messagesApi: () => messagesApi,
118
126
  normalizeConversation: () => normalizeConversation,
119
127
  onSocketStatus: () => onSocketStatus,
128
+ performHandshake: () => performHandshake,
120
129
  reconnectSocket: () => reconnectSocket,
121
130
  refreshSocketAuth: () => refreshSocketAuth,
122
131
  resetAuthStore: () => resetAuthStore,
@@ -391,6 +400,9 @@ function setTransitSession(session) {
391
400
  s.readyResolve?.();
392
401
  s.readyResolve = null;
393
402
  }
403
+ function getTransitSession() {
404
+ return getState().session;
405
+ }
394
406
  function clearTransitSession() {
395
407
  const s = getState();
396
408
  s.session = null;
@@ -407,10 +419,156 @@ function getSessionId() {
407
419
  return getState().session?.sessionId ?? null;
408
420
  }
409
421
 
422
+ // src/crypto/detect.ts
423
+ var _cached = null;
424
+ async function detectTransitAlgo() {
425
+ if (_cached) return _cached;
426
+ try {
427
+ await globalThis.crypto.subtle.generateKey(
428
+ { name: "X25519" },
429
+ false,
430
+ ["deriveKey"]
431
+ );
432
+ _cached = "x25519";
433
+ } catch {
434
+ _cached = "p256";
435
+ }
436
+ return _cached;
437
+ }
438
+ function resetAlgoCache() {
439
+ _cached = null;
440
+ }
441
+
442
+ // src/crypto/handshake.ts
443
+ function hasWebCrypto2() {
444
+ return typeof globalThis.crypto?.subtle !== "undefined";
445
+ }
446
+ async function fetchServerKeys(apiUrl) {
447
+ const res = await fetch(`${apiUrl}/crypto/pubkey`);
448
+ if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
449
+ const body = await res.json();
450
+ return body?.data ?? body;
451
+ }
452
+ async function generateEphemeralKey(algo, serverKeys) {
453
+ if (hasWebCrypto2()) {
454
+ return generateWebCryptoEphemeralKey(algo, serverKeys);
455
+ }
456
+ return generateNobleEphemeralKey(serverKeys);
457
+ }
458
+ async function createRestTransitSession(apiUrl) {
459
+ try {
460
+ const serverKeys = await fetchServerKeys(apiUrl);
461
+ if (!serverKeys.enabled) return null;
462
+ const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
463
+ const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
464
+ const res = await fetch(`${apiUrl}/crypto/session`, {
465
+ method: "POST",
466
+ headers: { "Content-Type": "application/json" },
467
+ body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
468
+ });
469
+ if (!res.ok) return null;
470
+ const body = await res.json();
471
+ const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
472
+ if (!sessionId) return null;
473
+ const sessionKey = await deriveSessionKey(sessionId);
474
+ return { sessionId, sessionKey };
475
+ } catch {
476
+ return null;
477
+ }
478
+ }
479
+ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
480
+ const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
481
+ socketHandshakeAuth["transitEphemeralPub"] = ephemeralPubB64;
482
+ socketHandshakeAuth["transitAlgo"] = algo;
483
+ return deriveSessionKey;
484
+ }
485
+ async function generateWebCryptoEphemeralKey(algo, serverKeys) {
486
+ const ephemeral = await globalThis.crypto.subtle.generateKey(
487
+ algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
488
+ false,
489
+ ["deriveBits"]
490
+ );
491
+ const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
492
+ const ephemeralPriv = ephemeral.privateKey;
493
+ return {
494
+ ephemeralPubB64: bufToB642(pubRaw),
495
+ deriveSessionKey: (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId)
496
+ };
497
+ }
498
+ async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
499
+ const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
500
+ const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
501
+ const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
502
+ const sharedBits = await globalThis.crypto.subtle.deriveBits(
503
+ { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
504
+ ephemeralPriv,
505
+ 256
506
+ );
507
+ const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
508
+ const salt = new TextEncoder().encode(sessionId);
509
+ const info = new TextEncoder().encode("antz-transit-v1");
510
+ return globalThis.crypto.subtle.deriveKey(
511
+ { name: "HKDF", hash: "SHA-256", salt, info },
512
+ hkdfKey,
513
+ { name: "AES-GCM", length: 256 },
514
+ false,
515
+ ["encrypt", "decrypt"]
516
+ );
517
+ }
518
+ async function generateNobleEphemeralKey(serverKeys) {
519
+ const { x25519 } = await import("@noble/curves/ed25519");
520
+ const { hkdf } = await import("@noble/hashes/hkdf");
521
+ const { sha256 } = await import("@noble/hashes/sha256");
522
+ const { randomBytes } = await import("@noble/hashes/utils");
523
+ const ephemeralPriv = randomBytes(32);
524
+ const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
525
+ const serverPubBytes = base64ToUint82(serverKeys.x25519);
526
+ return {
527
+ ephemeralPubB64: uint8ToBase64(ephemeralPub),
528
+ deriveSessionKey: (sessionId) => {
529
+ const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
530
+ const salt = new TextEncoder().encode(sessionId);
531
+ const info = new TextEncoder().encode("antz-transit-v1");
532
+ return Promise.resolve(hkdf(sha256, sharedSecret, salt, info, 32));
533
+ }
534
+ };
535
+ }
536
+ function bufToB642(buf) {
537
+ const bytes = new Uint8Array(buf);
538
+ let str = "";
539
+ bytes.forEach((b) => {
540
+ str += String.fromCharCode(b);
541
+ });
542
+ return btoa(str);
543
+ }
544
+ function b64ToBuf2(b64) {
545
+ const bin = atob(b64);
546
+ const buf = new Uint8Array(bin.length);
547
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
548
+ return buf.buffer;
549
+ }
550
+ function uint8ToBase64(bytes) {
551
+ let str = "";
552
+ bytes.forEach((b) => {
553
+ str += String.fromCharCode(b);
554
+ });
555
+ return btoa(str);
556
+ }
557
+ function base64ToUint82(b64) {
558
+ const bin = atob(b64);
559
+ const buf = new Uint8Array(bin.length);
560
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
561
+ return buf;
562
+ }
563
+
410
564
  // src/api/client.ts
411
565
  var _tokenStore = null;
412
566
  var _config = null;
413
567
  var _avatarSent = false;
568
+ var _transitHandshakePromise = null;
569
+ function getTransitHandshakePromise() {
570
+ return _transitHandshakePromise;
571
+ }
414
572
  function initApiClient(config, tokenStore) {
415
573
  _config = config;
416
574
  _tokenStore = tokenStore;
@@ -420,6 +578,23 @@ function initApiClient(config, tokenStore) {
420
578
  headers: { "Content-Type": "application/json" }
421
579
  });
422
580
  configureTransit(config.transitEncryption);
581
+ if (config.transitEncryption && !getTransitSession() && !_transitHandshakePromise) {
582
+ _transitHandshakePromise = (async () => {
583
+ try {
584
+ const session = await createRestTransitSession(config.apiUrl);
585
+ if (session && !getTransitSession()) {
586
+ const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
587
+ setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
588
+ } else if (!session) {
589
+ configureTransit(false);
590
+ }
591
+ } catch {
592
+ configureTransit(false);
593
+ } finally {
594
+ _transitHandshakePromise = null;
595
+ }
596
+ })();
597
+ }
423
598
  client.interceptors.request.use(async (req) => {
424
599
  const token = _tokenStore?.getAccessToken();
425
600
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
@@ -827,7 +1002,8 @@ function normalizeLastMessage(lastMsg) {
827
1002
  isEdited: false,
828
1003
  sentAt: lastMsg.sentAt ?? "",
829
1004
  createdAt: lastMsg.sentAt ?? "",
830
- ...lastMsg.senderName && { senderName: lastMsg.senderName }
1005
+ ...lastMsg.senderName && { senderName: lastMsg.senderName },
1006
+ ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
831
1007
  };
832
1008
  }
833
1009
  function normalizeConversation(conv) {
@@ -1191,122 +1367,6 @@ var usersApi = {
1191
1367
 
1192
1368
  // src/socket/socket.ts
1193
1369
  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
1370
  var _socket = null;
1311
1371
  var _socketProxy = null;
1312
1372
  var _connectingPromise = null;
@@ -1403,17 +1463,35 @@ async function _doConnect(config, getToken) {
1403
1463
  };
1404
1464
  let boundDeriveSessionKey = null;
1405
1465
  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
- );
1466
+ const inFlight = getTransitHandshakePromise();
1467
+ if (inFlight) await inFlight;
1468
+ const existingSession = getTransitSession();
1469
+ let httpsSession = null;
1470
+ if (existingSession?.enabled && existingSession.sessionId) {
1471
+ httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
1472
+ } else {
1473
+ httpsSession = await createRestTransitSession(config.apiUrl);
1474
+ if (httpsSession) {
1475
+ const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1476
+ setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
1477
+ }
1478
+ }
1479
+ if (httpsSession) {
1480
+ socketHandshakeAuth["transitSessionId"] = httpsSession.sessionId;
1481
+ boundDeriveSessionKey = null;
1482
+ } else {
1483
+ try {
1484
+ const serverKeys = await fetchServerKeys(config.apiUrl);
1485
+ if (!serverKeys.enabled) {
1486
+ throw new Error(
1487
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
1488
+ );
1489
+ }
1490
+ const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1491
+ boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
1492
+ } catch (err) {
1493
+ throw err;
1412
1494
  }
1413
- const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1414
- boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
1415
- } catch (err) {
1416
- throw err;
1417
1495
  }
1418
1496
  } else {
1419
1497
  try {
@@ -1571,11 +1649,7 @@ async function drainSendQueue(conversationId) {
1571
1649
  entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
1572
1650
  continue;
1573
1651
  }
1574
- try {
1575
- entry.resolve(await entry.run());
1576
- } catch (e) {
1577
- entry.reject(e);
1578
- }
1652
+ entry.run().then(entry.resolve).catch(entry.reject);
1579
1653
  }
1580
1654
  sendQueues.delete(conversationId);
1581
1655
  sendQueueRunning.delete(conversationId);
@@ -1619,11 +1693,13 @@ async function withAck(event, payload) {
1619
1693
  }
1620
1694
  if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
1621
1695
  return new Promise((resolve, reject) => {
1622
- const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
1696
+ let timer;
1623
1697
  secureEmit(socket, event, payload, (response) => {
1624
1698
  clearTimeout(timer);
1625
1699
  resolve(response);
1626
- });
1700
+ }).then(() => {
1701
+ timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
1702
+ }).catch(reject);
1627
1703
  });
1628
1704
  }
1629
1705
  function fireAndForget(event, payload) {
@@ -1673,7 +1749,7 @@ var socketEmit = {
1673
1749
  const socket = tryGetSocket();
1674
1750
  if (!socket) return Promise.resolve([]);
1675
1751
  return new Promise((resolve, reject) => {
1676
- const timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
1752
+ let timer;
1677
1753
  secureEmit(socket, "get_online_users", { userIds }, (response) => {
1678
1754
  clearTimeout(timer);
1679
1755
  if (response && typeof response === "object" && "onlineStatus" in response) {
@@ -1684,7 +1760,9 @@ var socketEmit = {
1684
1760
  } else {
1685
1761
  resolve([]);
1686
1762
  }
1687
- });
1763
+ }).then(() => {
1764
+ timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
1765
+ }).catch(reject);
1688
1766
  });
1689
1767
  },
1690
1768
  getTypingUsers(conversationId) {
@@ -1752,18 +1830,27 @@ var AntzChatClient = class {
1752
1830
  connectSocket,
1753
1831
  conversationsApi,
1754
1832
  createAuthStore,
1833
+ createRestTransitSession,
1834
+ decryptPayload,
1755
1835
  devicesApi,
1756
1836
  disconnectSocket,
1837
+ encryptPayload,
1838
+ fetchServerKeys,
1839
+ generateEphemeralKey,
1757
1840
  getApiClient,
1758
1841
  getAuthStore,
1759
1842
  getCompressionStrategy,
1843
+ getSessionId,
1844
+ getSessionKey,
1760
1845
  getSocket,
1761
1846
  getSocketStatus,
1762
1847
  initApiClient,
1763
1848
  initAuthStore,
1849
+ isTransitEnvelope,
1764
1850
  messagesApi,
1765
1851
  normalizeConversation,
1766
1852
  onSocketStatus,
1853
+ performHandshake,
1767
1854
  reconnectSocket,
1768
1855
  refreshSocketAuth,
1769
1856
  resetAuthStore,