@vex-chat/libvex 5.5.0 → 5.5.1
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 +25 -23
- package/dist/Client.d.ts +103 -103
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +295 -295
- package/dist/Client.js.map +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
- package/dist/__tests__/harness/memory-storage.js +1 -1
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/storage/node/http-agents.d.ts +1 -1
- package/dist/storage/node/http-agents.d.ts.map +1 -1
- package/dist/storage/node/http-agents.js +4 -4
- package/dist/storage/node/http-agents.js.map +1 -1
- package/dist/storage/sqlite.d.ts +8 -8
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +16 -16
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/utils/fipsMailExtra.d.ts +9 -9
- package/dist/utils/fipsMailExtra.d.ts.map +1 -1
- package/dist/utils/fipsMailExtra.js +47 -47
- package/dist/utils/fipsMailExtra.js.map +1 -1
- package/dist/utils/resolveAtRestAesKey.js +1 -1
- package/dist/utils/resolveAtRestAesKey.js.map +1 -1
- package/package.json +134 -152
- package/src/Client.ts +411 -413
- package/src/__tests__/harness/memory-storage.ts +1 -1
- package/src/__tests__/harness/shared-suite.ts +177 -177
- package/src/index.ts +1 -1
- package/src/storage/node/http-agents.ts +7 -7
- package/src/storage/sqlite.ts +23 -23
- package/src/utils/fipsMailExtra.ts +80 -80
- package/src/utils/resolveAtRestAesKey.ts +1 -1
package/dist/Client.js
CHANGED
|
@@ -11,12 +11,49 @@ import * as uuid from "uuid";
|
|
|
11
11
|
import { z } from "zod/v4";
|
|
12
12
|
import { WebSocketAdapter } from "./transport/websocket.js";
|
|
13
13
|
import { decodeFipsInitialExtraV1, decodeFipsSubsequentExtraV1, encodeFipsInitialExtraV1, encodeFipsSubsequentExtraV1, fipsP256AdFromIdentityPubs, fipsP256PreKeySignPayload, isFipsInitialExtraV1, isFipsSubsequentExtraV1, } from "./utils/fipsMailExtra.js";
|
|
14
|
-
function
|
|
15
|
-
|
|
14
|
+
function debugLibvexDm(msg, data) {
|
|
15
|
+
if (!libvexDebugDmEnabled()) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const payload = data ? `${msg} ${JSON.stringify(data)}` : msg;
|
|
19
|
+
// eslint-disable-next-line no-console -- gated by LIBVEX_DEBUG_DM; remove when debugging is done
|
|
20
|
+
console.error(`[libvex:debug-dm] ${payload}`);
|
|
16
21
|
}
|
|
17
22
|
function isRecord(x) {
|
|
18
23
|
return typeof x === "object" && x !== null;
|
|
19
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Set `LIBVEX_DEBUG_DM=1` (e.g. in vitest / shell) to log DM multi-device / X3DH paths.
|
|
27
|
+
* Uses indirect `globalThis` lookup so the bare `process` global never appears in
|
|
28
|
+
* source that the platform-guard plugin scans (browser/RN/Tauri).
|
|
29
|
+
*/
|
|
30
|
+
function libvexDebugDmEnabled() {
|
|
31
|
+
try {
|
|
32
|
+
const g = Object.getOwnPropertyDescriptor(globalThis, "\u0070rocess");
|
|
33
|
+
if (!g) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const proc = typeof g.get === "function" ? g.get() : g.value;
|
|
37
|
+
if (typeof proc !== "object" || proc === null) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const envDesc = Object.getOwnPropertyDescriptor(proc, "env");
|
|
41
|
+
if (!envDesc) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const env = typeof envDesc.get === "function" ? envDesc.get() : envDesc.value;
|
|
45
|
+
if (typeof env !== "object" || env === null) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return Reflect.get(env, "LIBVEX_DEBUG_DM") === "1";
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
20
57
|
/**
|
|
21
58
|
* Spire 5+ JSON error bodies use `{ "error": { "message", "requestId"?, "details"? } }`.
|
|
22
59
|
* Responses are `arraybuffer` — decode UTF-8 and parse for a one-line `Error` message
|
|
@@ -69,43 +106,6 @@ function spireErrorBodyMessage(data, max = 8_000) {
|
|
|
69
106
|
}
|
|
70
107
|
return t.length > max ? t.slice(0, max) + "…" : t;
|
|
71
108
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Set `LIBVEX_DEBUG_DM=1` (e.g. in vitest / shell) to log DM multi-device / X3DH paths.
|
|
74
|
-
* Uses indirect `globalThis` lookup so the bare `process` global never appears in
|
|
75
|
-
* source that the platform-guard plugin scans (browser/RN/Tauri).
|
|
76
|
-
*/
|
|
77
|
-
function libvexDebugDmEnabled() {
|
|
78
|
-
try {
|
|
79
|
-
const g = Object.getOwnPropertyDescriptor(globalThis, "\u0070rocess");
|
|
80
|
-
if (!g) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
const proc = typeof g.get === "function" ? g.get() : g.value;
|
|
84
|
-
if (typeof proc !== "object" || proc === null) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
const envDesc = Object.getOwnPropertyDescriptor(proc, "env");
|
|
88
|
-
if (!envDesc) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
const env = typeof envDesc.get === "function" ? envDesc.get() : envDesc.value;
|
|
92
|
-
if (typeof env !== "object" || env === null) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
return Reflect.get(env, "LIBVEX_DEBUG_DM") === "1";
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
function debugLibvexDm(msg, data) {
|
|
102
|
-
if (!libvexDebugDmEnabled()) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const payload = data ? `${msg} ${JSON.stringify(data)}` : msg;
|
|
106
|
-
// eslint-disable-next-line no-console -- gated by LIBVEX_DEBUG_DM; remove when debugging is done
|
|
107
|
-
console.error(`[libvex:debug-dm] ${payload}`);
|
|
108
|
-
}
|
|
109
109
|
import { msgpack } from "./codec.js";
|
|
110
110
|
import { ActionTokenCodec, AuthResponseCodec, ChannelArrayCodec, ChannelCodec, ConnectResponseCodec, decodeAxios, DeviceArrayCodec, DeviceChallengeCodec, DeviceCodec, DeviceRegistrationResultCodec, EmojiArrayCodec, EmojiCodec, FileSQLCodec, InviteArrayCodec, InviteCodec, KeyBundleCodec, OtkCountCodec, PendingDeviceRequestArrayCodec, PendingDeviceRequestCodec, PermissionArrayCodec, PermissionCodec, ServerArrayCodec, ServerCodec, UserArrayCodec, UserCodec, WhoamiCodec, } from "./codecs.js";
|
|
111
111
|
import { capitalize } from "./utils/capitalize.js";
|
|
@@ -205,8 +205,8 @@ export class Client {
|
|
|
205
205
|
delete: this.deleteDevice.bind(this),
|
|
206
206
|
getRequest: this.getDeviceRegistrationRequest.bind(this),
|
|
207
207
|
listRequests: this.listDeviceRegistrationRequests.bind(this),
|
|
208
|
-
rejectRequest: this.rejectDeviceRequest.bind(this),
|
|
209
208
|
register: this.registerDevice.bind(this),
|
|
209
|
+
rejectRequest: this.rejectDeviceRequest.bind(this),
|
|
210
210
|
retrieve: this.getDeviceByID.bind(this),
|
|
211
211
|
};
|
|
212
212
|
/**
|
|
@@ -409,6 +409,7 @@ export class Client {
|
|
|
409
409
|
*/
|
|
410
410
|
retrieve: this.fetchUser.bind(this),
|
|
411
411
|
};
|
|
412
|
+
cryptoProfile;
|
|
412
413
|
database;
|
|
413
414
|
dbPath;
|
|
414
415
|
device;
|
|
@@ -419,23 +420,18 @@ export class Client {
|
|
|
419
420
|
firstMailFetch = true;
|
|
420
421
|
forwarded = new Set();
|
|
421
422
|
host;
|
|
422
|
-
|
|
423
|
-
* Node-only: per-client HTTP(S) agents (see `init()` + `storage/node/http-agents`).
|
|
424
|
-
* Dropped on `close()` so idle keep-alive sockets do not keep the process alive.
|
|
425
|
-
*/
|
|
426
|
-
nodeHttpAgents;
|
|
423
|
+
http;
|
|
427
424
|
/** Cancels in-flight axios work on `close()` so `postAuth`/`getMail` cannot hang forever. */
|
|
428
425
|
httpAbortController = new AbortController();
|
|
429
|
-
http;
|
|
430
426
|
idKeys;
|
|
431
427
|
isAlive = true;
|
|
432
428
|
mailInterval;
|
|
433
429
|
manuallyClosing = false;
|
|
434
430
|
/**
|
|
435
|
-
*
|
|
436
|
-
* `
|
|
431
|
+
* Node-only: per-client HTTP(S) agents (see `init()` + `storage/node/http-agents`).
|
|
432
|
+
* Dropped on `close()` so idle keep-alive sockets do not keep the process alive.
|
|
437
433
|
*/
|
|
438
|
-
|
|
434
|
+
nodeHttpAgents;
|
|
439
435
|
/* Retrieves the userID with the user identifier.
|
|
440
436
|
user identifier is checked for userID, then signkey,
|
|
441
437
|
and finally falls back to username. */
|
|
@@ -443,6 +439,11 @@ export class Client {
|
|
|
443
439
|
notFoundUsers = new Map();
|
|
444
440
|
options;
|
|
445
441
|
pingInterval = null;
|
|
442
|
+
/**
|
|
443
|
+
* Bumped when the WebSocket is torn down and re-opened so the previous
|
|
444
|
+
* `postAuth` loop exits instead of overlapping a new one.
|
|
445
|
+
*/
|
|
446
|
+
postAuthVersion = 0;
|
|
446
447
|
prefixes;
|
|
447
448
|
reading = false;
|
|
448
449
|
seenMailIDs = new Set();
|
|
@@ -454,7 +455,6 @@ export class Client {
|
|
|
454
455
|
user;
|
|
455
456
|
userRecords = {};
|
|
456
457
|
xKeyRing;
|
|
457
|
-
cryptoProfile;
|
|
458
458
|
constructor(material, options, storage) {
|
|
459
459
|
this.options = options;
|
|
460
460
|
this.cryptoProfile = material.cryptoProfile;
|
|
@@ -617,27 +617,6 @@ export class Client {
|
|
|
617
617
|
static getMnemonic(session) {
|
|
618
618
|
return xMnemonic(xKDF(XUtils.decodeHex(session.fingerprint)));
|
|
619
619
|
}
|
|
620
|
-
/**
|
|
621
|
-
* True when running under Node (has `process.versions`).
|
|
622
|
-
* Uses indirect lookup so the bare `process` global never appears in
|
|
623
|
-
* source that the platform-guard plugin scans.
|
|
624
|
-
*/
|
|
625
|
-
static isNodeRuntime() {
|
|
626
|
-
try {
|
|
627
|
-
const g = Object.getOwnPropertyDescriptor(globalThis, "\u0070rocess");
|
|
628
|
-
if (!g)
|
|
629
|
-
return false;
|
|
630
|
-
const proc = typeof g.get === "function" ? g.get() : g.value;
|
|
631
|
-
if (typeof proc !== "object" || proc === null) {
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
return ("versions" in proc &&
|
|
635
|
-
typeof proc.versions === "object");
|
|
636
|
-
}
|
|
637
|
-
catch {
|
|
638
|
-
return false;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
620
|
/**
|
|
642
621
|
* Browser-safe NODE_ENV accessor.
|
|
643
622
|
* Uses indirect lookup so the bare `process` global never appears in
|
|
@@ -675,12 +654,25 @@ export class Client {
|
|
|
675
654
|
}
|
|
676
655
|
}
|
|
677
656
|
/**
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
657
|
+
* True when running under Node (has `process.versions`).
|
|
658
|
+
* Uses indirect lookup so the bare `process` global never appears in
|
|
659
|
+
* source that the platform-guard plugin scans.
|
|
681
660
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
661
|
+
static isNodeRuntime() {
|
|
662
|
+
try {
|
|
663
|
+
const g = Object.getOwnPropertyDescriptor(globalThis, "\u0070rocess");
|
|
664
|
+
if (!g)
|
|
665
|
+
return false;
|
|
666
|
+
const proc = typeof g.get === "function" ? g.get() : g.value;
|
|
667
|
+
if (typeof proc !== "object" || proc === null) {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
return ("versions" in proc &&
|
|
671
|
+
typeof proc.versions === "object");
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
684
676
|
}
|
|
685
677
|
/**
|
|
686
678
|
* Closes the client — disconnects the WebSocket, shuts down storage,
|
|
@@ -735,57 +727,6 @@ export class Client {
|
|
|
735
727
|
await new Promise((r) => setTimeout(r, 0));
|
|
736
728
|
await this.negotiateOTK();
|
|
737
729
|
}
|
|
738
|
-
/**
|
|
739
|
-
* Tears down the current WebSocket and opens a new one, keeping the same
|
|
740
|
-
* session (user + device in storage). Restarts the post-auth mail loop.
|
|
741
|
-
* Use for long-running processes or e2e where a fresh socket matches a
|
|
742
|
-
* newly-registered second device.
|
|
743
|
-
*/
|
|
744
|
-
async reconnectWebsocket() {
|
|
745
|
-
this.postAuthVersion++;
|
|
746
|
-
if (this.pingInterval) {
|
|
747
|
-
clearInterval(this.pingInterval);
|
|
748
|
-
this.pingInterval = null;
|
|
749
|
-
}
|
|
750
|
-
this.socket.close();
|
|
751
|
-
try {
|
|
752
|
-
await new Promise((resolve, reject) => {
|
|
753
|
-
const t = setTimeout(() => {
|
|
754
|
-
this.off("connected", onC);
|
|
755
|
-
reject(new Error("reconnectWebsocket: timed out waiting for authorized"));
|
|
756
|
-
}, 15_000);
|
|
757
|
-
const onC = () => {
|
|
758
|
-
clearTimeout(t);
|
|
759
|
-
this.off("connected", onC);
|
|
760
|
-
resolve();
|
|
761
|
-
};
|
|
762
|
-
this.on("connected", onC);
|
|
763
|
-
try {
|
|
764
|
-
this.initSocket();
|
|
765
|
-
}
|
|
766
|
-
catch (err) {
|
|
767
|
-
clearTimeout(t);
|
|
768
|
-
this.off("connected", onC);
|
|
769
|
-
const e = err instanceof Error
|
|
770
|
-
? err
|
|
771
|
-
: new Error(String(err), { cause: err });
|
|
772
|
-
reject(e);
|
|
773
|
-
}
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
catch (e) {
|
|
777
|
-
throw e instanceof Error ? e : new Error(String(e), { cause: e });
|
|
778
|
-
}
|
|
779
|
-
await new Promise((r) => setTimeout(r, 0));
|
|
780
|
-
await this.negotiateOTK();
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Triggers an immediate inbox sync by fetching `/mail` once.
|
|
784
|
-
* Useful on mobile foreground resume where background work may pause.
|
|
785
|
-
*/
|
|
786
|
-
async syncInboxNow() {
|
|
787
|
-
await this.getMail();
|
|
788
|
-
}
|
|
789
730
|
/**
|
|
790
731
|
* Delete all local data — message history, encryption sessions, and prekeys.
|
|
791
732
|
* Closes the client afterward. Credentials (keychain) must be cleared by the consumer.
|
|
@@ -911,6 +852,50 @@ export class Client {
|
|
|
911
852
|
this.emitter.once(event, fn, context);
|
|
912
853
|
return this;
|
|
913
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Tears down the current WebSocket and opens a new one, keeping the same
|
|
857
|
+
* session (user + device in storage). Restarts the post-auth mail loop.
|
|
858
|
+
* Use for long-running processes or e2e where a fresh socket matches a
|
|
859
|
+
* newly-registered second device.
|
|
860
|
+
*/
|
|
861
|
+
async reconnectWebsocket() {
|
|
862
|
+
this.postAuthVersion++;
|
|
863
|
+
if (this.pingInterval) {
|
|
864
|
+
clearInterval(this.pingInterval);
|
|
865
|
+
this.pingInterval = null;
|
|
866
|
+
}
|
|
867
|
+
this.socket.close();
|
|
868
|
+
try {
|
|
869
|
+
await new Promise((resolve, reject) => {
|
|
870
|
+
const t = setTimeout(() => {
|
|
871
|
+
this.off("connected", onC);
|
|
872
|
+
reject(new Error("reconnectWebsocket: timed out waiting for authorized"));
|
|
873
|
+
}, 15_000);
|
|
874
|
+
const onC = () => {
|
|
875
|
+
clearTimeout(t);
|
|
876
|
+
this.off("connected", onC);
|
|
877
|
+
resolve();
|
|
878
|
+
};
|
|
879
|
+
this.on("connected", onC);
|
|
880
|
+
try {
|
|
881
|
+
this.initSocket();
|
|
882
|
+
}
|
|
883
|
+
catch (err) {
|
|
884
|
+
clearTimeout(t);
|
|
885
|
+
this.off("connected", onC);
|
|
886
|
+
const e = err instanceof Error
|
|
887
|
+
? err
|
|
888
|
+
: new Error(String(err), { cause: err });
|
|
889
|
+
reject(e);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
catch (e) {
|
|
894
|
+
throw e instanceof Error ? e : new Error(String(e), { cause: e });
|
|
895
|
+
}
|
|
896
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
897
|
+
await this.negotiateOTK();
|
|
898
|
+
}
|
|
914
899
|
/**
|
|
915
900
|
* Registers a new account on the server.
|
|
916
901
|
*
|
|
@@ -968,6 +953,13 @@ export class Client {
|
|
|
968
953
|
this.emitter.removeAllListeners(event);
|
|
969
954
|
return this;
|
|
970
955
|
}
|
|
956
|
+
/**
|
|
957
|
+
* Triggers an immediate inbox sync by fetching `/mail` once.
|
|
958
|
+
* Useful on mobile foreground resume where background work may pause.
|
|
959
|
+
*/
|
|
960
|
+
async syncInboxNow() {
|
|
961
|
+
await this.getMail();
|
|
962
|
+
}
|
|
971
963
|
/**
|
|
972
964
|
* Returns a compact `<username><deviceID>` debug label.
|
|
973
965
|
*/
|
|
@@ -993,6 +985,24 @@ export class Client {
|
|
|
993
985
|
const whoami = decodeAxios(WhoamiCodec, res.data);
|
|
994
986
|
return whoami;
|
|
995
987
|
}
|
|
988
|
+
async approveDeviceRequest(requestID) {
|
|
989
|
+
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
990
|
+
if (!req) {
|
|
991
|
+
throw new Error("Device approval request not found.");
|
|
992
|
+
}
|
|
993
|
+
if (req.status !== "pending") {
|
|
994
|
+
throw new Error("Device approval request is not pending: " + req.status);
|
|
995
|
+
}
|
|
996
|
+
const signed = XUtils.encodeHex(await xSignAsync(XUtils.decodeUTF8(requestID), this.signKeys.secretKey));
|
|
997
|
+
const response = await this.http.post(this.prefixes.HTTP +
|
|
998
|
+
this.host +
|
|
999
|
+
"/user/" +
|
|
1000
|
+
this.getUser().userID +
|
|
1001
|
+
"/devices/requests/" +
|
|
1002
|
+
requestID +
|
|
1003
|
+
"/approve", msgpack.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1004
|
+
return decodeAxios(DeviceCodec, response.data);
|
|
1005
|
+
}
|
|
996
1006
|
censorPreKey(preKey) {
|
|
997
1007
|
if (!preKey.index) {
|
|
998
1008
|
throw new Error("Key index is required.");
|
|
@@ -1073,25 +1083,6 @@ export class Client {
|
|
|
1073
1083
|
const res = await this.http.post(this.getHost() + "/server/" + globalThis.btoa(name));
|
|
1074
1084
|
return decodeAxios(ServerCodec, res.data);
|
|
1075
1085
|
}
|
|
1076
|
-
/**
|
|
1077
|
-
* `xDHAsync` and other helpers in `@vex-chat/crypto` use the process-wide
|
|
1078
|
-
* active profile. When several {@link Client} instances use different
|
|
1079
|
-
* `cryptoProfile` values, scope the global to this instance for the duration
|
|
1080
|
-
* of that crypto work.
|
|
1081
|
-
*/
|
|
1082
|
-
async runWithThisCryptoProfile(fn) {
|
|
1083
|
-
const prev = getCryptoProfile();
|
|
1084
|
-
if (prev === this.cryptoProfile) {
|
|
1085
|
-
return await fn();
|
|
1086
|
-
}
|
|
1087
|
-
setCryptoProfile(this.cryptoProfile);
|
|
1088
|
-
try {
|
|
1089
|
-
return await fn();
|
|
1090
|
-
}
|
|
1091
|
-
finally {
|
|
1092
|
-
setCryptoProfile(prev);
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
1086
|
async createSession(device, user, message, group,
|
|
1096
1087
|
/* this is passed through if the first message is
|
|
1097
1088
|
part of a group message */
|
|
@@ -1244,71 +1235,19 @@ export class Client {
|
|
|
1244
1235
|
});
|
|
1245
1236
|
});
|
|
1246
1237
|
}
|
|
1247
|
-
async deleteChannel(channelID) {
|
|
1248
|
-
await this.http.delete(this.getHost() + "/channel/" + channelID);
|
|
1249
|
-
}
|
|
1250
|
-
async approveDeviceRequest(requestID) {
|
|
1251
|
-
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
1252
|
-
if (!req) {
|
|
1253
|
-
throw new Error("Device approval request not found.");
|
|
1254
|
-
}
|
|
1255
|
-
if (req.status !== "pending") {
|
|
1256
|
-
throw new Error("Device approval request is not pending: " + req.status);
|
|
1257
|
-
}
|
|
1258
|
-
const signed = XUtils.encodeHex(await xSignAsync(XUtils.decodeUTF8(requestID), this.signKeys.secretKey));
|
|
1259
|
-
const response = await this.http.post(this.prefixes.HTTP +
|
|
1260
|
-
this.host +
|
|
1261
|
-
"/user/" +
|
|
1262
|
-
this.getUser().userID +
|
|
1263
|
-
"/devices/requests/" +
|
|
1264
|
-
requestID +
|
|
1265
|
-
"/approve", msgpack.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1266
|
-
return decodeAxios(DeviceCodec, response.data);
|
|
1267
|
-
}
|
|
1268
|
-
async deleteDevice(deviceID) {
|
|
1269
|
-
if (deviceID === this.getDevice().deviceID) {
|
|
1270
|
-
throw new Error("You can't delete the device you're logged in to.");
|
|
1271
|
-
}
|
|
1272
|
-
await this.http.delete(this.prefixes.HTTP +
|
|
1273
|
-
this.host +
|
|
1274
|
-
"/user/" +
|
|
1275
|
-
this.getUser().userID +
|
|
1276
|
-
"/devices/" +
|
|
1277
|
-
deviceID);
|
|
1278
|
-
}
|
|
1279
|
-
async getDeviceRegistrationRequest(requestID) {
|
|
1280
|
-
try {
|
|
1281
|
-
const response = await this.http.get(this.prefixes.HTTP +
|
|
1282
|
-
this.host +
|
|
1283
|
-
"/user/" +
|
|
1284
|
-
this.getUser().userID +
|
|
1285
|
-
"/devices/requests/" +
|
|
1286
|
-
requestID);
|
|
1287
|
-
return decodeAxios(PendingDeviceRequestCodec, response.data);
|
|
1288
|
-
}
|
|
1289
|
-
catch (err) {
|
|
1290
|
-
if (isAxiosError(err) && err.response?.status === 404) {
|
|
1291
|
-
return null;
|
|
1292
|
-
}
|
|
1293
|
-
throw err;
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
async listDeviceRegistrationRequests() {
|
|
1297
|
-
const response = await this.http.get(this.prefixes.HTTP +
|
|
1298
|
-
this.host +
|
|
1299
|
-
"/user/" +
|
|
1300
|
-
this.getUser().userID +
|
|
1301
|
-
"/devices/requests");
|
|
1302
|
-
return decodeAxios(PendingDeviceRequestArrayCodec, response.data);
|
|
1238
|
+
async deleteChannel(channelID) {
|
|
1239
|
+
await this.http.delete(this.getHost() + "/channel/" + channelID);
|
|
1303
1240
|
}
|
|
1304
|
-
async
|
|
1305
|
-
|
|
1241
|
+
async deleteDevice(deviceID) {
|
|
1242
|
+
if (deviceID === this.getDevice().deviceID) {
|
|
1243
|
+
throw new Error("You can't delete the device you're logged in to.");
|
|
1244
|
+
}
|
|
1245
|
+
await this.http.delete(this.prefixes.HTTP +
|
|
1306
1246
|
this.host +
|
|
1307
1247
|
"/user/" +
|
|
1308
1248
|
this.getUser().userID +
|
|
1309
|
-
"/devices/
|
|
1310
|
-
|
|
1311
|
-
"/reject");
|
|
1249
|
+
"/devices/" +
|
|
1250
|
+
deviceID);
|
|
1312
1251
|
}
|
|
1313
1252
|
async deleteHistory(channelOrUserID) {
|
|
1314
1253
|
await this.database.deleteHistory(channelOrUserID);
|
|
@@ -1319,6 +1258,19 @@ export class Client {
|
|
|
1319
1258
|
async deleteServer(serverID) {
|
|
1320
1259
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1321
1260
|
}
|
|
1261
|
+
deviceListFailureDetail(err) {
|
|
1262
|
+
if (!isAxiosError(err)) {
|
|
1263
|
+
return "";
|
|
1264
|
+
}
|
|
1265
|
+
const st = err.response?.status;
|
|
1266
|
+
if (typeof st === "number") {
|
|
1267
|
+
return ` (HTTP ${String(st)})`;
|
|
1268
|
+
}
|
|
1269
|
+
if (err.code !== undefined) {
|
|
1270
|
+
return ` (${err.code})`;
|
|
1271
|
+
}
|
|
1272
|
+
return "";
|
|
1273
|
+
}
|
|
1322
1274
|
/**
|
|
1323
1275
|
* Gets a list of permissions for a server.
|
|
1324
1276
|
*
|
|
@@ -1359,6 +1311,50 @@ export class Client {
|
|
|
1359
1311
|
return [null, isAxiosError(err) ? err : null];
|
|
1360
1312
|
}
|
|
1361
1313
|
}
|
|
1314
|
+
async fetchUserDeviceListOnce(userID) {
|
|
1315
|
+
if (this.isManualCloseInFlight()) {
|
|
1316
|
+
return [];
|
|
1317
|
+
}
|
|
1318
|
+
const res = await this.http.get(this.getHost() + "/user/" + userID + "/devices");
|
|
1319
|
+
const devices = decodeAxios(DeviceArrayCodec, res.data);
|
|
1320
|
+
for (const device of devices) {
|
|
1321
|
+
this.deviceRecords[device.deviceID] = device;
|
|
1322
|
+
}
|
|
1323
|
+
return devices;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* DM / forward paths need the peer’s (or self) device rows under load: bounded
|
|
1327
|
+
* retries with exponential backoff (same shape as session pubkey hydration).
|
|
1328
|
+
*/
|
|
1329
|
+
async fetchUserDeviceListWithBackoff(userID, label) {
|
|
1330
|
+
const base = label === "own"
|
|
1331
|
+
? "Couldn't get own devices"
|
|
1332
|
+
: "Couldn't get device list";
|
|
1333
|
+
let lastErr;
|
|
1334
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
1335
|
+
if (this.isManualCloseInFlight()) {
|
|
1336
|
+
return [];
|
|
1337
|
+
}
|
|
1338
|
+
if (attempt > 0) {
|
|
1339
|
+
const delayMs = 100 * 2 ** (attempt - 1);
|
|
1340
|
+
// Chunk the delay so close() can finish before we retry HTTP.
|
|
1341
|
+
const chunkMs = 10;
|
|
1342
|
+
for (let elapsed = 0; elapsed < delayMs; elapsed += chunkMs) {
|
|
1343
|
+
if (this.isManualCloseInFlight()) {
|
|
1344
|
+
return [];
|
|
1345
|
+
}
|
|
1346
|
+
await sleep(Math.min(chunkMs, delayMs - elapsed));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
try {
|
|
1350
|
+
return await this.fetchUserDeviceListOnce(userID);
|
|
1351
|
+
}
|
|
1352
|
+
catch (err) {
|
|
1353
|
+
lastErr = err;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
1357
|
+
}
|
|
1362
1358
|
async forward(message) {
|
|
1363
1359
|
if (this.isManualCloseInFlight()) {
|
|
1364
1360
|
return;
|
|
@@ -1427,6 +1423,23 @@ export class Client {
|
|
|
1427
1423
|
return null;
|
|
1428
1424
|
}
|
|
1429
1425
|
}
|
|
1426
|
+
async getDeviceRegistrationRequest(requestID) {
|
|
1427
|
+
try {
|
|
1428
|
+
const response = await this.http.get(this.prefixes.HTTP +
|
|
1429
|
+
this.host +
|
|
1430
|
+
"/user/" +
|
|
1431
|
+
this.getUser().userID +
|
|
1432
|
+
"/devices/requests/" +
|
|
1433
|
+
requestID);
|
|
1434
|
+
return decodeAxios(PendingDeviceRequestCodec, response.data);
|
|
1435
|
+
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
if (isAxiosError(err) && err.response?.status === 404) {
|
|
1438
|
+
return null;
|
|
1439
|
+
}
|
|
1440
|
+
throw err;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1430
1443
|
/* Retrieves the current list of users you have sessions with. */
|
|
1431
1444
|
async getFamiliars() {
|
|
1432
1445
|
const sessions = await this.database.getAllSessions();
|
|
@@ -1479,8 +1492,8 @@ export class Client {
|
|
|
1479
1492
|
}
|
|
1480
1493
|
})();
|
|
1481
1494
|
debugLibvexDm("getMail: inbox", {
|
|
1482
|
-
deviceID: did,
|
|
1483
1495
|
count: String(inbox.length),
|
|
1496
|
+
deviceID: did,
|
|
1484
1497
|
});
|
|
1485
1498
|
}
|
|
1486
1499
|
for (const mailDetails of inbox) {
|
|
@@ -1489,8 +1502,8 @@ export class Client {
|
|
|
1489
1502
|
if (libvexDebugDmEnabled()) {
|
|
1490
1503
|
debugLibvexDm("getMail: readMail one", {
|
|
1491
1504
|
mailID: mailBody.mailID,
|
|
1492
|
-
type: String(mailBody.mailType),
|
|
1493
1505
|
recipient: mailBody.recipient,
|
|
1506
|
+
type: String(mailBody.mailType),
|
|
1494
1507
|
});
|
|
1495
1508
|
}
|
|
1496
1509
|
await this.readMail(mailHeader, mailBody, timestamp);
|
|
@@ -1590,19 +1603,6 @@ export class Client {
|
|
|
1590
1603
|
}
|
|
1591
1604
|
return this.user;
|
|
1592
1605
|
}
|
|
1593
|
-
deviceListFailureDetail(err) {
|
|
1594
|
-
if (!isAxiosError(err)) {
|
|
1595
|
-
return "";
|
|
1596
|
-
}
|
|
1597
|
-
const st = err.response?.status;
|
|
1598
|
-
if (typeof st === "number") {
|
|
1599
|
-
return ` (HTTP ${String(st)})`;
|
|
1600
|
-
}
|
|
1601
|
-
if (err.code !== undefined) {
|
|
1602
|
-
return ` (${err.code})`;
|
|
1603
|
-
}
|
|
1604
|
-
return "";
|
|
1605
|
-
}
|
|
1606
1606
|
/**
|
|
1607
1607
|
* Single GET for `/user/:id/devices`. On failure returns `null` (swallows errors)
|
|
1608
1608
|
* — callers that need reliability should use `fetchUserDeviceListWithBackoff`.
|
|
@@ -1617,60 +1617,12 @@ export class Client {
|
|
|
1617
1617
|
return null;
|
|
1618
1618
|
}
|
|
1619
1619
|
}
|
|
1620
|
-
async fetchUserDeviceListOnce(userID) {
|
|
1621
|
-
if (this.isManualCloseInFlight()) {
|
|
1622
|
-
return [];
|
|
1623
|
-
}
|
|
1624
|
-
const res = await this.http.get(this.getHost() + "/user/" + userID + "/devices");
|
|
1625
|
-
const devices = decodeAxios(DeviceArrayCodec, res.data);
|
|
1626
|
-
for (const device of devices) {
|
|
1627
|
-
this.deviceRecords[device.deviceID] = device;
|
|
1628
|
-
}
|
|
1629
|
-
return devices;
|
|
1630
|
-
}
|
|
1631
|
-
/**
|
|
1632
|
-
* DM / forward paths need the peer’s (or self) device rows under load: bounded
|
|
1633
|
-
* retries with exponential backoff (same shape as session pubkey hydration).
|
|
1634
|
-
*/
|
|
1635
|
-
async fetchUserDeviceListWithBackoff(userID, label) {
|
|
1636
|
-
const base = label === "own"
|
|
1637
|
-
? "Couldn't get own devices"
|
|
1638
|
-
: "Couldn't get device list";
|
|
1639
|
-
let lastErr;
|
|
1640
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
1641
|
-
if (this.isManualCloseInFlight()) {
|
|
1642
|
-
return [];
|
|
1643
|
-
}
|
|
1644
|
-
if (attempt > 0) {
|
|
1645
|
-
const delayMs = 100 * 2 ** (attempt - 1);
|
|
1646
|
-
// Chunk the delay so close() can finish before we retry HTTP.
|
|
1647
|
-
const chunkMs = 10;
|
|
1648
|
-
for (let elapsed = 0; elapsed < delayMs; elapsed += chunkMs) {
|
|
1649
|
-
if (this.isManualCloseInFlight()) {
|
|
1650
|
-
return [];
|
|
1651
|
-
}
|
|
1652
|
-
await sleep(Math.min(chunkMs, delayMs - elapsed));
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
try {
|
|
1656
|
-
return await this.fetchUserDeviceListOnce(userID);
|
|
1657
|
-
}
|
|
1658
|
-
catch (err) {
|
|
1659
|
-
lastErr = err;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
1663
|
-
}
|
|
1664
1620
|
async getUserList(channelID) {
|
|
1665
1621
|
const res = await this.http.post(this.getHost() + "/userList/" + channelID);
|
|
1666
1622
|
return decodeAxios(UserArrayCodec, res.data);
|
|
1667
1623
|
}
|
|
1668
1624
|
async handleNotify(msg) {
|
|
1669
1625
|
switch (msg.event) {
|
|
1670
|
-
case "mail":
|
|
1671
|
-
await this.getMail();
|
|
1672
|
-
this.fetchingMail = false;
|
|
1673
|
-
break;
|
|
1674
1626
|
case "deviceRequest": {
|
|
1675
1627
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
1676
1628
|
if (parsed.success) {
|
|
@@ -1678,6 +1630,10 @@ export class Client {
|
|
|
1678
1630
|
}
|
|
1679
1631
|
break;
|
|
1680
1632
|
}
|
|
1633
|
+
case "mail":
|
|
1634
|
+
await this.getMail();
|
|
1635
|
+
this.fetchingMail = false;
|
|
1636
|
+
break;
|
|
1681
1637
|
case "permission":
|
|
1682
1638
|
this.emitter.emit("permission", PermissionSchema.parse(msg.data));
|
|
1683
1639
|
break;
|
|
@@ -1688,24 +1644,6 @@ export class Client {
|
|
|
1688
1644
|
break;
|
|
1689
1645
|
}
|
|
1690
1646
|
}
|
|
1691
|
-
/**
|
|
1692
|
-
* Pipeline for decrypted messages — registered in `init`. After `close()` sets
|
|
1693
|
-
* `manuallyClosing`, this becomes a no-op so fire-and-forget `forward` does not
|
|
1694
|
-
* race HTTP teardown (we avoid `off()` here — it can interact badly with emit).
|
|
1695
|
-
*/
|
|
1696
|
-
onInternalMessage = (message) => {
|
|
1697
|
-
if (this.isManualCloseInFlight()) {
|
|
1698
|
-
return;
|
|
1699
|
-
}
|
|
1700
|
-
if (message.direction === "outgoing" && !message.forward) {
|
|
1701
|
-
void this.forward(message);
|
|
1702
|
-
}
|
|
1703
|
-
if (message.direction === "incoming" &&
|
|
1704
|
-
message.recipient === message.sender) {
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
void this.database.saveMessage(message);
|
|
1708
|
-
};
|
|
1709
1647
|
/**
|
|
1710
1648
|
* Initializes the keyring. This must be called before anything else.
|
|
1711
1649
|
*/
|
|
@@ -1793,6 +1731,14 @@ export class Client {
|
|
|
1793
1731
|
throw new Error("Error initiating websocket connection " + String(err));
|
|
1794
1732
|
}
|
|
1795
1733
|
}
|
|
1734
|
+
/**
|
|
1735
|
+
* Fresh read of the `manuallyClosing` flag for async loops — direct property checks
|
|
1736
|
+
* after `await` are flagged as always-false by control-flow analysis even though
|
|
1737
|
+
* `close()` can run concurrently.
|
|
1738
|
+
*/
|
|
1739
|
+
isManualCloseInFlight() {
|
|
1740
|
+
return this.manuallyClosing;
|
|
1741
|
+
}
|
|
1796
1742
|
async kickUser(userID, serverID) {
|
|
1797
1743
|
const permissionList = await this.fetchPermissionList(serverID);
|
|
1798
1744
|
for (const permission of permissionList) {
|
|
@@ -1811,6 +1757,14 @@ export class Client {
|
|
|
1811
1757
|
}
|
|
1812
1758
|
}
|
|
1813
1759
|
}
|
|
1760
|
+
async listDeviceRegistrationRequests() {
|
|
1761
|
+
const response = await this.http.get(this.prefixes.HTTP +
|
|
1762
|
+
this.host +
|
|
1763
|
+
"/user/" +
|
|
1764
|
+
this.getUser().userID +
|
|
1765
|
+
"/devices/requests");
|
|
1766
|
+
return decodeAxios(PendingDeviceRequestArrayCodec, response.data);
|
|
1767
|
+
}
|
|
1814
1768
|
async markSessionVerified(sessionID) {
|
|
1815
1769
|
return this.database.markSessionVerified(sessionID);
|
|
1816
1770
|
}
|
|
@@ -1831,6 +1785,24 @@ export class Client {
|
|
|
1831
1785
|
}
|
|
1832
1786
|
this.xKeyRing.ephemeralKeys = await xBoxKeyPairAsync();
|
|
1833
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Pipeline for decrypted messages — registered in `init`. After `close()` sets
|
|
1790
|
+
* `manuallyClosing`, this becomes a no-op so fire-and-forget `forward` does not
|
|
1791
|
+
* race HTTP teardown (we avoid `off()` here — it can interact badly with emit).
|
|
1792
|
+
*/
|
|
1793
|
+
onInternalMessage = (message) => {
|
|
1794
|
+
if (this.isManualCloseInFlight()) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (message.direction === "outgoing" && !message.forward) {
|
|
1798
|
+
void this.forward(message);
|
|
1799
|
+
}
|
|
1800
|
+
if (message.direction === "incoming" &&
|
|
1801
|
+
message.recipient === message.sender) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
void this.database.saveMessage(message);
|
|
1805
|
+
};
|
|
1834
1806
|
ping() {
|
|
1835
1807
|
if (!this.isAlive) {
|
|
1836
1808
|
}
|
|
@@ -1951,7 +1923,7 @@ export class Client {
|
|
|
1951
1923
|
const deviceEntry = await this.getDeviceByID(mail.sender);
|
|
1952
1924
|
const [user, _err] = await this.fetchUser(mail.authorID);
|
|
1953
1925
|
if (deviceEntry && user) {
|
|
1954
|
-
void this.createSession(deviceEntry, user,
|
|
1926
|
+
void this.createSession(deviceEntry, user, new Uint8Array(), mail.group, uuid.v4(), false, true);
|
|
1955
1927
|
}
|
|
1956
1928
|
};
|
|
1957
1929
|
switch (mail.mailType) {
|
|
@@ -1972,8 +1944,8 @@ export class Client {
|
|
|
1972
1944
|
try {
|
|
1973
1945
|
debugLibvexDm("readMail initial: abort (otk index mismatch)", {
|
|
1974
1946
|
mailID: mail.mailID,
|
|
1975
|
-
preKeyIndex: String(preKeyIndex),
|
|
1976
1947
|
otkIndex: String(otk?.index ?? "null"),
|
|
1948
|
+
preKeyIndex: String(preKeyIndex),
|
|
1977
1949
|
thisDevice: this.getDevice().deviceID,
|
|
1978
1950
|
});
|
|
1979
1951
|
}
|
|
@@ -2000,8 +1972,8 @@ export class Client {
|
|
|
2000
1972
|
if (libvexDebugDmEnabled()) {
|
|
2001
1973
|
try {
|
|
2002
1974
|
debugLibvexDm("readMail initial: abort (IK_A null, Ed→X25519?)", {
|
|
2003
|
-
mailID: mail.mailID,
|
|
2004
1975
|
fips: String(fipsRead),
|
|
1976
|
+
mailID: mail.mailID,
|
|
2005
1977
|
thisDevice: this.getDevice().deviceID,
|
|
2006
1978
|
});
|
|
2007
1979
|
}
|
|
@@ -2095,9 +2067,9 @@ export class Client {
|
|
|
2095
2067
|
try {
|
|
2096
2068
|
debugLibvexDm("readMail initial: ok (emit message)", {
|
|
2097
2069
|
mailID: mail.mailID,
|
|
2070
|
+
plaintextLen: String(plaintext.length),
|
|
2098
2071
|
preKeyIndex: String(preKeyIndex),
|
|
2099
2072
|
thisDevice: this.getDevice().deviceID,
|
|
2100
|
-
plaintextLen: String(plaintext.length),
|
|
2101
2073
|
});
|
|
2102
2074
|
}
|
|
2103
2075
|
catch {
|
|
@@ -2284,6 +2256,15 @@ export class Client {
|
|
|
2284
2256
|
"/devices", msgpack.encode(devMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
2285
2257
|
return decodeAxios(DeviceRegistrationResultCodec, res.data);
|
|
2286
2258
|
}
|
|
2259
|
+
async rejectDeviceRequest(requestID) {
|
|
2260
|
+
await this.http.post(this.prefixes.HTTP +
|
|
2261
|
+
this.host +
|
|
2262
|
+
"/user/" +
|
|
2263
|
+
this.getUser().userID +
|
|
2264
|
+
"/devices/requests/" +
|
|
2265
|
+
requestID +
|
|
2266
|
+
"/reject");
|
|
2267
|
+
}
|
|
2287
2268
|
async respond(msg) {
|
|
2288
2269
|
const response = {
|
|
2289
2270
|
signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
|
|
@@ -2370,6 +2351,25 @@ export class Client {
|
|
|
2370
2351
|
}
|
|
2371
2352
|
return device;
|
|
2372
2353
|
}
|
|
2354
|
+
/**
|
|
2355
|
+
* `xDHAsync` and other helpers in `@vex-chat/crypto` use the process-wide
|
|
2356
|
+
* active profile. When several {@link Client} instances use different
|
|
2357
|
+
* `cryptoProfile` values, scope the global to this instance for the duration
|
|
2358
|
+
* of that crypto work.
|
|
2359
|
+
*/
|
|
2360
|
+
async runWithThisCryptoProfile(fn) {
|
|
2361
|
+
const prev = getCryptoProfile();
|
|
2362
|
+
if (prev === this.cryptoProfile) {
|
|
2363
|
+
return await fn();
|
|
2364
|
+
}
|
|
2365
|
+
setCryptoProfile(this.cryptoProfile);
|
|
2366
|
+
try {
|
|
2367
|
+
return await fn();
|
|
2368
|
+
}
|
|
2369
|
+
finally {
|
|
2370
|
+
setCryptoProfile(prev);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
2373
|
/* header is 32 bytes and is either empty
|
|
2374
2374
|
or contains an HMAC of the message with
|
|
2375
2375
|
a derived SK */
|
|
@@ -2419,9 +2419,9 @@ export class Client {
|
|
|
2419
2419
|
if (!session || retry) {
|
|
2420
2420
|
if (libvexDebugDmEnabled()) {
|
|
2421
2421
|
debugLibvexDm("sendMail: createSession path", {
|
|
2422
|
+
hasSession: String(!!session),
|
|
2422
2423
|
peerDevice: device.deviceID,
|
|
2423
2424
|
retry: String(retry),
|
|
2424
|
-
hasSession: String(!!session),
|
|
2425
2425
|
});
|
|
2426
2426
|
}
|
|
2427
2427
|
await this.createSession(device, user, msg, group, mailID, forward, false);
|
|
@@ -2541,11 +2541,11 @@ export class Client {
|
|
|
2541
2541
|
const deviceList = [...deviceListRaw].sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
|
|
2542
2542
|
if (libvexDebugDmEnabled()) {
|
|
2543
2543
|
debugLibvexDm("sendMessage: peer device list (merged, sorted)", {
|
|
2544
|
-
userID,
|
|
2545
2544
|
nAfterBackoff: String(afterBackoff.length),
|
|
2546
2545
|
nMerged: String(deviceListRaw.length),
|
|
2547
2546
|
nSorted: String(deviceList.length),
|
|
2548
2547
|
ourDevice: this.getDevice().deviceID,
|
|
2548
|
+
userID,
|
|
2549
2549
|
});
|
|
2550
2550
|
for (const [i, d] of deviceList.entries()) {
|
|
2551
2551
|
debugLibvexDm(`sendMessage: device[${String(i)}]`, {
|
|
@@ -2562,8 +2562,8 @@ export class Client {
|
|
|
2562
2562
|
try {
|
|
2563
2563
|
if (libvexDebugDmEnabled()) {
|
|
2564
2564
|
debugLibvexDm("sendMessage: sendMail start", {
|
|
2565
|
-
recipientDevice: device.deviceID,
|
|
2566
2565
|
mailID: messageMailID,
|
|
2566
|
+
recipientDevice: device.deviceID,
|
|
2567
2567
|
});
|
|
2568
2568
|
}
|
|
2569
2569
|
await this.sendMail(device, userEntry, XUtils.decodeUTF8(message), null, messageMailID, false);
|