@vex-chat/libvex 6.8.0 → 7.0.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.
- package/dist/Client.d.ts +24 -8
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +220 -124
- package/dist/Client.js.map +1 -1
- package/dist/Storage.d.ts +6 -0
- package/dist/Storage.d.ts.map +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts +2 -1
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
- package/dist/__tests__/harness/memory-storage.js +27 -0
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/messageExtra.d.ts +18 -0
- package/dist/messageExtra.d.ts.map +1 -1
- package/dist/messageExtra.js +104 -0
- package/dist/messageExtra.js.map +1 -1
- package/dist/storage/sqlite.d.ts +2 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +47 -1
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +4 -4
- package/src/Client.ts +336 -169
- package/src/Storage.ts +11 -0
- package/src/__tests__/harness/memory-storage.ts +42 -1
- package/src/__tests__/messageExtra.test.ts +38 -0
- package/src/index.ts +6 -1
- package/src/messageExtra.ts +145 -0
- package/src/storage/sqlite.ts +65 -3
package/dist/Client.js
CHANGED
|
@@ -269,6 +269,19 @@ const messageSchema = z.object({
|
|
|
269
269
|
timestamp: z.string(),
|
|
270
270
|
});
|
|
271
271
|
const MESSAGE_BLOB_PREFIX = "vex-message:1\n";
|
|
272
|
+
const MAIL_FANOUT_CONCURRENCY = 8;
|
|
273
|
+
const MAIL_BATCH_MAX_SIZE = 32;
|
|
274
|
+
const MAIL_BATCH_FLUSH_DELAY_MS = 8;
|
|
275
|
+
const mailBatchResponseSchema = z.object({
|
|
276
|
+
results: z.array(z.object({
|
|
277
|
+
error: z.string().optional(),
|
|
278
|
+
index: z.number().int().nonnegative(),
|
|
279
|
+
mailID: z.string().optional(),
|
|
280
|
+
ok: z.boolean(),
|
|
281
|
+
recipient: z.string().optional(),
|
|
282
|
+
status: z.number().int().optional(),
|
|
283
|
+
})),
|
|
284
|
+
});
|
|
272
285
|
function decodeMessageBlob(body) {
|
|
273
286
|
if (!body.startsWith(MESSAGE_BLOB_PREFIX)) {
|
|
274
287
|
return { message: body };
|
|
@@ -535,16 +548,15 @@ export class Client {
|
|
|
535
548
|
* Passkey ("recovery credential") methods.
|
|
536
549
|
*
|
|
537
550
|
* Passkeys are an account-bound second-class credential that can
|
|
538
|
-
* authenticate the owning user, list devices, delete devices,
|
|
539
|
-
*
|
|
540
|
-
*
|
|
551
|
+
* authenticate the owning user, list devices, delete devices, recover a
|
|
552
|
+
* pending device enrollment, and reject pending device-enrollment
|
|
553
|
+
* requests. They cannot send/decrypt mail.
|
|
541
554
|
*
|
|
542
555
|
* The host app drives the WebAuthn ceremony (e.g. via
|
|
543
556
|
* `@simplewebauthn/browser`) and hands the JSON response to
|
|
544
557
|
* `finish*`.
|
|
545
558
|
*/
|
|
546
559
|
passkeys = {
|
|
547
|
-
approveDeviceRequest: this.passkeyApproveDeviceRequest.bind(this),
|
|
548
560
|
beginAuthentication: this.beginPasskeyAuthentication.bind(this),
|
|
549
561
|
beginRegistration: this.beginPasskeyRegistration.bind(this),
|
|
550
562
|
delete: this.deletePasskey.bind(this),
|
|
@@ -553,6 +565,7 @@ export class Client {
|
|
|
553
565
|
finishRegistration: this.finishPasskeyRegistration.bind(this),
|
|
554
566
|
list: this.listPasskeys.bind(this),
|
|
555
567
|
listDevices: this.passkeyListDevices.bind(this),
|
|
568
|
+
recoverDeviceRequest: this.passkeyRecoverDeviceRequest.bind(this),
|
|
556
569
|
rejectDeviceRequest: this.passkeyRejectDeviceRequest.bind(this),
|
|
557
570
|
};
|
|
558
571
|
/**
|
|
@@ -668,6 +681,9 @@ export class Client {
|
|
|
668
681
|
isAlive = true;
|
|
669
682
|
localMessageRetentionDays;
|
|
670
683
|
localRetentionPurgeTimer = null;
|
|
684
|
+
mailBatchFlushTimer = null;
|
|
685
|
+
mailBatchQueue = [];
|
|
686
|
+
mailBatchUnsupported = false;
|
|
671
687
|
mailInterval;
|
|
672
688
|
manuallyClosing = false;
|
|
673
689
|
/* Retrieves the userID with the user identifier.
|
|
@@ -924,6 +940,14 @@ export class Client {
|
|
|
924
940
|
if (this.mailInterval) {
|
|
925
941
|
clearInterval(this.mailInterval);
|
|
926
942
|
}
|
|
943
|
+
if (this.mailBatchFlushTimer) {
|
|
944
|
+
clearTimeout(this.mailBatchFlushTimer);
|
|
945
|
+
this.mailBatchFlushTimer = null;
|
|
946
|
+
}
|
|
947
|
+
const pendingMailBatch = this.mailBatchQueue.splice(0);
|
|
948
|
+
for (const pending of pendingMailBatch) {
|
|
949
|
+
pending.reject(new Error("Client closed before mail batch sent."));
|
|
950
|
+
}
|
|
927
951
|
if (this.localRetentionPurgeTimer) {
|
|
928
952
|
clearInterval(this.localRetentionPurgeTimer);
|
|
929
953
|
this.localRetentionPurgeTimer = null;
|
|
@@ -1479,7 +1503,8 @@ export class Client {
|
|
|
1479
1503
|
// my keys
|
|
1480
1504
|
const IK_A = this.xKeyRing.identityKeys.secretKey;
|
|
1481
1505
|
const IK_AP = this.xKeyRing.identityKeys.publicKey;
|
|
1482
|
-
const
|
|
1506
|
+
const ephemeralKeys = await xBoxKeyPairAsync();
|
|
1507
|
+
const EK_A = ephemeralKeys.secretKey;
|
|
1483
1508
|
const fips = this.cryptoProfile === "fips";
|
|
1484
1509
|
// their keys — FIPS: `signKey` in bundle is the peer P-256 ECDH identity (raw, typically 65B).
|
|
1485
1510
|
const SPK_B = new Uint8Array(keyBundle.preKey.publicKey);
|
|
@@ -1519,10 +1544,10 @@ export class Client {
|
|
|
1519
1544
|
const nonce = xMakeNonce();
|
|
1520
1545
|
const cipher = await xSecretboxAsync(message, nonce, SK);
|
|
1521
1546
|
const signKeyWire = fips ? IK_AP : this.signKeys.publicKey;
|
|
1522
|
-
const ephKeyWire =
|
|
1547
|
+
const ephKeyWire = ephemeralKeys.publicKey;
|
|
1523
1548
|
const extra = fips
|
|
1524
1549
|
? encodeFipsInitialExtraV1(signKeyWire, ephKeyWire, PK, AD, IDX)
|
|
1525
|
-
: xConcat(this.signKeys.publicKey,
|
|
1550
|
+
: xConcat(this.signKeys.publicKey, ephemeralKeys.publicKey, PK, AD, IDX);
|
|
1526
1551
|
const mail = {
|
|
1527
1552
|
authorID: this.getUser().userID,
|
|
1528
1553
|
cipher,
|
|
@@ -1544,8 +1569,6 @@ export class Client {
|
|
|
1544
1569
|
transmissionID: uuid.v4(),
|
|
1545
1570
|
type: "resource",
|
|
1546
1571
|
};
|
|
1547
|
-
// discard the ephemeral keys
|
|
1548
|
-
await this.newEphemeralKeys();
|
|
1549
1572
|
const ratchet = await initRatchetSession(SK, "initiator");
|
|
1550
1573
|
const sessionEntry = {
|
|
1551
1574
|
...ratchet,
|
|
@@ -1585,33 +1608,7 @@ export class Client {
|
|
|
1585
1608
|
if (shouldEmitHandshakeMessage) {
|
|
1586
1609
|
this.emitter.emit("message", emitMsg);
|
|
1587
1610
|
}
|
|
1588
|
-
|
|
1589
|
-
await new Promise((res, rej) => {
|
|
1590
|
-
const callback = (packedMsg) => {
|
|
1591
|
-
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
1592
|
-
if (receivedMsg.transmissionID === msg.transmissionID) {
|
|
1593
|
-
this.socket.off("message", callback);
|
|
1594
|
-
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
1595
|
-
if (parsed.success && parsed.data.type === "success") {
|
|
1596
|
-
res(parsed.data.data);
|
|
1597
|
-
}
|
|
1598
|
-
else {
|
|
1599
|
-
rej(new Error("Mail delivery failed: " +
|
|
1600
|
-
JSON.stringify(receivedMsg)));
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
};
|
|
1604
|
-
this.socket.on("message", callback);
|
|
1605
|
-
// Forward send failures to the outer promise instead
|
|
1606
|
-
// of leaking them as an unhandled rejection: the
|
|
1607
|
-
// listener above can never resolve if the send didn't
|
|
1608
|
-
// make it onto the wire, so without this the caller
|
|
1609
|
-
// would hang for the full 30s send-loop timeout.
|
|
1610
|
-
this.send(msg, hmac).catch((err) => {
|
|
1611
|
-
this.socket.off("message", callback);
|
|
1612
|
-
rej(err instanceof Error ? err : new Error(String(err)));
|
|
1613
|
-
});
|
|
1614
|
-
});
|
|
1611
|
+
await this.deliverMailResource(msg, hmac, mail);
|
|
1615
1612
|
});
|
|
1616
1613
|
}
|
|
1617
1614
|
async deleteChannel(channelID) {
|
|
@@ -1641,6 +1638,49 @@ export class Client {
|
|
|
1641
1638
|
async deleteServer(serverID) {
|
|
1642
1639
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1643
1640
|
}
|
|
1641
|
+
deliverMailResource(msg, header, mail) {
|
|
1642
|
+
if (this.mailBatchUnsupported) {
|
|
1643
|
+
return this.deliverMailResourceOverSocket(msg, header);
|
|
1644
|
+
}
|
|
1645
|
+
return new Promise((resolve, reject) => {
|
|
1646
|
+
this.mailBatchQueue.push({
|
|
1647
|
+
header,
|
|
1648
|
+
mail,
|
|
1649
|
+
msg,
|
|
1650
|
+
reject,
|
|
1651
|
+
resolve,
|
|
1652
|
+
});
|
|
1653
|
+
if (this.mailBatchQueue.length >= MAIL_BATCH_MAX_SIZE) {
|
|
1654
|
+
void this.flushMailBatchQueue();
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
this.scheduleMailBatchFlush();
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
async deliverMailResourceOverSocket(msg, header) {
|
|
1662
|
+
await new Promise((res, rej) => {
|
|
1663
|
+
const callback = (packedMsg) => {
|
|
1664
|
+
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
1665
|
+
if (receivedMsg.transmissionID === msg.transmissionID) {
|
|
1666
|
+
this.socket.off("message", callback);
|
|
1667
|
+
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
1668
|
+
if (parsed.success && parsed.data.type === "success") {
|
|
1669
|
+
res();
|
|
1670
|
+
}
|
|
1671
|
+
else {
|
|
1672
|
+
rej(new Error("Mail delivery failed: " +
|
|
1673
|
+
JSON.stringify(receivedMsg)));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
this.socket.on("message", callback);
|
|
1678
|
+
this.send(msg, header).catch((err) => {
|
|
1679
|
+
this.socket.off("message", callback);
|
|
1680
|
+
rej(err instanceof Error ? err : new Error(String(err)));
|
|
1681
|
+
});
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1644
1684
|
deviceListFailureDetail(err) {
|
|
1645
1685
|
if (!isHttpError(err)) {
|
|
1646
1686
|
return "";
|
|
@@ -1767,6 +1807,64 @@ export class Client {
|
|
|
1767
1807
|
const response = await this.http.post(this.getHost() + "/user/" + userID + "/passkeys/register/finish", msgpack.encode(args), { headers: { "Content-Type": "application/msgpack" } });
|
|
1768
1808
|
return decodeHttpResponse(PasskeyCodec, response.data);
|
|
1769
1809
|
}
|
|
1810
|
+
async flushMailBatchOverSocket(batch) {
|
|
1811
|
+
await Promise.all(batch.map(async (item) => {
|
|
1812
|
+
try {
|
|
1813
|
+
await this.deliverMailResourceOverSocket(item.msg, item.header);
|
|
1814
|
+
item.resolve();
|
|
1815
|
+
}
|
|
1816
|
+
catch (err) {
|
|
1817
|
+
item.reject(err);
|
|
1818
|
+
}
|
|
1819
|
+
}));
|
|
1820
|
+
}
|
|
1821
|
+
async flushMailBatchQueue() {
|
|
1822
|
+
if (this.mailBatchFlushTimer) {
|
|
1823
|
+
clearTimeout(this.mailBatchFlushTimer);
|
|
1824
|
+
this.mailBatchFlushTimer = null;
|
|
1825
|
+
}
|
|
1826
|
+
const batch = this.mailBatchQueue.splice(0, MAIL_BATCH_MAX_SIZE);
|
|
1827
|
+
if (this.mailBatchQueue.length > 0) {
|
|
1828
|
+
this.scheduleMailBatchFlush();
|
|
1829
|
+
}
|
|
1830
|
+
if (batch.length === 0) {
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (this.mailBatchUnsupported) {
|
|
1834
|
+
await this.flushMailBatchOverSocket(batch);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
try {
|
|
1838
|
+
const response = await this.http.post(this.getHost() + "/mail/batch", msgpack.encode({
|
|
1839
|
+
mails: batch.map((item) => ({
|
|
1840
|
+
header: item.header,
|
|
1841
|
+
mail: item.mail,
|
|
1842
|
+
})),
|
|
1843
|
+
}), { headers: { "Content-Type": "application/msgpack" } });
|
|
1844
|
+
const decoded = mailBatchResponseSchema.parse(msgpack.decode(new Uint8Array(response.data)));
|
|
1845
|
+
const resultsByIndex = new Map(decoded.results.map((result) => [result.index, result]));
|
|
1846
|
+
for (const [index, item] of batch.entries()) {
|
|
1847
|
+
const result = resultsByIndex.get(index);
|
|
1848
|
+
if (result?.ok === true) {
|
|
1849
|
+
item.resolve();
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
item.reject(new Error("Mail delivery failed: " +
|
|
1853
|
+
(result?.error ??
|
|
1854
|
+
`missing batch result for index ${String(index)}`)));
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
catch (err) {
|
|
1858
|
+
if (isHttpError(err) && err.response?.status === 404) {
|
|
1859
|
+
this.mailBatchUnsupported = true;
|
|
1860
|
+
await this.flushMailBatchOverSocket(batch);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
for (const item of batch) {
|
|
1864
|
+
item.reject(err);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1770
1868
|
async forward(message) {
|
|
1771
1869
|
if (this.isManualCloseInFlight()) {
|
|
1772
1870
|
return;
|
|
@@ -1784,16 +1882,17 @@ export class Client {
|
|
|
1784
1882
|
}
|
|
1785
1883
|
const msgBytes = Uint8Array.from(msgpack.encode(copy));
|
|
1786
1884
|
const devices = await this.fetchUserDeviceListWithBackoff(this.getUser().userID, "own");
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1885
|
+
const targetDevices = devices.filter((device) => device.deviceID !== this.getDevice().deviceID);
|
|
1886
|
+
for (let index = 0; index < targetDevices.length; index += MAIL_FANOUT_CONCURRENCY) {
|
|
1887
|
+
const batch = targetDevices.slice(index, index + MAIL_FANOUT_CONCURRENCY);
|
|
1888
|
+
await Promise.all(batch.map(async (device) => {
|
|
1889
|
+
try {
|
|
1890
|
+
await this.sendMailWithRecovery(device, this.getUser(), msgBytes, null, copy.mailID, true);
|
|
1891
|
+
}
|
|
1892
|
+
catch {
|
|
1893
|
+
/* best-effort per device */
|
|
1894
|
+
}
|
|
1895
|
+
}));
|
|
1797
1896
|
}
|
|
1798
1897
|
}
|
|
1799
1898
|
async getChannelByID(channelID) {
|
|
@@ -2093,7 +2192,6 @@ export class Client {
|
|
|
2093
2192
|
this.scheduleReconnect();
|
|
2094
2193
|
return true;
|
|
2095
2194
|
}
|
|
2096
|
-
// ── Passkeys ────────────────────────────────────────────────────────
|
|
2097
2195
|
/**
|
|
2098
2196
|
* Initializes the keyring. This must be called before anything else.
|
|
2099
2197
|
*/
|
|
@@ -2108,7 +2206,6 @@ export class Client {
|
|
|
2108
2206
|
this.localRetentionPurgeTimer = setInterval(() => void this.runLocalRetentionPurge(), 6 * 60 * 60 * 1000);
|
|
2109
2207
|
this.emitter.emit("ready");
|
|
2110
2208
|
}
|
|
2111
|
-
// ── Passkeys ────────────────────────────────────────────────────────
|
|
2112
2209
|
initSocket() {
|
|
2113
2210
|
try {
|
|
2114
2211
|
if (!this.token) {
|
|
@@ -2263,15 +2360,6 @@ export class Client {
|
|
|
2263
2360
|
}
|
|
2264
2361
|
await this.submitOTK(needs);
|
|
2265
2362
|
}
|
|
2266
|
-
async newEphemeralKeys() {
|
|
2267
|
-
if (!this.xKeyRing) {
|
|
2268
|
-
if (this.manuallyClosing) {
|
|
2269
|
-
return;
|
|
2270
|
-
}
|
|
2271
|
-
throw new Error("Key ring not initialized.");
|
|
2272
|
-
}
|
|
2273
|
-
this.xKeyRing.ephemeralKeys = await xBoxKeyPairAsync();
|
|
2274
|
-
}
|
|
2275
2363
|
/**
|
|
2276
2364
|
* Pipeline for decrypted messages — registered in `init`. After `close()` sets
|
|
2277
2365
|
* `manuallyClosing`, this becomes a no-op so fire-and-forget `forward` does not
|
|
@@ -2293,16 +2381,6 @@ export class Client {
|
|
|
2293
2381
|
void this.database.saveMessage(message);
|
|
2294
2382
|
this.scheduleRetentionPurge();
|
|
2295
2383
|
};
|
|
2296
|
-
async passkeyApproveDeviceRequest(requestID) {
|
|
2297
|
-
const userID = this.getUser().userID;
|
|
2298
|
-
const response = await this.http.post(this.getHost() +
|
|
2299
|
-
"/user/" +
|
|
2300
|
-
userID +
|
|
2301
|
-
"/passkey/devices/requests/" +
|
|
2302
|
-
requestID +
|
|
2303
|
-
"/approve");
|
|
2304
|
-
return decodeHttpResponse(DeviceCodec, response.data);
|
|
2305
|
-
}
|
|
2306
2384
|
async passkeyDeleteDevice(deviceID) {
|
|
2307
2385
|
const userID = this.getUser().userID;
|
|
2308
2386
|
await this.http.delete(this.getHost() + "/user/" + userID + "/passkey/devices/" + deviceID);
|
|
@@ -2312,6 +2390,15 @@ export class Client {
|
|
|
2312
2390
|
const response = await this.http.get(this.getHost() + "/user/" + userID + "/passkey/devices");
|
|
2313
2391
|
return decodeHttpResponse(DeviceArrayCodec, response.data);
|
|
2314
2392
|
}
|
|
2393
|
+
async passkeyRecoverDeviceRequest(requestID) {
|
|
2394
|
+
const userID = this.getUser().userID;
|
|
2395
|
+
const response = await this.http.post(this.getHost() +
|
|
2396
|
+
"/user/" +
|
|
2397
|
+
userID +
|
|
2398
|
+
"/passkey/recover/devices/requests/" +
|
|
2399
|
+
requestID);
|
|
2400
|
+
return decodeHttpResponse(DeviceCodec, response.data);
|
|
2401
|
+
}
|
|
2315
2402
|
async passkeyRejectDeviceRequest(requestID) {
|
|
2316
2403
|
const userID = this.getUser().userID;
|
|
2317
2404
|
await this.http.post(this.getHost() +
|
|
@@ -3116,6 +3203,15 @@ export class Client {
|
|
|
3116
3203
|
leaveCryptoProfileScope();
|
|
3117
3204
|
}
|
|
3118
3205
|
}
|
|
3206
|
+
scheduleMailBatchFlush() {
|
|
3207
|
+
if (this.mailBatchFlushTimer) {
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
this.mailBatchFlushTimer = setTimeout(() => {
|
|
3211
|
+
this.mailBatchFlushTimer = null;
|
|
3212
|
+
void this.flushMailBatchQueue();
|
|
3213
|
+
}, MAIL_BATCH_FLUSH_DELAY_MS);
|
|
3214
|
+
}
|
|
3119
3215
|
scheduleReconnect() {
|
|
3120
3216
|
if (!this.autoReconnectEnabled ||
|
|
3121
3217
|
this.isManualCloseInFlight() ||
|
|
@@ -3235,20 +3331,31 @@ export class Client {
|
|
|
3235
3331
|
const stableDevices = [...targetDevices.values()].sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
|
|
3236
3332
|
let failCount = 0;
|
|
3237
3333
|
let lastErr;
|
|
3238
|
-
for (
|
|
3239
|
-
const
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3334
|
+
for (let index = 0; index < stableDevices.length; index += MAIL_FANOUT_CONCURRENCY) {
|
|
3335
|
+
const batch = stableDevices.slice(index, index + MAIL_FANOUT_CONCURRENCY);
|
|
3336
|
+
const results = await Promise.all(batch.map(async (device) => {
|
|
3337
|
+
const ownerRecord = device.owner === myUserID
|
|
3338
|
+
? this.getUser()
|
|
3339
|
+
: this.userRecords[device.owner];
|
|
3340
|
+
if (!ownerRecord) {
|
|
3341
|
+
return new Error(`Missing owner record for device ${device.deviceID}.`);
|
|
3342
|
+
}
|
|
3343
|
+
try {
|
|
3344
|
+
await this.sendMailWithRecovery(device, ownerRecord, msgBytes, uuidToUint8(channelID), mailID, false);
|
|
3345
|
+
return undefined;
|
|
3346
|
+
}
|
|
3347
|
+
catch (e) {
|
|
3348
|
+
return e;
|
|
3349
|
+
}
|
|
3350
|
+
}));
|
|
3351
|
+
for (const result of results) {
|
|
3352
|
+
if (result !== undefined) {
|
|
3353
|
+
lastErr = result;
|
|
3354
|
+
failCount += 1;
|
|
3355
|
+
}
|
|
3248
3356
|
}
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
failCount += 1;
|
|
3357
|
+
if (failCount === stableDevices.length) {
|
|
3358
|
+
break;
|
|
3252
3359
|
}
|
|
3253
3360
|
}
|
|
3254
3361
|
if (failCount === stableDevices.length) {
|
|
@@ -3370,31 +3477,7 @@ export class Client {
|
|
|
3370
3477
|
};
|
|
3371
3478
|
await this.database.saveSession(persisted);
|
|
3372
3479
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
3373
|
-
await
|
|
3374
|
-
const callback = (packedMsg) => {
|
|
3375
|
-
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
3376
|
-
if (receivedMsg.transmissionID === msgb.transmissionID) {
|
|
3377
|
-
this.socket.off("message", callback);
|
|
3378
|
-
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
3379
|
-
if (parsed.success && parsed.data.type === "success") {
|
|
3380
|
-
res(parsed.data.data);
|
|
3381
|
-
}
|
|
3382
|
-
else {
|
|
3383
|
-
rej(new Error("Mail delivery failed: " +
|
|
3384
|
-
JSON.stringify(receivedMsg)));
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
|
-
};
|
|
3388
|
-
this.socket.on("message", callback);
|
|
3389
|
-
// See the matching block above (sendMail handshake):
|
|
3390
|
-
// forward send failures to the outer promise so the
|
|
3391
|
-
// caller doesn't hang waiting for a response we never
|
|
3392
|
-
// sent.
|
|
3393
|
-
this.send(msgb, hmac).catch((err) => {
|
|
3394
|
-
this.socket.off("message", callback);
|
|
3395
|
-
rej(err instanceof Error ? err : new Error(String(err)));
|
|
3396
|
-
});
|
|
3397
|
-
});
|
|
3480
|
+
await this.deliverMailResource(msgb, hmac, mail);
|
|
3398
3481
|
}
|
|
3399
3482
|
finally {
|
|
3400
3483
|
this.sending.delete(device.deviceID);
|
|
@@ -3464,28 +3547,41 @@ export class Client {
|
|
|
3464
3547
|
// One logical DM fan-outs to multiple recipient devices. Reuse a
|
|
3465
3548
|
// single mailID so local/UI dedupe treats it as one message.
|
|
3466
3549
|
const messageMailID = uuid.v4();
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3550
|
+
const msgBytes = XUtils.decodeUTF8(payload);
|
|
3551
|
+
for (let index = 0; index < deviceList.length; index += MAIL_FANOUT_CONCURRENCY) {
|
|
3552
|
+
const batch = deviceList.slice(index, index + MAIL_FANOUT_CONCURRENCY);
|
|
3553
|
+
const results = await Promise.all(batch.map(async (device) => {
|
|
3554
|
+
try {
|
|
3555
|
+
if (libvexDebugDmEnabled()) {
|
|
3556
|
+
debugLibvexDm("sendMessage: sendMail start", {
|
|
3557
|
+
mailID: messageMailID,
|
|
3558
|
+
recipientDevice: device.deviceID,
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
await this.sendMailWithRecovery(device, userEntry, msgBytes, null, messageMailID, false);
|
|
3562
|
+
if (libvexDebugDmEnabled()) {
|
|
3563
|
+
debugLibvexDm("sendMessage: sendMail ok", {
|
|
3564
|
+
recipientDevice: device.deviceID,
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
return undefined;
|
|
3474
3568
|
}
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
}
|
|
3569
|
+
catch (e) {
|
|
3570
|
+
if (libvexDebugDmEnabled()) {
|
|
3571
|
+
// eslint-disable-next-line no-console -- LIBVEX_DEBUG_DM only
|
|
3572
|
+
console.error("[libvex:debug-dm] sendMessage: sendMail failed for device", device.deviceID, e);
|
|
3573
|
+
}
|
|
3574
|
+
return e;
|
|
3480
3575
|
}
|
|
3481
|
-
}
|
|
3482
|
-
|
|
3483
|
-
if (
|
|
3484
|
-
|
|
3485
|
-
|
|
3576
|
+
}));
|
|
3577
|
+
for (const result of results) {
|
|
3578
|
+
if (result !== undefined) {
|
|
3579
|
+
lastErr = result;
|
|
3580
|
+
failCount += 1;
|
|
3486
3581
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3582
|
+
}
|
|
3583
|
+
if (failCount === deviceList.length) {
|
|
3584
|
+
break;
|
|
3489
3585
|
}
|
|
3490
3586
|
}
|
|
3491
3587
|
if (failCount > 0) {
|