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