@vex-chat/libvex 2.0.0 → 4.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 -59
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +143 -272
- 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/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 +206 -424
- 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 +0 -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/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,45 @@ 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 || typeof g.value !== "object" || g.value === null) {
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
1040
|
+
const env: unknown = Object.getOwnPropertyDescriptor(
|
|
1041
|
+
g.value,
|
|
1042
|
+
"env",
|
|
1043
|
+
)?.value;
|
|
1044
|
+
if (typeof env !== "object" || env === null) {
|
|
1045
|
+
return undefined;
|
|
1046
|
+
}
|
|
1047
|
+
const val: unknown = Object.getOwnPropertyDescriptor(
|
|
1048
|
+
env,
|
|
1049
|
+
"NODE_ENV",
|
|
1050
|
+
)?.value;
|
|
1051
|
+
return typeof val === "string" ? val : undefined;
|
|
1052
|
+
} catch {
|
|
1053
|
+
return undefined;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Closes the client — disconnects the WebSocket, shuts down storage,
|
|
1059
|
+
* and emits `closed` unless `muteEvent` is `true`.
|
|
1060
|
+
*
|
|
1061
|
+
* @param muteEvent - When `true`, suppresses the `closed` event.
|
|
1062
|
+
*/
|
|
1078
1063
|
public async close(muteEvent = false): Promise<void> {
|
|
1079
1064
|
this.manuallyClosing = true;
|
|
1080
|
-
this.log.info("Manually closing client.");
|
|
1081
|
-
|
|
1082
1065
|
this.socket.close();
|
|
1083
1066
|
await this.database.close();
|
|
1084
1067
|
|
|
@@ -1126,7 +1109,6 @@ export class Client {
|
|
|
1126
1109
|
const { deviceToken } = decodeAxios(ConnectResponseCodec, res.data);
|
|
1127
1110
|
this.http.defaults.headers.common["X-Device-Token"] = deviceToken;
|
|
1128
1111
|
|
|
1129
|
-
this.log.info("Starting websocket.");
|
|
1130
1112
|
this.initSocket();
|
|
1131
1113
|
// Yield the event loop so the WS open callback fires and sends the
|
|
1132
1114
|
// auth message before OTK generation blocks for ~5s on mobile.
|
|
@@ -1134,9 +1116,6 @@ export class Client {
|
|
|
1134
1116
|
await this.negotiateOTK();
|
|
1135
1117
|
}
|
|
1136
1118
|
|
|
1137
|
-
/**
|
|
1138
|
-
* Manually closes the client. Emits the closed event on successful shutdown.
|
|
1139
|
-
*/
|
|
1140
1119
|
/**
|
|
1141
1120
|
* Delete all local data — message history, encryption sessions, and prekeys.
|
|
1142
1121
|
* Closes the client afterward. Credentials (keychain) must be cleared by the consumer.
|
|
@@ -1172,8 +1151,8 @@ export class Client {
|
|
|
1172
1151
|
/**
|
|
1173
1152
|
* Authenticates with username/password and stores the Bearer auth token.
|
|
1174
1153
|
*
|
|
1175
|
-
* @param username Account username.
|
|
1176
|
-
* @param password Account password.
|
|
1154
|
+
* @param username - Account username.
|
|
1155
|
+
* @param password - Account password.
|
|
1177
1156
|
* @returns `{ ok: true }` on success, `{ ok: false, error }` on failure.
|
|
1178
1157
|
*
|
|
1179
1158
|
* @example
|
|
@@ -1205,7 +1184,6 @@ export class Client {
|
|
|
1205
1184
|
return { ok: true };
|
|
1206
1185
|
} catch (err: unknown) {
|
|
1207
1186
|
const error = err instanceof Error ? err.message : String(err);
|
|
1208
|
-
this.log.error("Login failed: " + error);
|
|
1209
1187
|
return { error, ok: false };
|
|
1210
1188
|
}
|
|
1211
1189
|
}
|
|
@@ -1258,7 +1236,6 @@ export class Client {
|
|
|
1258
1236
|
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
1259
1237
|
} catch (err: unknown) {
|
|
1260
1238
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1261
|
-
this.log.error("Device-key auth failed: " + error.message);
|
|
1262
1239
|
return error;
|
|
1263
1240
|
}
|
|
1264
1241
|
return null;
|
|
@@ -1271,6 +1248,7 @@ export class Client {
|
|
|
1271
1248
|
await this.http.post(this.getHost() + "/goodbye");
|
|
1272
1249
|
}
|
|
1273
1250
|
|
|
1251
|
+
/** Removes an event listener. See {@link ClientEvents} for available events. */
|
|
1274
1252
|
off<E extends keyof ClientEvents>(
|
|
1275
1253
|
event: E,
|
|
1276
1254
|
fn?: ClientEvents[E],
|
|
@@ -1285,6 +1263,7 @@ export class Client {
|
|
|
1285
1263
|
return this;
|
|
1286
1264
|
}
|
|
1287
1265
|
|
|
1266
|
+
/** Subscribes to an event. See {@link ClientEvents} for available events. */
|
|
1288
1267
|
on<E extends keyof ClientEvents>(
|
|
1289
1268
|
event: E,
|
|
1290
1269
|
fn: ClientEvents[E],
|
|
@@ -1295,6 +1274,7 @@ export class Client {
|
|
|
1295
1274
|
return this;
|
|
1296
1275
|
}
|
|
1297
1276
|
|
|
1277
|
+
/** Subscribes to an event for a single firing, then auto-removes. */
|
|
1298
1278
|
once<E extends keyof ClientEvents>(
|
|
1299
1279
|
event: E,
|
|
1300
1280
|
fn: ClientEvents[E],
|
|
@@ -1307,11 +1287,15 @@ export class Client {
|
|
|
1307
1287
|
|
|
1308
1288
|
/**
|
|
1309
1289
|
* Registers a new account on the server.
|
|
1310
|
-
* @param username The username to register. Must be unique.
|
|
1311
1290
|
*
|
|
1312
|
-
* @
|
|
1291
|
+
* @param username - The username to register. Must be unique.
|
|
1292
|
+
* @param password - Account password.
|
|
1293
|
+
* @returns `[user, null]` on success, `[null, error]` on failure.
|
|
1313
1294
|
*
|
|
1314
|
-
* @example
|
|
1295
|
+
* @example
|
|
1296
|
+
* ```ts
|
|
1297
|
+
* const [user, err] = await client.register("MyUsername", "hunter2");
|
|
1298
|
+
* ```
|
|
1315
1299
|
*/
|
|
1316
1300
|
public async register(
|
|
1317
1301
|
username: string,
|
|
@@ -1354,7 +1338,12 @@ export class Client {
|
|
|
1354
1338
|
return [this.getUser(), null];
|
|
1355
1339
|
} catch (err: unknown) {
|
|
1356
1340
|
if (isAxiosError(err) && err.response) {
|
|
1357
|
-
|
|
1341
|
+
const raw: unknown = err.response.data;
|
|
1342
|
+
const msg =
|
|
1343
|
+
raw instanceof ArrayBuffer || raw instanceof Uint8Array
|
|
1344
|
+
? new TextDecoder().decode(raw)
|
|
1345
|
+
: String(raw);
|
|
1346
|
+
return [null, new Error(msg)];
|
|
1358
1347
|
}
|
|
1359
1348
|
return [
|
|
1360
1349
|
null,
|
|
@@ -1432,14 +1421,10 @@ export class Client {
|
|
|
1432
1421
|
|
|
1433
1422
|
// returns the file details and the encryption key
|
|
1434
1423
|
private async createFile(file: Uint8Array): Promise<[FileSQL, string]> {
|
|
1435
|
-
this.log.info("Creating file, size: " + formatBytes(file.byteLength));
|
|
1436
|
-
|
|
1437
1424
|
const nonce = xMakeNonce();
|
|
1438
1425
|
const key = xBoxKeyPair();
|
|
1439
1426
|
const box = xSecretbox(Uint8Array.from(file), nonce, key.secretKey);
|
|
1440
1427
|
|
|
1441
|
-
this.log.info("Encrypted size: " + formatBytes(box.byteLength));
|
|
1442
|
-
|
|
1443
1428
|
if (typeof FormData !== "undefined") {
|
|
1444
1429
|
const fpayload = new FormData();
|
|
1445
1430
|
fpayload.set("owner", this.getDevice().deviceID);
|
|
@@ -1500,7 +1485,8 @@ export class Client {
|
|
|
1500
1485
|
|
|
1501
1486
|
const res = await this.http.post(
|
|
1502
1487
|
this.getHost() + "/server/" + serverID + "/invites",
|
|
1503
|
-
payload,
|
|
1488
|
+
msgpack.encode(payload),
|
|
1489
|
+
{ headers: { "Content-Type": "application/msgpack" } },
|
|
1504
1490
|
);
|
|
1505
1491
|
|
|
1506
1492
|
return decodeAxios(InviteCodec, res.data);
|
|
@@ -1536,28 +1522,12 @@ export class Client {
|
|
|
1536
1522
|
): Promise<void> {
|
|
1537
1523
|
let keyBundle: KeyBundle;
|
|
1538
1524
|
|
|
1539
|
-
this.log.info(
|
|
1540
|
-
"Requesting key bundle for device: " +
|
|
1541
|
-
JSON.stringify(device, null, 4),
|
|
1542
|
-
);
|
|
1543
1525
|
try {
|
|
1544
1526
|
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
|
-
);
|
|
1527
|
+
} catch {
|
|
1550
1528
|
return;
|
|
1551
1529
|
}
|
|
1552
1530
|
|
|
1553
|
-
this.log.warn(
|
|
1554
|
-
this.toString() +
|
|
1555
|
-
" retrieved keybundle #" +
|
|
1556
|
-
String(keyBundle.otk?.index ?? "none") +
|
|
1557
|
-
" for " +
|
|
1558
|
-
device.deviceID,
|
|
1559
|
-
);
|
|
1560
|
-
|
|
1561
1531
|
if (!this.xKeyRing) {
|
|
1562
1532
|
throw new Error("Key ring not initialized.");
|
|
1563
1533
|
}
|
|
@@ -1596,16 +1566,7 @@ export class Client {
|
|
|
1596
1566
|
|
|
1597
1567
|
// shared secret key
|
|
1598
1568
|
const SK = xKDF(IKM);
|
|
1599
|
-
this.log.info("Obtained SK, " + XUtils.encodeHex(SK));
|
|
1600
|
-
|
|
1601
1569
|
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
1570
|
|
|
1610
1571
|
const AD = xConcat(
|
|
1611
1572
|
xEncode(xConstants.CURVE, IK_AP),
|
|
@@ -1615,8 +1576,6 @@ export class Client {
|
|
|
1615
1576
|
const nonce = xMakeNonce();
|
|
1616
1577
|
const cipher = xSecretbox(message, nonce, SK);
|
|
1617
1578
|
|
|
1618
|
-
this.log.info("Encrypted ciphertext.");
|
|
1619
|
-
|
|
1620
1579
|
/* 32 bytes for signkey, 32 bytes for ephemeral key,
|
|
1621
1580
|
68 bytes for AD, 6 bytes for otk index (empty for no otk) */
|
|
1622
1581
|
const extra = xConcat(
|
|
@@ -1642,8 +1601,6 @@ export class Client {
|
|
|
1642
1601
|
};
|
|
1643
1602
|
|
|
1644
1603
|
const hmac = xHMAC(mail, SK);
|
|
1645
|
-
this.log.info("Mail hash: " + JSON.stringify(mail));
|
|
1646
|
-
this.log.info("Generated hmac: " + XUtils.encodeHex(hmac));
|
|
1647
1604
|
|
|
1648
1605
|
const msg: ResourceMsg = {
|
|
1649
1606
|
action: "CREATE",
|
|
@@ -1656,8 +1613,6 @@ export class Client {
|
|
|
1656
1613
|
// discard the ephemeral keys
|
|
1657
1614
|
this.newEphemeralKeys();
|
|
1658
1615
|
|
|
1659
|
-
// save the encryption session
|
|
1660
|
-
this.log.info("Saving new session.");
|
|
1661
1616
|
const sessionEntry: SessionSQL = {
|
|
1662
1617
|
deviceID: device.deviceID,
|
|
1663
1618
|
fingerprint: XUtils.encodeHex(AD),
|
|
@@ -1717,7 +1672,6 @@ export class Client {
|
|
|
1717
1672
|
};
|
|
1718
1673
|
this.socket.on("message", callback);
|
|
1719
1674
|
void this.send(msg, hmac);
|
|
1720
|
-
this.log.info("Mail sent.");
|
|
1721
1675
|
});
|
|
1722
1676
|
this.sending.delete(device.deviceID);
|
|
1723
1677
|
}
|
|
@@ -1754,7 +1708,7 @@ export class Client {
|
|
|
1754
1708
|
/**
|
|
1755
1709
|
* Gets a list of permissions for a server.
|
|
1756
1710
|
*
|
|
1757
|
-
* @returns
|
|
1711
|
+
* @returns The list of Permission objects.
|
|
1758
1712
|
*/
|
|
1759
1713
|
private async fetchPermissionList(serverID: string): Promise<Permission[]> {
|
|
1760
1714
|
const res = await this.http.get(
|
|
@@ -1816,10 +1770,6 @@ export class Client {
|
|
|
1816
1770
|
const msgBytes = Uint8Array.from(msgpack.encode(copy));
|
|
1817
1771
|
|
|
1818
1772
|
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
1773
|
|
|
1824
1774
|
if (!devices) {
|
|
1825
1775
|
throw new Error("Couldn't get own devices.");
|
|
@@ -1843,8 +1793,6 @@ export class Client {
|
|
|
1843
1793
|
for (const result of results) {
|
|
1844
1794
|
const { status } = result;
|
|
1845
1795
|
if (status === "rejected") {
|
|
1846
|
-
this.log.warn("Message failed.");
|
|
1847
|
-
this.log.warn(JSON.stringify(result));
|
|
1848
1796
|
}
|
|
1849
1797
|
}
|
|
1850
1798
|
});
|
|
@@ -1879,13 +1827,11 @@ export class Client {
|
|
|
1879
1827
|
|
|
1880
1828
|
private async getDeviceByID(deviceID: string): Promise<Device | null> {
|
|
1881
1829
|
if (deviceID in this.deviceRecords) {
|
|
1882
|
-
this.log.info("Found device in local cache.");
|
|
1883
1830
|
return this.deviceRecords[deviceID] ?? null;
|
|
1884
1831
|
}
|
|
1885
1832
|
|
|
1886
1833
|
const device = await this.database.getDevice(deviceID);
|
|
1887
1834
|
if (device) {
|
|
1888
|
-
this.log.info("Found device in local db.");
|
|
1889
1835
|
this.deviceRecords[deviceID] = device;
|
|
1890
1836
|
return device;
|
|
1891
1837
|
}
|
|
@@ -1893,7 +1839,6 @@ export class Client {
|
|
|
1893
1839
|
const res = await this.http.get(
|
|
1894
1840
|
this.getHost() + "/device/" + deviceID,
|
|
1895
1841
|
);
|
|
1896
|
-
this.log.info("Retrieved device from server.");
|
|
1897
1842
|
const fetchedDevice = decodeAxios(DeviceCodec, res.data);
|
|
1898
1843
|
this.deviceRecords[deviceID] = fetchedDevice;
|
|
1899
1844
|
await this.database.saveDevice(fetchedDevice);
|
|
@@ -1940,7 +1885,6 @@ export class Client {
|
|
|
1940
1885
|
this.emitter.emit("decryptingMail");
|
|
1941
1886
|
}
|
|
1942
1887
|
|
|
1943
|
-
this.log.info("fetching mail for device " + this.getDevice().deviceID);
|
|
1944
1888
|
try {
|
|
1945
1889
|
const res = await this.http.post<ArrayBuffer>(
|
|
1946
1890
|
this.getHost() +
|
|
@@ -1958,12 +1902,12 @@ export class Client {
|
|
|
1958
1902
|
const [mailHeader, mailBody, timestamp] = mailDetails;
|
|
1959
1903
|
try {
|
|
1960
1904
|
await this.readMail(mailHeader, mailBody, timestamp);
|
|
1961
|
-
} catch (
|
|
1962
|
-
|
|
1905
|
+
} catch (_readMailErr) {
|
|
1906
|
+
// non-fatal — inspect _readMailErr in a debugger
|
|
1963
1907
|
}
|
|
1964
1908
|
}
|
|
1965
|
-
} catch (
|
|
1966
|
-
|
|
1909
|
+
} catch (_fetchErr) {
|
|
1910
|
+
// non-fatal — inspect _fetchErr in a debugger
|
|
1967
1911
|
}
|
|
1968
1912
|
this.fetchingMail = false;
|
|
1969
1913
|
}
|
|
@@ -2006,7 +1950,7 @@ export class Client {
|
|
|
2006
1950
|
/**
|
|
2007
1951
|
* Gets all permissions for the logged in user.
|
|
2008
1952
|
*
|
|
2009
|
-
* @returns
|
|
1953
|
+
* @returns The list of Permission objects.
|
|
2010
1954
|
*/
|
|
2011
1955
|
private async getPermissions(): Promise<Permission[]> {
|
|
2012
1956
|
const res = await this.http.get(
|
|
@@ -2064,8 +2008,7 @@ export class Client {
|
|
|
2064
2008
|
responseType: "arraybuffer",
|
|
2065
2009
|
});
|
|
2066
2010
|
return decodeAxios(ActionTokenCodec, res.data);
|
|
2067
|
-
} catch
|
|
2068
|
-
this.log.warn(String(err));
|
|
2011
|
+
} catch {
|
|
2069
2012
|
return null;
|
|
2070
2013
|
}
|
|
2071
2014
|
}
|
|
@@ -2107,7 +2050,6 @@ export class Client {
|
|
|
2107
2050
|
private async handleNotify(msg: NotifyMsg) {
|
|
2108
2051
|
switch (msg.event) {
|
|
2109
2052
|
case "mail":
|
|
2110
|
-
this.log.info("Server has informed us of new mail.");
|
|
2111
2053
|
await this.getMail();
|
|
2112
2054
|
this.fetchingMail = false;
|
|
2113
2055
|
break;
|
|
@@ -2121,7 +2063,6 @@ export class Client {
|
|
|
2121
2063
|
// msg.data is the messageID for retry
|
|
2122
2064
|
break;
|
|
2123
2065
|
default:
|
|
2124
|
-
this.log.info("Unsupported notification event " + msg.event);
|
|
2125
2066
|
break;
|
|
2126
2067
|
}
|
|
2127
2068
|
}
|
|
@@ -2162,8 +2103,6 @@ export class Client {
|
|
|
2162
2103
|
// Auth sent as first message after open
|
|
2163
2104
|
this.socket = new WebSocketAdapter(wsUrl);
|
|
2164
2105
|
this.socket.on("open", () => {
|
|
2165
|
-
this.log.info("Connection opened.");
|
|
2166
|
-
// Send auth as first message (encoded to bytes — protocol is binary).
|
|
2167
2106
|
const authMsg = JSON.stringify({
|
|
2168
2107
|
token: this.token,
|
|
2169
2108
|
type: "auth",
|
|
@@ -2173,7 +2112,6 @@ export class Client {
|
|
|
2173
2112
|
});
|
|
2174
2113
|
|
|
2175
2114
|
this.socket.on("close", () => {
|
|
2176
|
-
this.log.info("Connection closed.");
|
|
2177
2115
|
if (this.pingInterval) {
|
|
2178
2116
|
clearInterval(this.pingInterval);
|
|
2179
2117
|
this.pingInterval = null;
|
|
@@ -2183,30 +2121,26 @@ export class Client {
|
|
|
2183
2121
|
}
|
|
2184
2122
|
});
|
|
2185
2123
|
|
|
2186
|
-
this.socket.on("error", (
|
|
2187
|
-
|
|
2124
|
+
this.socket.on("error", (_error: Error) => {
|
|
2125
|
+
if (!this.manuallyClosing) {
|
|
2126
|
+
this.emitter.emit("disconnect");
|
|
2127
|
+
}
|
|
2188
2128
|
});
|
|
2189
2129
|
|
|
2190
2130
|
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));
|
|
2131
|
+
const [_header, raw] = XUtils.unpackMessage(message);
|
|
2195
2132
|
|
|
2196
2133
|
const parseResult = WSMessageSchema.safeParse(raw);
|
|
2197
2134
|
if (!parseResult.success) {
|
|
2198
|
-
this.log.warn("Unknown WS message: " + JSON.stringify(raw));
|
|
2199
2135
|
return;
|
|
2200
2136
|
}
|
|
2201
2137
|
const msg = parseResult.data;
|
|
2202
2138
|
|
|
2203
2139
|
switch (msg.type) {
|
|
2204
2140
|
case "challenge":
|
|
2205
|
-
this.log.info("Received challenge from server.");
|
|
2206
2141
|
this.respond(msg);
|
|
2207
2142
|
break;
|
|
2208
2143
|
case "error":
|
|
2209
|
-
this.log.warn(JSON.stringify(msg));
|
|
2210
2144
|
break;
|
|
2211
2145
|
case "notify":
|
|
2212
2146
|
void this.handleNotify(msg);
|
|
@@ -2224,15 +2158,10 @@ export class Client {
|
|
|
2224
2158
|
"Received unauthorized message from server.",
|
|
2225
2159
|
);
|
|
2226
2160
|
case "authorized":
|
|
2227
|
-
this.log.info(
|
|
2228
|
-
"Authenticated with userID " +
|
|
2229
|
-
(this.user?.userID ?? "unknown"),
|
|
2230
|
-
);
|
|
2231
2161
|
this.emitter.emit("connected");
|
|
2232
2162
|
void this.postAuth();
|
|
2233
2163
|
break;
|
|
2234
2164
|
default:
|
|
2235
|
-
this.log.info("Unsupported message " + msg.type);
|
|
2236
2165
|
break;
|
|
2237
2166
|
}
|
|
2238
2167
|
});
|
|
@@ -2269,10 +2198,8 @@ export class Client {
|
|
|
2269
2198
|
|
|
2270
2199
|
private async negotiateOTK() {
|
|
2271
2200
|
const otkCount = await this.getOTKCount();
|
|
2272
|
-
this.log.info("Server reported OTK: " + otkCount.toString());
|
|
2273
2201
|
const needs = xConstants.MIN_OTK_SUPPLY - otkCount;
|
|
2274
2202
|
if (needs === 0) {
|
|
2275
|
-
this.log.info("Server otk supply full.");
|
|
2276
2203
|
return;
|
|
2277
2204
|
}
|
|
2278
2205
|
|
|
@@ -2288,7 +2215,6 @@ export class Client {
|
|
|
2288
2215
|
|
|
2289
2216
|
private ping() {
|
|
2290
2217
|
if (!this.isAlive) {
|
|
2291
|
-
this.log.warn("Ping failed.");
|
|
2292
2218
|
}
|
|
2293
2219
|
this.setAlive(false);
|
|
2294
2220
|
void this.send({ transmissionID: uuid.v4(), type: "ping" });
|
|
@@ -2309,9 +2235,6 @@ export class Client {
|
|
|
2309
2235
|
const preKeys: PreKeysCrypto =
|
|
2310
2236
|
existingPreKeys ??
|
|
2311
2237
|
(await (async () => {
|
|
2312
|
-
this.log.warn(
|
|
2313
|
-
"No prekeys found in database, creating a new one.",
|
|
2314
|
-
);
|
|
2315
2238
|
const unsaved = this.createPreKey();
|
|
2316
2239
|
const [saved] = await this.database.savePreKeys(
|
|
2317
2240
|
[unsaved],
|
|
@@ -2337,19 +2260,6 @@ export class Client {
|
|
|
2337
2260
|
identityKeys,
|
|
2338
2261
|
preKeys,
|
|
2339
2262
|
};
|
|
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
2263
|
}
|
|
2354
2264
|
|
|
2355
2265
|
private async postAuth() {
|
|
@@ -2364,9 +2274,7 @@ export class Client {
|
|
|
2364
2274
|
void this.negotiateOTK();
|
|
2365
2275
|
count = 0;
|
|
2366
2276
|
}
|
|
2367
|
-
} catch
|
|
2368
|
-
this.log.warn("Problem fetching mail" + String(err));
|
|
2369
|
-
}
|
|
2277
|
+
} catch {}
|
|
2370
2278
|
await sleep(1000 * 60);
|
|
2371
2279
|
}
|
|
2372
2280
|
}
|
|
@@ -2380,6 +2288,11 @@ export class Client {
|
|
|
2380
2288
|
mail: MailWS,
|
|
2381
2289
|
timestamp: string,
|
|
2382
2290
|
) {
|
|
2291
|
+
if (this.seenMailIDs.has(mail.mailID)) {
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
this.seenMailIDs.add(mail.mailID);
|
|
2295
|
+
|
|
2383
2296
|
this.sendReceipt(new Uint8Array(mail.nonce));
|
|
2384
2297
|
let timeout = 1;
|
|
2385
2298
|
while (this.reading) {
|
|
@@ -2390,7 +2303,6 @@ export class Client {
|
|
|
2390
2303
|
|
|
2391
2304
|
try {
|
|
2392
2305
|
const healSession = async () => {
|
|
2393
|
-
this.log.info("Requesting retry of " + mail.mailID);
|
|
2394
2306
|
const deviceEntry = await this.getDeviceByID(mail.sender);
|
|
2395
2307
|
const [user, _err] = await this.fetchUser(mail.authorID);
|
|
2396
2308
|
if (deviceEntry && user) {
|
|
@@ -2405,10 +2317,8 @@ export class Client {
|
|
|
2405
2317
|
}
|
|
2406
2318
|
};
|
|
2407
2319
|
|
|
2408
|
-
this.log.info("Received mail from " + mail.sender);
|
|
2409
2320
|
switch (mail.mailType) {
|
|
2410
2321
|
case MailType.initial:
|
|
2411
|
-
this.log.info("Initiating new session.");
|
|
2412
2322
|
const extraParts = Client.deserializeExtra(
|
|
2413
2323
|
MailType.initial,
|
|
2414
2324
|
new Uint8Array(mail.extra),
|
|
@@ -2424,48 +2334,18 @@ export class Client {
|
|
|
2424
2334
|
|
|
2425
2335
|
const preKeyIndex = XUtils.uint8ArrToNumber(indexBytes);
|
|
2426
2336
|
|
|
2427
|
-
this.log.info(
|
|
2428
|
-
this.toString() +
|
|
2429
|
-
" otk #" +
|
|
2430
|
-
String(preKeyIndex) +
|
|
2431
|
-
" indicated",
|
|
2432
|
-
);
|
|
2433
|
-
|
|
2434
2337
|
const otk =
|
|
2435
2338
|
preKeyIndex === 0
|
|
2436
2339
|
? null
|
|
2437
2340
|
: await this.database.getOneTimeKey(preKeyIndex);
|
|
2438
2341
|
|
|
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
2342
|
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
2343
|
return;
|
|
2463
2344
|
}
|
|
2464
2345
|
|
|
2465
2346
|
// their public keys
|
|
2466
2347
|
const IK_A_raw = XKeyConvert.convertPublicKey(signKey);
|
|
2467
2348
|
if (!IK_A_raw) {
|
|
2468
|
-
this.log.warn("Could not convert sign key to X25519.");
|
|
2469
2349
|
return;
|
|
2470
2350
|
}
|
|
2471
2351
|
const IK_A = IK_A_raw;
|
|
@@ -2493,26 +2373,9 @@ export class Client {
|
|
|
2493
2373
|
|
|
2494
2374
|
// shared secret key
|
|
2495
2375
|
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
2376
|
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
2377
|
|
|
2513
2378
|
const hmac = xHMAC(mail, SK);
|
|
2514
|
-
this.log.info("Mail hash: " + JSON.stringify(mail));
|
|
2515
|
-
this.log.info("Calculated hmac: " + XUtils.encodeHex(hmac));
|
|
2516
2379
|
|
|
2517
2380
|
// associated data
|
|
2518
2381
|
const AD = xConcat(
|
|
@@ -2521,22 +2384,14 @@ export class Client {
|
|
|
2521
2384
|
);
|
|
2522
2385
|
|
|
2523
2386
|
if (!XUtils.bytesEqual(hmac, header)) {
|
|
2524
|
-
console.warn(
|
|
2525
|
-
"Mail authentication failed (HMAC did not match).",
|
|
2526
|
-
);
|
|
2527
|
-
console.warn(mail);
|
|
2528
2387
|
return;
|
|
2529
2388
|
}
|
|
2530
|
-
this.log.info("Mail authenticated successfully.");
|
|
2531
|
-
|
|
2532
2389
|
const unsealed = xSecretboxOpen(
|
|
2533
2390
|
new Uint8Array(mail.cipher),
|
|
2534
2391
|
new Uint8Array(mail.nonce),
|
|
2535
2392
|
SK,
|
|
2536
2393
|
);
|
|
2537
2394
|
if (unsealed) {
|
|
2538
|
-
this.log.info("Decryption successful.");
|
|
2539
|
-
|
|
2540
2395
|
let plaintext = "";
|
|
2541
2396
|
if (!mail.forward) {
|
|
2542
2397
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
@@ -2607,12 +2462,8 @@ export class Client {
|
|
|
2607
2462
|
if (user) {
|
|
2608
2463
|
this.emitter.emit("session", newSession, user);
|
|
2609
2464
|
} else {
|
|
2610
|
-
this.log.warn(
|
|
2611
|
-
"Couldn't retrieve user " + newSession.userID,
|
|
2612
|
-
);
|
|
2613
2465
|
}
|
|
2614
2466
|
} else {
|
|
2615
|
-
this.log.warn("Mail decryption failed.");
|
|
2616
2467
|
}
|
|
2617
2468
|
break;
|
|
2618
2469
|
case MailType.subsequent:
|
|
@@ -2628,37 +2479,21 @@ export class Client {
|
|
|
2628
2479
|
let session = await this.getSessionByPubkey(publicKey);
|
|
2629
2480
|
let retries = 0;
|
|
2630
2481
|
while (!session) {
|
|
2631
|
-
if (retries
|
|
2482
|
+
if (retries >= 3) {
|
|
2632
2483
|
break;
|
|
2633
2484
|
}
|
|
2634
|
-
|
|
2635
|
-
session = await this.getSessionByPubkey(publicKey);
|
|
2485
|
+
await sleep(100 * 2 ** retries);
|
|
2636
2486
|
retries++;
|
|
2637
|
-
|
|
2487
|
+
session = await this.getSessionByPubkey(publicKey);
|
|
2638
2488
|
}
|
|
2639
2489
|
|
|
2640
2490
|
if (!session) {
|
|
2641
|
-
this.log.warn(
|
|
2642
|
-
"Couldn't find session public key " +
|
|
2643
|
-
XUtils.encodeHex(publicKey),
|
|
2644
|
-
);
|
|
2645
2491
|
void healSession();
|
|
2646
2492
|
return;
|
|
2647
2493
|
}
|
|
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
2494
|
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
2495
|
|
|
2658
2496
|
if (!XUtils.bytesEqual(HMAC, header)) {
|
|
2659
|
-
this.log.warn(
|
|
2660
|
-
"Message authentication failed (HMAC does not match).",
|
|
2661
|
-
);
|
|
2662
2497
|
void healSession();
|
|
2663
2498
|
return;
|
|
2664
2499
|
}
|
|
@@ -2670,8 +2505,6 @@ export class Client {
|
|
|
2670
2505
|
);
|
|
2671
2506
|
|
|
2672
2507
|
if (decrypted) {
|
|
2673
|
-
this.log.info("Decryption successful.");
|
|
2674
|
-
// emit the message
|
|
2675
2508
|
const fwdMsg2 = mail.forward
|
|
2676
2509
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
2677
2510
|
: null;
|
|
@@ -2702,7 +2535,6 @@ export class Client {
|
|
|
2702
2535
|
|
|
2703
2536
|
void this.database.markSessionUsed(session.sessionID);
|
|
2704
2537
|
} else {
|
|
2705
|
-
this.log.info("Decryption failed.");
|
|
2706
2538
|
void healSession();
|
|
2707
2539
|
|
|
2708
2540
|
// emit the message
|
|
@@ -2726,7 +2558,6 @@ export class Client {
|
|
|
2726
2558
|
}
|
|
2727
2559
|
break;
|
|
2728
2560
|
default:
|
|
2729
|
-
this.log.warn("Unsupported MailType:", mail.mailType);
|
|
2730
2561
|
break;
|
|
2731
2562
|
}
|
|
2732
2563
|
} finally {
|
|
@@ -2894,14 +2725,10 @@ export class Client {
|
|
|
2894
2725
|
);
|
|
2895
2726
|
device = decodeAxios(DeviceCodec, res.data);
|
|
2896
2727
|
} catch (err: unknown) {
|
|
2897
|
-
this.log.error(err instanceof Error ? err.message : String(err));
|
|
2898
2728
|
if (isAxiosError(err) && err.response?.status === 404) {
|
|
2899
|
-
// just in case
|
|
2900
2729
|
await this.database.purgeKeyData();
|
|
2901
2730
|
await this.populateKeyRing();
|
|
2902
2731
|
|
|
2903
|
-
this.log.info("Attempting to register device.");
|
|
2904
|
-
|
|
2905
2732
|
const newDevice = await this.registerDevice();
|
|
2906
2733
|
if (newDevice) {
|
|
2907
2734
|
device = newDevice;
|
|
@@ -2912,7 +2739,6 @@ export class Client {
|
|
|
2912
2739
|
throw err;
|
|
2913
2740
|
}
|
|
2914
2741
|
}
|
|
2915
|
-
this.log.info("Got device " + JSON.stringify(device, null, 4));
|
|
2916
2742
|
return device;
|
|
2917
2743
|
}
|
|
2918
2744
|
|
|
@@ -2920,17 +2746,20 @@ export class Client {
|
|
|
2920
2746
|
or contains an HMAC of the message with
|
|
2921
2747
|
a derived SK */
|
|
2922
2748
|
private async send(msg: ClientMessage, header?: Uint8Array) {
|
|
2923
|
-
|
|
2749
|
+
const maxWaitMs = 30_000;
|
|
2750
|
+
let elapsed = 0;
|
|
2751
|
+
let backoff = 50;
|
|
2924
2752
|
while (this.socket.readyState !== 1) {
|
|
2925
|
-
|
|
2926
|
-
|
|
2753
|
+
if (elapsed >= maxWaitMs) {
|
|
2754
|
+
throw new Error(
|
|
2755
|
+
"WebSocket did not reach OPEN state within 30 seconds.",
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
await sleep(backoff);
|
|
2759
|
+
elapsed += backoff;
|
|
2760
|
+
backoff = Math.min(backoff * 2, 4_000);
|
|
2927
2761
|
}
|
|
2928
2762
|
|
|
2929
|
-
this.log.debug(
|
|
2930
|
-
"OUTH " + XUtils.encodeHex(header || XUtils.emptyHeader()),
|
|
2931
|
-
);
|
|
2932
|
-
this.log.debug("OUT " + JSON.stringify(msg, null, 4));
|
|
2933
|
-
|
|
2934
2763
|
this.socket.send(XUtils.packMessage(msg, header));
|
|
2935
2764
|
}
|
|
2936
2765
|
|
|
@@ -2943,29 +2772,15 @@ export class Client {
|
|
|
2943
2772
|
this.userRecords[user.userID] = user;
|
|
2944
2773
|
}
|
|
2945
2774
|
|
|
2946
|
-
this.log.info(
|
|
2947
|
-
"Sending to userlist:\n" + JSON.stringify(userList, null, 4),
|
|
2948
|
-
);
|
|
2949
|
-
|
|
2950
2775
|
const mailID = uuid.v4();
|
|
2951
2776
|
const promises: Array<Promise<void>> = [];
|
|
2952
2777
|
|
|
2953
2778
|
const userIDs = [...new Set(userList.map((user) => user.userID))];
|
|
2954
2779
|
const devices = await this.getMultiUserDeviceList(userIDs);
|
|
2955
2780
|
|
|
2956
|
-
this.log.info(
|
|
2957
|
-
"Retrieved devicelist:\n" + JSON.stringify(devices, null, 4),
|
|
2958
|
-
);
|
|
2959
|
-
|
|
2960
2781
|
for (const device of devices) {
|
|
2961
2782
|
const ownerRecord = this.userRecords[device.owner];
|
|
2962
2783
|
if (!ownerRecord) {
|
|
2963
|
-
this.log.warn(
|
|
2964
|
-
"Skipping device " +
|
|
2965
|
-
device.deviceID +
|
|
2966
|
-
": no user record for owner " +
|
|
2967
|
-
device.owner,
|
|
2968
|
-
);
|
|
2969
2784
|
continue;
|
|
2970
2785
|
}
|
|
2971
2786
|
promises.push(
|
|
@@ -2983,8 +2798,6 @@ export class Client {
|
|
|
2983
2798
|
for (const result of results) {
|
|
2984
2799
|
const { status } = result;
|
|
2985
2800
|
if (status === "rejected") {
|
|
2986
|
-
this.log.warn("Message failed.");
|
|
2987
|
-
this.log.warn(JSON.stringify(result));
|
|
2988
2801
|
}
|
|
2989
2802
|
}
|
|
2990
2803
|
});
|
|
@@ -3001,20 +2814,8 @@ export class Client {
|
|
|
3001
2814
|
retry = false,
|
|
3002
2815
|
): Promise<void> {
|
|
3003
2816
|
while (this.sending.has(device.deviceID)) {
|
|
3004
|
-
this.log.warn(
|
|
3005
|
-
"Sending in progress to device ID " +
|
|
3006
|
-
device.deviceID +
|
|
3007
|
-
", waiting.",
|
|
3008
|
-
);
|
|
3009
2817
|
await sleep(100);
|
|
3010
2818
|
}
|
|
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
2819
|
this.sending.set(device.deviceID, device);
|
|
3019
2820
|
|
|
3020
2821
|
const session = await this.database.getSessionByDeviceID(
|
|
@@ -3022,11 +2823,8 @@ export class Client {
|
|
|
3022
2823
|
);
|
|
3023
2824
|
|
|
3024
2825
|
if (!session || retry) {
|
|
3025
|
-
this.log.info("Creating new session for " + device.deviceID);
|
|
3026
2826
|
await this.createSession(device, user, msg, group, mailID, forward);
|
|
3027
2827
|
return;
|
|
3028
|
-
} else {
|
|
3029
|
-
this.log.info("Found existing session for " + device.deviceID);
|
|
3030
2828
|
}
|
|
3031
2829
|
|
|
3032
2830
|
const nonce = xMakeNonce();
|
|
@@ -3056,8 +2854,6 @@ export class Client {
|
|
|
3056
2854
|
};
|
|
3057
2855
|
|
|
3058
2856
|
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
2857
|
|
|
3062
2858
|
const fwdOut = forward
|
|
3063
2859
|
? messageSchema.parse(msgpack.decode(msg))
|
|
@@ -3143,14 +2939,10 @@ export class Client {
|
|
|
3143
2939
|
for (const result of results) {
|
|
3144
2940
|
const { status } = result;
|
|
3145
2941
|
if (status === "rejected") {
|
|
3146
|
-
this.log.warn("Message failed.");
|
|
3147
|
-
this.log.warn(JSON.stringify(result));
|
|
3148
2942
|
}
|
|
3149
2943
|
}
|
|
3150
2944
|
});
|
|
3151
2945
|
} catch (err: unknown) {
|
|
3152
|
-
this.log.error("Message threw exception.");
|
|
3153
|
-
this.log.error(err instanceof Error ? err.message : String(err));
|
|
3154
2946
|
throw err;
|
|
3155
2947
|
}
|
|
3156
2948
|
}
|
|
@@ -3175,19 +2967,9 @@ export class Client {
|
|
|
3175
2967
|
private async submitOTK(amount: number) {
|
|
3176
2968
|
const otks: UnsavedPreKey[] = [];
|
|
3177
2969
|
|
|
3178
|
-
const t0 = performance.now();
|
|
3179
2970
|
for (let i = 0; i < amount; i++) {
|
|
3180
2971
|
otks[i] = this.createPreKey();
|
|
3181
2972
|
}
|
|
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
2973
|
|
|
3192
2974
|
const savedKeys = await this.database.savePreKeys(otks, true);
|
|
3193
2975
|
|