@antzsoft/chat-core 1.1.4 → 1.1.6

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
@@ -1,3 +1,19 @@
1
+ import {
2
+ clearTransitSession,
3
+ configureTransit,
4
+ decryptPayload,
5
+ encryptPayload,
6
+ getApiClient,
7
+ getCompressionStrategy,
8
+ getSessionKey,
9
+ initApiClient,
10
+ isTransitEnabled,
11
+ isTransitEnvelope,
12
+ setApiClientInstance,
13
+ setTransitSession,
14
+ storageApi,
15
+ uploadBatch
16
+ } from "./chunk-3ISWHKXI.js";
1
17
  import {
2
18
  useChatStore
3
19
  } from "./chunk-TB52RCSF.js";
@@ -61,299 +77,6 @@ function resolveConfig(config) {
61
77
  };
62
78
  }
63
79
 
64
- // src/compression/compress.ts
65
- var GZIP_MIME_TYPES = /* @__PURE__ */ new Set([
66
- "text/plain",
67
- "text/csv",
68
- "text/markdown",
69
- "text/x-markdown",
70
- "text/xml",
71
- "application/xml",
72
- "text/yaml",
73
- "text/x-yaml",
74
- "application/x-yaml",
75
- "application/rtf",
76
- "text/rtf",
77
- "application/json",
78
- "image/svg+xml"
79
- ]);
80
- var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
81
- "image/jpeg",
82
- "image/png",
83
- "image/gif",
84
- "image/webp",
85
- "image/bmp",
86
- "image/tiff"
87
- ]);
88
- var SKIP_MIME_TYPES = /* @__PURE__ */ new Set([
89
- "video/mp4",
90
- "video/webm",
91
- "video/quicktime",
92
- "audio/mpeg",
93
- "audio/wav",
94
- "audio/ogg",
95
- "audio/webm",
96
- "audio/mp4",
97
- "application/zip",
98
- "application/pdf",
99
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
100
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
101
- "application/vnd.openxmlformats-officedocument.presentationml.presentation"
102
- ]);
103
- function getCompressionStrategy(mimeType, config) {
104
- if (SKIP_MIME_TYPES.has(mimeType)) return "skip";
105
- if (IMAGE_MIME_TYPES.has(mimeType)) return "image";
106
- if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return "gzip";
107
- return "skip";
108
- }
109
- async function compressFile(file, platformCompressFn, config) {
110
- const noop = {
111
- ...file,
112
- originalSize: file.size,
113
- compressed: false,
114
- compressionAlgorithm: "none"
115
- };
116
- if (!config.enabled || !platformCompressFn) return noop;
117
- const strategy = getCompressionStrategy(file.type, config);
118
- if (strategy === "skip") return noop;
119
- try {
120
- return await platformCompressFn(file, config);
121
- } catch {
122
- return noop;
123
- }
124
- }
125
-
126
- // src/api/client.ts
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
238
- var _tokenStore = null;
239
- var _config = null;
240
- var _avatarSent = false;
241
- function initApiClient(config, tokenStore) {
242
- _config = config;
243
- _tokenStore = tokenStore;
244
- _avatarSent = false;
245
- const client = axios.create({
246
- baseURL: config.apiUrl,
247
- headers: { "Content-Type": "application/json" }
248
- });
249
- configureTransit(config.transitEncryption);
250
- client.interceptors.request.use(async (req) => {
251
- const token = _tokenStore?.getAccessToken();
252
- if (token) req.headers["Authorization"] = `Bearer ${token}`;
253
- if (_config?.userId) req.headers["x-user-id"] = _config.userId;
254
- if (_config?.tenantId) req.headers["X-Tenant-ID"] = _config.tenantId;
255
- if (token && !_avatarSent && _config?.avatar) {
256
- if (_config.avatar.base64) req.headers["x-avatar-base64"] = _config.avatar.base64;
257
- else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
258
- _avatarSent = true;
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
- }
273
- return req;
274
- });
275
- let isRefreshing = false;
276
- let refreshQueue = [];
277
- client.interceptors.response.use(
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
- }
289
- if (response.data && typeof response.data === "object" && "success" in response.data && "data" in response.data) {
290
- response.data = response.data.data;
291
- }
292
- return response;
293
- },
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
- }
308
- const original = error.config;
309
- if (error.response?.status === 401 && !original._retry) {
310
- const refreshToken = _tokenStore?.getRefreshToken();
311
- if (!refreshToken) {
312
- _tokenStore?.clearTokens();
313
- return Promise.reject(error);
314
- }
315
- if (isRefreshing) {
316
- return new Promise((resolve) => {
317
- refreshQueue.push((newToken) => {
318
- original.headers["Authorization"] = `Bearer ${newToken}`;
319
- resolve(client(original));
320
- });
321
- });
322
- }
323
- original._retry = true;
324
- isRefreshing = true;
325
- try {
326
- const { data } = await axios.post(
327
- `${_config.apiUrl}/auth/refresh`,
328
- { refreshToken }
329
- );
330
- const tokens = data.data ?? data;
331
- _tokenStore?.setTokens(tokens);
332
- refreshQueue.forEach((cb) => cb(tokens.accessToken));
333
- refreshQueue = [];
334
- original.headers["Authorization"] = `Bearer ${tokens.accessToken}`;
335
- return client(original);
336
- } catch {
337
- _tokenStore?.clearTokens();
338
- return Promise.reject(error);
339
- } finally {
340
- isRefreshing = false;
341
- }
342
- }
343
- return Promise.reject(error);
344
- }
345
- );
346
- return client;
347
- }
348
- var _instance = null;
349
- function setApiClientInstance(instance) {
350
- _instance = instance;
351
- }
352
- function getApiClient() {
353
- if (!_instance) throw new Error("[AntzChat] API client not initialized. Call initApiClient first.");
354
- return _instance;
355
- }
356
-
357
80
  // src/api/auth.ts
