@antzsoft/chat-core 1.2.3 → 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
@@ -52,6 +52,7 @@ var init_chat_store = __esm({
52
52
  isSidebarOpen: true,
53
53
  isGroupInfoOpen: false,
54
54
  isStarredPanelOpen: false,
55
+ messageInfoId: null,
55
56
  setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
56
57
  setPendingTarget: (target) => set({ pendingTarget: target }),
57
58
  addTypingUser: (conversationId, user) => set((state) => {
@@ -89,7 +90,8 @@ var init_chat_store = __esm({
89
90
  toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
90
91
  setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
91
92
  toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
92
- setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open })
93
+ setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),
94
+ setMessageInfoId: (id) => set({ messageInfoId: id })
93
95
  }));
94
96
  }
95
97
  });
@@ -103,18 +105,27 @@ __export(src_exports, {
103
105
  connectSocket: () => connectSocket,
104
106
  conversationsApi: () => conversationsApi,
105
107
  createAuthStore: () => createAuthStore,
108
+ createRestTransitSession: () => createRestTransitSession,
109
+ decryptPayload: () => decryptPayload,
106
110
  devicesApi: () => devicesApi,
107
111
  disconnectSocket: () => disconnectSocket,
112
+ encryptPayload: () => encryptPayload,
113
+ fetchServerKeys: () => fetchServerKeys,
114
+ generateEphemeralKey: () => generateEphemeralKey,
108
115
  getApiClient: () => getApiClient,
109
116
  getAuthStore: () => getAuthStore,
110
117
  getCompressionStrategy: () => getCompressionStrategy,
118
+ getSessionId: () => getSessionId,
119
+ getSessionKey: () => getSessionKey,
111
120
  getSocket: () => getSocket,
112
121
  getSocketStatus: () => getSocketStatus,
113
122
  initApiClient: () => initApiClient,
114
123
  initAuthStore: () => initAuthStore,
124
+ isTransitEnvelope: () => isTransitEnvelope,
115
125
  messagesApi: () => messagesApi,
116
126
  normalizeConversation: () => normalizeConversation,
117
127
  onSocketStatus: () => onSocketStatus,
128
+ performHandshake: () => performHandshake,
118
129
  reconnectSocket: () => reconnectSocket,
119
130
  refreshSocketAuth: () => refreshSocketAuth,
120
131
  resetAuthStore: () => resetAuthStore,
@@ -176,6 +187,7 @@ function resolveConfig(config) {
176
187
  onProgress: config.upload?.onProgress
177
188
  },
178
189
  platformUploadFn: config.platformUploadFn,
190
+ platformUploadPartFn: config.platformUploadPartFn,
179
191
  platformCompressFn: config.platformCompressFn,
180
192
  compression: {
181
193
  enabled: config.compression?.enabled ?? config.platformCompressFn != null,
@@ -388,6 +400,9 @@ function setTransitSession(session) {
388
400
  s.readyResolve?.();
389
401
  s.readyResolve = null;
390
402
  }
403
+ function getTransitSession() {
404
+ return getState().session;
405
+ }
391
406
  function clearTransitSession() {
392
407
  const s = getState();
393
408
  s.session = null;
@@ -404,10 +419,156 @@ function getSessionId() {
404
419
  return getState().session?.sessionId ?? null;
405
420
  }
406
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
+
407
564
  // src/api/client.ts
408
565
  var _tokenStore = null;
409
566
  var _config = null;
410
567
  var _avatarSent = false;
568
+ var _transitHandshakePromise = null;
569
+ function getTransitHandshakePromise() {
570
+ return _transitHandshakePromise;
571
+ }
411
572
  function initApiClient(config, tokenStore) {
412
573
  _config = config;
413
574
  _tokenStore = tokenStore;
@@ -417,6 +578,23 @@ function initApiClient(config, tokenStore) {
417
578
  headers: { "Content-Type": "application/json" }
418
579
  });
419
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
+ }
420
598
  client.interceptors.request.use(async (req) => {
421
599
  const token = _tokenStore?.getAccessToken();
422
600
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
@@ -813,7 +991,7 @@ function normalizeLastMessage(lastMsg) {
813
991
  id: lastMsg.messageId ?? "",
814
992
  tenantId: "",
815
993
  conversationId: "",
816
- senderId: "",
994
+ senderId: lastMsg.senderId ?? "",
817
995
  content: {
818
996
  type: lastMsg.hasAttachments ? "attachment" : "text",
819
997
  text: lastMsg.contentPreview
@@ -824,7 +1002,8 @@ function normalizeLastMessage(lastMsg) {
824
1002
  isEdited: false,
825
1003
  sentAt: lastMsg.sentAt ?? "",
826
1004
  createdAt: lastMsg.sentAt ?? "",
827
- ...lastMsg.senderName && { senderName: lastMsg.senderName }
1005
+ ...lastMsg.senderName && { senderName: lastMsg.senderName },
1006
+ ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
828
1007
  };
829
1008
  }
830
1009
  function normalizeConversation(conv) {
@@ -978,6 +1157,13 @@ var storageApi = {
978
1157
  async deleteFile(fileId) {
979
1158
  await getApiClient().post(`/storage/files/${fileId}/delete`);
980
1159
  },
1160
+ async completeMultipartUpload(fileId, uploadId, parts) {
1161
+ const { data } = await getApiClient().post(
1162
+ `/storage/multipart/complete/${fileId}`,
1163
+ { uploadId, parts }
1164
+ );
1165
+ return data;
1166
+ },
981
1167
  async getConversationFiles(conversationId, params = {}) {
982
1168
  const { data } = await getApiClient().get(
983
1169
  `/storage/conversations/${conversationId}/files`,
@@ -990,7 +1176,52 @@ var storageApi = {
990
1176
  return data;
991
1177
  }
992
1178
  };
993
- async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig) {
1179
+ async function runMultipartUpload(presigned, file, platformUploadPartFn, onProgress) {
1180
+ const { multipart } = presigned;
1181
+ if (!multipart) throw new Error("No multipart info on presigned response");
1182
+ const CONCURRENCY = 3;
1183
+ const completedParts = [];
1184
+ const partProgress = {};
1185
+ multipart.partUrls.forEach(({ partNumber }) => {
1186
+ partProgress[partNumber] = 0;
1187
+ });
1188
+ const reportProgress = () => {
1189
+ if (!onProgress) return;
1190
+ const vals = Object.values(partProgress);
1191
+ const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1192
+ onProgress(Math.round(avg * 0.95));
1193
+ };
1194
+ const uploadPart = async (partNumber, uploadUrl, method) => {
1195
+ const offset = (partNumber - 1) * multipart.chunkSize;
1196
+ const end = Math.min(offset + multipart.chunkSize, file.size);
1197
+ const blob = await fetch(file.uri).then((r) => r.blob());
1198
+ const slice = blob.slice(offset, end);
1199
+ const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {
1200
+ partProgress[partNumber] = pct;
1201
+ reportProgress();
1202
+ }, method);
1203
+ completedParts.push({ partNumber, etag });
1204
+ partProgress[partNumber] = 100;
1205
+ reportProgress();
1206
+ };
1207
+ for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {
1208
+ const batch = multipart.partUrls.slice(i, i + CONCURRENCY);
1209
+ const results = await Promise.allSettled(
1210
+ batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? "PUT"))
1211
+ );
1212
+ const failed = results.find((r) => r.status === "rejected");
1213
+ if (failed) throw failed.reason;
1214
+ }
1215
+ completedParts.sort((a, b) => a.partNumber - b.partNumber);
1216
+ const fileResponse = await storageApi.completeMultipartUpload(
1217
+ presigned.fileId,
1218
+ multipart.uploadId,
1219
+ completedParts
1220
+ );
1221
+ onProgress?.(100);
1222
+ return fileResponse;
1223
+ }
1224
+ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
994
1225
  const compressedFiles = await Promise.all(
995
1226
  files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
996
1227
  );
@@ -1032,11 +1263,19 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
1032
1263
  const { file, slotId } = slotted[originalIdx];
1033
1264
  progressMap[originalIdx] = 0;
1034
1265
  try {
1035
- await platformUploadFn(presigned, file, (pct) => {
1036
- progressMap[originalIdx] = Math.round(pct * 0.9);
1037
- reportProgress();
1038
- });
1039
- const fileResponse = await storageApi.confirmUpload(presigned.fileId);
1266
+ let fileResponse;
1267
+ if (presigned.multipart && platformUploadPartFn) {
1268
+ fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {
1269
+ progressMap[originalIdx] = pct;
1270
+ reportProgress();
1271
+ });
1272
+ } else {
1273
+ await platformUploadFn(presigned, file, (pct) => {
1274
+ progressMap[originalIdx] = Math.round(pct * 0.9);
1275
+ reportProgress();
1276
+ });
1277
+ fileResponse = await storageApi.confirmUpload(presigned.fileId);
1278
+ }
1040
1279
  progressMap[originalIdx] = 100;
1041
1280
  reportProgress();
1042
1281
  successful.push(fileResponse);
@@ -1048,9 +1287,9 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
1048
1287
  );
1049
1288
  return { result: { successful, failed }, slotToFile };
1050
1289
  }
1051
- async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig) {
1290
+ async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1052
1291
  const slotIds = files.map(() => generateUUID());
1053
- const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);
1292
+ const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);
1054
1293
  return result;
1055
1294
  }
1056
1295
 
@@ -1128,122 +1367,6 @@ var usersApi = {
1128
1367
 
1129
1368
  // src/socket/socket.ts
1130
1369
  var import_socket = require("socket.io-client");
1131
-
1132
- // src/crypto/detect.ts
1133
- var _cached = null;
1134
- async function detectTransitAlgo() {
1135
- if (_cached) return _cached;
1136
- try {
1137
- await globalThis.crypto.subtle.generateKey(
1138
- { name: "X25519" },
1139
- false,
1140
- ["deriveKey"]
1141
- );
1142
- _cached = "x25519";
1143
- } catch {
1144
- _cached = "p256";
1145
- }
1146
- return _cached;
1147
- }
1148
- function resetAlgoCache() {
1149
- _cached = null;
1150
- }
1151
-
1152
- // src/crypto/handshake.ts
1153
- function hasWebCrypto2() {
1154
- return typeof globalThis.crypto?.subtle !== "undefined";
1155
- }
1156
- async function fetchServerKeys(apiUrl) {
1157
- const res = await fetch(`${apiUrl}/crypto/pubkey`);
1158
- if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
1159
- const body = await res.json();
1160
- return body?.data ?? body;
1161
- }
1162
- async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
1163
- if (hasWebCrypto2()) {
1164
- return performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth);
1165
- }
1166
- return performNobleHandshake(serverKeys, socketHandshakeAuth);
1167
- }
1168
- async function performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth) {
1169
- const ephemeral = await globalThis.crypto.subtle.generateKey(
1170
- algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
1171
- false,
1172
- ["deriveBits"]
1173
- );
1174
- const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
1175
- socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
1176
- socketHandshakeAuth["transitAlgo"] = algo;
1177
- const ephemeralPriv = ephemeral.privateKey;
1178
- return (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
1179
- }
1180
- async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
1181
- const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
1182
- const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
1183
- const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
1184
- const sharedBits = await globalThis.crypto.subtle.deriveBits(
1185
- { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
1186
- ephemeralPriv,
1187
- 256
1188
- );
1189
- const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
1190
- const salt = new TextEncoder().encode(sessionId);
1191
- const info = new TextEncoder().encode("antz-transit-v1");
1192
- return globalThis.crypto.subtle.deriveKey(
1193
- { name: "HKDF", hash: "SHA-256", salt, info },
1194
- hkdfKey,
1195
- { name: "AES-GCM", length: 256 },
1196
- false,
1197
- ["encrypt", "decrypt"]
1198
- );
1199
- }
1200
- async function performNobleHandshake(serverKeys, socketHandshakeAuth) {
1201
- const { x25519 } = await import("@noble/curves/ed25519");
1202
- const { hkdf } = await import("@noble/hashes/hkdf");
1203
- const { sha256 } = await import("@noble/hashes/sha256");
1204
- const { randomBytes } = await import("@noble/hashes/utils");
1205
- const ephemeralPriv = randomBytes(32);
1206
- const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
1207
- const serverPubBytes = base64ToUint82(serverKeys.x25519);
1208
- socketHandshakeAuth["transitEphemeralPub"] = uint8ToBase64(ephemeralPub);
1209
- socketHandshakeAuth["transitAlgo"] = "x25519";
1210
- return (sessionId) => {
1211
- const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
1212
- const salt = new TextEncoder().encode(sessionId);
1213
- const info = new TextEncoder().encode("antz-transit-v1");
1214
- const sessionKey = hkdf(sha256, sharedSecret, salt, info, 32);
1215
- return Promise.resolve(sessionKey);
1216
- };
1217
- }
1218
- function bufToB642(buf) {
1219
- const bytes = new Uint8Array(buf);
1220
- let str = "";
1221
- bytes.forEach((b) => {
1222
- str += String.fromCharCode(b);
1223
- });
1224
- return btoa(str);
1225
- }
1226
- function b64ToBuf2(b64) {
1227
- const bin = atob(b64);
1228
- const buf = new Uint8Array(bin.length);
1229
- for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
1230
- return buf.buffer;
1231
- }
1232
- function uint8ToBase64(bytes) {
1233
- let str = "";
1234
- bytes.forEach((b) => {
1235
- str += String.fromCharCode(b);
1236
- });
1237
- return btoa(str);
1238
- }
1239
- function base64ToUint82(b64) {
1240
- const bin = atob(b64);
1241
- const buf = new Uint8Array(bin.length);
1242
- for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
1243
- return buf;
1244
- }
1245
-
1246
- // src/socket/socket.ts
1247
1370
  var _socket = null;
1248
1371
  var _socketProxy = null;
1249
1372
  var _connectingPromise = null;
@@ -1340,17 +1463,35 @@ async function _doConnect(config, getToken) {
1340
1463
  };
1341
1464
  let boundDeriveSessionKey = null;
1342
1465
  if (config.transitEncryption) {
1343
- try {
1344
- const serverKeys = await fetchServerKeys(config.apiUrl);
1345
- if (!serverKeys.enabled) {
1346
- throw new Error(
1347
- "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
1348
- );
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;
1349
1494
  }
1350
- const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
1351
- boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
1352
- } catch (err) {
1353
- throw err;
1354
1495
  }
1355
1496
  } else {
1356
1497
  try {
@@ -1508,11 +1649,7 @@ async function drainSendQueue(conversationId) {
1508
1649
  entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
1509
1650
  continue;
1510
1651
  }
1511
- try {
1512
- entry.resolve(await entry.run());
1513
- } catch (e) {
1514
- entry.reject(e);
1515
- }
1652
+ entry.run().then(entry.resolve).catch(entry.reject);
1516
1653
  }
1517
1654
  sendQueues.delete(conversationId);
1518
1655
  sendQueueRunning.delete(conversationId);
@@ -1556,11 +1693,13 @@ async function withAck(event, payload) {
1556
1693
  }
1557
1694
  if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
1558
1695
  return new Promise((resolve, reject) => {
1559
- const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
1696
+ let timer;
1560
1697
  secureEmit(socket, event, payload, (response) => {
1561
1698
  clearTimeout(timer);
1562
1699
  resolve(response);
1563
- });
1700
+ }).then(() => {
1701
+ timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
1702
+ }).catch(reject);
1564
1703
  });
