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