@antzsoft/chat-core 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -0
- package/dist/{chunk-6NMA64BX.js → chunk-XTQYF5HU.js} +333 -65
- package/dist/chunk-XTQYF5HU.js.map +1 -0
- package/dist/index.cjs +351 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -3
- package/dist/index.d.ts +74 -3
- package/dist/index.js +92 -145
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +6 -1
- 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-Bp6fk9aM.d.cts → storage-CctLCOnZ.d.cts} +1 -1
- package/dist/{storage-Bp6fk9aM.d.ts → storage-CctLCOnZ.d.ts} +1 -1
- package/docs/integration-guide.html +835 -130
- package/package.json +1 -1
- package/dist/chunk-6NMA64BX.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -99,24 +99,40 @@ var init_chat_store = __esm({
|
|
|
99
99
|
// src/index.ts
|
|
100
100
|
var src_exports = {};
|
|
101
101
|
__export(src_exports, {
|
|
102
|
+
AntzChatAuthError: () => AntzChatAuthError,
|
|
102
103
|
AntzChatClient: () => AntzChatClient,
|
|
104
|
+
AntzChatError: () => AntzChatError,
|
|
105
|
+
AntzChatNetworkError: () => AntzChatNetworkError,
|
|
106
|
+
AntzChatPermissionError: () => AntzChatPermissionError,
|
|
107
|
+
AntzChatServerError: () => AntzChatServerError,
|
|
108
|
+
AntzChatValidationError: () => AntzChatValidationError,
|
|
103
109
|
appConfigApi: () => appConfigApi,
|
|
104
110
|
authApi: () => authApi,
|
|
105
111
|
connectSocket: () => connectSocket,
|
|
106
112
|
conversationsApi: () => conversationsApi,
|
|
107
113
|
createAuthStore: () => createAuthStore,
|
|
114
|
+
createRestTransitSession: () => createRestTransitSession,
|
|
115
|
+
decryptPayload: () => decryptPayload,
|
|
108
116
|
devicesApi: () => devicesApi,
|
|
109
117
|
disconnectSocket: () => disconnectSocket,
|
|
118
|
+
encryptPayload: () => encryptPayload,
|
|
119
|
+
fetchServerKeys: () => fetchServerKeys,
|
|
120
|
+
generateEphemeralKey: () => generateEphemeralKey,
|
|
110
121
|
getApiClient: () => getApiClient,
|
|
111
122
|
getAuthStore: () => getAuthStore,
|
|
112
123
|
getCompressionStrategy: () => getCompressionStrategy,
|
|
124
|
+
getSessionId: () => getSessionId,
|
|
125
|
+
getSessionKey: () => getSessionKey,
|
|
113
126
|
getSocket: () => getSocket,
|
|
114
127
|
getSocketStatus: () => getSocketStatus,
|
|
115
128
|
initApiClient: () => initApiClient,
|
|
116
129
|
initAuthStore: () => initAuthStore,
|
|
130
|
+
isTransitEnvelope: () => isTransitEnvelope,
|
|
117
131
|
messagesApi: () => messagesApi,
|
|
132
|
+
normalizeAxiosError: () => normalizeAxiosError,
|
|
118
133
|
normalizeConversation: () => normalizeConversation,
|
|
119
134
|
onSocketStatus: () => onSocketStatus,
|
|
135
|
+
performHandshake: () => performHandshake,
|
|
120
136
|
reconnectSocket: () => reconnectSocket,
|
|
121
137
|
refreshSocketAuth: () => refreshSocketAuth,
|
|
122
138
|
resetAuthStore: () => resetAuthStore,
|
|
@@ -256,7 +272,7 @@ async function compressFile(file, platformCompressFn, config) {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
// src/api/client.ts
|
|
259
|
-
var
|
|
275
|
+
var import_axios2 = __toESM(require("axios"), 1);
|
|
260
276
|
|
|
261
277
|
// src/crypto/transit.ts
|
|
262
278
|
function hasWebCrypto() {
|
|
@@ -391,6 +407,9 @@ function setTransitSession(session) {
|
|
|
391
407
|
s.readyResolve?.();
|
|
392
408
|
s.readyResolve = null;
|
|
393
409
|
}
|
|
410
|
+
function getTransitSession() {
|
|
411
|
+
return getState().session;
|
|
412
|
+
}
|
|
394
413
|
function clearTransitSession() {
|
|
395
414
|
const s = getState();
|
|
396
415
|
s.session = null;
|
|
@@ -407,19 +426,270 @@ function getSessionId() {
|
|
|
407
426
|
return getState().session?.sessionId ?? null;
|
|
408
427
|
}
|
|
409
428
|
|
|
429
|
+
// src/crypto/detect.ts
|
|
430
|
+
var _cached = null;
|
|
431
|
+
async function detectTransitAlgo() {
|
|
432
|
+
if (_cached) return _cached;
|
|
433
|
+
try {
|
|
434
|
+
await globalThis.crypto.subtle.generateKey(
|
|
435
|
+
{ name: "X25519" },
|
|
436
|
+
false,
|
|
437
|
+
["deriveKey"]
|
|
438
|
+
);
|
|
439
|
+
_cached = "x25519";
|
|
440
|
+
} catch {
|
|
441
|
+
_cached = "p256";
|
|
442
|
+
}
|
|
443
|
+
return _cached;
|
|
444
|
+
}
|
|
445
|
+
function resetAlgoCache() {
|
|
446
|
+
_cached = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/crypto/handshake.ts
|
|
450
|
+
function hasWebCrypto2() {
|
|
451
|
+
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
452
|
+
}
|
|
453
|
+
async function fetchServerKeys(apiUrl) {
|
|
454
|
+
const res = await fetch(`${apiUrl}/crypto/pubkey`);
|
|
455
|
+
if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
|
|
456
|
+
const body = await res.json();
|
|
457
|
+
return body?.data ?? body;
|
|
458
|
+
}
|
|
459
|
+
async function generateEphemeralKey(algo, serverKeys) {
|
|
460
|
+
if (hasWebCrypto2()) {
|
|
461
|
+
return generateWebCryptoEphemeralKey(algo, serverKeys);
|
|
462
|
+
}
|
|
463
|
+
return generateNobleEphemeralKey(serverKeys);
|
|
464
|
+
}
|
|
465
|
+
async function createRestTransitSession(apiUrl) {
|
|
466
|
+
try {
|
|
467
|
+
const serverKeys = await fetchServerKeys(apiUrl);
|
|
468
|
+
if (!serverKeys.enabled) return null;
|
|
469
|
+
const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
|
|
470
|
+
const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
|
|
471
|
+
const res = await fetch(`${apiUrl}/crypto/session`, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: { "Content-Type": "application/json" },
|
|
474
|
+
body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
|
|
475
|
+
});
|
|
476
|
+
if (!res.ok) return null;
|
|
477
|
+
const body = await res.json();
|
|
478
|
+
const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
|
|
479
|
+
if (!sessionId) return null;
|
|
480
|
+
const sessionKey = await deriveSessionKey(sessionId);
|
|
481
|
+
return { sessionId, sessionKey };
|
|
482
|
+
} catch {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
487
|
+
const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
|
|
488
|
+
socketHandshakeAuth["transitEphemeralPub"] = ephemeralPubB64;
|
|
489
|
+
socketHandshakeAuth["transitAlgo"] = algo;
|
|
490
|
+
return deriveSessionKey;
|
|
491
|
+
}
|
|
492
|
+
async function generateWebCryptoEphemeralKey(algo, serverKeys) {
|
|
493
|
+
const ephemeral = await globalThis.crypto.subtle.generateKey(
|
|
494
|
+
algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
|
|
495
|
+
false,
|
|
496
|
+
["deriveBits"]
|
|
497
|
+
);
|
|
498
|
+
const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
|
|
499
|
+
const ephemeralPriv = ephemeral.privateKey;
|
|
500
|
+
return {
|
|
501
|
+
ephemeralPubB64: bufToB642(pubRaw),
|
|
502
|
+
deriveSessionKey: (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId)
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
|
|
506
|
+
const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
|
|
507
|
+
const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
|
|
508
|
+
const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
|
|
509
|
+
const sharedBits = await globalThis.crypto.subtle.deriveBits(
|
|
510
|
+
{ name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
|
|
511
|
+
ephemeralPriv,
|
|
512
|
+
256
|
|
513
|
+
);
|
|
514
|
+
const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
515
|
+
const salt = new TextEncoder().encode(sessionId);
|
|
516
|
+
const info = new TextEncoder().encode("antz-transit-v1");
|
|
517
|
+
return globalThis.crypto.subtle.deriveKey(
|
|
518
|
+
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
519
|
+
hkdfKey,
|
|
520
|
+
{ name: "AES-GCM", length: 256 },
|
|
521
|
+
false,
|
|
522
|
+
["encrypt", "decrypt"]
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
async function generateNobleEphemeralKey(serverKeys) {
|
|
526
|
+
const { x25519 } = await import("@noble/curves/ed25519");
|
|
527
|
+
const { hkdf } = await import("@noble/hashes/hkdf");
|
|
528
|
+
const { sha256 } = await import("@noble/hashes/sha256");
|
|
529
|
+
const { randomBytes } = await import("@noble/hashes/utils");
|
|
530
|
+
const ephemeralPriv = randomBytes(32);
|
|
531
|
+
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
532
|
+
const serverPubBytes = base64ToUint82(serverKeys.x25519);
|
|
533
|
+
return {
|
|
534
|
+
ephemeralPubB64: uint8ToBase64(ephemeralPub),
|
|
535
|
+
deriveSessionKey: (sessionId) => {
|
|
536
|
+
const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
|
|
537
|
+
const salt = new TextEncoder().encode(sessionId);
|
|
538
|
+
const info = new TextEncoder().encode("antz-transit-v1");
|
|
539
|
+
return Promise.resolve(hkdf(sha256, sharedSecret, salt, info, 32));
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function bufToB642(buf) {
|
|
544
|
+
const bytes = new Uint8Array(buf);
|
|
545
|
+
let str = "";
|
|
546
|
+
bytes.forEach((b) => {
|
|
547
|
+
str += String.fromCharCode(b);
|
|
548
|
+
});
|
|
549
|
+
return btoa(str);
|
|
550
|
+
}
|
|
551
|
+
function b64ToBuf2(b64) {
|
|
552
|
+
const bin = atob(b64);
|
|
553
|
+
const buf = new Uint8Array(bin.length);
|
|
554
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
555
|
+
return buf.buffer;
|
|
556
|
+
}
|
|
557
|
+
function uint8ToBase64(bytes) {
|
|
558
|
+
let str = "";
|
|
559
|
+
bytes.forEach((b) => {
|
|
560
|
+
str += String.fromCharCode(b);
|
|
561
|
+
});
|
|
562
|
+
return btoa(str);
|
|
563
|
+
}
|
|
564
|
+
function base64ToUint82(b64) {
|
|
565
|
+
const bin = atob(b64);
|
|
566
|
+
const buf = new Uint8Array(bin.length);
|
|
567
|
+
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
568
|
+
return buf;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/errors.ts
|
|
572
|
+
var import_axios = require("axios");
|
|
573
|
+
var AntzChatError = class extends Error {
|
|
574
|
+
constructor(code, message, retryable = false, context) {
|
|
575
|
+
super(message);
|
|
576
|
+
this.name = "AntzChatError";
|
|
577
|
+
this.code = code;
|
|
578
|
+
this.retryable = retryable;
|
|
579
|
+
this.context = context;
|
|
580
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
var AntzChatAuthError = class extends AntzChatError {
|
|
584
|
+
constructor(message, code = "AUTH_FAILED", context) {
|
|
585
|
+
super(code, message, false, context);
|
|
586
|
+
this.name = "AntzChatAuthError";
|
|
587
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
var AntzChatValidationError = class extends AntzChatError {
|
|
591
|
+
constructor(message, context) {
|
|
592
|
+
const msg = Array.isArray(message) ? message.join("; ") : message;
|
|
593
|
+
super("VALIDATION_ERROR", msg, false, context);
|
|
594
|
+
this.name = "AntzChatValidationError";
|
|
595
|
+
this.fields = Array.isArray(message) ? message : void 0;
|
|
596
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
var AntzChatNetworkError = class extends AntzChatError {
|
|
600
|
+
constructor(message, code = "NETWORK_ERROR", context) {
|
|
601
|
+
super(code, message, true, context);
|
|
602
|
+
this.name = "AntzChatNetworkError";
|
|
603
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
var AntzChatPermissionError = class extends AntzChatError {
|
|
607
|
+
constructor(message, context) {
|
|
608
|
+
super("PERMISSION_DENIED", message, false, context);
|
|
609
|
+
this.name = "AntzChatPermissionError";
|
|
610
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
var AntzChatServerError = class extends AntzChatError {
|
|
614
|
+
constructor(message, httpStatus, context) {
|
|
615
|
+
super("SERVER_ERROR", message, true, context);
|
|
616
|
+
this.name = "AntzChatServerError";
|
|
617
|
+
this.httpStatus = httpStatus;
|
|
618
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
function normalizeAxiosError(error) {
|
|
622
|
+
if (error instanceof AntzChatError) return error;
|
|
623
|
+
if ((0, import_axios.isAxiosError)(error)) {
|
|
624
|
+
const status = error.response?.status;
|
|
625
|
+
const body = error.response?.data;
|
|
626
|
+
const decryptionFailed = body != null && isTransitEnvelope(body);
|
|
627
|
+
const rawMessage = decryptionFailed ? void 0 : body?.message ?? void 0;
|
|
628
|
+
const message = (Array.isArray(rawMessage) ? rawMessage.join("; ") : rawMessage) || error.message || "Request failed";
|
|
629
|
+
const ctx = {
|
|
630
|
+
...status != null && { httpStatus: status },
|
|
631
|
+
...body?.path != null && { path: body.path },
|
|
632
|
+
...body?.error != null && { serverError: body.error },
|
|
633
|
+
...error.code != null && { axiosCode: error.code },
|
|
634
|
+
...decryptionFailed && { decryptionFailed: true, note: "Transit decryption failed \u2014 server error body is an encrypted envelope" }
|
|
635
|
+
};
|
|
636
|
+
if (!error.response) {
|
|
637
|
+
return new AntzChatNetworkError(message || "Network error", "NETWORK_ERROR", ctx);
|
|
638
|
+
}
|
|
639
|
+
if (status === 401) {
|
|
640
|
+
const code = error.config?._retry ? "AUTH_FAILED" : "SESSION_EXPIRED";
|
|
641
|
+
return new AntzChatAuthError(message, code, ctx);
|
|
642
|
+
}
|
|
643
|
+
if (status === 403) return new AntzChatPermissionError(message, ctx);
|
|
644
|
+
if (status === 400 || status === 422) {
|
|
645
|
+
return new AntzChatValidationError(
|
|
646
|
+
Array.isArray(rawMessage) ? rawMessage : message,
|
|
647
|
+
ctx
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (status === 404) return new AntzChatServerError(message, 404, ctx);
|
|
651
|
+
if (status === 429) return new AntzChatNetworkError(message, "RATE_LIMITED", ctx);
|
|
652
|
+
if (status != null && status >= 500) return new AntzChatServerError(message, status, ctx);
|
|
653
|
+
return new AntzChatServerError(message, status, ctx);
|
|
654
|
+
}
|
|
655
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
656
|
+
return new AntzChatError("UNKNOWN_ERROR", msg, false);
|
|
657
|
+
}
|
|
658
|
+
|
|
410
659
|
// src/api/client.ts
|
|
411
660
|
var _tokenStore = null;
|
|
412
661
|
var _config = null;
|
|
413
662
|
var _avatarSent = false;
|
|
663
|
+
var _transitHandshakePromise = null;
|
|
664
|
+
function getTransitHandshakePromise() {
|
|
665
|
+
return _transitHandshakePromise;
|
|
666
|
+
}
|
|
414
667
|
function initApiClient(config, tokenStore) {
|
|
415
668
|
_config = config;
|
|
416
669
|
_tokenStore = tokenStore;
|
|
417
670
|
_avatarSent = false;
|
|
418
|
-
const client =
|
|
671
|
+
const client = import_axios2.default.create({
|
|
419
672
|
baseURL: config.apiUrl,
|
|
420
673
|
headers: { "Content-Type": "application/json" }
|
|
421
674
|
});
|
|
422
675
|
configureTransit(config.transitEncryption);
|
|
676
|
+
if (config.transitEncryption && !getTransitSession() && !_transitHandshakePromise) {
|
|
677
|
+
_transitHandshakePromise = (async () => {
|
|
678
|
+
try {
|
|
679
|
+
const session = await createRestTransitSession(config.apiUrl);
|
|
680
|
+
if (session && !getTransitSession()) {
|
|
681
|
+
const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
|
|
682
|
+
setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
|
|
683
|
+
} else if (!session) {
|
|
684
|
+
configureTransit(false);
|
|
685
|
+
}
|
|
686
|
+
} catch {
|
|
687
|
+
configureTransit(false);
|
|
688
|
+
} finally {
|
|
689
|
+
_transitHandshakePromise = null;
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
692
|
+
}
|
|
423
693
|
client.interceptors.request.use(async (req) => {
|
|
424
694
|
const token = _tokenStore?.getAccessToken();
|
|
425
695
|
if (token) req.headers["Authorization"] = `Bearer ${token}`;
|
|
@@ -483,7 +753,7 @@ function initApiClient(config, tokenStore) {
|
|
|
483
753
|
const refreshToken = _tokenStore?.getRefreshToken();
|
|
484
754
|
if (!refreshToken) {
|
|
485
755
|
_tokenStore?.clearTokens();
|
|
486
|
-
return Promise.reject(error);
|
|
756
|
+
return Promise.reject(normalizeAxiosError(error));
|
|
487
757
|
}
|
|
488
758
|
if (isRefreshing) {
|
|
489
759
|
return new Promise((resolve) => {
|
|
@@ -496,7 +766,7 @@ function initApiClient(config, tokenStore) {
|
|
|
496
766
|
original._retry = true;
|
|
497
767
|
isRefreshing = true;
|
|
498
768
|
try {
|
|
499
|
-
const { data } = await
|
|
769
|
+
const { data } = await import_axios2.default.post(
|
|
500
770
|
`${_config.apiUrl}/auth/refresh`,
|
|
501
771
|
{ refreshToken }
|
|
502
772
|
);
|
|
@@ -508,12 +778,12 @@ function initApiClient(config, tokenStore) {
|
|
|
508
778
|
return client(original);
|
|
509
779
|
} catch {
|
|
510
780
|
_tokenStore?.clearTokens();
|
|
511
|
-
return Promise.reject(error);
|
|
781
|
+
return Promise.reject(normalizeAxiosError(error));
|
|
512
782
|
} finally {
|
|
513
783
|
isRefreshing = false;
|
|
514
784
|
}
|
|
515
785
|
}
|
|
516
|
-
return Promise.reject(error);
|
|
786
|
+
return Promise.reject(normalizeAxiosError(error));
|
|
517
787
|
}
|
|
518
788
|
);
|
|
519
789
|
return client;
|
|
@@ -827,7 +1097,8 @@ function normalizeLastMessage(lastMsg) {
|
|
|
827
1097
|
isEdited: false,
|
|
828
1098
|
sentAt: lastMsg.sentAt ?? "",
|
|
829
1099
|
createdAt: lastMsg.sentAt ?? "",
|
|
830
|
-
...lastMsg.senderName && { senderName: lastMsg.senderName }
|
|
1100
|
+
...lastMsg.senderName && { senderName: lastMsg.senderName },
|
|
1101
|
+
...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
|
|
831
1102
|
};
|
|
832
1103
|
}
|
|
833
1104
|
function normalizeConversation(conv) {
|
|
@@ -940,6 +1211,9 @@ var conversationsApi = {
|
|
|
940
1211
|
async removeIcon(conversationId) {
|
|
941
1212
|
const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
|
|
942
1213
|
return normalizeConversation(data);
|
|
1214
|
+
},
|
|
1215
|
+
async clearChat(conversationId) {
|
|
1216
|
+
await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
|
|
943
1217
|
}
|
|
944
1218
|
};
|
|
945
1219
|
|
|
@@ -1191,122 +1465,6 @@ var usersApi = {
|
|
|
1191
1465
|
|
|
1192
1466
|
// src/socket/socket.ts
|
|
1193
1467
|
var import_socket = require("socket.io-client");
|
|
1194
|
-
|
|
1195
|
-
// src/crypto/detect.ts
|
|
1196
|
-
var _cached = null;
|
|
1197
|
-
async function detectTransitAlgo() {
|
|
1198
|
-
if (_cached) return _cached;
|
|
1199
|
-
try {
|
|
1200
|
-
await globalThis.crypto.subtle.generateKey(
|
|
1201
|
-
{ name: "X25519" },
|
|
1202
|
-
false,
|
|
1203
|
-
["deriveKey"]
|
|
1204
|
-
);
|
|
1205
|
-
_cached = "x25519";
|
|
1206
|
-
} catch {
|
|
1207
|
-
_cached = "p256";
|
|
1208
|
-
}
|
|
1209
|
-
return _cached;
|
|
1210
|
-
}
|
|
1211
|
-
function resetAlgoCache() {
|
|
1212
|
-
_cached = null;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// src/crypto/handshake.ts
|
|
1216
|
-
function hasWebCrypto2() {
|
|
1217
|
-
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
1218
|
-
}
|
|
1219
|
-
async function fetchServerKeys(apiUrl) {
|
|
1220
|
-
const res = await fetch(`${apiUrl}/crypto/pubkey`);
|
|
1221
|
-
if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
|
|
1222
|
-
const body = await res.json();
|
|
1223
|
-
return body?.data ?? body;
|
|
1224
|
-
}
|
|
1225
|
-
async function performHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
1226
|
-
if (hasWebCrypto2()) {
|
|
1227
|
-
return performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1228
|
-
}
|
|
1229
|
-
return performNobleHandshake(serverKeys, socketHandshakeAuth);
|
|
1230
|
-
}
|
|
1231
|
-
async function performWebCryptoHandshake(algo, serverKeys, socketHandshakeAuth) {
|
|
1232
|
-
const ephemeral = await globalThis.crypto.subtle.generateKey(
|
|
1233
|
-
algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" },
|
|
1234
|
-
false,
|
|
1235
|
-
["deriveBits"]
|
|
1236
|
-
);
|
|
1237
|
-
const pubRaw = await globalThis.crypto.subtle.exportKey("raw", ephemeral.publicKey);
|
|
1238
|
-
socketHandshakeAuth["transitEphemeralPub"] = bufToB642(pubRaw);
|
|
1239
|
-
socketHandshakeAuth["transitAlgo"] = algo;
|
|
1240
|
-
const ephemeralPriv = ephemeral.privateKey;
|
|
1241
|
-
return (sessionId) => deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId);
|
|
1242
|
-
}
|
|
1243
|
-
async function deriveWebCryptoSessionKey(ephemeralPriv, algo, serverKeys, sessionId) {
|
|
1244
|
-
const serverPubRaw = b64ToBuf2(algo === "x25519" ? serverKeys.x25519 : serverKeys.p256);
|
|
1245
|
-
const keyAlgoParams = algo === "x25519" ? { name: "X25519" } : { name: "ECDH", namedCurve: "P-256" };
|
|
1246
|
-
const serverPubKey = await globalThis.crypto.subtle.importKey("raw", serverPubRaw, keyAlgoParams, false, []);
|
|
1247
|
-
const sharedBits = await globalThis.crypto.subtle.deriveBits(
|
|
1248
|
-
{ name: algo === "x25519" ? "X25519" : "ECDH", public: serverPubKey },
|
|
1249
|
-
ephemeralPriv,
|
|
1250
|
-
256
|
|
1251
|
-
);
|
|
1252
|
-
const hkdfKey = await globalThis.crypto.subtle.importKey("raw", sharedBits, "HKDF", false, ["deriveKey"]);
|
|
1253
|
-
const salt = new TextEncoder().encode(sessionId);
|
|
1254
|
-
const info = new TextEncoder().encode("antz-transit-v1");
|
|
1255
|
-
return globalThis.crypto.subtle.deriveKey(
|
|
1256
|
-
{ name: "HKDF", hash: "SHA-256", salt, info },
|
|
1257
|
-
hkdfKey,
|
|
1258
|
-
{ name: "AES-GCM", length: 256 },
|
|
1259
|
-
false,
|
|
1260
|
-
["encrypt", "decrypt"]
|
|
1261
|
-
);
|
|
1262
|
-
}
|
|
1263
|
-
async function performNobleHandshake(serverKeys, socketHandshakeAuth) {
|
|
1264
|
-
const { x25519 } = await import("@noble/curves/ed25519");
|
|
1265
|
-
const { hkdf } = await import("@noble/hashes/hkdf");
|
|
1266
|
-
const { sha256 } = await import("@noble/hashes/sha256");
|
|
1267
|
-
const { randomBytes } = await import("@noble/hashes/utils");
|
|
1268
|
-
const ephemeralPriv = randomBytes(32);
|
|
1269
|
-
const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
|
|
1270
|
-
const serverPubBytes = base64ToUint82(serverKeys.x25519);
|
|
1271
|
-
socketHandshakeAuth["transitEphemeralPub"] = uint8ToBase64(ephemeralPub);
|
|
1272
|
-
socketHandshakeAuth["transitAlgo"] = "x25519";
|
|
1273
|
-
return (sessionId) => {
|
|
1274
|
-
const sharedSecret = x25519.getSharedSecret(ephemeralPriv, serverPubBytes);
|
|
1275
|
-
const salt = new TextEncoder().encode(sessionId);
|
|
1276
|
-
const info = new TextEncoder().encode("antz-transit-v1");
|
|
1277
|
-
const sessionKey = hkdf(sha256, sharedSecret, salt, info, 32);
|
|
1278
|
-
return Promise.resolve(sessionKey);
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
function bufToB642(buf) {
|
|
1282
|
-
const bytes = new Uint8Array(buf);
|
|
1283
|
-
let str = "";
|
|
1284
|
-
bytes.forEach((b) => {
|
|
1285
|
-
str += String.fromCharCode(b);
|
|
1286
|
-
});
|
|
1287
|
-
return btoa(str);
|
|
1288
|
-
}
|
|
1289
|
-
function b64ToBuf2(b64) {
|
|
1290
|
-
const bin = atob(b64);
|
|
1291
|
-
const buf = new Uint8Array(bin.length);
|
|
1292
|
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1293
|
-
return buf.buffer;
|
|
1294
|
-
}
|
|
1295
|
-
function uint8ToBase64(bytes) {
|
|
1296
|
-
let str = "";
|
|
1297
|
-
bytes.forEach((b) => {
|
|
1298
|
-
str += String.fromCharCode(b);
|
|
1299
|
-
});
|
|
1300
|
-
return btoa(str);
|
|
1301
|
-
}
|
|
1302
|
-
function base64ToUint82(b64) {
|
|
1303
|
-
const bin = atob(b64);
|
|
1304
|
-
const buf = new Uint8Array(bin.length);
|
|
1305
|
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i);
|
|
1306
|
-
return buf;
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
// src/socket/socket.ts
|
|
1310
1468
|
var _socket = null;
|
|
1311
1469
|
var _socketProxy = null;
|
|
1312
1470
|
var _connectingPromise = null;
|
|
@@ -1403,28 +1561,52 @@ async function _doConnect(config, getToken) {
|
|
|
1403
1561
|
};
|
|
1404
1562
|
let boundDeriveSessionKey = null;
|
|
1405
1563
|
if (config.transitEncryption) {
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1564
|
+
const inFlight = getTransitHandshakePromise();
|
|
1565
|
+
if (inFlight) await inFlight;
|
|
1566
|
+
const existingSession = getTransitSession();
|
|
1567
|
+
let httpsSession = null;
|
|
1568
|
+
if (existingSession?.enabled && existingSession.sessionId) {
|
|
1569
|
+
httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
|
|
1570
|
+
} else {
|
|
1571
|
+
httpsSession = await createRestTransitSession(config.apiUrl);
|
|
1572
|
+
if (httpsSession) {
|
|
1573
|
+
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1574
|
+
setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (httpsSession) {
|
|
1578
|
+
socketHandshakeAuth["transitSessionId"] = httpsSession.sessionId;
|
|
1579
|
+
boundDeriveSessionKey = null;
|
|
1580
|
+
} else {
|
|
1581
|
+
try {
|
|
1582
|
+
const serverKeys = await fetchServerKeys(config.apiUrl);
|
|
1583
|
+
if (!serverKeys.enabled) {
|
|
1584
|
+
throw new AntzChatError(
|
|
1585
|
+
"TRANSIT_MISMATCH",
|
|
1586
|
+
"[AntzChat] Transit encryption mismatch: SDK has transitEncryption=true but server has TRANSIT_ENCRYPTION_ENABLED=false. Align the config on both sides.",
|
|
1587
|
+
false,
|
|
1588
|
+
{ sdkEnabled: true, serverEnabled: false }
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1592
|
+
boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
throw err;
|
|
1412
1595
|
}
|
|
1413
|
-
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1414
|
-
boundDeriveSessionKey = await performHandshake(algo, serverKeys, socketHandshakeAuth);
|
|
1415
|
-
} catch (err) {
|
|
1416
|
-
throw err;
|
|
1417
1596
|
}
|
|
1418
1597
|
} else {
|
|
1419
1598
|
try {
|
|
1420
1599
|
const serverKeys = await fetchServerKeys(config.apiUrl);
|
|
1421
1600
|
if (serverKeys.enabled) {
|
|
1422
|
-
throw new
|
|
1423
|
-
"
|
|
1601
|
+
throw new AntzChatError(
|
|
1602
|
+
"TRANSIT_MISMATCH",
|
|
1603
|
+
"[AntzChat] Transit encryption mismatch: SDK has transitEncryption=false but server has TRANSIT_ENCRYPTION_ENABLED=true. Align the config on both sides.",
|
|
1604
|
+
false,
|
|
1605
|
+
{ sdkEnabled: false, serverEnabled: true }
|
|
1424
1606
|
);
|
|
1425
1607
|
}
|
|
1426
1608
|
} catch (err) {
|
|
1427
|
-
if (err
|
|
1609
|
+
if (err?.code === "TRANSIT_MISMATCH") throw err;
|
|
1428
1610
|
}
|
|
1429
1611
|
}
|
|
1430
1612
|
_socket = (0, import_socket.io)(`${config.socketOrigin}/chat`, {
|
|
@@ -1568,14 +1750,10 @@ async function drainSendQueue(conversationId) {
|
|
|
1568
1750
|
while (queue.length > 0) {
|
|
1569
1751
|
const entry = queue.shift();
|
|
1570
1752
|
if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
|
|
1571
|
-
entry.reject(new
|
|
1753
|
+
entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
|
|
1572
1754
|
continue;
|
|
1573
1755
|
}
|
|
1574
|
-
|
|
1575
|
-
entry.resolve(await entry.run());
|
|
1576
|
-
} catch (e) {
|
|
1577
|
-
entry.reject(e);
|
|
1578
|
-
}
|
|
1756
|
+
entry.run().then(entry.resolve).catch(entry.reject);
|
|
1579
1757
|
}
|
|
1580
1758
|
sendQueues.delete(conversationId);
|
|
1581
1759
|
sendQueueRunning.delete(conversationId);
|
|
@@ -1585,7 +1763,7 @@ function queueSendMessage(payload) {
|
|
|
1585
1763
|
if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);
|
|
1586
1764
|
const queue = sendQueues.get(conversationId);
|
|
1587
1765
|
if (queue.length >= QUEUE_MAX_SIZE) {
|
|
1588
|
-
return Promise.reject(new
|
|
1766
|
+
return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
|
|
1589
1767
|
}
|
|
1590
1768
|
return new Promise((resolve, reject) => {
|
|
1591
1769
|
queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
|
|
@@ -1596,7 +1774,7 @@ function waitForReconnect() {
|
|
|
1596
1774
|
return new Promise((resolve, reject) => {
|
|
1597
1775
|
const timer = setTimeout(() => {
|
|
1598
1776
|
unsubscribe();
|
|
1599
|
-
reject(new
|
|
1777
|
+
reject(new AntzChatNetworkError("Socket reconnect timeout", "SOCKET_TIMEOUT"));
|
|
1600
1778
|
}, RECONNECT_WAIT_TIMEOUT);
|
|
1601
1779
|
const unsubscribe = onSocketStatus((status) => {
|
|
1602
1780
|
if (status === "connected") {
|
|
@@ -1606,7 +1784,7 @@ function waitForReconnect() {
|
|
|
1606
1784
|
} else if (status === "error") {
|
|
1607
1785
|
clearTimeout(timer);
|
|
1608
1786
|
unsubscribe();
|
|
1609
|
-
reject(new
|
|
1787
|
+
reject(new AntzChatNetworkError("Socket reconnect failed", "SOCKET_NOT_CONNECTED"));
|
|
1610
1788
|
}
|
|
1611
1789
|
});
|
|
1612
1790
|
});
|
|
@@ -1617,13 +1795,15 @@ async function withAck(event, payload) {
|
|
|
1617
1795
|
await waitForReconnect();
|
|
1618
1796
|
socket = tryGetSocket();
|
|
1619
1797
|
}
|
|
1620
|
-
if (!socket) return Promise.reject(new
|
|
1798
|
+
if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
|
|
1621
1799
|
return new Promise((resolve, reject) => {
|
|
1622
|
-
|
|
1800
|
+
let timer;
|
|
1623
1801
|
secureEmit(socket, event, payload, (response) => {
|
|
1624
1802
|
clearTimeout(timer);
|
|
1625
1803
|
resolve(response);
|
|
1626
|
-
})
|
|
1804
|
+
}).then(() => {
|
|
1805
|
+
timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
|
|
1806
|
+
}).catch(reject);
|
|
1627
1807
|
});
|
|
1628
1808
|
}
|
|
1629
1809
|
function fireAndForget(event, payload) {
|
|
@@ -1650,6 +1830,9 @@ var socketEmit = {
|
|
|
1650
1830
|
deleteMessageForMe(messageId) {
|
|
1651
1831
|
return withAck("delete_message_for_me", { messageId });
|
|
1652
1832
|
},
|
|
1833
|
+
clearChat(conversationId) {
|
|
1834
|
+
return withAck("clear_chat_for_me", { conversationId });
|
|
1835
|
+
},
|
|
1653
1836
|
addReaction(messageId, emoji) {
|
|
1654
1837
|
return withAck("add_reaction", { messageId, emoji });
|
|
1655
1838
|
},
|
|
@@ -1673,7 +1856,7 @@ var socketEmit = {
|
|
|
1673
1856
|
const socket = tryGetSocket();
|
|
1674
1857
|
if (!socket) return Promise.resolve([]);
|
|
1675
1858
|
return new Promise((resolve, reject) => {
|
|
1676
|
-
|
|
1859
|
+
let timer;
|
|
1677
1860
|
secureEmit(socket, "get_online_users", { userIds }, (response) => {
|
|
1678
1861
|
clearTimeout(timer);
|
|
1679
1862
|
if (response && typeof response === "object" && "onlineStatus" in response) {
|
|
@@ -1684,7 +1867,9 @@ var socketEmit = {
|
|
|
1684
1867
|
} else {
|
|
1685
1868
|
resolve([]);
|
|
1686
1869
|
}
|
|
1687
|
-
})
|
|
1870
|
+
}).then(() => {
|
|
1871
|
+
timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
|
|
1872
|
+
}).catch(reject);
|
|
1688
1873
|
});
|
|
1689
1874
|
},
|
|
1690
1875
|
getTypingUsers(conversationId) {
|
|
@@ -1746,24 +1931,40 @@ var AntzChatClient = class {
|
|
|
1746
1931
|
};
|
|
1747
1932
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1748
1933
|
0 && (module.exports = {
|
|
1934
|
+
AntzChatAuthError,
|
|
1749
1935
|
AntzChatClient,
|
|
1936
|
+
AntzChatError,
|
|
1937
|
+
AntzChatNetworkError,
|
|
1938
|
+
AntzChatPermissionError,
|
|
1939
|
+
AntzChatServerError,
|
|
1940
|
+
AntzChatValidationError,
|
|
1750
1941
|
appConfigApi,
|
|
1751
1942
|
authApi,
|
|
1752
1943
|
connectSocket,
|
|
1753
1944
|
conversationsApi,
|
|
1754
1945
|
createAuthStore,
|
|
1946
|
+
createRestTransitSession,
|
|
1947
|
+
decryptPayload,
|
|
1755
1948
|
devicesApi,
|
|
1756
1949
|
disconnectSocket,
|
|
1950
|
+
encryptPayload,
|
|
1951
|
+
fetchServerKeys,
|
|
1952
|
+
generateEphemeralKey,
|
|
1757
1953
|
getApiClient,
|
|
1758
1954
|
getAuthStore,
|
|
1759
1955
|
getCompressionStrategy,
|
|
1956
|
+
getSessionId,
|
|
1957
|
+
getSessionKey,
|
|
1760
1958
|
getSocket,
|
|
1761
1959
|
getSocketStatus,
|
|
1762
1960
|
initApiClient,
|
|
1763
1961
|
initAuthStore,
|
|
1962
|
+
isTransitEnvelope,
|
|
1764
1963
|
messagesApi,
|
|
1964
|
+
normalizeAxiosError,
|
|
1765
1965
|
normalizeConversation,
|
|
1766
1966
|
onSocketStatus,
|
|
1967
|
+
performHandshake,
|
|
1767
1968
|
reconnectSocket,
|
|
1768
1969
|
refreshSocketAuth,
|
|
1769
1970
|
resetAuthStore,
|