@vex-chat/libvex 5.5.0 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -23
- package/dist/Client.d.ts +114 -103
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +317 -314
- 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 +448 -437
- 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";
|
|
@@ -141,6 +141,12 @@ const deviceRequestNotifyData = z.object({
|
|
|
141
141
|
z.literal("rejected"),
|
|
142
142
|
]),
|
|
143
143
|
});
|
|
144
|
+
const retryRequestNotifyData = z.union([
|
|
145
|
+
z.string(),
|
|
146
|
+
z.object({
|
|
147
|
+
mailID: z.string(),
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
144
150
|
export class Client {
|
|
145
151
|
/**
|
|
146
152
|
* Decrypts a secret key from encrypted data produced by encryptKeyData().
|
|
@@ -205,8 +211,8 @@ export class Client {
|
|
|
205
211
|
delete: this.deleteDevice.bind(this),
|
|
206
212
|
getRequest: this.getDeviceRegistrationRequest.bind(this),
|
|
207
213
|
listRequests: this.listDeviceRegistrationRequests.bind(this),
|
|
208
|
-
rejectRequest: this.rejectDeviceRequest.bind(this),
|
|
209
214
|
register: this.registerDevice.bind(this),
|
|
215
|
+
rejectRequest: this.rejectDeviceRequest.bind(this),
|
|
210
216
|
retrieve: this.getDeviceByID.bind(this),
|
|
211
217
|
};
|
|
212
218
|
/**
|
|
@@ -409,6 +415,7 @@ export class Client {
|
|
|
409
415
|
*/
|
|
410
416
|
retrieve: this.fetchUser.bind(this),
|
|
411
417
|
};
|
|
418
|
+
cryptoProfile;
|
|
412
419
|
database;
|
|
413
420
|
dbPath;
|
|
414
421
|
device;
|
|
@@ -419,23 +426,18 @@ export class Client {
|
|
|
419
426
|
firstMailFetch = true;
|
|
420
427
|
forwarded = new Set();
|
|
421
428
|
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;
|
|
429
|
+
http;
|
|
427
430
|
/** Cancels in-flight axios work on `close()` so `postAuth`/`getMail` cannot hang forever. */
|
|
428
431
|
httpAbortController = new AbortController();
|
|
429
|
-
http;
|
|
430
432
|
idKeys;
|
|
431
433
|
isAlive = true;
|
|
432
434
|
mailInterval;
|
|
433
435
|
manuallyClosing = false;
|
|
434
436
|
/**
|
|
435
|
-
*
|
|
436
|
-
* `
|
|
437
|
+
* Node-only: per-client HTTP(S) agents (see `init()` + `storage/node/http-agents`).
|
|
438
|
+
* Dropped on `close()` so idle keep-alive sockets do not keep the process alive.
|
|
437
439
|
*/
|
|
438
|
-
|
|
440
|
+
nodeHttpAgents;
|
|
439
441
|
/* Retrieves the userID with the user identifier.
|
|
440
442
|
user identifier is checked for userID, then signkey,
|
|
441
443
|
and finally falls back to username. */
|
|
@@ -443,6 +445,11 @@ export class Client {
|
|
|
443
445
|
notFoundUsers = new Map();
|
|
444
446
|
options;
|
|
445
447
|
pingInterval = null;
|
|
448
|
+
/**
|
|
449
|
+
* Bumped when the WebSocket is torn down and re-opened so the previous
|
|
450
|
+
* `postAuth` loop exits instead of overlapping a new one.
|
|
451
|
+
*/
|
|
452
|
+
postAuthVersion = 0;
|
|
446
453
|
prefixes;
|
|
447
454
|
reading = false;
|
|
448
455
|
seenMailIDs = new Set();
|
|
@@ -454,7 +461,6 @@ export class Client {
|
|
|
454
461
|
user;
|
|
455
462
|
userRecords = {};
|
|
456
463
|
xKeyRing;
|
|
457
|
-
cryptoProfile;
|
|
458
464
|
constructor(material, options, storage) {
|
|
459
465
|
this.options = options;
|
|
460
466
|
this.cryptoProfile = material.cryptoProfile;
|
|
@@ -617,27 +623,6 @@ export class Client {
|
|
|
617
623
|
static getMnemonic(session) {
|
|
618
624
|
return xMnemonic(xKDF(XUtils.decodeHex(session.fingerprint)));
|
|
619
625
|
}
|
|
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
626
|
/**
|
|
642
627
|
* Browser-safe NODE_ENV accessor.
|
|
643
628
|
* Uses indirect lookup so the bare `process` global never appears in
|
|
@@ -675,12 +660,25 @@ export class Client {
|
|
|
675
660
|
}
|
|
676
661
|
}
|
|
677
662
|
/**
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
663
|
+
* True when running under Node (has `process.versions`).
|
|
664
|
+
* Uses indirect lookup so the bare `process` global never appears in
|
|
665
|
+
* source that the platform-guard plugin scans.
|
|
681
666
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
667
|
+
static isNodeRuntime() {
|
|
668
|
+
try {
|
|
669
|
+
const g = Object.getOwnPropertyDescriptor(globalThis, "\u0070rocess");
|
|
670
|
+
if (!g)
|
|
671
|
+
return false;
|
|
672
|
+
const proc = typeof g.get === "function" ? g.get() : g.value;
|
|
673
|
+
if (typeof proc !== "object" || proc === null) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
return ("versions" in proc &&
|
|
677
|
+
typeof proc.versions === "object");
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
684
682
|
}
|
|
685
683
|
/**
|
|
686
684
|
* Closes the client — disconnects the WebSocket, shuts down storage,
|
|
@@ -735,57 +733,6 @@ export class Client {
|
|
|
735
733
|
await new Promise((r) => setTimeout(r, 0));
|
|
736
734
|
await this.negotiateOTK();
|
|
737
735
|
}
|
|
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
736
|
/**
|
|
790
737
|
* Delete all local data — message history, encryption sessions, and prekeys.
|
|
791
738
|
* Closes the client afterward. Credentials (keychain) must be cleared by the consumer.
|
|
@@ -911,6 +858,50 @@ export class Client {
|
|
|
911
858
|
this.emitter.once(event, fn, context);
|
|
912
859
|
return this;
|
|
913
860
|
}
|
|
861
|
+
/**
|
|
862
|
+
* Tears down the current WebSocket and opens a new one, keeping the same
|
|
863
|
+
* session (user + device in storage). Restarts the post-auth mail loop.
|
|
864
|
+
* Use for long-running processes or e2e where a fresh socket matches a
|
|
865
|
+
* newly-registered second device.
|
|
866
|
+
*/
|
|
867
|
+
async reconnectWebsocket() {
|
|
868
|
+
this.postAuthVersion++;
|
|
869
|
+
if (this.pingInterval) {
|
|
870
|
+
clearInterval(this.pingInterval);
|
|
871
|
+
this.pingInterval = null;
|
|
872
|
+
}
|
|
873
|
+
this.socket.close();
|
|
874
|
+
try {
|
|
875
|
+
await new Promise((resolve, reject) => {
|
|
876
|
+
const t = setTimeout(() => {
|
|
877
|
+
this.off("connected", onC);
|
|
878
|
+
reject(new Error("reconnectWebsocket: timed out waiting for authorized"));
|
|
879
|
+
}, 15_000);
|
|
880
|
+
const onC = () => {
|
|
881
|
+
clearTimeout(t);
|
|
882
|
+
this.off("connected", onC);
|
|
883
|
+
resolve();
|
|
884
|
+
};
|
|
885
|
+
this.on("connected", onC);
|
|
886
|
+
try {
|
|
887
|
+
this.initSocket();
|
|
888
|
+
}
|
|
889
|
+
catch (err) {
|
|
890
|
+
clearTimeout(t);
|
|
891
|
+
this.off("connected", onC);
|
|
892
|
+
const e = err instanceof Error
|
|
893
|
+
? err
|
|
894
|
+
: new Error(String(err), { cause: err });
|
|
895
|
+
reject(e);
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
catch (e) {
|
|
900
|
+
throw e instanceof Error ? e : new Error(String(e), { cause: e });
|
|
901
|
+
}
|
|
902
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
903
|
+
await this.negotiateOTK();
|
|
904
|
+
}
|
|
914
905
|
/**
|
|
915
906
|
* Registers a new account on the server.
|
|
916
907
|
*
|
|
@@ -968,6 +959,13 @@ export class Client {
|
|
|
968
959
|
this.emitter.removeAllListeners(event);
|
|
969
960
|
return this;
|
|
970
961
|
}
|
|
962
|
+
/**
|
|
963
|
+
* Triggers an immediate inbox sync by fetching `/mail` once.
|
|
964
|
+
* Useful on mobile foreground resume where background work may pause.
|
|
965
|
+
*/
|
|
966
|
+
async syncInboxNow() {
|
|
967
|
+
await this.getMail();
|
|
968
|
+
}
|
|
971
969
|
/**
|
|
972
970
|
* Returns a compact `<username><deviceID>` debug label.
|
|
973
971
|
*/
|
|
@@ -993,6 +991,24 @@ export class Client {
|
|
|
993
991
|
const whoami = decodeAxios(WhoamiCodec, res.data);
|
|
994
992
|
return whoami;
|
|
995
993
|
}
|
|
994
|
+
async approveDeviceRequest(requestID) {
|
|
995
|
+
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
996
|
+
if (!req) {
|
|
997
|
+
throw new Error("Device approval request not found.");
|
|
998
|
+
}
|
|
999
|
+
if (req.status !== "pending") {
|
|
1000
|
+
throw new Error("Device approval request is not pending: " + req.status);
|
|
1001
|
+
}
|
|
1002
|
+
const signed = XUtils.encodeHex(await xSignAsync(XUtils.decodeUTF8(requestID), this.signKeys.secretKey));
|
|
1003
|
+
const response = await this.http.post(this.prefixes.HTTP +
|
|
1004
|
+
this.host +
|
|
1005
|
+
"/user/" +
|
|
1006
|
+
this.getUser().userID +
|
|
1007
|
+
"/devices/requests/" +
|
|
1008
|
+
requestID +
|
|
1009
|
+
"/approve", msgpack.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1010
|
+
return decodeAxios(DeviceCodec, response.data);
|
|
1011
|
+
}
|
|
996
1012
|
censorPreKey(preKey) {
|
|
997
1013
|
if (!preKey.index) {
|
|
998
1014
|
throw new Error("Key index is required.");
|
|
@@ -1073,25 +1089,6 @@ export class Client {
|
|
|
1073
1089
|
const res = await this.http.post(this.getHost() + "/server/" + globalThis.btoa(name));
|
|
1074
1090
|
return decodeAxios(ServerCodec, res.data);
|
|
1075
1091
|
}
|
|
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
1092
|
async createSession(device, user, message, group,
|
|
1096
1093
|
/* this is passed through if the first message is
|
|
1097
1094
|
part of a group message */
|
|
@@ -1244,71 +1241,19 @@ export class Client {
|
|
|
1244
1241
|
});
|
|
1245
1242
|
});
|
|
1246
1243
|
}
|
|
1247
|
-
async deleteChannel(channelID) {
|
|
1248
|
-
await this.http.delete(this.getHost() + "/channel/" + channelID);
|
|
1249
|
-
}
|
|
1250
|
-
async
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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);
|
|
1303
|
-
}
|
|
1304
|
-
async rejectDeviceRequest(requestID) {
|
|
1305
|
-
await this.http.post(this.prefixes.HTTP +
|
|
1244
|
+
async deleteChannel(channelID) {
|
|
1245
|
+
await this.http.delete(this.getHost() + "/channel/" + channelID);
|
|
1246
|
+
}
|
|
1247
|
+
async deleteDevice(deviceID) {
|
|
1248
|
+
if (deviceID === this.getDevice().deviceID) {
|
|
1249
|
+
throw new Error("You can't delete the device you're logged in to.");
|
|
1250
|
+
}
|
|
1251
|
+
await this.http.delete(this.prefixes.HTTP +
|
|
1306
1252
|
this.host +
|
|
1307
1253
|
"/user/" +
|
|
1308
1254
|
this.getUser().userID +
|
|
1309
|
-
"/devices/
|
|
1310
|
-
|
|
1311
|
-
"/reject");
|
|
1255
|
+
"/devices/" +
|
|
1256
|
+
deviceID);
|
|
1312
1257
|
}
|
|
1313
1258
|
async deleteHistory(channelOrUserID) {
|
|
1314
1259
|
await this.database.deleteHistory(channelOrUserID);
|
|
@@ -1319,6 +1264,19 @@ export class Client {
|
|
|
1319
1264
|
async deleteServer(serverID) {
|
|
1320
1265
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1321
1266
|
}
|
|
1267
|
+
deviceListFailureDetail(err) {
|
|
1268
|
+
if (!isAxiosError(err)) {
|
|
1269
|
+
return "";
|
|
1270
|
+
}
|
|
1271
|
+
const st = err.response?.status;
|
|
1272
|
+
if (typeof st === "number") {
|
|
1273
|
+
return ` (HTTP ${String(st)})`;
|
|
1274
|
+
}
|
|
1275
|
+
if (err.code !== undefined) {
|
|
1276
|
+
return ` (${err.code})`;
|
|
1277
|
+
}
|
|
1278
|
+
return "";
|
|
1279
|
+
}
|
|
1322
1280
|
/**
|
|
1323
1281
|
* Gets a list of permissions for a server.
|
|
1324
1282
|
*
|
|
@@ -1359,6 +1317,50 @@ export class Client {
|
|
|
1359
1317
|
return [null, isAxiosError(err) ? err : null];
|
|
1360
1318
|
}
|
|
1361
1319
|
}
|
|
1320
|
+
async fetchUserDeviceListOnce(userID) {
|
|
1321
|
+
if (this.isManualCloseInFlight()) {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
const res = await this.http.get(this.getHost() + "/user/" + userID + "/devices");
|
|
1325
|
+
const devices = decodeAxios(DeviceArrayCodec, res.data);
|
|
1326
|
+
for (const device of devices) {
|
|
1327
|
+
this.deviceRecords[device.deviceID] = device;
|
|
1328
|
+
}
|
|
1329
|
+
return devices;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* DM / forward paths need the peer’s (or self) device rows under load: bounded
|
|
1333
|
+
* retries with exponential backoff (same shape as session pubkey hydration).
|
|
1334
|
+
*/
|
|
1335
|
+
async fetchUserDeviceListWithBackoff(userID, label) {
|
|
1336
|
+
const base = label === "own"
|
|
1337
|
+
? "Couldn't get own devices"
|
|
1338
|
+
: "Couldn't get device list";
|
|
1339
|
+
let lastErr;
|
|
1340
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
1341
|
+
if (this.isManualCloseInFlight()) {
|
|
1342
|
+
return [];
|
|
1343
|
+
}
|
|
1344
|
+
if (attempt > 0) {
|
|
1345
|
+
const delayMs = 100 * 2 ** (attempt - 1);
|
|
1346
|
+
// Chunk the delay so close() can finish before we retry HTTP.
|
|
1347
|
+
const chunkMs = 10;
|
|
1348
|
+
for (let elapsed = 0; elapsed < delayMs; elapsed += chunkMs) {
|
|
1349
|
+
if (this.isManualCloseInFlight()) {
|
|
1350
|
+
return [];
|
|
1351
|
+
}
|
|
1352
|
+
await sleep(Math.min(chunkMs, delayMs - elapsed));
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
try {
|
|
1356
|
+
return await this.fetchUserDeviceListOnce(userID);
|
|
1357
|
+
}
|
|
1358
|
+
catch (err) {
|
|
1359
|
+
lastErr = err;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
1363
|
+
}
|
|
1362
1364
|
async forward(message) {
|
|
1363
1365
|
if (this.isManualCloseInFlight()) {
|
|
1364
1366
|
return;
|
|
@@ -1427,6 +1429,23 @@ export class Client {
|
|
|
1427
1429
|
return null;
|
|
1428
1430
|
}
|
|
1429
1431
|
}
|
|
1432
|
+
async getDeviceRegistrationRequest(requestID) {
|
|
1433
|
+
try {
|
|
1434
|
+
const response = await this.http.get(this.prefixes.HTTP +
|
|
1435
|
+
this.host +
|
|
1436
|
+
"/user/" +
|
|
1437
|
+
this.getUser().userID +
|
|
1438
|
+
"/devices/requests/" +
|
|
1439
|
+
requestID);
|
|
1440
|
+
return decodeAxios(PendingDeviceRequestCodec, response.data);
|
|
1441
|
+
}
|
|
1442
|
+
catch (err) {
|
|
1443
|
+
if (isAxiosError(err) && err.response?.status === 404) {
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
throw err;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1430
1449
|
/* Retrieves the current list of users you have sessions with. */
|
|
1431
1450
|
async getFamiliars() {
|
|
1432
1451
|
const sessions = await this.database.getAllSessions();
|
|
@@ -1479,8 +1498,8 @@ export class Client {
|
|
|
1479
1498
|
}
|
|
1480
1499
|
})();
|
|
1481
1500
|
debugLibvexDm("getMail: inbox", {
|
|
1482
|
-
deviceID: did,
|
|
1483
1501
|
count: String(inbox.length),
|
|
1502
|
+
deviceID: did,
|
|
1484
1503
|
});
|
|
1485
1504
|
}
|
|
1486
1505
|
for (const mailDetails of inbox) {
|
|
@@ -1489,8 +1508,8 @@ export class Client {
|
|
|
1489
1508
|
if (libvexDebugDmEnabled()) {
|
|
1490
1509
|
debugLibvexDm("getMail: readMail one", {
|
|
1491
1510
|
mailID: mailBody.mailID,
|
|
1492
|
-
type: String(mailBody.mailType),
|
|
1493
1511
|
recipient: mailBody.recipient,
|
|
1512
|
+
type: String(mailBody.mailType),
|
|
1494
1513
|
});
|
|
1495
1514
|
}
|
|
1496
1515
|
await this.readMail(mailHeader, mailBody, timestamp);
|
|
@@ -1590,19 +1609,6 @@ export class Client {
|
|
|
1590
1609
|
}
|
|
1591
1610
|
return this.user;
|
|
1592
1611
|
}
|
|
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
1612
|
/**
|
|
1607
1613
|
* Single GET for `/user/:id/devices`. On failure returns `null` (swallows errors)
|
|
1608
1614
|
* — callers that need reliability should use `fetchUserDeviceListWithBackoff`.
|
|
@@ -1617,60 +1623,12 @@ export class Client {
|
|
|
1617
1623
|
return null;
|
|
1618
1624
|
}
|
|
1619
1625
|
}
|
|
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
1626
|
async getUserList(channelID) {
|
|
1665
1627
|
const res = await this.http.post(this.getHost() + "/userList/" + channelID);
|
|
1666
1628
|
return decodeAxios(UserArrayCodec, res.data);
|
|
1667
1629
|
}
|
|
1668
1630
|
async handleNotify(msg) {
|
|
1669
1631
|
switch (msg.event) {
|
|
1670
|
-
case "mail":
|
|
1671
|
-
await this.getMail();
|
|
1672
|
-
this.fetchingMail = false;
|
|
1673
|
-
break;
|
|
1674
1632
|
case "deviceRequest": {
|
|
1675
1633
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
1676
1634
|
if (parsed.success) {
|
|
@@ -1678,34 +1636,31 @@ export class Client {
|
|
|
1678
1636
|
}
|
|
1679
1637
|
break;
|
|
1680
1638
|
}
|
|
1639
|
+
case "mail":
|
|
1640
|
+
await this.getMail();
|
|
1641
|
+
this.fetchingMail = false;
|
|
1642
|
+
break;
|
|
1681
1643
|
case "permission":
|
|
1682
1644
|
this.emitter.emit("permission", PermissionSchema.parse(msg.data));
|
|
1683
1645
|
break;
|
|
1684
1646
|
case "retryRequest":
|
|
1685
|
-
|
|
1647
|
+
{
|
|
1648
|
+
const parsed = retryRequestNotifyData.safeParse(msg.data);
|
|
1649
|
+
if (parsed.success) {
|
|
1650
|
+
const mailID = typeof parsed.data === "string"
|
|
1651
|
+
? parsed.data
|
|
1652
|
+
: parsed.data.mailID;
|
|
1653
|
+
this.emitter.emit("retryRequest", {
|
|
1654
|
+
mailID,
|
|
1655
|
+
source: "server_notify",
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1686
1659
|
break;
|
|
1687
1660
|
default:
|
|
1688
1661
|
break;
|
|
1689
1662
|
}
|
|
1690
1663
|
}
|
|
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
1664
|
/**
|
|
1710
1665
|
* Initializes the keyring. This must be called before anything else.
|
|
1711
1666
|
*/
|
|
@@ -1793,6 +1748,14 @@ export class Client {
|
|
|
1793
1748
|
throw new Error("Error initiating websocket connection " + String(err));
|
|
1794
1749
|
}
|
|
1795
1750
|
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Fresh read of the `manuallyClosing` flag for async loops — direct property checks
|
|
1753
|
+
* after `await` are flagged as always-false by control-flow analysis even though
|
|
1754
|
+
* `close()` can run concurrently.
|
|
1755
|
+
*/
|
|
1756
|
+
isManualCloseInFlight() {
|
|
1757
|
+
return this.manuallyClosing;
|
|
1758
|
+
}
|
|
1796
1759
|
async kickUser(userID, serverID) {
|
|
1797
1760
|
const permissionList = await this.fetchPermissionList(serverID);
|
|
1798
1761
|
for (const permission of permissionList) {
|
|
@@ -1811,6 +1774,14 @@ export class Client {
|
|
|
1811
1774
|
}
|
|
1812
1775
|
}
|
|
1813
1776
|
}
|
|
1777
|
+
async listDeviceRegistrationRequests() {
|
|
1778
|
+
const response = await this.http.get(this.prefixes.HTTP +
|
|
1779
|
+
this.host +
|
|
1780
|
+
"/user/" +
|
|
1781
|
+
this.getUser().userID +
|
|
1782
|
+
"/devices/requests");
|
|
1783
|
+
return decodeAxios(PendingDeviceRequestArrayCodec, response.data);
|
|
1784
|
+
}
|
|
1814
1785
|
async markSessionVerified(sessionID) {
|
|
1815
1786
|
return this.database.markSessionVerified(sessionID);
|
|
1816
1787
|
}
|
|
@@ -1831,6 +1802,24 @@ export class Client {
|
|
|
1831
1802
|
}
|
|
1832
1803
|
this.xKeyRing.ephemeralKeys = await xBoxKeyPairAsync();
|
|
1833
1804
|
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Pipeline for decrypted messages — registered in `init`. After `close()` sets
|
|
1807
|
+
* `manuallyClosing`, this becomes a no-op so fire-and-forget `forward` does not
|
|
1808
|
+
* race HTTP teardown (we avoid `off()` here — it can interact badly with emit).
|
|
1809
|
+
*/
|
|
1810
|
+
onInternalMessage = (message) => {
|
|
1811
|
+
if (this.isManualCloseInFlight()) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
if (message.direction === "outgoing" && !message.forward) {
|
|
1815
|
+
void this.forward(message);
|
|
1816
|
+
}
|
|
1817
|
+
if (message.direction === "incoming" &&
|
|
1818
|
+
message.recipient === message.sender) {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
void this.database.saveMessage(message);
|
|
1822
|
+
};
|
|
1834
1823
|
ping() {
|
|
1835
1824
|
if (!this.isAlive) {
|
|
1836
1825
|
}
|
|
@@ -1951,7 +1940,7 @@ export class Client {
|
|
|
1951
1940
|
const deviceEntry = await this.getDeviceByID(mail.sender);
|
|
1952
1941
|
const [user, _err] = await this.fetchUser(mail.authorID);
|
|
1953
1942
|
if (deviceEntry && user) {
|
|
1954
|
-
void this.createSession(deviceEntry, user,
|
|
1943
|
+
void this.createSession(deviceEntry, user, new Uint8Array(), mail.group, uuid.v4(), false, true);
|
|
1955
1944
|
}
|
|
1956
1945
|
};
|
|
1957
1946
|
switch (mail.mailType) {
|
|
@@ -1972,8 +1961,8 @@ export class Client {
|
|
|
1972
1961
|
try {
|
|
1973
1962
|
debugLibvexDm("readMail initial: abort (otk index mismatch)", {
|
|
1974
1963
|
mailID: mail.mailID,
|
|
1975
|
-
preKeyIndex: String(preKeyIndex),
|
|
1976
1964
|
otkIndex: String(otk?.index ?? "null"),
|
|
1965
|
+
preKeyIndex: String(preKeyIndex),
|
|
1977
1966
|
thisDevice: this.getDevice().deviceID,
|
|
1978
1967
|
});
|
|
1979
1968
|
}
|
|
@@ -2000,8 +1989,8 @@ export class Client {
|
|
|
2000
1989
|
if (libvexDebugDmEnabled()) {
|
|
2001
1990
|
try {
|
|
2002
1991
|
debugLibvexDm("readMail initial: abort (IK_A null, Ed→X25519?)", {
|
|
2003
|
-
mailID: mail.mailID,
|
|
2004
1992
|
fips: String(fipsRead),
|
|
1993
|
+
mailID: mail.mailID,
|
|
2005
1994
|
thisDevice: this.getDevice().deviceID,
|
|
2006
1995
|
});
|
|
2007
1996
|
}
|
|
@@ -2095,9 +2084,9 @@ export class Client {
|
|
|
2095
2084
|
try {
|
|
2096
2085
|
debugLibvexDm("readMail initial: ok (emit message)", {
|
|
2097
2086
|
mailID: mail.mailID,
|
|
2087
|
+
plaintextLen: String(plaintext.length),
|
|
2098
2088
|
preKeyIndex: String(preKeyIndex),
|
|
2099
2089
|
thisDevice: this.getDevice().deviceID,
|
|
2100
|
-
plaintextLen: String(plaintext.length),
|
|
2101
2090
|
});
|
|
2102
2091
|
}
|
|
2103
2092
|
catch {
|
|
@@ -2209,24 +2198,10 @@ export class Client {
|
|
|
2209
2198
|
}
|
|
2210
2199
|
else {
|
|
2211
2200
|
void healSession();
|
|
2212
|
-
|
|
2213
|
-
const message = {
|
|
2214
|
-
authorID: mail.authorID,
|
|
2215
|
-
decrypted: false,
|
|
2216
|
-
direction: "incoming",
|
|
2217
|
-
forward: mail.forward,
|
|
2218
|
-
group: mail.group
|
|
2219
|
-
? uuid.stringify(mail.group)
|
|
2220
|
-
: null,
|
|
2201
|
+
this.emitter.emit("retryRequest", {
|
|
2221
2202
|
mailID: mail.mailID,
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
readerID: mail.readerID,
|
|
2225
|
-
recipient: mail.recipient,
|
|
2226
|
-
sender: mail.sender,
|
|
2227
|
-
timestamp: timestamp,
|
|
2228
|
-
};
|
|
2229
|
-
this.emitter.emit("message", message);
|
|
2203
|
+
source: "decrypt_failure",
|
|
2204
|
+
});
|
|
2230
2205
|
}
|
|
2231
2206
|
break;
|
|
2232
2207
|
}
|
|
@@ -2284,6 +2259,15 @@ export class Client {
|
|
|
2284
2259
|
"/devices", msgpack.encode(devMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
2285
2260
|
return decodeAxios(DeviceRegistrationResultCodec, res.data);
|
|
2286
2261
|
}
|
|
2262
|
+
async rejectDeviceRequest(requestID) {
|
|
2263
|
+
await this.http.post(this.prefixes.HTTP +
|
|
2264
|
+
this.host +
|
|
2265
|
+
"/user/" +
|
|
2266
|
+
this.getUser().userID +
|
|
2267
|
+
"/devices/requests/" +
|
|
2268
|
+
requestID +
|
|
2269
|
+
"/reject");
|
|
2270
|
+
}
|
|
2287
2271
|
async respond(msg) {
|
|
2288
2272
|
const response = {
|
|
2289
2273
|
signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
|
|
@@ -2370,6 +2354,25 @@ export class Client {
|
|
|
2370
2354
|
}
|
|
2371
2355
|
return device;
|
|
2372
2356
|
}
|
|
2357
|
+
/**
|
|
2358
|
+
* `xDHAsync` and other helpers in `@vex-chat/crypto` use the process-wide
|
|
2359
|
+
* active profile. When several {@link Client} instances use different
|
|
2360
|
+
* `cryptoProfile` values, scope the global to this instance for the duration
|
|
2361
|
+
* of that crypto work.
|
|
2362
|
+
*/
|
|
2363
|
+
async runWithThisCryptoProfile(fn) {
|
|
2364
|
+
const prev = getCryptoProfile();
|
|
2365
|
+
if (prev === this.cryptoProfile) {
|
|
2366
|
+
return await fn();
|
|
2367
|
+
}
|
|
2368
|
+
setCryptoProfile(this.cryptoProfile);
|
|
2369
|
+
try {
|
|
2370
|
+
return await fn();
|
|
2371
|
+
}
|
|
2372
|
+
finally {
|
|
2373
|
+
setCryptoProfile(prev);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2373
2376
|
/* header is 32 bytes and is either empty
|
|
2374
2377
|
or contains an HMAC of the message with
|
|
2375
2378
|
a derived SK */
|
|
@@ -2419,9 +2422,9 @@ export class Client {
|
|
|
2419
2422
|
if (!session || retry) {
|
|
2420
2423
|
if (libvexDebugDmEnabled()) {
|
|
2421
2424
|
debugLibvexDm("sendMail: createSession path", {
|
|
2425
|
+
hasSession: String(!!session),
|
|
2422
2426
|
peerDevice: device.deviceID,
|
|
2423
2427
|
retry: String(retry),
|
|
2424
|
-
hasSession: String(!!session),
|
|
2425
2428
|
});
|
|
2426
2429
|
}
|
|
2427
2430
|
await this.createSession(device, user, msg, group, mailID, forward, false);
|
|
@@ -2541,11 +2544,11 @@ export class Client {
|
|
|
2541
2544
|
const deviceList = [...deviceListRaw].sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
|
|
2542
2545
|
if (libvexDebugDmEnabled()) {
|
|
2543
2546
|
debugLibvexDm("sendMessage: peer device list (merged, sorted)", {
|
|
2544
|
-
userID,
|
|
2545
2547
|
nAfterBackoff: String(afterBackoff.length),
|
|
2546
2548
|
nMerged: String(deviceListRaw.length),
|
|
2547
2549
|
nSorted: String(deviceList.length),
|
|
2548
2550
|
ourDevice: this.getDevice().deviceID,
|
|
2551
|
+
userID,
|
|
2549
2552
|
});
|
|
2550
2553
|
for (const [i, d] of deviceList.entries()) {
|
|
2551
2554
|
debugLibvexDm(`sendMessage: device[${String(i)}]`, {
|
|
@@ -2562,8 +2565,8 @@ export class Client {
|
|
|
2562
2565
|
try {
|
|
2563
2566
|
if (libvexDebugDmEnabled()) {
|
|
2564
2567
|
debugLibvexDm("sendMessage: sendMail start", {
|
|
2565
|
-
recipientDevice: device.deviceID,
|
|
2566
2568
|
mailID: messageMailID,
|
|
2569
|
+
recipientDevice: device.deviceID,
|
|
2567
2570
|
});
|
|
2568
2571
|
}
|
|
2569
2572
|
await this.sendMail(device, userEntry, XUtils.decodeUTF8(message), null, messageMailID, false);
|