@antzsoft/chat-core 1.4.3 → 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();
@@ -1013,559 +1081,38 @@ function resolveSystemMessageText(message, currentUserId) {
1013
1081
  }
1014
1082
  }
1015
1083
 
1016
- // src/api/messages.ts
1017
- var MAX_FORWARD_TARGETS = 5;
1018
- var HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;
1019
- var messagesApi = {
1020
- async list(conversationId, params = {}) {
1021
- const { cursor, direction, ...rest } = params;
1022
- const serverParams = { ...rest };
1023
- if (cursor) {
1024
- serverParams[direction === "after" ? "after" : "before"] = cursor;
1025
- }
1026
- const { data } = await getApiClient().get(
1027
- `/conversations/${conversationId}/messages`,
1028
- { params: serverParams }
1029
- );
1030
- const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
1031
- if (!currentUserId) return data;
1032
- return {
1033
- ...data,
1034
- data: data.data.map(
1035
- (m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
1036
- )
1037
- };
1038
- },
1039
- async get(messageId) {
1040
- const { data } = await getApiClient().get(`/messages/${messageId}`);
1041
- return data;
1042
- },
1043
- async send(conversationId, payload) {
1044
- const { data } = await getApiClient().post(
1045
- `/conversations/${conversationId}/messages`,
1046
- payload
1047
- );
1048
- return data;
1049
- },
1050
- async update(messageId, text) {
1051
- const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
1052
- return data;
1053
- },
1054
- async delete(messageId) {
1055
- await getApiClient().post(`/messages/${messageId}/delete`);
1056
- },
1057
- async deleteForMe(messageId) {
1058
- await getApiClient().post(`/messages/${messageId}/delete-for-me`);
1059
- },
1060
- async addReaction(messageId, emoji) {
1061
- const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
1062
- return data;
1063
- },
1064
- async removeReaction(messageId, emoji) {
1065
- const { data } = await getApiClient().post(
1066
- `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
1067
- );
1068
- return data;
1069
- },
1070
- async getReactions(messageId) {
1071
- const { data } = await getApiClient().post(`/messages/${messageId}/reactions/list`);
1072
- return data;
1073
- },
1074
- async star(messageId) {
1075
- await getApiClient().post(`/messages/${messageId}/star`);
1076
- },
1077
- async unstar(messageId) {
1078
- await getApiClient().post(`/messages/${messageId}/unstar`);
1079
- },
1080
- async getStarred(params = {}) {
1081
- const { data } = await getApiClient().get("/messages/starred", { params });
1082
- return data;
1083
- },
1084
- async search(params) {
1085
- const { data } = await getApiClient().get("/messages/search", { params });
1086
- return data;
1087
- },
1088
- async getLastRead(conversationId) {
1089
- const { data } = await getApiClient().get(
1090
- `/conversations/${conversationId}/read-receipt`
1091
- );
1092
- return data;
1093
- },
1094
- async markAsRead(conversationId, messageId) {
1095
- await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
1096
- },
1097
- async pin(messageId) {
1098
- const { data } = await getApiClient().post(`/messages/${messageId}/pin`);
1099
- return data;
1100
- },
1101
- async unpin(messageId) {
1102
- const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
1103
- return data;
1104
- },
1105
- async getPinned(conversationId) {
1106
- const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
1107
- return data;
1108
- },
1109
- async getReceipts(messageId) {
1110
- const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
1111
- return data;
1112
- },
1113
- /**
1114
- * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
1115
- * per call, also enforced server-side). Each target is independent — one failing
1116
- * (e.g. no longer a participant) does not block the others; check `success`/`error`
1117
- * per entry in the returned array.
1118
- *
1119
- * Pass `attachmentIds` to forward only a subset of the source message's attachments
1120
- * (e.g. one image out of a multi-image message) — omit it to forward the whole
1121
- * message, including all its attachments, unchanged. An ID not present on the
1122
- * source message is ignored server-side.
1123
- */
1124
- async forward(messageId, targetConversationIds, attachmentIds) {
1125
- const { data } = await getApiClient().post(
1126
- `/messages/${messageId}/forward`,
1127
- { targetConversationIds, ...attachmentIds ? { attachmentIds } : {} }
1128
- );
1129
- return data;
1084
+ // src/crypto/uuid.ts
1085
+ function generateUUID() {
1086
+ if (typeof globalThis.crypto?.randomUUID === "function") {
1087
+ return globalThis.crypto.randomUUID();
1130
1088
  }
1131
- };
1089
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1090
+ const r = Math.random() * 16 | 0;
1091
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1092
+ });
1093
+ }
1132
1094
 
1133
- // src/api/conversations.ts
1134
- function normalizeParticipant(p) {
1135
- const hasUserDetails = p.displayName || p.username || p.avatarUrl;
1136
- return {
1137
- userId: p.userId,
1138
- externalId: p.externalId ?? p.user?.externalId,
1139
- role: p.role,
1140
- joinedAt: p.joinedAt,
1141
- isActive: p.isActive,
1142
- user: hasUserDetails ? {
1143
- id: p.userId,
1144
- externalId: p.externalId,
1145
- tenantId: "",
1146
- email: "",
1147
- username: p.username ?? "",
1148
- displayName: p.displayName ?? p.username ?? "",
1149
- avatarUrl: p.avatarUrl,
1150
- status: "offline",
1151
- createdAt: p.joinedAt ?? "",
1152
- updatedAt: p.joinedAt ?? ""
1153
- } : p.user
1154
- };
1095
+ // src/socket/socket.ts
1096
+ var import_socket = require("socket.io-client");
1097
+ var _socket = null;
1098
+ var _socketProxy = null;
1099
+ var _connectingPromise = null;
1100
+ var _status = "disconnected";
1101
+ var _statusListeners = /* @__PURE__ */ new Set();
1102
+ var _getToken = null;
1103
+ var _userId;
1104
+ var _tenantId;
1105
+ var _config2 = null;
1106
+ function setStatus(s) {
1107
+ _status = s;
1108
+ _statusListeners.forEach((l) => l(s));
1155
1109
  }
1156
- function normalizeLastMessage(lastMsg) {
1157
- if (!lastMsg) return void 0;
1158
- if (lastMsg.content !== void 0) return lastMsg;
1159
- return {
1160
- id: lastMsg.messageId ?? "",
1161
- tenantId: "",
1162
- conversationId: "",
1163
- senderId: lastMsg.senderId ?? "",
1164
- content: {
1165
- type: lastMsg.hasAttachments ? "attachment" : "text",
1166
- text: lastMsg.contentPreview
1167
- },
1168
- reactions: [],
1169
- lastReaction: lastMsg.lastReaction ?? null,
1170
- status: lastMsg.status ?? "active",
1171
- deliveryStatus: lastMsg.deliveryStatus ?? "sent",
1172
- isEdited: false,
1173
- sentAt: lastMsg.sentAt ?? "",
1174
- createdAt: lastMsg.sentAt ?? "",
1175
- ...lastMsg.senderName && { senderName: lastMsg.senderName },
1176
- ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
1177
- };
1110
+ function getSocket() {
1111
+ if (!_socketProxy) throw new Error("[AntzChat] Socket not initialized. Call connectSocket first.");
1112
+ return _socketProxy;
1178
1113
  }
1179
- function normalizeConversation(conv) {
1180
- return {
1181
- ...conv,
1182
- id: conv.id ?? conv.conversationId,
1183
- participants: (conv.participants ?? []).map(normalizeParticipant),
1184
- lastMessage: normalizeLastMessage(conv.lastMessage)
1185
- };
1186
- }
1187
- var conversationsApi = {
1188
- async list(params = {}) {
1189
- const { data } = await getApiClient().get("/conversations", { params });
1190
- return { ...data, data: data.data.map(normalizeConversation) };
1191
- },
1192
- async get(conversationId) {
1193
- const { data } = await getApiClient().get(`/conversations/${conversationId}`);
1194
- return normalizeConversation(data);
1195
- },
1196
- async createGroup(payload) {
1197
- const { data } = await getApiClient().post("/conversations", payload);
1198
- return normalizeConversation(data);
1199
- },
1200
- async createDirect(payload) {
1201
- const { data } = await getApiClient().post("/conversations/direct", payload);
1202
- return normalizeConversation(data);
1203
- },
1204
- async update(conversationId, payload) {
1205
- const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
1206
- return normalizeConversation(data);
1207
- },
1208
- async delete(conversationId) {
1209
- await getApiClient().post(`/conversations/${conversationId}/delete`);
1210
- },
1211
- async addParticipants(conversationId, userIds, role) {
1212
- const { data } = await getApiClient().post(
1213
- `/conversations/${conversationId}/participants`,
1214
- { userIds, ...role && { role } }
1215
- );
1216
- return normalizeConversation(data);
1217
- },
1218
- async removeParticipant(conversationId, userId) {
1219
- const { data } = await getApiClient().post(
1220
- `/conversations/${conversationId}/participants/${userId}/remove`
1221
- );
1222
- return normalizeConversation(data);
1223
- },
1224
- async updateParticipantRole(conversationId, userId, role) {
1225
- const { data } = await getApiClient().post(
1226
- `/conversations/${conversationId}/participants/${userId}/role`,
1227
- { role }
1228
- );
1229
- return normalizeConversation(data);
1230
- },
1231
- async mute(conversationId, mutedUntil) {
1232
- await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
1233
- },
1234
- async unmute(conversationId) {
1235
- await getApiClient().post(`/conversations/${conversationId}/unmute`);
1236
- },
1237
- async pin(conversationId) {
1238
- await getApiClient().post(`/conversations/${conversationId}/pin`);
1239
- },
1240
- async unpin(conversationId) {
1241
- await getApiClient().post(`/conversations/${conversationId}/unpin`);
1242
- },
1243
- async markUnread(conversationId) {
1244
- await getApiClient().post(`/conversations/${conversationId}/unread`);
1245
- },
1246
- async markRead(conversationId) {
1247
- await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
1248
- },
1249
- async leave(conversationId, andDelete) {
1250
- const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
1251
- await getApiClient().post(url);
1252
- },
1253
- async getMembers(conversationId, filter) {
1254
- const { data } = await getApiClient().get(
1255
- `/conversations/${conversationId}/participants`,
1256
- filter ? { params: { filter } } : void 0
1257
- );
1258
- return (data ?? []).map(normalizeParticipant);
1259
- },
1260
- /**
1261
- * Get unread message count for a single conversation.
1262
- * Use this after app foreground or socket reconnect to refresh a specific count.
1263
- */
1264
- async getUnreadCount(conversationId) {
1265
- const { data } = await getApiClient().get(
1266
- `/conversations/${conversationId}/unread`
1267
- );
1268
- return data;
1269
- },
1270
- /**
1271
- * Get total unread count across all conversations + per-conversation breakdown.
1272
- * Use on app cold start, foreground resume, or after socket reconnect.
1273
- * The socket keeps counts live while connected — this is the source of truth
1274
- * when the socket was down.
1275
- */
1276
- async getUnreadSummary() {
1277
- const { data } = await getApiClient().get("/conversations/unread");
1278
- return data;
1279
- },
1280
- /**
1281
- * Set the group icon from an already-uploaded file (admin only).
1282
- * The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.
1283
- * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
1284
- */
1285
- async uploadIcon(conversationId, fileId) {
1286
- const { data } = await getApiClient().post(
1287
- `/conversations/${conversationId}/icon`,
1288
- { fileId }
1289
- );
1290
- return normalizeConversation(data);
1291
- },
1292
- async removeIcon(conversationId) {
1293
- const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
1294
- return normalizeConversation(data);
1295
- },
1296
- async clearChat(conversationId) {
1297
- await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
1298
- }
1299
- };
1300
-
1301
- // src/crypto/uuid.ts
1302
- function generateUUID() {
1303
- if (typeof globalThis.crypto?.randomUUID === "function") {
1304
- return globalThis.crypto.randomUUID();
1305
- }
1306
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1307
- const r = Math.random() * 16 | 0;
1308
- return (c === "x" ? r : r & 3 | 8).toString(16);
1309
- });
1310
- }
1311
-
1312
- // src/api/storage.ts
1313
- var storageApi = {
1314
- async requestPresignedUrl(payload) {
1315
- const { data } = await getApiClient().post("/storage/presigned-url", payload);
1316
- return data;
1317
- },
1318
- async requestPresignedUrlBatch(files) {
1319
- const { data } = await getApiClient().post("/storage/presigned-url/batch", { files });
1320
- return data;
1321
- },
1322
- async confirmUpload(fileId) {
1323
- const { data } = await getApiClient().post(`/storage/confirm/${fileId}`);
1324
- return data;
1325
- },
1326
- async getFile(fileId) {
1327
- const { data } = await getApiClient().get(`/storage/files/${fileId}`);
1328
- return data;
1329
- },
1330
- async getFileUrl(fileId, expiresIn) {
1331
- const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {
1332
- params: expiresIn ? { expiresIn } : {}
1333
- });
1334
- return data;
1335
- },
1336
- async deleteFile(fileId) {
1337
- await getApiClient().post(`/storage/files/${fileId}/delete`);
1338
- },
1339
- async completeMultipartUpload(fileId, uploadId, parts) {
1340
- const { data } = await getApiClient().post(
1341
- `/storage/multipart/complete/${fileId}`,
1342
- { uploadId, parts }
1343
- );
1344
- return data;
1345
- },
1346
- async getConversationFiles(conversationId, params = {}) {
1347
- const { data } = await getApiClient().get(
1348
- `/storage/conversations/${conversationId}/files`,
1349
- { params }
1350
- );
1351
- return data;
1352
- },
1353
- async getMyFiles(params = {}) {
1354
- const { data } = await getApiClient().get("/storage/my-files", { params });
1355
- return data;
1356
- }
1357
- };
1358
- async function runMultipartUpload(presigned, file, platformUploadPartFn, onProgress) {
1359
- const { multipart } = presigned;
1360
- if (!multipart) throw new Error("No multipart info on presigned response");
1361
- const CONCURRENCY = 3;
1362
- const completedParts = [];
1363
- const partProgress = {};
1364
- multipart.partUrls.forEach(({ partNumber }) => {
1365
- partProgress[partNumber] = 0;
1366
- });
1367
- const reportProgress = () => {
1368
- if (!onProgress) return;
1369
- const vals = Object.values(partProgress);
1370
- const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1371
- onProgress(Math.round(avg * 0.95));
1372
- };
1373
- const uploadPart = async (partNumber, uploadUrl, method) => {
1374
- const offset = (partNumber - 1) * multipart.chunkSize;
1375
- const end = Math.min(offset + multipart.chunkSize, file.size);
1376
- const blob = await fetch(file.uri).then((r) => r.blob());
1377
- const slice = blob.slice(offset, end);
1378
- const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {
1379
- partProgress[partNumber] = pct;
1380
- reportProgress();
1381
- }, method);
1382
- completedParts.push({ partNumber, etag });
1383
- partProgress[partNumber] = 100;
1384
- reportProgress();
1385
- };
1386
- for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {
1387
- const batch = multipart.partUrls.slice(i, i + CONCURRENCY);
1388
- const results = await Promise.allSettled(
1389
- batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? "PUT"))
1390
- );
1391
- const failed = results.find((r) => r.status === "rejected");
1392
- if (failed) throw failed.reason;
1393
- }
1394
- completedParts.sort((a, b) => a.partNumber - b.partNumber);
1395
- const fileResponse = await storageApi.completeMultipartUpload(
1396
- presigned.fileId,
1397
- multipart.uploadId,
1398
- completedParts
1399
- );
1400
- onProgress?.(100);
1401
- return fileResponse;
1402
- }
1403
- async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1404
- const compressedFiles = await Promise.all(
1405
- files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
1406
- );
1407
- const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));
1408
- const requests = slotted.map(({ file: f, clientIndex }) => ({
1409
- filename: f.name,
1410
- mimeType: f.type,
1411
- size: f.size,
1412
- conversationId,
1413
- clientIndex,
1414
- ...f.compressed && {
1415
- metadata: {
1416
- compressed: f.compressed,
1417
- originalSize: f.originalSize,
1418
- compressionAlgorithm: f.compressionAlgorithm
1419
- }
1420
- }
1421
- }));
1422
- const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
1423
- const failedSlotIds = /* @__PURE__ */ new Set();
1424
- const failed = requestErrors.map((e) => {
1425
- const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);
1426
- const slotId = slotted[idx]?.slotId;
1427
- if (slotId) failedSlotIds.add(slotId);
1428
- return { filename: e.filename, error: e.error };
1429
- });
1430
- const progressMap = {};
1431
- const reportProgress = () => {
1432
- if (!onProgress) return;
1433
- const vals = Object.values(progressMap);
1434
- const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1435
- onProgress(Math.round(avg));
1436
- };
1437
- const successful = [];
1438
- const slotToFile = /* @__PURE__ */ new Map();
1439
- await Promise.all(
1440
- urls.map(async (presigned, idx) => {
1441
- const originalIdx = presigned.clientIndex ?? idx;
1442
- const { file, slotId } = slotted[originalIdx];
1443
- progressMap[originalIdx] = 0;
1444
- try {
1445
- let fileResponse;
1446
- if (presigned.multipart && platformUploadPartFn) {
1447
- fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {
1448
- progressMap[originalIdx] = pct;
1449
- reportProgress();
1450
- });
1451
- } else {
1452
- await platformUploadFn(presigned, file, (pct) => {
1453
- progressMap[originalIdx] = Math.round(pct * 0.9);
1454
- reportProgress();
1455
- });
1456
- fileResponse = await storageApi.confirmUpload(presigned.fileId);
1457
- }
1458
- progressMap[originalIdx] = 100;
1459
- reportProgress();
1460
- successful.push(fileResponse);
1461
- slotToFile.set(slotId, fileResponse);
1462
- } catch (err) {
1463
- failed.push({ filename: file.name, error: err.message });
1464
- }
1465
- })
1466
- );
1467
- return { result: { successful, failed }, slotToFile };
1468
- }
1469
- async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1470
- const slotIds = files.map(() => generateUUID());
1471
- const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);
1472
- return result;
1473
- }
1474
-
1475
- // src/api/devices.ts
1476
- var devicesApi = {
1477
- /**
1478
- * Register or update a device push token with the chat server.
1479
- *
1480
- * Upserts by `deviceId` — calling this multiple times with the same deviceId
1481
- * simply refreshes the token value (tokens can rotate silently on some platforms).
1482
- *
1483
- * The SDK never calls this automatically. The parent app or the `tokenProvider`
1484
- * option in `pushNotifications` config is responsible for calling it after
1485
- * obtaining the token from the OS / browser.
1486
- */
1487
- async register(payload) {
1488
- await getApiClient().post("/users/me/devices", payload);
1489
- },
1490
- /**
1491
- * Remove a device token from the chat server.
1492
- * Call this on logout so the user stops receiving push notifications on this device.
1493
- */
1494
- async remove(deviceId) {
1495
- await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
1496
- }
1497
- };
1498
-
1499
- // src/api/users.ts
1500
- var usersApi = {
1501
- async list(params = {}) {
1502
- const { data } = await getApiClient().get("/users", { params });
1503
- return data;
1504
- },
1505
- async getById(userId) {
1506
- const { data } = await getApiClient().get(`/users/${userId}`);
1507
- return data;
1508
- },
1509
- async getLastSeen(userId) {
1510
- const { data } = await getApiClient().get(`/users/${userId}`);
1511
- return { lastSeenAt: data.lastSeenAt ?? null };
1512
- },
1513
- /**
1514
- * Update basic profile fields for the current user.
1515
- * Works in both builtin and non-builtin modes. Use this to push an immediate
1516
- * profile update to the chat server when the host app knows a change just
1517
- * happened — without waiting for the next 2-hour sync cycle.
1518
- */
1519
- async updateProfile(payload) {
1520
- const { data } = await getApiClient().post("/users/me/update", payload);
1521
- return data;
1522
- },
1523
- /**
1524
- * Update notification preferences for the current user.
1525
- * Partial update — only send fields you want to change.
1526
- * A prefs record is automatically created with defaults when a device
1527
- * token is first registered, so this never fails with "not found".
1528
- */
1529
- async updatePreferences(prefs) {
1530
- const { data } = await getApiClient().post("/users/me/preferences", prefs);
1531
- return data;
1532
- },
1533
- /**
1534
- * Fetch current notification preferences for the current user.
1535
- * Returns null if no prefs record exists yet (all defaults apply).
1536
- */
1537
- async getPreferences() {
1538
- try {
1539
- const { data } = await getApiClient().get("/users/me/preferences");
1540
- return data;
1541
- } catch {
1542
- return null;
1543
- }
1544
- }
1545
- };
1546
-
1547
- // src/socket/socket.ts
1548
- var import_socket = require("socket.io-client");
1549
- var _socket = null;
1550
- var _socketProxy = null;
1551
- var _connectingPromise = null;
1552
- var _status = "disconnected";
1553
- var _statusListeners = /* @__PURE__ */ new Set();
1554
- var _getToken = null;
1555
- var _userId;
1556
- var _tenantId;
1557
- var _config2 = null;
1558
- var _preservingSession = false;
1559
- function setStatus(s) {
1560
- _status = s;
1561
- _statusListeners.forEach((l) => l(s));
1562
- }
1563
- function getSocket() {
1564
- if (!_socketProxy) throw new Error("[AntzChat] Socket not initialized. Call connectSocket first.");
1565
- return _socketProxy;
1566
- }
1567
- function tryGetSocket() {
1568
- return _socket?.connected ? _socketProxy : null;
1114
+ function tryGetSocket() {
1115
+ return _socket?.connected ? _socketProxy : null;
1569
1116
  }
1570
1117
  function getSocketStatus() {
1571
1118
  return _status;
@@ -1574,29 +1121,48 @@ function onSocketStatus(listener) {
1574
1121
  _statusListeners.add(listener);
1575
1122
  return () => _statusListeners.delete(listener);
1576
1123
  }
1577
- async function secureEmit(socket, event, payload, ack) {
1578
- if (isTransitEnabled()) {
1579
- const key = getSessionKey();
1580
- if (key) {
1581
- const envelope = await encryptPayload(payload, key);
1582
- if (ack) {
1583
- socket.emit(event, envelope, async (encryptedResponse) => {
1584
- if (isTransitEnvelope(encryptedResponse)) {
1585
- try {
1586
- const decrypted = await decryptPayload(encryptedResponse, key);
1587
- ack(decrypted);
1588
- } catch {
1589
- ack(encryptedResponse);
1590
- }
1591
- } else {
1592
- ack(encryptedResponse);
1593
- }
1594
- });
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
+ }
1595
1134
  } else {
1596
- socket.emit(event, envelope);
1135
+ ack(encryptedResponse);
1597
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);
1598
1157
  return;
1599
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
+ }
1600
1166
  }
1601
1167
  if (ack) {
1602
1168
  socket.emit(event, payload, ack);
@@ -1622,6 +1188,28 @@ function secureOn(socket, event, handler) {
1622
1188
  handler(raw);
1623
1189
  });
1624
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
+ });
1625
1213
  async function connectSocket(config, getToken) {
1626
1214
  if (_socket && !_socket.disconnected) return _socket;
1627
1215
  if (_connectingPromise) return _connectingPromise;
@@ -1643,6 +1231,7 @@ async function _doConnect(config, getToken) {
1643
1231
  ...config.avatar?.url && { avatarUrl: config.avatar.url },
1644
1232
  ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
1645
1233
  };
1234
+ configureTransitIfUnset(config.transitEncryption);
1646
1235
  let boundDeriveSessionKey = null;
1647
1236
  if (config.transitEncryption) {
1648
1237
  const inFlight = getTransitHandshakePromise();
@@ -1707,7 +1296,6 @@ async function _doConnect(config, getToken) {
1707
1296
  _socket.on("connect", () => setStatus("connected"));
1708
1297
  _socket.on("disconnect", () => {
1709
1298
  setStatus("disconnected");
1710
- if (!_preservingSession) clearTransitSession();
1711
1299
  });
1712
1300
  _socket.on("connect_error", (err) => {
1713
1301
  console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
@@ -1722,8 +1310,9 @@ async function _doConnect(config, getToken) {
1722
1310
  resolve();
1723
1311
  };
1724
1312
  const timeout = setTimeout(() => {
1725
- console.warn("[AntzChat] transit_session timeout \u2014 unblocking HTTP without transit encryption");
1726
- 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
+ );
1727
1316
  done();
1728
1317
  }, 5e3);
1729
1318
  _socket.on("transit_session", async ({ sessionId }) => {
@@ -1763,225 +1352,796 @@ async function _doConnect(config, getToken) {
1763
1352
  if (e.lastSeenAt) store.setLastSeen(e.userId, e.lastSeenAt);
1764
1353
  });
1765
1354
  });
1766
- _socketProxy = createSecureSocketProxy(_socket);
1767
- return _socketProxy;
1355
+ _socketProxy = createSecureSocketProxy(_socket);
1356
+ return _socketProxy;
1357
+ }
1358
+ function createSecureSocketProxy(socket) {
1359
+ return new Proxy(socket, {
1360
+ get(target, prop) {
1361
+ if (prop === "on") {
1362
+ return (event, handler) => {
1363
+ const internal = ["connect", "disconnect", "connect_error", "reconnect", "reconnecting", "error"];
1364
+ if (internal.includes(event)) {
1365
+ return target.on(event, handler);
1366
+ }
1367
+ secureOn(target, event, handler);
1368
+ return socket;
1369
+ };
1370
+ }
1371
+ const val = target[prop];
1372
+ return typeof val === "function" ? val.bind(target) : val;
1373
+ }
1374
+ });
1375
+ }
1376
+ function disconnectSocket() {
1377
+ _connectingPromise = null;
1378
+ if (_socket) {
1379
+ _socket.disconnect();
1380
+ _socket = null;
1381
+ _socketProxy = null;
1382
+ setStatus("disconnected");
1383
+ }
1384
+ clearTransitSession();
1385
+ resetAlgoCache();
1386
+ _getToken = null;
1387
+ _userId = void 0;
1388
+ _tenantId = void 0;
1389
+ _config2 = null;
1390
+ }
1391
+ function reconnectSocket(token, userId, tenantId) {
1392
+ if (!_socket) return;
1393
+ if (_config2?.transitEncryption) {
1394
+ const existing = getTransitSession();
1395
+ if (existing?.enabled && existing.sessionId) {
1396
+ _socket.auth = {
1397
+ token: `Bearer ${token}`,
1398
+ ...userId && { userId },
1399
+ ...tenantId && { tenantId },
1400
+ transitSessionId: existing.sessionId
1401
+ };
1402
+ if (_socket.connected) _socket.disconnect();
1403
+ _socket.connect();
1404
+ return;
1405
+ }
1406
+ if (_getToken) {
1407
+ if (!_socket.disconnected) _socket.disconnect();
1408
+ void connectSocket(_config2, _getToken);
1409
+ return;
1410
+ }
1411
+ }
1412
+ _socket.auth = {
1413
+ token: `Bearer ${token}`,
1414
+ ...userId && { userId },
1415
+ ...tenantId && { tenantId }
1416
+ };
1417
+ _socket.connect();
1418
+ }
1419
+ function refreshSocketAuth() {
1420
+ if (!_socket || !_getToken) return false;
1421
+ const fresh = _getToken();
1422
+ if (!fresh) return false;
1423
+ _socket.auth = {
1424
+ token: `Bearer ${fresh}`,
1425
+ ..._userId && { userId: _userId },
1426
+ ..._tenantId && { tenantId: _tenantId }
1427
+ };
1428
+ return true;
1429
+ }
1430
+
1431
+ // src/socket/emitters.ts
1432
+ var ACK_TIMEOUT = 5e3;
1433
+ var RECONNECT_WAIT_TIMEOUT = 15e3;
1434
+ var QUEUE_MAX_SIZE = 100;
1435
+ var QUEUE_ENTRY_TTL = 3e4;
1436
+ var sendQueues = /* @__PURE__ */ new Map();
1437
+ var sendQueueRunning = /* @__PURE__ */ new Map();
1438
+ async function drainSendQueue(conversationId) {
1439
+ if (sendQueueRunning.get(conversationId)) return;
1440
+ sendQueueRunning.set(conversationId, true);
1441
+ const queue = sendQueues.get(conversationId);
1442
+ while (queue.length > 0) {
1443
+ const entry = queue.shift();
1444
+ if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
1445
+ entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
1446
+ continue;
1447
+ }
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);
1457
+ }
1458
+ sendQueues.delete(conversationId);
1459
+ sendQueueRunning.delete(conversationId);
1460
+ }
1461
+ function queueSendMessage(payload) {
1462
+ const conversationId = payload.conversationId;
1463
+ if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);
1464
+ const queue = sendQueues.get(conversationId);
1465
+ if (queue.length >= QUEUE_MAX_SIZE) {
1466
+ return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1467
+ }
1468
+ return new Promise((resolve, reject) => {
1469
+ queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });
1470
+ drainSendQueue(conversationId);
1471
+ });
1768
1472
  }
1769
- function createSecureSocketProxy(socket) {
1770
- return new Proxy(socket, {
1771
- get(target, prop) {
1772
- if (prop === "on") {
1773
- return (event, handler) => {
1774
- const internal = ["connect", "disconnect", "connect_error", "reconnect", "reconnecting", "error"];
1775
- if (internal.includes(event)) {
1776
- return target.on(event, handler);
1777
- }
1778
- secureOn(target, event, handler);
1779
- return socket;
1780
- };
1473
+ function waitForReconnect() {
1474
+ return new Promise((resolve, reject) => {
1475
+ const timer = setTimeout(() => {
1476
+ unsubscribe();
1477
+ reject(new AntzChatNetworkError("Socket reconnect timeout", "SOCKET_TIMEOUT"));
1478
+ }, RECONNECT_WAIT_TIMEOUT);
1479
+ const unsubscribe = onSocketStatus((status) => {
1480
+ if (status === "connected") {
1481
+ clearTimeout(timer);
1482
+ unsubscribe();
1483
+ resolve();
1484
+ } else if (status === "error") {
1485
+ clearTimeout(timer);
1486
+ unsubscribe();
1487
+ reject(new AntzChatNetworkError("Socket reconnect failed", "SOCKET_NOT_CONNECTED"));
1781
1488
  }
1782
- const val = target[prop];
1783
- return typeof val === "function" ? val.bind(target) : val;
1784
- }
1489
+ });
1785
1490
  });
1786
1491
  }
1787
- function disconnectSocket() {
1788
- _connectingPromise = null;
1789
- if (_socket) {
1790
- _socket.disconnect();
1791
- _socket = null;
1792
- _socketProxy = null;
1793
- setStatus("disconnected");
1492
+ async function emitWithAck(event, payload) {
1493
+ let socket = tryGetSocket();
1494
+ if (!socket) {
1495
+ await waitForReconnect();
1496
+ socket = tryGetSocket();
1794
1497
  }
1795
- clearTransitSession();
1796
- resetAlgoCache();
1797
- _getToken = null;
1798
- _userId = void 0;
1799
- _tenantId = void 0;
1800
- _config2 = null;
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;
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 };
1801
1512
  }
1802
- function reconnectSocket(token, userId, tenantId) {
1803
- if (!_socket) return;
1804
- if (_config2?.transitEncryption) {
1805
- const existing = getTransitSession();
1806
- if (existing?.enabled && existing.sessionId) {
1807
- _socket.auth = {
1808
- token: `Bearer ${token}`,
1809
- ...userId && { userId },
1810
- ...tenantId && { tenantId },
1811
- transitSessionId: existing.sessionId
1812
- };
1813
- _preservingSession = true;
1814
- try {
1815
- if (_socket.connected) _socket.disconnect();
1816
- _socket.connect();
1817
- } finally {
1818
- _preservingSession = false;
1819
- }
1820
- return;
1513
+ async function withAck(event, payload) {
1514
+ const { acked } = await emitWithAck(event, payload);
1515
+ return acked;
1516
+ }
1517
+ function fireAndForget(event, payload) {
1518
+ const socket = tryGetSocket();
1519
+ if (!socket) return;
1520
+ secureEmit(socket, event, payload).catch(() => {
1521
+ });
1522
+ }
1523
+ var socketEmit = {
1524
+ joinRoom(conversationId) {
1525
+ trackRoomJoin(conversationId);
1526
+ fireAndForget("join_room", { conversationId });
1527
+ },
1528
+ leaveRoom(conversationId) {
1529
+ trackRoomLeave(conversationId);
1530
+ fireAndForget("leave_room", { conversationId });
1531
+ },
1532
+ sendMessage(payload) {
1533
+ return queueSendMessage({ ...payload, sentAt: Date.now() });
1534
+ },
1535
+ // Not queued like sendMessage — forward targets are independent conversations,
1536
+ // not the single conversation ordering that queueSendMessage protects, and each
1537
+ // target's own new_message/push already fires server-side via MessagesService.forward()'s
1538
+ // internal create() calls, so there's nothing here that needs in-order draining.
1539
+ forwardMessage(payload) {
1540
+ return withAck("forward_message", payload);
1541
+ },
1542
+ updateMessage(messageId, text) {
1543
+ return withAck("update_message", { messageId, text });
1544
+ },
1545
+ deleteMessage(messageId) {
1546
+ return withAck("delete_message", { messageId });
1547
+ },
1548
+ deleteMessageForMe(messageId) {
1549
+ return withAck("delete_message_for_me", { messageId });
1550
+ },
1551
+ clearChat(conversationId) {
1552
+ return withAck("clear_chat_for_me", { conversationId });
1553
+ },
1554
+ addReaction(messageId, emoji) {
1555
+ return withAck("add_reaction", { messageId, emoji });
1556
+ },
1557
+ removeReaction(messageId, emoji) {
1558
+ return withAck("remove_reaction", { messageId, emoji });
1559
+ },
1560
+ pinMessage(messageId) {
1561
+ return withAck("pin_message", { messageId });
1562
+ },
1563
+ unpinMessage(messageId) {
1564
+ return withAck("unpin_message", { messageId });
1565
+ },
1566
+ // markRead and typing are best-effort — silently dropped if socket not ready
1567
+ typing(conversationId, isTyping) {
1568
+ fireAndForget("typing", { conversationId, isTyping });
1569
+ },
1570
+ markRead(conversationId, messageId) {
1571
+ fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
1572
+ },
1573
+ getOnlineUsers(userIds) {
1574
+ const socket = tryGetSocket();
1575
+ if (!socket) return Promise.resolve([]);
1576
+ return new Promise((resolve, reject) => {
1577
+ let timer;
1578
+ secureEmit(socket, "get_online_users", { userIds }, (response) => {
1579
+ clearTimeout(timer);
1580
+ if (response && typeof response === "object" && "onlineStatus" in response) {
1581
+ const status = response.onlineStatus;
1582
+ resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));
1583
+ } else if (Array.isArray(response)) {
1584
+ resolve(response);
1585
+ } else {
1586
+ resolve([]);
1587
+ }
1588
+ }).then(() => {
1589
+ timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
1590
+ }).catch(reject);
1591
+ });
1592
+ },
1593
+ getTypingUsers(conversationId) {
1594
+ return withAck("get_typing_users", { conversationId });
1595
+ }
1596
+ };
1597
+
1598
+ // src/api/messages.ts
1599
+ var MAX_FORWARD_TARGETS = 5;
1600
+ var HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;
1601
+ var messagesApi = {
1602
+ async list(conversationId, params = {}) {
1603
+ const { cursor, direction, ...rest } = params;
1604
+ const serverParams = { ...rest };
1605
+ if (cursor) {
1606
+ serverParams[direction === "after" ? "after" : "before"] = cursor;
1821
1607
  }
1822
- if (_getToken) {
1823
- if (!_socket.disconnected) _socket.disconnect();
1824
- void connectSocket(_config2, _getToken);
1825
- return;
1608
+ const { data } = await getApiClient().get(
1609
+ `/conversations/${conversationId}/messages`,
1610
+ { params: serverParams }
1611
+ );
1612
+ const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
1613
+ if (!currentUserId) return data;
1614
+ return {
1615
+ ...data,
1616
+ data: data.data.map(
1617
+ (m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
1618
+ )
1619
+ };
1620
+ },
1621
+ async get(messageId) {
1622
+ const { data } = await getApiClient().get(`/messages/${messageId}`);
1623
+ return data;
1624
+ },
1625
+ /**
1626
+ * Sends a message via REST. For real-time delivery use `socketEmit.sendMessage`
1627
+ * instead — this is a lower-level entry point (used e.g. by the socket path's
1628
+ * REST-mirror flows). Pass `payload.tempId` and reuse the SAME value on retry to
1629
+ * make a retry-after-timeout safe — see `SendData.tempId`.
1630
+ */
1631
+ async send(conversationId, payload) {
1632
+ const { data } = await getApiClient().post(
1633
+ `/conversations/${conversationId}/messages`,
1634
+ payload
1635
+ );
1636
+ return data;
1637
+ },
1638
+ async update(messageId, text) {
1639
+ const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
1640
+ return data;
1641
+ },
1642
+ async delete(messageId) {
1643
+ await getApiClient().post(`/messages/${messageId}/delete`);
1644
+ },
1645
+ async deleteForMe(messageId) {
1646
+ await getApiClient().post(`/messages/${messageId}/delete-for-me`);
1647
+ },
1648
+ async addReaction(messageId, emoji) {
1649
+ const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
1650
+ return data;
1651
+ },
1652
+ async removeReaction(messageId, emoji) {
1653
+ const { data } = await getApiClient().post(
1654
+ `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
1655
+ );
1656
+ return data;
1657
+ },
1658
+ async getReactions(messageId) {
1659
+ const { data } = await getApiClient().post(`/messages/${messageId}/reactions/list`);
1660
+ return data;
1661
+ },
1662
+ async star(messageId) {
1663
+ await getApiClient().post(`/messages/${messageId}/star`);
1664
+ },
1665
+ async unstar(messageId) {
1666
+ await getApiClient().post(`/messages/${messageId}/unstar`);
1667
+ },
1668
+ async getStarred(params = {}) {
1669
+ const { data } = await getApiClient().get("/messages/starred", { params });
1670
+ return data;
1671
+ },
1672
+ async search(params) {
1673
+ const { data } = await getApiClient().get("/messages/search", { params });
1674
+ return data;
1675
+ },
1676
+ async getLastRead(conversationId) {
1677
+ const { data } = await getApiClient().get(
1678
+ `/conversations/${conversationId}/read-receipt`
1679
+ );
1680
+ return data;
1681
+ },
1682
+ async markAsRead(conversationId, messageId) {
1683
+ await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
1684
+ },
1685
+ async pin(messageId) {
1686
+ const { data } = await getApiClient().post(`/messages/${messageId}/pin`);
1687
+ return data;
1688
+ },
1689
+ async unpin(messageId) {
1690
+ const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
1691
+ return data;
1692
+ },
1693
+ async getPinned(conversationId) {
1694
+ const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
1695
+ return data;
1696
+ },
1697
+ async getReceipts(messageId) {
1698
+ const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
1699
+ return data;
1700
+ },
1701
+ /**
1702
+ * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
1703
+ * per call, also enforced server-side). Each target is independent — one failing
1704
+ * (e.g. no longer a participant) does not block the others; check `success`/`error`
1705
+ * per entry in the returned array.
1706
+ *
1707
+ * Pass `attachmentIds` to forward only a subset of the source message's attachments
1708
+ * (e.g. one image out of a multi-image message) — omit it to forward the whole
1709
+ * message, including all its attachments, unchanged. An ID not present on the
1710
+ * source message is ignored server-side.
1711
+ *
1712
+ * Idempotency: pass `tempId` — generate ONE value per forward action (e.g. via
1713
+ * `generateUUID` from '@antzsoft/chat-core/internal') and reuse that SAME value if
1714
+ * you retry this exact forward (e.g. after a client-side timeout). The server then
1715
+ * recognizes the retry per target and returns the already-created message instead
1716
+ * of creating a duplicate. Never mint a new tempId for a retry — only when the user
1717
+ * initiates a genuinely new forward action. If omitted, a fresh one is generated
1718
+ * per call, which means a retry without an explicit tempId gets NO dedup protection.
1719
+ *
1720
+ * Transport: uses the 'forward_message' socket event when a socket is connected
1721
+ * (lower latency, same server-side MessagesService.forward() path, so broadcast/
1722
+ * push notification behavior is identical either way) and transparently falls back
1723
+ * to the REST endpoint when no socket is available.
1724
+ */
1725
+ async forward(messageId, targetConversationIds, attachmentIds, tempId) {
1726
+ const resolvedTempId = tempId ?? generateUUID();
1727
+ if (tryGetSocket()) {
1728
+ const ack = await socketEmit.forwardMessage({
1729
+ messageId,
1730
+ targetConversationIds,
1731
+ ...attachmentIds ? { attachmentIds } : {},
1732
+ tempId: resolvedTempId
1733
+ });
1734
+ if (ack.error) throw new Error(ack.error);
1735
+ return ack.results;
1826
1736
  }
1737
+ const { data } = await getApiClient().post(
1738
+ `/messages/${messageId}/forward`,
1739
+ { targetConversationIds, ...attachmentIds ? { attachmentIds } : {}, tempId: resolvedTempId }
1740
+ );
1741
+ return data;
1827
1742
  }
1828
- _socket.auth = {
1829
- token: `Bearer ${token}`,
1830
- ...userId && { userId },
1831
- ...tenantId && { tenantId }
1743
+ };
1744
+
1745
+ // src/api/conversations.ts
1746
+ function normalizeParticipant(p) {
1747
+ const hasUserDetails = p.displayName || p.username || p.avatarUrl;
1748
+ return {
1749
+ userId: p.userId,
1750
+ externalId: p.externalId ?? p.user?.externalId,
1751
+ role: p.role,
1752
+ joinedAt: p.joinedAt,
1753
+ isActive: p.isActive,
1754
+ user: hasUserDetails ? {
1755
+ id: p.userId,
1756
+ externalId: p.externalId,
1757
+ tenantId: "",
1758
+ email: "",
1759
+ username: p.username ?? "",
1760
+ displayName: p.displayName ?? p.username ?? "",
1761
+ avatarUrl: p.avatarUrl,
1762
+ status: "offline",
1763
+ createdAt: p.joinedAt ?? "",
1764
+ updatedAt: p.joinedAt ?? ""
1765
+ } : p.user
1832
1766
  };
1833
- _socket.connect();
1834
1767
  }
1835
- function refreshSocketAuth() {
1836
- if (!_socket || !_getToken) return false;
1837
- const fresh = _getToken();
1838
- if (!fresh) return false;
1839
- _socket.auth = {
1840
- token: `Bearer ${fresh}`,
1841
- ..._userId && { userId: _userId },
1842
- ..._tenantId && { tenantId: _tenantId }
1768
+ function normalizeLastMessage(lastMsg) {
1769
+ if (!lastMsg) return void 0;
1770
+ if (lastMsg.content !== void 0) return lastMsg;
1771
+ return {
1772
+ id: lastMsg.messageId ?? "",
1773
+ tenantId: "",
1774
+ conversationId: "",
1775
+ senderId: lastMsg.senderId ?? "",
1776
+ content: {
1777
+ type: lastMsg.hasAttachments ? "attachment" : "text",
1778
+ text: lastMsg.contentPreview
1779
+ },
1780
+ reactions: [],
1781
+ lastReaction: lastMsg.lastReaction ?? null,
1782
+ status: lastMsg.status ?? "active",
1783
+ deliveryStatus: lastMsg.deliveryStatus ?? "sent",
1784
+ isEdited: false,
1785
+ sentAt: lastMsg.sentAt ?? "",
1786
+ createdAt: lastMsg.sentAt ?? "",
1787
+ ...lastMsg.senderName && { senderName: lastMsg.senderName },
1788
+ ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
1843
1789
  };
1844
- return true;
1845
- }
1846
-
1847
- // src/socket/emitters.ts
1848
- var ACK_TIMEOUT = 5e3;
1849
- var RECONNECT_WAIT_TIMEOUT = 15e3;
1850
- var QUEUE_MAX_SIZE = 100;
1851
- var QUEUE_ENTRY_TTL = 3e4;
1852
- var sendQueues = /* @__PURE__ */ new Map();
1853
- var sendQueueRunning = /* @__PURE__ */ new Map();
1854
- async function drainSendQueue(conversationId) {
1855
- if (sendQueueRunning.get(conversationId)) return;
1856
- sendQueueRunning.set(conversationId, true);
1857
- const queue = sendQueues.get(conversationId);
1858
- while (queue.length > 0) {
1859
- const entry = queue.shift();
1860
- if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
1861
- entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
1862
- continue;
1863
- }
1864
- entry.run().then(entry.resolve).catch(entry.reject);
1865
- }
1866
- sendQueues.delete(conversationId);
1867
- sendQueueRunning.delete(conversationId);
1868
- }
1869
- function queueSendMessage(payload) {
1870
- const conversationId = payload.conversationId;
1871
- if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);
1872
- const queue = sendQueues.get(conversationId);
1873
- if (queue.length >= QUEUE_MAX_SIZE) {
1874
- return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1875
- }
1876
- return new Promise((resolve, reject) => {
1877
- queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1878
- drainSendQueue(conversationId);
1879
- });
1880
1790
  }
1881
- function waitForReconnect() {
1882
- return new Promise((resolve, reject) => {
1883
- const timer = setTimeout(() => {
1884
- unsubscribe();
1885
- reject(new AntzChatNetworkError("Socket reconnect timeout", "SOCKET_TIMEOUT"));
1886
- }, RECONNECT_WAIT_TIMEOUT);
1887
- const unsubscribe = onSocketStatus((status) => {
1888
- if (status === "connected") {
1889
- clearTimeout(timer);
1890
- unsubscribe();
1891
- resolve();
1892
- } else if (status === "error") {
1893
- clearTimeout(timer);
1894
- unsubscribe();
1895
- reject(new AntzChatNetworkError("Socket reconnect failed", "SOCKET_NOT_CONNECTED"));
1896
- }
1897
- });
1898
- });
1791
+ function normalizeConversation(conv) {
1792
+ return {
1793
+ ...conv,
1794
+ id: conv.id ?? conv.conversationId,
1795
+ participants: (conv.participants ?? []).map(normalizeParticipant),
1796
+ lastMessage: normalizeLastMessage(conv.lastMessage)
1797
+ };
1899
1798
  }
1900
- async function withAck(event, payload) {
1901
- let socket = tryGetSocket();
1902
- if (!socket) {
1903
- await waitForReconnect();
1904
- socket = tryGetSocket();
1799
+ var conversationsApi = {
1800
+ async list(params = {}) {
1801
+ const { data } = await getApiClient().get("/conversations", { params });
1802
+ return { ...data, data: data.data.map(normalizeConversation) };
1803
+ },
1804
+ async get(conversationId) {
1805
+ const { data } = await getApiClient().get(`/conversations/${conversationId}`);
1806
+ return normalizeConversation(data);
1807
+ },
1808
+ async createGroup(payload) {
1809
+ const { data } = await getApiClient().post("/conversations", payload);
1810
+ return normalizeConversation(data);
1811
+ },
1812
+ async createDirect(payload) {
1813
+ const { data } = await getApiClient().post("/conversations/direct", payload);
1814
+ return normalizeConversation(data);
1815
+ },
1816
+ async update(conversationId, payload) {
1817
+ const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
1818
+ return normalizeConversation(data);
1819
+ },
1820
+ async delete(conversationId) {
1821
+ await getApiClient().post(`/conversations/${conversationId}/delete`);
1822
+ },
1823
+ async addParticipants(conversationId, userIds, role) {
1824
+ const { data } = await getApiClient().post(
1825
+ `/conversations/${conversationId}/participants`,
1826
+ { userIds, ...role && { role } }
1827
+ );
1828
+ return normalizeConversation(data);
1829
+ },
1830
+ async removeParticipant(conversationId, userId) {
1831
+ const { data } = await getApiClient().post(
1832
+ `/conversations/${conversationId}/participants/${userId}/remove`
1833
+ );
1834
+ return normalizeConversation(data);
1835
+ },
1836
+ async updateParticipantRole(conversationId, userId, role) {
1837
+ const { data } = await getApiClient().post(
1838
+ `/conversations/${conversationId}/participants/${userId}/role`,
1839
+ { role }
1840
+ );
1841
+ return normalizeConversation(data);
1842
+ },
1843
+ async mute(conversationId, mutedUntil) {
1844
+ await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
1845
+ },
1846
+ async unmute(conversationId) {
1847
+ await getApiClient().post(`/conversations/${conversationId}/unmute`);
1848
+ },
1849
+ async pin(conversationId) {
1850
+ await getApiClient().post(`/conversations/${conversationId}/pin`);
1851
+ },
1852
+ async unpin(conversationId) {
1853
+ await getApiClient().post(`/conversations/${conversationId}/unpin`);
1854
+ },
1855
+ async markUnread(conversationId) {
1856
+ await getApiClient().post(`/conversations/${conversationId}/unread`);
1857
+ },
1858
+ async markRead(conversationId) {
1859
+ await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
1860
+ },
1861
+ async leave(conversationId, andDelete) {
1862
+ const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
1863
+ await getApiClient().post(url);
1864
+ },
1865
+ async getMembers(conversationId, filter) {
1866
+ const { data } = await getApiClient().get(
1867
+ `/conversations/${conversationId}/participants`,
1868
+ filter ? { params: { filter } } : void 0
1869
+ );
1870
+ return (data ?? []).map(normalizeParticipant);
1871
+ },
1872
+ /**
1873
+ * Get unread message count for a single conversation.
1874
+ * Use this after app foreground or socket reconnect to refresh a specific count.
1875
+ */
1876
+ async getUnreadCount(conversationId) {
1877
+ const { data } = await getApiClient().get(
1878
+ `/conversations/${conversationId}/unread`
1879
+ );
1880
+ return data;
1881
+ },
1882
+ /**
1883
+ * Get total unread count across all conversations + per-conversation breakdown.
1884
+ * Use on app cold start, foreground resume, or after socket reconnect.
1885
+ * The socket keeps counts live while connected — this is the source of truth
1886
+ * when the socket was down.
1887
+ */
1888
+ async getUnreadSummary() {
1889
+ const { data } = await getApiClient().get("/conversations/unread");
1890
+ return data;
1891
+ },
1892
+ /**
1893
+ * Set the group icon from an already-uploaded file (admin only).
1894
+ * The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.
1895
+ * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
1896
+ */
1897
+ async uploadIcon(conversationId, fileId) {
1898
+ const { data } = await getApiClient().post(
1899
+ `/conversations/${conversationId}/icon`,
1900
+ { fileId }
1901
+ );
1902
+ return normalizeConversation(data);
1903
+ },
1904
+ async removeIcon(conversationId) {
1905
+ const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
1906
+ return normalizeConversation(data);
1907
+ },
1908
+ async clearChat(conversationId) {
1909
+ await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
1905
1910
  }
1906
- if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
1907
- return new Promise((resolve, reject) => {
1908
- let timer;
1909
- secureEmit(socket, event, payload, (response) => {
1910
- clearTimeout(timer);
1911
- resolve(response);
1912
- }).then(() => {
1913
- timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
1914
- }).catch(reject);
1915
- });
1916
- }
1917
- function fireAndForget(event, payload) {
1918
- const socket = tryGetSocket();
1919
- if (!socket) return;
1920
- secureEmit(socket, event, payload);
1921
- }
1922
- var socketEmit = {
1923
- joinRoom(conversationId) {
1924
- fireAndForget("join_room", { conversationId });
1911
+ };
1912
+
1913
+ // src/api/storage.ts
1914
+ var storageApi = {
1915
+ async requestPresignedUrl(payload) {
1916
+ const { data } = await getApiClient().post("/storage/presigned-url", payload);
1917
+ return data;
1925
1918
  },
1926
- leaveRoom(conversationId) {
1927
- fireAndForget("leave_room", { conversationId });
1919
+ async requestPresignedUrlBatch(files) {
1920
+ const { data } = await getApiClient().post("/storage/presigned-url/batch", { files });
1921
+ return data;
1928
1922
  },
1929
- sendMessage(payload) {
1930
- return queueSendMessage({ ...payload, sentAt: Date.now() });
1923
+ async confirmUpload(fileId) {
1924
+ const { data } = await getApiClient().post(`/storage/confirm/${fileId}`);
1925
+ return data;
1931
1926
  },
1932
- updateMessage(messageId, text) {
1933
- return withAck("update_message", { messageId, text });
1927
+ async getFile(fileId) {
1928
+ const { data } = await getApiClient().get(`/storage/files/${fileId}`);
1929
+ return data;
1934
1930
  },
1935
- deleteMessage(messageId) {
1936
- return withAck("delete_message", { messageId });
1931
+ async getFileUrl(fileId, expiresIn) {
1932
+ const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {
1933
+ params: expiresIn ? { expiresIn } : {}
1934
+ });
1935
+ return data;
1937
1936
  },
1938
- deleteMessageForMe(messageId) {
1939
- return withAck("delete_message_for_me", { messageId });
1937
+ async deleteFile(fileId) {
1938
+ await getApiClient().post(`/storage/files/${fileId}/delete`);
1940
1939
  },
1941
- clearChat(conversationId) {
1942
- return withAck("clear_chat_for_me", { conversationId });
1940
+ async completeMultipartUpload(fileId, uploadId, parts) {
1941
+ const { data } = await getApiClient().post(
1942
+ `/storage/multipart/complete/${fileId}`,
1943
+ { uploadId, parts }
1944
+ );
1945
+ return data;
1943
1946
  },
1944
- addReaction(messageId, emoji) {
1945
- return withAck("add_reaction", { messageId, emoji });
1947
+ async getConversationFiles(conversationId, params = {}) {
1948
+ const { data } = await getApiClient().get(
1949
+ `/storage/conversations/${conversationId}/files`,
1950
+ { params }
1951
+ );
1952
+ return data;
1946
1953
  },
1947
- removeReaction(messageId, emoji) {
1948
- return withAck("remove_reaction", { messageId, emoji });
1954
+ async getMyFiles(params = {}) {
1955
+ const { data } = await getApiClient().get("/storage/my-files", { params });
1956
+ return data;
1957
+ }
1958
+ };
1959
+ async function runMultipartUpload(presigned, file, platformUploadPartFn, onProgress) {
1960
+ const { multipart } = presigned;
1961
+ if (!multipart) throw new Error("No multipart info on presigned response");
1962
+ const CONCURRENCY = 3;
1963
+ const completedParts = [];
1964
+ const partProgress = {};
1965
+ multipart.partUrls.forEach(({ partNumber }) => {
1966
+ partProgress[partNumber] = 0;
1967
+ });
1968
+ const reportProgress = () => {
1969
+ if (!onProgress) return;
1970
+ const vals = Object.values(partProgress);
1971
+ const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1972
+ onProgress(Math.round(avg * 0.95));
1973
+ };
1974
+ const uploadPart = async (partNumber, uploadUrl, method) => {
1975
+ const offset = (partNumber - 1) * multipart.chunkSize;
1976
+ const end = Math.min(offset + multipart.chunkSize, file.size);
1977
+ const blob = await fetch(file.uri).then((r) => r.blob());
1978
+ const slice = blob.slice(offset, end);
1979
+ const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {
1980
+ partProgress[partNumber] = pct;
1981
+ reportProgress();
1982
+ }, method);
1983
+ completedParts.push({ partNumber, etag });
1984
+ partProgress[partNumber] = 100;
1985
+ reportProgress();
1986
+ };
1987
+ for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {
1988
+ const batch = multipart.partUrls.slice(i, i + CONCURRENCY);
1989
+ const results = await Promise.allSettled(
1990
+ batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? "PUT"))
1991
+ );
1992
+ const failed = results.find((r) => r.status === "rejected");
1993
+ if (failed) throw failed.reason;
1994
+ }
1995
+ completedParts.sort((a, b) => a.partNumber - b.partNumber);
1996
+ const fileResponse = await storageApi.completeMultipartUpload(
1997
+ presigned.fileId,
1998
+ multipart.uploadId,
1999
+ completedParts
2000
+ );
2001
+ onProgress?.(100);
2002
+ return fileResponse;
2003
+ }
2004
+ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
2005
+ const compressedFiles = await Promise.all(
2006
+ files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
2007
+ );
2008
+ const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));
2009
+ const requests = slotted.map(({ file: f, clientIndex }) => ({
2010
+ filename: f.name,
2011
+ mimeType: f.type,
2012
+ size: f.size,
2013
+ conversationId,
2014
+ clientIndex,
2015
+ ...f.compressed && {
2016
+ metadata: {
2017
+ compressed: f.compressed,
2018
+ originalSize: f.originalSize,
2019
+ compressionAlgorithm: f.compressionAlgorithm
2020
+ }
2021
+ }
2022
+ }));
2023
+ const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
2024
+ const failedSlotIds = /* @__PURE__ */ new Set();
2025
+ const failed = requestErrors.map((e) => {
2026
+ const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);
2027
+ const slotId = slotted[idx]?.slotId;
2028
+ if (slotId) failedSlotIds.add(slotId);
2029
+ return { filename: e.filename, error: e.error };
2030
+ });
2031
+ const progressMap = {};
2032
+ const reportProgress = () => {
2033
+ if (!onProgress) return;
2034
+ const vals = Object.values(progressMap);
2035
+ const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
2036
+ onProgress(Math.round(avg));
2037
+ };
2038
+ const successful = [];
2039
+ const slotToFile = /* @__PURE__ */ new Map();
2040
+ await Promise.all(
2041
+ urls.map(async (presigned, idx) => {
2042
+ const originalIdx = presigned.clientIndex ?? idx;
2043
+ const { file, slotId } = slotted[originalIdx];
2044
+ progressMap[originalIdx] = 0;
2045
+ try {
2046
+ let fileResponse;
2047
+ if (presigned.multipart && platformUploadPartFn) {
2048
+ fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {
2049
+ progressMap[originalIdx] = pct;
2050
+ reportProgress();
2051
+ });
2052
+ } else {
2053
+ await platformUploadFn(presigned, file, (pct) => {
2054
+ progressMap[originalIdx] = Math.round(pct * 0.9);
2055
+ reportProgress();
2056
+ });
2057
+ fileResponse = await storageApi.confirmUpload(presigned.fileId);
2058
+ }
2059
+ progressMap[originalIdx] = 100;
2060
+ reportProgress();
2061
+ successful.push(fileResponse);
2062
+ slotToFile.set(slotId, fileResponse);
2063
+ } catch (err) {
2064
+ failed.push({ filename: file.name, error: err.message });
2065
+ }
2066
+ })
2067
+ );
2068
+ return { result: { successful, failed }, slotToFile };
2069
+ }
2070
+ async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
2071
+ const slotIds = files.map(() => generateUUID());
2072
+ const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);
2073
+ return result;
2074
+ }
2075
+
2076
+ // src/api/devices.ts
2077
+ var devicesApi = {
2078
+ /**
2079
+ * Register or update a device push token with the chat server.
2080
+ *
2081
+ * Upserts by `deviceId` — calling this multiple times with the same deviceId
2082
+ * simply refreshes the token value (tokens can rotate silently on some platforms).
2083
+ *
2084
+ * The SDK never calls this automatically. The parent app or the `tokenProvider`
2085
+ * option in `pushNotifications` config is responsible for calling it after
2086
+ * obtaining the token from the OS / browser.
2087
+ */
2088
+ async register(payload) {
2089
+ await getApiClient().post("/users/me/devices", payload);
1949
2090
  },
1950
- pinMessage(messageId) {
1951
- return withAck("pin_message", { messageId });
2091
+ /**
2092
+ * Remove a device token from the chat server.
2093
+ * Call this on logout so the user stops receiving push notifications on this device.
2094
+ */
2095
+ async remove(deviceId) {
2096
+ await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
2097
+ }
2098
+ };
2099
+
2100
+ // src/api/users.ts
2101
+ var usersApi = {
2102
+ async list(params = {}) {
2103
+ const { data } = await getApiClient().get("/users", { params });
2104
+ return data;
1952
2105
  },
1953
- unpinMessage(messageId) {
1954
- return withAck("unpin_message", { messageId });
2106
+ async getById(userId) {
2107
+ const { data } = await getApiClient().get(`/users/${userId}`);
2108
+ return data;
1955
2109
  },
1956
- // markRead and typing are best-effort — silently dropped if socket not ready
1957
- typing(conversationId, isTyping) {
1958
- fireAndForget("typing", { conversationId, isTyping });
2110
+ async getLastSeen(userId) {
2111
+ const { data } = await getApiClient().get(`/users/${userId}`);
2112
+ return { lastSeenAt: data.lastSeenAt ?? null };
1959
2113
  },
1960
- markRead(conversationId, messageId) {
1961
- fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
2114
+ /**
2115
+ * Update basic profile fields for the current user.
2116
+ * Works in both builtin and non-builtin modes. Use this to push an immediate
2117
+ * profile update to the chat server when the host app knows a change just
2118
+ * happened — without waiting for the next 2-hour sync cycle.
2119
+ */
2120
+ async updateProfile(payload) {
2121
+ const { data } = await getApiClient().post("/users/me/update", payload);
2122
+ return data;
1962
2123
  },
1963
- getOnlineUsers(userIds) {
1964
- const socket = tryGetSocket();
1965
- if (!socket) return Promise.resolve([]);
1966
- return new Promise((resolve, reject) => {
1967
- let timer;
1968
- secureEmit(socket, "get_online_users", { userIds }, (response) => {
1969
- clearTimeout(timer);
1970
- if (response && typeof response === "object" && "onlineStatus" in response) {
1971
- const status = response.onlineStatus;
1972
- resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));
1973
- } else if (Array.isArray(response)) {
1974
- resolve(response);
1975
- } else {
1976
- resolve([]);
1977
- }
1978
- }).then(() => {
1979
- timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
1980
- }).catch(reject);
1981
- });
2124
+ /**
2125
+ * Update notification preferences for the current user.
2126
+ * Partial update — only send fields you want to change.
2127
+ * A prefs record is automatically created with defaults when a device
2128
+ * token is first registered, so this never fails with "not found".
2129
+ */
2130
+ async updatePreferences(prefs) {
2131
+ const { data } = await getApiClient().post("/users/me/preferences", prefs);
2132
+ return data;
1982
2133
  },
1983
- getTypingUsers(conversationId) {
1984
- return withAck("get_typing_users", { conversationId });
2134
+ /**
2135
+ * Fetch current notification preferences for the current user.
2136
+ * Returns null if no prefs record exists yet (all defaults apply).
2137
+ */
2138
+ async getPreferences() {
2139
+ try {
2140
+ const { data } = await getApiClient().get("/users/me/preferences");
2141
+ return data;
2142
+ } catch {
2143
+ return null;
2144
+ }
1985
2145
  }
1986
2146
  };
1987
2147
 
@@ -2146,6 +2306,7 @@ var AntzChatClient = class {
2146
2306
  getSocketStatus,
2147
2307
  initApiClient,
2148
2308
  initAuthStore,
2309
+ isApiClientConfigured,
2149
2310
  isMentionAll,
2150
2311
  isTransitEnvelope,
2151
2312
  messagesApi,
@@ -2158,9 +2319,11 @@ var AntzChatClient = class {
2158
2319
  refreshSocketAuth,
2159
2320
  renderMentionParts,
2160
2321
  resetAuthStore,
2322
+ resetTrackedRooms,
2161
2323
  resolveConfig,
2162
2324
  resolveSystemMessageText,
2163
2325
  setApiClientInstance,
2326
+ setAuthReadyPromise,
2164
2327
  setTransitSession,
2165
2328
  socketEmit,
2166
2329
  storageApi,