@antzsoft/chat-core 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ function resolveConfig(config) {
38
38
  userId: config.userId,
39
39
  tenantId: config.tenantId,
40
40
  avatar: config.avatar,
41
- encryptionMode: config.encryptionMode ?? "none",
41
+ transitEncryption: config.transitEncryption ?? true,
42
42
  upload: {
43
43
  maxFileSizeMB: limits,
44
44
  maxFilesPerMessage: config.upload?.maxFilesPerMessage ?? 10,
@@ -125,6 +125,100 @@ 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 _session = null;
181
+ var _readyResolve = null;
182
+ var _readyPromise = null;
183
+ var _transitConfigured = null;
184
+ function configureTransit(enabled) {
185
+ _transitConfigured = enabled;
186
+ if (!enabled) {
187
+ _readyResolve?.();
188
+ _readyResolve = null;
189
+ }
190
+ }
191
+ function waitForTransitReady() {
192
+ if (!_transitConfigured) return Promise.resolve();
193
+ if (_session) return Promise.resolve();
194
+ if (!_readyPromise) {
195
+ _readyPromise = new Promise((resolve) => {
196
+ _readyResolve = resolve;
197
+ });
198
+ }
199
+ return _readyPromise;
200
+ }
201
+ function setTransitSession(session) {
202
+ _session = session;
203
+ _readyResolve?.();
204
+ _readyResolve = null;
205
+ }
206
+ function clearTransitSession() {
207
+ _session = null;
208
+ _readyPromise = null;
209
+ _readyResolve = null;
210
+ }
211
+ function isTransitEnabled() {
212
+ return _session?.enabled === true;
213
+ }
214
+ function getSessionKey() {
215
+ return _session?.sessionKey ?? null;
216
+ }
217
+ function getSessionId() {
218
+ return _session?.sessionId ?? null;
219
+ }
220
+
221
+ // src/api/client.ts
128
222
  var _tokenStore = null;
129
223
  var _config = null;
130
224
  var _avatarSent = false;
@@ -136,7 +230,8 @@ function initApiClient(config, tokenStore) {
136
230
  baseURL: config.apiUrl,
137
231
  headers: { "Content-Type": "application/json" }
138
232
  });
139
- client.interceptors.request.use((req) => {
233
+ configureTransit(config.transitEncryption);
234
+ client.interceptors.request.use(async (req) => {
140
235
  const token = _tokenStore?.getAccessToken();
141
236
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
142
237
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
@@ -146,18 +241,54 @@ function initApiClient(config, tokenStore) {
146
241
  else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
147
242
  _avatarSent = true;
148
243
  }
244
+ await waitForTransitReady();
245
+ if (isTransitEnabled()) {
246
+ const sessionId = getSessionId();
247
+ const key = getSessionKey();
248
+ if (sessionId && key) {
249
+ req.headers["x-transit-session"] = sessionId;
250
+ if (req.data !== void 0 && req.data !== null) {
251
+ const envelope = await encryptPayload(req.data, key);
252
+ req.data = envelope;
253
+ req.headers["x-transit-encrypted"] = "1";
254
+ }
255
+ }
256
+ }
149
257
  return req;
150
258
  });
151
259
  let isRefreshing = false;
152
260
  let refreshQueue = [];
153
261
  client.interceptors.response.use(
154
- (response) => {
262
+ async (response) => {
263
+ if (isTransitEnabled()) {
264
+ const key = getSessionKey();
265
+ if (key) {
266
+ if (isTransitEnvelope(response.data)) {
267
+ response.data = await decryptPayload(response.data, key);
268
+ } else if (response.data?.data && isTransitEnvelope(response.data.data)) {
269
+ response.data.data = await decryptPayload(response.data.data, key);
270
+ }
271
+ }
272
+ }
155
273
  if (response.data && typeof response.data === "object" && "success" in response.data && "data" in response.data) {
156
274
  response.data = response.data.data;
157
275
  }
158
276
  return response;
159
277
  },
160
278
  async (error) => {
279
+ if (isTransitEnabled() && error.response?.data) {
280
+ const key = getSessionKey();
281
+ if (key) {
282
+ try {
283
+ if (isTransitEnvelope(error.response.data)) {
284
+ error.response.data = await decryptPayload(error.response.data, key);
285
+ } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {
286
+ error.response.data.data = await decryptPayload(error.response.data.data, key);
287
+ }
288
+ } catch {
289
+ }
290
+ }
291
+ }
161
292
  const original = error.config;
162
293
  if (error.response?.status === 401 && !original._retry) {
163
294
  const refreshToken = _tokenStore?.getRefreshToken();
@@ -412,10 +543,10 @@ var conversationsApi = {
412
543
  async delete(conversationId) {
413
544
  await getApiClient().delete(`/conversations/${conversationId}`);
414
545
  },
415
- async addParticipants(conversationId, userIds) {
546
+ async addParticipants(conversationId, userIds, role) {
416
547
  const { data } = await getApiClient().post(
417
548
  `/conversations/${conversationId}/participants`,
418
- { userIds }
549
+ { userIds, ...role && { role } }
419
550
  );
420
551
  return normalizeConversation(data);
421
552
  },
@@ -535,7 +666,14 @@ async function uploadBatch(files, platformUploadFn, conversationId, onProgress,
535
666
  filename: f.name,
536
667
  mimeType: f.type,
537
668
  size: f.size,
538
- conversationId
669
+ conversationId,
670
+ ...f.compressed && {
671
+ metadata: {
672
+ compressed: f.compressed,
673
+ originalSize: f.originalSize,
674
+ compressionAlgorithm: f.compressionAlgorithm
675
+ }
676
+ }
539
677
  }));
540
678
  const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
541
679
  const progressMap = {};
@@ -632,6 +770,95 @@ var usersApi = {
632
770
 
633
771
  // src/socket/socket.ts
634
772
  import { io } from "socket.io-client";
773
+
774
+ // src/crypto/detect.ts
775
+ var _cached = null;
776
+ async function detectTransitAlgo() {
777
+ if (_cached) return _cached;
778
+ try {
779
+ await globalThis.crypto.subtle.generateKey(
780
+ { name: "X25519" },
781
+ false,
782
+ ["deriveKey"]
783
+ );
784
+ _cached = "x25519";
785
+ } catch {
786
+ _cached = "p256";
787
+ }
788
+ return _cached;
789
+ }
790
+ function resetAlgoCache() {
791
+ _cached = null;
792
+ }
793
+
794
+ // src/crypto/handshake.ts
795
+ async function fetchServerKeys(apiUrl) {
796
+ const res = await fetch(`${apiUrl}/crypto/pubkey`);
797
+ if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
798
+ const body = await res.json();
799
+ return body?.data ?? body;
800
+ }
801
+ async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
802
+ const ephemeral = await globalThis.crypto.subtle.generateKey(
803
+ algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
804
+ false,
805
+ ["deriveBits"]
806
+ );
807
+ const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
808
+ socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
809
+ socketHandshakeAuth["transitAlgo"] = algo;
810
+ const ephemeralPriv = ephemeral.privateKey;
811
+ return (sessionId) => deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
812
+ }
813
+ async function deriveSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
814
+ const serverPubB64 = algo === "x25519" ? serverKeys.x25519 : serverKeys.p256;
815
+ const serverPubRaw = b64ToBuf2(serverPubB64);
816
+ const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
817
+ const serverPubKey = await globalThis.crypto.subtle.importKey(
818
+ "raw",
819
+ serverPubRaw,
820
+ keyAlgoParams,
821
+ false,
822
+ []
823
+ );
824
+ const sharedBits = await globalThis.crypto.subtle.deriveBits(
825
+ { name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
826
+ ephemeralPriv,
827
+ 256
828
+ );
829
+ const hkdfKey = await globalThis.crypto.subtle.importKey(
830
+ "raw",
831
+ sharedBits,
832
+ "HKDF",
833
+ false,
834
+ ["deriveKey"]
835
+ );
836
+ const salt = new TextEncoder().encode(sessionId);
837
+ const info = new TextEncoder().encode("antz-transit-v1");
838
+ return globalThis.crypto.subtle.deriveKey(
839
+ { name: "HKDF", hash: "SHA-256", salt, info },
840
+ hkdfKey,
841
+ { name: "AES-GCM", length: 256 },
842
+ false,
843
+ ["encrypt", "decrypt"]
844
+ );
845
+ }
846
+ function bufToB642(buf) {
847
+ const bytes = new Uint8Array(buf);
848
+ let str = "";
849
+ bytes.forEach((b) => {
850
+ str += String.fromCharCode(b);
851
+ });
852
+ return btoa(str);
853
+ }
854
+ function b64ToBuf2(b64) {
855
+ const bin = atob(b64);
856
+ const buf = new Uint8Array(bin.length);
857
+ for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
858
+ return buf.buffer;
859
+ }
860
+
861
+ // src/socket/socket.ts
635
862
  var _socket = null;
636
863
  var _status = "disconnected";
637
864
  var _statusListeners = /* @__PURE__ */ new Set();
@@ -656,23 +883,96 @@ function onSocketStatus(listener) {
656
883
  _statusListeners.add(listener);
657
884
  return () => _statusListeners.delete(listener);
658
885
  }
886
+ async function secureEmit(socket, event, payload, ack) {
887
+ if (isTransitEnabled()) {
888
+ const key = getSessionKey();
889
+ if (key) {
890
+ const envelope = await encryptPayload(payload, key);
891
+ if (ack) {
892
+ socket.emit(event, envelope, async (encryptedResponse) => {
893
+ if (isTransitEnvelope(encryptedResponse)) {
894
+ try {
895
+ const decrypted = await decryptPayload(encryptedResponse, key);
896
+ ack(decrypted);
897
+ } catch {
898
+ ack(encryptedResponse);
899
+ }
900
+ } else {
901
+ ack(encryptedResponse);
902
+ }
903
+ });
904
+ } else {
905
+ socket.emit(event, envelope);
906
+ }
907
+ return;
908
+ }
909
+ }
910
+ if (ack) {
911
+ socket.emit(event, payload, ack);
912
+ } else {
913
+ socket.emit(event, payload);
914
+ }
915
+ }
916
+ function secureOn(socket, event, handler) {
917
+ socket.on(event, async (raw) => {
918
+ if (isTransitEnabled() && isTransitEnvelope(raw)) {
919
+ const key = getSessionKey();
920
+ if (key) {
921
+ try {
922
+ const data = await decryptPayload(raw, key);
923
+ handler(data);
924
+ return;
925
+ } catch {
926
+ console.error(`[AntzChat] Transit decryption failed for event: ${event}`);
927
+ return;
928
+ }
929
+ }
930
+ }
931
+ handler(raw);
932
+ });
933
+ }
659
934
  async function connectSocket(config, getToken) {
660
935
  if (_socket && !_socket.disconnected) return _socket;
661
936
  _getToken = getToken;
662
937
  _userId = config.userId;
663
938
  _tenantId = config.tenantId;
664
939
  const token = getToken();
940
+ const socketHandshakeAuth = {
941
+ token: token ? `Bearer ${token}` : "",
942
+ ...config.userId && { userId: config.userId },
943
+ ...config.tenantId && { tenantId: config.tenantId },
944
+ ...config.avatar?.url && { avatarUrl: config.avatar.url },
945
+ ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
946
+ };
947
+ let boundDeriveSessionKey = null;
948
+ if (config.transitEncryption) {
949
+ try {
950
+ const serverKeys = await fetchServerKeys(config.apiUrl);
951
+ if (!serverKeys.enabled) {
952
+ throw new Error(
953
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
954
+ );
955
+ }
956
+ const algo = await detectTransitAlgo();
957
+ boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
958
+ } catch (err) {
959
+ if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
960
+ console.warn("[AntzChat] Transit handshake setup failed, connecting without transit encryption:", err.message);
961
+ }
962
+ } else {
963
+ try {
964
+ const serverKeys = await fetchServerKeys(config.apiUrl);
965
+ if (serverKeys.enabled) {
966
+ throw new Error(
967
+ "[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides."
968
+ );
969
+ }
970
+ } catch (err) {
971
+ if (err.message?.startsWith("[AntzChat] Transit encryption mismatch")) throw err;
972
+ }
973
+ }
665
974
  _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).
975
+ auth: socketHandshakeAuth,
676
976
  path: config.socketPath,
677
977
  transports: ["websocket", "polling"],
678
978
  reconnection: true,
@@ -683,32 +983,80 @@ async function connectSocket(config, getToken) {
683
983
  });
684
984
  setStatus("connecting");
685
985
  _socket.on("connect", () => setStatus("connected"));
686
- _socket.on("disconnect", () => setStatus("disconnected"));
986
+ _socket.on("disconnect", () => {
987
+ setStatus("disconnected");
988
+ clearTransitSession();
989
+ });
687
990
  _socket.on("connect_error", (err) => {
688
991
  console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
689
992
  setStatus("error");
690
993
  });
691
994
  _socket.on("reconnecting", () => setStatus("reconnecting"));
692
995
  _socket.on("reconnect", () => setStatus("connected"));
693
- _socket.on("read_receipt", (event) => {
996
+ if (config.transitEncryption && boundDeriveSessionKey) {
997
+ await new Promise((resolve) => {
998
+ const done = () => {
999
+ clearTimeout(timeout);
1000
+ resolve();
1001
+ };
1002
+ const timeout = setTimeout(() => {
1003
+ console.warn("[AntzChat] transit_session timeout \u2014 unblocking HTTP without transit encryption");
1004
+ configureTransit(false);
1005
+ done();
1006
+ }, 5e3);
1007
+ _socket.on("transit_session", async ({ sessionId }) => {
1008
+ try {
1009
+ if (boundDeriveSessionKey) {
1010
+ const sessionKey = await boundDeriveSessionKey(sessionId);
1011
+ const algo = socketHandshakeAuth["transitAlgo"] ?? "p256";
1012
+ setTransitSession({ sessionKey, algo, sessionId, enabled: true });
1013
+ }
1014
+ } catch (err) {
1015
+ console.error("[AntzChat] Failed to derive transit session key:", err.message);
1016
+ }
1017
+ done();
1018
+ });
1019
+ _socket.on("connect_error", () => done());
1020
+ });
1021
+ }
1022
+ secureOn(_socket, "read_receipt", (event) => {
694
1023
  import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
695
- useChatStore2.getState().setLastRead(event.conversationId, event.messageId, event.readAt);
1024
+ const e = event;
1025
+ useChatStore2.getState().setLastRead(e.conversationId, e.messageId, e.readAt);
696
1026
  });
697
1027
  });
698
- _socket.on("user_online", (event) => {
1028
+ secureOn(_socket, "user_online", (event) => {
699
1029
  import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
700
- const store = useChatStore2.getState();
701
- store.setUserOnline(event.userId);
1030
+ useChatStore2.getState().setUserOnline(event.userId);
702
1031
  });
703
1032
  });
704
- _socket.on("user_offline", (event) => {
1033
+ secureOn(_socket, "user_offline", (event) => {
705
1034
  import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
1035
+ const e = event;
706
1036
  const store = useChatStore2.getState();
707
- store.setUserOffline(event.userId);
708
- if (event.lastSeenAt) store.setLastSeen(event.userId, event.lastSeenAt);
1037
+ store.setUserOffline(e.userId);
1038
+ if (e.lastSeenAt) store.setLastSeen(e.userId, e.lastSeenAt);
709
1039
  });
710
1040
  });
711
- return _socket;
1041
+ return createSecureSocketProxy(_socket);
1042
+ }
1043
+ function createSecureSocketProxy(socket) {
1044
+ return new Proxy(socket, {
1045
+ get(target, prop) {
1046
+ if (prop === "on") {
1047
+ return (event, handler) => {
1048
+ const internal = ["connect", "disconnect", "connect_error", "reconnect", "reconnecting", "error"];
1049
+ if (internal.includes(event)) {
1050
+ return target.on(event, handler);
1051
+ }
1052
+ secureOn(target, event, handler);
1053
+ return socket;
1054
+ };
1055
+ }
1056
+ const val = target[prop];
1057
+ return typeof val === "function" ? val.bind(target) : val;
1058
+ }
1059
+ });
712
1060
  }
713
1061
  function disconnectSocket() {
714
1062
  if (_socket) {
@@ -716,6 +1064,8 @@ function disconnectSocket() {
716
1064
  _socket = null;
717
1065
  setStatus("disconnected");
718
1066
  }
1067
+ clearTransitSession();
1068
+ resetAlgoCache();
719
1069
  _getToken = null;
720
1070
  _userId = void 0;
721
1071
  _tenantId = void 0;
@@ -773,7 +1123,7 @@ async function withAck(event, payload) {
773
1123
  if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
774
1124
  return new Promise((resolve, reject) => {
775
1125
  const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
776
- socket.emit(event, payload, (response) => {
1126
+ secureEmit(socket, event, payload, (response) => {
777
1127
  clearTimeout(timer);
778
1128
  resolve(response);
779
1129
  });
@@ -782,7 +1132,7 @@ async function withAck(event, payload) {
782
1132
  function fireAndForget(event, payload) {
783
1133
  const socket = tryGetSocket();
784
1134
  if (!socket) return;
785
- socket.emit(event, payload);
1135
+ secureEmit(socket, event, payload);
786
1136
  }
787
1137
  var socketEmit = {
788
1138
  joinRoom(conversationId) {
@@ -827,7 +1177,7 @@ var socketEmit = {
827
1177
  if (!socket) return Promise.resolve([]);
828
1178
  return new Promise((resolve, reject) => {
829
1179
  const timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
830
- socket.emit("get_online_users", { userIds }, (response) => {
1180
+ secureEmit(socket, "get_online_users", { userIds }, (response) => {
831
1181
  clearTimeout(timer);
832
1182
  if (response && typeof response === "object" && "onlineStatus" in response) {
833
1183
  const status = response.onlineStatus;
@@ -1005,6 +1355,7 @@ export {
1005
1355
  resetAuthStore,
1006
1356
  resolveConfig,
1007
1357
  setApiClientInstance,
1358
+ setTransitSession,
1008
1359
  socketEmit,
1009
1360
  storageApi,
1010
1361
  tryGetSocket,