1565
1704
  }
1566
1705
  function fireAndForget(event, payload) {
@@ -1610,7 +1749,7 @@ var socketEmit = {
1610
1749
  const socket = tryGetSocket();
1611
1750
  if (!socket) return Promise.resolve([]);
1612
1751
  return new Promise((resolve, reject) => {
1613
- const timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
1752
+ let timer;
1614
1753
  secureEmit(socket, "get_online_users", { userIds }, (response) => {
1615
1754
  clearTimeout(timer);
1616
1755
  if (response && typeof response === "object" && "onlineStatus" in response) {
@@ -1621,7 +1760,9 @@ var socketEmit = {
1621
1760
  } else {
1622
1761
  resolve([]);
1623
1762
  }
1624
- });
1763
+ }).then(() => {
1764
+ timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
1765
+ }).catch(reject);
1625
1766
  });
1626
1767
  },
1627
1768
  getTypingUsers(conversationId) {
@@ -1689,18 +1830,27 @@ var AntzChatClient = class {
1689
1830
  connectSocket,
1690
1831
  conversationsApi,
1691
1832
  createAuthStore,
1833
+ createRestTransitSession,
1834
+ decryptPayload,
1692
1835
  devicesApi,
1693
1836
  disconnectSocket,
1837
+ encryptPayload,
1838
+ fetchServerKeys,
1839
+ generateEphemeralKey,
1694
1840
  getApiClient,
1695
1841
  getAuthStore,
1696
1842
  getCompressionStrategy,
1843
+ getSessionId,
1844
+ getSessionKey,
1697
1845
  getSocket,
1698
1846
  getSocketStatus,
1699
1847
  initApiClient,
1700
1848
  initAuthStore,
1849
+ isTransitEnvelope,
1701
1850
  messagesApi,
1702
1851
  normalizeConversation,
1703
1852
  onSocketStatus,
1853
+ performHandshake,
1704
1854
  reconnectSocket,
1705
1855
  refreshSocketAuth,
1706
1856
  resetAuthStore,