@vex-chat/libvex 5.2.0 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CLA.md +38 -0
  2. package/LICENSE-COMMERCIAL +10 -0
  3. package/LICENSING.md +15 -0
  4. package/README.md +8 -2
  5. package/dist/Client.d.ts +39 -3
  6. package/dist/Client.d.ts.map +1 -1
  7. package/dist/Client.js +961 -480
  8. package/dist/Client.js.map +1 -1
  9. package/dist/Storage.d.ts +5 -0
  10. package/dist/Storage.d.ts.map +1 -1
  11. package/dist/Storage.js +5 -0
  12. package/dist/Storage.js.map +1 -1
  13. package/dist/__tests__/harness/memory-storage.d.ts +7 -2
  14. package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
  15. package/dist/__tests__/harness/memory-storage.js +44 -29
  16. package/dist/__tests__/harness/memory-storage.js.map +1 -1
  17. package/dist/codec.d.ts +9 -9
  18. package/dist/codec.d.ts.map +1 -1
  19. package/dist/codec.js +17 -19
  20. package/dist/codec.js.map +1 -1
  21. package/dist/codecs.d.ts +5 -0
  22. package/dist/codecs.d.ts.map +1 -1
  23. package/dist/codecs.js +5 -0
  24. package/dist/codecs.js.map +1 -1
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/keystore/memory.d.ts +5 -0
  30. package/dist/keystore/memory.d.ts.map +1 -1
  31. package/dist/keystore/memory.js +5 -0
  32. package/dist/keystore/memory.js.map +1 -1
  33. package/dist/keystore/node.d.ts +5 -0
  34. package/dist/keystore/node.d.ts.map +1 -1
  35. package/dist/keystore/node.js +16 -8
  36. package/dist/keystore/node.js.map +1 -1
  37. package/dist/preset/common.d.ts +5 -0
  38. package/dist/preset/common.d.ts.map +1 -1
  39. package/dist/preset/common.js +5 -0
  40. package/dist/preset/common.js.map +1 -1
  41. package/dist/preset/node.d.ts +5 -0
  42. package/dist/preset/node.d.ts.map +1 -1
  43. package/dist/preset/node.js +9 -1
  44. package/dist/preset/node.js.map +1 -1
  45. package/dist/preset/test.d.ts +5 -0
  46. package/dist/preset/test.d.ts.map +1 -1
  47. package/dist/preset/test.js +9 -1
  48. package/dist/preset/test.js.map +1 -1
  49. package/dist/storage/node/http-agents.d.ts +5 -0
  50. package/dist/storage/node/http-agents.d.ts.map +1 -1
  51. package/dist/storage/node/http-agents.js +5 -0
  52. package/dist/storage/node/http-agents.js.map +1 -1
  53. package/dist/storage/node.d.ts +6 -1
  54. package/dist/storage/node.d.ts.map +1 -1
  55. package/dist/storage/node.js +7 -4
  56. package/dist/storage/node.js.map +1 -1
  57. package/dist/storage/schema.d.ts +5 -0
  58. package/dist/storage/schema.d.ts.map +1 -1
  59. package/dist/storage/schema.js +5 -0
  60. package/dist/storage/schema.js.map +1 -1
  61. package/dist/storage/sqlite.d.ts +22 -4
  62. package/dist/storage/sqlite.d.ts.map +1 -1
  63. package/dist/storage/sqlite.js +172 -98
  64. package/dist/storage/sqlite.js.map +1 -1
  65. package/dist/transport/types.d.ts +5 -0
  66. package/dist/transport/types.d.ts.map +1 -1
  67. package/dist/transport/types.js +5 -0
  68. package/dist/transport/types.js.map +1 -1
  69. package/dist/transport/websocket.d.ts +5 -0
  70. package/dist/transport/websocket.d.ts.map +1 -1
  71. package/dist/transport/websocket.js +5 -0
  72. package/dist/transport/websocket.js.map +1 -1
  73. package/dist/types/crypto.d.ts +5 -0
  74. package/dist/types/crypto.d.ts.map +1 -1
  75. package/dist/types/crypto.js +3 -5
  76. package/dist/types/crypto.js.map +1 -1
  77. package/dist/types/identity.d.ts +5 -0
  78. package/dist/types/identity.d.ts.map +1 -1
  79. package/dist/types/identity.js +3 -2
  80. package/dist/types/identity.js.map +1 -1
  81. package/dist/types/index.d.ts +5 -0
  82. package/dist/types/index.d.ts.map +1 -1
  83. package/dist/types/index.js +5 -0
  84. package/dist/types/index.js.map +1 -1
  85. package/dist/utils/capitalize.d.ts +5 -0
  86. package/dist/utils/capitalize.d.ts.map +1 -1
  87. package/dist/utils/capitalize.js +5 -0
  88. package/dist/utils/capitalize.js.map +1 -1
  89. package/dist/utils/fipsMailExtra.d.ts +30 -0
  90. package/dist/utils/fipsMailExtra.d.ts.map +1 -0
  91. package/dist/utils/fipsMailExtra.js +114 -0
  92. package/dist/utils/fipsMailExtra.js.map +1 -0
  93. package/dist/utils/formatBytes.d.ts +5 -0
  94. package/dist/utils/formatBytes.d.ts.map +1 -1
  95. package/dist/utils/formatBytes.js +5 -0
  96. package/dist/utils/formatBytes.js.map +1 -1
  97. package/dist/utils/resolveAtRestAesKey.d.ts +13 -0
  98. package/dist/utils/resolveAtRestAesKey.d.ts.map +1 -0
  99. package/dist/utils/resolveAtRestAesKey.js +26 -0
  100. package/dist/utils/resolveAtRestAesKey.js.map +1 -0
  101. package/dist/utils/sqlSessionToCrypto.d.ts +5 -0
  102. package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -1
  103. package/dist/utils/sqlSessionToCrypto.js +5 -0
  104. package/dist/utils/sqlSessionToCrypto.js.map +1 -1
  105. package/dist/utils/uint8uuid.d.ts +5 -0
  106. package/dist/utils/uint8uuid.d.ts.map +1 -1
  107. package/dist/utils/uint8uuid.js +5 -0
  108. package/dist/utils/uint8uuid.js.map +1 -1
  109. package/package.json +14 -4
  110. package/src/Client.ts +1239 -619
  111. package/src/Storage.ts +6 -0
  112. package/src/__tests__/codec.test.ts +6 -0
  113. package/src/__tests__/harness/fixtures.ts +6 -0
  114. package/src/__tests__/harness/memory-storage.ts +72 -52
  115. package/src/__tests__/harness/platform-transports.ts +6 -0
  116. package/src/__tests__/harness/poison-node-imports.ts +6 -0
  117. package/src/__tests__/harness/shared-suite.ts +288 -124
  118. package/src/__tests__/platform-browser.test.ts +15 -1
  119. package/src/__tests__/platform-node.test.ts +17 -3
  120. package/src/codec.ts +21 -8
  121. package/src/codecs.ts +6 -0
  122. package/src/index.ts +6 -0
  123. package/src/keystore/memory.ts +6 -0
  124. package/src/keystore/node.ts +27 -13
  125. package/src/preset/common.ts +6 -0
  126. package/src/preset/node.ts +14 -1
  127. package/src/preset/test.ts +14 -1
  128. package/src/storage/node/http-agents.ts +6 -0
  129. package/src/storage/node.ts +11 -4
  130. package/src/storage/schema.ts +6 -0
  131. package/src/storage/sqlite.ts +208 -135
  132. package/src/transport/types.ts +6 -0
  133. package/src/transport/websocket.ts +6 -0
  134. package/src/types/crypto.ts +6 -0
  135. package/src/types/identity.ts +6 -0
  136. package/src/types/index.ts +6 -0
  137. package/src/utils/capitalize.ts +6 -0
  138. package/src/utils/fipsMailExtra.ts +164 -0
  139. package/src/utils/formatBytes.ts +6 -0
  140. package/src/utils/resolveAtRestAesKey.ts +39 -0
  141. package/src/utils/sqlSessionToCrypto.ts +6 -0
  142. package/src/utils/uint8uuid.ts +6 -0
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  import type { Message } from "../index.js";
2
8
  import type { Storage } from "../Storage.js";
