@vex-chat/libvex 2.0.0 → 5.0.0
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 +3 -2
- package/dist/Client.d.ts +83 -60
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +161 -275
- package/dist/Client.js.map +1 -1
- package/dist/Storage.d.ts +3 -3
- package/dist/codec.d.ts +4 -4
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +4 -4
- package/dist/codec.js.map +1 -1
- package/dist/codecs.d.ts +0 -4
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +0 -1
- package/dist/codecs.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/node.d.ts +2 -1
- package/dist/keystore/node.d.ts.map +1 -1
- package/dist/keystore/node.js +9 -3
- package/dist/keystore/node.js.map +1 -1
- package/dist/preset/common.d.ts +1 -3
- package/dist/preset/common.d.ts.map +1 -1
- package/dist/preset/node.d.ts +1 -2
- package/dist/preset/node.d.ts.map +1 -1
- package/dist/preset/node.js +3 -7
- package/dist/preset/node.js.map +1 -1
- package/dist/preset/test.d.ts +0 -1
- package/dist/preset/test.d.ts.map +1 -1
- package/dist/preset/test.js +1 -15
- package/dist/preset/test.js.map +1 -1
- package/dist/storage/node.d.ts +1 -2
- package/dist/storage/node.d.ts.map +1 -1
- package/dist/storage/node.js +2 -8
- package/dist/storage/node.js.map +1 -1
- package/dist/storage/sqlite.d.ts +11 -3
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +36 -33
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transport/types.d.ts +0 -6
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/types/crypto.d.ts +5 -2
- package/dist/types/crypto.d.ts.map +1 -1
- package/dist/types/crypto.js +2 -2
- package/dist/types/identity.d.ts +6 -1
- package/dist/types/identity.d.ts.map +1 -1
- package/dist/types/identity.js +1 -1
- package/package.json +20 -12
- package/src/Client.ts +220 -428
- package/src/Storage.ts +3 -3
- package/src/__tests__/codec.test.ts +26 -21
- package/src/__tests__/harness/platform-transports.ts +2 -15
- package/src/__tests__/harness/poison-node-imports.ts +0 -1
- package/src/__tests__/harness/shared-suite.ts +1 -20
- package/src/__tests__/platform-browser.test.ts +5 -10
- package/src/__tests__/platform-node.test.ts +1 -2
- package/src/codec.ts +4 -4
- package/src/codecs.ts +0 -1
- package/src/index.ts +9 -2
- package/src/keystore/node.ts +14 -3
- package/src/preset/common.ts +1 -7
- package/src/preset/node.ts +3 -19
- package/src/preset/test.ts +1 -18
- package/src/storage/node.ts +2 -13
- package/src/storage/sqlite.ts +44 -65
- package/src/transport/types.ts +0 -7
- package/src/types/crypto.ts +5 -2
- package/src/types/identity.ts +6 -1
- package/dist/utils/createLogger.d.ts +0 -6
- package/dist/utils/createLogger.d.ts.map +0 -1
- package/dist/utils/createLogger.js +0 -27
- package/dist/utils/createLogger.js.map +0 -1
- package/src/utils/createLogger.ts +0 -37
package/src/Client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Storage } from "./Storage.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { WebSocketLike } from "./transport/types.js";
|
|
3
3
|
import type {
|
|
4
4
|
PreKeysCrypto,
|
|
5
5
|
SessionCrypto,
|
|
@@ -98,7 +98,6 @@ import {
|
|
|
98
98
|
WhoamiCodec,
|
|
99
99
|
} from "./codecs.js";
|
|
100
100
|
import { capitalize } from "./utils/capitalize.js";
|
|
101
|
-
import { formatBytes } from "./utils/formatBytes.js";
|
|
102
101
|
import { sqlSessionToCrypto } from "./utils/sqlSessionToCrypto.js";
|
|
103
102
|
import { uuidToUint8 } from "./utils/uint8uuid.js";
|
|
104
103
|
|
|
@@ -151,32 +150,12 @@ export type { Device } from "@vex-chat/types";
|
|
|
151
150
|
export interface ClientOptions {
|
|
152
151
|
/** Folder path where the sqlite file is created. */
|
|
153
152
|
dbFolder?: string;
|
|
154
|
-
/** Logging level for storage/database logs. */
|
|
155
|
-
dbLogLevel?:
|
|
156
|
-
| "debug"
|
|
157
|
-
| "error"
|
|
158
|
-
| "http"
|
|
159
|
-
| "info"
|
|
160
|
-
| "silly"
|
|
161
|
-
| "verbose"
|
|
162
|
-
| "warn";
|
|
163
153
|
/** Platform label for device registration (e.g. "ios", "macos", "linux"). */
|
|
164
154
|
deviceName?: string;
|
|
165
155
|
/** API host without protocol. Defaults to `api.vex.wtf`. */
|
|
166
156
|
host?: string;
|
|
167
157
|
/** Use sqlite in-memory mode (`:memory:`) instead of a file. */
|
|
168
158
|
inMemoryDb?: boolean;
|
|
169
|
-
/** Logger implementation. When omitted, defaults to winston (Node.js). */
|
|
170
|
-
logger?: Logger;
|
|
171
|
-
/** Logging level for client runtime logs. */
|
|
172
|
-
logLevel?:
|
|
173
|
-
| "debug"
|
|
174
|
-
| "error"
|
|
175
|
-
| "http"
|
|
176
|
-
| "info"
|
|
177
|
-
| "silly"
|
|
178
|
-
| "verbose"
|
|
179
|
-
| "warn";
|
|
180
159
|
/** Whether local message history should be persisted by default storage. */
|
|
181
160
|
saveHistory?: boolean;
|
|
182
161
|
/** Use `http/ws` instead of `https/wss`. Intended for local/dev environments. */
|
|
@@ -366,6 +345,33 @@ const mailInboxEntry = z.tuple([
|
|
|
366
345
|
z.string(),
|
|
367
346
|
]);
|
|
368
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Event signatures emitted by {@link Client}.
|
|
350
|
+
*
|
|
351
|
+
* Used as the type parameter for {@link Client.on}, {@link Client.off},
|
|
352
|
+
* and {@link Client.once}.
|
|
353
|
+
*/
|
|
354
|
+
export interface ClientEvents {
|
|
355
|
+
/** The client has been shut down (via {@link Client.close}). */
|
|
356
|
+
closed: () => void;
|
|
357
|
+
/** WebSocket authorized by the server; pre-auth setup begins. */
|
|
358
|
+
connected: () => void;
|
|
359
|
+
/** Mail decryption pass is in progress. */
|
|
360
|
+
decryptingMail: () => void;
|
|
361
|
+
/** WebSocket connection lost. */
|
|
362
|
+
disconnect: () => void;
|
|
363
|
+
/** Progress update for a file upload or download. */
|
|
364
|
+
fileProgress: (progress: FileProgress) => void;
|
|
365
|
+
/** A direct or group message was sent or received. */
|
|
366
|
+
message: (message: Message) => void;
|
|
367
|
+
/** A permission grant was created or modified. */
|
|
368
|
+
permission: (permission: Permission) => void;
|
|
369
|
+
/** Post-auth setup complete — safe to call messaging/user APIs. */
|
|
370
|
+
ready: () => void;
|
|
371
|
+
/** A new encryption session was established with a peer device. */
|
|
372
|
+
session: (session: Session, user: User) => void;
|
|
373
|
+
}
|
|
374
|
+
|
|
369
375
|
/**
|
|
370
376
|
* @ignore
|
|
371
377
|
*/
|
|
@@ -490,25 +496,6 @@ export interface Users {
|
|
|
490
496
|
retrieve: (userID: string) => Promise<[null | User, AxiosError | null]>;
|
|
491
497
|
}
|
|
492
498
|
|
|
493
|
-
/**
|
|
494
|
-
* VexFile is an uploaded encrypted file.
|
|
495
|
-
*
|
|
496
|
-
* Common fields:
|
|
497
|
-
* - `fileID`: file identifier
|
|
498
|
-
* - `owner`: owner device/user ID
|
|
499
|
-
* - `nonce`: file encryption nonce (hex)
|
|
500
|
-
*
|
|
501
|
-
* @example
|
|
502
|
-
* ```ts
|
|
503
|
-
* const file: VexFile = {
|
|
504
|
-
* fileID: "bb1c3fd1-4928-48ab-9d09-3ea0972fbd9d",
|
|
505
|
-
* owner: "9b0f3f46-06ad-4bc4-8adf-4de10e13cb9c",
|
|
506
|
-
* nonce: "aa6c8d42f3fdd032a1e9fced4be379582d26ce8f69822d64",
|
|
507
|
-
* };
|
|
508
|
-
* ```
|
|
509
|
-
*/
|
|
510
|
-
export type VexFile = FileSQL;
|
|
511
|
-
|
|
512
499
|
/**
|
|
513
500
|
* Client provides an interface for you to use a vex chat server and
|
|
514
501
|
* send end to end encrypted messages to other users.
|
|
@@ -527,12 +514,10 @@ export type VexFile = FileSQL;
|
|
|
527
514
|
* await client.register(Client.randomUsername());
|
|
528
515
|
* await client.login();
|
|
529
516
|
*
|
|
530
|
-
* // The
|
|
531
|
-
* //
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* client.on("authed", async () => {
|
|
535
|
-
* const me = await client.users.me();
|
|
517
|
+
* // The ready event fires after connect() finishes post-auth setup.
|
|
518
|
+
* // Wait for it before performing messaging or user operations.
|
|
519
|
+
* client.on("ready", async () => {
|
|
520
|
+
* const me = client.me.user();
|
|
536
521
|
*
|
|
537
522
|
* // send a message
|
|
538
523
|
* await client.messages.send(me.userID, "Hello world!");
|
|
@@ -549,19 +534,23 @@ export type VexFile = FileSQL;
|
|
|
549
534
|
*/
|
|
550
535
|
|
|
551
536
|
/**
|
|
552
|
-
*
|
|
537
|
+
* VexFile is an uploaded encrypted file.
|
|
538
|
+
*
|
|
539
|
+
* Common fields:
|
|
540
|
+
* - `fileID`: file identifier
|
|
541
|
+
* - `owner`: owner device/user ID
|
|
542
|
+
* - `nonce`: file encryption nonce (hex)
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```ts
|
|
546
|
+
* const file: VexFile = {
|
|
547
|
+
* fileID: "bb1c3fd1-4928-48ab-9d09-3ea0972fbd9d",
|
|
548
|
+
* owner: "9b0f3f46-06ad-4bc4-8adf-4de10e13cb9c",
|
|
549
|
+
* nonce: "aa6c8d42f3fdd032a1e9fced4be379582d26ce8f69822d64",
|
|
550
|
+
* };
|
|
551
|
+
* ```
|
|
553
552
|
*/
|
|
554
|
-
|
|
555
|
-
closed: () => void;
|
|
556
|
-
connected: () => void;
|
|
557
|
-
decryptingMail: () => void;
|
|
558
|
-
disconnect: () => void;
|
|
559
|
-
fileProgress: (progress: FileProgress) => void;
|
|
560
|
-
message: (message: Message) => void;
|
|
561
|
-
permission: (permission: Permission) => void;
|
|
562
|
-
ready: () => void;
|
|
563
|
-
session: (session: Session, user: User) => void;
|
|
564
|
-
}
|
|
553
|
+
export type VexFile = FileSQL;
|
|
565
554
|
|
|
566
555
|
export class Client {
|
|
567
556
|
/**
|
|
@@ -580,38 +569,43 @@ export class Client {
|
|
|
580
569
|
|
|
581
570
|
private static readonly NOT_FOUND_TTL = 30 * 60 * 1000;
|
|
582
571
|
|
|
572
|
+
/**
|
|
573
|
+
* Browser-safe NODE_ENV accessor.
|
|
574
|
+
* Uses indirect lookup so the bare `process` global never appears in
|
|
575
|
+
* source that the platform-guard plugin scans.
|
|
576
|
+
*/
|
|
583
577
|
/**
|
|
584
578
|
* Channel operations.
|
|
585
579
|
*/
|
|
586
580
|
public channels: Channels = {
|
|
587
581
|
/**
|
|
588
582
|
* Creates a new channel in a server.
|
|
589
|
-
* @param name
|
|
590
|
-
* @param serverID
|
|
583
|
+
* @param name - The channel name.
|
|
584
|
+
* @param serverID - The server to create the channel in.
|
|
591
585
|
*
|
|
592
|
-
* @returns
|
|
586
|
+
* @returns The created Channel object.
|
|
593
587
|
*/
|
|
594
588
|
create: this.createChannel.bind(this),
|
|
595
589
|
/**
|
|
596
590
|
* Deletes a channel.
|
|
597
|
-
* @param channelID
|
|
591
|
+
* @param channelID - The channel to delete.
|
|
598
592
|
*/
|
|
599
593
|
delete: this.deleteChannel.bind(this),
|
|
600
594
|
/**
|
|
601
595
|
* Retrieves all channels in a server.
|
|
602
596
|
*
|
|
603
|
-
* @returns
|
|
597
|
+
* @returns The list of Channel objects.
|
|
604
598
|
*/
|
|
605
599
|
retrieve: this.getChannelList.bind(this),
|
|
606
600
|
/**
|
|
607
601
|
* Retrieves channel details by its unique channelID.
|
|
608
602
|
*
|
|
609
|
-
* @returns
|
|
603
|
+
* @returns The Channel object, or null.
|
|
610
604
|
*/
|
|
611
605
|
retrieveByID: this.getChannelByID.bind(this),
|
|
612
606
|
/**
|
|
613
607
|
* Retrieves a channel's userlist.
|
|
614
|
-
* @param channelID
|
|
608
|
+
* @param channelID - The channel to retrieve the userlist for.
|
|
615
609
|
*/
|
|
616
610
|
userList: this.getUserList.bind(this),
|
|
617
611
|
};
|
|
@@ -644,9 +638,9 @@ export class Client {
|
|
|
644
638
|
public files: Files = {
|
|
645
639
|
/**
|
|
646
640
|
* Uploads an encrypted file and returns the details and the secret key.
|
|
647
|
-
* @param file
|
|
641
|
+
* @param file - The file bytes.
|
|
648
642
|
*
|
|
649
|
-
* @returns
|
|
643
|
+
* @returns `[details, key]` — file metadata and the encryption key.
|
|
650
644
|
*/
|
|
651
645
|
create: this.createFile.bind(this),
|
|
652
646
|
retrieve: this.retrieveFile.bind(this),
|
|
@@ -677,19 +671,17 @@ export class Client {
|
|
|
677
671
|
*/
|
|
678
672
|
public me: Me = {
|
|
679
673
|
/**
|
|
680
|
-
* Retrieves current device details
|
|
674
|
+
* Retrieves current device details.
|
|
681
675
|
*
|
|
682
|
-
* @returns
|
|
676
|
+
* @returns The logged in device's Device object.
|
|
683
677
|
*/
|
|
684
678
|
device: this.getDevice.bind(this),
|
|
685
|
-
/**
|
|
686
|
-
* Changes your avatar.
|
|
687
|
-
*/
|
|
679
|
+
/** Changes your avatar. */
|
|
688
680
|
setAvatar: this.uploadAvatar.bind(this),
|
|
689
681
|
/**
|
|
690
|
-
* Retrieves your user information
|
|
682
|
+
* Retrieves your user information.
|
|
691
683
|
*
|
|
692
|
-
* @returns
|
|
684
|
+
* @returns The logged in user's User object.
|
|
693
685
|
*/
|
|
694
686
|
user: this.getUser.bind(this),
|
|
695
687
|
};
|
|
@@ -707,29 +699,29 @@ export class Client {
|
|
|
707
699
|
delete: this.deleteHistory.bind(this),
|
|
708
700
|
/**
|
|
709
701
|
* Send a group message to a channel.
|
|
710
|
-
* @param channelID
|
|
711
|
-
* @param message
|
|
702
|
+
* @param channelID - The channel to send a message to.
|
|
703
|
+
* @param message - The message to send.
|
|
712
704
|
*/
|
|
713
705
|
group: this.sendGroupMessage.bind(this),
|
|
714
706
|
purge: this.purgeHistory.bind(this),
|
|
715
707
|
/**
|
|
716
708
|
* Gets the message history with a specific userID.
|
|
717
|
-
* @param userID
|
|
709
|
+
* @param userID - The user to retrieve message history for.
|
|
718
710
|
*
|
|
719
|
-
* @returns
|
|
711
|
+
* @returns The list of Message objects.
|
|
720
712
|
*/
|
|
721
713
|
retrieve: this.getMessageHistory.bind(this),
|
|
722
714
|
/**
|
|
723
|
-
* Gets the group message history
|
|
724
|
-
* @param
|
|
715
|
+
* Gets the group message history for a channel.
|
|
716
|
+
* @param channelID - The channel to retrieve message history for.
|
|
725
717
|
*
|
|
726
|
-
* @returns
|
|
718
|
+
* @returns The list of Message objects.
|
|
727
719
|
*/
|
|
728
720
|
retrieveGroup: this.getGroupHistory.bind(this),
|
|
729
721
|
/**
|
|
730
722
|
* Send a direct message.
|
|
731
|
-
* @param userID
|
|
732
|
-
* @param message
|
|
723
|
+
* @param userID - The user to send a message to.
|
|
724
|
+
* @param message - The message to send.
|
|
733
725
|
*/
|
|
734
726
|
send: this.sendMessage.bind(this),
|
|
735
727
|
};
|
|
@@ -764,27 +756,27 @@ export class Client {
|
|
|
764
756
|
public servers: Servers = {
|
|
765
757
|
/**
|
|
766
758
|
* Creates a new server.
|
|
767
|
-
* @param name
|
|
759
|
+
* @param name - The server name.
|
|
768
760
|
*
|
|
769
|
-
* @returns
|
|
761
|
+
* @returns The created Server object.
|
|
770
762
|
*/
|
|
771
763
|
create: this.createServer.bind(this),
|
|
772
764
|
/**
|
|
773
765
|
* Deletes a server.
|
|
774
|
-
* @param serverID
|
|
766
|
+
* @param serverID - The server to delete.
|
|
775
767
|
*/
|
|
776
768
|
delete: this.deleteServer.bind(this),
|
|
777
769
|
leave: this.leaveServer.bind(this),
|
|
778
770
|
/**
|
|
779
771
|
* Retrieves all servers the logged in user has access to.
|
|
780
772
|
*
|
|
781
|
-
* @returns
|
|
773
|
+
* @returns The list of Server objects.
|
|
782
774
|
*/
|
|
783
775
|
retrieve: this.getServerList.bind(this),
|
|
784
776
|
/**
|
|
785
777
|
* Retrieves server details by its unique serverID.
|
|
786
778
|
*
|
|
787
|
-
* @returns
|
|
779
|
+
* @returns The requested Server object, or null if the id does not exist.
|
|
788
780
|
*/
|
|
789
781
|
retrieveByID: this.getServerByID.bind(this),
|
|
790
782
|
};
|
|
@@ -794,25 +786,24 @@ export class Client {
|
|
|
794
786
|
*/
|
|
795
787
|
public sessions: Sessions = {
|
|
796
788
|
/**
|
|
797
|
-
* Marks a
|
|
789
|
+
* Marks a session as verified, implying that the user has confirmed
|
|
798
790
|
* that the session mnemonic matches with the other user.
|
|
799
|
-
* @param sessionID
|
|
800
|
-
* @param status Optionally, what to mark it as. Defaults to true.
|
|
791
|
+
* @param sessionID - The session to mark.
|
|
801
792
|
*/
|
|
802
793
|
markVerified: this.markSessionVerified.bind(this),
|
|
803
794
|
|
|
804
795
|
/**
|
|
805
796
|
* Gets all encryption sessions.
|
|
806
797
|
*
|
|
807
|
-
* @returns
|
|
798
|
+
* @returns The list of Session encryption sessions.
|
|
808
799
|
*/
|
|
809
800
|
retrieve: this.getSessionList.bind(this),
|
|
810
801
|
|
|
811
802
|
/**
|
|
812
803
|
* Returns a mnemonic for the session, to verify with the other user.
|
|
813
|
-
* @param session
|
|
804
|
+
* @param session - The session to get the mnemonic for.
|
|
814
805
|
*
|
|
815
|
-
* @returns
|
|
806
|
+
* @returns The mnemonic representation of the session.
|
|
816
807
|
*/
|
|
817
808
|
verify: (session: SessionSQL) => Client.getMnemonic(session),
|
|
818
809
|
};
|
|
@@ -830,14 +821,14 @@ export class Client {
|
|
|
830
821
|
/**
|
|
831
822
|
* Retrieves the list of users you can currently access, or are already familiar with.
|
|
832
823
|
*
|
|
833
|
-
* @returns
|
|
824
|
+
* @returns The list of User objects.
|
|
834
825
|
*/
|
|
835
826
|
familiars: this.getFamiliars.bind(this),
|
|
836
827
|
/**
|
|
837
828
|
* Retrieves a user's information by a string identifier.
|
|
838
|
-
* @param identifier
|
|
829
|
+
* @param identifier - A userID, hex string public key, or a username.
|
|
839
830
|
*
|
|
840
|
-
* @returns
|
|
831
|
+
* @returns The user's User object, or null if the user does not exist.
|
|
841
832
|
*/
|
|
842
833
|
retrieve: this.fetchUser.bind(this),
|
|
843
834
|
};
|
|
@@ -862,8 +853,6 @@ export class Client {
|
|
|
862
853
|
private readonly http: AxiosInstance;
|
|
863
854
|
private readonly idKeys: KeyPair | null;
|
|
864
855
|
private isAlive: boolean = true;
|
|
865
|
-
private readonly log: Logger;
|
|
866
|
-
|
|
867
856
|
private readonly mailInterval?: NodeJS.Timeout;
|
|
868
857
|
|
|
869
858
|
private manuallyClosing: boolean = false;
|
|
@@ -881,6 +870,7 @@ export class Client {
|
|
|
881
870
|
| { HTTP: "https://"; WS: "wss://" };
|
|
882
871
|
|
|
883
872
|
private reading: boolean = false;
|
|
873
|
+
private readonly seenMailIDs: Set<string> = new Set();
|
|
884
874
|
private sessionRecords: Record<string, SessionCrypto> = {};
|
|
885
875
|
// these are created from one set of sign keys
|
|
886
876
|
private readonly signKeys: KeyPair;
|
|
@@ -901,16 +891,18 @@ export class Client {
|
|
|
901
891
|
// (no super — composition, not inheritance)
|
|
902
892
|
this.options = options;
|
|
903
893
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
894
|
+
if (options?.unsafeHttp) {
|
|
895
|
+
const env = Client.getNodeEnv();
|
|
896
|
+
if (env !== "development" && env !== "test") {
|
|
897
|
+
throw new Error(
|
|
898
|
+
"unsafeHttp is only allowed when NODE_ENV is 'development' or 'test'. " +
|
|
899
|
+
"Set NODE_ENV=development to use unencrypted transport.",
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
this.prefixes = { HTTP: "http://", WS: "ws://" };
|
|
903
|
+
} else {
|
|
904
|
+
this.prefixes = { HTTP: "https://", WS: "wss://" };
|
|
905
|
+
}
|
|
914
906
|
|
|
915
907
|
this.signKeys = privateKey
|
|
916
908
|
? xSignKeyPairFromSecret(XUtils.decodeHex(privateKey))
|
|
@@ -936,46 +928,21 @@ export class Client {
|
|
|
936
928
|
}
|
|
937
929
|
this.database = storage;
|
|
938
930
|
|
|
939
|
-
this.database.on("error", (
|
|
940
|
-
this.log.error(error.toString());
|
|
931
|
+
this.database.on("error", (_error: Error) => {
|
|
941
932
|
void this.close(true);
|
|
942
933
|
});
|
|
943
934
|
|
|
944
935
|
this.http = axios.create({ responseType: "arraybuffer" });
|
|
945
936
|
|
|
946
|
-
|
|
947
|
-
this.socket = new WebSocketAdapter("ws://localhost:1234");
|
|
937
|
+
this.socket = new WebSocketAdapter(this.prefixes.WS + this.host);
|
|
948
938
|
this.socket.onerror = () => {};
|
|
949
|
-
|
|
950
|
-
// Strip the `logger` field before stringifying — when a consumer
|
|
951
|
-
// passes a Winston logger instance (which has a circular
|
|
952
|
-
// `_readableState.pipes[0].parent` back-reference from the
|
|
953
|
-
// underlying file transport), JSON.stringify throws
|
|
954
|
-
// `TypeError: Converting circular structure to JSON`.
|
|
955
|
-
const { logger: _logger, ...safeOptions } = options ?? {};
|
|
956
|
-
this.log.info(
|
|
957
|
-
"Client debug information: " +
|
|
958
|
-
JSON.stringify(
|
|
959
|
-
{
|
|
960
|
-
dbPath: this.dbPath,
|
|
961
|
-
environment: {
|
|
962
|
-
platform: this.options?.deviceName ?? "unknown",
|
|
963
|
-
},
|
|
964
|
-
host: this.getHost(),
|
|
965
|
-
options: safeOptions,
|
|
966
|
-
publicKey: this.getKeys().public,
|
|
967
|
-
},
|
|
968
|
-
null,
|
|
969
|
-
4,
|
|
970
|
-
),
|
|
971
|
-
);
|
|
972
939
|
}
|
|
973
940
|
/**
|
|
974
941
|
* Creates and initializes a client in one step.
|
|
975
942
|
*
|
|
976
|
-
* @param privateKey
|
|
977
|
-
* @param options Runtime options.
|
|
978
|
-
* @param storage
|
|
943
|
+
* @param privateKey - Hex secret key. When omitted, a fresh key is generated.
|
|
944
|
+
* @param options - Runtime options.
|
|
945
|
+
* @param storage - Custom storage backend implementing {@link Storage}.
|
|
979
946
|
*
|
|
980
947
|
* @example
|
|
981
948
|
* ```ts
|
|
@@ -987,41 +954,22 @@ export class Client {
|
|
|
987
954
|
options?: ClientOptions,
|
|
988
955
|
storage?: Storage,
|
|
989
956
|
): Promise<Client> => {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const { createLogger: makeLog } =
|
|
993
|
-
await import("./utils/createLogger.js");
|
|
994
|
-
opts = {
|
|
995
|
-
...opts,
|
|
996
|
-
logger: makeLog("libvex", opts?.logLevel),
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
// Lazily create Node Storage only on the Node path (no logger override).
|
|
1000
|
-
// When a logger is provided (browser/RN), the caller must supply storage
|
|
1001
|
-
// via BootstrapConfig.createStorage() — there is no Node fallback.
|
|
957
|
+
const opts = options;
|
|
958
|
+
const sk = privateKey ?? XUtils.encodeHex(xSignKeyPair().secretKey);
|
|
1002
959
|
let resolvedStorage = storage;
|
|
1003
960
|
if (!resolvedStorage) {
|
|
1004
|
-
if (opts.logger) {
|
|
1005
|
-
throw new Error(
|
|
1006
|
-
"No storage provided. When using a custom logger (browser/RN), pass storage from your BootstrapConfig.",
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
961
|
const { createNodeStorage } = await import("./storage/node.js");
|
|
1010
|
-
const dbFileName = opts
|
|
962
|
+
const dbFileName = opts?.inMemoryDb
|
|
1011
963
|
? ":memory:"
|
|
1012
964
|
: XUtils.encodeHex(
|
|
1013
|
-
xSignKeyPairFromSecret(XUtils.decodeHex(
|
|
1014
|
-
.publicKey,
|
|
965
|
+
xSignKeyPairFromSecret(XUtils.decodeHex(sk)).publicKey,
|
|
1015
966
|
) + ".sqlite";
|
|
1016
|
-
const dbPath = opts
|
|
967
|
+
const dbPath = opts?.dbFolder
|
|
1017
968
|
? opts.dbFolder + "/" + dbFileName
|
|
1018
969
|
: dbFileName;
|
|
1019
|
-
resolvedStorage = createNodeStorage(
|
|
1020
|
-
dbPath,
|
|
1021
|
-
privateKey || XUtils.encodeHex(xSignKeyPair().secretKey),
|
|
1022
|
-
);
|
|
970
|
+
resolvedStorage = createNodeStorage(dbPath, sk);
|
|
1023
971
|
}
|
|
1024
|
-
const client = new Client(
|
|
972
|
+
const client = new Client(sk, opts, resolvedStorage);
|
|
1025
973
|
await client.init();
|
|
1026
974
|
return client;
|
|
1027
975
|
};
|
|
@@ -1029,7 +977,7 @@ export class Client {
|
|
|
1029
977
|
/**
|
|
1030
978
|
* Generates an ed25519 secret key as a hex string.
|
|
1031
979
|
*
|
|
1032
|
-
* @returns
|
|
980
|
+
* @returns A secret key to use for the client. Save it permanently somewhere safe.
|
|
1033
981
|
*/
|
|
1034
982
|
public static generateSecretKey(): string {
|
|
1035
983
|
return XUtils.encodeHex(xSignKeyPair().secretKey);
|
|
@@ -1038,7 +986,7 @@ export class Client {
|
|
|
1038
986
|
/**
|
|
1039
987
|
* Generates a random username using bip39.
|
|
1040
988
|
*
|
|
1041
|
-
* @returns
|
|
989
|
+
* @returns The username.
|
|
1042
990
|
*/
|
|
1043
991
|
public static randomUsername() {
|
|
1044
992
|
const IKM = XUtils.decodeHex(XUtils.encodeHex(xRandomBytes(16)));
|
|
@@ -1075,10 +1023,53 @@ export class Client {
|
|
|
1075
1023
|
return xMnemonic(xKDF(XUtils.decodeHex(session.fingerprint)));
|
|
1076
1024
|
}
|
|
1077
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Browser-safe NODE_ENV accessor.
|
|
1028
|
+
* Uses indirect lookup so the bare `process` global never appears in
|
|
1029
|
+
* source that the platform-guard plugin scans.
|
|
1030
|
+
*/
|
|
1031
|
+
private static getNodeEnv(): string | undefined {
|
|
1032
|
+
try {
|
|
1033
|
+
const g = Object.getOwnPropertyDescriptor(
|
|
1034
|
+
globalThis,
|
|
1035
|
+
"\u0070rocess",
|
|
1036
|
+
);
|
|
1037
|
+
if (!g) return undefined;
|
|
1038
|
+
// Node 24+ exposes `process` as an accessor (get/set), not a value.
|
|
1039
|
+
const proc: unknown =
|
|
1040
|
+
typeof g.get === "function" ? g.get() : g.value;
|
|
1041
|
+
if (typeof proc !== "object" || proc === null) {
|
|
1042
|
+
return undefined;
|
|
1043
|
+
}
|
|
1044
|
+
const envDesc = Object.getOwnPropertyDescriptor(proc, "env");
|
|
1045
|
+
if (!envDesc) return undefined;
|
|
1046
|
+
const env: unknown =
|
|
1047
|
+
typeof envDesc.get === "function"
|
|
1048
|
+
? envDesc.get()
|
|
1049
|
+
: envDesc.value;
|
|
1050
|
+
if (typeof env !== "object" || env === null) {
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1053
|
+
const valDesc = Object.getOwnPropertyDescriptor(env, "NODE_ENV");
|
|
1054
|
+
if (!valDesc) return undefined;
|
|
1055
|
+
const val: unknown =
|
|
1056
|
+
typeof valDesc.get === "function"
|
|
1057
|
+
? valDesc.get()
|
|
1058
|
+
: valDesc.value;
|
|
1059
|
+
return typeof val === "string" ? val : undefined;
|
|
1060
|
+
} catch {
|
|
1061
|
+
return undefined;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Closes the client — disconnects the WebSocket, shuts down storage,
|
|
1067
|
+
* and emits `closed` unless `muteEvent` is `true`.
|
|
1068
|
+
*
|
|
1069
|
+
* @param muteEvent - When `true`, suppresses the `closed` event.
|
|
1070
|
+
*/
|
|
1078
1071
|
public async close(muteEvent = false): Promise<void> {
|
|
1079
1072
|
this.manuallyClosing = true;
|
|
1080
|
-
this.log.info("Manually closing client.");
|
|
1081
|
-
|
|
1082
1073
|
this.socket.close();
|
|
1083
1074
|
await this.database.close();
|
|
1084
1075
|
|
|
@@ -1102,9 +1093,12 @@ export class Client {
|
|
|
1102
1093
|
* You can check whoami() to see before calling connect().
|
|
1103
1094
|
*/
|
|
1104
1095
|
public async connect(): Promise<void> {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1096
|
+
if (!this.token) {
|
|
1097
|
+
throw new Error(
|
|
1098
|
+
"No token — call login() or loginWithDeviceKey() first.",
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
const { user } = await this.whoami();
|
|
1108
1102
|
this.setUser(user);
|
|
1109
1103
|
|
|
1110
1104
|
this.device = await this.retrieveOrCreateDevice();
|
|
@@ -1126,7 +1120,6 @@ export class Client {
|
|
|
1126
1120
|
const { deviceToken } = decodeAxios(ConnectResponseCodec, res.data);
|
|
1127
1121
|
this.http.defaults.headers.common["X-Device-Token"] = deviceToken;
|
|
1128
1122
|
|
|
1129
|
-
this.log.info("Starting websocket.");
|
|
1130
1123
|
this.initSocket();
|
|
1131
1124
|
// Yield the event loop so the WS open callback fires and sends the
|
|
1132
1125
|
// auth message before OTK generation blocks for ~5s on mobile.
|
|
@@ -1134,9 +1127,6 @@ export class Client {
|
|
|
1134
1127
|
await this.negotiateOTK();
|
|
1135
1128
|
}
|
|
1136
1129
|
|
|
1137
|
-
/**
|
|
1138
|
-
* Manually closes the client. Emits the closed event on successful shutdown.
|
|
1139
|
-
*/
|
|
1140
1130
|
/**
|
|
1141
1131
|
* Delete all local data — message history, encryption sessions, and prekeys.
|
|
1142
1132
|
* Closes the client afterward. Credentials (keychain) must be cleared by the consumer.
|
|
@@ -1172,8 +1162,8 @@ export class Client {
|
|
|
1172
1162
|
/**
|
|
1173
1163
|
* Authenticates with username/password and stores the Bearer auth token.
|
|
1174
1164
|
*
|
|
1175
|
-
* @param username Account username.
|
|
1176
|
-
* @param password Account password.
|
|
1165
|
+
* @param username - Account username.
|
|
1166
|
+
* @param password - Account password.
|
|
1177
1167
|
* @returns `{ ok: true }` on success, `{ ok: false, error }` on failure.
|
|
1178
1168
|
*
|
|
1179
1169
|
* @example
|
|
@@ -1205,7 +1195,6 @@ export class Client {
|
|
|
1205
1195
|
return { ok: true };
|
|
1206
1196
|
} catch (err: unknown) {
|
|
1207
1197
|
const error = err instanceof Error ? err.message : String(err);
|
|
1208
|
-
this.log.error("Login failed: " + error);
|
|
1209
1198
|
return { error, ok: false };
|
|
1210
1199
|
}
|
|
1211
1200
|
}
|
|
@@ -1258,7 +1247,6 @@ export class Client {
|
|
|
1258
1247
|
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
1259
1248
|
} catch (err: unknown) {
|
|
1260
1249
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1261
|
-
this.log.error("Device-key auth failed: " + error.message);
|
|
1262
1250
|
return error;
|
|
1263
1251
|
}
|
|
1264
1252
|
return null;
|
|
@@ -1271,6 +1259,7 @@ export class Client {
|
|
|
1271
1259
|
await this.http.post(this.getHost() + "/goodbye");
|
|
1272
1260
|
}
|
|
1273
1261
|
|
|
1262
|
+
/** Removes an event listener. See {@link ClientEvents} for available events. */
|
|
1274
1263
|
off<E extends keyof ClientEvents>(
|
|
1275
1264
|
event: E,
|
|
1276
1265
|
fn?: ClientEvents[E],
|
|
@@ -1285,6 +1274,7 @@ export class Client {
|
|
|
1285
1274
|
return this;
|
|
1286
1275
|
}
|
|
1287
1276
|
|
|
1277
|
+
/** Subscribes to an event. See {@link ClientEvents} for available events. */
|
|
1288
1278
|
on<E extends keyof ClientEvents>(
|
|
1289
1279
|
event: E,
|
|
1290
1280
|
fn: ClientEvents[E],
|
|
@@ -1295,6 +1285,7 @@ export class Client {
|
|
|
1295
1285
|
return this;
|
|
1296
1286
|
}
|
|
1297
1287
|
|
|
1288
|
+
/** Subscribes to an event for a single firing, then auto-removes. */
|
|
1298
1289
|
once<E extends keyof ClientEvents>(
|
|
1299
1290
|
event: E,
|
|
1300
1291
|
fn: ClientEvents[E],
|
|
@@ -1307,11 +1298,15 @@ export class Client {
|
|
|
1307
1298
|
|
|
1308
1299
|
/**
|
|
1309
1300
|
* Registers a new account on the server.
|
|
1310
|
-
* @param username The username to register. Must be unique.
|
|
1311
1301
|
*
|
|
1312
|
-
* @
|
|
1302
|
+
* @param username - The username to register. Must be unique.
|
|
1303
|
+
* @param password - Account password.
|
|
1304
|
+
* @returns `[user, null]` on success, `[null, error]` on failure.
|
|
1313
1305
|
*
|
|
1314
|
-
* @example
|
|
1306
|
+
* @example
|
|
1307
|
+
* ```ts
|
|
1308
|
+
* const [user, err] = await client.register("MyUsername", "hunter2");
|
|
1309
|
+
* ```
|
|
1315
1310
|
*/
|
|
1316
1311
|
public async register(
|
|
1317
1312
|
username: string,
|
|
@@ -1354,7 +1349,12 @@ export class Client {
|
|
|
1354
1349
|
return [this.getUser(), null];
|
|
1355
1350
|
} catch (err: unknown) {
|
|
1356
1351
|
if (isAxiosError(err) && err.response) {
|
|
1357
|
-
|
|
1352
|
+
const raw: unknown = err.response.data;
|
|
1353
|
+
const msg =
|
|
1354
|
+
raw instanceof ArrayBuffer || raw instanceof Uint8Array
|
|
1355
|
+
? new TextDecoder().decode(raw)
|
|
1356
|
+
: String(raw);
|
|
1357
|
+
return [null, new Error(msg)];
|
|
1358
1358
|
}
|
|
1359
1359
|
return [
|
|
1360
1360
|
null,
|
|
@@ -1396,7 +1396,6 @@ export class Client {
|
|
|
1396
1396
|
*/
|
|
1397
1397
|
public async whoami(): Promise<{
|
|
1398
1398
|
exp: number;
|
|
1399
|
-
token: string;
|
|
1400
1399
|
user: User;
|
|
1401
1400
|
}> {
|
|
1402
1401
|
const res = await this.http.post(this.getHost() + "/whoami");
|
|
@@ -1432,14 +1431,10 @@ export class Client {
|
|
|
1432
1431
|
|
|
1433
1432
|
// returns the file details and the encryption key
|
|
1434
1433
|
private async createFile(file: Uint8Array): Promise<[FileSQL, string]> {
|
|
1435
|
-
this.log.info("Creating file, size: " + formatBytes(file.byteLength));
|
|
1436
|
-
|
|
1437
1434
|
const nonce = xMakeNonce();
|
|
1438
1435
|
const key = xBoxKeyPair();
|
|
1439
1436
|
const box = xSecretbox(Uint8Array.from(file), nonce, key.secretKey);
|
|
1440
1437
|
|
|
1441
|
-
this.log.info("Encrypted size: " + formatBytes(box.byteLength));
|
|
1442
|
-
|
|
1443
1438
|
if (typeof FormData !== "undefined") {
|
|
1444
1439
|
const fpayload = new FormData();
|
|
1445
1440
|
fpayload.set("owner", this.getDevice().deviceID);
|
|
@@ -1500,7 +1495,8 @@ export class Client {
|
|
|
1500
1495
|
|
|
1501
1496
|
const res = await this.http.post(
|
|
1502
1497
|
this.getHost() + "/server/" + serverID + "/invites",
|
|
1503
|
-
payload,
|
|
1498
|
+
msgpack.encode(payload),
|
|
1499
|
+
{ headers: { "Content-Type": "application/msgpack" } },
|
|
1504
1500
|
);
|
|
1505
1501
|
|
|
1506
1502
|
return decodeAxios(InviteCodec, res.data);
|
|
@@ -1536,28 +1532,12 @@ export class Client {
|
|
|
1536
1532
|
): Promise<void> {
|
|
1537
1533
|
let keyBundle: KeyBundle;
|
|
1538
1534
|
|
|
1539
|
-
this.log.info(
|
|
1540
|
-
"Requesting key bundle for device: " +
|
|
1541
|
-
JSON.stringify(device, null, 4),
|
|
1542
|
-
);
|
|
1543
1535
|
try {
|
|
1544
1536
|
keyBundle = await this.retrieveKeyBundle(device.deviceID);
|
|
1545
|
-
} catch
|
|
1546
|
-
this.log.warn(
|
|
1547
|
-
"Couldn't get key bundle:",
|
|
1548
|
-
err instanceof Error ? err.message : String(err),
|
|
1549
|
-
);
|
|
1537
|
+
} catch {
|
|
1550
1538
|
return;
|
|
1551
1539
|
}
|
|
1552
1540
|
|
|
1553
|
-
this.log.warn(
|
|
1554
|
-
this.toString() +
|
|
1555
|
-
" retrieved keybundle #" +
|
|
1556
|
-
String(keyBundle.otk?.index ?? "none") +
|
|
1557
|
-
" for " +
|
|
1558
|
-
device.deviceID,
|
|
1559
|
-
);
|
|
1560
|
-
|
|
1561
1541
|
if (!this.xKeyRing) {
|
|
1562
1542
|
throw new Error("Key ring not initialized.");
|
|
1563
1543
|
}
|
|
@@ -1596,16 +1576,7 @@ export class Client {
|
|
|
1596
1576
|
|
|
1597
1577
|
// shared secret key
|
|
1598
1578
|
const SK = xKDF(IKM);
|
|
1599
|
-
this.log.info("Obtained SK, " + XUtils.encodeHex(SK));
|
|
1600
|
-
|
|
1601
1579
|
const PK = xBoxKeyPairFromSecret(SK).publicKey;
|
|
1602
|
-
this.log.info(
|
|
1603
|
-
this.toString() +
|
|
1604
|
-
" Obtained PK for " +
|
|
1605
|
-
device.deviceID +
|
|
1606
|
-
" " +
|
|
1607
|
-
XUtils.encodeHex(PK),
|
|
1608
|
-
);
|
|
1609
1580
|
|
|
1610
1581
|
const AD = xConcat(
|
|
1611
1582
|
xEncode(xConstants.CURVE, IK_AP),
|
|
@@ -1615,8 +1586,6 @@ export class Client {
|
|
|
1615
1586
|
const nonce = xMakeNonce();
|
|
1616
1587
|
const cipher = xSecretbox(message, nonce, SK);
|
|
1617
1588
|
|
|
1618
|
-
this.log.info("Encrypted ciphertext.");
|
|
1619
|
-
|
|
1620
1589
|
/* 32 bytes for signkey, 32 bytes for ephemeral key,
|
|
1621
1590
|
68 bytes for AD, 6 bytes for otk index (empty for no otk) */
|
|
1622
1591
|
const extra = xConcat(
|
|
@@ -1642,8 +1611,6 @@ export class Client {
|
|
|
1642
1611
|
};
|
|
1643
1612
|
|
|
1644
1613
|
const hmac = xHMAC(mail, SK);
|
|
1645
|
-
this.log.info("Mail hash: " + JSON.stringify(mail));
|
|
1646
|
-
this.log.info("Generated hmac: " + XUtils.encodeHex(hmac));
|
|
1647
1614
|
|
|
1648
1615
|
const msg: ResourceMsg = {
|
|
1649
1616
|
action: "CREATE",
|
|
@@ -1656,8 +1623,6 @@ export class Client {
|
|
|
1656
1623
|
// discard the ephemeral keys
|
|
1657
1624
|
this.newEphemeralKeys();
|
|
1658
1625
|
|
|
1659
|
-
// save the encryption session
|
|
1660
|
-
this.log.info("Saving new session.");
|
|
1661
1626
|
const sessionEntry: SessionSQL = {
|
|
1662
1627
|
deviceID: device.deviceID,
|
|
1663
1628
|
fingerprint: XUtils.encodeHex(AD),
|
|
@@ -1717,7 +1682,6 @@ export class Client {
|
|
|
1717
1682
|
};
|
|
1718
1683
|
this.socket.on("message", callback);
|
|
1719
1684
|
void this.send(msg, hmac);
|
|
1720
|
-
this.log.info("Mail sent.");
|
|
1721
1685
|
});
|
|
1722
1686
|
this.sending.delete(device.deviceID);
|
|
1723
1687
|
}
|
|
@@ -1754,7 +1718,7 @@ export class Client {
|
|
|
1754
1718
|
/**
|
|
1755
1719
|
* Gets a list of permissions for a server.
|
|
1756
1720
|
*
|
|
1757
|
-
* @returns
|
|
1721
|
+
* @returns The list of Permission objects.
|
|
1758
1722
|
*/
|
|
1759
1723
|
private async fetchPermissionList(serverID: string): Promise<Permission[]> {
|
|
1760
1724
|
const res = await this.http.get(
|
|
@@ -1816,10 +1780,6 @@ export class Client {
|
|
|
1816
1780
|
const msgBytes = Uint8Array.from(msgpack.encode(copy));
|
|
1817
1781
|
|
|
1818
1782
|
const devices = await this.getUserDeviceList(this.getUser().userID);
|
|
1819
|
-
this.log.info(
|
|
1820
|
-
"Forwarding to my other devices, deviceList length is " +
|
|
1821
|
-
String(devices?.length ?? 0),
|
|
1822
|
-
);
|
|
1823
1783
|
|
|
1824
1784
|
if (!devices) {
|
|
1825
1785
|
throw new Error("Couldn't get own devices.");
|
|
@@ -1843,8 +1803,6 @@ export class Client {
|
|
|
1843
1803
|
for (const result of results) {
|
|
1844
1804
|
const { status } = result;
|
|
1845
1805
|
if (status === "rejected") {
|
|
1846
|
-
this.log.warn("Message failed.");
|
|
1847
|
-
this.log.warn(JSON.stringify(result));
|
|
1848
1806
|
}
|
|
1849
1807
|
}
|
|
1850
1808
|
});
|
|
@@ -1879,13 +1837,11 @@ export class Client {
|
|
|
1879
1837
|
|
|
1880
1838
|
private async getDeviceByID(deviceID: string): Promise<Device | null> {
|
|
1881
1839
|
if (deviceID in this.deviceRecords) {
|
|
1882
|
-
this.log.info("Found device in local cache.");
|
|
1883
1840
|
return this.deviceRecords[deviceID] ?? null;
|
|
1884
1841
|
}
|
|
1885
1842
|
|
|
1886
1843
|
const device = await this.database.getDevice(deviceID);
|
|
1887
1844
|
if (device) {
|
|
1888
|
-
this.log.info("Found device in local db.");
|
|
1889
1845
|
this.deviceRecords[deviceID] = device;
|
|
1890
1846
|
return device;
|
|
1891
1847
|
}
|
|
@@ -1893,7 +1849,6 @@ export class Client {
|
|
|
1893
1849
|
const res = await this.http.get(
|
|
1894
1850
|
this.getHost() + "/device/" + deviceID,
|
|
1895
1851
|
);
|
|
1896
|
-
this.log.info("Retrieved device from server.");
|
|
1897
1852
|
const fetchedDevice = decodeAxios(DeviceCodec, res.data);
|
|
1898
1853
|
this.deviceRecords[deviceID] = fetchedDevice;
|
|
1899
1854
|
await this.database.saveDevice(fetchedDevice);
|
|
@@ -1940,7 +1895,6 @@ export class Client {
|
|
|
1940
1895
|
this.emitter.emit("decryptingMail");
|
|
1941
1896
|
}
|
|
1942
1897
|
|
|
1943
|
-
this.log.info("fetching mail for device " + this.getDevice().deviceID);
|
|
1944
1898
|
try {
|
|
1945
1899
|
const res = await this.http.post<ArrayBuffer>(
|
|
1946
1900
|
this.getHost() +
|
|
@@ -1958,12 +1912,12 @@ export class Client {
|
|
|
1958
1912
|
const [mailHeader, mailBody, timestamp] = mailDetails;
|
|
1959
1913
|
try {
|
|
1960
1914
|
await this.readMail(mailHeader, mailBody, timestamp);
|
|
1961
|
-
} catch (
|
|
1962
|
-
|
|
1915
|
+
} catch (_readMailErr) {
|
|
1916
|
+
// non-fatal — inspect _readMailErr in a debugger
|
|
1963
1917
|
}
|
|
1964
1918
|
}
|
|
1965
|
-
} catch (
|
|
1966
|
-
|
|
1919
|
+
} catch (_fetchErr) {
|
|
1920
|
+
// non-fatal — inspect _fetchErr in a debugger
|
|
1967
1921
|
}
|
|
1968
1922
|
this.fetchingMail = false;
|
|
1969
1923
|
}
|
|
@@ -2006,7 +1960,7 @@ export class Client {
|
|
|
2006
1960
|
/**
|
|
2007
1961
|
* Gets all permissions for the logged in user.
|
|
2008
1962
|
*
|
|
2009
|
-
* @returns
|
|
1963
|
+
* @returns The list of Permission objects.
|
|
2010
1964
|
*/
|
|
2011
1965
|
private async getPermissions(): Promise<Permission[]> {
|
|
2012
1966
|
const res = await this.http.get(
|
|
@@ -2064,8 +2018,7 @@ export class Client {
|
|
|
2064
2018
|
responseType: "arraybuffer",
|
|
2065
2019
|
});
|
|
2066
2020
|
return decodeAxios(ActionTokenCodec, res.data);
|
|
2067
|
-
} catch
|
|
2068
|
-
this.log.warn(String(err));
|
|
2021
|
+
} catch {
|
|
2069
2022
|
return null;
|
|
2070
2023
|
}
|
|
2071
2024
|
}
|
|
@@ -2107,7 +2060,6 @@ export class Client {
|
|
|
2107
2060
|
private async handleNotify(msg: NotifyMsg) {
|
|
2108
2061
|
switch (msg.event) {
|
|
2109
2062
|
case "mail":
|
|
2110
|
-
this.log.info("Server has informed us of new mail.");
|
|
2111
2063
|
await this.getMail();
|
|
2112
2064
|
this.fetchingMail = false;
|
|
2113
2065
|
break;
|
|
@@ -2121,7 +2073,6 @@ export class Client {
|
|
|
2121
2073
|
// msg.data is the messageID for retry
|
|
2122
2074
|
break;
|
|
2123
2075
|
default:
|
|
2124
|
-
this.log.info("Unsupported notification event " + msg.event);
|
|
2125
2076
|
break;
|
|
2126
2077
|
}
|
|
2127
2078
|
}
|
|
@@ -2162,8 +2113,6 @@ export class Client {
|
|
|
2162
2113
|
// Auth sent as first message after open
|
|
2163
2114
|
this.socket = new WebSocketAdapter(wsUrl);
|
|
2164
2115
|
this.socket.on("open", () => {
|
|
2165
|
-
this.log.info("Connection opened.");
|
|
2166
|
-
// Send auth as first message (encoded to bytes — protocol is binary).
|
|
2167
2116
|
const authMsg = JSON.stringify({
|
|
2168
2117
|
token: this.token,
|
|
2169
2118
|
type: "auth",
|
|
@@ -2173,7 +2122,6 @@ export class Client {
|
|
|
2173
2122
|
});
|
|
2174
2123
|
|
|
2175
2124
|
this.socket.on("close", () => {
|
|
2176
|
-
this.log.info("Connection closed.");
|
|
2177
2125
|
if (this.pingInterval) {
|
|
2178
2126
|
clearInterval(this.pingInterval);
|
|
2179
2127
|
this.pingInterval = null;
|
|
@@ -2183,30 +2131,26 @@ export class Client {
|
|
|
2183
2131
|
}
|
|
2184
2132
|
});
|
|
2185
2133
|
|
|
2186
|
-
this.socket.on("error", (
|
|
2187
|
-
|
|
2134
|
+
this.socket.on("error", (_error: Error) => {
|
|
2135
|
+
if (!this.manuallyClosing) {
|
|
2136
|
+
this.emitter.emit("disconnect");
|
|
2137
|
+
}
|
|
2188
2138
|
});
|
|
2189
2139
|
|
|
2190
2140
|
this.socket.on("message", (message: Uint8Array) => {
|
|
2191
|
-
const [
|
|
2192
|
-
|
|
2193
|
-
this.log.debug("INH " + XUtils.encodeHex(header));
|
|
2194
|
-
this.log.debug("IN " + JSON.stringify(raw, null, 4));
|
|
2141
|
+
const [_header, raw] = XUtils.unpackMessage(message);
|
|
2195
2142
|
|
|
2196
2143
|
const parseResult = WSMessageSchema.safeParse(raw);
|
|
2197
2144
|
if (!parseResult.success) {
|
|
2198
|
-
this.log.warn("Unknown WS message: " + JSON.stringify(raw));
|
|
2199
2145
|
return;
|
|
2200
2146
|
}
|
|
2201
2147
|
const msg = parseResult.data;
|
|
2202
2148
|
|
|
2203
2149
|
switch (msg.type) {
|
|
2204
2150
|
case "challenge":
|
|
2205
|
-
this.log.info("Received challenge from server.");
|
|
2206
2151
|
this.respond(msg);
|
|
2207
2152
|
break;
|
|
2208
2153
|
case "error":
|
|
2209
|
-
this.log.warn(JSON.stringify(msg));
|
|
2210
2154
|
break;
|
|
2211
2155
|
case "notify":
|
|
2212
2156
|
void this.handleNotify(msg);
|
|
@@ -2224,15 +2168,10 @@ export class Client {
|
|
|
2224
2168
|
"Received unauthorized message from server.",
|
|
2225
2169
|
);
|
|
2226
2170
|
case "authorized":
|
|
2227
|
-
this.log.info(
|
|
2228
|
-
"Authenticated with userID " +
|
|
2229
|
-
(this.user?.userID ?? "unknown"),
|
|
2230
|
-
);
|
|
2231
2171
|
this.emitter.emit("connected");
|
|
2232
2172
|
void this.postAuth();
|
|
2233
2173
|
break;
|
|
2234
2174
|
default:
|
|
2235
|
-
this.log.info("Unsupported message " + msg.type);
|
|
2236
2175
|
break;
|
|
2237
2176
|
}
|
|
2238
2177
|
});
|
|
@@ -2269,10 +2208,8 @@ export class Client {
|
|
|
2269
2208
|
|
|
2270
2209
|
private async negotiateOTK() {
|
|
2271
2210
|
const otkCount = await this.getOTKCount();
|
|
2272
|
-
this.log.info("Server reported OTK: " + otkCount.toString());
|
|
2273
2211
|
const needs = xConstants.MIN_OTK_SUPPLY - otkCount;
|
|
2274
2212
|
if (needs === 0) {
|
|
2275
|
-
this.log.info("Server otk supply full.");
|
|
2276
2213
|
return;
|
|
2277
2214
|
}
|
|
2278
2215
|
|
|
@@ -2288,7 +2225,6 @@ export class Client {
|
|
|
2288
2225
|
|
|
2289
2226
|
private ping() {
|
|
2290
2227
|
if (!this.isAlive) {
|
|
2291
|
-
this.log.warn("Ping failed.");
|
|
2292
2228
|
}
|
|
2293
2229
|
this.setAlive(false);
|
|
2294
2230
|
void this.send({ transmissionID: uuid.v4(), type: "ping" });
|
|
@@ -2309,9 +2245,6 @@ export class Client {
|
|
|
2309
2245
|
const preKeys: PreKeysCrypto =
|
|
2310
2246
|
existingPreKeys ??
|
|
2311
2247
|
(await (async () => {
|
|
2312
|
-
this.log.warn(
|
|
2313
|
-
"No prekeys found in database, creating a new one.",
|
|
2314
|
-
);
|
|
2315
2248
|
const unsaved = this.createPreKey();
|
|
2316
2249
|
const [saved] = await this.database.savePreKeys(
|
|
2317
2250
|
[unsaved],
|
|
@@ -2337,19 +2270,6 @@ export class Client {
|
|
|
2337
2270
|
identityKeys,
|
|
2338
2271
|
preKeys,
|
|
2339
2272
|
};
|
|
2340
|
-
|
|
2341
|
-
this.log.info(
|
|
2342
|
-
"Keyring populated:\n" +
|
|
2343
|
-
JSON.stringify(
|
|
2344
|
-
{
|
|
2345
|
-
ephemeralKey: XUtils.encodeHex(ephemeralKeys.publicKey),
|
|
2346
|
-
preKey: XUtils.encodeHex(preKeys.keyPair.publicKey),
|
|
2347
|
-
signKey: XUtils.encodeHex(this.signKeys.publicKey),
|
|
2348
|
-
},
|
|
2349
|
-
null,
|
|
2350
|
-
4,
|
|
2351
|
-
),
|
|
2352
|
-
);
|
|
2353
2273
|
}
|
|
2354
2274
|
|
|
2355
2275
|
private async postAuth() {
|
|
@@ -2364,9 +2284,7 @@ export class Client {
|
|
|
2364
2284
|
void this.negotiateOTK();
|
|
2365
2285
|
count = 0;
|
|
2366
2286
|
}
|
|
2367
|
-
} catch
|
|
2368
|
-
this.log.warn("Problem fetching mail" + String(err));
|
|
2369
|
-
}
|
|
2287
|
+
} catch {}
|
|
2370
2288
|
await sleep(1000 * 60);
|
|
2371
2289
|
}
|
|
2372
2290
|
}
|
|
@@ -2380,6 +2298,11 @@ export class Client {
|
|
|
2380
2298
|
mail: MailWS,
|
|
2381
2299
|
timestamp: string,
|
|
2382
2300
|
) {
|
|
2301
|
+
if (this.seenMailIDs.has(mail.mailID)) {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
this.seenMailIDs.add(mail.mailID);
|
|
2305
|
+
|
|
2383
2306
|
this.sendReceipt(new Uint8Array(mail.nonce));
|
|
2384
2307
|
let timeout = 1;
|
|
2385
2308
|
while (this.reading) {
|
|
@@ -2390,7 +2313,6 @@ export class Client {
|
|
|
2390
2313
|
|
|
2391
2314
|
try {
|
|
2392
2315
|
const healSession = async () => {
|
|
2393
|
-
this.log.info("Requesting retry of " + mail.mailID);
|
|
2394
2316
|
const deviceEntry = await this.getDeviceByID(mail.sender);
|
|
2395
2317
|
const [user, _err] = await this.fetchUser(mail.authorID);
|
|
2396
2318
|
if (deviceEntry && user) {
|
|
@@ -2405,10 +2327,8 @@ export class Client {
|
|
|
2405
2327
|
}
|
|
2406
2328
|
};
|
|
2407
2329
|
|
|
2408
|
-
this.log.info("Received mail from " + mail.sender);
|
|
2409
2330
|
switch (mail.mailType) {
|
|
2410
2331
|
case MailType.initial:
|
|
2411
|
-
this.log.info("Initiating new session.");
|
|
2412
2332
|
const extraParts = Client.deserializeExtra(
|
|
2413
2333
|
MailType.initial,
|
|
2414
2334
|
new Uint8Array(mail.extra),
|
|
@@ -2424,48 +2344,18 @@ export class Client {
|
|
|
2424
2344
|
|
|
2425
2345
|
const preKeyIndex = XUtils.uint8ArrToNumber(indexBytes);
|
|
2426
2346
|
|
|
2427
|
-
this.log.info(
|
|
2428
|
-
this.toString() +
|
|
2429
|
-
" otk #" +
|
|
2430
|
-
String(preKeyIndex) +
|
|
2431
|
-
" indicated",
|
|
2432
|
-
);
|
|
2433
|
-
|
|
2434
2347
|
const otk =
|
|
2435
2348
|
preKeyIndex === 0
|
|
2436
2349
|
? null
|
|
2437
2350
|
: await this.database.getOneTimeKey(preKeyIndex);
|
|
2438
2351
|
|
|
2439
|
-
if (otk) {
|
|
2440
|
-
this.log.info(
|
|
2441
|
-
"otk #" +
|
|
2442
|
-
JSON.stringify(otk.index) +
|
|
2443
|
-
" retrieved from database.",
|
|
2444
|
-
);
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
this.log.info("signKey: " + XUtils.encodeHex(signKey));
|
|
2448
|
-
this.log.info("preKey: " + XUtils.encodeHex(ephKey));
|
|
2449
|
-
if (otk) {
|
|
2450
|
-
this.log.info(
|
|
2451
|
-
"OTK: " + XUtils.encodeHex(otk.keyPair.publicKey),
|
|
2452
|
-
);
|
|
2453
|
-
}
|
|
2454
|
-
|
|
2455
2352
|
if (otk?.index !== preKeyIndex && preKeyIndex !== 0) {
|
|
2456
|
-
this.log.warn(
|
|
2457
|
-
"OTK index mismatch, received " +
|
|
2458
|
-
JSON.stringify(otk?.index) +
|
|
2459
|
-
", expected " +
|
|
2460
|
-
preKeyIndex.toString(),
|
|
2461
|
-
);
|
|
2462
2353
|
return;
|
|
2463
2354
|
}
|
|
2464
2355
|
|
|
2465
2356
|
// their public keys
|
|
2466
2357
|
const IK_A_raw = XKeyConvert.convertPublicKey(signKey);
|
|
2467
2358
|
if (!IK_A_raw) {
|
|
2468
|
-
this.log.warn("Could not convert sign key to X25519.");
|
|
2469
2359
|
return;
|
|
2470
2360
|
}
|
|
2471
2361
|
const IK_A = IK_A_raw;
|
|
@@ -2493,26 +2383,9 @@ export class Client {
|
|
|
2493
2383
|
|
|
2494
2384
|
// shared secret key
|
|
2495
2385
|
const SK = xKDF(IKM);
|
|
2496
|
-
this.log.info(
|
|
2497
|
-
"Obtained SK for " +
|
|
2498
|
-
mail.sender +
|
|
2499
|
-
", " +
|
|
2500
|
-
XUtils.encodeHex(SK),
|
|
2501
|
-
);
|
|
2502
|
-
|
|
2503
|
-
// shared public key
|
|
2504
2386
|
const PK = xBoxKeyPairFromSecret(SK).publicKey;
|
|
2505
|
-
this.log.info(
|
|
2506
|
-
this.toString() +
|
|
2507
|
-
"Obtained PK for " +
|
|
2508
|
-
mail.sender +
|
|
2509
|
-
" " +
|
|
2510
|
-
XUtils.encodeHex(PK),
|
|
2511
|
-
);
|
|
2512
2387
|
|
|
2513
2388
|
const hmac = xHMAC(mail, SK);
|
|
2514
|
-
this.log.info("Mail hash: " + JSON.stringify(mail));
|
|
2515
|
-
this.log.info("Calculated hmac: " + XUtils.encodeHex(hmac));
|
|
2516
2389
|
|
|
2517
2390
|
// associated data
|
|
2518
2391
|
const AD = xConcat(
|
|
@@ -2521,22 +2394,14 @@ export class Client {
|
|
|
2521
2394
|
);
|
|
2522
2395
|
|
|
2523
2396
|
if (!XUtils.bytesEqual(hmac, header)) {
|
|
2524
|
-
console.warn(
|
|
2525
|
-
"Mail authentication failed (HMAC did not match).",
|
|
2526
|
-
);
|
|
2527
|
-
console.warn(mail);
|
|
2528
2397
|
return;
|
|
2529
2398
|
}
|
|
2530
|
-
this.log.info("Mail authenticated successfully.");
|
|
2531
|
-
|
|
2532
2399
|
const unsealed = xSecretboxOpen(
|
|
2533
2400
|
new Uint8Array(mail.cipher),
|
|
2534
2401
|
new Uint8Array(mail.nonce),
|
|
2535
2402
|
SK,
|
|
2536
2403
|
);
|
|
2537
2404
|
if (unsealed) {
|
|
2538
|
-
this.log.info("Decryption successful.");
|
|
2539
|
-
|
|
2540
2405
|
let plaintext = "";
|
|
2541
2406
|
if (!mail.forward) {
|
|
2542
2407
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
@@ -2607,12 +2472,8 @@ export class Client {
|
|
|
2607
2472
|
if (user) {
|
|
2608
2473
|
this.emitter.emit("session", newSession, user);
|
|
2609
2474
|
} else {
|
|
2610
|
-
this.log.warn(
|
|
2611
|
-
"Couldn't retrieve user " + newSession.userID,
|
|
2612
|
-
);
|
|
2613
2475
|
}
|
|
2614
2476
|
} else {
|
|
2615
|
-
this.log.warn("Mail decryption failed.");
|
|
2616
2477
|
}
|
|
2617
2478
|
break;
|
|
2618
2479
|
case MailType.subsequent:
|
|
@@ -2628,37 +2489,21 @@ export class Client {
|
|
|
2628
2489
|
let session = await this.getSessionByPubkey(publicKey);
|
|
2629
2490
|
let retries = 0;
|
|
2630
2491
|
while (!session) {
|
|
2631
|
-
if (retries
|
|
2492
|
+
if (retries >= 3) {
|
|
2632
2493
|
break;
|
|
2633
2494
|
}
|
|
2634
|
-
|
|
2635
|
-
session = await this.getSessionByPubkey(publicKey);
|
|
2495
|
+
await sleep(100 * 2 ** retries);
|
|
2636
2496
|
retries++;
|
|
2637
|
-
|
|
2497
|
+
session = await this.getSessionByPubkey(publicKey);
|
|
2638
2498
|
}
|
|
2639
2499
|
|
|
2640
2500
|
if (!session) {
|
|
2641
|
-
this.log.warn(
|
|
2642
|
-
"Couldn't find session public key " +
|
|
2643
|
-
XUtils.encodeHex(publicKey),
|
|
2644
|
-
);
|
|
2645
2501
|
void healSession();
|
|
2646
2502
|
return;
|
|
2647
2503
|
}
|
|
2648
|
-
this.log.info("Session found for " + mail.sender);
|
|
2649
|
-
this.log.info(
|
|
2650
|
-
"Mail nonce " +
|
|
2651
|
-
XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
2652
|
-
);
|
|
2653
|
-
|
|
2654
2504
|
const HMAC = xHMAC(mail, session.SK);
|
|
2655
|
-
this.log.info("Mail hash: " + JSON.stringify(mail));
|
|
2656
|
-
this.log.info("Calculated hmac: " + XUtils.encodeHex(HMAC));
|
|
2657
2505
|
|
|
2658
2506
|
if (!XUtils.bytesEqual(HMAC, header)) {
|
|
2659
|
-
this.log.warn(
|
|
2660
|
-
"Message authentication failed (HMAC does not match).",
|
|
2661
|
-
);
|
|
2662
2507
|
void healSession();
|
|
2663
2508
|
return;
|
|
2664
2509
|
}
|
|
@@ -2670,8 +2515,6 @@ export class Client {
|
|
|
2670
2515
|
);
|
|
2671
2516
|
|
|
2672
2517
|
if (decrypted) {
|
|
2673
|
-
this.log.info("Decryption successful.");
|
|
2674
|
-
// emit the message
|
|
2675
2518
|
const fwdMsg2 = mail.forward
|
|
2676
2519
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
2677
2520
|
: null;
|
|
@@ -2702,7 +2545,6 @@ export class Client {
|
|
|
2702
2545
|
|
|
2703
2546
|
void this.database.markSessionUsed(session.sessionID);
|
|
2704
2547
|
} else {
|
|
2705
|
-
this.log.info("Decryption failed.");
|
|
2706
2548
|
void healSession();
|
|
2707
2549
|
|
|
2708
2550
|
// emit the message
|
|
@@ -2726,7 +2568,6 @@ export class Client {
|
|
|
2726
2568
|
}
|
|
2727
2569
|
break;
|
|
2728
2570
|
default:
|
|
2729
|
-
this.log.warn("Unsupported MailType:", mail.mailType);
|
|
2730
2571
|
break;
|
|
2731
2572
|
}
|
|
2732
2573
|
} finally {
|
|
@@ -2894,14 +2735,10 @@ export class Client {
|
|
|
2894
2735
|
);
|
|
2895
2736
|
device = decodeAxios(DeviceCodec, res.data);
|
|
2896
2737
|
} catch (err: unknown) {
|
|
2897
|
-
this.log.error(err instanceof Error ? err.message : String(err));
|
|
2898
2738
|
if (isAxiosError(err) && err.response?.status === 404) {
|
|
2899
|
-
// just in case
|
|
2900
2739
|
await this.database.purgeKeyData();
|
|
2901
2740
|
await this.populateKeyRing();
|
|
2902
2741
|
|
|
2903
|
-
this.log.info("Attempting to register device.");
|
|
2904
|
-
|
|
2905
2742
|
const newDevice = await this.registerDevice();
|
|
2906
2743
|
if (newDevice) {
|
|
2907
2744
|
device = newDevice;
|
|
@@ -2912,7 +2749,6 @@ export class Client {
|
|
|
2912
2749
|
throw err;
|
|
2913
2750
|
}
|
|
2914
2751
|
}
|
|
2915
|
-
this.log.info("Got device " + JSON.stringify(device, null, 4));
|
|
2916
2752
|
return device;
|
|
2917
2753
|
}
|
|
2918
2754
|
|
|
@@ -2920,17 +2756,20 @@ export class Client {
|
|
|
2920
2756
|
or contains an HMAC of the message with
|
|
2921
2757
|
a derived SK */
|
|
2922
2758
|
private async send(msg: ClientMessage, header?: Uint8Array) {
|
|
2923
|
-
|
|
2759
|
+
const maxWaitMs = 30_000;
|
|
2760
|
+
let elapsed = 0;
|
|
2761
|
+
let backoff = 50;
|
|
2924
2762
|
while (this.socket.readyState !== 1) {
|
|
2925
|
-
|
|
2926
|
-
|
|
2763
|
+
if (elapsed >= maxWaitMs) {
|
|
2764
|
+
throw new Error(
|
|
2765
|
+
"WebSocket did not reach OPEN state within 30 seconds.",
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
await sleep(backoff);
|
|
2769
|
+
elapsed += backoff;
|
|
2770
|
+
backoff = Math.min(backoff * 2, 4_000);
|
|
2927
2771
|
}
|
|
2928
2772
|
|
|
2929
|
-
this.log.debug(
|
|
2930
|
-
"OUTH " + XUtils.encodeHex(header || XUtils.emptyHeader()),
|
|
2931
|
-
);
|
|
2932
|
-
this.log.debug("OUT " + JSON.stringify(msg, null, 4));
|
|
2933
|
-
|
|
2934
2773
|
this.socket.send(XUtils.packMessage(msg, header));
|
|
2935
2774
|
}
|
|
2936
2775
|
|
|
@@ -2943,29 +2782,15 @@ export class Client {
|
|
|
2943
2782
|
this.userRecords[user.userID] = user;
|
|
2944
2783
|
}
|
|
2945
2784
|
|
|
2946
|
-
this.log.info(
|
|
2947
|
-
"Sending to userlist:\n" + JSON.stringify(userList, null, 4),
|
|
2948
|
-
);
|
|
2949
|
-
|
|
2950
2785
|
const mailID = uuid.v4();
|
|
2951
2786
|
const promises: Array<Promise<void>> = [];
|
|
2952
2787
|
|
|
2953
2788
|
const userIDs = [...new Set(userList.map((user) => user.userID))];
|
|
2954
2789
|
const devices = await this.getMultiUserDeviceList(userIDs);
|
|
2955
2790
|
|
|
2956
|
-
this.log.info(
|
|
2957
|
-
"Retrieved devicelist:\n" + JSON.stringify(devices, null, 4),
|
|
2958
|
-
);
|
|
2959
|
-
|
|
2960
2791
|
for (const device of devices) {
|
|
2961
2792
|
const ownerRecord = this.userRecords[device.owner];
|
|
2962
2793
|
if (!ownerRecord) {
|
|
2963
|
-
this.log.warn(
|
|
2964
|
-
"Skipping device " +
|
|
2965
|
-
device.deviceID +
|
|
2966
|
-
": no user record for owner " +
|
|
2967
|
-
device.owner,
|
|
2968
|
-
);
|
|
2969
2794
|
continue;
|
|
2970
2795
|
}
|
|
2971
2796
|
promises.push(
|
|
@@ -2983,8 +2808,6 @@ export class Client {
|
|
|
2983
2808
|
for (const result of results) {
|
|
2984
2809
|
const { status } = result;
|
|
2985
2810
|
if (status === "rejected") {
|
|
2986
|
-
this.log.warn("Message failed.");
|
|
2987
|
-
this.log.warn(JSON.stringify(result));
|
|
2988
2811
|
}
|
|
2989
2812
|
}
|
|
2990
2813
|
});
|
|
@@ -3001,20 +2824,8 @@ export class Client {
|
|
|
3001
2824
|
retry = false,
|
|
3002
2825
|
): Promise<void> {
|
|
3003
2826
|
while (this.sending.has(device.deviceID)) {
|
|
3004
|
-
this.log.warn(
|
|
3005
|
-
"Sending in progress to device ID " +
|
|
3006
|
-
device.deviceID +
|
|
3007
|
-
", waiting.",
|
|
3008
|
-
);
|
|
3009
2827
|
await sleep(100);
|
|
3010
2828
|
}
|
|
3011
|
-
this.log.info(
|
|
3012
|
-
"Sending mail to user: \n" + JSON.stringify(user, null, 4),
|
|
3013
|
-
);
|
|
3014
|
-
this.log.info(
|
|
3015
|
-
"Sending mail to device:\n " +
|
|
3016
|
-
JSON.stringify(device.deviceID, null, 4),
|
|
3017
|
-
);
|
|
3018
2829
|
this.sending.set(device.deviceID, device);
|
|
3019
2830
|
|
|
3020
2831
|
const session = await this.database.getSessionByDeviceID(
|
|
@@ -3022,11 +2833,8 @@ export class Client {
|
|
|
3022
2833
|
);
|
|
3023
2834
|
|
|
3024
2835
|
if (!session || retry) {
|
|
3025
|
-
this.log.info("Creating new session for " + device.deviceID);
|
|
3026
2836
|
await this.createSession(device, user, msg, group, mailID, forward);
|
|
3027
2837
|
return;
|
|
3028
|
-
} else {
|
|
3029
|
-
this.log.info("Found existing session for " + device.deviceID);
|
|
3030
2838
|
}
|
|
3031
2839
|
|
|
3032
2840
|
const nonce = xMakeNonce();
|
|
@@ -3056,8 +2864,6 @@ export class Client {
|
|
|
3056
2864
|
};
|
|
3057
2865
|
|
|
3058
2866
|
const hmac = xHMAC(mail, session.SK);
|
|
3059
|
-
this.log.info("Mail hash: " + JSON.stringify(mail));
|
|
3060
|
-
this.log.info("Calculated hmac: " + XUtils.encodeHex(hmac));
|
|
3061
2867
|
|
|
3062
2868
|
const fwdOut = forward
|
|
3063
2869
|
? messageSchema.parse(msgpack.decode(msg))
|
|
@@ -3143,14 +2949,10 @@ export class Client {
|
|
|
3143
2949
|
for (const result of results) {
|
|
3144
2950
|
const { status } = result;
|
|
3145
2951
|
if (status === "rejected") {
|
|
3146
|
-
this.log.warn("Message failed.");
|
|
3147
|
-
this.log.warn(JSON.stringify(result));
|
|
3148
2952
|
}
|
|
3149
2953
|
}
|
|
3150
2954
|
});
|
|
3151
2955
|
} catch (err: unknown) {
|
|
3152
|
-
this.log.error("Message threw exception.");
|
|
3153
|
-
this.log.error(err instanceof Error ? err.message : String(err));
|
|
3154
2956
|
throw err;
|
|
3155
2957
|
}
|
|
3156
2958
|
}
|
|
@@ -3175,19 +2977,9 @@ export class Client {
|
|
|
3175
2977
|
private async submitOTK(amount: number) {
|
|
3176
2978
|
const otks: UnsavedPreKey[] = [];
|
|
3177
2979
|
|
|
3178
|
-
const t0 = performance.now();
|
|
3179
2980
|
for (let i = 0; i < amount; i++) {
|
|
3180
2981
|
otks[i] = this.createPreKey();
|
|
3181
2982
|
}
|
|
3182
|
-
const t1 = performance.now();
|
|
3183
|
-
|
|
3184
|
-
this.log.info(
|
|
3185
|
-
"Generated " +
|
|
3186
|
-
String(amount) +
|
|
3187
|
-
" one time keys in " +
|
|
3188
|
-
String(t1 - t0) +
|
|
3189
|
-
" ms.",
|
|
3190
|
-
);
|
|
3191
2983
|
|
|
3192
2984
|
const savedKeys = await this.database.savePreKeys(otks, true);
|
|
3193
2985
|
|