@vex-chat/libvex 5.5.0 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -23
- package/dist/Client.d.ts +114 -103
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +317 -314
- package/dist/Client.js.map +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
- package/dist/__tests__/harness/memory-storage.js +1 -1
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/storage/node/http-agents.d.ts +1 -1
- package/dist/storage/node/http-agents.d.ts.map +1 -1
- package/dist/storage/node/http-agents.js +4 -4
- package/dist/storage/node/http-agents.js.map +1 -1
- package/dist/storage/sqlite.d.ts +8 -8
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +16 -16
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/utils/fipsMailExtra.d.ts +9 -9
- package/dist/utils/fipsMailExtra.d.ts.map +1 -1
- package/dist/utils/fipsMailExtra.js +47 -47
- package/dist/utils/fipsMailExtra.js.map +1 -1
- package/dist/utils/resolveAtRestAesKey.js +1 -1
- package/dist/utils/resolveAtRestAesKey.js.map +1 -1
- package/package.json +134 -152
- package/src/Client.ts +448 -437
- package/src/__tests__/harness/memory-storage.ts +1 -1
- package/src/__tests__/harness/shared-suite.ts +177 -177
- package/src/index.ts +1 -1
- package/src/storage/node/http-agents.ts +7 -7
- package/src/storage/sqlite.ts +23 -23
- package/src/utils/fipsMailExtra.ts +80 -80
- package/src/utils/resolveAtRestAesKey.ts +1 -1
|
@@ -34,8 +34,8 @@ import { EventEmitter } from "eventemitter3";
|
|
|
34
34
|
|
|
35
35
|
export class MemoryStorage extends EventEmitter implements Storage {
|
|
36
36
|
public ready = false;
|
|
37
|
-
private readonly devices: Device[] = [];
|
|
38
37
|
private readonly atRestAesKey: Uint8Array;
|
|
38
|
+
private readonly devices: Device[] = [];
|
|
39
39
|
private messages: Message[] = [];
|
|
40
40
|
private nextOtkIndex = 1;
|
|
41
41
|
private nextPreKeyIndex = 1;
|
|
@@ -38,141 +38,6 @@ import { Client } from "../../index.js";
|
|
|
38
38
|
|
|
39
39
|
import { testFile, testImage } from "./fixtures.js";
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
* `GET` `{API_URL or http://}{host}/status` — used for crypto profile preflight
|
|
43
|
-
* (must match `getCryptoProfile()` when running e2e against a custom Spire).
|
|
44
|
-
*/
|
|
45
|
-
function spireStatusUrlFromEnv(): null | string {
|
|
46
|
-
const raw = process.env["API_URL"]?.trim();
|
|
47
|
-
if (raw === undefined || raw.length === 0) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
if (/^https?:\/\//i.test(raw)) {
|
|
51
|
-
const u = new URL(raw);
|
|
52
|
-
return `${u.protocol}//${u.host}/status`;
|
|
53
|
-
}
|
|
54
|
-
return `http://${raw}/status`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** `LIBVEX_E2E_CRYPTO` only — used when status auto-detect is skipped. */
|
|
58
|
-
function e2eCryptoProfileFromEnvOnly(): "fips" | "tweetnacl" {
|
|
59
|
-
const v = process.env["LIBVEX_E2E_CRYPTO"]?.trim().toLowerCase();
|
|
60
|
-
if (v === "fips" || v === "p-256" || v === "p256") {
|
|
61
|
-
return "fips";
|
|
62
|
-
}
|
|
63
|
-
if (v === "tweetnacl" || v === "nacl" || v === "ed25519") {
|
|
64
|
-
return "tweetnacl";
|
|
65
|
-
}
|
|
66
|
-
return "tweetnacl";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Picks the signing profile for the suite: optional env override, else `GET` Spire
|
|
71
|
-
* `/status` when `API_URL` is set, else tweetnacl.
|
|
72
|
-
*/
|
|
73
|
-
async function resolveE2eCryptoProfile(): Promise<"fips" | "tweetnacl"> {
|
|
74
|
-
if (process.env["LIBVEX_E2E_SKIP_STATUS_CHECK"] === "1") {
|
|
75
|
-
return e2eCryptoProfileFromEnvOnly();
|
|
76
|
-
}
|
|
77
|
-
const v = process.env["LIBVEX_E2E_CRYPTO"]?.trim().toLowerCase();
|
|
78
|
-
if (v === "fips" || v === "p-256" || v === "p256") {
|
|
79
|
-
return "fips";
|
|
80
|
-
}
|
|
81
|
-
if (v === "tweetnacl" || v === "nacl" || v === "ed25519") {
|
|
82
|
-
return "tweetnacl";
|
|
83
|
-
}
|
|
84
|
-
if (v !== undefined && v.length > 0) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`libvex e2e: invalid LIBVEX_E2E_CRYPTO=${JSON.stringify(v)}. Use fips, tweetnacl, or leave unset to auto-detect from Spire /status`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
const url = spireStatusUrlFromEnv();
|
|
90
|
-
if (url === null) {
|
|
91
|
-
return "tweetnacl";
|
|
92
|
-
}
|
|
93
|
-
let res: Response;
|
|
94
|
-
try {
|
|
95
|
-
res = await fetch(url, { method: "GET" });
|
|
96
|
-
} catch {
|
|
97
|
-
return "tweetnacl";
|
|
98
|
-
}
|
|
99
|
-
if (!res.ok) {
|
|
100
|
-
return "tweetnacl";
|
|
101
|
-
}
|
|
102
|
-
const data: unknown = await res.json();
|
|
103
|
-
if (
|
|
104
|
-
typeof data !== "object" ||
|
|
105
|
-
data === null ||
|
|
106
|
-
!("cryptoProfile" in data)
|
|
107
|
-
) {
|
|
108
|
-
return "tweetnacl";
|
|
109
|
-
}
|
|
110
|
-
const cp = (data as { cryptoProfile: unknown }).cryptoProfile;
|
|
111
|
-
if (cp === "fips" || cp === "tweetnacl") {
|
|
112
|
-
return cp;
|
|
113
|
-
}
|
|
114
|
-
return "tweetnacl";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function e2eClientOptionsBase(): ClientOptions {
|
|
118
|
-
return {
|
|
119
|
-
inMemoryDb: true,
|
|
120
|
-
...apiUrlOverrideFromEnv(),
|
|
121
|
-
cryptoProfile: getCryptoProfile(),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function e2eGenerateSecretKey(): Promise<string> {
|
|
126
|
-
return await Client.generateSecretKeyAsync();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function assertSpireCryptoProfileMatchesTest(): Promise<void> {
|
|
130
|
-
if (process.env["LIBVEX_E2E_SKIP_STATUS_CHECK"] === "1") {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const url = spireStatusUrlFromEnv();
|
|
134
|
-
if (url === null) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const want = getCryptoProfile();
|
|
138
|
-
let res: Response;
|
|
139
|
-
try {
|
|
140
|
-
res = await fetch(url, { method: "GET" });
|
|
141
|
-
} catch (e: unknown) {
|
|
142
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
143
|
-
throw new Error(
|
|
144
|
-
`libvex e2e: could not GET ${url} (check API_URL; is Spire running?): ${msg}`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
if (!res.ok) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`libvex e2e: ${url} returned HTTP ${String(res.status)} — Spire not reachable on this base URL?`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
const data: unknown = await res.json();
|
|
153
|
-
if (
|
|
154
|
-
typeof data !== "object" ||
|
|
155
|
-
data === null ||
|
|
156
|
-
!("cryptoProfile" in data) ||
|
|
157
|
-
typeof (data as { cryptoProfile: unknown }).cryptoProfile !== "string"
|
|
158
|
-
) {
|
|
159
|
-
throw new Error(
|
|
160
|
-
`libvex e2e: Spire /status is missing a string "cryptoProfile" (upgrade Spire) or set LIBVEX_E2E_SKIP_STATUS_CHECK=1 to skip this check`,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
const gotStr = (data as { cryptoProfile: string }).cryptoProfile;
|
|
164
|
-
if (gotStr !== "fips" && gotStr !== "tweetnacl") {
|
|
165
|
-
throw new Error(
|
|
166
|
-
`libvex e2e: Spire /status cryptoProfile is not fips|tweetnacl: ${gotStr}`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
if (gotStr !== want) {
|
|
170
|
-
throw new Error(
|
|
171
|
-
`libvex e2e: Spire is cryptoProfile=${gotStr} (see SPIRE_FIPS + SPK) but this test has getCryptoProfile()=${want}. Use matching keys/scripts (gen-spk.js vs gen-spk-fips.js) and the same mode on client and server.`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
41
|
export function platformSuite(
|
|
177
42
|
platformName: string,
|
|
178
43
|
makeStorage: (SK: string, opts: ClientOptions) => Promise<Storage>,
|
|
@@ -469,7 +334,7 @@ export function platformSuite(
|
|
|
469
334
|
}
|
|
470
335
|
|
|
471
336
|
function apiUrlOverrideFromEnv():
|
|
472
|
-
| Pick<ClientOptions, "
|
|
337
|
+
| Pick<ClientOptions, "devApiKey" | "host" | "unsafeHttp">
|
|
473
338
|
| undefined {
|
|
474
339
|
const raw = process.env["API_URL"]?.trim();
|
|
475
340
|
const devKey = process.env["DEV_API_KEY"]?.trim();
|
|
@@ -496,26 +361,161 @@ function apiUrlOverrideFromEnv():
|
|
|
496
361
|
};
|
|
497
362
|
}
|
|
498
363
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
let last: unknown;
|
|
503
|
-
for (let i = 0; i < attempts; i++) {
|
|
504
|
-
try {
|
|
505
|
-
return await fn();
|
|
506
|
-
} catch (e) {
|
|
507
|
-
last = e;
|
|
508
|
-
const transient =
|
|
509
|
-
isAxiosError(e) &&
|
|
510
|
-
(e.response?.status === 502 || e.response?.status === 503);
|
|
511
|
-
if (transient && i < attempts - 1) {
|
|
512
|
-
await new Promise((r) => setTimeout(r, 400 * (i + 1)));
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
throw e;
|
|
516
|
-
}
|
|
364
|
+
async function assertSpireCryptoProfileMatchesTest(): Promise<void> {
|
|
365
|
+
if (process.env["LIBVEX_E2E_SKIP_STATUS_CHECK"] === "1") {
|
|
366
|
+
return;
|
|
517
367
|
}
|
|
518
|
-
|
|
368
|
+
const url = spireStatusUrlFromEnv();
|
|
369
|
+
if (url === null) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const want = getCryptoProfile();
|
|
373
|
+
let res: Response;
|
|
374
|
+
try {
|
|
375
|
+
res = await fetch(url, { method: "GET" });
|
|
376
|
+
} catch (e: unknown) {
|
|
377
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
378
|
+
throw new Error(
|
|
379
|
+
`libvex e2e: could not GET ${url} (check API_URL; is Spire running?): ${msg}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
if (!res.ok) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`libvex e2e: ${url} returned HTTP ${String(res.status)} — Spire not reachable on this base URL?`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const data: unknown = await res.json();
|
|
388
|
+
if (
|
|
389
|
+
typeof data !== "object" ||
|
|
390
|
+
data === null ||
|
|
391
|
+
!("cryptoProfile" in data) ||
|
|
392
|
+
typeof (data as { cryptoProfile: unknown }).cryptoProfile !== "string"
|
|
393
|
+
) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`libvex e2e: Spire /status is missing a string "cryptoProfile" (upgrade Spire) or set LIBVEX_E2E_SKIP_STATUS_CHECK=1 to skip this check`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
const gotStr = (data as { cryptoProfile: string }).cryptoProfile;
|
|
399
|
+
if (gotStr !== "fips" && gotStr !== "tweetnacl") {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`libvex e2e: Spire /status cryptoProfile is not fips|tweetnacl: ${gotStr}`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (gotStr !== want) {
|
|
405
|
+
throw new Error(
|
|
406
|
+
`libvex e2e: Spire is cryptoProfile=${gotStr} (see SPIRE_FIPS + SPK) but this test has getCryptoProfile()=${want}. Use matching keys/scripts (gen-spk.js vs gen-spk-fips.js) and the same mode on client and server.`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function connectAndWait(
|
|
412
|
+
c: Client,
|
|
413
|
+
label: string,
|
|
414
|
+
timeout = 10_000,
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const timer = setTimeout(() => {
|
|
418
|
+
reject(new Error(`${label} connect timed out`));
|
|
419
|
+
}, timeout);
|
|
420
|
+
const onConnected = () => {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
c.off("connected", onConnected);
|
|
423
|
+
resolve();
|
|
424
|
+
};
|
|
425
|
+
c.on("connected", onConnected);
|
|
426
|
+
c.connect().catch((err: unknown) => {
|
|
427
|
+
clearTimeout(timer);
|
|
428
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function e2eClientOptionsBase(): ClientOptions {
|
|
434
|
+
return {
|
|
435
|
+
inMemoryDb: true,
|
|
436
|
+
...apiUrlOverrideFromEnv(),
|
|
437
|
+
cryptoProfile: getCryptoProfile(),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** `LIBVEX_E2E_CRYPTO` only — used when status auto-detect is skipped. */
|
|
442
|
+
function e2eCryptoProfileFromEnvOnly(): "fips" | "tweetnacl" {
|
|
443
|
+
const v = process.env["LIBVEX_E2E_CRYPTO"]?.trim().toLowerCase();
|
|
444
|
+
if (v === "fips" || v === "p-256" || v === "p256") {
|
|
445
|
+
return "fips";
|
|
446
|
+
}
|
|
447
|
+
if (v === "tweetnacl" || v === "nacl" || v === "ed25519") {
|
|
448
|
+
return "tweetnacl";
|
|
449
|
+
}
|
|
450
|
+
return "tweetnacl";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function e2eGenerateSecretKey(): Promise<string> {
|
|
454
|
+
return await Client.generateSecretKeyAsync();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Picks the signing profile for the suite: optional env override, else `GET` Spire
|
|
459
|
+
* `/status` when `API_URL` is set, else tweetnacl.
|
|
460
|
+
*/
|
|
461
|
+
async function resolveE2eCryptoProfile(): Promise<"fips" | "tweetnacl"> {
|
|
462
|
+
if (process.env["LIBVEX_E2E_SKIP_STATUS_CHECK"] === "1") {
|
|
463
|
+
return e2eCryptoProfileFromEnvOnly();
|
|
464
|
+
}
|
|
465
|
+
const v = process.env["LIBVEX_E2E_CRYPTO"]?.trim().toLowerCase();
|
|
466
|
+
if (v === "fips" || v === "p-256" || v === "p256") {
|
|
467
|
+
return "fips";
|
|
468
|
+
}
|
|
469
|
+
if (v === "tweetnacl" || v === "nacl" || v === "ed25519") {
|
|
470
|
+
return "tweetnacl";
|
|
471
|
+
}
|
|
472
|
+
if (v !== undefined && v.length > 0) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`libvex e2e: invalid LIBVEX_E2E_CRYPTO=${JSON.stringify(v)}. Use fips, tweetnacl, or leave unset to auto-detect from Spire /status`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const url = spireStatusUrlFromEnv();
|
|
478
|
+
if (url === null) {
|
|
479
|
+
return "tweetnacl";
|
|
480
|
+
}
|
|
481
|
+
let res: Response;
|
|
482
|
+
try {
|
|
483
|
+
res = await fetch(url, { method: "GET" });
|
|
484
|
+
} catch {
|
|
485
|
+
return "tweetnacl";
|
|
486
|
+
}
|
|
487
|
+
if (!res.ok) {
|
|
488
|
+
return "tweetnacl";
|
|
489
|
+
}
|
|
490
|
+
const data: unknown = await res.json();
|
|
491
|
+
if (
|
|
492
|
+
typeof data !== "object" ||
|
|
493
|
+
data === null ||
|
|
494
|
+
!("cryptoProfile" in data)
|
|
495
|
+
) {
|
|
496
|
+
return "tweetnacl";
|
|
497
|
+
}
|
|
498
|
+
const cp = (data as { cryptoProfile: unknown }).cryptoProfile;
|
|
499
|
+
if (cp === "fips" || cp === "tweetnacl") {
|
|
500
|
+
return cp;
|
|
501
|
+
}
|
|
502
|
+
return "tweetnacl";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* `GET` `{API_URL or http://}{host}/status` — used for crypto profile preflight
|
|
507
|
+
* (must match `getCryptoProfile()` when running e2e against a custom Spire).
|
|
508
|
+
*/
|
|
509
|
+
function spireStatusUrlFromEnv(): null | string {
|
|
510
|
+
const raw = process.env["API_URL"]?.trim();
|
|
511
|
+
if (raw === undefined || raw.length === 0) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
515
|
+
const u = new URL(raw);
|
|
516
|
+
return `${u.protocol}//${u.host}/status`;
|
|
517
|
+
}
|
|
518
|
+
return `http://${raw}/status`;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
/*
|
|
@@ -547,28 +547,6 @@ async function e2eWaitForPeerDeviceCount(
|
|
|
547
547
|
}
|
|
548
548
|
*/
|
|
549
549
|
|
|
550
|
-
function connectAndWait(
|
|
551
|
-
c: Client,
|
|
552
|
-
label: string,
|
|
553
|
-
timeout = 10_000,
|
|
554
|
-
): Promise<void> {
|
|
555
|
-
return new Promise((resolve, reject) => {
|
|
556
|
-
const timer = setTimeout(() => {
|
|
557
|
-
reject(new Error(`${label} connect timed out`));
|
|
558
|
-
}, timeout);
|
|
559
|
-
const onConnected = () => {
|
|
560
|
-
clearTimeout(timer);
|
|
561
|
-
c.off("connected", onConnected);
|
|
562
|
-
resolve();
|
|
563
|
-
};
|
|
564
|
-
c.on("connected", onConnected);
|
|
565
|
-
c.connect().catch((err: unknown) => {
|
|
566
|
-
clearTimeout(timer);
|
|
567
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
|
|
572
550
|
async function waitForMessage(
|
|
573
551
|
c: Client,
|
|
574
552
|
predicate: (m: Message) => boolean,
|
|
@@ -589,3 +567,25 @@ async function waitForMessage(
|
|
|
589
567
|
c.on("message", onMsg);
|
|
590
568
|
});
|
|
591
569
|
}
|
|
570
|
+
|
|
571
|
+
/** Shared staging / CI proxies sometimes return 502; retry a few times. */
|
|
572
|
+
async function withTransientRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
573
|
+
const attempts = 4;
|
|
574
|
+
let last: unknown;
|
|
575
|
+
for (let i = 0; i < attempts; i++) {
|
|
576
|
+
try {
|
|
577
|
+
return await fn();
|
|
578
|
+
} catch (e) {
|
|
579
|
+
last = e;
|
|
580
|
+
const transient =
|
|
581
|
+
isAxiosError(e) &&
|
|
582
|
+
(e.response?.status === 502 || e.response?.status === 503);
|
|
583
|
+
if (transient && i < attempts - 1) {
|
|
584
|
+
await new Promise((r) => setTimeout(r, 400 * (i + 1)));
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
throw e;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
throw last;
|
|
591
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,13 +18,6 @@ export interface NodeHttpAgentPair {
|
|
|
18
18
|
readonly https: nodeHttps.Agent;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export function createNodeHttpAgents(): NodeHttpAgentPair {
|
|
22
|
-
return {
|
|
23
|
-
http: new nodeHttp.Agent({ keepAlive: true }),
|
|
24
|
-
https: new nodeHttps.Agent({ keepAlive: true }),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
export function attachNodeAgentsToAxios(
|
|
29
22
|
instance: AxiosInstance,
|
|
30
23
|
agents: NodeHttpAgentPair,
|
|
@@ -33,6 +26,13 @@ export function attachNodeAgentsToAxios(
|
|
|
33
26
|
instance.defaults.httpsAgent = agents.https;
|
|
34
27
|
}
|
|
35
28
|
|
|
29
|
+
export function createNodeHttpAgents(): NodeHttpAgentPair {
|
|
30
|
+
return {
|
|
31
|
+
http: new nodeHttp.Agent({ keepAlive: true }),
|
|
32
|
+
https: new nodeHttps.Agent({ keepAlive: true }),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
36
|
export function destroyNodeHttpAgents(agents: NodeHttpAgentPair): void {
|
|
37
37
|
agents.http.destroy();
|
|
38
38
|
agents.https.destroy();
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -46,12 +46,12 @@ import { EventEmitter } from "eventemitter3";
|
|
|
46
46
|
|
|
47
47
|
export class SqliteStorage extends EventEmitter implements Storage {
|
|
48
48
|
public ready = false;
|
|
49
|
-
private closing = false;
|
|
50
|
-
/** Shared across concurrent `init()` callers; `close()` awaits it before `destroy()`. */
|
|
51
|
-
private initInFlight: Promise<void> | null = null;
|
|
52
|
-
private readonly db: Kysely<ClientDatabase>;
|
|
53
49
|
/** 32-byte AES-256 (or nacl) key for local at-rest `secretbox` (see `XUtils.deriveLocalAtRestAesKey`). */
|
|
54
50
|
private readonly atRestAesKey: Uint8Array;
|
|
51
|
+
private closing = false;
|
|
52
|
+
private readonly db: Kysely<ClientDatabase>;
|
|
53
|
+
/** Shared across concurrent `init()` callers; `close()` awaits it before `destroy()`. */
|
|
54
|
+
private initInFlight: null | Promise<void> = null;
|
|
55
55
|
|
|
56
56
|
constructor(db: Kysely<ClientDatabase>, atRestAesKey: Uint8Array) {
|
|
57
57
|
super();
|
|
@@ -64,14 +64,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
64
64
|
|
|
65
65
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
66
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
|
-
|
|
75
67
|
async close(): Promise<void> {
|
|
76
68
|
this.closing = true;
|
|
77
69
|
const pending = this.initInFlight;
|
|
@@ -104,8 +96,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
104
96
|
.execute();
|
|
105
97
|
}
|
|
106
98
|
|
|
107
|
-
// ── Messages ─────────────────────────────────────────────────────────────
|
|
108
|
-
|
|
109
99
|
async deleteMessage(mailID: string): Promise<void> {
|
|
110
100
|
if (this.closing) {
|
|
111
101
|
return;
|
|
@@ -116,6 +106,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
116
106
|
.execute();
|
|
117
107
|
}
|
|
118
108
|
|
|
109
|
+
// ── Messages ─────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
119
111
|
async deleteOneTimeKey(index: number): Promise<void> {
|
|
120
112
|
if (this.closing) {
|
|
121
113
|
return;
|
|
@@ -196,8 +188,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
196
188
|
return this.decryptMessagesAsync(messages);
|
|
197
189
|
}
|
|
198
190
|
|
|
199
|
-
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
200
|
-
|
|
201
191
|
async getOneTimeKey(index: number): Promise<null | PreKeysCrypto> {
|
|
202
192
|
await this.untilReady();
|
|
203
193
|
if (this.closing) {
|
|
@@ -225,6 +215,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
225
215
|
};
|
|
226
216
|
}
|
|
227
217
|
|
|
218
|
+
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
228
220
|
async getPreKeys(): Promise<null | PreKeysCrypto> {
|
|
229
221
|
await this.untilReady();
|
|
230
222
|
if (this.closing) {
|
|
@@ -394,8 +386,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
394
386
|
.execute();
|
|
395
387
|
}
|
|
396
388
|
|
|
397
|
-
// ── PreKeys / OneTimeKeys ────────────────────────────────────────────────
|
|
398
|
-
|
|
399
389
|
async markSessionVerified(sessionID: string): Promise<void> {
|
|
400
390
|
if (this.closing) {
|
|
401
391
|
return;
|
|
@@ -407,6 +397,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
407
397
|
.execute();
|
|
408
398
|
}
|
|
409
399
|
|
|
400
|
+
// ── PreKeys / OneTimeKeys ────────────────────────────────────────────────
|
|
401
|
+
|
|
410
402
|
async purgeHistory(): Promise<void> {
|
|
411
403
|
await this.db.deleteFrom("messages").execute();
|
|
412
404
|
}
|
|
@@ -443,8 +435,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
443
435
|
}
|
|
444
436
|
}
|
|
445
437
|
|
|
446
|
-
// ── Devices ──────────────────────────────────────────────────────────────
|
|
447
|
-
|
|
448
438
|
async saveMessage(message: Message): Promise<void> {
|
|
449
439
|
if (this.isClosingNow()) {
|
|
450
440
|
return;
|
|
@@ -497,6 +487,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
497
487
|
}
|
|
498
488
|
}
|
|
499
489
|
|
|
490
|
+
// ── Devices ──────────────────────────────────────────────────────────────
|
|
491
|
+
|
|
500
492
|
async savePreKeys(
|
|
501
493
|
preKeys: UnsavedPreKey[],
|
|
502
494
|
oneTime: boolean,
|
|
@@ -535,8 +527,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
535
527
|
return saved;
|
|
536
528
|
}
|
|
537
529
|
|
|
538
|
-
// ── Purge ────────────────────────────────────────────────────────────────
|
|
539
|
-
|
|
540
530
|
async saveSession(session: SessionSQL): Promise<void> {
|
|
541
531
|
if (this.closing) {
|
|
542
532
|
return;
|
|
@@ -565,7 +555,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
565
555
|
}
|
|
566
556
|
}
|
|
567
557
|
|
|
568
|
-
// ──
|
|
558
|
+
// ── Purge ────────────────────────────────────────────────────────────────
|
|
569
559
|
|
|
570
560
|
private async decryptMessagesAsync(
|
|
571
561
|
messages: MessageRow[],
|
|
@@ -611,6 +601,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
611
601
|
return out;
|
|
612
602
|
}
|
|
613
603
|
|
|
604
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
605
|
+
|
|
614
606
|
private deviceRowToDevice(row: DeviceRow): Device {
|
|
615
607
|
return {
|
|
616
608
|
deleted: row.deleted !== 0,
|
|
@@ -622,6 +614,14 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
622
614
|
};
|
|
623
615
|
}
|
|
624
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Read `closing` where TypeScript would incorrectly assume it cannot
|
|
619
|
+
* become true after an earlier guard (e.g. across `await`).
|
|
620
|
+
*/
|
|
621
|
+
private isClosingNow(): boolean {
|
|
622
|
+
return this.closing;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
625
|
private isDuplicateError(err: unknown): boolean {
|
|
626
626
|
if (err instanceof Error) {
|
|
627
627
|
return err.message.includes("UNIQUE");
|