@antzsoft/chat-core 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -36
- package/dist/chat.store-DLNRJ5ZT.js +7 -0
- package/dist/{chunk-GUO5QQGK.js → chunk-EOL5B7GS.js} +4 -2
- package/dist/chunk-EOL5B7GS.js.map +1 -0
- package/dist/{chunk-P7VAN6NA.js → chunk-ZNA6B2R5.js} +305 -72
- package/dist/chunk-ZNA6B2R5.js.map +1 -0
- package/dist/index.cjs +296 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -3
- package/dist/index.d.ts +36 -3
- package/dist/index.js +64 -142
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +68 -8
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/{storage-DO8QIqKq.d.cts → storage-CctLCOnZ.d.cts} +54 -4
- package/dist/{storage-DO8QIqKq.d.ts → storage-CctLCOnZ.d.ts} +54 -4
- package/docs/integration-guide.html +209 -28
- package/package.json +1 -1
- package/dist/chat.store-JC6QYDDL.js +0 -7
- package/dist/chunk-GUO5QQGK.js.map +0 -1
- package/dist/chunk-P7VAN6NA.js.map +0 -1
- /package/dist/{chat.store-JC6QYDDL.js.map → chat.store-DLNRJ5ZT.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -52,6 +52,7 @@ var init_chat_store = __esm({
|
|
|
52
52
|
isSidebarOpen: true,
|
|
53
53
|
isGroupInfoOpen: false,
|
|
54
54
|
isStarredPanelOpen: false,
|
|
55
|
+
messageInfoId: null,
|
|
55
56
|
setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
|
|
56
57
|
setPendingTarget: (target) => set({ pendingTarget: target }),
|
|
57
58
|
addTypingUser: (conversationId, user) => set((state) => {
|
|
@@ -89,7 +90,8 @@ var init_chat_store = __esm({
|
|
|
89
90
|
toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
|
|
90
91
|
setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
|
|
91
92
|
toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
|
|
92
|
-
setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open })
|
|
93
|
+
setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),
|
|
94
|
+
setMessageInfoId: (id) => set({ messageInfoId: id })
|
|
93
95
|
}));
|
|
94
96
|
}
|
|
95
97
|
});
|
|
@@ -103,18 +105,27 @@ __export(src_exports, {
|
|
|
103
105
|
connectSocket: () => connectSocket,
|
|
104
106
|
conversationsApi: () => conversationsApi,
|
|
105
107
|
createAuthStore: () => createAuthStore,
|
|
108
|
+
createRestTransitSession: () => createRestTransitSession,
|
|
109
|
+
decryptPayload: () => decryptPayload,
|
|
106
110
|
devicesApi: () => devicesApi,
|
|
107
111
|
disconnectSocket: () => disconnectSocket,
|
|
112
|
+
encryptPayload: () => encryptPayload,
|
|
113
|
+
fetchServerKeys: () => fetchServerKeys,
|
|
114
|
+
generateEphemeralKey: () => generateEphemeralKey,
|
|
108
115
|
getApiClient: () => getApiClient,
|
|
109
116
|
getAuthStore: () => getAuthStore,
|
|
110
117
|
getCompressionStrategy: () => getCompressionStrategy,
|
|
118
|
+
getSessionId: () => getSessionId,
|
|
119
|
+
getSessionKey: () => getSessionKey,
|
|
111
120
|
getSocket: () => getSocket,
|
|
112
121
|
getSocketStatus: () => getSocketStatus,
|
|
113
122
|
initApiClient: () => initApiClient,
|
|
114
123
|
initAuthStore: () => initAuthStore,
|
|
124
|
+
isTransitEnvelope: () => isTransitEnvelope,
|
|
115
125
|
messagesApi: () => messagesApi,
|
|
116
126
|
normalizeConversation: () => normalizeConversation,
|
|
117
127
|
onSocketStatus: () => onSocketStatus,
|
|
128
|
+
performHandshake: () => performHandshake,
|
|
118
129
|
reconnectSocket: () => reconnectSocket,
|
|
119
130
|
refreshSocketAuth: () => refreshSocketAuth,
|
|
120
131
|
resetAuthStore: () => resetAuthStore,
|
|
@@ -176,6 +187,7 @@ function resolveConfig(config) {
|
|
|
176
187
|
onProgress: config.upload?.onProgress
|
|
177
188
|
},
|
|
178
189
|
platformUploadFn: config.platformUploadFn,
|
|
190
|
+
platformUploadPartFn: config.platformUploadPartFn,
|
|
179
191
|
platformCompressFn: config.platformCompressFn,
|
|
180
192
|
compression: {
|
|
181
193
|
enabled: config.compression?.enabled ?? config.platformCompressFn != null,
|
|
@@ -388,6 +400,9 @@ function setTransitSession(session) {
|
|
|
388
400
|
s.readyResolve?.();
|
|
389
401
|
s.readyResolve = null;
|
|
390
402
|
}
|
|
403
|
+
function getTransitSession() {
|
|
404
|
+
return getState().session;
|
|
405
|
+
}
|
|
391
406
|
function clearTransitSession() {
|
|
392
407
|
const s = getState();
|
|
393
408
|
s.session = null;
|
|
@@ -404,10 +419,156 @@ function getSessionId() {
|
|
|
404
419
|
return getState().session?.sessionId ?? null;
|
|
405
420
|
}
|
|
406
421
|
|
|
422
|
+
// src/crypto/detect.ts
|
|
423
|
+
var _cached = null;
|
|
424
|
+
async function detectTransitAlgo() {
|
|
425
|
+
if (_cached) return _cached;
|
|
426
|
+
try {
|
|
427
|
+
await globalThis.crypto.subtle.generateKey(
|
|
428
|
+
{ name: "X25519" },
|
|
429
|
+
false,
|
|
430
|
+
["deriveKey"]
|
|
431
|
+
);
|
|
432
|
+
_cached = "x25519";
|
|
433
|
+
} catch {
|
|
434
|
+
_cached = "p256";
|
|
435
|
+
}
|
|
436
|
+
return _cached;
|
|
437
|
+
}
|
|
438
|
+
function resetAlgoCache() {
|
|
439
|
+
_cached = null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/crypto/handshake.ts
|
|
443
|
+
function hasWebCrypto2() {
|
|
444
|
+
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
445
|
+
}
|
|
446
|
+
async function fetchServerKeys(apiUrl) {
|
|
447
|
+
const res = await fetch(`${apiUrl}/crypto/pubkey`);
|
|
448
|
+
if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
|
|
449
|
+
const body = await res.json();
|
|
450
|
+
return body?.data ?? body;
|
|
451
|
+
}
|
|
452
|
+
async function generateEphemeralKey(algo, serverKeys) {
|
|
453
|
+
if (hasWebCrypto2()) {
|
|
454
|
+
return generateWebCryptoEphemeralKey(algo, serverKeys);
|
|
455
|
+
}
|
|
456
|
+
return generateNobleEphemeralKey(serverKeys);
|
|
457
|
+
}
|
|
458
|
+
async function createRestTransitSession(apiUrl) {
|
|
459
|
+
try {
|
|
460
|
+
const serverKeys = await fetchServerKeys(apiUrl);
|
|
461
|
+
if (!serverKeys.enabled) return null;
|
|
462
|
+
const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
|
|
463
|
+
const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
|
|
464
|
+
const res = await fetch(`${apiUrl}/crypto/session`, {
|
|
465
|
+
method: "POST",
|
|
466
|
+
headers: { "Content-Type": "application/json" },
|
|
467
|
+
body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
|
|
468
|
+
});
|
|
469
|
+
if (!res.ok) return null;
|
|
470
|
+
const body = await res.json();
|
|
471
|
+
const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
|
|
472
|
+
if (!sessionId) return null;
|
|
473
|
+
const sessionKey = await deriveSessionKey(sessionId);
|
|
474
|
+
return { sessionId, sessionKey };
|
|
475
|
+
} catch {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
480
|
+
const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
|
|
481
|
+
socketHandshakeAuth["transitEphemeralPub"] = ephemeralPubB64;
|
|
482
|
+
socketHandshakeAuth["transitAlgo"] = algo;
|
|
483
|
+
return deriveSessionKey;
|
|
484
|
+
}
|
|
485
|
+
async function generateWebCryptoEphemeralKey(algo, serverKeys) {
|
|
486
|
+
const ephemeral = await globalThis.crypto.subtle.generateKey(
|
|
487
|
+
algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
|
|
488
|
+
false,
|
|
489
|
+
["deriveBits"]
|
|
490
|
+
);
|
|
491
|
+
const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
|
|
492
|
+
const ephemeralPriv = ephemeral.privateKey;
|
|
493
|
+
return {
|
|
494
|
+
ephemeralPubB64: bufToB642(pubRaw),
|
|
495
|
+
deriveSessionKey: (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId)
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
|
|
499
|
+
const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
|
|
500
|
+
const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
|
|
501
|
+
const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
|
|
502
|
+
const sharedBits = await globalThis.crypto.subtle.deriveBits(
|
|
503
|
+
{ name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
|
|
504
|
+
ephemeralPriv,
|
|
505
|
+
256
|
|
506
|
+
);
|
|
507
|
+
const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
508
|
+
const salt = new TextEncoder().encode(sessionId);
|
|
509
|
+
const info = new TextEncoder().encode("antz-transit-v1");
|
|
510
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
511
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
512
|
+
hkdfKey,
|
|
513
|
+
{ name: "AES-GCM", length: 256 },
|
|
514
|
+
false,
|
|
515
|
+
["encrypt", "decrypt"]
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
async function generateNobleEphemeralKey(serverKeys) {
|
|
519
|
+
const { x25519 } = await import("@noble/curves/ed25519");
|
|
520
|
+
const { hkdf } = await import("@noble/hashes/hkdf");
|
|
521
|
+
const { sha256 } = await import("@noble/hashes/sha256");
|
|
522
|
+
const { randomBytes } = await import("@noble/hashes/utils");
|
|
523
|
+
const ephemeralPriv = randomBytes(32);
|
|
524
|
+
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
525
|
+
const serverPubBytes = base64ToUint82(serverKeys.x25519);
|
|
526
|
+
return {
|
|
527
|
+
ephemeralPubB64: uint8ToBase64(ephemeralPub),
|
|
528
|
+
deriveSessionKey: (sessionId) => {
|
|
529
|
+
const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
|
|
530
|
+
const salt = new TextEncoder().encode(sessionId);
|
|
531
|
+
const info = new TextEncoder().encode("antz-transit-v1");
|
|
532
|
+
return Promise.resolve(hkdf(sha256, sharedSecret, salt, info, 32));
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function bufToB642(buf) {
|
|
537
|
+
const bytes = new Uint8Array(buf);
|
|
538
|
+
let str = "";
|
|
539
|
+
bytes.forEach((b) => {
|
|
540
|
+
str += String.fromCharCode(b);
|
|
541
|
+
});
|
|
542
|
+
return btoa(str);
|
|
543
|
+
}
|
|
544
|
+
function b64ToBuf2(b64) {
|
|
545
|
+
const bin = atob(b64);
|
|
546
|
+
const buf = new Uint8Array(bin.length);
|
|
547
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
548
|
+
return buf.buffer;
|
|
549
|
+
}
|
|
550
|
+
function uint8ToBase64(bytes) {
|
|
551
|
+
let str = "";
|
|
552
|
+
bytes.forEach((b) => {
|
|
553
|
+
str += String.fromCharCode(b);
|
|
554
|
+
});
|
|
555
|
+
return btoa(str);
|
|
556
|
+
}
|
|
557
|
+
function base64ToUint82(b64) {
|
|
558
|
+
const bin = atob(b64);
|
|
559
|
+
const buf = new Uint8Array(bin.length);
|
|
560
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
561
|
+
return buf;
|
|
562
|
+
}
|
|
563
|
+
|
|
407
564
|
// src/api/client.ts
|
|
408
565
|
var _tokenStore = null;
|
|
409
566
|
var _config = null;
|
|
410
567
|
var _avatarSent = false;
|
|
568
|
+
var _transitHandshakePromise = null;
|
|
569
|
+
function getTransitHandshakePromise() {
|
|
570
|
+
return _transitHandshakePromise;
|
|
571
|
+
}
|
|
411
572
|
function initApiClient(config, tokenStore) {
|
|
412
573
|
_config = config;
|
|
413
574
|
_tokenStore = tokenStore;
|
|
@@ -417,6 +578,23 @@ function initApiClient(config, tokenStore) {
|
|
|
417
578
|
headers: { "Content-Type": "application/json" }
|
|
418
579
|
});
|
|
419
580
|
configureTransit(config.transitEncryption);
|
|
581
|
+
if (config.transitEncryption && !getTransitSession() && !_transitHandshakePromise) {
|
|
582
|
+
_transitHandshakePromise = (async () => {
|
|
583
|
+
try {
|
|
584
|
+
const session = await createRestTransitSession(config.apiUrl);
|
|
585
|
+
if (session && !getTransitSession()) {
|
|
586
|
+
const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
|
|
587
|
+
setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
|
|
588
|
+
} else if (!session) {
|
|
589
|
+
configureTransit(false);
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
configureTransit(false);
|
|
593
|
+
} finally {
|
|
594
|
+
_transitHandshakePromise = null;
|
|
595
|
+
}
|
|
596
|
+
})();
|
|
597
|
+
}
|
|
420
598
|
client.interceptors.request.use(async (req) => {
|
|
421
599
|
const token = _tokenStore?.getAccessToken();
|
|
422
600
|
if (token) req.headers["Authorization"] = `Bearer ${token}`;
|
|
@@ -813,7 +991,7 @@ function normalizeLastMessage(lastMsg) {
|
|
|
813
991
|
id: lastMsg.messageId ?? "",
|
|
814
992
|
tenantId: "",
|
|
815
993
|
conversationId: "",
|
|
816
|
-
senderId: "",
|
|
994
|
+
senderId: lastMsg.senderId ?? "",
|
|
817
995
|
content: {
|
|
818
996
|
type: lastMsg.hasAttachments ? "attachment" : "text",
|
|
819
997
|
text: lastMsg.contentPreview
|
|
@@ -824,7 +1002,8 @@ function normalizeLastMessage(lastMsg) {
|
|
|
824
1002
|
isEdited: false,
|
|
825
1003
|
sentAt: lastMsg.sentAt ?? "",
|
|
826
1004
|
createdAt: lastMsg.sentAt ?? "",
|
|
827
|
-
...lastMsg.senderName && { senderName: lastMsg.senderName }
|
|
1005
|
+
...lastMsg.senderName && { senderName: lastMsg.senderName },
|
|
1006
|
+
...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
|
|
828
1007
|
};
|
|
829
1008
|
}
|
|
830
1009
|
function normalizeConversation(conv) {
|
|
@@ -978,6 +1157,13 @@ var storageApi = {
|
|
|
978
1157
|
async deleteFile(fileId) {
|
|
979
1158
|
await getApiClient().post(`/storage/files/${fileId}/delete`);
|
|
980
1159
|
},
|
|
1160
|
+
async completeMultipartUpload(fileId, uploadId, parts) {
|
|
1161
|
+
const { data } = await getApiClient().post(
|
|
1162
|
+
`/storage/multipart/complete/${fileId}`,
|
|
1163
|
+
{ uploadId, parts }
|
|
1164
|
+
);
|
|
1165
|
+
return data;
|
|
1166
|
+
},
|
|
981
1167
|
async getConversationFiles(conversationId, params = {}) {
|
|
982
1168
|
const { data } = await getApiClient().get(
|
|
983
1169
|
`/storage/conversations/${conversationId}/files`,
|
|
@@ -990,7 +1176,52 @@ var storageApi = {
|
|
|
990
1176
|
return data;
|
|
991
1177
|
}
|
|
992
1178
|
};
|
|
993
|
-
async function
|
|
1179
|
+
async function runMultipartUpload(presigned, file, platformUploadPartFn, onProgress) {
|
|
1180
|
+
const { multipart } = presigned;
|
|
1181
|
+
if (!multipart) throw new Error("No multipart info on presigned response");
|
|
1182
|
+
const CONCURRENCY = 3;
|
|
1183
|
+
const completedParts = [];
|
|
1184
|
+
const partProgress = {};
|
|
1185
|
+
multipart.partUrls.forEach(({ partNumber }) => {
|
|
1186
|
+
partProgress[partNumber] = 0;
|
|
1187
|
+
});
|
|
1188
|
+
const reportProgress = () => {
|
|
1189
|
+
if (!onProgress) return;
|
|
1190
|
+
const vals = Object.values(partProgress);
|
|
1191
|
+
const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
|
|
1192
|
+
onProgress(Math.round(avg * 0.95));
|
|
1193
|
+
};
|
|
1194
|
+
const uploadPart = async (partNumber, uploadUrl, method) => {
|
|
1195
|
+
const offset = (partNumber - 1) * multipart.chunkSize;
|
|
1196
|
+
const end = Math.min(offset + multipart.chunkSize, file.size);
|
|
1197
|
+
const blob = await fetch(file.uri).then((r) => r.blob());
|
|
1198
|
+
const slice = blob.slice(offset, end);
|
|
1199
|
+
const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {
|
|
1200
|
+
partProgress[partNumber] = pct;
|
|
1201
|
+
reportProgress();
|
|
1202
|
+
}, method);
|
|
1203
|
+
completedParts.push({ partNumber, etag });
|
|
1204
|
+
partProgress[partNumber] = 100;
|
|
1205
|
+
reportProgress();
|
|
1206
|
+
};
|
|
1207
|
+
for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {
|
|
1208
|
+
const batch = multipart.partUrls.slice(i, i + CONCURRENCY);
|
|
1209
|
+
const results = await Promise.allSettled(
|
|
1210
|
+
batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? "PUT"))
|
|
1211
|
+
);
|
|
1212
|
+
const failed = results.find((r) => r.status === "rejected");
|
|
1213
|
+
if (failed) throw failed.reason;
|
|
1214
|
+
}
|
|
1215
|
+
completedParts.sort((a, b) => a.partNumber - b.partNumber);
|
|
1216
|
+
const fileResponse = await storageApi.completeMultipartUpload(
|
|
1217
|
+
presigned.fileId,
|
|
1218
|
+
multipart.uploadId,
|
|
1219
|
+
completedParts
|
|
1220
|
+
);
|
|
1221
|
+
onProgress?.(100);
|
|
1222
|
+
return fileResponse;
|
|
1223
|
+
}
|
|
1224
|
+
async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
|
|
994
1225
|
const compressedFiles = await Promise.all(
|
|
995
1226
|
files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
|
|
996
1227
|
);
|
|
@@ -1032,11 +1263,19 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
1032
1263
|
const { file, slotId } = slotted[originalIdx];
|
|
1033
1264
|
progressMap[originalIdx] = 0;
|
|
1034
1265
|
try {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1266
|
+
let fileResponse;
|
|
1267
|
+
if (presigned.multipart && platformUploadPartFn) {
|
|
1268
|
+
fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {
|
|
1269
|
+
progressMap[originalIdx] = pct;
|
|
1270
|
+
reportProgress();
|
|
1271
|
+
});
|
|
1272
|
+
} else {
|
|
1273
|
+
await platformUploadFn(presigned, file, (pct) => {
|
|
1274
|
+
progressMap[originalIdx] = Math.round(pct * 0.9);
|
|
1275
|
+
reportProgress();
|
|
1276
|
+
});
|
|
1277
|
+
fileResponse = await storageApi.confirmUpload(presigned.fileId);
|
|
1278
|
+
}
|
|
1040
1279
|
progressMap[originalIdx] = 100;
|
|
1041
1280
|
reportProgress();
|
|
1042
1281
|
successful.push(fileResponse);
|
|
@@ -1048,9 +1287,9 @@ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId,
|
|
|
1048
1287
|
);
|
|
1049
1288
|
return { result: { successful, failed }, slotToFile };
|
|
1050
1289
|
}
|
|
1051
|
-
async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig) {
|
|
1290
|
+
async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
|
|
1052
1291
|
const slotIds = files.map(() => generateUUID());
|
|
1053
|
-
const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);
|
|
1292
|
+
const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);
|
|
1054
1293
|
return result;
|
|
1055
1294
|
}
|
|
1056
1295
|
|
|
@@ -1128,122 +1367,6 @@ var usersApi = {
|
|
|
1128
1367
|
|
|
1129
1368
|
// src/socket/socket.ts
|
|
1130
1369
|
var import_socket = require("socket.io-client");
|
|
1131
|
-
|
|
1132
|
-
// src/crypto/detect.ts
|
|
1133
|
-
var _cached = null;
|
|
1134
|
-
async function detectTransitAlgo() {
|
|
1135
|
-
if (_cached) return _cached;
|
|
1136
|
-
try {
|
|
1137
|
-
await globalThis.crypto.subtle.generateKey(
|
|
1138
|
-
{ name: "X25519" },
|
|
1139
|
-
false,
|
|
1140
|
-
["deriveKey"]
|
|
1141
|
-
);
|
|
1142
|
-
_cached = "x25519";
|
|
1143
|
-
} catch {
|
|
1144
|
-
_cached = "p256";
|
|
1145
|
-
}
|
|
1146
|
-
return _cached;
|
|
1147
|
-
}
|
|
1148
|
-
function resetAlgoCache() {
|
|
1149
|
-
_cached = null;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// src/crypto/handshake.ts
|
|
1153
|
-
function hasWebCrypto2() {
|
|
1154
|
-
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
1155
|
-
}
|
|
1156
|
-
async function fetchServerKeys(apiUrl) {
|
|
1157
|
-
const res = await fetch(`${apiUrl}/crypto/pubkey`);
|
|
1158
|
-
if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
|
|
1159
|
-
const body = await res.json();
|
|
1160
|
-
return body?.data ?? body;
|
|
1161
|
-
}
|
|
1162
|
-
async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
1163
|
-
if (hasWebCrypto2()) {
|
|
1164
|
-
return performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1165
|
-
}
|
|
1166
|
-
return performNobleHandshake(serverKeys, socketHandshakeAuth);
|
|
1167
|
-
}
|
|
1168
|
-
async function performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
1169
|
-
const ephemeral = await globalThis.crypto.subtle.generateKey(
|
|
1170
|
-
algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
|
|
1171
|
-
false,
|
|
1172
|
-
["deriveBits"]
|
|
1173
|
-
);
|
|
1174
|
-
const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
|
|
1175
|
-
socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
|
|
1176
|
-
socketHandshakeAuth["transitAlgo"] = algo;
|
|
1177
|
-
const ephemeralPriv = ephemeral.privateKey;
|
|
1178
|
-
return (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
|
|
1179
|
-
}
|
|
1180
|
-
async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
|
|
1181
|
-
const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
|
|
1182
|
-
const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
|
|
1183
|
-
const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
|
|
1184
|
-
const sharedBits = await globalThis.crypto.subtle.deriveBits(
|
|
1185
|
-
{ name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
|
|
1186
|
-
ephemeralPriv,
|
|
1187
|
-
256
|
|
1188
|
-
);
|
|
1189
|
-
const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
1190
|
-
const salt = new TextEncoder().encode(sessionId);
|
|
1191
|
-
const info = new TextEncoder().encode("antz-transit-v1");
|
|
1192
|
-
return globalThis.crypto.subtle.deriveKey(
|
|
1193
|
-
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
1194
|
-
hkdfKey,
|
|
1195
|
-
{ name: "AES-GCM", length: 256 },
|
|
1196
|
-
false,
|
|
1197
|
-
["encrypt", "decrypt"]
|
|
1198
|
-
);
|
|
1199
|
-
}
|
|
1200
|
-
async function performNobleHandshake(serverKeys, socketHandshakeAuth) {
|
|
1201
|
-
const { x25519 } = await import("@noble/curves/ed25519");
|
|
1202
|
-
const { hkdf } = await import("@noble/hashes/hkdf");
|
|
1203
|
-
const { sha256 } = await import("@noble/hashes/sha256");
|
|
1204
|
-
const { randomBytes } = await import("@noble/hashes/utils");
|
|
1205
|
-
const ephemeralPriv = randomBytes(32);
|
|
1206
|
-
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
1207
|
-
const serverPubBytes = base64ToUint82(serverKeys.x25519);
|
|
1208
|
-
socketHandshakeAuth["transitEphemeralPub"] = uint8ToBase64(ephemeralPub);
|
|
1209
|
-
socketHandshakeAuth["transitAlgo"] = "x25519";
|
|
1210
|
-
return (sessionId) => {
|
|
1211
|
-
const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
|
|
1212
|
-
const salt = new TextEncoder().encode(sessionId);
|
|
1213
|
-
const info = new TextEncoder().encode("antz-transit-v1");
|
|
1214
|
-
const sessionKey = hkdf(sha256, sharedSecret, salt, info, 32);
|
|
1215
|
-
return Promise.resolve(sessionKey);
|
|
1216
|
-
};
|
|
1217
|
-
}
|
|
1218
|
-
function bufToB642(buf) {
|
|
1219
|
-
const bytes = new Uint8Array(buf);
|
|
1220
|
-
let str = "";
|
|
1221
|
-
bytes.forEach((b) => {
|
|
1222
|
-
str += String.fromCharCode(b);
|
|
1223
|
-
});
|
|
1224
|
-
return btoa(str);
|
|
1225
|
-
}
|
|
1226
|
-
function b64ToBuf2(b64) {
|
|
1227
|
-
const bin = atob(b64);
|
|
1228
|
-
const buf = new Uint8Array(bin.length);
|
|
1229
|
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1230
|
-
return buf.buffer;
|
|
1231
|
-
}
|
|
1232
|
-
function uint8ToBase64(bytes) {
|
|
1233
|
-
let str = "";
|
|
1234
|
-
bytes.forEach((b) => {
|
|
1235
|
-
str += String.fromCharCode(b);
|
|
1236
|
-
});
|
|
1237
|
-
return btoa(str);
|
|
1238
|
-
}
|
|
1239
|
-
function base64ToUint82(b64) {
|
|
1240
|
-
const bin = atob(b64);
|
|
1241
|
-
const buf = new Uint8Array(bin.length);
|
|
1242
|
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1243
|
-
return buf;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
// src/socket/socket.ts
|
|
1247
1370
|
var _socket = null;
|
|
1248
1371
|
var _socketProxy = null;
|
|
1249
1372
|
var _connectingPromise = null;
|
|
@@ -1340,17 +1463,35 @@ async function _doConnect(config, getToken) {
|
|
|
1340
1463
|
};
|
|
1341
1464
|
let boundDeriveSessionKey = null;
|
|
1342
1465
|
if (config.transitEncryption) {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1466
|
+
const inFlight = getTransitHandshakePromise();
|
|
1467
|
+
if (inFlight) await inFlight;
|
|
1468
|
+
const existingSession = getTransitSession();
|
|
1469
|
+
let httpsSession = null;
|
|
1470
|
+
if (existingSession?.enabled && existingSession.sessionId) {
|
|
1471
|
+
httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
|
|
1472
|
+
} else {
|
|
1473
|
+
httpsSession = await createRestTransitSession(config.apiUrl);
|
|
1474
|
+
if (httpsSession) {
|
|
1475
|
+
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1476
|
+
setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
if (httpsSession) {
|
|
1480
|
+
socketHandshakeAuth["transitSessionId"] = httpsSession.sessionId;
|
|
1481
|
+
boundDeriveSessionKey = null;
|
|
1482
|
+
} else {
|
|
1483
|
+
try {
|
|
1484
|
+
const serverKeys = await fetchServerKeys(config.apiUrl);
|
|
1485
|
+
if (!serverKeys.enabled) {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
"[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides."
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1491
|
+
boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1492
|
+
} catch (err) {
|
|
1493
|
+
throw err;
|
|
1349
1494
|
}
|
|
1350
|
-
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1351
|
-
boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1352
|
-
} catch (err) {
|
|
1353
|
-
throw err;
|
|
1354
1495
|
}
|
|
1355
1496
|
} else {
|
|
1356
1497
|
try {
|
|
@@ -1508,11 +1649,7 @@ async function drainSendQueue(conversationId) {
|
|
|
1508
1649
|
entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
|
|
1509
1650
|
continue;
|
|
1510
1651
|
}
|
|
1511
|
-
|
|
1512
|
-
entry.resolve(await entry.run());
|
|
1513
|
-
} catch (e) {
|
|
1514
|
-
entry.reject(e);
|
|
1515
|
-
}
|
|
1652
|
+
entry.run().then(entry.resolve).catch(entry.reject);
|
|
1516
1653
|
}
|
|
1517
1654
|
sendQueues.delete(conversationId);
|
|
1518
1655
|
sendQueueRunning.delete(conversationId);
|
|
@@ -1556,11 +1693,13 @@ async function withAck(event, payload) {
|
|
|
1556
1693
|
}
|
|
1557
1694
|
if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
|
|
1558
1695
|
return new Promise((resolve, reject) => {
|
|
1559
|
-
|
|
1696
|
+
let timer;
|
|
1560
1697
|
secureEmit(socket, event, payload, (response) => {
|
|
1561
1698
|
clearTimeout(timer);
|
|
1562
1699
|
resolve(response);
|
|
1563
|
-
})
|
|
1700
|
+
}).then(() => {
|
|
1701
|
+
timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
|
|
1702
|
+
}).catch(reject);
|
|
1564
1703
|
});
|
|
1565
1704
|
}
|
|
1566
1705
|
function fireAndForget(event, payload) {
|
|
@@ -1610,7 +1749,7 @@ var socketEmit = {
|
|
|
1610
1749
|
const socket = tryGetSocket();
|
|
1611
1750
|
if (!socket) return Promise.resolve([]);
|
|
1612
1751
|
return new Promise((resolve, reject) => {
|
|
1613
|
-
|
|
1752
|
+
let timer;
|
|
1614
1753
|
secureEmit(socket, "get_online_users", { userIds }, (response) => {
|
|
1615
1754
|
clearTimeout(timer);
|
|
1616
1755
|
if (response && typeof response === "object" && "onlineStatus" in response) {
|
|
@@ -1621,7 +1760,9 @@ var socketEmit = {
|
|
|
1621
1760
|
} else {
|
|
1622
1761
|
resolve([]);
|
|
1623
1762
|
}
|
|
1624
|
-
})
|
|
1763
|
+
}).then(() => {
|
|
1764
|
+
timer = setTimeout(() => reject(new Error("get_online_users timeout")), ACK_TIMEOUT);
|
|
1765
|
+
}).catch(reject);
|
|
1625
1766
|
});
|
|
1626
1767
|
},
|
|
1627
1768
|
getTypingUsers(conversationId) {
|
|
@@ -1689,18 +1830,27 @@ var AntzChatClient = class {
|
|
|
1689
1830
|
connectSocket,
|
|
1690
1831
|
conversationsApi,
|
|
1691
1832
|
createAuthStore,
|
|
1833
|
+
createRestTransitSession,
|
|
1834
|
+
decryptPayload,
|
|
1692
1835
|
devicesApi,
|
|
1693
1836
|
disconnectSocket,
|
|
1837
|
+
encryptPayload,
|
|
1838
|
+
fetchServerKeys,
|
|
1839
|
+
generateEphemeralKey,
|
|
1694
1840
|
getApiClient,
|
|
1695
1841
|
getAuthStore,
|
|
1696
1842
|
getCompressionStrategy,
|
|
1843
|
+
getSessionId,
|
|
1844
|
+
getSessionKey,
|
|
1697
1845
|
getSocket,
|
|
1698
1846
|
getSocketStatus,
|
|
1699
1847
|
initApiClient,
|
|
1700
1848
|
initAuthStore,
|
|
1849
|
+
isTransitEnvelope,
|
|
1701
1850
|
messagesApi,
|
|
1702
1851
|
normalizeConversation,
|
|
1703
1852
|
onSocketStatus,
|
|
1853
|
+
performHandshake,
|
|
1704
1854
|
reconnectSocket,
|
|
1705
1855
|
refreshSocketAuth,
|
|
1706
1856
|
resetAuthStore,
|