@vex-chat/libvex 7.2.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Client.d.ts +27 -3
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +653 -190
- package/dist/Client.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +845 -228
- package/src/__tests__/harness/shared-suite.ts +80 -1
package/dist/Client.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
6
|
import { enterCryptoProfileScope, getCryptoProfile, leaveCryptoProfileScope, setCryptoProfile, xBoxKeyPairAsync, xBoxKeyPairFromSecretAsync, xConcat, xConstants, xDHAsync, xEcdhKeyPairFromEcdsaKeyPairAsync, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMnemonic, xRandomBytes, xSecretboxAsync, xSecretboxOpenAsync, xSignAsync, xSignKeyPair, xSignKeyPairAsync, xSignKeyPairFromSecret, xSignKeyPairFromSecretAsync, xSignOpenAsync, XUtils, } from "@vex-chat/crypto";
|
|
7
|
-
import {
|
|
7
|
+
import { CallEnvelopeBodySchema, CallEventSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, SignedCallEnvelopeSchema, WSMessageSchema, } from "@vex-chat/types";
|
|
8
8
|
import { EventEmitter } from "eventemitter3";
|
|
9
9
|
import * as uuid from "uuid";
|
|
10
10
|
import { z } from "zod/v4";
|
|
@@ -74,15 +74,6 @@ function debugLibvexDm(msg, data) {
|
|
|
74
74
|
// eslint-disable-next-line no-console -- gated by LIBVEX_DEBUG_DM; remove when debugging is done
|
|
75
75
|
console.error(`[libvex:debug-dm] ${payload}`);
|
|
76
76
|
}
|
|
77
|
-
function errorFromUnknown(err) {
|
|
78
|
-
if (err instanceof Error) {
|
|
79
|
-
return err;
|
|
80
|
-
}
|
|
81
|
-
if (typeof err === "string") {
|
|
82
|
-
return new Error(err);
|
|
83
|
-
}
|
|
84
|
-
return new Error(JSON.stringify(err));
|
|
85
|
-
}
|
|
86
77
|
function ignoreSocketTeardown(err) {
|
|
87
78
|
if (err instanceof WebSocketNotOpenError)
|
|
88
79
|
return;
|
|
@@ -208,8 +199,13 @@ import { ActionTokenCodec, AuthResponseCodec, ChannelArrayCodec, ChannelCodec, C
|
|
|
208
199
|
import { sqlSessionToCrypto } from "./utils/sqlSessionToCrypto.js";
|
|
209
200
|
import { uuidToUint8 } from "./utils/uint8uuid.js";
|
|
210
201
|
const _protocolMsgRegex = /��\w+:\w+��/g;
|
|
202
|
+
const NotificationSubscriptionChannelSchema = z.enum([
|
|
203
|
+
"apnsVoip",
|
|
204
|
+
"expo",
|
|
205
|
+
"fcmCall",
|
|
206
|
+
]);
|
|
211
207
|
const NotificationSubscriptionSchema = z.object({
|
|
212
|
-
channel:
|
|
208
|
+
channel: NotificationSubscriptionChannelSchema,
|
|
213
209
|
createdAt: z.string(),
|
|
214
210
|
deviceID: z.string(),
|
|
215
211
|
enabled: z.boolean(),
|
|
@@ -277,6 +273,9 @@ const messageSchema = z.object({
|
|
|
277
273
|
sender: z.string(),
|
|
278
274
|
timestamp: z.string(),
|
|
279
275
|
});
|
|
276
|
+
const CALL_ENVELOPE_PREFIX = "vex-call:1\n";
|
|
277
|
+
const CALL_INVITE_TTL_MS = 60_000;
|
|
278
|
+
const CALL_MAX_TTL_MS = 2 * 60 * 60 * 1000;
|
|
280
279
|
const MESSAGE_BLOB_PREFIX = "vex-message:1\n";
|
|
281
280
|
const MAIL_FANOUT_CONCURRENCY = 8;
|
|
282
281
|
const MAIL_BATCH_MAX_SIZE = 32;
|
|
@@ -291,6 +290,52 @@ const mailBatchResponseSchema = z.object({
|
|
|
291
290
|
status: z.number().int().optional(),
|
|
292
291
|
})),
|
|
293
292
|
});
|
|
293
|
+
const callWakeNotifyData = z.object({
|
|
294
|
+
callID: z.string(),
|
|
295
|
+
expiresAt: z.string().optional(),
|
|
296
|
+
mailID: z.string().optional(),
|
|
297
|
+
mailNonce: z.string().optional(),
|
|
298
|
+
});
|
|
299
|
+
function canonicalizeJson(value) {
|
|
300
|
+
if (Array.isArray(value)) {
|
|
301
|
+
return value.map((item) => canonicalizeJson(item));
|
|
302
|
+
}
|
|
303
|
+
if (!isRecord(value)) {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
const out = {};
|
|
307
|
+
for (const key of Object.keys(value).sort()) {
|
|
308
|
+
const item = value[key];
|
|
309
|
+
if (item !== undefined) {
|
|
310
|
+
out[key] = canonicalizeJson(item);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
function canonicalJsonBytes(value) {
|
|
316
|
+
return XUtils.decodeUTF8(JSON.stringify(canonicalizeJson(jsonWireValue(value))));
|
|
317
|
+
}
|
|
318
|
+
function cloneCallSession(session) {
|
|
319
|
+
return {
|
|
320
|
+
...session,
|
|
321
|
+
participants: session.participants.map((participant) => ({
|
|
322
|
+
...participant,
|
|
323
|
+
})),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function decodeCallEnvelopePlaintext(plaintext) {
|
|
327
|
+
if (!plaintext.startsWith(CALL_ENVELOPE_PREFIX)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const raw = JSON.parse(plaintext.slice(CALL_ENVELOPE_PREFIX.length));
|
|
332
|
+
const parsed = SignedCallEnvelopeSchema.safeParse(raw);
|
|
333
|
+
return parsed.success ? parsed.data : null;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
294
339
|
function decodeMessageBlob(body) {
|
|
295
340
|
if (!body.startsWith(MESSAGE_BLOB_PREFIX)) {
|
|
296
341
|
return { message: body };
|
|
@@ -324,6 +369,9 @@ function decodeMessagePlaintext(plaintext) {
|
|
|
324
369
|
}
|
|
325
370
|
: blob;
|
|
326
371
|
}
|
|
372
|
+
function encodeCallEnvelopePlaintext(envelope) {
|
|
373
|
+
return XUtils.decodeUTF8(CALL_ENVELOPE_PREFIX + JSON.stringify(envelope));
|
|
374
|
+
}
|
|
327
375
|
function encodeMessagePlaintext(message, opts) {
|
|
328
376
|
const body = opts?.extra === undefined
|
|
329
377
|
? message
|
|
@@ -334,6 +382,12 @@ function encodeMessagePlaintext(message, opts) {
|
|
|
334
382
|
});
|
|
335
383
|
return formatVexRetentionEnvelope(body, opts?.retentionHintDays);
|
|
336
384
|
}
|
|
385
|
+
function jsonWireValue(value) {
|
|
386
|
+
// Match the payload JSON.stringify will send, including RTC toJSON() output.
|
|
387
|
+
const encoded = JSON.stringify(value);
|
|
388
|
+
const parsed = JSON.parse(encoded);
|
|
389
|
+
return parsed;
|
|
390
|
+
}
|
|
337
391
|
function messageFromDecodedPlaintext(decoded) {
|
|
338
392
|
return {
|
|
339
393
|
...(decoded.extra !== undefined ? { extra: decoded.extra } : {}),
|
|
@@ -343,6 +397,9 @@ function messageFromDecodedPlaintext(decoded) {
|
|
|
343
397
|
: {}),
|
|
344
398
|
};
|
|
345
399
|
}
|
|
400
|
+
function normalizeCallEnvelopeBodyForWire(body) {
|
|
401
|
+
return CallEnvelopeBodySchema.parse(jsonWireValue(body));
|
|
402
|
+
}
|
|
346
403
|
function normalizeForwardedMessage(message) {
|
|
347
404
|
const decoded = decodeMessagePlaintext(message.message);
|
|
348
405
|
return {
|
|
@@ -396,22 +453,15 @@ export class Client {
|
|
|
396
453
|
* authenticated signaling and call state over Spire.
|
|
397
454
|
*/
|
|
398
455
|
calls = {
|
|
399
|
-
accept: (callID, signal) => this.
|
|
400
|
-
callID,
|
|
401
|
-
...(signal ? { signal } : {}),
|
|
402
|
-
}),
|
|
456
|
+
accept: (callID, signal) => this.sendEncryptedCallAction("accept", callID, signal),
|
|
403
457
|
active: this.fetchActiveCalls.bind(this),
|
|
404
|
-
cancel: (callID) => this.
|
|
405
|
-
hangup: (callID) => this.
|
|
406
|
-
ice: (callID, signal) => this.
|
|
458
|
+
cancel: (callID) => this.sendEncryptedCallAction("cancel", callID),
|
|
459
|
+
hangup: (callID) => this.sendEncryptedCallAction("hangup", callID),
|
|
460
|
+
ice: (callID, signal) => this.sendEncryptedCallAction("ice", callID, signal),
|
|
407
461
|
iceServers: this.fetchIceServers.bind(this),
|
|
408
|
-
reject: (callID) => this.
|
|
409
|
-
signal: (callID, signal) => this.
|
|
410
|
-
startDM: (recipientUserID, signal) => this.
|
|
411
|
-
conversationType: "dm",
|
|
412
|
-
recipientUserID,
|
|
413
|
-
...(signal ? { signal } : {}),
|
|
414
|
-
}),
|
|
462
|
+
reject: (callID) => this.sendEncryptedCallAction("reject", callID),
|
|
463
|
+
signal: (callID, signal) => this.sendEncryptedCallAction("signal", callID, signal),
|
|
464
|
+
startDM: (recipientUserID, signal) => this.startEncryptedDmCall(recipientUserID, signal),
|
|
415
465
|
};
|
|
416
466
|
/**
|
|
417
467
|
* Browser-safe NODE_ENV accessor.
|
|
@@ -697,6 +747,7 @@ export class Client {
|
|
|
697
747
|
retrieve: this.fetchUser.bind(this),
|
|
698
748
|
};
|
|
699
749
|
autoReconnectEnabled = false;
|
|
750
|
+
callStates = new Map();
|
|
700
751
|
cryptoProfile;
|
|
701
752
|
database;
|
|
702
753
|
dbPath;
|
|
@@ -1389,6 +1440,86 @@ export class Client {
|
|
|
1389
1440
|
this.emitUndecryptedMessage(mail, timestamp);
|
|
1390
1441
|
this.acknowledgeInboundMail(mail);
|
|
1391
1442
|
}
|
|
1443
|
+
applyCallEnvelopeBody(body) {
|
|
1444
|
+
const localUserID = this.getUser().userID;
|
|
1445
|
+
const peerUserID = body.fromUserID === localUserID ? body.toUserID : body.fromUserID;
|
|
1446
|
+
const peerDeviceID = body.fromUserID === localUserID
|
|
1447
|
+
? body.toDeviceID
|
|
1448
|
+
: body.fromDeviceID;
|
|
1449
|
+
let state = this.callStates.get(body.callID);
|
|
1450
|
+
if (!state) {
|
|
1451
|
+
state = {
|
|
1452
|
+
peerDeviceID,
|
|
1453
|
+
peerUserID,
|
|
1454
|
+
pendingPeerDevices: [],
|
|
1455
|
+
sequence: 0,
|
|
1456
|
+
session: this.sessionFromCallEnvelope(body),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
state.peerUserID = peerUserID;
|
|
1460
|
+
if (body.fromUserID !== localUserID || body.action === "accept") {
|
|
1461
|
+
state.peerDeviceID = peerDeviceID;
|
|
1462
|
+
}
|
|
1463
|
+
state.sequence = Math.max(state.sequence, body.sequence);
|
|
1464
|
+
state.session.expiresAt = body.expiresAt;
|
|
1465
|
+
const now = new Date().toISOString();
|
|
1466
|
+
switch (body.action) {
|
|
1467
|
+
case "accept":
|
|
1468
|
+
state.session.status = "active";
|
|
1469
|
+
this.upsertCallParticipant(state.session, {
|
|
1470
|
+
acceptedAt: now,
|
|
1471
|
+
deviceID: body.fromDeviceID,
|
|
1472
|
+
joinedAt: now,
|
|
1473
|
+
state: "accepted",
|
|
1474
|
+
userID: body.fromUserID,
|
|
1475
|
+
});
|
|
1476
|
+
break;
|
|
1477
|
+
case "cancel":
|
|
1478
|
+
case "end":
|
|
1479
|
+
case "hangup":
|
|
1480
|
+
case "reject":
|
|
1481
|
+
case "timeout":
|
|
1482
|
+
state.session.status = "ended";
|
|
1483
|
+
state.session.endedAt = now;
|
|
1484
|
+
this.upsertCallParticipant(state.session, {
|
|
1485
|
+
leftAt: now,
|
|
1486
|
+
state: body.action === "reject" ? "rejected" : "left",
|
|
1487
|
+
userID: body.fromUserID,
|
|
1488
|
+
});
|
|
1489
|
+
break;
|
|
1490
|
+
case "ice":
|
|
1491
|
+
case "signal":
|
|
1492
|
+
break;
|
|
1493
|
+
case "invite":
|
|
1494
|
+
state.session.status = "ringing";
|
|
1495
|
+
this.upsertCallParticipant(state.session, {
|
|
1496
|
+
acceptedAt: body.createdAt,
|
|
1497
|
+
deviceID: body.createdByDeviceID,
|
|
1498
|
+
joinedAt: body.createdAt,
|
|
1499
|
+
state: "accepted",
|
|
1500
|
+
userID: body.createdBy,
|
|
1501
|
+
});
|
|
1502
|
+
this.upsertCallParticipant(state.session, {
|
|
1503
|
+
state: "ringing",
|
|
1504
|
+
userID: body.toUserID,
|
|
1505
|
+
});
|
|
1506
|
+
break;
|
|
1507
|
+
}
|
|
1508
|
+
const event = {
|
|
1509
|
+
action: body.action,
|
|
1510
|
+
call: cloneCallSession(state.session),
|
|
1511
|
+
fromDeviceID: body.fromDeviceID,
|
|
1512
|
+
fromUserID: body.fromUserID,
|
|
1513
|
+
...(body.signal ? { signal: body.signal } : {}),
|
|
1514
|
+
};
|
|
1515
|
+
if (state.session.status === "ended") {
|
|
1516
|
+
this.callStates.delete(body.callID);
|
|
1517
|
+
}
|
|
1518
|
+
else {
|
|
1519
|
+
this.callStates.set(body.callID, state);
|
|
1520
|
+
}
|
|
1521
|
+
return event;
|
|
1522
|
+
}
|
|
1392
1523
|
async approveDeviceRequest(requestID) {
|
|
1393
1524
|
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
1394
1525
|
if (!req) {
|
|
@@ -1425,6 +1556,35 @@ export class Client {
|
|
|
1425
1556
|
"/passkeys/register/begin", msgpack.encode({ name: args.name, signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1426
1557
|
return decodeHttpResponse(PasskeyOptionsCodec, response.data);
|
|
1427
1558
|
}
|
|
1559
|
+
async callEnvelopeForBody(body) {
|
|
1560
|
+
const wireBody = normalizeCallEnvelopeBodyForWire(body);
|
|
1561
|
+
const signed = await xSignAsync(canonicalJsonBytes(wireBody), this.signKeys.secretKey);
|
|
1562
|
+
return {
|
|
1563
|
+
body: wireBody,
|
|
1564
|
+
signed: XUtils.encodeHex(signed),
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
async callTargetsForState(state) {
|
|
1568
|
+
if (state.peerDeviceID) {
|
|
1569
|
+
const cached = this.deviceRecords[state.peerDeviceID];
|
|
1570
|
+
const device = cached ?? (await this.getDeviceByID(state.peerDeviceID));
|
|
1571
|
+
if (!device) {
|
|
1572
|
+
throw new Error(`Call peer device not found: ${state.peerDeviceID}`);
|
|
1573
|
+
}
|
|
1574
|
+
return [device];
|
|
1575
|
+
}
|
|
1576
|
+
return state.pendingPeerDevices;
|
|
1577
|
+
}
|
|
1578
|
+
callWakeForEnvelope(body) {
|
|
1579
|
+
if (body.action !== "invite") {
|
|
1580
|
+
return undefined;
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
callID: body.callID,
|
|
1584
|
+
event: "callWake",
|
|
1585
|
+
expiresAt: body.expiresAt,
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1428
1588
|
censorPreKey(preKey) {
|
|
1429
1589
|
if (!preKey.index) {
|
|
1430
1590
|
throw new Error("Key index is required.");
|
|
@@ -1527,7 +1687,7 @@ export class Client {
|
|
|
1527
1687
|
* When `readMail` triggers a best-effort session re-establish, key-bundle
|
|
1528
1688
|
* errors should not reject the full read pipeline.
|
|
1529
1689
|
*/
|
|
1530
|
-
allowKeyBundleFailure = false) {
|
|
1690
|
+
allowKeyBundleFailure = false, notify) {
|
|
1531
1691
|
return this.runWithThisCryptoProfile(async () => {
|
|
1532
1692
|
let keyBundle;
|
|
1533
1693
|
try {
|
|
@@ -1608,10 +1768,11 @@ export class Client {
|
|
|
1608
1768
|
recipient: device.deviceID,
|
|
1609
1769
|
sender: this.getDevice().deviceID,
|
|
1610
1770
|
};
|
|
1771
|
+
const wireMail = notify ? { ...mail, notify } : mail;
|
|
1611
1772
|
const hmac = xHMAC(mail, SK);
|
|
1612
1773
|
const msg = {
|
|
1613
1774
|
action: "CREATE",
|
|
1614
|
-
data:
|
|
1775
|
+
data: wireMail,
|
|
1615
1776
|
resourceType: "mail",
|
|
1616
1777
|
transmissionID: uuid.v4(),
|
|
1617
1778
|
type: "resource",
|
|
@@ -1631,32 +1792,38 @@ export class Client {
|
|
|
1631
1792
|
};
|
|
1632
1793
|
await this.database.saveSession(sessionEntry);
|
|
1633
1794
|
this.emitter.emit("session", sessionEntry, user);
|
|
1634
|
-
|
|
1795
|
+
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(message);
|
|
1796
|
+
const callEnvelope = forward
|
|
1797
|
+
? null
|
|
1798
|
+
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
1635
1799
|
const forwardedMsg = forward
|
|
1636
1800
|
? messageSchema.parse(msgpack.decode(message))
|
|
1637
1801
|
: null;
|
|
1638
|
-
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
1639
1802
|
const emitMsg = forwardedMsg
|
|
1640
1803
|
? { ...normalizeForwardedMessage(forwardedMsg), forward: true }
|
|
1641
|
-
:
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1804
|
+
: callEnvelope
|
|
1805
|
+
? null
|
|
1806
|
+
: message.length > 0
|
|
1807
|
+
? {
|
|
1808
|
+
authorID: mail.authorID,
|
|
1809
|
+
decrypted: true,
|
|
1810
|
+
direction: "outgoing",
|
|
1811
|
+
forward: mail.forward,
|
|
1812
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
1813
|
+
mailID: mail.mailID,
|
|
1814
|
+
...messageFromDecodedPlaintext(decodeMessagePlaintext(rawPlaintext)),
|
|
1815
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
1816
|
+
readerID: mail.readerID,
|
|
1817
|
+
recipient: mail.recipient,
|
|
1818
|
+
sender: mail.sender,
|
|
1819
|
+
timestamp: new Date().toISOString(),
|
|
1820
|
+
}
|
|
1821
|
+
: null;
|
|
1822
|
+
if (emitMsg) {
|
|
1656
1823
|
this.emitter.emit("message", emitMsg);
|
|
1657
1824
|
}
|
|
1658
|
-
await this.deliverMailResource(msg, hmac,
|
|
1659
|
-
return
|
|
1825
|
+
await this.deliverMailResource(msg, hmac, wireMail);
|
|
1826
|
+
return emitMsg;
|
|
1660
1827
|
});
|
|
1661
1828
|
}
|
|
1662
1829
|
async deleteChannel(channelID) {
|
|
@@ -1686,6 +1853,49 @@ export class Client {
|
|
|
1686
1853
|
async deleteServer(serverID) {
|
|
1687
1854
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1688
1855
|
}
|
|
1856
|
+
async deliverCallEnvelopeBatch(args) {
|
|
1857
|
+
let failCount = 0;
|
|
1858
|
+
let lastErr;
|
|
1859
|
+
for (let index = 0; index < args.bodies.length; index += MAIL_FANOUT_CONCURRENCY) {
|
|
1860
|
+
const batch = args.bodies.slice(index, index + MAIL_FANOUT_CONCURRENCY);
|
|
1861
|
+
const results = await Promise.all(batch.map(async (body) => {
|
|
1862
|
+
try {
|
|
1863
|
+
const targetDevice = this.deviceRecords[body.toDeviceID] ??
|
|
1864
|
+
(await this.getDeviceByID(body.toDeviceID));
|
|
1865
|
+
if (!targetDevice) {
|
|
1866
|
+
throw new Error(`Call target device not found: ${body.toDeviceID}`);
|
|
1867
|
+
}
|
|
1868
|
+
await this.sendCallEnvelopeMail({
|
|
1869
|
+
body,
|
|
1870
|
+
mailID: args.mailID,
|
|
1871
|
+
notify: this.callWakeForEnvelope(body),
|
|
1872
|
+
targetDevice,
|
|
1873
|
+
targetUser: args.targetUser,
|
|
1874
|
+
});
|
|
1875
|
+
return undefined;
|
|
1876
|
+
}
|
|
1877
|
+
catch (err) {
|
|
1878
|
+
return err;
|
|
1879
|
+
}
|
|
1880
|
+
}));
|
|
1881
|
+
for (const result of results) {
|
|
1882
|
+
if (result !== undefined) {
|
|
1883
|
+
lastErr = result;
|
|
1884
|
+
failCount += 1;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
if (failCount > 0) {
|
|
1889
|
+
const base = lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
1890
|
+
if (failCount === args.bodies.length) {
|
|
1891
|
+
throw base;
|
|
1892
|
+
}
|
|
1893
|
+
const partial = new Error(`Call signaling failed to reach ${String(failCount)} of ` +
|
|
1894
|
+
`${String(args.bodies.length)} peer device(s).`);
|
|
1895
|
+
partial.cause = base;
|
|
1896
|
+
throw partial;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1689
1899
|
deliverMailResource(msg, header, mail) {
|
|
1690
1900
|
if (this.mailBatchUnsupported) {
|
|
1691
1901
|
return this.deliverMailResourceOverSocket(msg, header);
|
|
@@ -1758,12 +1968,50 @@ export class Client {
|
|
|
1758
1968
|
timestamp,
|
|
1759
1969
|
});
|
|
1760
1970
|
}
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1971
|
+
fetchActiveCalls() {
|
|
1972
|
+
const now = Date.now();
|
|
1973
|
+
const active = [];
|
|
1974
|
+
for (const [callID, state] of this.callStates.entries()) {
|
|
1975
|
+
if (state.session.status === "ended" ||
|
|
1976
|
+
Date.parse(state.session.expiresAt) <= now) {
|
|
1977
|
+
this.callStates.delete(callID);
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
active.push(cloneCallSession(state.session));
|
|
1981
|
+
}
|
|
1982
|
+
return Promise.resolve(active);
|
|
1983
|
+
}
|
|
1984
|
+
async fetchCallPeer(args) {
|
|
1985
|
+
const [user, err] = await this.fetchUser(args.userID);
|
|
1986
|
+
if (err) {
|
|
1987
|
+
throw err;
|
|
1988
|
+
}
|
|
1989
|
+
if (!user) {
|
|
1990
|
+
throw new Error("Call peer not found.");
|
|
1991
|
+
}
|
|
1992
|
+
const afterBackoff = await this.fetchUserDeviceListWithBackoff(args.userID, "peer");
|
|
1993
|
+
let deviceListRaw;
|
|
1994
|
+
try {
|
|
1995
|
+
const again = await this.fetchUserDeviceListOnce(args.userID);
|
|
1996
|
+
const byID = new Map();
|
|
1997
|
+
for (const device of afterBackoff) {
|
|
1998
|
+
byID.set(device.deviceID, device);
|
|
1999
|
+
}
|
|
2000
|
+
for (const device of again) {
|
|
2001
|
+
byID.set(device.deviceID, device);
|
|
2002
|
+
}
|
|
2003
|
+
deviceListRaw = [...byID.values()];
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
deviceListRaw = afterBackoff;
|
|
2007
|
+
}
|
|
2008
|
+
const devices = deviceListRaw
|
|
2009
|
+
.filter((device) => !device.deleted)
|
|
2010
|
+
.sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
|
|
2011
|
+
if (devices.length === 0) {
|
|
2012
|
+
throw new Error("Call peer has no active devices.");
|
|
2013
|
+
}
|
|
2014
|
+
return { devices, user };
|
|
1767
2015
|
}
|
|
1768
2016
|
async fetchIceServers() {
|
|
1769
2017
|
const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
|
|
@@ -1867,6 +2115,23 @@ export class Client {
|
|
|
1867
2115
|
}
|
|
1868
2116
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
1869
2117
|
}
|
|
2118
|
+
async fetchUserOrThrow(userID) {
|
|
2119
|
+
if (userID === this.getUser().userID) {
|
|
2120
|
+
return this.getUser();
|
|
2121
|
+
}
|
|
2122
|
+
const cached = this.userRecords[userID];
|
|
2123
|
+
if (cached) {
|
|
2124
|
+
return cached;
|
|
2125
|
+
}
|
|
2126
|
+
const [user, err] = await this.fetchUser(userID);
|
|
2127
|
+
if (err) {
|
|
2128
|
+
throw err;
|
|
2129
|
+
}
|
|
2130
|
+
if (!user) {
|
|
2131
|
+
throw new Error(`User not found: ${userID}`);
|
|
2132
|
+
}
|
|
2133
|
+
return user;
|
|
2134
|
+
}
|
|
1870
2135
|
/**
|
|
1871
2136
|
* Finish a passkey login and adopt the resulting JWT as the
|
|
1872
2137
|
* client's bearer token. After this call, `client.passkeys.*`
|
|
@@ -2253,6 +2518,14 @@ export class Client {
|
|
|
2253
2518
|
}
|
|
2254
2519
|
break;
|
|
2255
2520
|
}
|
|
2521
|
+
case "callWake": {
|
|
2522
|
+
const parsed = callWakeNotifyData.safeParse(msg.data);
|
|
2523
|
+
await this.getMail();
|
|
2524
|
+
if (parsed.success) {
|
|
2525
|
+
this.emitter.emit("callWake", parsed.data);
|
|
2526
|
+
}
|
|
2527
|
+
break;
|
|
2528
|
+
}
|
|
2256
2529
|
case "deviceRequest": {
|
|
2257
2530
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
2258
2531
|
if (parsed.success) {
|
|
@@ -2470,6 +2743,67 @@ export class Client {
|
|
|
2470
2743
|
const response = await this.http.get(this.getHost() + "/user/" + userID + "/passkeys");
|
|
2471
2744
|
return decodeHttpResponse(PasskeyArrayCodec, response.data);
|
|
2472
2745
|
}
|
|
2746
|
+
makeCallEnvelopeBody(args) {
|
|
2747
|
+
return {
|
|
2748
|
+
action: args.action,
|
|
2749
|
+
callID: args.state.session.callID,
|
|
2750
|
+
conversationID: args.state.session.conversationID,
|
|
2751
|
+
conversationType: args.state.session.conversationType,
|
|
2752
|
+
createdAt: args.state.session.createdAt,
|
|
2753
|
+
createdBy: args.state.session.createdBy,
|
|
2754
|
+
createdByDeviceID: args.state.session.createdByDeviceID,
|
|
2755
|
+
expiresAt: args.expiresAt,
|
|
2756
|
+
fromDeviceID: this.getDevice().deviceID,
|
|
2757
|
+
fromUserID: this.getUser().userID,
|
|
2758
|
+
media: "audio",
|
|
2759
|
+
sequence: args.sequence,
|
|
2760
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
2761
|
+
toDeviceID: args.toDeviceID,
|
|
2762
|
+
toUserID: args.toUserID,
|
|
2763
|
+
version: 1,
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
markLocalCallAction(state, action, signal) {
|
|
2767
|
+
const now = new Date().toISOString();
|
|
2768
|
+
if (action === "accept") {
|
|
2769
|
+
state.session.status = "active";
|
|
2770
|
+
state.session.expiresAt = new Date(Date.now() + CALL_MAX_TTL_MS).toISOString();
|
|
2771
|
+
this.upsertCallParticipant(state.session, {
|
|
2772
|
+
acceptedAt: now,
|
|
2773
|
+
deviceID: this.getDevice().deviceID,
|
|
2774
|
+
joinedAt: now,
|
|
2775
|
+
state: "accepted",
|
|
2776
|
+
userID: this.getUser().userID,
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
else if (action === "cancel" ||
|
|
2780
|
+
action === "end" ||
|
|
2781
|
+
action === "hangup" ||
|
|
2782
|
+
action === "reject" ||
|
|
2783
|
+
action === "timeout") {
|
|
2784
|
+
state.session.status = "ended";
|
|
2785
|
+
state.session.endedAt = now;
|
|
2786
|
+
this.upsertCallParticipant(state.session, {
|
|
2787
|
+
leftAt: now,
|
|
2788
|
+
state: action === "reject" ? "rejected" : "left",
|
|
2789
|
+
userID: this.getUser().userID,
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
const event = {
|
|
2793
|
+
action,
|
|
2794
|
+
call: cloneCallSession(state.session),
|
|
2795
|
+
fromDeviceID: this.getDevice().deviceID,
|
|
2796
|
+
fromUserID: this.getUser().userID,
|
|
2797
|
+
...(signal ? { signal } : {}),
|
|
2798
|
+
};
|
|
2799
|
+
if (state.session.status === "ended") {
|
|
2800
|
+
this.callStates.delete(state.session.callID);
|
|
2801
|
+
}
|
|
2802
|
+
else {
|
|
2803
|
+
this.callStates.set(state.session.callID, state);
|
|
2804
|
+
}
|
|
2805
|
+
return event;
|
|
2806
|
+
}
|
|
2473
2807
|
async markSessionVerified(sessionID) {
|
|
2474
2808
|
return this.database.markSessionVerified(sessionID);
|
|
2475
2809
|
}
|
|
@@ -2652,6 +2986,29 @@ export class Client {
|
|
|
2652
2986
|
}
|
|
2653
2987
|
}
|
|
2654
2988
|
}
|
|
2989
|
+
async processDecryptedCallEnvelope(args) {
|
|
2990
|
+
const body = args.envelope.body;
|
|
2991
|
+
if (body.fromDeviceID !== args.mail.sender ||
|
|
2992
|
+
body.fromUserID !== args.mail.authorID ||
|
|
2993
|
+
body.toDeviceID !== args.mail.recipient ||
|
|
2994
|
+
body.toUserID !== args.mail.readerID ||
|
|
2995
|
+
body.toDeviceID !== this.getDevice().deviceID ||
|
|
2996
|
+
body.toUserID !== this.getUser().userID) {
|
|
2997
|
+
return null;
|
|
2998
|
+
}
|
|
2999
|
+
const senderDevice = await this.getDeviceByID(body.fromDeviceID);
|
|
3000
|
+
if (!senderDevice || senderDevice.owner !== body.fromUserID) {
|
|
3001
|
+
return null;
|
|
3002
|
+
}
|
|
3003
|
+
const opened = await xSignOpenAsync(XUtils.decodeHex(args.envelope.signed), XUtils.decodeHex(senderDevice.signKey));
|
|
3004
|
+
if (!opened) {
|
|
3005
|
+
return null;
|
|
3006
|
+
}
|
|
3007
|
+
if (!XUtils.bytesEqual(opened, canonicalJsonBytes(body))) {
|
|
3008
|
+
return null;
|
|
3009
|
+
}
|
|
3010
|
+
return this.applyCallEnvelopeBody(body);
|
|
3011
|
+
}
|
|
2655
3012
|
async publishPendingDeviceRegistration(args) {
|
|
2656
3013
|
const signed = await this.signPendingRegistrationChallenge(args.challenge);
|
|
2657
3014
|
await this.http.post(this.getHost() +
|
|
@@ -2904,39 +3261,52 @@ export class Client {
|
|
|
2904
3261
|
if (!mail.forward) {
|
|
2905
3262
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
2906
3263
|
}
|
|
2907
|
-
const
|
|
3264
|
+
const callEnvelope = mail.forward
|
|
2908
3265
|
? null
|
|
2909
|
-
:
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
3266
|
+
: decodeCallEnvelopePlaintext(plaintext);
|
|
3267
|
+
if (callEnvelope) {
|
|
3268
|
+
const event = await this.processDecryptedCallEnvelope({
|
|
3269
|
+
envelope: callEnvelope,
|
|
3270
|
+
mail,
|
|
3271
|
+
});
|
|
3272
|
+
if (event) {
|
|
3273
|
+
this.emitter.emit("call", event);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
else {
|
|
3277
|
+
const decodedPlaintext = mail.forward
|
|
3278
|
+
? null
|
|
3279
|
+
: decodeMessagePlaintext(plaintext);
|
|
3280
|
+
const fwdMsg1 = mail.forward
|
|
3281
|
+
? messageSchema.parse(msgpack.decode(unsealed))
|
|
3282
|
+
: null;
|
|
3283
|
+
const message = fwdMsg1
|
|
3284
|
+
? {
|
|
3285
|
+
...normalizeForwardedMessage(fwdMsg1),
|
|
3286
|
+
forward: true,
|
|
3287
|
+
}
|
|
3288
|
+
: {
|
|
3289
|
+
authorID: mail.authorID,
|
|
3290
|
+
decrypted: true,
|
|
3291
|
+
direction: "incoming",
|
|
3292
|
+
forward: mail.forward,
|
|
3293
|
+
group: mail.group
|
|
3294
|
+
? uuid.stringify(mail.group)
|
|
3295
|
+
: null,
|
|
3296
|
+
mailID: mail.mailID,
|
|
3297
|
+
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
3298
|
+
message: plaintext,
|
|
3299
|
+
}),
|
|
3300
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3301
|
+
readerID: mail.readerID,
|
|
3302
|
+
recipient: mail.recipient,
|
|
3303
|
+
sender: mail.sender,
|
|
3304
|
+
timestamp: timestamp,
|
|
3305
|
+
};
|
|
3306
|
+
const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
|
|
3307
|
+
if (shouldEmitIncomingInitial) {
|
|
3308
|
+
this.emitter.emit("message", message);
|
|
2918
3309
|
}
|
|
2919
|
-
: {
|
|
2920
|
-
authorID: mail.authorID,
|
|
2921
|
-
decrypted: true,
|
|
2922
|
-
direction: "incoming",
|
|
2923
|
-
forward: mail.forward,
|
|
2924
|
-
group: mail.group
|
|
2925
|
-
? uuid.stringify(mail.group)
|
|
2926
|
-
: null,
|
|
2927
|
-
mailID: mail.mailID,
|
|
2928
|
-
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
2929
|
-
message: plaintext,
|
|
2930
|
-
}),
|
|
2931
|
-
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
2932
|
-
readerID: mail.readerID,
|
|
2933
|
-
recipient: mail.recipient,
|
|
2934
|
-
sender: mail.sender,
|
|
2935
|
-
timestamp: timestamp,
|
|
2936
|
-
};
|
|
2937
|
-
const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
|
|
2938
|
-
if (shouldEmitIncomingInitial) {
|
|
2939
|
-
this.emitter.emit("message", message);
|
|
2940
3310
|
}
|
|
2941
3311
|
if (libvexDebugDmEnabled()) {
|
|
2942
3312
|
try {
|
|
@@ -3102,33 +3472,47 @@ export class Client {
|
|
|
3102
3472
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
3103
3473
|
: null;
|
|
3104
3474
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
3105
|
-
const
|
|
3475
|
+
const callEnvelope = mail.forward
|
|
3106
3476
|
? null
|
|
3107
|
-
:
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3477
|
+
: decodeCallEnvelopePlaintext(rawIncoming);
|
|
3478
|
+
if (callEnvelope) {
|
|
3479
|
+
const event = await this.processDecryptedCallEnvelope({
|
|
3480
|
+
envelope: callEnvelope,
|
|
3481
|
+
mail,
|
|
3482
|
+
});
|
|
3483
|
+
if (event) {
|
|
3484
|
+
this.emitter.emit("call", event);
|
|
3112
3485
|
}
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3486
|
+
}
|
|
3487
|
+
else {
|
|
3488
|
+
const decodedPlaintext = mail.forward
|
|
3489
|
+
? null
|
|
3490
|
+
: decodeMessagePlaintext(rawIncoming);
|
|
3491
|
+
const message = fwdMsg2
|
|
3492
|
+
? {
|
|
3493
|
+
...normalizeForwardedMessage(fwdMsg2),
|
|
3494
|
+
forward: true,
|
|
3495
|
+
}
|
|
3496
|
+
: {
|
|
3497
|
+
authorID: mail.authorID,
|
|
3498
|
+
decrypted: true,
|
|
3499
|
+
direction: "incoming",
|
|
3500
|
+
forward: mail.forward,
|
|
3501
|
+
group: mail.group
|
|
3502
|
+
? uuid.stringify(mail.group)
|
|
3503
|
+
: null,
|
|
3504
|
+
mailID: mail.mailID,
|
|
3505
|
+
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
3506
|
+
message: rawIncoming,
|
|
3507
|
+
}),
|
|
3508
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3509
|
+
readerID: mail.readerID,
|
|
3510
|
+
recipient: mail.recipient,
|
|
3511
|
+
sender: mail.sender,
|
|
3512
|
+
timestamp: timestamp,
|
|
3513
|
+
};
|
|
3514
|
+
this.emitter.emit("message", message);
|
|
3515
|
+
}
|
|
3132
3516
|
const sqlPatch = sessionToSqlPatch(session);
|
|
3133
3517
|
const persisted = {
|
|
3134
3518
|
CKr: sqlPatch.CKr,
|
|
@@ -3466,68 +3850,44 @@ export class Client {
|
|
|
3466
3850
|
throw err;
|
|
3467
3851
|
}
|
|
3468
3852
|
}
|
|
3469
|
-
async
|
|
3470
|
-
const
|
|
3853
|
+
async sendCallEnvelopeMail(args) {
|
|
3854
|
+
const envelope = await this.callEnvelopeForBody(args.body);
|
|
3855
|
+
await this.sendMailWithRecovery(args.targetDevice, args.targetUser, encodeCallEnvelopePlaintext(envelope), null, args.mailID, false, false, args.notify);
|
|
3856
|
+
}
|
|
3857
|
+
async sendEncryptedCallAction(action, callID, signal) {
|
|
3858
|
+
const state = this.callStates.get(callID);
|
|
3859
|
+
if (!state) {
|
|
3860
|
+
throw new Error("Unknown encrypted call: " + callID);
|
|
3861
|
+
}
|
|
3862
|
+
const targets = await this.callTargetsForState(state);
|
|
3863
|
+
if (targets.length === 0) {
|
|
3864
|
+
throw new Error("Call has no reachable peer devices.");
|
|
3865
|
+
}
|
|
3866
|
+
const targetUser = await this.fetchUserOrThrow(state.peerUserID);
|
|
3867
|
+
const sequence = state.sequence + 1;
|
|
3868
|
+
state.sequence = sequence;
|
|
3869
|
+
const expiresAt = action === "accept"
|
|
3870
|
+
? new Date(Date.now() + CALL_MAX_TTL_MS).toISOString()
|
|
3871
|
+
: state.session.expiresAt;
|
|
3872
|
+
const bodies = targets.map((target) => this.makeCallEnvelopeBody({
|
|
3471
3873
|
action,
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
}
|
|
3484
|
-
if (!event) {
|
|
3485
|
-
reject(new Error("Call signaling response was empty."));
|
|
3486
|
-
return;
|
|
3487
|
-
}
|
|
3488
|
-
resolve(event);
|
|
3489
|
-
};
|
|
3490
|
-
const callback = (packedMsg) => {
|
|
3491
|
-
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
3492
|
-
if (receivedMsg.transmissionID !== msg.transmissionID) {
|
|
3493
|
-
return;
|
|
3494
|
-
}
|
|
3495
|
-
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
3496
|
-
if (!parsed.success) {
|
|
3497
|
-
settle("Call signaling failed: " + JSON.stringify(receivedMsg));
|
|
3498
|
-
return;
|
|
3499
|
-
}
|
|
3500
|
-
if (parsed.data.type === "success") {
|
|
3501
|
-
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
3502
|
-
if (!event.success) {
|
|
3503
|
-
settle("Invalid call signaling response: " +
|
|
3504
|
-
JSON.stringify(event.error.issues));
|
|
3505
|
-
return;
|
|
3506
|
-
}
|
|
3507
|
-
settle(null, event.data);
|
|
3508
|
-
return;
|
|
3509
|
-
}
|
|
3510
|
-
if (parsed.data.type === "error") {
|
|
3511
|
-
settle(new Error(parsed.data.error));
|
|
3512
|
-
return;
|
|
3513
|
-
}
|
|
3514
|
-
if (parsed.data.type === "notify" &&
|
|
3515
|
-
(parsed.data.event === "call" ||
|
|
3516
|
-
parsed.data.event === "callInvite")) {
|
|
3517
|
-
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
3518
|
-
if (event.success) {
|
|
3519
|
-
settle(null, event.data);
|
|
3520
|
-
}
|
|
3521
|
-
return;
|
|
3522
|
-
}
|
|
3523
|
-
settle("Unexpected call signaling response: " +
|
|
3524
|
-
JSON.stringify(parsed.data));
|
|
3525
|
-
};
|
|
3526
|
-
this.socket.on("message", callback);
|
|
3527
|
-
this.send(msg).catch((err) => {
|
|
3528
|
-
settle(err);
|
|
3529
|
-
});
|
|
3874
|
+
expiresAt,
|
|
3875
|
+
sequence,
|
|
3876
|
+
signal,
|
|
3877
|
+
state,
|
|
3878
|
+
toDeviceID: target.deviceID,
|
|
3879
|
+
toUserID: target.owner,
|
|
3880
|
+
}));
|
|
3881
|
+
await this.deliverCallEnvelopeBatch({
|
|
3882
|
+
bodies,
|
|
3883
|
+
mailID: uuid.v4(),
|
|
3884
|
+
targetUser,
|
|
3530
3885
|
});
|
|
3886
|
+
if (targets.length === 1) {
|
|
3887
|
+
state.peerDeviceID = targets[0]?.deviceID;
|
|
3888
|
+
}
|
|
3889
|
+
state.session.expiresAt = expiresAt;
|
|
3890
|
+
return this.markLocalCallAction(state, action, signal);
|
|
3531
3891
|
}
|
|
3532
3892
|
async sendGroupMessage(channelID, message, opts) {
|
|
3533
3893
|
const userList = await this.getUserList(channelID);
|
|
@@ -3616,7 +3976,7 @@ export class Client {
|
|
|
3616
3976
|
}
|
|
3617
3977
|
}
|
|
3618
3978
|
/* Sends encrypted mail to a user. */
|
|
3619
|
-
async sendMail(device, user, msg, group, mailID, forward, retry = false) {
|
|
3979
|
+
async sendMail(device, user, msg, group, mailID, forward, retry = false, notify) {
|
|
3620
3980
|
while (this.sending.has(device.deviceID)) {
|
|
3621
3981
|
await sleep(100);
|
|
3622
3982
|
}
|
|
@@ -3631,7 +3991,7 @@ export class Client {
|
|
|
3631
3991
|
retry: String(retry),
|
|
3632
3992
|
});
|
|
3633
3993
|
}
|
|
3634
|
-
const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false);
|
|
3994
|
+
const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false, notify);
|
|
3635
3995
|
if (libvexDebugDmEnabled()) {
|
|
3636
3996
|
debugLibvexDm("sendMail: createSession returned", {
|
|
3637
3997
|
peerDevice: device.deviceID,
|
|
@@ -3670,34 +4030,43 @@ export class Client {
|
|
|
3670
4030
|
recipient: device.deviceID,
|
|
3671
4031
|
sender: this.getDevice().deviceID,
|
|
3672
4032
|
};
|
|
4033
|
+
const wireMail = notify ? { ...mail, notify } : mail;
|
|
3673
4034
|
const msgb = {
|
|
3674
4035
|
action: "CREATE",
|
|
3675
|
-
data:
|
|
4036
|
+
data: wireMail,
|
|
3676
4037
|
resourceType: "mail",
|
|
3677
4038
|
transmissionID: uuid.v4(),
|
|
3678
4039
|
type: "resource",
|
|
3679
4040
|
};
|
|
3680
4041
|
const hmac = xHMAC(mail, messageKey);
|
|
4042
|
+
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
|
|
4043
|
+
const callEnvelope = forward
|
|
4044
|
+
? null
|
|
4045
|
+
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
3681
4046
|
const fwdOut = forward
|
|
3682
4047
|
? messageSchema.parse(msgpack.decode(msg))
|
|
3683
4048
|
: null;
|
|
3684
4049
|
const outMsg = fwdOut
|
|
3685
4050
|
? { ...normalizeForwardedMessage(fwdOut), forward: true }
|
|
3686
|
-
:
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
4051
|
+
: callEnvelope
|
|
4052
|
+
? null
|
|
4053
|
+
: {
|
|
4054
|
+
authorID: mail.authorID,
|
|
4055
|
+
decrypted: true,
|
|
4056
|
+
direction: "outgoing",
|
|
4057
|
+
forward: mail.forward,
|
|
4058
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
4059
|
+
mailID: mail.mailID,
|
|
4060
|
+
...messageFromDecodedPlaintext(decodeMessagePlaintext(rawPlaintext)),
|
|
4061
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
4062
|
+
readerID: mail.readerID,
|
|
4063
|
+
recipient: mail.recipient,
|
|
4064
|
+
sender: mail.sender,
|
|
4065
|
+
timestamp: new Date().toISOString(),
|
|
4066
|
+
};
|
|
4067
|
+
if (outMsg) {
|
|
4068
|
+
this.emitter.emit("message", outMsg);
|
|
4069
|
+
}
|
|
3701
4070
|
const sqlPatch = sessionToSqlPatch(session);
|
|
3702
4071
|
const persisted = {
|
|
3703
4072
|
CKr: sqlPatch.CKr,
|
|
@@ -3722,22 +4091,22 @@ export class Client {
|
|
|
3722
4091
|
};
|
|
3723
4092
|
await this.database.saveSession(persisted);
|
|
3724
4093
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
3725
|
-
await this.deliverMailResource(msgb, hmac,
|
|
4094
|
+
await this.deliverMailResource(msgb, hmac, wireMail);
|
|
3726
4095
|
return outMsg;
|
|
3727
4096
|
}
|
|
3728
4097
|
finally {
|
|
3729
4098
|
this.sending.delete(device.deviceID);
|
|
3730
4099
|
}
|
|
3731
4100
|
}
|
|
3732
|
-
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false) {
|
|
4101
|
+
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false, notify) {
|
|
3733
4102
|
try {
|
|
3734
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession);
|
|
4103
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession, notify);
|
|
3735
4104
|
}
|
|
3736
4105
|
catch (err) {
|
|
3737
4106
|
if (!this.shouldRetryDeliveryWithFreshSession(err)) {
|
|
3738
4107
|
throw err;
|
|
3739
4108
|
}
|
|
3740
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, true);
|
|
4109
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, true, notify);
|
|
3741
4110
|
}
|
|
3742
4111
|
}
|
|
3743
4112
|
async sendMessage(userID, message, opts) {
|
|
@@ -3872,6 +4241,32 @@ export class Client {
|
|
|
3872
4241
|
// Don't surface a teardown race as an unhandled rejection.
|
|
3873
4242
|
this.send(receipt).catch(ignoreSocketTeardown);
|
|
3874
4243
|
}
|
|
4244
|
+
sessionFromCallEnvelope(body) {
|
|
4245
|
+
const session = {
|
|
4246
|
+
callID: body.callID,
|
|
4247
|
+
conversationID: body.conversationID,
|
|
4248
|
+
conversationType: body.conversationType,
|
|
4249
|
+
createdAt: body.createdAt,
|
|
4250
|
+
createdBy: body.createdBy,
|
|
4251
|
+
createdByDeviceID: body.createdByDeviceID,
|
|
4252
|
+
expiresAt: body.expiresAt,
|
|
4253
|
+
media: "audio",
|
|
4254
|
+
participants: [],
|
|
4255
|
+
status: body.action === "invite" ? "ringing" : "active",
|
|
4256
|
+
};
|
|
4257
|
+
this.upsertCallParticipant(session, {
|
|
4258
|
+
acceptedAt: body.createdAt,
|
|
4259
|
+
deviceID: body.createdByDeviceID,
|
|
4260
|
+
joinedAt: body.createdAt,
|
|
4261
|
+
state: "accepted",
|
|
4262
|
+
userID: body.createdBy,
|
|
4263
|
+
});
|
|
4264
|
+
this.upsertCallParticipant(session, {
|
|
4265
|
+
state: "ringing",
|
|
4266
|
+
userID: body.conversationID,
|
|
4267
|
+
});
|
|
4268
|
+
return session;
|
|
4269
|
+
}
|
|
3875
4270
|
setAlive(status) {
|
|
3876
4271
|
this.isAlive = status;
|
|
3877
4272
|
}
|
|
@@ -3904,6 +4299,66 @@ export class Client {
|
|
|
3904
4299
|
async signPendingRegistrationChallenge(challengeHex) {
|
|
3905
4300
|
return XUtils.encodeHex(await xSignAsync(XUtils.decodeHex(challengeHex), this.signKeys.secretKey));
|
|
3906
4301
|
}
|
|
4302
|
+
async startEncryptedDmCall(recipientUserID, signal) {
|
|
4303
|
+
const { devices, user } = await this.fetchCallPeer({
|
|
4304
|
+
userID: recipientUserID,
|
|
4305
|
+
});
|
|
4306
|
+
const now = new Date();
|
|
4307
|
+
const createdAt = now.toISOString();
|
|
4308
|
+
const expiresAt = new Date(now.getTime() + CALL_INVITE_TTL_MS).toISOString();
|
|
4309
|
+
const session = {
|
|
4310
|
+
callID: uuid.v4(),
|
|
4311
|
+
conversationID: recipientUserID,
|
|
4312
|
+
conversationType: "dm",
|
|
4313
|
+
createdAt,
|
|
4314
|
+
createdBy: this.getUser().userID,
|
|
4315
|
+
createdByDeviceID: this.getDevice().deviceID,
|
|
4316
|
+
expiresAt,
|
|
4317
|
+
media: "audio",
|
|
4318
|
+
participants: [
|
|
4319
|
+
{
|
|
4320
|
+
acceptedAt: createdAt,
|
|
4321
|
+
deviceID: this.getDevice().deviceID,
|
|
4322
|
+
joinedAt: createdAt,
|
|
4323
|
+
state: "accepted",
|
|
4324
|
+
userID: this.getUser().userID,
|
|
4325
|
+
},
|
|
4326
|
+
{
|
|
4327
|
+
state: "ringing",
|
|
4328
|
+
userID: recipientUserID,
|
|
4329
|
+
},
|
|
4330
|
+
],
|
|
4331
|
+
status: "ringing",
|
|
4332
|
+
};
|
|
4333
|
+
const state = {
|
|
4334
|
+
peerUserID: recipientUserID,
|
|
4335
|
+
pendingPeerDevices: devices,
|
|
4336
|
+
sequence: 1,
|
|
4337
|
+
session,
|
|
4338
|
+
};
|
|
4339
|
+
this.callStates.set(session.callID, state);
|
|
4340
|
+
const bodies = devices.map((device) => this.makeCallEnvelopeBody({
|
|
4341
|
+
action: "invite",
|
|
4342
|
+
expiresAt,
|
|
4343
|
+
sequence: state.sequence,
|
|
4344
|
+
signal,
|
|
4345
|
+
state,
|
|
4346
|
+
toDeviceID: device.deviceID,
|
|
4347
|
+
toUserID: recipientUserID,
|
|
4348
|
+
}));
|
|
4349
|
+
await this.deliverCallEnvelopeBatch({
|
|
4350
|
+
bodies,
|
|
4351
|
+
mailID: uuid.v4(),
|
|
4352
|
+
targetUser: user,
|
|
4353
|
+
});
|
|
4354
|
+
return {
|
|
4355
|
+
action: "invite",
|
|
4356
|
+
call: cloneCallSession(session),
|
|
4357
|
+
fromDeviceID: this.getDevice().deviceID,
|
|
4358
|
+
fromUserID: this.getUser().userID,
|
|
4359
|
+
...(signal ? { signal } : {}),
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
3907
4362
|
async submitOTK(amount) {
|
|
3908
4363
|
const otks = [];
|
|
3909
4364
|
for (let i = 0; i < amount; i++) {
|
|
@@ -4001,5 +4456,13 @@ export class Client {
|
|
|
4001
4456
|
return null;
|
|
4002
4457
|
}
|
|
4003
4458
|
}
|
|
4459
|
+
upsertCallParticipant(session, patch) {
|
|
4460
|
+
const existing = session.participants.find((participant) => participant.userID === patch.userID);
|
|
4461
|
+
if (!existing) {
|
|
4462
|
+
session.participants.push({ ...patch });
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
Object.assign(existing, patch);
|
|
4466
|
+
}
|
|
4004
4467
|
}
|
|
4005
4468
|
//# sourceMappingURL=Client.js.map
|