@antzsoft/chat-core 1.4.4 → 1.4.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 +64 -1
- package/dist/{chunk-XTQYF5HU.js → chunk-U637W5MD.js} +147 -28
- package/dist/chunk-U637W5MD.js.map +1 -0
- package/dist/index.cjs +249 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -5
- package/dist/index.d.ts +60 -5
- package/dist/index.js +117 -48
- package/dist/index.js.map +1 -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-C8V7aVum.d.cts → storage-DlxfLVqx.d.cts} +21 -1
- package/dist/{storage-C8V7aVum.d.ts → storage-DlxfLVqx.d.ts} +21 -1
- package/docs/integration-guide.html +151 -4
- package/package.json +1 -1
- package/dist/chunk-XTQYF5HU.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -111,6 +111,7 @@ __export(src_exports, {
|
|
|
111
111
|
HIGHLY_FORWARDED_DEPTH_THRESHOLD: () => HIGHLY_FORWARDED_DEPTH_THRESHOLD,
|
|
112
112
|
MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
|
|
113
113
|
MENTION_ALL_ID: () => MENTION_ALL_ID,
|
|
114
|
+
TransitRateLimitedError: () => TransitRateLimitedError,
|
|
114
115
|
appConfigApi: () => appConfigApi,
|
|
115
116
|
authApi: () => authApi,
|
|
116
117
|
buildMentionText: () => buildMentionText,
|
|
@@ -134,6 +135,7 @@ __export(src_exports, {
|
|
|
134
135
|
getSocketStatus: () => getSocketStatus,
|
|
135
136
|
initApiClient: () => initApiClient,
|
|
136
137
|
initAuthStore: () => initAuthStore,
|
|
138
|
+
isApiClientConfigured: () => isApiClientConfigured,
|
|
137
139
|
isMentionAll: () => isMentionAll,
|
|
138
140
|
isTransitEnvelope: () => isTransitEnvelope,
|
|
139
141
|
messagesApi: () => messagesApi,
|
|
@@ -142,13 +144,16 @@ __export(src_exports, {
|
|
|
142
144
|
onSocketStatus: () => onSocketStatus,
|
|
143
145
|
parseMentions: () => parseMentions,
|
|
144
146
|
performHandshake: () => performHandshake,
|
|
147
|
+
readRetryAfterMs: () => readRetryAfterMs,
|
|
145
148
|
reconnectSocket: () => reconnectSocket,
|
|
146
149
|
refreshSocketAuth: () => refreshSocketAuth,
|
|
147
150
|
renderMentionParts: () => renderMentionParts,
|
|
148
151
|
resetAuthStore: () => resetAuthStore,
|
|
152
|
+
resetTrackedRooms: () => resetTrackedRooms,
|
|
149
153
|
resolveConfig: () => resolveConfig,
|
|
150
154
|
resolveSystemMessageText: () => resolveSystemMessageText,
|
|
151
155
|
setApiClientInstance: () => setApiClientInstance,
|
|
156
|
+
setAuthReadyPromise: () => setAuthReadyPromise,
|
|
152
157
|
setTransitSession: () => setTransitSession,
|
|
153
158
|
socketEmit: () => socketEmit,
|
|
154
159
|
storageApi: () => storageApi,
|
|
@@ -214,6 +219,7 @@ function resolveConfig(config) {
|
|
|
214
219
|
compressDocuments: config.compression?.compressDocuments ?? true
|
|
215
220
|
},
|
|
216
221
|
persistStorage: config.persistStorage,
|
|
222
|
+
onSendError: config.onSendError,
|
|
217
223
|
messagePageSize: config.messagePageSize ?? 40,
|
|
218
224
|
starredMessagePageSize: config.starredMessagePageSize ?? 30,
|
|
219
225
|
searchPageSize: config.searchPageSize ?? 50
|
|
@@ -386,7 +392,8 @@ function getState() {
|
|
|
386
392
|
sessionEverEstablished: false,
|
|
387
393
|
readyResolve: null,
|
|
388
394
|
readyPromise: null,
|
|
389
|
-
transitConfigured: null
|
|
395
|
+
transitConfigured: null,
|
|
396
|
+
readyListeners: /* @__PURE__ */ new Set()
|
|
390
397
|
};
|
|
391
398
|
}
|
|
392
399
|
return g[_KEY];
|
|
@@ -399,11 +406,13 @@ function configureTransit(enabled) {
|
|
|
399
406
|
s.readyResolve = null;
|
|
400
407
|
}
|
|
401
408
|
}
|
|
409
|
+
function configureTransitIfUnset(enabled) {
|
|
410
|
+
if (getState().transitConfigured === null) configureTransit(enabled);
|
|
411
|
+
}
|
|
402
412
|
function waitForTransitReady() {
|
|
403
413
|
const s = getState();
|
|
404
414
|
if (!s.transitConfigured) return Promise.resolve();
|
|
405
415
|
if (s.session) return Promise.resolve();
|
|
406
|
-
if (s.sessionEverEstablished) return Promise.resolve();
|
|
407
416
|
if (!s.readyPromise) {
|
|
408
417
|
s.readyPromise = new Promise((resolve) => {
|
|
409
418
|
s.readyResolve = resolve;
|
|
@@ -417,6 +426,29 @@ function setTransitSession(session) {
|
|
|
417
426
|
s.sessionEverEstablished = true;
|
|
418
427
|
s.readyResolve?.();
|
|
419
428
|
s.readyResolve = null;
|
|
429
|
+
s.readyListeners.forEach((fn) => {
|
|
430
|
+
try {
|
|
431
|
+
fn();
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function onTransitReady(listener) {
|
|
437
|
+
const s = getState();
|
|
438
|
+
s.readyListeners.add(listener);
|
|
439
|
+
return () => {
|
|
440
|
+
s.readyListeners.delete(listener);
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async function awaitTransitReadyOr(timeoutMs) {
|
|
444
|
+
const s = getState();
|
|
445
|
+
if (!s.transitConfigured || s.session) return true;
|
|
446
|
+
await Promise.race([
|
|
447
|
+
waitForTransitReady(),
|
|
448
|
+
new Promise((r) => setTimeout(r, timeoutMs))
|
|
449
|
+
]);
|
|
450
|
+
const now = getState();
|
|
451
|
+
return Boolean(now.session) || now.transitConfigured !== true;
|
|
420
452
|
}
|
|
421
453
|
function getTransitSession() {
|
|
422
454
|
return getState().session;
|
|
@@ -424,12 +456,16 @@ function getTransitSession() {
|
|
|
424
456
|
function clearTransitSession() {
|
|
425
457
|
const s = getState();
|
|
426
458
|
s.session = null;
|
|
459
|
+
s.sessionEverEstablished = false;
|
|
427
460
|
s.readyPromise = null;
|
|
428
461
|
s.readyResolve = null;
|
|
429
462
|
}
|
|
430
463
|
function isTransitEnabled() {
|
|
431
464
|
return getState().session?.enabled === true;
|
|
432
465
|
}
|
|
466
|
+
function isTransitRequired() {
|
|
467
|
+
return getState().transitConfigured === true;
|
|
468
|
+
}
|
|
433
469
|
function getSessionKey() {
|
|
434
470
|
return getState().session?.sessionKey ?? null;
|
|
435
471
|
}
|
|
@@ -458,11 +494,36 @@ function resetAlgoCache() {
|
|
|
458
494
|
}
|
|
459
495
|
|
|
460
496
|
// src/crypto/handshake.ts
|
|
497
|
+
function readRetryAfterMs(headers) {
|
|
498
|
+
const parse = (raw) => {
|
|
499
|
+
if (!raw) return void 0;
|
|
500
|
+
const secs = Number(raw);
|
|
501
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
502
|
+
const when = Date.parse(raw);
|
|
503
|
+
return Number.isNaN(when) ? void 0 : Math.max(0, when - Date.now());
|
|
504
|
+
};
|
|
505
|
+
const found = ["Retry-After-identity", "Retry-After-ip", "Retry-After"].map((name) => parse(headers.get(name))).filter((ms) => ms != null);
|
|
506
|
+
return found.length > 0 ? Math.max(...found) : void 0;
|
|
507
|
+
}
|
|
508
|
+
var TransitRateLimitedError = class extends Error {
|
|
509
|
+
constructor(retryAfterMs) {
|
|
510
|
+
super("[AntzChat] transit handshake rate-limited (429)");
|
|
511
|
+
this.name = "TransitRateLimitedError";
|
|
512
|
+
this.retryAfterMs = retryAfterMs;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
function identityHeaders(identity) {
|
|
516
|
+
const headers = {};
|
|
517
|
+
if (identity?.userId) headers["x-user-id"] = identity.userId;
|
|
518
|
+
if (identity?.tenantId) headers["X-Tenant-ID"] = identity.tenantId;
|
|
519
|
+
return headers;
|
|
520
|
+
}
|
|
461
521
|
function hasWebCrypto2() {
|
|
462
522
|
return typeof globalThis.crypto?.subtle !== "undefined";
|
|
463
523
|
}
|
|
464
|
-
async function fetchServerKeys(apiUrl) {
|
|
465
|
-
const res = await fetch(`${apiUrl}/crypto/pubkey
|
|
524
|
+
async function fetchServerKeys(apiUrl, identity) {
|
|
525
|
+
const res = await fetch(`${apiUrl}/crypto/pubkey`, { headers: identityHeaders(identity) });
|
|
526
|
+
if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
|
|
466
527
|
if (!res.ok) throw new Error(`[AntzChat] Failed to fetch server public key: ${res.status}`);
|
|
467
528
|
const body = await res.json();
|
|
468
529
|
return body?.data ?? body;
|
|
@@ -473,24 +534,26 @@ async function generateEphemeralKey(algo, serverKeys) {
|
|
|
473
534
|
}
|
|
474
535
|
return generateNobleEphemeralKey(serverKeys);
|
|
475
536
|
}
|
|
476
|
-
async function createRestTransitSession(apiUrl) {
|
|
537
|
+
async function createRestTransitSession(apiUrl, identity) {
|
|
477
538
|
try {
|
|
478
|
-
const serverKeys = await fetchServerKeys(apiUrl);
|
|
539
|
+
const serverKeys = await fetchServerKeys(apiUrl, identity);
|
|
479
540
|
if (!serverKeys.enabled) return null;
|
|
480
541
|
const algo = hasWebCrypto2() ? await detectTransitAlgo() : "x25519";
|
|
481
542
|
const { ephemeralPubB64, deriveSessionKey } = await generateEphemeralKey(algo, serverKeys);
|
|
482
543
|
const res = await fetch(`${apiUrl}/crypto/session`, {
|
|
483
544
|
method: "POST",
|
|
484
|
-
headers: { "Content-Type": "application/json" },
|
|
545
|
+
headers: { "Content-Type": "application/json", ...identityHeaders(identity) },
|
|
485
546
|
body: JSON.stringify({ ephemeralPub: ephemeralPubB64, algo })
|
|
486
547
|
});
|
|
548
|
+
if (res.status === 429) throw new TransitRateLimitedError(readRetryAfterMs(res.headers));
|
|
487
549
|
if (!res.ok) return null;
|
|
488
550
|
const body = await res.json();
|
|
489
551
|
const sessionId = (body?.data ?? body)?.sessionId ?? body?.sessionId;
|
|
490
552
|
if (!sessionId) return null;
|
|
491
553
|
const sessionKey = await deriveSessionKey(sessionId);
|
|
492
554
|
return { sessionId, sessionKey };
|
|
493
|
-
} catch {
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (err instanceof TransitRateLimitedError) throw err;
|
|
494
557
|
return null;
|
|
495
558
|
}
|
|
496
559
|
}
|
|
@@ -668,13 +731,72 @@ function normalizeAxiosError(error) {
|
|
|
668
731
|
}
|
|
669
732
|
|
|
670
733
|
// src/api/client.ts
|
|
734
|
+
var TRANSIT_GATE_MAX_WAIT_MS = 3e4;
|
|
671
735
|
var _tokenStore = null;
|
|
672
736
|
var _config = null;
|
|
673
737
|
var _avatarSent = false;
|
|
674
738
|
var _transitHandshakePromise = null;
|
|
739
|
+
var _authReadyPromise = null;
|
|
675
740
|
function getTransitHandshakePromise() {
|
|
676
741
|
return _transitHandshakePromise;
|
|
677
742
|
}
|
|
743
|
+
function setAuthReadyPromise(promise) {
|
|
744
|
+
_authReadyPromise = promise;
|
|
745
|
+
}
|
|
746
|
+
function isApiClientConfigured() {
|
|
747
|
+
return _config !== null;
|
|
748
|
+
}
|
|
749
|
+
function ensureRestTransitHandshake() {
|
|
750
|
+
if (!_config?.transitEncryption || getTransitSession() || _transitHandshakePromise) return;
|
|
751
|
+
const apiUrl = _config.apiUrl;
|
|
752
|
+
_transitHandshakePromise = (async () => {
|
|
753
|
+
try {
|
|
754
|
+
const MAX_FAILURES = 5;
|
|
755
|
+
const BACKSTOP_MS = 2 * 6e4;
|
|
756
|
+
const deadline = Date.now() + BACKSTOP_MS;
|
|
757
|
+
let failures = 0;
|
|
758
|
+
let rateLimitHits = 0;
|
|
759
|
+
while (failures < MAX_FAILURES && Date.now() < deadline) {
|
|
760
|
+
if (getTransitSession()) return;
|
|
761
|
+
let waitMs;
|
|
762
|
+
try {
|
|
763
|
+
const identity = { userId: _config?.userId, tenantId: _config?.tenantId };
|
|
764
|
+
const keys = await fetchServerKeys(apiUrl, identity);
|
|
765
|
+
if (!keys?.enabled) {
|
|
766
|
+
configureTransit(false);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const session = await createRestTransitSession(apiUrl, identity);
|
|
770
|
+
if (session && !getTransitSession()) {
|
|
771
|
+
const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
|
|
772
|
+
setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
failures++;
|
|
776
|
+
waitMs = Math.min(500 * 2 ** failures, 8e3);
|
|
777
|
+
} catch (err) {
|
|
778
|
+
if (err instanceof TransitRateLimitedError) {
|
|
779
|
+
const blind = Math.min(15e3 * 2 ** rateLimitHits, 6e4);
|
|
780
|
+
waitMs = err.retryAfterMs != null ? Math.min(Math.max(err.retryAfterMs, 1e3), 6e4) : blind;
|
|
781
|
+
rateLimitHits++;
|
|
782
|
+
console.warn(
|
|
783
|
+
`[AntzChat] transit handshake rate-limited (429) \u2014 retrying in ${Math.round(waitMs / 1e3)}s${err.retryAfterMs != null ? " (per Retry-After)" : ""}.`
|
|
784
|
+
);
|
|
785
|
+
} else {
|
|
786
|
+
failures++;
|
|
787
|
+
waitMs = Math.min(500 * 2 ** failures, 8e3);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
791
|
+
}
|
|
792
|
+
console.error(
|
|
793
|
+
"[AntzChat] transit handshake could not establish a session \u2014 chat requests stay gated until one succeeds (server requires transit)."
|
|
794
|
+
);
|
|
795
|
+
} finally {
|
|
796
|
+
_transitHandshakePromise = null;
|
|
797
|
+
}
|
|
798
|
+
})();
|
|
799
|
+
}
|
|
678
800
|
function initApiClient(config, tokenStore) {
|
|
679
801
|
_config = config;
|
|
680
802
|
_tokenStore = tokenStore;
|
|
@@ -684,24 +806,9 @@ function initApiClient(config, tokenStore) {
|
|
|
684
806
|
headers: { "Content-Type": "application/json" }
|
|
685
807
|
});
|
|
686
808
|
configureTransit(config.transitEncryption);
|
|
687
|
-
|
|
688
|
-
_transitHandshakePromise = (async () => {
|
|
689
|
-
try {
|
|
690
|
-
const session = await createRestTransitSession(config.apiUrl);
|
|
691
|
-
if (session && !getTransitSession()) {
|
|
692
|
-
const algo = typeof globalThis.crypto?.subtle !== "undefined" ? await detectTransitAlgo() : "x25519";
|
|
693
|
-
setTransitSession({ sessionKey: session.sessionKey, algo, sessionId: session.sessionId, enabled: true });
|
|
694
|
-
} else if (!session) {
|
|
695
|
-
configureTransit(false);
|
|
696
|
-
}
|
|
697
|
-
} catch {
|
|
698
|
-
configureTransit(false);
|
|
699
|
-
} finally {
|
|
700
|
-
_transitHandshakePromise = null;
|
|
701
|
-
}
|
|
702
|
-
})();
|
|
703
|
-
}
|
|
809
|
+
ensureRestTransitHandshake();
|
|
704
810
|
client.interceptors.request.use(async (req) => {
|
|
811
|
+
if (_authReadyPromise) await _authReadyPromise;
|
|
705
812
|
const token = _tokenStore?.getAccessToken();
|
|
706
813
|
if (token) req.headers["Authorization"] = `Bearer ${token}`;
|
|
707
814
|
if (_config?.userId) req.headers["x-user-id"] = _config.userId;
|
|
@@ -711,7 +818,17 @@ function initApiClient(config, tokenStore) {
|
|
|
711
818
|
else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
|
|
712
819
|
_avatarSent = true;
|
|
713
820
|
}
|
|
714
|
-
if (
|
|
821
|
+
if (_config?.transitEncryption) {
|
|
822
|
+
if (!getTransitSession()) ensureRestTransitHandshake();
|
|
823
|
+
const ready = await awaitTransitReadyOr(TRANSIT_GATE_MAX_WAIT_MS);
|
|
824
|
+
if (!ready) {
|
|
825
|
+
throw new AntzChatNetworkError(
|
|
826
|
+
"Secure channel to chat server not established \u2014 request not sent. It will retry automatically.",
|
|
827
|
+
"TRANSIT_NOT_READY",
|
|
828
|
+
{ url: req.url }
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
715
832
|
if (isTransitEnabled()) {
|
|
716
833
|
const sessionId = getSessionId();
|
|
717
834
|
const key = getSessionKey();
|
|
@@ -1035,7 +1152,6 @@ var _getToken = null;
|
|
|
1035
1152
|
var _userId;
|
|
1036
1153
|
var _tenantId;
|
|
1037
1154
|
var _config2 = null;
|
|
1038
|
-
var _preservingSession = false;
|
|
1039
1155
|
function setStatus(s) {
|
|
1040
1156
|
_status = s;
|
|
1041
1157
|
_statusListeners.forEach((l) => l(s));
|
|
@@ -1054,29 +1170,48 @@ function onSocketStatus(listener) {
|
|
|
1054
1170
|
_statusListeners.add(listener);
|
|
1055
1171
|
return () => _statusListeners.delete(listener);
|
|
1056
1172
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
ack(decrypted);
|
|
1068
|
-
} catch {
|
|
1069
|
-
ack(encryptedResponse);
|
|
1070
|
-
}
|
|
1071
|
-
} else {
|
|
1072
|
-
ack(encryptedResponse);
|
|
1073
|
-
}
|
|
1074
|
-
});
|
|
1173
|
+
var SECURE_EMIT_WAIT_MS = 4e3;
|
|
1174
|
+
function emitEncrypted(socket, event, envelope, key, ack) {
|
|
1175
|
+
if (ack) {
|
|
1176
|
+
socket.emit(event, envelope, async (encryptedResponse) => {
|
|
1177
|
+
if (isTransitEnvelope(encryptedResponse)) {
|
|
1178
|
+
try {
|
|
1179
|
+
ack(await decryptPayload(encryptedResponse, key));
|
|
1180
|
+
} catch {
|
|
1181
|
+
ack(encryptedResponse);
|
|
1182
|
+
}
|
|
1075
1183
|
} else {
|
|
1076
|
-
|
|
1184
|
+
ack(encryptedResponse);
|
|
1077
1185
|
}
|
|
1186
|
+
});
|
|
1187
|
+
} else {
|
|
1188
|
+
socket.emit(event, envelope);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async function secureEmit(socket, event, payload, ack) {
|
|
1192
|
+
let key = getSessionKey();
|
|
1193
|
+
if (key && isTransitEnabled()) {
|
|
1194
|
+
emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (isTransitRequired()) {
|
|
1198
|
+
try {
|
|
1199
|
+
ensureRestTransitHandshake();
|
|
1200
|
+
} catch {
|
|
1201
|
+
}
|
|
1202
|
+
const ok = await awaitTransitReadyOr(SECURE_EMIT_WAIT_MS);
|
|
1203
|
+
key = getSessionKey();
|
|
1204
|
+
if (ok && key && isTransitEnabled()) {
|
|
1205
|
+
emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
|
|
1078
1206
|
return;
|
|
1079
1207
|
}
|
|
1208
|
+
if (isTransitRequired()) {
|
|
1209
|
+
throw new AntzChatNetworkError(
|
|
1210
|
+
`Transit encryption required but no session established \u2014 "${event}" not sent`,
|
|
1211
|
+
"TRANSIT_NOT_READY",
|
|
1212
|
+
{ event }
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1080
1215
|
}
|
|
1081
1216
|
if (ack) {
|
|
1082
1217
|
socket.emit(event, payload, ack);
|
|
@@ -1102,6 +1237,28 @@ function secureOn(socket, event, handler) {
|
|
|
1102
1237
|
handler(raw);
|
|
1103
1238
|
});
|
|
1104
1239
|
}
|
|
1240
|
+
var _joinedRooms = /* @__PURE__ */ new Set();
|
|
1241
|
+
function trackRoomJoin(conversationId) {
|
|
1242
|
+
_joinedRooms.add(conversationId);
|
|
1243
|
+
}
|
|
1244
|
+
function trackRoomLeave(conversationId) {
|
|
1245
|
+
_joinedRooms.delete(conversationId);
|
|
1246
|
+
}
|
|
1247
|
+
function resetTrackedRooms() {
|
|
1248
|
+
_joinedRooms.clear();
|
|
1249
|
+
}
|
|
1250
|
+
function flushJoinedRooms() {
|
|
1251
|
+
const socket = tryGetSocket();
|
|
1252
|
+
if (!socket || _joinedRooms.size === 0) return;
|
|
1253
|
+
for (const conversationId of _joinedRooms) {
|
|
1254
|
+
void secureEmit(socket, "join_room", { conversationId }).catch(() => {
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
onTransitReady(flushJoinedRooms);
|
|
1259
|
+
onSocketStatus((status) => {
|
|
1260
|
+
if (status === "connected") flushJoinedRooms();
|
|
1261
|
+
});
|
|
1105
1262
|
async function connectSocket(config, getToken) {
|
|
1106
1263
|
if (_socket && !_socket.disconnected) return _socket;
|
|
1107
1264
|
if (_connectingPromise) return _connectingPromise;
|
|
@@ -1123,6 +1280,7 @@ async function _doConnect(config, getToken) {
|
|
|
1123
1280
|
...config.avatar?.url && { avatarUrl: config.avatar.url },
|
|
1124
1281
|
...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
|
|
1125
1282
|
};
|
|
1283
|
+
configureTransitIfUnset(config.transitEncryption);
|
|
1126
1284
|
let boundDeriveSessionKey = null;
|
|
1127
1285
|
if (config.transitEncryption) {
|
|
1128
1286
|
const inFlight = getTransitHandshakePromise();
|
|
@@ -1132,7 +1290,7 @@ async function _doConnect(config, getToken) {
|
|
|
1132
1290
|
if (existingSession?.enabled && existingSession.sessionId) {
|
|
1133
1291
|
httpsSession = { sessionId: existingSession.sessionId, sessionKey: existingSession.sessionKey };
|
|
1134
1292
|
} else {
|
|
1135
|
-
httpsSession = await createRestTransitSession(config.apiUrl);
|
|
1293
|
+
httpsSession = await createRestTransitSession(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
|
|
1136
1294
|
if (httpsSession) {
|
|
1137
1295
|
const algo = globalThis.crypto?.subtle ? await detectTransitAlgo() : "x25519";
|
|
1138
1296
|
setTransitSession({ sessionKey: httpsSession.sessionKey, algo, sessionId: httpsSession.sessionId, enabled: true });
|
|
@@ -1143,7 +1301,7 @@ async function _doConnect(config, getToken) {
|
|
|
1143
1301
|
boundDeriveSessionKey = null;
|
|
1144
1302
|
} else {
|
|
1145
1303
|
try {
|
|
1146
|
-
const serverKeys = await fetchServerKeys(config.apiUrl);
|
|
1304
|
+
const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
|
|
1147
1305
|
if (!serverKeys.enabled) {
|
|
1148
1306
|
throw new AntzChatError(
|
|
1149
1307
|
"TRANSIT_MISMATCH",
|
|
@@ -1160,7 +1318,7 @@ async function _doConnect(config, getToken) {
|
|
|
1160
1318
|
}
|
|
1161
1319
|
} else {
|
|
1162
1320
|
try {
|
|
1163
|
-
const serverKeys = await fetchServerKeys(config.apiUrl);
|
|
1321
|
+
const serverKeys = await fetchServerKeys(config.apiUrl, { userId: config.userId, tenantId: config.tenantId });
|
|
1164
1322
|
if (serverKeys.enabled) {
|
|
1165
1323
|
throw new AntzChatError(
|
|
1166
1324
|
"TRANSIT_MISMATCH",
|
|
@@ -1187,7 +1345,6 @@ async function _doConnect(config, getToken) {
|
|
|
1187
1345
|
_socket.on("connect", () => setStatus("connected"));
|
|
1188
1346
|
_socket.on("disconnect", () => {
|
|
1189
1347
|
setStatus("disconnected");
|
|
1190
|
-
if (!_preservingSession) clearTransitSession();
|
|
1191
1348
|
});
|
|
1192
1349
|
_socket.on("connect_error", (err) => {
|
|
1193
1350
|
console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
|
|
@@ -1202,8 +1359,9 @@ async function _doConnect(config, getToken) {
|
|
|
1202
1359
|
resolve();
|
|
1203
1360
|
};
|
|
1204
1361
|
const timeout = setTimeout(() => {
|
|
1205
|
-
console.
|
|
1206
|
-
|
|
1362
|
+
console.error(
|
|
1363
|
+
"[AntzChat] transit_session did not arrive within 5s. Transit stays REQUIRED \u2014 chat requests are gated (not downgraded to plaintext) until the handshake succeeds via REST retry or socket reconnect. Persistent failure = check POST /crypto/session (rate limit / 5xx) and the socket transit middleware."
|
|
1364
|
+
);
|
|
1207
1365
|
done();
|
|
1208
1366
|
}, 5e3);
|
|
1209
1367
|
_socket.on("transit_session", async ({ sessionId }) => {
|
|
@@ -1290,13 +1448,8 @@ function reconnectSocket(token, userId, tenantId) {
|
|
|
1290
1448
|
...tenantId && { tenantId },
|
|
1291
1449
|
transitSessionId: existing.sessionId
|
|
1292
1450
|
};
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
if (_socket.connected) _socket.disconnect();
|
|
1296
|
-
_socket.connect();
|
|
1297
|
-
} finally {
|
|
1298
|
-
_preservingSession = false;
|
|
1299
|
-
}
|
|
1451
|
+
if (_socket.connected) _socket.disconnect();
|
|
1452
|
+
_socket.connect();
|
|
1300
1453
|
return;
|
|
1301
1454
|
}
|
|
1302
1455
|
if (_getToken) {
|
|
@@ -1341,7 +1494,15 @@ async function drainSendQueue(conversationId) {
|
|
|
1341
1494
|
entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
|
|
1342
1495
|
continue;
|
|
1343
1496
|
}
|
|
1344
|
-
|
|
1497
|
+
let acked;
|
|
1498
|
+
try {
|
|
1499
|
+
({ acked } = await emitWithAck("send_message", entry.payload));
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
entry.reject(err);
|
|
1502
|
+
while (queue.length > 0) queue.shift().reject(err);
|
|
1503
|
+
break;
|
|
1504
|
+
}
|
|
1505
|
+
acked.then(entry.resolve).catch(entry.reject);
|
|
1345
1506
|
}
|
|
1346
1507
|
sendQueues.delete(conversationId);
|
|
1347
1508
|
sendQueueRunning.delete(conversationId);
|
|
@@ -1354,7 +1515,7 @@ function queueSendMessage(payload) {
|
|
|
1354
1515
|
return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
|
|
1355
1516
|
}
|
|
1356
1517
|
return new Promise((resolve, reject) => {
|
|
1357
|
-
queue.push({
|
|
1518
|
+
queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });
|
|
1358
1519
|
drainSendQueue(conversationId);
|
|
1359
1520
|
});
|
|
1360
1521
|
}
|
|
@@ -1377,33 +1538,44 @@ function waitForReconnect() {
|
|
|
1377
1538
|
});
|
|
1378
1539
|
});
|
|
1379
1540
|
}
|
|
1380
|
-
async function
|
|
1541
|
+
async function emitWithAck(event, payload) {
|
|
1381
1542
|
let socket = tryGetSocket();
|
|
1382
1543
|
if (!socket) {
|
|
1383
1544
|
await waitForReconnect();
|
|
1384
1545
|
socket = tryGetSocket();
|
|
1385
1546
|
}
|
|
1386
|
-
if (!socket)
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}).then(() => {
|
|
1393
|
-
timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
|
|
1394
|
-
}).catch(reject);
|
|
1547
|
+
if (!socket) throw new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event });
|
|
1548
|
+
let resolveAck;
|
|
1549
|
+
let rejectAck;
|
|
1550
|
+
const acked = new Promise((res, rej) => {
|
|
1551
|
+
resolveAck = res;
|
|
1552
|
+
rejectAck = rej;
|
|
1395
1553
|
});
|
|
1554
|
+
await secureEmit(socket, event, payload, (response) => resolveAck(response));
|
|
1555
|
+
const timer = setTimeout(
|
|
1556
|
+
() => rejectAck(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })),
|
|
1557
|
+
ACK_TIMEOUT
|
|
1558
|
+
);
|
|
1559
|
+
void acked.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
1560
|
+
return { acked };
|
|
1561
|
+
}
|
|
1562
|
+
async function withAck(event, payload) {
|
|
1563
|
+
const { acked } = await emitWithAck(event, payload);
|
|
1564
|
+
return acked;
|
|
1396
1565
|
}
|
|
1397
1566
|
function fireAndForget(event, payload) {
|
|
1398
1567
|
const socket = tryGetSocket();
|
|
1399
1568
|
if (!socket) return;
|
|
1400
|
-
secureEmit(socket, event, payload)
|
|
1569
|
+
secureEmit(socket, event, payload).catch(() => {
|
|
1570
|
+
});
|
|
1401
1571
|
}
|
|
1402
1572
|
var socketEmit = {
|
|
1403
1573
|
joinRoom(conversationId) {
|
|
1574
|
+
trackRoomJoin(conversationId);
|
|
1404
1575
|
fireAndForget("join_room", { conversationId });
|
|
1405
1576
|
},
|
|
1406
1577
|
leaveRoom(conversationId) {
|
|
1578
|
+
trackRoomLeave(conversationId);
|
|
1407
1579
|
fireAndForget("leave_room", { conversationId });
|
|
1408
1580
|
},
|
|
1409
1581
|
sendMessage(payload) {
|
|
@@ -2160,6 +2332,7 @@ var AntzChatClient = class {
|
|
|
2160
2332
|
HIGHLY_FORWARDED_DEPTH_THRESHOLD,
|
|
2161
2333
|
MAX_FORWARD_TARGETS,
|
|
2162
2334
|
MENTION_ALL_ID,
|
|
2335
|
+
TransitRateLimitedError,
|
|
2163
2336
|
appConfigApi,
|
|
2164
2337
|
authApi,
|
|
2165
2338
|
buildMentionText,
|
|
@@ -2183,6 +2356,7 @@ var AntzChatClient = class {
|
|
|
2183
2356
|
getSocketStatus,
|
|
2184
2357
|
initApiClient,
|
|
2185
2358
|
initAuthStore,
|
|
2359
|
+
isApiClientConfigured,
|
|
2186
2360
|
isMentionAll,
|
|
2187
2361
|
isTransitEnvelope,
|
|
2188
2362
|
messagesApi,
|
|
@@ -2191,13 +2365,16 @@ var AntzChatClient = class {
|
|
|
2191
2365
|
onSocketStatus,
|
|
2192
2366
|
parseMentions,
|
|
2193
2367
|
performHandshake,
|
|
2368
|
+
readRetryAfterMs,
|
|
2194
2369
|
reconnectSocket,
|
|
2195
2370
|
refreshSocketAuth,
|
|
2196
2371
|
renderMentionParts,
|
|
2197
2372
|
resetAuthStore,
|
|
2373
|
+
resetTrackedRooms,
|
|
2198
2374
|
resolveConfig,
|
|
2199
2375
|
resolveSystemMessageText,
|
|
2200
2376
|
setApiClientInstance,
|
|
2377
|
+
setAuthReadyPromise,
|
|
2201
2378
|
setTransitSession,
|
|
2202
2379
|
socketEmit,
|
|
2203
2380
|
storageApi,
|