@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.js CHANGED
@@ -38,7 +38,7 @@ function resolveConfig(config) {
38
38
  userId: config.userId,
39
39
  tenantId: config.tenantId,
40
40
  avatar: config.avatar,
41
- encryptionMode: config.encryptionMode ?? "none",
41
+ transitEncryption: config.transitEncryption ?? true,
42
42
  upload: {
43
43
  maxFileSizeMB: limits,
44
44
  maxFilesPerMessage: config.upload?.maxFilesPerMessage ?? 10,
@@ -125,6 +125,116 @@ async function compressFile(file, platformCompressFn, config) {
125
125
 
126
126
  // src/api/client.ts
127
127
  import axios from "axios";
128
+
129
+ // src/crypto/transit.ts
130
+ async function encryptPayload(data, sessionKey) {
131
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
132
+ const plaintext = new TextEncoder().encode(JSON.stringify(data));
133
+ const encrypted = await globalThis.crypto.subtle.encrypt(
134
+ { name: "AES-GCM", iv },
135
+ sessionKey,
136
+ plaintext
137
+ );
138
+ const ct = encrypted.slice(0, encrypted.byteLength - 16);
139
+ const tag = encrypted.slice(encrypted.byteLength - 16);
140
+ return {
141
+ v: 1,
142
+ iv: bufToB64(iv),
143
+ tag: bufToB64(tag),
144
+ ct: bufToB64(ct)
145
+ };
146
+ }
147
+ async function decryptPayload(envelope, sessionKey) {
148
+ const iv = b64ToBuf(envelope.iv);
149
+ const tag = b64ToBuf(envelope.tag);
150
+ const ct = b64ToBuf(envelope.ct);
151
+ const combined = new Uint8Array(ct.byteLength + tag.byteLength);
152
+ combined.set(new Uint8Array(ct), 0);
153
+ combined.set(new Uint8Array(tag), ct.byteLength);
154
+ const decrypted = await globalThis.crypto.subtle.decrypt(
155
+ { name: "AES-GCM", iv: new Uint8Array(iv) },
156
+ sessionKey,
157
+ combined
158
+ );
159
+ return JSON.parse(new TextDecoder().decode(decrypted));
160
+ }
161
+ function isTransitEnvelope(v) {
162
+ return typeof v === "object" && v !== null && v.v === 1 && typeof v.iv === "string" && typeof v.tag === "string" && typeof v.ct === "string";
163
+ }
164
+ function bufToB64(buf) {
165
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
166
+ let str = "";
167
+ bytes.forEach((b) => {
168
+ str += String.fromCharCode(b);
169
+ });
170
+ return btoa(str);
171
+ }
172
+ function b64ToBuf(b64) {
173
+ const bin = atob(b64);
174
+ const buf = new Uint8Array(bin.length);
175
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
176
+ return buf.buffer;
177
+ }
178
+
179
+ // src/crypto/session.ts
180
+ var _KEY = /* @__PURE__ */ Symbol.for("__antz_chat_transit__");
181
+ function getState() {
182
+ const g = globalThis;
183
+ if (!g[_KEY]) {
184
+ g[_KEY] = {
185
+ session: null,
186
+ sessionEverEstablished: false,
187
+ readyResolve: null,
188
+ readyPromise: null,
189
+ transitConfigured: null
190
+ };
191
+ }
192
+ return g[_KEY];
193
+ }
194
+ function configureTransit(enabled) {
195
+ const s = getState();
196
+ s.transitConfigured = enabled;
197
+ if (!enabled) {
198
+ s.readyResolve?.();
199
+ s.readyResolve = null;
200
+ }
201
+ }
202
+ function waitForTransitReady() {
203
+ const s = getState();
204
+ if (!s.transitConfigured) return Promise.resolve();
205
+ if (s.session) return Promise.resolve();
206
+ if (s.sessionEverEstablished) return Promise.resolve();
207
+ if (!s.readyPromise) {
208
+ s.readyPromise = new Promise((resolve) => {
209
+ s.readyResolve = resolve;
210
+ });
211
+ }
212
+ return s.readyPromise;
213
+ }
214
+ function setTransitSession(session) {
215
+ const s = getState();
216
+ s.session = session;
217
+ s.sessionEverEstablished = true;
218
+ s.readyResolve?.();
219
+ s.readyResolve = null;
220
+ }
221
+ function clearTransitSession() {
222
+ const s = getState();
223
+ s.session = null;
224
+ s.readyPromise = null;
225
+ s.readyResolve = null;
226
+ }
227
+ function isTransitEnabled() {
228
+ return getState().session?.enabled === true;
229
+ }
230
+ function getSessionKey() {
231
+ return getState().session?.sessionKey ?? null;
232
+ }
233
+ function getSessionId() {
234
+ return getState().session?.sessionId ?? null;
235
+ }
236
+
237
+ // src/api/client.ts
128
238
  var _tokenStore = null;
129
239
  var _config = null;
130
240
  var _avatarSent = false;
@@ -136,7 +246,8 @@ function initApiClient(config, tokenStore) {
136
246
  baseURL: config.apiUrl,
137
247
  headers: { "Content-Type": "application/json" }
138
248
  });
139
- client.interceptors.request.use((req) => {
249
+ configureTransit(config.transitEncryption);
250
+ client.interceptors.request.use(async (req) => {
140
251
  const token = _tokenStore?.getAccessToken();
141
252
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
142
253
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
@@ -146,18 +257,54 @@ function initApiClient(config, tokenStore) {
146
257
  else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
147
258
  _avatarSent = true;
148
259
  }
260
+ await waitForTransitReady();
261
+ if (isTransitEnabled()) {
262
+ const sessionId = getSessionId();
263
+ const key = getSessionKey();
264
+ if (sessionId && key) {
265
+ req.headers["x-transit-session"] = sessionId;
266
+ if (req.data !== void 0 && req.data !== null) {
267
+ const envelope = await encryptPayload(req.data, key);
268
+ req.data = envelope;
269
+ req.headers["x-transit-encrypted"] = "1";
270
+ }
271
+ }
272
+ }
149
273
  return req;
150
274
  });
151
275
  let isRefreshing = false;
152
276
  let refreshQueue = [];
153
277
  client.interceptors.response.use(
154
- (response) => {
278
+ async (response) => {
279
+ if (isTransitEnabled()) {
280
+ const key = getSessionKey();
281
+ if (key) {
282
+ if (isTransitEnvelope(response.data)) {
283
+ response.data = await decryptPayload(response.data, key);
284
+ } else if (response.data?.data && isTransitEnvelope(response.data.data)) {
285
+ response.data.data = await decryptPayload(response.data.data, key);
286
+ }
287
+ }
288
+ }
155
289
  if (response.data && typeof response.data === "object" && "success" in response.data && "data" in response.data) {
156
290
  response.data = response.data.data;
157
291
  }
158
292
  return response;
159
293
  },
160
294
  async (error) => {
295
+ if (isTransitEnabled() && error.response?.data) {
296
+ const key = getSessionKey();
297
+ if (key) {
298
+ try {
299
+ if (isTransitEnvelope(error.response.data)) {
300
+ error.response.data = await decryptPayload(error.response.data, key);
301
+ } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {
302
+ error.response.data.data = await decryptPayload(error.response.data.data, key);
303
+ }
304
+ } catch {
305
+ }
306
+ }
307
+ }
161
308
  const original = error.config;
162
309
  if (error.response?.status === 401 && !original._retry) {
163
310
  const refreshToken = _tokenStore?.getRefreshToken();
@@ -444,8 +591,9 @@ var conversationsApi = {
444
591
  async unpin(conversationId) {
445
592
  await getApiClient().delete(`/conversations/${conversationId}/pin`);
446
593
  },
447
- async leave(conversationId) {
448
- await getApiClient().delete(`/conversations/${conversationId}/leave`);
594
+ async leave(conversationId, andDelete) {
595
+ const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
596
+ await getApiClient().delete(url);
449
597
  },
450
598
  async getMembers(conversationId, filter) {
451
599
  const { data } = await getApiClient().get(
@@ -485,6 +633,12 @@ var conversationsApi = {
485
633
  { fileId }
486
634
  );
487
635
  return normalizeConversation(data);
636
+ },
637
+ async removeIcon(conversationId) {
638
+ const { data } = await getApiClient().delete(
639
+ `/conversations/${conversationId}/icon`
640
+ );
641
+ return normalizeConversation(data);
488
642
  }
489
643
  };
490
644
 
@@ -535,7 +689,14 @@ async function uploadBatch(files, platformUploadFn, conversationId, onProgress,
535
689
  filename: f.name,
536
690
  mimeType: f.type,
537
691
  size: f.size,
538
- conversationId
692
+ conversationId,
693
+ ...f.compressed && {
694
+ metadata: {
695
+ compressed: f.compressed,
696
+ originalSize: f.originalSize,
697
+ compressionAlgorithm: f.compressionAlgorithm
698
+ }
699
+ }
539
700
  }));
540
701
  const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
541
702
  const progressMap = {};
@@ -606,6 +767,16 @@ var usersApi = {
606
767
  const { data } = await getApiClient().get(`/users/${userId}`);
607
768
  return { lastSeenAt: data.lastSeenAt ?? null };
608
769
  },
770
+ /**
771
+ * Update basic profile fields for the current user.
772
+ * Works in both builtin and non-builtin modes. Use this to push an immediate
773
+ * profile update to the chat server when the host app knows a change just
774
+ * happened — without waiting for the next 2-hour sync cycle.
775
+ */
776
+ async updateProfile(payload) {
777
+ const { data } = await getApiClient().put("/users/me", payload);
778
+ return data;
779
+ },
609
780
  /**
610
781
  * Update notification preferences for the current user.
611
782
  * Partial update — only send fields you want to change.
@@ -632,6 +803,95 @@ var usersApi = {
632
803
 
633
804
  // src/socket/socket.ts
634
805
  import { io } from "socket.io-client";
806
+
807
+ // src/crypto/detect.ts
808
+ var _cached = null;
809
+ async function detectTransitAlgo() {
810
+ if (_cached) return _cached;
811
+ try {
812
+ await globalThis.crypto.subtle.generateKey(
813
+ { name: "X25519" },
814
+ false,
815
+ ["deriveKey"]
816
+ );
817
+ _cached = "x25519";
818
+ } catch {
819
+ _cached = "p256";
820
+ }
821
+ return _cached;
822
+ }
823
+ function resetAlgoCache() {
824
+ _cached = null;
825
+ }
826
+
827
+ // src/crypto/handshake.ts
828
+ async function fetchServerKeys(apiUrl) {
829
+ const res = await fetch(`${apiUrl}/crypto/pubkey`);
830
+ if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
831
+ const body = await res.json();
832
+ return body?.data ?? body;
833
+ }
834
+ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
835
+ const ephemeral = await globalThis.crypto.subtle.generateKey(
836
+ algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
837
+ false,
838
+ ["deriveBits"]
839
+ );
840
+ const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
841
+ socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
842
+ socketHandshakeAuth["transitAlgo"] = algo;
843
+ const ephemeralPriv = ephemeral.privateKey;
844
+ return (sessionId) => deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
845
+ }
846
+ async function deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
847
+ const serverPubB64 = algo === "x25519" ? serverKeys.x25519 : serverKeys.p256;
848
+ const serverPubRaw = b64ToBuf2(serverPubB64);
849
+ const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
850
+ const serverPubKey = await globalThis.crypto.subtle.importKey(
851
+ "raw",
852
+ serverPubRaw,
853
+ keyAlgoParams,
854
+ false,
855
+ []
856
+ );
857
+ const sharedBits = await globalThis.crypto.subtle.deriveBits(
858
+ { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
859
+ ephemeralPriv,
860
+ 256
861
+ );
862
+ const hkdfKey = await globalThis.crypto.subtle.importKey(
863
+ "raw",
864
+ sharedBits,
865
+ "HKDF",
866
+ false,
867
+ ["deriveKey"]
868
+ );
869
+ const salt = new TextEncoder().encode(sessionId);
870
+ const info = new TextEncoder().encode("antz-transit-v1");
871
+ return globalThis.crypto.subtle.deriveKey(
872
+ { name: "HKDF", hash: "SHA-256", salt, info },
873
+ hkdfKey,
874
+ { name: "AES-GCM", length: 256 },
875
+ false,
876
+ ["encrypt", "decrypt"]
877
+ );
878
+ }
879
+ function bufToB642(buf) {
880
+ const bytes = new Uint8Array(buf);
881
+ let str = "";
882
+ bytes.forEach((b) => {
883
+ str += String.fromCharCode(b);
884
+ });
885
+ return btoa(str);
886
+ }
887
+ function b64ToBuf2(b64) {
888
+ const bin = atob(b64);
889
+ const buf = new Uint8Array(bin.length);
890
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
891
+ return buf.buffer;
892
+ }
893
+
894
+ // src/socket/socket.ts
635
895
  var _socket = null;
636
896
  var _status = "disconnected";
637
897
  var _statusListeners = /* @__PURE__ */ new Set();
@@ -656,23 +916,96 @@ function onSocketStatus(listener) {
656
916
  _statusListeners.add(listener);
657
917
  return () => _statusListeners.delete(listener);
658
918
  }
919
+ async function secureEmit(socket, event, payload, ack) {
920
+ if (isTransitEnabled()) {
921
+ const key = getSessionKey();
922
+ if (key) {
923
+ const envelope = await encryptPayload(payload, key);
924
+ if (ack) {
925
+ socket.emit(event, envelope, async (encryptedResponse) => {
926
+ if (isTransitEnvelope(encryptedResponse)) {
927
+ try {
928
+ const decrypted = await decryptPayload(encryptedResponse, key);
929
+ ack(decrypted);
930
+ } catch {
931
+ ack(encryptedResponse);
932
+ }
933
+ } else {
934
+ ack(encryptedResponse);
935
+ }
936
+ });
937
+ } else {
938
+ socket.emit(event, envelope);
939
+ }
940
+ return;
941
+ }
942
+ }
943
+ if (ack) {
944
+ socket.emit(event, payload, ack);
945
+ } else {
946
+ socket.emit(event, payload);
947
+ }
948
+ }
949
+ function secureOn(socket, event, handler) {
950
+ socket.on(event, async (raw) => {
951
+ if (isTransitEnabled() && isTransitEnvelope(raw)) {
952
+ const key = getSessionKey();
953
+ if (key) {
954
+ try {
955
+ const data = await decryptPayload(raw, key);
956
+ handler(data);
957
+ return;
958
+ } catch {
959
+ console.error(`[AntzChat] Transit decryption failed for event: ${event}`);
960
+ return;
961
+ }
962
+ }
963
+ }
964
+ handler(raw);
965
+ });
966
+ }
659
967
  async function connectSocket(config, getToken) {
660
968
  if (_socket && !_socket.disconnected) return _socket;
661
969
  _getToken = getToken;
662
970
  _userId = config.userId;
663
971
  _tenantId = config.tenantId;
664
972
  const token = getToken();
973
+ const socketHandshakeAuth = {
974
+ token: token ? `Bearer ${token}` : "",
975
+ ...config.userId && { userId: config.userId },
976
+ ...config.tenantId && { tenantId: config.tenantId },
977
+ ...config.avatar?.url && { avatarUrl: config.avatar.url },
978
+ ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
979
+ };
980
+ let boundDeriveSessionKey = null;
981
+ if (config.transitEncryption) {
982
+ try {
983
+ const serverKeys = await fetchServerKeys(config.apiUrl);
984
+ if (!serverKeys.enabled) {
985
+ throw new Error(
986
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
987
+ );
988
+ }
989
+ const algo = await detectTransitAlgo();
990
+ boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
991
+ } catch (err) {
992
+ if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
993
+ console.warn("[AntzChat] Transit handshake setup failed, connecting without transit encryption:", err.message);
994
+ }
995
+ } else {
996
+ try {
997
+ const serverKeys = await fetchServerKeys(config.apiUrl);
998
+ if (serverKeys.enabled) {
999
+ throw new Error(
1000
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides."
1001
+ );
1002
+ }
1003
+ } catch (err) {
1004
+ if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
1005
+ }
1006
+ }
665
1007
  _socket = io(`${config.socketOrigin}/chat`, {
666
- auth: {
667
- token: token ? `Bearer ${token}` : "",
668
- ...config.userId && { userId: config.userId },
669
- ...config.tenantId && { tenantId: config.tenantId },
670
- ...config.avatar?.url && { avatarUrl: config.avatar.url },
671
- ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
672
- },
673
- // path must match SOCKET_IO_PATH on the server (default '/socket.io').
674
- // Set socketPath in SDK config when the server is behind a reverse proxy
675
- // that adds a path prefix (e.g. '/chat-api/socket.io' for UAT).
1008
+ auth: socketHandshakeAuth,
676
1009
  path: config.socketPath,
677
1010
  transports: ["websocket", "polling"],
678
1011
  reconnection: true,
@@ -683,32 +1016,80 @@ async function connectSocket(config, getToken) {
683
1016
  });
684
1017
  setStatus("connecting");
685
1018
  _socket.on("connect", () => setStatus("connected"));
686
- _socket.on("disconnect", () => setStatus("disconnected"));
1019
+ _socket.on("disconnect", () => {
1020
+ setStatus("disconnected");
1021
+ clearTransitSession();
1022
+ });
687
1023
  _socket.on("connect_error", (err) => {
688
1024
  console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
689
1025
  setStatus("error");
690
1026
  });
691
1027
  _socket.on("reconnecting", () => setStatus("reconnecting"));
692
1028
  _socket.on("reconnect", () => setStatus("connected"));
693
- _socket.on("read_receipt", (event) => {
1029
+ if (config.transitEncryption && boundDeriveSessionKey) {
1030
+ await new Promise((resolve) => {
1031
+ const done = () => {
1032
+ clearTimeout(timeout);
1033
+ resolve();
1034
+ };
1035
+ const timeout = setTimeout(() => {
1036
+ console.warn("[AntzChat] transit_session timeout \u2014 unblocking HTTP without transit encryption");
1037
+ configureTransit(false);
1038
+ done();
1039
+ }, 5e3);
1040
+ _socket.on("transit_session", async ({ sessionId }) => {
1041
+ try {
1042
+ if (boundDeriveSessionKey) {
1043
+ const sessionKey = await boundDeriveSessionKey(sessionId);
1044
+ const algo = socketHandshakeAuth["transitAlgo"] ?? "p256";
1045
+ setTransitSession({ sessionKey, algo, sessionId, enabled: true });
1046
+ }
1047
+ } catch (err) {
1048
+ console.error("[AntzChat] Failed to derive transit session key:", err.message);
1049
+ }
1050
+ done();
1051
+ });
1052
+ _socket.on("connect_error", () => done());
1053
+ });
1054
+ }
1055
+ secureOn(_socket, "read_receipt", (event) => {
694
1056
  import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
695
- useChatStore2.getState().setLastRead(event.conversationId, event.messageId, event.readAt);
1057
+ const e = event;
1058
+ useChatStore2.getState().setLastRead(e.conversationId, e.messageId, e.readAt);
696
1059
  });
697
1060
  });
698
- _socket.on("user_online", (event) => {
1061
+ secureOn(_socket, "user_online", (event) => {
699
1062
  import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
700
- const store = useChatStore2.getState();
701
- store.setUserOnline(event.userId);
1063
+ useChatStore2.getState().setUserOnline(event.userId);
702
1064
  });
703
1065
  });
704
- _socket.on("user_offline", (event) => {
1066
+ secureOn(_socket, "user_offline", (event) => {
705
1067
  import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
1068
+ const e = event;
706
1069
  const store = useChatStore2.getState();
707
- store.setUserOffline(event.userId);
708
- if (event.lastSeenAt) store.setLastSeen(event.userId, event.lastSeenAt);
1070
+ store.setUserOffline(e.userId);
1071
+ if (e.lastSeenAt) store.setLastSeen(e.userId, e.lastSeenAt);
709
1072
  });
710
1073
  });
711
- return _socket;
1074
+ return createSecureSocketProxy(_socket);
1075
+ }
1076
+ function createSecureSocketProxy(socket) {
1077
+ return new Proxy(socket, {
1078
+ get(target, prop) {
1079
+ if (prop === "on") {
1080
+ return (event, handler) => {
1081
+ const internal = ["connect", "disconnect", "connect_error", "reconnect", "reconnecting", "error"];
1082
+ if (internal.includes(event)) {
1083
+ return target.on(event, handler);
1084
+ }
1085
+ secureOn(target, event, handler);
1086
+ return socket;
1087
+ };
1088
+ }
1089
+ const val = target[prop];
1090
+ return typeof val === "function" ? val.bind(target) : val;
1091
+ }
1092
+ });
712
1093
  }
713
1094
  function disconnectSocket() {
714
1095
  if (_socket) {
@@ -716,6 +1097,8 @@ function disconnectSocket() {
716
1097
  _socket = null;
717
1098
  setStatus("disconnected");
718
1099
  }
1100
+ clearTransitSession();
1101
+ resetAlgoCache();
719
1102
  _getToken = null;
720
1103
  _userId = void 0;
721
1104
  _tenantId = void 0;
@@ -745,6 +1128,36 @@ function refreshSocketAuth() {
745
1128
  // src/socket/emitters.ts
746
1129
  var ACK_TIMEOUT = 5e3;
747
1130
  var RECONNECT_WAIT_TIMEOUT = 15e3;
1131
+ var QUEUE_MAX_SIZE = 100;
1132
+ var QUEUE_ENTRY_TTL = 3e4;
1133
+ var sendQueue = [];
1134
+ var sendQueueRunning = false;
1135
+ async function drainSendQueue() {
1136
+ if (sendQueueRunning) return;
1137
+ sendQueueRunning = true;
1138
+ while (sendQueue.length > 0) {
1139
+ const entry = sendQueue.shift();
1140
+ if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
1141
+ entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
1142
+ continue;
1143
+ }
1144
+ try {
1145
+ entry.resolve(await entry.run());
1146
+ } catch (e) {
1147
+ entry.reject(e);
1148
+ }
1149
+ }
1150
+ sendQueueRunning = false;
1151
+ }
1152
+ function queueSendMessage(payload) {
1153
+ if (sendQueue.length >= QUEUE_MAX_SIZE) {
1154
+ return Promise.reject(new Error("[AntzChat] Send queue full: too many messages in flight"));
1155
+ }
1156
+ return new Promise((resolve, reject) => {
1157
+ sendQueue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1158
+ drainSendQueue();
1159
+ });
1160
+ }
748
1161
  function waitForReconnect() {
749
1162
  return new Promise((resolve, reject) => {
750
1163
  const timer = setTimeout(() => {
@@ -773,7 +1186,7 @@ async function withAck(event, payload) {
773
1186
  if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
774
1187
  return new Promise((resolve, reject) => {
775
1188
  const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
776
- socket.emit(event, payload, (response) => {
1189
+ secureEmit(socket, event, payload, (response) => {
777
1190
  clearTimeout(timer);
778
1191
  resolve(response);
779
1192
  });
@@ -782,7 +1195,7 @@ async function withAck(event, payload) {
782
1195
  function fireAndForget(event, payload) {
783
1196
  const socket = tryGetSocket();
784
1197
  if (!socket) return;
785
- socket.emit(event, payload);
1198
+ secureEmit(socket, event, payload);
786
1199
  }
787
1200
  var socketEmit = {
788
1201
  joinRoom(conversationId) {
@@ -792,7 +1205,7 @@ var socketEmit = {
792
1205
  fireAndForget("leave_room", { conversationId });
793
1206
  },
794
1207
  sendMessage(payload) {
795
- return withAck("send_message", payload);
1208
+ return queueSendMessage(payload);
796
1209
  },
797
1210
  updateMessage(messageId, text) {
798
1211
  return withAck("update_message", { messageId, text });
@@ -827,7 +1240,7 @@ var socketEmit = {
827
1240
  if (!socket) return Promise.resolve([]);
828
1241
  return new Promise((resolve, reject) => {
829
1242
  const timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
830
- socket.emit("get_online_users", { userIds }, (response) => {
1243
+ secureEmit(socket, "get_online_users", { userIds }, (response) => {
831
1244
  clearTimeout(timer);
832
1245
  if (response && typeof response === "object" && "onlineStatus" in response) {
833
1246
  const status = response.onlineStatus;
@@ -1005,6 +1418,7 @@ export {
1005
1418
  resetAuthStore,
1006
1419
  resolveConfig,
1007
1420
  setApiClientInstance,
1421
+ setTransitSession,
1008
1422
  socketEmit,
1009
1423
  storageApi,
1010
1424
  tryGetSocket,