358
81
  var authApi = {
359
82
  async login(credentials) {
@@ -382,7 +105,7 @@ var authApi = {
382
105
  async uploadAvatar(file, mimeType) {
383
106
  const form = new FormData();
384
107
  form.append("avatar", file instanceof File ? file : new File([file], "avatar.jpg", { type: mimeType ?? "image/jpeg" }));
385
- const { data } = await getApiClient().put("/users/me/avatar", form, {
108
+ const { data } = await getApiClient().post("/users/me/avatar", form, {
386
109
  headers: { "Content-Type": "multipart/form-data" }
387
110
  });
388
111
  return data;
@@ -431,22 +154,22 @@ var messagesApi = {
431
154
  return data;
432
155
  },
433
156
  async update(messageId, text) {
434
- const { data } = await getApiClient().put(`/messages/${messageId}`, { text });
157
+ const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
435
158
  return data;
436
159
  },
437
160
  async delete(messageId) {
438
- await getApiClient().delete(`/messages/${messageId}`);
161
+ await getApiClient().post(`/messages/${messageId}/delete`);
439
162
  },
440
163
  async deleteForMe(messageId) {
441
- await getApiClient().delete(`/messages/${messageId}/for-me`);
164
+ await getApiClient().post(`/messages/${messageId}/delete-for-me`);
442
165
  },
443
166
  async addReaction(messageId, emoji) {
444
167
  const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
445
168
  return data;
446
169
  },
447
170
  async removeReaction(messageId, emoji) {
448
- const { data } = await getApiClient().delete(
449
- `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`
171
+ const { data } = await getApiClient().post(
172
+ `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
450
173
  );
451
174
  return data;
452
175
  },
@@ -454,7 +177,7 @@ var messagesApi = {
454
177
  await getApiClient().post(`/messages/${messageId}/star`);
455
178
  },
456
179
  async unstar(messageId) {
457
- await getApiClient().delete(`/messages/${messageId}/star`);
180
+ await getApiClient().post(`/messages/${messageId}/unstar`);
458
181
  },
459
182
  async getStarred(params = {}) {
460
183
  const { data } = await getApiClient().get("/messages/starred", { params });
@@ -478,7 +201,7 @@ var messagesApi = {
478
201
  return data;
479
202
  },
480
203
  async unpin(messageId) {
481
- const { data } = await getApiClient().delete(`/messages/${messageId}/pin`);
204
+ const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
482
205
  return data;
483
206
  },
484
207
  async getPinned(conversationId) {
@@ -521,10 +244,12 @@ function normalizeLastMessage(lastMsg) {
521
244
  text: lastMsg.contentPreview
522
245
  },
523
246
  reactions: [],
247
+ lastReaction: lastMsg.lastReaction ?? null,
524
248
  status: lastMsg.status ?? "sent",
525
249
  isEdited: false,
526
250
  sentAt: lastMsg.sentAt ?? "",
527
- createdAt: lastMsg.sentAt ?? ""
251
+ createdAt: lastMsg.sentAt ?? "",
252
+ ...lastMsg.senderName && { senderName: lastMsg.senderName }
528
253
  };
529
254
  }
530
255
  function normalizeConversation(conv) {
@@ -553,11 +278,11 @@ var conversationsApi = {
553
278
  return normalizeConversation(data);
554
279
  },
555
280
  async update(conversationId, payload) {
556
- const { data } = await getApiClient().put(`/conversations/${conversationId}`, payload);
281
+ const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
557
282
  return normalizeConversation(data);
558
283
  },
559
284
  async delete(conversationId) {
560
- await getApiClient().delete(`/conversations/${conversationId}`);
285
+ await getApiClient().post(`/conversations/${conversationId}/delete`);
561
286
  },
562
287
  async addParticipants(conversationId, userIds, role) {
563
288
  const { data } = await getApiClient().post(
@@ -567,13 +292,13 @@ var conversationsApi = {
567
292
  return normalizeConversation(data);
568
293
  },
569
294
  async removeParticipant(conversationId, userId) {
570
- const { data } = await getApiClient().delete(
571
- `/conversations/${conversationId}/participants/${userId}`
295
+ const { data } = await getApiClient().post(
296
+ `/conversations/${conversationId}/participants/${userId}/remove`
572
297
  );
573
298
  return normalizeConversation(data);
574
299
  },
575
300
  async updateParticipantRole(conversationId, userId, role) {
576
- const { data } = await getApiClient().put(
301
+ const { data } = await getApiClient().post(
577
302
  `/conversations/${conversationId}/participants/${userId}/role`,
578
303
  { role }
579
304
  );
@@ -583,17 +308,17 @@ var conversationsApi = {
583
308
  await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
584
309
  },
585
310
  async unmute(conversationId) {
586
- await getApiClient().delete(`/conversations/${conversationId}/mute`);
311
+ await getApiClient().post(`/conversations/${conversationId}/unmute`);
587
312
  },
588
313
  async pin(conversationId) {
589
314
  await getApiClient().post(`/conversations/${conversationId}/pin`);
590
315
  },
591
316
  async unpin(conversationId) {
592
- await getApiClient().delete(`/conversations/${conversationId}/pin`);
317
+ await getApiClient().post(`/conversations/${conversationId}/unpin`);
593
318
  },
594
319
  async leave(conversationId, andDelete) {
595
320
  const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
596
- await getApiClient().delete(url);
321
+ await getApiClient().post(url);
597
322
  },
598
323
  async getMembers(conversationId, filter) {
599
324
  const { data } = await getApiClient().get(
@@ -628,107 +353,18 @@ var conversationsApi = {
628
353
  * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
629
354
  */
630
355
  async uploadIcon(conversationId, fileId) {
631
- const { data } = await getApiClient().put(
356
+ const { data } = await getApiClient().post(
632
357
  `/conversations/${conversationId}/icon`,
633
358
  { fileId }
634
359
  );
635
360
  return normalizeConversation(data);
636
361
  },
637
362
  async removeIcon(conversationId) {
638
- const { data } = await getApiClient().delete(
639
- `/conversations/${conversationId}/icon`
640
- );
363
+ const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
641
364
  return normalizeConversation(data);
642
365
  }
643
366
  };
644
367
 
645
- // src/api/storage.ts
646
- var storageApi = {
647
- async requestPresignedUrl(payload) {
648
- const { data } = await getApiClient().post("/storage/presigned-url", payload);
649
- return data;
650
- },
651
- async requestPresignedUrlBatch(files) {
652
- const { data } = await getApiClient().post("/storage/presigned-url/batch", { files });
653
- return data;
654
- },
655
- async confirmUpload(fileId) {
656
- const { data } = await getApiClient().post(`/storage/confirm/${fileId}`);
657
- return data;
658
- },
659
- async getFile(fileId) {
660
- const { data } = await getApiClient().get(`/storage/files/${fileId}`);
661
- return data;
662
- },
663
- async getFileUrl(fileId, expiresIn) {
664
- const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {
665
- params: expiresIn ? { expiresIn } : {}
666
- });
667
- return data;
668
- },
669
- async deleteFile(fileId) {
670
- await getApiClient().delete(`/storage/files/${fileId}`);
671
- },
672
- async getConversationFiles(conversationId, params = {}) {
673
- const { data } = await getApiClient().get(
674
- `/storage/conversations/${conversationId}/files`,
675
- { params }
676
- );
677
- return data;
678
- },
679
- async getMyFiles(params = {}) {
680
- const { data } = await getApiClient().get("/storage/my-files", { params });
681
- return data;
682
- }
683
- };
684
- async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig) {
685
- const compressedFiles = await Promise.all(
686
- files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
687
- );
688
- const requests = compressedFiles.map((f) => ({
689
- filename: f.name,
690
- mimeType: f.type,
691
- size: f.size,
692
- conversationId,
693
- ...f.compressed && {
694
- metadata: {
695
- compressed: f.compressed,
696
- originalSize: f.originalSize,
697
- compressionAlgorithm: f.compressionAlgorithm
698
- }
699
- }
700
- }));
701
- const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
702
- const progressMap = {};
703
- const reportProgress = () => {
704
- if (!onProgress) return;
705
- const vals = Object.values(progressMap);
706
- const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
707
- onProgress(Math.round(avg));
708
- };
709
- const successful = [];
710
- const failed = [...requestErrors];
711
- await Promise.all(
712
- urls.map(async (presigned, idx) => {
713
- const file = compressedFiles[idx];
714
- progressMap[idx] = 0;
715
- try {
716
- await platformUploadFn(presigned, file, (pct) => {
717
- progressMap[idx] = Math.round(pct * 0.9);
718
- reportProgress();
719
- });
720
- const result = await storageApi.confirmUpload(presigned.fileId);
721
- progressMap[idx] = 100;
722
- reportProgress();
723
- successful.push(result);
724
- } catch (err) {
725
- failed.push({ filename: file.name, error: err.message });
726
- }
727
- })
728
- );
729
- return { successful, failed };
730
- }
731
-
732
368
  // src/api/devices.ts
733
369
  var devicesApi = {
734
370
  /**
@@ -749,7 +385,7 @@ var devicesApi = {
749
385
  * Call this on logout so the user stops receiving push notifications on this device.
750
386
  */
751
387
  async remove(deviceId) {
752
- await getApiClient().delete(`/users/me/devices/${deviceId}`);
388
+ await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
753
389
  }
754
390
  };
755
391
 
@@ -774,7 +410,7 @@ var usersApi = {
774
410
  * happened — without waiting for the next 2-hour sync cycle.
775
411
  */
776
412
  async updateProfile(payload) {
777
- const { data } = await getApiClient().put("/users/me", payload);
413
+ const { data } = await getApiClient().post("/users/me/update", payload);
778
414
  return data;
779
415
  },
780
416
  /**
@@ -784,7 +420,7 @@ var usersApi = {
784
420
  * token is first registered, so this never fails with "not found".
785
421
  */
786
422
  async updatePreferences(prefs) {
787
- const { data } = await getApiClient().put("/users/me/preferences", prefs);
423
+ const { data } = await getApiClient().post("/users/me/preferences", prefs);
788
424
  return data;
789
425
  },
790
426
  /**
@@ -838,14 +474,14 @@ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
838
474
  ["deriveBits"]
839
475
  );
840
476
  const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
841
- socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
477
+ socketHandshakeAuth["transitEphemeralPub"] = bufToB64(pubRaw);
842
478
  socketHandshakeAuth["transitAlgo"] = algo;
843
479
  const ephemeralPriv = ephemeral.privateKey;
844
480
  return (sessionId) => deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
845
481
  }
846
482
  async function deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
847
483
  const serverPubB64 = algo === "x25519" ? serverKeys.x25519 : serverKeys.p256;
848
- const serverPubRaw = b64ToBuf2(serverPubB64);
484
+ const serverPubRaw = b64ToBuf(serverPubB64);
849
485
  const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
850
486
  const serverPubKey = await globalThis.crypto.subtle.importKey(
851
487
  "raw",
@@ -876,7 +512,7 @@ async function deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
876
512
  ["encrypt", "decrypt"]
877
513
  );
878
514
  }
879
- function bufToB642(buf) {
515
+ function bufToB64(buf) {
880
516
  const bytes = new Uint8Array(buf);
881
517
  let str = "";
882
518
  bytes.forEach((b) => {
@@ -884,7 +520,7 @@ function bufToB642(buf) {
884
520
  });
885
521
  return btoa(str);
886
522
  }
887
- function b64ToBuf2(b64) {
523
+ function b64ToBuf(b64) {
888
524
  const bin = atob(b64);
889
525
  const buf = new Uint8Array(bin.length);
890
526
  for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
@@ -893,6 +529,7 @@ function b64ToBuf2(b64) {
893
529
 
894
530
  // src/socket/socket.ts
895
531
  var _socket = null;
532
+ var _connectingPromise = null;
896
533
  var _status = "disconnected";
897
534
  var _statusListeners = /* @__PURE__ */ new Set();
898
535
  var _getToken = null;
@@ -966,6 +603,13 @@ function secureOn(socket, event, handler) {
966
603
  }
967
604
  async function connectSocket(config, getToken) {
968
605
  if (_socket && !_socket.disconnected) return _socket;
606
+ if (_connectingPromise) return _connectingPromise;
607
+ _connectingPromise = _doConnect(config, getToken).finally(() => {
608
+ _connectingPromise = null;
609
+ });
610
+ return _connectingPromise;
611
+ }
612
+ async function _doConnect(config, getToken) {
969
613
  _getToken = getToken;
970
614
  _userId = config.userId;
971
615
  _tenantId = config.tenantId;
@@ -1092,6 +736,7 @@ function createSecureSocketProxy(socket) {
1092
736
  });
1093
737
  }
1094
738
  function disconnectSocket() {
739
+ _connectingPromise = null;
1095
740
  if (_socket) {
1096
741
  _socket.disconnect();
1097
742
  _socket = null;
@@ -1210,7 +855,7 @@ var socketEmit = {
1210
855
  fireAndForget("leave_room", { conversationId });
1211
856
  },
1212
857
  sendMessage(payload) {
1213
- return queueSendMessage(payload);
858
+ return queueSendMessage({ ...payload, sentAt: Date.now() });
1214
859
  },
1215
860
  updateMessage(messageId, text) {
1216
861
  return withAck("update_message", { messageId, text });