@vex-chat/libvex 1.0.1 → 1.1.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.
Files changed (60) hide show
  1. package/dist/Client.d.ts +25 -15
  2. package/dist/Client.js +210 -159
  3. package/dist/Client.js.map +1 -1
  4. package/dist/IStorage.d.ts +1 -1
  5. package/dist/IStorage.js +1 -1
  6. package/dist/IStorage.js.map +1 -1
  7. package/dist/__tests__/harness/memory-storage.d.ts +45 -0
  8. package/dist/__tests__/harness/memory-storage.js +181 -0
  9. package/dist/__tests__/harness/memory-storage.js.map +1 -0
  10. package/dist/index.d.ts +3 -0
  11. package/dist/keystore/memory.d.ts +11 -0
  12. package/dist/keystore/memory.js +18 -0
  13. package/dist/keystore/memory.js.map +1 -0
  14. package/dist/keystore/node.d.ts +10 -0
  15. package/dist/keystore/node.js +64 -0
  16. package/dist/keystore/node.js.map +1 -0
  17. package/dist/keystore/types.d.ts +4 -0
  18. package/dist/keystore/types.js +2 -0
  19. package/dist/keystore/types.js.map +1 -0
  20. package/dist/preset/expo.d.ts +2 -0
  21. package/dist/preset/expo.js +39 -0
  22. package/dist/preset/expo.js.map +1 -0
  23. package/dist/preset/node.d.ts +12 -0
  24. package/dist/preset/node.js +17 -0
  25. package/dist/preset/node.js.map +1 -0
  26. package/dist/preset/tauri.d.ts +2 -0
  27. package/dist/preset/tauri.js +36 -0
  28. package/dist/preset/tauri.js.map +1 -0
  29. package/dist/preset/test.d.ts +10 -0
  30. package/dist/preset/test.js +29 -0
  31. package/dist/preset/test.js.map +1 -0
  32. package/dist/preset/types.d.ts +14 -0
  33. package/dist/preset/types.js +2 -0
  34. package/dist/preset/types.js.map +1 -0
  35. package/dist/storage/expo.d.ts +3 -0
  36. package/dist/storage/expo.js +18 -0
  37. package/dist/storage/expo.js.map +1 -0
  38. package/dist/storage/node.d.ts +3 -0
  39. package/dist/storage/node.js +24 -0
  40. package/dist/storage/node.js.map +1 -0
  41. package/dist/storage/schema.d.ts +76 -0
  42. package/dist/storage/schema.js +2 -0
  43. package/dist/storage/schema.js.map +1 -0
  44. package/dist/{Storage.d.ts → storage/sqlite.d.ts} +19 -21
  45. package/dist/storage/sqlite.js +488 -0
  46. package/dist/storage/sqlite.js.map +1 -0
  47. package/dist/storage/tauri.d.ts +3 -0
  48. package/dist/storage/tauri.js +21 -0
  49. package/dist/storage/tauri.js.map +1 -0
  50. package/dist/transport/browser.d.ts +17 -0
  51. package/dist/transport/browser.js +56 -0
  52. package/dist/transport/browser.js.map +1 -0
  53. package/dist/transport/types.d.ts +20 -0
  54. package/dist/transport/types.js +2 -0
  55. package/dist/transport/types.js.map +1 -0
  56. package/dist/utils/createLogger.js +1 -8
  57. package/dist/utils/createLogger.js.map +1 -1
  58. package/package.json +100 -16
  59. package/dist/Storage.js +0 -444
  60. package/dist/Storage.js.map +0 -1
package/dist/Client.js CHANGED
@@ -2,32 +2,31 @@
2
2
  import { sleep } from "@extrahash/sleep";
3
3
  import { xConcat, xConstants, xDH, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMnemonic, XUtils, } from "@vex-chat/crypto";
4
4
  import { MailType } from "@vex-chat/types";
5
- import ax, { AxiosError } from "axios";
6
- import { isBrowser, isNode } from "browser-or-node";
7
- import btoa from "btoa";
8
- import chalk from "chalk";
9
- import { EventEmitter } from "events";
5
+ import axios, { AxiosError } from "axios";
6
+ import pc from "picocolors";
7
+ import { EventEmitter } from "eventemitter3";
10
8
  import { Packr } from "msgpackr";
11
9
  // useRecords:false emits standard msgpack (no nonstandard record extension).
12
10
  // moreTypes:false keeps the extension set to what every other decoder understands.
13
- // Packr.pack() returns Node Buffer, which axios sends correctly (plain Uint8Array
14
- // would have its pool buffer sent in full — see axios issue #4068).
15
- const msgpack = new Packr({ useRecords: false, moreTypes: false });
11
+ const _packr = new Packr({ useRecords: false, moreTypes: false });
12
+ // Packr.encode() returns a subarray of its internal pool buffer.
13
+ // In browsers, XMLHttpRequest.send() sends the full underlying ArrayBuffer,
14
+ // not just the slice — corrupting the payload (axios issue #4068).
15
+ // Wrap encode to always return a fresh copy.
16
+ const msgpack = {
17
+ encode: (value) => {
18
+ const packed = _packr.encode(value);
19
+ return new Uint8Array(packed.buffer.slice(packed.byteOffset, packed.byteOffset + packed.byteLength));
20
+ },
21
+ decode: _packr.decode.bind(_packr),
22
+ };
16
23
  import objectHash from "object-hash";
17
- import * as os from "node:os";
18
- import { performance } from "node:perf_hooks";
19
24
  import nacl from "tweetnacl";
20
25
  import * as uuid from "uuid";
21
- import winston from "winston";
22
- import WebSocket from "ws";
23
- import { Storage } from "./Storage.js";
24
26
  import { capitalize } from "./utils/capitalize.js";