3
9
  import type {
@@ -25,13 +31,14 @@ import type { Kysely } from "kysely";
25
31
  * ExpoStorage) with a single implementation.
26
32
  */
27
33
  import {
28
- type KeyPair,
34
+ getCryptoProfile,
29
35
  xBoxKeyPairFromSecret,
30
- XKeyConvert,
36
+ xBoxKeyPairFromSecretAsync,
31
37
  xMakeNonce,
32
38
  xSecretbox,
39
+ xSecretboxAsync,
33
40
  xSecretboxOpen,
34
- xSignKeyPairFromSecret,
41
+ xSecretboxOpenAsync,
35
42
  XUtils,
36
43
  } from "@vex-chat/crypto";
37
44
 
@@ -40,26 +47,41 @@ import { EventEmitter } from "eventemitter3";
40
47
  export class SqliteStorage extends EventEmitter implements Storage {
41
48
  public ready = false;
42
49
  private closing = false;
50
+ /** Shared across concurrent `init()` callers; `close()` awaits it before `destroy()`. */
51
+ private initInFlight: Promise<void> | null = null;
43
52
  private readonly db: Kysely<ClientDatabase>;
44
- private readonly idKeys: KeyPair;
53
+ /** 32-byte AES-256 (or nacl) key for local at-rest `secretbox` (see `XUtils.deriveLocalAtRestAesKey`). */
54
+ private readonly atRestAesKey: Uint8Array;
45
55
 
46
- constructor(db: Kysely<ClientDatabase>, SK: string) {
56
+ constructor(db: Kysely<ClientDatabase>, atRestAesKey: Uint8Array) {
47
57
  super();
48
58
  this.db = db;
49
-
50
- const idKeys = XKeyConvert.convertKeyPair(
51
- xSignKeyPairFromSecret(XUtils.decodeHex(SK)),
52
- );
53
- if (!idKeys) {
54
- throw new Error("Can't convert SK!");
59
+ if (atRestAesKey.length !== 32) {
60
+ throw new Error("SqliteStorage requires a 32-byte atRestAes key.");
55
61
  }
56
- this.idKeys = idKeys;
62
+ this.atRestAesKey = atRestAesKey;
57
63
  }
58
64
 
59
65
  // ── Lifecycle ────────────────────────────────────────────────────────────
60
66
 
67
+ /**
68
+ * Read `closing` where TypeScript would incorrectly assume it cannot
69
+ * become true after an earlier guard (e.g. across `await`).
70
+ */
71
+ private isClosingNow(): boolean {
72
+ return this.closing;
73
+ }
74
+
61
75
  async close(): Promise<void> {
62
76
  this.closing = true;
77
+ const pending = this.initInFlight;
78
+ if (pending) {
79
+ try {
80
+ await pending;
81
+ } catch {
82
+ // Schema init may have failed; still tear down the driver.
83
+ }
84
+ }
63
85
  await this.db.destroy();
64
86
  }
65
87
 
@@ -114,7 +136,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
114
136
  .orderBy("lastUsed", "desc")
115
137
  .execute();
116
138
 
117
- return rows.map((s) => this.sessionRowToSQL(s));
139
+ return await Promise.all(rows.map((s) => this.sessionRowToSQLAsync(s)));
118
140
  }
119
141
 
120
142
  async getDevice(deviceID: string): Promise<Device | null> {
@@ -143,7 +165,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
143
165
  .orderBy("timestamp", "asc")
144
166
  .execute();
145
167
 
146
- return this.decryptMessages(messages);
168
+ return this.decryptMessagesAsync(messages);
147
169
  }
148
170
 
149
171
  async getMessageHistory(userID: string): Promise<Message[]> {
@@ -171,7 +193,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
171
193
  .orderBy("timestamp", "asc")
172
194
  .execute();
173
195
 
174
- return this.decryptMessages(messages);
196
+ return this.decryptMessagesAsync(messages);
175
197
  }
176
198
 
177
199
  // ── Sessions ─────────────────────────────────────────────────────────────
@@ -192,11 +214,13 @@ export class SqliteStorage extends EventEmitter implements Storage {
192
214
  if (!otkInfo) {
193
215
  return null;
194
216
  }
217
+ const rawSk = await this.unsealHex(otkInfo.privateKey);
195
218
  return {
196
219
  index: otkInfo.index,
197
- keyPair: xBoxKeyPairFromSecret(
198
- XUtils.decodeHex(this.unsealHex(otkInfo.privateKey)),
199
- ),
220
+ keyPair:
221
+ getCryptoProfile() === "fips"
222
+ ? await xBoxKeyPairFromSecretAsync(XUtils.decodeHex(rawSk))
223
+ : xBoxKeyPairFromSecret(XUtils.decodeHex(rawSk)),
200
224
  signature: XUtils.decodeHex(otkInfo.signature),
201
225
  };
202
226
  }
@@ -213,11 +237,13 @@ export class SqliteStorage extends EventEmitter implements Storage {
213
237
  if (!preKeyInfo) {
214
238
  return null;
215
239
  }
240
+ const rawPk = await this.unsealHex(preKeyInfo.privateKey);
216
241
  return {
217
242
  index: preKeyInfo.index,
218
- keyPair: xBoxKeyPairFromSecret(
219
- XUtils.decodeHex(this.unsealHex(preKeyInfo.privateKey)),
220
- ),
243
+ keyPair:
244
+ getCryptoProfile() === "fips"
245
+ ? await xBoxKeyPairFromSecretAsync(XUtils.decodeHex(rawPk))
246
+ : xBoxKeyPairFromSecret(XUtils.decodeHex(rawPk)),
221
247
  signature: XUtils.decodeHex(preKeyInfo.signature),
222
248
  };
223
249
  }
@@ -241,7 +267,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
241
267
  return null;
242
268
  }
243
269
 
244
- return this.sqlToCrypto(this.sessionRowToSQL(sessionRow));
270
+ return this.sqlToCrypto(await this.sessionRowToSQLAsync(sessionRow));
245
271
  }
