@antzsoft/chat-core 1.1.1 → 1.1.3

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
@@ -116,6 +116,7 @@ __export(src_exports, {
116
116
  resetAuthStore: () => resetAuthStore,
117
117
  resolveConfig: () => resolveConfig,
118
118
  setApiClientInstance: () => setApiClientInstance,
119
+ setTransitSession: () => setTransitSession,
119
120
  socketEmit: () => socketEmit,
120
121
  storageApi: () => storageApi,
121
122
  tryGetSocket: () => tryGetSocket,
@@ -161,7 +162,7 @@ function resolveConfig(config) {
161
162
  userId: config.userId,
162
163
  tenantId: config.tenantId,
163
164
  avatar: config.avatar,
164
- encryptionMode: config.encryptionMode ?? "none",
165
+ transitEncryption: config.transitEncryption ?? true,
165
166
  upload: {
166
167
  maxFileSizeMB: limits,
167
168
  maxFilesPerMessage: config.upload?.maxFilesPerMessage ?? 10,
@@ -248,6 +249,116 @@ async function compressFile(file, platformCompressFn, config) {
248
249
 
249
250
  // src/api/client.ts
250
251
  var import_axios = __toESM(require("axios"), 1);
252
+
253
+ // src/crypto/transit.ts
254
+ async function encryptPayload(data, sessionKey) {
255
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
256
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
257
+ const encrypted = await globalThis.crypto.subtle.encrypt(
258
+ { name: "AES-GCM", iv },
259
+ sessionKey,
260
+ plaintext
261
+ );
262
+ const ct = encrypted.slice(0, encrypted.byteLength - 16);
263
+ const tag = encrypted.slice(encrypted.byteLength - 16);
264
+ return {
265
+ v: 1,
266
+ iv: bufToB64(iv),
267
+ tag: bufToB64(tag),
268
+ ct: bufToB64(ct)
269
+ };
270
+ }
271
+ async function decryptPayload(envelope, sessionKey) {
272
+ const iv = b64ToBuf(envelope.iv);
273
+ const tag = b64ToBuf(envelope.tag);
274
+ const ct = b64ToBuf(envelope.ct);
275
+ const combined = new Uint8Array(ct.byteLength + tag.byteLength);
276
+ combined.set(new Uint8Array(ct), 0);
277
+ combined.set(new Uint8Array(tag), ct.byteLength);
278
+ const decrypted = await globalThis.crypto.subtle.decrypt(
279
+ { name: "AES-GCM", iv: new Uint8Array(iv) },
280
+ sessionKey,
281
+ combined
282
+ );
283
+ return JSON.parse(new TextDecoder().decode(decrypted));
284
+ }
285
+ function isTransitEnvelope(v) {
286
+ return typeof v === "object" && v !== null && v.v === 1 && typeof v.iv === "string" && typeof v.tag === "string" && typeof v.ct === "string";
287
+ }
288
+ function bufToB64(buf) {
289
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
290
+ let str = "";
291
+ bytes.forEach((b) => {
292
+ str += String.fromCharCode(b);
293
+ });
294
+ return btoa(str);
295
+ }
296
+ function b64ToBuf(b64) {
297
+ const bin = atob(b64);
298
+ const buf = new Uint8Array(bin.length);
299
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
300
+ return buf.buffer;
301
+ }
302
+
303
+ // src/crypto/session.ts
304
+ var _KEY = /* @__PURE__ */ Symbol.for("__antz_chat_transit__");
305
+ function getState() {
306
+ const g = globalThis;
307
+ if (!g[_KEY]) {
308
+ g[_KEY] = {
309
+ session: null,
310
+ sessionEverEstablished: false,
311
+ readyResolve: null,
312
+ readyPromise: null,
313
+ transitConfigured: null
314
+ };
315
+ }
316
+ return g[_KEY];
317
+ }
318
+ function configureTransit(enabled) {
319
+ const s = getState();
320
+ s.transitConfigured = enabled;
321
+ if (!enabled) {
322
+ s.readyResolve?.();
323
+ s.readyResolve = null;
324
+ }
325
+ }
326
+ function waitForTransitReady() {
327
+ const s = getState();
328
+ if (!s.transitConfigured) return Promise.resolve();
329
+ if (s.session) return Promise.resolve();
330
+ if (s.sessionEverEstablished) return Promise.resolve();
331
+ if (!s.readyPromise) {
332
+ s.readyPromise = new Promise((resolve) => {
333
+ s.readyResolve = resolve;
334
+ });
335
+ }
336
+ return s.readyPromise;
337
+ }
338
+ function setTransitSession(session) {
339
+ const s = getState();
340
+ s.session = session;
341
+ s.sessionEverEstablished = true;
342
+ s.readyResolve?.();
343
+ s.readyResolve = null;
344
+ }
345
+ function clearTransitSession() {
346
+ const s = getState();
347
+ s.session = null;
348
+ s.readyPromise = null;
349
+ s.readyResolve = null;
350
+ }
351
+ function isTransitEnabled() {
352
+ return getState().session?.enabled === true;
353
+ }
354
+ function getSessionKey() {
355
+ return getState().session?.sessionKey ?? null;
356
+ }
357
+ function getSessionId() {
358
+ return getState().session?.sessionId ?? null;
359
+ }
360
+
361
+ // src/api/client.ts
251
362
  var _tokenStore = null;
252
363
  var _config = null;
253
364
  var _avatarSent = false;
@@ -259,7 +370,8 @@ function initApiClient(config, tokenStore) {
259
370
  baseURL: config.apiUrl,
260
371
  headers: { "Content-Type": "application/json" }
261
372
  });
262
- client.interceptors.request.use((req) => {
373
+ configureTransit(config.transitEncryption);
374
+ client.interceptors.request.use(async (req) => {
263
375
  const token = _tokenStore?.getAccessToken();
264
376
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
265
377
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
@@ -269,18 +381,54 @@ function initApiClient(config, tokenStore) {
269
381
  else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
270
382
  _avatarSent = true;
271
383
  }
384
+ await waitForTransitReady();
385
+ if (isTransitEnabled()) {
386
+ const sessionId = getSessionId();
387
+ const key = getSessionKey();
388
+ if (sessionId && key) {
389
+ req.headers["x-transit-session"] = sessionId;
390
+ if (req.data !== void 0 && req.data !== null) {
391
+ const envelope = await encryptPayload(req.data, key);
392
+ req.data = envelope;
393
+ req.headers["x-transit-encrypted"] = "1";
394
+ }
395
+ }
396
+ }
272
397
  return req;
273
398
  });
274
399
  let isRefreshing = false;
275
400
  let refreshQueue = [];
276
401
  client.interceptors.response.use(
277
- (response) => {
402
+ async (response) => {
403
+ if (isTransitEnabled()) {
404
+ const key = getSessionKey();
405
+ if (key) {
406
+ if (isTransitEnvelope(response.data)) {
407
+ response.data = await decryptPayload(response.data, key);
408
+ } else if (response.data?.data && isTransitEnvelope(response.data.data)) {
409
+ response.data.data = await decryptPayload(response.data.data, key);
410
+ }
411
+ }
412
+ }
278
413
  if (response.data && typeof response.data === "object" && "success" in response.data && "data" in response.data) {
279
414
  response.data = response.data.data;
280
415
  }
281
416
  return response;
282
417
  },
283
418
  async (error) => {
419
+ if (isTransitEnabled() && error.response?.data) {
420
+ const key = getSessionKey();
421
+ if (key) {
422
+ try {
423
+ if (isTransitEnvelope(error.response.data)) {
424
+ error.response.data = await decryptPayload(error.response.data, key);
425
+ } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {
426
+ error.response.data.data = await decryptPayload(error.response.data.data, key);
427
+ }
428
+ } catch {
429
+ }
430
+ }
431
+ }
284
432
  const original = error.config;
285
433
  if (error.response?.status === 401 && !original._retry) {
286
434
  const refreshToken = _tokenStore?.getRefreshToken();
@@ -567,8 +715,9 @@ var conversationsApi = {
567
715
  async unpin(conversationId) {
568
716
  await getApiClient().delete(`/conversations/${conversationId}/pin`);
569
717
  },
570
- async leave(conversationId) {
571
- await getApiClient().delete(`/conversations/${conversationId}/leave`);
718
+ async leave(conversationId, andDelete) {
719
+ const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
720
+ await getApiClient().delete(url);
572
721
  },
573
722
  async getMembers(conversationId, filter) {
574
723
  const { data } = await getApiClient().get(
@@ -608,6 +757,12 @@ var conversationsApi = {
608
757
  { fileId }
609
758
  );
610
759
  return normalizeConversation(data);
760
+ },
761
+ async removeIcon(conversationId) {
762
+ const { data } = await getApiClient().delete(
763
+ `/conversations/${conversationId}/icon`
764
+ );
765
+ return normalizeConversation(data);
611
766
  }
612
767
  };
613
768
 
@@ -658,7 +813,14 @@ async function uploadBatch(files, platformUploadFn, conversationId, onProgress,
658
813
  filename: f.name,
659
814
  mimeType: f.type,
660
815
  size: f.size,
661
- conversationId
816
+ conversationId,
817
+ ...f.compressed && {
818
+ metadata: {
819
+ compressed: f.compressed,
820
+ originalSize: f.originalSize,
821
+ compressionAlgorithm: f.compressionAlgorithm
822
+ }
823
+ }
662
824
  }));
663
825
  const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
664
826
  const progressMap = {};
@@ -729,6 +891,16 @@ var usersApi = {
729
891
  const { data } = await getApiClient().get(`/users/${userId}`);
730
892
  return { lastSeenAt: data.lastSeenAt ?? null };
731
893
  },
894
+ /**
895
+ * Update basic profile fields for the current user.
896
+ * Works in both builtin and non-builtin modes. Use this to push an immediate
897
+ * profile update to the chat server when the host app knows a change just
898
+ * happened — without waiting for the next 2-hour sync cycle.
899
+ */
900
+ async updateProfile(payload) {
901
+ const { data } = await getApiClient().put("/users/me", payload);
902
+ return data;
903
+ },
732
904
  /**
733
905
  * Update notification preferences for the current user.
734
906
  * Partial update — only send fields you want to change.
@@ -755,6 +927,95 @@ var usersApi = {
755
927
 
756
928
  // src/socket/socket.ts
757
929
  var import_socket = require("socket.io-client");
930
+
931
+ // src/crypto/detect.ts
932
+ var _cached = null;
933
+ async function detectTransitAlgo() {
934
+ if (_cached) return _cached;
935
+ try {
936
+ await globalThis.crypto.subtle.generateKey(
937
+ { name: "X25519" },
938
+ false,
939
+ ["deriveKey"]
940
+ );
941
+ _cached = "x25519";
942
+ } catch {
943
+ _cached = "p256";
944
+ }
945
+ return _cached;
946
+ }
947
+ function resetAlgoCache() {
948
+ _cached = null;
949
+ }
950
+
951
+ // src/crypto/handshake.ts
952
+ async function fetchServerKeys(apiUrl) {
953
+ const res = await fetch(`${apiUrl}/crypto/pubkey`);
954
+ if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
955
+ const body = await res.json();
956
+ return body?.data ?? body;
957
+ }
958
+ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
959
+ const ephemeral = await globalThis.crypto.subtle.generateKey(
960
+ algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
961
+ false,
962
+ ["deriveBits"]
963
+ );
964
+ const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
965
+ socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
966
+ socketHandshakeAuth["transitAlgo"] = algo;
967
+ const ephemeralPriv = ephemeral.privateKey;
968
+ return (sessionId) => deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
969
+ }
970
+ async function deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
971
+ const serverPubB64 = algo === "x25519" ? serverKeys.x25519 : serverKeys.p256;
972
+ const serverPubRaw = b64ToBuf2(serverPubB64);
973
+ const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
974
+ const serverPubKey = await globalThis.crypto.subtle.importKey(
975
+ "raw",
976
+ serverPubRaw,
977
+ keyAlgoParams,
978
+ false,
979
+ []
980
+ );
981
+ const sharedBits = await globalThis.crypto.subtle.deriveBits(
982
+ { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
983
+ ephemeralPriv,
984
+ 256
985
+ );
986
+ const hkdfKey = await globalThis.crypto.subtle.importKey(
987
+ "raw",
988
+ sharedBits,
989
+ "HKDF",
990
+ false,
991
+ ["deriveKey"]
992
+ );
993
+ const salt = new TextEncoder().encode(sessionId);
994
+ const info = new TextEncoder().encode("antz-transit-v1");
995
+ return globalThis.crypto.subtle.deriveKey(
996
+ { name: "HKDF", hash: "SHA-256", salt, info },
997
+ hkdfKey,
998
+ { name: "AES-GCM", length: 256 },
999
+ false,
1000
+ ["encrypt", "decrypt"]
1001
+ );
1002
+ }
1003
+ function bufToB642(buf) {
1004
+ const bytes = new Uint8Array(buf);
1005
+ let str = "";
1006
+ bytes.forEach((b) => {
1007
+ str += String.fromCharCode(b);
1008
+ });
1009
+ return btoa(str);
1010
+ }
1011
+ function b64ToBuf2(b64) {
1012
+ const bin = atob(b64);
1013
+ const buf = new Uint8Array(bin.length);
1014
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
1015
+ return buf.buffer;
1016
+ }
1017
+
1018
+ // src/socket/socket.ts
758
1019
  var _socket = null;
759
1020
  var _status = "disconnected";
760
1021
  var _statusListeners = /* @__PURE__ */ new Set();
@@ -779,23 +1040,96 @@ function onSocketStatus(listener) {
779
1040
  _statusListeners.add(listener);
780
1041
  return () => _statusListeners.delete(listener);
781
1042
  }
1043
+ async function secureEmit(socket, event, payload, ack) {
1044
+ if (isTransitEnabled()) {
1045
+ const key = getSessionKey();
1046
+ if (key) {
1047
+ const envelope = await encryptPayload(payload, key);
1048
+ if (ack) {
1049
+ socket.emit(event, envelope, async (encryptedResponse) => {
1050
+ if (isTransitEnvelope(encryptedResponse)) {
1051
+ try {
1052
+ const decrypted = await decryptPayload(encryptedResponse, key);
1053
+ ack(decrypted);
1054
+ } catch {
1055
+ ack(encryptedResponse);
1056
+ }
1057
+ } else {
1058
+ ack(encryptedResponse);
1059
+ }
1060
+ });
1061
+ } else {
1062
+ socket.emit(event, envelope);
1063
+ }
1064
+ return;
1065
+ }
1066
+ }
1067
+ if (ack) {
1068
+ socket.emit(event, payload, ack);
1069
+ } else {
1070
+ socket.emit(event, payload);
1071
+ }
1072
+ }
1073
+ function secureOn(socket, event, handler) {
1074
+ socket.on(event, async (raw) => {
1075
+ if (isTransitEnabled() && isTransitEnvelope(raw)) {
1076
+ const key = getSessionKey();
1077
+ if (key) {
1078
+ try {
1079
+ const data = await decryptPayload(raw, key);
1080
+ handler(data);
1081
+ return;
1082
+ } catch {
1083
+ console.error(`[AntzChat] Transit decryption failed for event: ${event}`);
1084
+ return;
1085
+ }
1086
+ }
1087
+ }
1088
+ handler(raw);
1089
+ });
1090
+ }
782
1091
  async function connectSocket(config, getToken) {
783
1092
  if (_socket && !_socket.disconnected) return _socket;
784
1093
  _getToken = getToken;
785
1094
  _userId = config.userId;
786
1095
  _tenantId = config.tenantId;
787
1096
  const token = getToken();
1097
+ const socketHandshakeAuth = {
1098
+ token: token ? `Bearer ${token}` : "",
1099
+ ...config.userId && { userId: config.userId },
1100
+ ...config.tenantId && { tenantId: config.tenantId },
1101
+ ...config.avatar?.url && { avatarUrl: config.avatar.url },
1102
+ ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
1103
+ };
1104
+ let boundDeriveSessionKey = null;
1105
+ if (config.transitEncryption) {
1106
+ try {
1107
+ const serverKeys = await fetchServerKeys(config.apiUrl);
1108
+ if (!serverKeys.enabled) {
1109
+ throw new Error(
1110
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
1111
+ );
1112
+ }
1113
+ const algo = await detectTransitAlgo();
1114
+ boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
1115
+ } catch (err) {
1116
+ if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
1117
+ console.warn("[AntzChat] Transit handshake setup failed, connecting without transit encryption:", err.message);
1118
+ }
1119
+ } else {
1120
+ try {
1121
+ const serverKeys = await fetchServerKeys(config.apiUrl);
1122
+ if (serverKeys.enabled) {
1123
+ throw new Error(
1124
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides."
1125
+ );
1126
+ }
1127
+ } catch (err) {
1128
+ if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
1129
+ }
1130
+ }
788
1131
  _socket = (0, import_socket.io)(`${config.socketOrigin}/chat`, {
789
- auth: {
790
- token: token ? `Bearer ${token}` : "",
791
- ...config.userId && { userId: config.userId },
792
- ...config.tenantId && { tenantId: config.tenantId },
793
- ...config.avatar?.url && { avatarUrl: config.avatar.url },
794
- ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
795
- },
796
- // path must match SOCKET_IO_PATH on the server (default '/socket.io').
797
- // Set socketPath in SDK config when the server is behind a reverse proxy
798
- // that adds a path prefix (e.g. '/chat-api/socket.io' for UAT).
1132
+ auth: socketHandshakeAuth,
799
1133
  path: config.socketPath,
800
1134
  transports: ["websocket", "polling"],
801
1135
  reconnection: true,
@@ -806,32 +1140,80 @@ async function connectSocket(config, getToken) {
806
1140
  });
807
1141
  setStatus("connecting");
808
1142
  _socket.on("connect", () => setStatus("connected"));
809
- _socket.on("disconnect", () => setStatus("disconnected"));
1143
+ _socket.on("disconnect", () => {
1144
+ setStatus("disconnected");
1145
+ clearTransitSession();
1146
+ });
810
1147
  _socket.on("connect_error", (err) => {
811
1148
  console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
812
1149
  setStatus("error");
813
1150
  });
814
1151
  _socket.on("reconnecting", () => setStatus("reconnecting"));
815
1152
  _socket.on("reconnect", () => setStatus("connected"));
816
- _socket.on("read_receipt", (event) => {
1153
+ if (config.transitEncryption && boundDeriveSessionKey) {
1154
+ await new Promise((resolve) => {
1155
+ const done = () => {
1156
+ clearTimeout(timeout);
1157
+ resolve();
1158
+ };
1159
+ const timeout = setTimeout(() => {
1160
+ console.warn("[AntzChat] transit_session timeout \u2014 unblocking HTTP without transit encryption");
1161
+ configureTransit(false);
1162
+ done();
1163
+ }, 5e3);
1164
+ _socket.on("transit_session", async ({ sessionId }) => {
1165
+ try {
1166
+ if (boundDeriveSessionKey) {
1167
+ const sessionKey = await boundDeriveSessionKey(sessionId);
1168
+ const algo = socketHandshakeAuth["transitAlgo"] ?? "p256";
1169
+ setTransitSession({ sessionKey, algo, sessionId, enabled: true });
1170
+ }
1171
+ } catch (err) {
1172
+ console.error("[AntzChat] Failed to derive transit session key:", err.message);
1173
+ }
1174
+ done();
1175
+ });
1176
+ _socket.on("connect_error", () => done());
1177
+ });
1178
+ }
1179
+ secureOn(_socket, "read_receipt", (event) => {
817
1180
  Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
818
- useChatStore2.getState().setLastRead(event.conversationId, event.messageId, event.readAt);
1181
+ const e = event;
1182
+ useChatStore2.getState().setLastRead(e.conversationId, e.messageId, e.readAt);
819
1183
  });
820
1184
  });
821
- _socket.on("user_online", (event) => {
1185
+ secureOn(_socket, "user_online", (event) => {
822
1186
  Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
823
- const store = useChatStore2.getState();
824
- store.setUserOnline(event.userId);
1187
+ useChatStore2.getState().setUserOnline(event.userId);
825
1188
  });
826
1189
  });
827
- _socket.on("user_offline", (event) => {
1190
+ secureOn(_socket, "user_offline", (event) => {
828
1191
  Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
1192
+ const e = event;
829
1193
  const store = useChatStore2.getState();
830
- store.setUserOffline(event.userId);
831
- if (event.lastSeenAt) store.setLastSeen(event.userId, event.lastSeenAt);
1194
+ store.setUserOffline(e.userId);
1195
+ if (e.lastSeenAt) store.setLastSeen(e.userId, e.lastSeenAt);
832
1196
  });
833
1197
  });
834
- return _socket;
1198
+ return createSecureSocketProxy(_socket);
1199
+ }
1200
+ function createSecureSocketProxy(socket) {
1201
+ return new Proxy(socket, {
1202
+ get(target, prop) {
1203
+ if (prop === "on") {
1204
+ return (event, handler) => {
1205
+ const internal = ["connect", "disconnect", "connect_error", "reconnect", "reconnecting", "error"];
1206
+ if (internal.includes(event)) {
1207
+ return target.on(event, handler);
1208
+ }
1209
+ secureOn(target, event, handler);
1210
+ return socket;
1211
+ };
1212
+ }
1213
+ const val = target[prop];
1214
+ return typeof val === "function" ? val.bind(target) : val;
1215
+ }
1216
+ });
835
1217
  }
836
1218
  function disconnectSocket() {
837
1219
  if (_socket) {
@@ -839,6 +1221,8 @@ function disconnectSocket() {
839
1221
  _socket = null;
840
1222
  setStatus("disconnected");
841
1223
  }
1224
+ clearTransitSession();
1225
+ resetAlgoCache();
842
1226
  _getToken = null;
843
1227
  _userId = void 0;
844
1228
  _tenantId = void 0;
@@ -868,6 +1252,36 @@ function refreshSocketAuth() {
868
1252
  // src/socket/emitters.ts
869
1253
  var ACK_TIMEOUT = 5e3;
870
1254
  var RECONNECT_WAIT_TIMEOUT = 15e3;
1255
+ var QUEUE_MAX_SIZE = 100;
1256
+ var QUEUE_ENTRY_TTL = 3e4;
1257
+ var sendQueue = [];
1258
+ var sendQueueRunning = false;
1259
+ async function drainSendQueue() {
1260
+ if (sendQueueRunning) return;
1261
+ sendQueueRunning = true;
1262
+ while (sendQueue.length > 0) {
1263
+ const entry = sendQueue.shift();
1264
+ if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
1265
+ entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
1266
+ continue;
1267
+ }
1268
+ try {
1269
+ entry.resolve(await entry.run());
1270
+ } catch (e) {
1271
+ entry.reject(e);
1272
+ }
1273
+ }
1274
+ sendQueueRunning = false;
1275
+ }
1276
+ function queueSendMessage(payload) {
1277
+ if (sendQueue.length >= QUEUE_MAX_SIZE) {
1278
+ return Promise.reject(new Error("[AntzChat] Send queue full: too many messages in flight"));
1279
+ }
1280
+ return new Promise((resolve, reject) => {
1281
+ sendQueue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1282
+ drainSendQueue();
1283
+ });
1284
+ }
871
1285
  function waitForReconnect() {
872
1286
  return new Promise((resolve, reject) => {
873
1287
  const timer = setTimeout(() => {
@@ -896,7 +1310,7 @@ async function withAck(event, payload) {
896
1310
  if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
897
1311
  return new Promise((resolve, reject) => {
898
1312
  const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
899
- socket.emit(event, payload, (response) => {
1313
+ secureEmit(socket, event, payload, (response) => {
900
1314
  clearTimeout(timer);
901
1315
  resolve(response);
902
1316
  });
@@ -905,7 +1319,7 @@ async function withAck(event, payload) {
905
1319
  function fireAndForget(event, payload) {
906
1320
  const socket = tryGetSocket();
907
1321
  if (!socket) return;
908
- socket.emit(event, payload);
1322
+ secureEmit(socket, event, payload);
909
1323
  }
910
1324
  var socketEmit = {
911
1325
  joinRoom(conversationId) {
@@ -915,7 +1329,7 @@ var socketEmit = {
915
1329
  fireAndForget("leave_room", { conversationId });
916
1330
  },
917
1331
  sendMessage(payload) {
918
- return withAck("send_message", payload);
1332
+ return queueSendMessage(payload);
919
1333
  },
920
1334
  updateMessage(messageId, text) {
921
1335
  return withAck("update_message", { messageId, text });
@@ -950,7 +1364,7 @@ var socketEmit = {
950
1364
  if (!socket) return Promise.resolve([]);
951
1365
  return new Promise((resolve, reject) => {
952
1366
  const timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
953
- socket.emit("get_online_users", { userIds }, (response) => {
1367
+ secureEmit(socket, "get_online_users", { userIds }, (response) => {
954
1368
  clearTimeout(timer);
955
1369
  if (response && typeof response === "object" && "onlineStatus" in response) {
956
1370
  const status = response.onlineStatus;
@@ -1132,6 +1546,7 @@ var AntzChatClient = class {
1132
1546
  resetAuthStore,
1133
1547
  resolveConfig,
1134
1548
  setApiClientInstance,
1549
+ setTransitSession,
1135
1550
  socketEmit,
1136
1551
  storageApi,
1137
1552
  tryGetSocket,