@antzsoft/chat-core 1.4.4 → 1.4.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
@@ -134,6 +134,7 @@ __export(src_exports, {
134
134
  getSocketStatus: () => getSocketStatus,
135
135
  initApiClient: () => initApiClient,
136
136
  initAuthStore: () => initAuthStore,
137
+ isApiClientConfigured: () => isApiClientConfigured,
137
138
  isMentionAll: () => isMentionAll,
138
139
  isTransitEnvelope: () => isTransitEnvelope,
139
140
  messagesApi: () => messagesApi,
@@ -146,9 +147,11 @@ __export(src_exports, {
146
147
  refreshSocketAuth: () => refreshSocketAuth,
147
148
  renderMentionParts: () => renderMentionParts,
148
149
  resetAuthStore: () => resetAuthStore,
150
+ resetTrackedRooms: () => resetTrackedRooms,
149
151
  resolveConfig: () => resolveConfig,
150
152
  resolveSystemMessageText: () => resolveSystemMessageText,
151
153
  setApiClientInstance: () => setApiClientInstance,
154
+ setAuthReadyPromise: () => setAuthReadyPromise,
152
155
  setTransitSession: () => setTransitSession,
153
156
  socketEmit: () => socketEmit,
154
157
  storageApi: () => storageApi,
@@ -214,6 +217,7 @@ function resolveConfig(config) {
214
217
  compressDocuments: config.compression?.compressDocuments ?? true
215
218
  },
216
219
  persistStorage: config.persistStorage,
220
+ onSendError: config.onSendError,
217
221
  messagePageSize: config.messagePageSize ?? 40,
218
222
  starredMessagePageSize: config.starredMessagePageSize ?? 30,
219
223
  searchPageSize: config.searchPageSize ?? 50
@@ -386,7 +390,8 @@ function getState() {
386
390
  sessionEverEstablished: false,
387
391
  readyResolve: null,
388
392
  readyPromise: null,
389
- transitConfigured: null
393
+ transitConfigured: null,
394
+ readyListeners: /* @__PURE__ */ new Set()
390
395
  };
391
396
  }
392
397
  return g[_KEY];
@@ -399,11 +404,13 @@ function configureTransit(enabled) {
399
404
  s.readyResolve = null;
400
405
  }
401
406
  }
407
+ function configureTransitIfUnset(enabled) {
408
+ if (getState().transitConfigured === null) configureTransit(enabled);
409
+ }
402
410
  function waitForTransitReady() {
403
411
  const s = getState();
404
412
  if (!s.transitConfigured) return Promise.resolve();
405
413
  if (s.session) return Promise.resolve();
406
- if (s.sessionEverEstablished) return Promise.resolve();
407
414
  if (!s.readyPromise) {
408
415
  s.readyPromise = new Promise((resolve) => {
409
416
  s.readyResolve = resolve;
@@ -417,6 +424,29 @@ function setTransitSession(session) {
417
424
  s.sessionEverEstablished = true;
418
425
  s.readyResolve?.();
419
426
  s.readyResolve = null;
427
+ s.readyListeners.forEach((fn) => {
428
+ try {
429
+ fn();
430
+ } catch {
431
+ }
432
+ });
433
+ }
434
+ function onTransitReady(listener) {
435
+ const s = getState();
436
+ s.readyListeners.add(listener);
437
+ return () => {
438
+ s.readyListeners.delete(listener);
439
+ };
440
+ }
441
+ async function awaitTransitReadyOr(timeoutMs) {
442
+ const s = getState();
443
+ if (!s.transitConfigured || s.session) return true;
444
+ await Promise.race([
445
+ waitForTransitReady(),
446
+ new Promise((r) => setTimeout(r, timeoutMs))
447
+ ]);
448
+ const now = getState();
449
+ return Boolean(now.session) || now.transitConfigured !== true;
420
450
  }
421
451
  function getTransitSession() {
422
452
  return getState().session;
@@ -424,12 +454,16 @@ function getTransitSession() {
424
454
  function clearTransitSession() {
425
455
  const s = getState();
426
456
  s.session = null;
457
+ s.sessionEverEstablished = false;
427
458
  s.readyPromise = null;
428
459
  s.readyResolve = null;
429
460
  }
430
461
  function isTransitEnabled() {
431
462
  return getState().session?.enabled === true;
432
463
  }
464
+ function isTransitRequired() {
465
+ return getState().transitConfigured === true;
466
+ }
433
467
  function getSessionKey() {
434
468
  return getState().session?.sessionKey ?? null;
435
469
  }
@@ -668,13 +702,52 @@ function normalizeAxiosError(error) {
668
702
  }
669
703
 
670
704
  // src/api/client.ts
705
+ var TRANSIT_GATE_MAX_WAIT_MS = 3e4;
671
706
  var _tokenStore = null;
672
707
  var _config = null;
673
708
  var _avatarSent = false;
674
709
  var _transitHandshakePromise = null;
710
+ var _authReadyPromise = null;
675
711
  function getTransitHandshakePromise() {
676
712
  return _transitHandshakePromise;
677
713
  }
714
+ function setAuthReadyPromise(promise) {
715
+ _authReadyPromise = promise;
716
+ }
717
+ function isApiClientConfigured() {
718
+ return _config !== null;
719
+ }
720
+ function ensureRestTransitHandshake() {
721
+ if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;
722
+ const apiUrl = _config.apiUrl;
723
+ _transitHandshakePromise = (async () => {
724
+ try {
725
+ for (let attempt = 0; attempt < 5; attempt++) {
726
+ if (getTransitSession()) return;
727
+ try {
728
+ const keys = await fetchServerKeys(apiUrl);
729
+ if (!keys?.enabled) {
730
+ configureTransit(false);
731
+ return;
732
+ }
733
+ const session = await createRestTransitSession(apiUrl);
734
+ if (session && !getTransitSession()) {
735
+ const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
736
+ setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
737
+ return;
738
+ }
739
+ } catch {
740
+ }
741
+ await new Promise((r) => setTimeout(r, Math.min(500 * 2 ** attempt, 8e3)));
742
+ }
743
+ console.error(
744
+ "[AntzChat] transit handshake could not establish a session after 5 attempts \u2014 chat requests stay gated until one succeeds (server requires transit)."
745
+ );
746
+ } finally {
747
+ _transitHandshakePromise = null;
748
+ }
749
+ })();
750
+ }
678
751
  function initApiClient(config, tokenStore) {
679
752
  _config = config;
680
753
  _tokenStore = tokenStore;
@@ -684,24 +757,9 @@ function initApiClient(config, tokenStore) {
684
757
  headers: { "Content-Type": "application/json" }
685
758
  });
686
759
  configureTransit(config.transitEncryption);
687
- if (config.transitEncryption && !getTransitSession() && !_transitHandshakePromise) {
688
- _transitHandshakePromise = (async () => {
689
- try {
690
- const session = await createRestTransitSession(config.apiUrl);
691
- if (session && !getTransitSession()) {
692
- const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
693
- setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
694
- } else if (!session) {
695
- configureTransit(false);
696
- }
697
- } catch {
698
- configureTransit(false);
699
- } finally {
700
- _transitHandshakePromise = null;
701
- }
702
- })();
703
- }
760
+ ensureRestTransitHandshake();
704
761
  client.interceptors.request.use(async (req) => {
762
+ if (_authReadyPromise) await _authReadyPromise;
705
763
  const token = _tokenStore?.getAccessToken();
706
764
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
707
765
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
@@ -711,7 +769,17 @@ function initApiClient(config, tokenStore) {
711
769
  else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
712
770
  _avatarSent = true;
713
771
  }
714
- if (token) await waitForTransitReady();
772
+ if (_config?.transitEncryption) {
773
+ if (!getTransitSession()) ensureRestTransitHandshake();
774
+ const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);
775
+ if (!ready) {
776
+ throw new AntzChatNetworkError(
777
+ "Secure channel to chat server not established \u2014 request not sent. It will retry automatically.",
778
+ "TRANSIT_NOT_READY",
779
+ { url: req.url }
780
+ );
781
+ }
782
+ }
715
783
  if (isTransitEnabled()) {
716
784
  const sessionId = getSessionId();
717
785
  const key = getSessionKey();
@@ -1035,7 +1103,6 @@ var _getToken = null;
1035
1103
  var _userId;
1036
1104
  var _tenantId;
1037
1105
  var _config2 = null;
1038
- var _preservingSession = false;
1039
1106
  function setStatus(s) {
1040
1107
  _status = s;
1041
1108
  _statusListeners.forEach((l) => l(s));
@@ -1054,29 +1121,48 @@ function onSocketStatus(listener) {
1054
1121
  _statusListeners.add(listener);
1055
1122
  return () => _statusListeners.delete(listener);
1056
1123
  }
1057
- async function secureEmit(socket, event, payload, ack) {
1058
- if (isTransitEnabled()) {
1059
- const key = getSessionKey();
1060
- if (key) {
1061
- const envelope = await encryptPayload(payload, key);
1062
- if (ack) {
1063
- socket.emit(event, envelope, async (encryptedResponse) => {
1064
- if (isTransitEnvelope(encryptedResponse)) {
1065
- try {
1066
- const decrypted = await decryptPayload(encryptedResponse, key);
1067
- ack(decrypted);
1068
- } catch {
1069
- ack(encryptedResponse);
1070
- }
1071
- } else {
1072
- ack(encryptedResponse);
1073
- }
1074
- });
1124
+ var SECURE_EMIT_WAIT_MS = 4e3;
1125
+ function emitEncrypted(socket, event, envelope, key, ack) {
1126
+ if (ack) {
1127
+ socket.emit(event, envelope, async (encryptedResponse) => {
1128
+ if (isTransitEnvelope(encryptedResponse)) {
1129
+ try {
1130
+ ack(await decryptPayload(encryptedResponse, key));
1131
+ } catch {
1132
+ ack(encryptedResponse);
1133
+ }
1075
1134
  } else {
1076
- socket.emit(event, envelope);
1135
+ ack(encryptedResponse);
1077
1136
  }
1137
+ });
1138
+ } else {
1139
+ socket.emit(event, envelope);
1140
+ }
1141
+ }
1142
+ async function secureEmit(socket, event, payload, ack) {
1143
+ let key = getSessionKey();
1144
+ if (key && isTransitEnabled()) {
1145
+ emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
1146
+ return;
1147
+ }
1148
+ if (isTransitRequired()) {
1149
+ try {
1150
+ ensureRestTransitHandshake();
1151
+ } catch {
1152
+ }
1153
+ const ok = await awaitTransitReadyOr(SECURE_EMIT_WAIT_MS);
1154
+ key = getSessionKey();
1155
+ if (ok && key && isTransitEnabled()) {
1156
+ emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
1078
1157
  return;
1079
1158
  }
1159
+ if (isTransitRequired()) {
1160
+ throw new AntzChatNetworkError(
1161
+ `Transit encryption required but no session established \u2014 "${event}" not sent`,
1162
+ "TRANSIT_NOT_READY",
1163
+ { event }
1164
+ );
1165
+ }
1080
1166
  }
1081
1167
  if (ack) {
1082
1168
  socket.emit(event, payload, ack);
@@ -1102,6 +1188,28 @@ function secureOn(socket, event, handler) {
1102
1188
  handler(raw);
1103
1189
  });
1104
1190
  }
1191
+ var _joinedRooms = /* @__PURE__ */ new Set();
1192
+ function trackRoomJoin(conversationId) {
1193
+ _joinedRooms.add(conversationId);
1194
+ }
1195
+ function trackRoomLeave(conversationId) {
1196
+ _joinedRooms.delete(conversationId);
1197
+ }
1198
+ function resetTrackedRooms() {
1199
+ _joinedRooms.clear();
1200
+ }
1201
+ function flushJoinedRooms() {
1202
+ const socket = tryGetSocket();
1203
+ if (!socket || _joinedRooms.size === 0) return;
1204
+ for (const conversationId of _joinedRooms) {
1205
+ void secureEmit(socket, "join_room", { conversationId }).catch(() => {
1206
+ });
1207
+ }
1208
+ }
1209
+ onTransitReady(flushJoinedRooms);
1210
+ onSocketStatus((status) => {
1211
+ if (status === "connected") flushJoinedRooms();
1212
+ });
1105
1213
  async function connectSocket(config, getToken) {
1106
1214
  if (_socket && !_socket.disconnected) return _socket;
1107
1215
  if (_connectingPromise) return _connectingPromise;
@@ -1123,6 +1231,7 @@ async function _doConnect(config, getToken) {
1123
1231
  ...config.avatar?.url && { avatarUrl: config.avatar.url },
1124
1232
  ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
1125
1233
  };
1234
+ configureTransitIfUnset(config.transitEncryption);
1126
1235
  let boundDeriveSessionKey = null;
1127
1236
  if (config.transitEncryption) {
1128
1237
  const inFlight = getTransitHandshakePromise();
@@ -1187,7 +1296,6 @@ async function _doConnect(config, getToken) {
1187
1296
  _socket.on("connect", () => setStatus("connected"));
1188
1297
  _socket.on("disconnect", () => {
1189
1298
  setStatus("disconnected");
1190
- if (!_preservingSession) clearTransitSession();
1191
1299
  });
1192
1300
  _socket.on("connect_error", (err) => {
1193
1301
  console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
@@ -1202,8 +1310,9 @@ async function _doConnect(config, getToken) {
1202
1310
  resolve();
1203
1311
  };
1204
1312
  const timeout = setTimeout(() => {
1205
- console.warn("[AntzChat] transit_session timeout \u2014 unblocking HTTP without transit encryption");
1206
- configureTransit(false);
1313
+ console.error(
1314
+ "[AntzChat] transit_session did not arrive within 5s. Transit stays REQUIRED \u2014 chat requests are gated (not downgraded to plaintext) until the handshake succeeds via REST retry or socket reconnect. Persistent failure = check POST /crypto/session (rate limit / 5xx) and the socket transit middleware."
1315
+ );
1207
1316
  done();
1208
1317
  }, 5e3);
1209
1318
  _socket.on("transit_session", async ({ sessionId }) => {
@@ -1290,13 +1399,8 @@ function reconnectSocket(token, userId, tenantId) {
1290
1399
  ...tenantId && { tenantId },
1291
1400
  transitSessionId: existing.sessionId
1292
1401
  };
1293
- _preservingSession = true;
1294
- try {
1295
- if (_socket.connected) _socket.disconnect();
1296
- _socket.connect();
1297
- } finally {
1298
- _preservingSession = false;
1299
- }
1402
+ if (_socket.connected) _socket.disconnect();
1403
+ _socket.connect();
1300
1404
  return;
1301
1405
  }
1302
1406
  if (_getToken) {
@@ -1341,7 +1445,15 @@ async function drainSendQueue(conversationId) {
1341
1445
  entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
1342
1446
  continue;
1343
1447
  }
1344
- entry.run().then(entry.resolve).catch(entry.reject);
1448
+ let acked;
1449
+ try {
1450
+ ({ acked } = await emitWithAck("send_message", entry.payload));
1451
+ } catch (err) {
1452
+ entry.reject(err);
1453
+ while (queue.length > 0) queue.shift().reject(err);
1454
+ break;
1455
+ }
1456
+ acked.then(entry.resolve).catch(entry.reject);
1345
1457
  }
1346
1458
  sendQueues.delete(conversationId);
1347
1459
  sendQueueRunning.delete(conversationId);
@@ -1354,7 +1466,7 @@ function queueSendMessage(payload) {
1354
1466
  return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1355
1467
  }
1356
1468
  return new Promise((resolve, reject) => {
1357
- queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1469
+ queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });
1358
1470
  drainSendQueue(conversationId);
1359
1471
  });
1360
1472
  }
@@ -1377,33 +1489,44 @@ function waitForReconnect() {
1377
1489
  });
1378
1490
  });
1379
1491
  }
1380
- async function withAck(event, payload) {
1492
+ async function emitWithAck(event, payload) {
1381
1493
  let socket = tryGetSocket();
1382
1494
  if (!socket) {
1383
1495
  await waitForReconnect();
1384
1496
  socket = tryGetSocket();
1385
1497
  }
1386
- if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
1387
- return new Promise((resolve, reject) => {
1388
- let timer;
1389
- secureEmit(socket, event, payload, (response) => {
1390
- clearTimeout(timer);
1391
- resolve(response);
1392
- }).then(() => {
1393
- timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
1394
- }).catch(reject);
1498
+ if (!socket) throw new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event });
1499
+ let resolveAck;
1500
+ let rejectAck;
1501
+ const acked = new Promise((res, rej) => {
1502
+ resolveAck = res;
1503
+ rejectAck = rej;
1395
1504
  });
1505
+ await secureEmit(socket, event, payload, (response) => resolveAck(response));
1506
+ const timer = setTimeout(
1507
+ () => rejectAck(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })),
1508
+ ACK_TIMEOUT
1509
+ );
1510
+ void acked.then(() => clearTimeout(timer), () => clearTimeout(timer));
1511
+ return { acked };
1512
+ }
1513
+ async function withAck(event, payload) {
1514
+ const { acked } = await emitWithAck(event, payload);
1515
+ return acked;
1396
1516
  }
1397
1517
  function fireAndForget(event, payload) {
1398
1518
  const socket = tryGetSocket();
1399
1519
  if (!socket) return;
1400
- secureEmit(socket, event, payload);
1520
+ secureEmit(socket, event, payload).catch(() => {
1521
+ });
1401
1522
  }
1402
1523
  var socketEmit = {
1403
1524
  joinRoom(conversationId) {
1525
+ trackRoomJoin(conversationId);
1404
1526
  fireAndForget("join_room", { conversationId });
1405
1527
  },
1406
1528
  leaveRoom(conversationId) {
1529
+ trackRoomLeave(conversationId);
1407
1530
  fireAndForget("leave_room", { conversationId });
1408
1531
  },
1409
1532
  sendMessage(payload) {
@@ -2183,6 +2306,7 @@ var AntzChatClient = class {
2183
2306
  getSocketStatus,
2184
2307
  initApiClient,
2185
2308
  initAuthStore,
2309
+ isApiClientConfigured,
2186
2310
  isMentionAll,
2187
2311
  isTransitEnvelope,
2188
2312
  messagesApi,
@@ -2195,9 +2319,11 @@ var AntzChatClient = class {
2195
2319
  refreshSocketAuth,
2196
2320
  renderMentionParts,
2197
2321
  resetAuthStore,
2322
+ resetTrackedRooms,
2198
2323
  resolveConfig,
2199
2324
  resolveSystemMessageText,
2200
2325
  setApiClientInstance,
2326
+ setAuthReadyPromise,
2201
2327
  setTransitSession,
2202
2328
  socketEmit,
2203
2329
  storageApi,