25
- import { createLogger } from "./utils/createLogger.js";
26
27
  import { formatBytes } from "./utils/formatBytes.js";
27
28
  import { sqlSessionToCrypto } from "./utils/sqlSessionToCrypto.js";
28
29
  import { uuidToUint8 } from "./utils/uint8uuid.js";
29
- ax.defaults.withCredentials = true;
30
- ax.defaults.responseType = "arraybuffer";
31
30
  const protocolMsgRegex = /��\w+:\w+��/g;
32
31
  /**
33
32
  * Client provides an interface for you to use a vex chat server and
@@ -78,17 +77,17 @@ const protocolMsgRegex = /��\w+:\w+��/g;
78
77
  */
79
78
  export class Client extends EventEmitter {
80
79
  /**
81
- * Loads a key file from disk.
80
+ * Encrypts a secret key with a password.
82
81
  *
83
82
  * Pass-through utility from `@vex-chat/crypto`.
84
83
  */
85
- static loadKeyFile = XUtils.loadKeyFile;
84
+ static encryptKeyData = XUtils.encryptKeyData;
86
85
  /**
87
- * Saves a key file to disk.
86
+ * Decrypts a secret key from encrypted data produced by encryptKeyData().
88
87
  *
89
88
  * Pass-through utility from `@vex-chat/crypto`.
90
89
  */
91
- static saveKeyFile = XUtils.saveKeyFile;
90
+ static decryptKeyData = XUtils.decryptKeyData;
92
91
  /**
93
92
  * Creates and initializes a client in one step.
94
93
  *
@@ -102,7 +101,36 @@ export class Client extends EventEmitter {
102
101
  * ```
103
102
  */
104
103
  static create = async (privateKey, options, storage) => {
105
- const client = new Client(privateKey, options, storage);
104
+ let opts = options;
105
+ if (!opts?.adapters) {
106
+ const { default: WebSocket } = await import("ws");
107
+ const { createLogger: makeLog } = await import("./utils/createLogger.js");
108
+ opts = {
109
+ ...opts,
110
+ adapters: {
111
+ logger: makeLog("libvex", opts?.logLevel),
112
+ WebSocket: WebSocket,
113
+ },
114
+ };
115
+ }
116
+ // Lazily create Node Storage only on the Node path (no adapters).
117
+ // When adapters are provided (browser/RN), the caller must supply storage
118
+ // via PlatformPreset.createStorage() — there is no Node fallback.
119
+ let resolvedStorage = storage;
120
+ if (!resolvedStorage) {
121
+ if (opts?.adapters) {
122
+ throw new Error("No storage provided. When using adapters (browser/RN), pass storage from your PlatformPreset.");
123
+ }
124
+ const { createNodeStorage } = await import("./storage/node.js");
125
+ const dbFileName = opts?.inMemoryDb
126
+ ? ":memory:"
127
+ : XUtils.encodeHex(nacl.sign.keyPair.fromSecretKey(XUtils.decodeHex(privateKey || "")).publicKey) + ".sqlite";
128
+ const dbPath = opts?.dbFolder
129
+ ? opts.dbFolder + "/" + dbFileName
130
+ : dbFileName;
131
+ resolvedStorage = createNodeStorage(dbPath, privateKey || XUtils.encodeHex(nacl.sign.keyPair().secretKey));
132
+ }
133
+ const client = new Client(privateKey, opts, resolvedStorage);
106
134
  await client.init();
107
135
  return client;
108
136
  };
@@ -397,6 +425,7 @@ export class Client extends EventEmitter {
397
425
  dbPath;
398
426
  conn;
399
427
  host;
428
+ adapters;
400
429
  firstMailFetch = true;
401
430
  // these are created from one set of sign keys
402
431
  signKeys;
@@ -410,7 +439,7 @@ export class Client extends EventEmitter {
410
439
  isAlive = true;
411
440
  reading = false;
412
441
  fetchingMail = false;
413
- cookies = [];
442
+ ax;
414
443
  log;
415
444
  pingInterval = null;
416
445
  mailInterval;
@@ -418,9 +447,16 @@ export class Client extends EventEmitter {
418
447
  token = null;
419
448
  forwarded = [];
420
449
  prefixes;
450
+ options;
421
451
  constructor(privateKey, options, storage) {
422
452
  super();
423
- this.log = createLogger("client", options?.logLevel);
453
+ this.options = options;
454
+ this.log = options?.adapters?.logger ?? {
455
+ info() { },
456
+ warn() { },
457
+ error() { },
458
+ debug() { },
459
+ };
424
460
  this.prefixes = options?.unsafeHttp
425
461
  ? { HTTP: "http://", WS: "ws://" }
426
462
  : { HTTP: "https://", WS: "wss://" };
@@ -438,17 +474,21 @@ export class Client extends EventEmitter {
438
474
  this.dbPath = options?.dbFolder
439
475
  ? options?.dbFolder + "/" + dbFileName
440
476
  : dbFileName;
441
- this.database = storage
442
- ? storage
443
- : new Storage(this.dbPath, XUtils.encodeHex(this.signKeys.secretKey), options);
477
+ if (!storage) {
478
+ throw new Error("No storage provided. Use Client.create() which resolves storage automatically.");
479
+ }
480
+ this.database = storage;
444
481
  this.database.on("error", (error) => {
445
482
  this.log.error(error.toString());
446
483
  this.close(true);
447
484
  });
448
- // we want to initialize this later with login()
449
- this.conn = new WebSocket("ws://localhost:1234");
450
- // silence the error for connecting to junk ws
451
- // tslint:disable-next-line: no-empty
485
+ if (!options?.adapters) {
486
+ throw new Error("No adapters provided. Use Client.create() which resolves adapters automatically.");
487
+ }
488
+ this.adapters = options.adapters;
489
+ this.ax = axios.create({ responseType: "arraybuffer" });
490
+ // Placeholder connection — replaced by initSocket() during connect()
491
+ this.conn = new this.adapters.WebSocket("ws://localhost:1234");
452
492
  this.conn.onerror = () => { };
453
493
  this.log.info("Client debug information: " +
454
494
  JSON.stringify({
@@ -456,8 +496,7 @@ export class Client extends EventEmitter {
456
496
  host: this.getHost(),
457
497
  dbPath: this.dbPath,
458
498
  environment: {
459
- isBrowser,
460
- isNode,
499
+ platform: this.options?.deviceName ?? "unknown",
461
500
  },
462
501
  options,
463
502
  }, null, 4));
@@ -503,7 +542,7 @@ export class Client extends EventEmitter {
503
542
  };
504
543
  }
505
544
  /**
506
- * Authenticates with username/password and stores the auth token/cookie.
545
+ * Authenticates with username/password and stores the Bearer auth token.
507
546
  *
508
547
  * @param username Account username.
509
548
  * @param password Account password.
@@ -517,23 +556,16 @@ export class Client extends EventEmitter {
517
556
  */
518
557
  async login(username, password) {
519
558
  try {
520
- const res = await ax.post(this.getHost() + "/auth", msgpack.encode({
559
+ const res = await this.ax.post(this.getHost() + "/auth", msgpack.encode({
521
560
  username,
522
561
  password,
523
562
  }), {
524
563
  headers: { "Content-Type": "application/msgpack" },
525
564
  });
526
- const { user, token } = msgpack.decode(Buffer.from(res.data));
527
- const cookies = res.headers["set-cookie"];
528
- if (cookies) {
529
- for (const cookie of cookies) {
530
- if (cookie.includes("auth")) {
531
- this.addCookie(cookie);
532
- }
533
- }
534
- }
565
+ const { user, token } = msgpack.decode(new Uint8Array(res.data));
535
566
  this.setUser(user);
536
567
  this.token = token;
568
+ this.ax.defaults.headers.common.Authorization = `Bearer ${token}`;
537
569
  }
538
570
  catch (err) {
539
571
  console.error(err.toString());
@@ -542,9 +574,38 @@ export class Client extends EventEmitter {
542
574
  return null;
543
575
  }
544
576
  /**
545
- * Returns the authorization cookie details. Throws if you don't have a
546
- * valid authorization cookie.
577
+ * Authenticates using the device's Ed25519 signing key.
578
+ * No password needed — proves possession of the private key via
579
+ * challenge-response. Issues a short-lived (1-hour) JWT.
580
+ *
581
+ * Used by auto-login when stored credentials have a deviceKey
582
+ * but no valid session.
547
583
  */
584
+ async loginWithDeviceKey(deviceID) {
585
+ try {
586
+ const id = deviceID ?? this.device?.deviceID;
587
+ if (!id) {
588
+ return new Error("No deviceID — pass it or connect first.");
589
+ }
590
+ const signKeyHex = XUtils.encodeHex(this.signKeys.publicKey);
591
+ const challengeRes = await this.ax.post(this.getHost() + "/auth/device", msgpack.encode({
592
+ deviceID: id,
593
+ signKey: signKeyHex,
594
+ }), { headers: { "Content-Type": "application/msgpack" } });
595
+ const { challengeID, challenge } = msgpack.decode(new Uint8Array(challengeRes.data));
596
+ const signed = XUtils.encodeHex(nacl.sign(XUtils.decodeHex(challenge), this.signKeys.secretKey));
597
+ const verifyRes = await this.ax.post(this.getHost() + "/auth/device/verify", msgpack.encode({ challengeID, signed }), { headers: { "Content-Type": "application/msgpack" } });
598
+ const { user, token } = msgpack.decode(new Uint8Array(verifyRes.data));
599
+ this.setUser(user);
600
+ this.token = token;
601
+ this.ax.defaults.headers.common.Authorization = `Bearer ${token}`;
602
+ }
603
+ catch (err) {
604
+ this.log.error("Device-key auth failed: " + err);
605
+ return err instanceof Error ? err : new Error(String(err));
606
+ }
607
+ return null;
608
+ }
548
609
  /**
549
610
  * Returns details about the currently authenticated session.
550
611
  *
@@ -557,28 +618,26 @@ export class Client extends EventEmitter {
557
618
  * ```
558
619
  */
559
620
  async whoami() {
560
- const res = await ax.post(this.getHost() + "/whoami", null, {
561
- withCredentials: true,
562
- responseType: "arraybuffer",
563
- });
564
- const whoami = msgpack.decode(Buffer.from(res.data));
621
+ const res = await this.ax.post(this.getHost() + "/whoami");
622
+ const whoami = msgpack.decode(new Uint8Array(res.data));
565
623
  return whoami;
566
624
  }
567
625
  /**
568
626
  * Logs out the current authenticated session from the server.
569
627
  */
570
628
  async logout() {
571
- await ax.post(this.getHost() + "/goodbye");
629
+ await this.ax.post(this.getHost() + "/goodbye");
572
630
  }
573
631
  /**
574
- * Connects your device to the chat. You must have an valid authorization cookie.
632
+ * Connects your device to the chat. You must have a valid Bearer token.
575
633
  * You can check whoami() to see before calling connect().
576
634
  */
577
635
  async connect() {
578
636
  const { user, token } = await this.whoami();
579
637
  this.token = token;
638
+ this.ax.defaults.headers.common.Authorization = `Bearer ${token}`;
580
639
  if (!user || !token) {
581
- throw new Error("Auth cookie missing or expired. Log in again.");
640
+ throw new Error("Auth token missing or expired. Log in again.");
582
641
  }
583
642
  this.setUser(user);
584
643
  this.device = await this.retrieveOrCreateDevice();
@@ -587,17 +646,14 @@ export class Client extends EventEmitter {
587
646
  throw new Error("Couldn't get connect token.");
588
647
  }
589
648
  const signed = nacl.sign(Uint8Array.from(uuid.parse(connectToken.key)), this.signKeys.secretKey);
590
- const res = await ax.post(this.getHost() + "/device/" + this.device.deviceID + "/connect", msgpack.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
591
- const cookies = res.headers["set-cookie"];
592
- if (cookies) {
593
- for (const cookie of cookies) {
594
- if (cookie.includes("device")) {
595
- this.addCookie(cookie);
596
- }
597
- }
598
- }
649
+ const res = await this.ax.post(this.getHost() + "/device/" + this.device.deviceID + "/connect", msgpack.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
650
+ const { deviceToken } = msgpack.decode(new Uint8Array(res.data));
651
+ this.ax.defaults.headers.common["X-Device-Token"] = deviceToken;
599
652
  this.log.info("Starting websocket.");
600
653
  this.initSocket();
654
+ // Yield the event loop so the WS open callback fires and sends the
655
+ // auth message before OTK generation blocks for ~5s on mobile.
656
+ await new Promise((r) => setTimeout(r, 0));
601
657
  await this.negotiateOTK();
602
658
  }
603
659
  /**
@@ -624,11 +680,11 @@ export class Client extends EventEmitter {
624
680
  preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
625
681
  preKeyIndex: this.xKeyRing.preKeys.index,
626
682
  password,
627
- deviceName: `${os.platform()}`,
683
+ deviceName: this.options?.deviceName ?? "unknown",
628
684
  };
629
685
  try {
630
- const res = await ax.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
631
- this.setUser(msgpack.decode(Buffer.from(res.data)));
686
+ const res = await this.ax.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
687
+ this.setUser(msgpack.decode(new Uint8Array(res.data)));
632
688
  return [this.getUser(), null];
633
689
  }
634
690
  catch (err) {
@@ -651,32 +707,31 @@ export class Client extends EventEmitter {
651
707
  return this.user?.username + "<" + this.device?.deviceID + ">";
652
708
  }
653
709
  async redeemInvite(inviteID) {
654
- const res = await ax.patch(this.getHost() + "/invite/" + inviteID);
655
- return msgpack.decode(Buffer.from(res.data));
710
+ const res = await this.ax.patch(this.getHost() + "/invite/" + inviteID);
711
+ return msgpack.decode(new Uint8Array(res.data));
656
712
  }
657
713
  async retrieveInvites(serverID) {
658
- const res = await ax.get(this.getHost() + "/server/" + serverID + "/invites");
659
- return msgpack.decode(Buffer.from(res.data));
714
+ const res = await this.ax.get(this.getHost() + "/server/" + serverID + "/invites");
715
+ return msgpack.decode(new Uint8Array(res.data));
660
716
  }
661
717
  async createInvite(serverID, duration) {
662
718
  const payload = {
663
719
  serverID,
664
720
  duration,
665
721
  };
666
- const res = await ax.post(this.getHost() + "/server/" + serverID + "/invites", payload);
667
- return msgpack.decode(Buffer.from(res.data));
722
+ const res = await this.ax.post(this.getHost() + "/server/" + serverID + "/invites", payload);
723
+ return msgpack.decode(new Uint8Array(res.data));
668
724
  }
669
725
  async retrieveEmojiList(serverID) {
670
- const res = await ax.get(this.getHost() + "/server/" + serverID + "/emoji");
671
- return msgpack.decode(Buffer.from(res.data));
726
+ const res = await this.ax.get(this.getHost() + "/server/" + serverID + "/emoji");
727
+ return msgpack.decode(new Uint8Array(res.data));
672
728
  }
673
729
  async retrieveEmojiByID(emojiID) {
674
- const res = await ax.get(this.getHost() + "/emoji/" + emojiID + "/details");
675
- // this is actually empty string
730
+ const res = await this.ax.get(this.getHost() + "/emoji/" + emojiID + "/details");
676
731
  if (!res.data) {
677
732
  return null;
678
733
  }
679
- return msgpack.decode(Buffer.from(res.data));
734
+ return msgpack.decode(new Uint8Array(res.data));
680
735
  }
681
736
  async leaveServer(serverID) {
682
737
  const permissionList = await this.permissions.retrieve();
@@ -696,25 +751,13 @@ export class Client extends EventEmitter {
696
751
  }
697
752
  throw new Error("Couldn't kick user.");
698
753
  }
699
- addCookie(cookie) {
700
- if (!this.cookies.includes(cookie)) {
701
- this.cookies.push(cookie);
702
- this.log.info("cookies changed", this.getCookies());
703
- if (isNode) {
704
- ax.defaults.headers.cookie = this.cookies.join(";");
705
- }
706
- }
707
- }
708
- getCookies() {
709
- return this.cookies.join(";");
710
- }
711
754
  async uploadEmoji(emoji, name, serverID) {
712
755
  if (typeof FormData !== "undefined") {
713
756
  const fpayload = new FormData();
714
757
  fpayload.set("emoji", new Blob([new Uint8Array(emoji)]));
715
758
  fpayload.set("name", name);
716
759
  try {
717
- const res = await ax.post(this.getHost() + "/emoji/" + serverID, fpayload, {
760
+ const res = await this.ax.post(this.getHost() + "/emoji/" + serverID, fpayload, {
718
761
  headers: { "Content-Type": "multipart/form-data" },
719
762
  onUploadProgress: (progressEvent) => {
720
763
  const percentCompleted = Math.round((progressEvent.loaded * 100) /
@@ -730,7 +773,7 @@ export class Client extends EventEmitter {
730
773
  this.emit("fileProgress", progress);
731
774
  },
732
775
  });
733
- return msgpack.decode(Buffer.from(res.data));
776
+ return msgpack.decode(new Uint8Array(res.data));
734
777
  }
735
778
  catch (err) {
736
779
  return null;
@@ -741,8 +784,8 @@ export class Client extends EventEmitter {
741
784
  name,
742
785
  };
743
786
  try {
744
- const res = await ax.post(this.getHost() + "/emoji/" + serverID + "/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
745
- return msgpack.decode(Buffer.from(res.data));
787
+ const res = await this.ax.post(this.getHost() + "/emoji/" + serverID + "/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
788
+ return msgpack.decode(new Uint8Array(res.data));
746
789
  }
747
790
  catch (err) {
748
791
  return null;
@@ -751,11 +794,11 @@ export class Client extends EventEmitter {
751
794
  async retrieveOrCreateDevice() {
752
795
  let device;
753
796
  try {
754
- const res = await ax.get(this.prefixes.HTTP +
797
+ const res = await this.ax.get(this.prefixes.HTTP +
755
798
  this.host +
756
799
  "/device/" +
757
800
  XUtils.encodeHex(this.signKeys.publicKey));
758
- device = msgpack.decode(Buffer.from(res.data));
801
+ device = msgpack.decode(new Uint8Array(res.data));
759
802
  }
760
803
  catch (err) {
761
804
  this.log.error(err.toString());
@@ -803,15 +846,15 @@ export class Client extends EventEmitter {
803
846
  preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
804
847
  preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
805
848
  preKeyIndex: this.xKeyRing.preKeys.index,
806
- deviceName: `${os.platform()}`,
849
+ deviceName: this.options?.deviceName ?? "unknown",
807
850
  };
808
851
  try {
809
- const res = await ax.post(this.prefixes.HTTP +
852
+ const res = await this.ax.post(this.prefixes.HTTP +
810
853
  this.host +
811
854
  "/user/" +
812
855
  userDetails.userID +
813
856
  "/devices", msgpack.encode(devMsg), { headers: { "Content-Type": "application/msgpack" } });
814
- return msgpack.decode(Buffer.from(res.data));
857
+ return msgpack.decode(new Uint8Array(res.data));
815
858
  }
816
859
  catch (err) {
817
860
  throw err;
@@ -819,10 +862,10 @@ export class Client extends EventEmitter {
819
862
  }
820
863
  async getToken(type) {
821
864
  try {
822
- const res = await ax.get(this.getHost() + "/token/" + type, {
865
+ const res = await this.ax.get(this.getHost() + "/token/" + type, {
823
866
  responseType: "arraybuffer",
824
867
  });
825
- return msgpack.decode(Buffer.from(res.data));
868
+ return msgpack.decode(new Uint8Array(res.data));
826
869
  }
827
870
  catch (err) {
828
871
  this.log.warn(err.toString());
@@ -833,7 +876,7 @@ export class Client extends EventEmitter {
833
876
  if (typeof FormData !== "undefined") {
834
877
  const fpayload = new FormData();
835
878
  fpayload.set("avatar", new Blob([new Uint8Array(avatar)]));
836
- await ax.post(this.prefixes.HTTP +
879
+ await this.ax.post(this.prefixes.HTTP +
837
880
  this.host +
838
881
  "/avatar/" +
839
882
  this.me.user().userID, fpayload, {
@@ -857,7 +900,7 @@ export class Client extends EventEmitter {
857
900
  const payload = {
858
901
  file: XUtils.encodeBase64(avatar),
859
902
  };
860
- await ax.post(this.prefixes.HTTP +
903
+ await this.ax.post(this.prefixes.HTTP +
861
904
  this.host +
862
905
  "/avatar/" +
863
906
  this.me.user().userID +
@@ -869,12 +912,12 @@ export class Client extends EventEmitter {
869
912
  * @returns - The list of IPermissions objects.
870
913
  */
871
914
  async fetchPermissionList(serverID) {
872
- const res = await ax.get(this.prefixes.HTTP +
915
+ const res = await this.ax.get(this.prefixes.HTTP +
873
916
  this.host +
874
917
  "/server/" +
875
918
  serverID +
876
919
  "/permissions");
877
- return msgpack.decode(Buffer.from(res.data));
920
+ return msgpack.decode(new Uint8Array(res.data));
878
921
  }
879
922
  /**
880
923
  * Gets all permissions for the logged in user.
@@ -882,17 +925,17 @@ export class Client extends EventEmitter {
882
925
  * @returns - The list of IPermissions objects.
883
926
  */
884
927
  async getPermissions() {
885
- const res = await ax.get(this.getHost() + "/user/" + this.getUser().userID + "/permissions");
886
- return msgpack.decode(Buffer.from(res.data));
928
+ const res = await this.ax.get(this.getHost() + "/user/" + this.getUser().userID + "/permissions");
929
+ return msgpack.decode(new Uint8Array(res.data));
887
930
  }
888
931
  async deletePermission(permissionID) {
889
- await ax.delete(this.getHost() + "/permission/" + permissionID);
932
+ await this.ax.delete(this.getHost() + "/permission/" + permissionID);
890
933
  }
891
934
  async retrieveFile(fileID, key) {
892
935
  try {
893
- const detailsRes = await ax.get(this.getHost() + "/file/" + fileID + "/details");
894
- const details = msgpack.decode(Buffer.from(detailsRes.data));
895
- const res = await ax.get(this.getHost() + "/file/" + fileID, {
936
+ const detailsRes = await this.ax.get(this.getHost() + "/file/" + fileID + "/details");
937
+ const details = msgpack.decode(new Uint8Array(detailsRes.data));
938
+ const res = await this.ax.get(this.getHost() + "/file/" + fileID, {
896
939
  onDownloadProgress: (progressEvent) => {
897
940
  const percentCompleted = Math.round((progressEvent.loaded * 100) /
898
941
  (progressEvent.total ?? 1));
@@ -908,11 +951,11 @@ export class Client extends EventEmitter {
908
951
  },
909
952
  });
910
953
  const fileData = res.data;
911
- const decrypted = nacl.secretbox.open(Uint8Array.from(Buffer.from(fileData)), XUtils.decodeHex(details.nonce), XUtils.decodeHex(key));
954
+ const decrypted = nacl.secretbox.open(new Uint8Array(fileData), XUtils.decodeHex(details.nonce), XUtils.decodeHex(key));
912
955
  if (decrypted) {
913
956
  const resp = {
914
957
  details,
915
- data: Buffer.from(decrypted),
958
+ data: new Uint8Array(decrypted),
916
959
  };
917
960
  return resp;
918
961
  }
@@ -923,14 +966,14 @@ export class Client extends EventEmitter {
923
966
  }
924
967
  }
925
968
  async deleteServer(serverID) {
926
- await ax.delete(this.getHost() + "/server/" + serverID);
969
+ await this.ax.delete(this.getHost() + "/server/" + serverID);
927
970
  }
928
971
  /**
929
972
  * Initializes the keyring. This must be called before anything else.
930
973
  */
931
974
  async init() {
932
975
  if (this.hasInit) {
933
- return new Error("You should only call init() once.");
976
+ throw new Error("You should only call init() once.");
934
977
  }
935
978
  this.hasInit = true;
936
979
  await this.populateKeyRing();
@@ -947,21 +990,21 @@ export class Client extends EventEmitter {
947
990
  this.emit("ready");
948
991
  }
949
992
  async deleteChannel(channelID) {
950
- await ax.delete(this.getHost() + "/channel/" + channelID);
993
+ await this.ax.delete(this.getHost() + "/channel/" + channelID);
951
994
  }
952
995
  // returns the file details and the encryption key
953
996
  async createFile(file) {
954
- this.log.info("Creating file, size: " + formatBytes(Buffer.byteLength(file)));
997
+ this.log.info("Creating file, size: " + formatBytes(file.byteLength));
955
998
  const nonce = xMakeNonce();
956
999
  const key = nacl.box.keyPair();
957
1000
  const box = nacl.secretbox(Uint8Array.from(file), nonce, key.secretKey);
958
- this.log.info("Encrypted size: " + formatBytes(Buffer.byteLength(box)));
1001
+ this.log.info("Encrypted size: " + formatBytes(box.byteLength));
959
1002
  if (typeof FormData !== "undefined") {
960
1003
  const fpayload = new FormData();
961
1004
  fpayload.set("owner", this.getDevice().deviceID);
962
1005
  fpayload.set("nonce", XUtils.encodeHex(nonce));
963
1006
  fpayload.set("file", new Blob([new Uint8Array(box)]));
964
- const fres = await ax.post(this.getHost() + "/file", fpayload, {
1007
+ const fres = await this.ax.post(this.getHost() + "/file", fpayload, {
965
1008
  headers: { "Content-Type": "multipart/form-data" },
966
1009
  onUploadProgress: (progressEvent) => {
967
1010
  const percentCompleted = Math.round((progressEvent.loaded * 100) /
@@ -977,7 +1020,7 @@ export class Client extends EventEmitter {
977
1020
  this.emit("fileProgress", progress);
978
1021
  },
979
1022
  });
980
- const fcreatedFile = msgpack.decode(Buffer.from(fres.data));
1023
+ const fcreatedFile = msgpack.decode(new Uint8Array(fres.data));
981
1024
  return [fcreatedFile, XUtils.encodeHex(key.secretKey)];
982
1025
  }
983
1026
  const payload = {
@@ -985,13 +1028,13 @@ export class Client extends EventEmitter {
985
1028
  nonce: XUtils.encodeHex(nonce),
986
1029
  file: XUtils.encodeBase64(box),
987
1030
  };
988
- const res = await ax.post(this.getHost() + "/file/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
989
- const createdFile = msgpack.decode(Buffer.from(res.data));
1031
+ const res = await this.ax.post(this.getHost() + "/file/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
1032
+ const createdFile = msgpack.decode(new Uint8Array(res.data));
990
1033
  return [createdFile, XUtils.encodeHex(key.secretKey)];
991
1034
  }
992
1035
  async getUserList(channelID) {
993
- const res = await ax.post(this.getHost() + "/userList/" + channelID);
994
- return msgpack.decode(Buffer.from(res.data));
1036
+ const res = await this.ax.post(this.getHost() + "/userList/" + channelID);
1037
+ return msgpack.decode(new Uint8Array(res.data));
995
1038
  }
996
1039
  async markSessionVerified(sessionID) {
997
1040
  return this.database.markSessionVerified(sessionID);
@@ -1040,7 +1083,7 @@ export class Client extends EventEmitter {
1040
1083
  const { status } = result;
1041
1084
  if (status === "rejected") {
1042
1085
  this.log.warn("Message failed.");
1043
- this.log.warn(result);
1086
+ this.log.warn(JSON.stringify(result));
1044
1087
  }
1045
1088
  }
1046
1089
  });
@@ -1073,14 +1116,14 @@ export class Client extends EventEmitter {
1073
1116
  const { status } = result;
1074
1117
  if (status === "rejected") {
1075
1118
  this.log.warn("Message failed.");
1076
- this.log.warn(result);
1119
+ this.log.warn(JSON.stringify(result));
1077
1120
  }
1078
1121
  }
1079
1122
  });
1080
1123
  }
1081
1124
  async createServer(name) {
1082
- const res = await ax.post(this.getHost() + "/server/" + btoa(name));
1083
- return msgpack.decode(Buffer.from(res.data));
1125
+ const res = await this.ax.post(this.getHost() + "/server/" + globalThis.btoa(name));
1126
+ return msgpack.decode(new Uint8Array(res.data));
1084
1127
  }
1085
1128
  async forward(message) {
1086
1129
  const copy = { ...message };
@@ -1109,7 +1152,7 @@ export class Client extends EventEmitter {
1109
1152
  const { status } = result;
1110
1153
  if (status === "rejected") {
1111
1154
  this.log.warn("Message failed.");
1112
- this.log.warn(result);
1155
+ this.log.warn(JSON.stringify(result));
1113
1156
  }
1114
1157
  }
1115
1158
  });
@@ -1203,13 +1246,13 @@ export class Client extends EventEmitter {
1203
1246
  return this.database.getAllSessions();
1204
1247
  }
1205
1248
  async getServerList() {
1206
- const res = await ax.get(this.getHost() + "/user/" + this.getUser().userID + "/servers");
1207
- return msgpack.decode(Buffer.from(res.data));
1249
+ const res = await this.ax.get(this.getHost() + "/user/" + this.getUser().userID + "/servers");
1250
+ return msgpack.decode(new Uint8Array(res.data));
1208
1251
  }
1209
1252
  async createChannel(name, serverID) {
1210
1253
  const body = { name };
1211
- const res = await ax.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
1212
- return msgpack.decode(Buffer.from(res.data));
1254
+ const res = await this.ax.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
1255
+ return msgpack.decode(new Uint8Array(res.data));
1213
1256
  }
1214
1257
  async getDeviceByID(deviceID) {
1215
1258
  if (this.deviceRecords[deviceID]) {
@@ -1223,9 +1266,9 @@ export class Client extends EventEmitter {
1223
1266
  return device;
1224
1267
  }
1225
1268
  try {
1226
- const res = await ax.get(this.getHost() + "/device/" + deviceID);
1269
+ const res = await this.ax.get(this.getHost() + "/device/" + deviceID);
1227
1270
  this.log.info("Retrieved device from server.");
1228
- const fetchedDevice = msgpack.decode(Buffer.from(res.data));
1271
+ const fetchedDevice = msgpack.decode(new Uint8Array(res.data));
1229
1272
  this.deviceRecords[deviceID] = fetchedDevice;
1230
1273
  await this.database.saveDevice(fetchedDevice);
1231
1274
  return fetchedDevice;
@@ -1238,7 +1281,7 @@ export class Client extends EventEmitter {
1238
1281
  if (deviceID === this.getDevice().deviceID) {
1239
1282
  throw new Error("You can't delete the device you're logged in to.");
1240
1283
  }
1241
- await ax.delete(this.prefixes.HTTP +
1284
+ await this.ax.delete(this.prefixes.HTTP +
1242
1285
  this.host +
1243
1286
  "/user/" +
1244
1287
  this.getUser().userID +
@@ -1247,8 +1290,8 @@ export class Client extends EventEmitter {
1247
1290
  }
1248
1291
  async getMultiUserDeviceList(userIDs) {
1249
1292
  try {
1250
- const res = await ax.post(this.getHost() + "/deviceList", msgpack.encode(userIDs), { headers: { "Content-Type": "application/msgpack" } });
1251
- const devices = msgpack.decode(Buffer.from(res.data));
1293
+ const res = await this.ax.post(this.getHost() + "/deviceList", msgpack.encode(userIDs), { headers: { "Content-Type": "application/msgpack" } });
1294
+ const devices = msgpack.decode(new Uint8Array(res.data));
1252
1295
  for (const device of devices) {
1253
1296
  this.deviceRecords[device.deviceID] = device;
1254
1297
  }
@@ -1260,8 +1303,8 @@ export class Client extends EventEmitter {
1260
1303
  }
1261
1304
  async getUserDeviceList(userID) {
1262
1305
  try {
1263
- const res = await ax.get(this.getHost() + "/user/" + userID + "/devices");
1264
- const devices = msgpack.decode(Buffer.from(res.data));
1306
+ const res = await this.ax.get(this.getHost() + "/user/" + userID + "/devices");
1307
+ const devices = msgpack.decode(new Uint8Array(res.data));
1265
1308
  for (const device of devices) {
1266
1309
  this.deviceRecords[device.deviceID] = device;
1267
1310
  }
@@ -1273,8 +1316,8 @@ export class Client extends EventEmitter {
1273
1316
  }
1274
1317
  async getServerByID(serverID) {
1275
1318
  try {
1276
- const res = await ax.get(this.getHost() + "/server/" + serverID);
1277
- return msgpack.decode(Buffer.from(res.data));
1319
+ const res = await this.ax.get(this.getHost() + "/server/" + serverID);
1320
+ return msgpack.decode(new Uint8Array(res.data));
1278
1321
  }
1279
1322
  catch (err) {
1280
1323
  return null;
@@ -1282,16 +1325,16 @@ export class Client extends EventEmitter {
1282
1325
  }
1283
1326
  async getChannelByID(channelID) {
1284
1327
  try {
1285
- const res = await ax.get(this.getHost() + "/channel/" + channelID);
1286
- return msgpack.decode(Buffer.from(res.data));
1328
+ const res = await this.ax.get(this.getHost() + "/channel/" + channelID);
1329
+ return msgpack.decode(new Uint8Array(res.data));
1287
1330
  }
1288
1331
  catch (err) {
1289
1332
  return null;
1290
1333
  }
1291
1334
  }
1292
1335
  async getChannelList(serverID) {
1293
- const res = await ax.get(this.getHost() + "/server/" + serverID + "/channels");
1294
- return msgpack.decode(Buffer.from(res.data));
1336
+ const res = await this.ax.get(this.getHost() + "/server/" + serverID + "/channels");
1337
+ return msgpack.decode(new Uint8Array(res.data));
1295
1338
  }
1296
1339
  /* Get the currently logged in user. You cannot call this until
1297
1340
  after the auth event is emitted. */
@@ -1318,8 +1361,8 @@ export class Client extends EventEmitter {
1318
1361
  return [this.userRecords[userIdentifier], null];
1319
1362
  }
1320
1363
  try {
1321
- const res = await ax.get(this.getHost() + "/user/" + userIdentifier);
1322
- const userRecord = msgpack.decode(Buffer.from(res.data));
1364
+ const res = await this.ax.get(this.getHost() + "/user/" + userIdentifier);
1365
+ const userRecord = msgpack.decode(new Uint8Array(res.data));
1323
1366
  this.userRecords[userIdentifier] = userRecord;
1324
1367
  return [userRecord, null];
1325
1368
  }
@@ -1804,13 +1847,21 @@ export class Client extends EventEmitter {
1804
1847
  if (!this.token) {
1805
1848
  throw new Error("No token found, did you call login()?");
1806
1849
  }
1807
- this.conn = new WebSocket(this.prefixes.WS + this.host + "/socket", { headers: { Cookie: "auth=" + this.token } });
1850
+ const wsUrl = this.prefixes.WS + this.host + "/socket";
1851
+ // Auth sent as first message after open
1852
+ this.conn = new this.adapters.WebSocket(wsUrl);
1808
1853
  this.conn.on("open", () => {
1809
1854
  this.log.info("Connection opened.");
1855
+ // Send auth as first message before anything else.
1856
+ this.conn.send(JSON.stringify({ type: "auth", token: this.token }));
1810
1857
  this.pingInterval = setInterval(this.ping.bind(this), 15000);
1811
1858
  });
1812
1859
  this.conn.on("close", () => {
1813
1860
  this.log.info("Connection closed.");
1861
+ if (this.pingInterval) {
1862
+ clearInterval(this.pingInterval);
1863
+ this.pingInterval = null;
1864
+ }
1814
1865
  if (!this.manuallyClosing) {
1815
1866
  this.emit("disconnect");
1816
1867
  }
@@ -1820,8 +1871,8 @@ export class Client extends EventEmitter {
1820
1871
  });
1821
1872
  this.conn.on("message", async (message) => {
1822
1873
  const [header, msg] = XUtils.unpackMessage(message);
1823
- this.log.debug(chalk.red.bold("INH ") + XUtils.encodeHex(header));
1824
- this.log.debug(chalk.red.bold("IN ") + JSON.stringify(msg, null, 4));
1874
+ this.log.debug(pc.red(pc.bold("INH ") + XUtils.encodeHex(header)));
1875
+ this.log.debug(pc.red(pc.bold("IN ") + JSON.stringify(msg, null, 4)));
1825
1876
  switch (msg.type) {
1826
1877
  case "ping":
1827
1878
  this.pong(msg.transmissionID);
@@ -1894,12 +1945,12 @@ export class Client extends EventEmitter {
1894
1945
  }
1895
1946
  this.log.info("fetching mail for device " + this.getDevice().deviceID);
1896
1947
  try {
1897
- const res = await ax.post(this.getHost() +
1948
+ const res = await this.ax.post(this.getHost() +
1898
1949
  "/device/" +
1899
1950
  this.getDevice().deviceID +
1900
1951
  "/mail");
1901
1952
  const inbox = msgpack
1902
- .decode(Buffer.from(res.data))
1953
+ .decode(new Uint8Array(res.data))
1903
1954
  .sort((a, b) => b[2].getTime() - a[2].getTime());
1904
1955
  for (const mailDetails of inbox) {
1905
1956
  const [mailHeader, mailBody, timestamp] = mailDetails;
@@ -1925,21 +1976,21 @@ export class Client extends EventEmitter {
1925
1976
  await sleep(i);
1926
1977
  i *= 2;
1927
1978
  }
1928
- this.log.debug(chalk.red.bold("OUTH ") +
1929
- XUtils.encodeHex(header || XUtils.emptyHeader()));
1930
- this.log.debug(chalk.red.bold("OUT ") + JSON.stringify(msg, null, 4));
1979
+ this.log.debug(pc.red(pc.bold("OUTH ") +
1980
+ XUtils.encodeHex(header || XUtils.emptyHeader())));
1981
+ this.log.debug(pc.red(pc.bold("OUT ") + JSON.stringify(msg, null, 4)));
1931
1982
  this.conn.send(XUtils.packMessage(msg, header));
1932
1983
  }
1933
1984
  async retrieveKeyBundle(deviceID) {
1934
- const res = await ax.post(this.getHost() + "/device/" + deviceID + "/keyBundle");
1935
- return msgpack.decode(Buffer.from(res.data));
1985
+ const res = await this.ax.post(this.getHost() + "/device/" + deviceID + "/keyBundle");
1986
+ return msgpack.decode(new Uint8Array(res.data));
1936
1987
  }
1937
1988
  async getOTKCount() {
1938
- const res = await ax.get(this.getHost() +
1989
+ const res = await this.ax.get(this.getHost() +
1939
1990
  "/device/" +
1940
1991
  this.getDevice().deviceID +
1941
1992
  "/otk/count");
1942
- return msgpack.decode(Buffer.from(res.data)).count;
1993
+ return msgpack.decode(new Uint8Array(res.data)).count;
1943
1994
  }
1944
1995
  async submitOTK(amount) {
1945
1996
  const otks = [];
@@ -1950,7 +2001,7 @@ export class Client extends EventEmitter {
1950
2001
  const t1 = performance.now();
1951
2002
  this.log.info("Generated " + amount + " one time keys in " + (t1 - t0) + " ms.");
1952
2003
  const savedKeys = await this.database.savePreKeys(otks, true);
1953
- await ax.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {
2004
+ await this.ax.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {
1954
2005
  headers: { "Content-Type": "application/msgpack" },
1955
2006
  });
1956
2007
  }