246
272
 
247
273
  async getSessionByPublicKey(
@@ -264,89 +290,97 @@ export class SqliteStorage extends EventEmitter implements Storage {
264
290
  return null;
265
291
  }
266
292
 
267
- return this.sqlToCrypto(this.sessionRowToSQL(sessionRow));
293
+ return this.sqlToCrypto(await this.sessionRowToSQLAsync(sessionRow));
268
294
  }
269
295
 
270
296
  async init(): Promise<void> {
271
- try {
272
- await this.db.schema
273
- .createTable("messages")
274
- .ifNotExists()
275
- .addColumn("nonce", "text", (col) => col.primaryKey())
276
- .addColumn("sender", "text")
277
- .addColumn("recipient", "text")
278
- .addColumn("group", "text")
279
- .addColumn("mailID", "text")
280
- .addColumn("message", "text")
281
- .addColumn("direction", "text")
282
- .addColumn("timestamp", "text")
283
- .addColumn("decrypted", "integer")
284
- .addColumn("forward", "integer")
285
- .addColumn("authorID", "text")
286
- .addColumn("readerID", "text")
287
- .execute();
288
-
289
- await this.db.schema
290
- .createTable("devices")
291
- .ifNotExists()
292
- .addColumn("deviceID", "text", (col) => col.primaryKey())
293
- .addColumn("owner", "text")
294
- .addColumn("signKey", "text")
295
- .addColumn("name", "text")
296
- .addColumn("lastLogin", "text")
297
- .addColumn("deleted", "integer")
298
- .execute();
299
-
300
- await this.db.schema
301
- .createTable("sessions")
302
- .ifNotExists()
303
- .addColumn("sessionID", "text", (col) => col.primaryKey())
304
- .addColumn("userID", "text")
305
- .addColumn("deviceID", "text")
306
- .addColumn("SK", "text", (col) => col.unique())
307
- .addColumn("publicKey", "text")
308
- .addColumn("fingerprint", "text")
309
- .addColumn("mode", "text")
310
- .addColumn("lastUsed", "text")
311
- .addColumn("verified", "integer")
312
- .execute();
313
-
314
- await this.db.schema
315
- .createTable("preKeys")
316
- .ifNotExists()
317
- .addColumn("index", "integer", (col) =>
318
- col.primaryKey().autoIncrement(),
319
- )
320
- .addColumn("keyID", "text", (col) => col.unique())
321
- .addColumn("userID", "text")
322
- .addColumn("deviceID", "text")
323
- .addColumn("privateKey", "text")
324
- .addColumn("publicKey", "text")
325
- .addColumn("signature", "text")
326
- .execute();
327
-
328
- await this.db.schema
329
- .createTable("oneTimeKeys")
330
- .ifNotExists()
331
- .addColumn("index", "integer", (col) =>
332
- col.primaryKey().autoIncrement(),
333
- )
334
- .addColumn("keyID", "text", (col) => col.unique())
335
- .addColumn("userID", "text")
336
- .addColumn("deviceID", "text")
337
- .addColumn("privateKey", "text")
338
- .addColumn("publicKey", "text")
339
- .addColumn("signature", "text")
340
- .execute();
341
-
342
- this.ready = true;
343
- this.emit("ready");
344
- } catch (err: unknown) {
345
- this.emit(
346
- "error",
347
- err instanceof Error ? err : new Error(String(err)),
348
- );
297
+ if (this.ready || this.closing) {
298
+ return;
349
299
  }
300
+ this.initInFlight ??= (async () => {
301
+ try {
302
+ await this.db.schema
303
+ .createTable("messages")
304
+ .ifNotExists()
305
+ .addColumn("nonce", "text", (col) => col.primaryKey())
306
+ .addColumn("sender", "text")
307
+ .addColumn("recipient", "text")
308
+ .addColumn("group", "text")
309
+ .addColumn("mailID", "text")
310
+ .addColumn("message", "text")
311
+ .addColumn("direction", "text")
312
+ .addColumn("timestamp", "text")
313
+ .addColumn("decrypted", "integer")
314
+ .addColumn("forward", "integer")
315
+ .addColumn("authorID", "text")
316
+ .addColumn("readerID", "text")
317
+ .execute();
318
+
319
+ await this.db.schema
320
+ .createTable("devices")
321
+ .ifNotExists()
322
+ .addColumn("deviceID", "text", (col) => col.primaryKey())
323
+ .addColumn("owner", "text")
324
+ .addColumn("signKey", "text")
325
+ .addColumn("name", "text")
326
+ .addColumn("lastLogin", "text")
327
+ .addColumn("deleted", "integer")
328
+ .execute();
329
+
330
+ await this.db.schema
331
+ .createTable("sessions")
332
+ .ifNotExists()
333
+ .addColumn("sessionID", "text", (col) => col.primaryKey())
334
+ .addColumn("userID", "text")
335
+ .addColumn("deviceID", "text")
336
+ .addColumn("SK", "text", (col) => col.unique())
337
+ .addColumn("publicKey", "text")
338
+ .addColumn("fingerprint", "text")
339
+ .addColumn("mode", "text")
340
+ .addColumn("lastUsed", "text")
341
+ .addColumn("verified", "integer")
342
+ .execute();
343
+
344
+ await this.db.schema
345
+ .createTable("preKeys")
346
+ .ifNotExists()
347
+ .addColumn("index", "integer", (col) =>
348
+ col.primaryKey().autoIncrement(),
349
+ )
350
+ .addColumn("keyID", "text", (col) => col.unique())
351
+ .addColumn("userID", "text")
352
+ .addColumn("deviceID", "text")
353
+ .addColumn("privateKey", "text")
354
+ .addColumn("publicKey", "text")
355
+ .addColumn("signature", "text")
356
+ .execute();
357
+
358
+ await this.db.schema
359
+ .createTable("oneTimeKeys")
360
+ .ifNotExists()
361
+ .addColumn("index", "integer", (col) =>
362
+ col.primaryKey().autoIncrement(),
363
+ )
364
+ .addColumn("keyID", "text", (col) => col.unique())
365
+ .addColumn("userID", "text")
366
+ .addColumn("deviceID", "text")
367
+ .addColumn("privateKey", "text")
368
+ .addColumn("publicKey", "text")
369
+ .addColumn("signature", "text")
370
+ .execute();
371
+
372
+ this.ready = true;
373
+ this.emit("ready");
374
+ } catch (err: unknown) {
375
+ this.emit(
376
+ "error",
377
+ err instanceof Error ? err : new Error(String(err)),
378
+ );
379
+ } finally {
380
+ this.initInFlight = null;
381
+ }
382
+ })();
383
+ await this.initInFlight;
350
384
  }
351
385
 
352
386
  async markSessionUsed(sessionID: string): Promise<void> {
@@ -412,18 +446,27 @@ export class SqliteStorage extends EventEmitter implements Storage {
412
446
  // ── Devices ──────────────────────────────────────────────────────────────
413
447
 
414
448
  async saveMessage(message: Message): Promise<void> {
415
- if (this.closing) {
449
+ if (this.isClosingNow()) {
416
450
  return;
417
451
  }
418
452
 
419
- // Encrypt plaintext with our idkey before saving to disk
420
- const encryptedMessage = XUtils.encodeHex(
421
- xSecretbox(
422
- XUtils.decodeUTF8(message.message),
423
- XUtils.decodeHex(message.nonce),
424
- this.idKeys.secretKey,
425
- ),
426
- );
453
+ // Encrypt plaintext with at-rest key before saving to disk
454
+ const fips = getCryptoProfile() === "fips";
455
+ const ct = fips
456
+ ? await xSecretboxAsync(
457
+ XUtils.decodeUTF8(message.message),
458
+ XUtils.decodeHex(message.nonce),
459
+ this.atRestAesKey,
460
+ )
461
+ : xSecretbox(
462
+ XUtils.decodeUTF8(message.message),
463
+ XUtils.decodeHex(message.nonce),
464
+ this.atRestAesKey,
465
+ );
466
+ if (this.isClosingNow()) {
467
+ return;
468
+ }
469
+ const encryptedMessage = XUtils.encodeHex(ct);
427
470
 
428
471
  try {
429
472
  await this.db
@@ -446,6 +489,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
446
489
  } catch (err: unknown) {
447
490
  if (this.isDuplicateError(err)) {
448
491
  // duplicate nonce — ignore
492
+ } else if (this.isClosingNow() || this.isTornDownError(err)) {
493
+ // e.g. WS/mail still saving after `close()` destroyed the driver
449
494
  } else {
450
495
  throw err;
451
496
  }
@@ -468,7 +513,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
468
513
  const row = await this.db
469
514
  .insertInto(table)
470
515
  .values({
471
- privateKey: this.sealHex(
516
+ privateKey: await this.sealHex(
472
517
  XUtils.encodeHex(preKey.keyPair.secretKey),
473
518
  ),
474
519
  publicKey: XUtils.encodeHex(preKey.keyPair.publicKey),
@@ -506,7 +551,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
506
551
  mode: session.mode,
507
552
  publicKey: session.publicKey,
508
553
  sessionID: session.sessionID,
509
- SK: this.sealHex(session.SK),
554
+ SK: await this.sealHex(session.SK),
510
555
  userID: session.userID,
511
556
  verified: session.verified ? 1 : 0,
512
557
  })
@@ -522,28 +567,33 @@ export class SqliteStorage extends EventEmitter implements Storage {
522
567
 
523
568
  // ── Private helpers ──────────────────────────────────────────────────────
524
569
 
525
- private decryptMessages(messages: MessageRow[]): Message[] {
526
- return messages.map((msg): Message => {
570
+ private async decryptMessagesAsync(
571
+ messages: MessageRow[],
572
+ ): Promise<Message[]> {
573
+ const fips = getCryptoProfile() === "fips";
574
+ const out: Message[] = [];
575
+ for (const msg of messages) {
527
576
  const decryptedFlag = msg.decrypted !== 0;
528
577
  let plaintext = msg.message;
529
-
530
578
  if (decryptedFlag) {
531
- const decrypted = xSecretboxOpen(
532
- XUtils.decodeHex(msg.message),
533
- XUtils.decodeHex(msg.nonce),
534
- this.idKeys.secretKey,
535
- );
579
+ const cipher = XUtils.decodeHex(msg.message);
580
+ const nonce = XUtils.decodeHex(msg.nonce);
581
+ const decrypted = fips
582
+ ? await xSecretboxOpenAsync(
583
+ cipher,
584
+ nonce,
585
+ this.atRestAesKey,
586
+ )
587
+ : xSecretboxOpen(cipher, nonce, this.atRestAesKey);
536
588
  if (decrypted) {
537
589
  plaintext = XUtils.encodeUTF8(decrypted);
538
590
  } else {
539
591
  throw new Error("Couldn't decrypt messages on disk!");
540
592
  }
541
593
  }
542
-
543
594
  const direction =
544
595
  msg.direction === "incoming" ? "incoming" : "outgoing";
545
-
546
- return {
596
+ out.push({
547
597
  authorID: msg.authorID,
548
598
  decrypted: decryptedFlag,
549
599
  direction,
@@ -556,8 +606,9 @@ export class SqliteStorage extends EventEmitter implements Storage {
556
606
  recipient: msg.recipient,
557
607
  sender: msg.sender,
558
608
  timestamp: msg.timestamp,
559
- };
560
- });
609
+ });
610
+ }
611
+ return out;
561
612
  }
562
613
 
563
614
  private deviceRowToDevice(row: DeviceRow): Device {
@@ -581,24 +632,43 @@ export class SqliteStorage extends EventEmitter implements Storage {
581
632
  return false;
582
633
  }
583
634
 
635
+ /**
636
+ * After `close` runs, Kysely / better-sqlite3 can reject with this if a
637
+ * message handler is still in flight.
638
+ */
639
+ private isTornDownError(err: unknown): boolean {
640
+ if (err instanceof Error) {
641
+ const m = err.message.toLowerCase();
642
+ return (
643
+ m.includes("driver has already been destroyed") ||
644
+ m.includes("connection is not open") ||
645
+ m.includes("database is closed")
646
+ );
647
+ }
648
+ return false;
649
+ }
650
+
584
651
  /**
585
652
  * Encrypt a hex-encoded secret for at-rest storage.
586
653
  * Returns hex(nonce || ciphertext) where nonce is 24 random bytes.
587
654
  */
588
- private sealHex(plainHex: string): string {
655
+ private async sealHex(plainHex: string): Promise<string> {
589
656
  const nonce = xMakeNonce();
590
- const ct = xSecretbox(
591
- XUtils.decodeHex(plainHex),
592
- nonce,
593
- this.idKeys.secretKey,
594
- );
657
+ const fips = getCryptoProfile() === "fips";
658
+ const ct = fips
659
+ ? await xSecretboxAsync(
660
+ XUtils.decodeHex(plainHex),
661
+ nonce,
662
+ this.atRestAesKey,
663
+ )
664
+ : xSecretbox(XUtils.decodeHex(plainHex), nonce, this.atRestAesKey);
595
665
  const sealed = new Uint8Array(nonce.length + ct.length);
596
666
  sealed.set(nonce);
597
667
  sealed.set(ct, nonce.length);
598
668
  return XUtils.encodeHex(sealed);
599
669
  }
600
670
 
601
- private sessionRowToSQL(row: SessionRow): SessionSQL {
671
+ private async sessionRowToSQLAsync(row: SessionRow): Promise<SessionSQL> {
602
672
  return {
603
673
  deviceID: row.deviceID,
604
674
  fingerprint: row.fingerprint,
@@ -606,7 +676,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
606
676
  mode: row.mode === "initiator" ? "initiator" : "receiver",
607
677
  publicKey: row.publicKey,
608
678
  sessionID: row.sessionID,
609
- SK: this.unsealHex(row.SK),
679
+ SK: await this.unsealHex(row.SK),
610
680
  userID: row.userID,
611
681
  verified: row.verified !== 0,
612
682
  };
@@ -628,11 +698,14 @@ export class SqliteStorage extends EventEmitter implements Storage {
628
698
  * Decrypt a value produced by sealHex().
629
699
  * Expects hex(nonce || ciphertext), returns the original hex string.
630
700
  */
631
- private unsealHex(sealed: string): string {
701
+ private async unsealHex(sealed: string): Promise<string> {
632
702
  const bytes = XUtils.decodeHex(sealed);
633
703
  const nonce = bytes.slice(0, 24);
634
704
  const ct = bytes.slice(24);
635
- const plain = xSecretboxOpen(ct, nonce, this.idKeys.secretKey);
705
+ const fips = getCryptoProfile() === "fips";
706
+ const plain = fips
707
+ ? await xSecretboxOpenAsync(ct, nonce, this.atRestAesKey)
708
+ : xSecretboxOpen(ct, nonce, this.atRestAesKey);
636
709
  if (!plain) {
637
710
  throw new Error("Failed to decrypt sealed column value.");
638
711
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  export type WebSocketEvent = keyof WebSocketEventMap;
2
8
 
3
9
  export interface WebSocketEventMap {
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * Adapts the standard WebSocket API (addEventListener/MessageEvent) to the
3
9
  * EventEmitter-style .on()/.off() interface used internally by Client.
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * SDK-internal crypto types. These were moved from `@vex-chat/types`
3
9
  * because they are only used by the SDK, never by the server.
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * SDK credential storage types. Moved from `@vex-chat/types`
3
9
  * because only the SDK and app consumers use them.
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  export type {
2
8
  KeyPair,
3
9
  PreKeysCrypto,
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2020-2026 Vex Heavy Industries LLC
3
+ * Licensed under AGPL-3.0. See LICENSE for details.
4
+ * Commercial licenses available at vex.wtf
5
+ */
6
+
1
7
  /**
2
8
  * @hidden
3
9
  */