@vex-chat/libvex 7.1.6 → 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 +57 -3
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +683 -101
- package/dist/Client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +915 -118
- package/src/__tests__/harness/shared-suite.ts +80 -1
- package/src/index.ts +11 -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 { MailType, MailWSSchema, PermissionSchema, WSMessageSchema, } from "@vex-chat/types";
|
|
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";
|
|
@@ -199,8 +199,13 @@ import { ActionTokenCodec, AuthResponseCodec, ChannelArrayCodec, ChannelCodec, C
|
|
|
199
199
|
import { sqlSessionToCrypto } from "./utils/sqlSessionToCrypto.js";
|
|
200
200
|
import { uuidToUint8 } from "./utils/uint8uuid.js";
|
|
201
201
|
const _protocolMsgRegex = /��\w+:\w+��/g;
|
|
202
|
+
const NotificationSubscriptionChannelSchema = z.enum([
|
|
203
|
+
"apnsVoip",
|
|
204
|
+
"expo",
|
|
205
|
+
"fcmCall",
|
|
206
|
+
]);
|
|
202
207
|
const NotificationSubscriptionSchema = z.object({
|
|
203
|
-
channel:
|
|
208
|
+
channel: NotificationSubscriptionChannelSchema,
|
|
204
209
|
createdAt: z.string(),
|
|
205
210
|
deviceID: z.string(),
|
|
206
211
|
enabled: z.boolean(),
|
|
@@ -268,6 +273,9 @@ const messageSchema = z.object({
|
|
|
268
273
|
sender: z.string(),
|
|
269
274
|
timestamp: z.string(),
|
|
270
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;
|
|
271
279
|
const MESSAGE_BLOB_PREFIX = "vex-message:1\n";
|
|
272
280
|
const MAIL_FANOUT_CONCURRENCY = 8;
|
|
273
281
|
const MAIL_BATCH_MAX_SIZE = 32;
|
|
@@ -282,6 +290,52 @@ const mailBatchResponseSchema = z.object({
|
|
|
282
290
|
status: z.number().int().optional(),
|
|
283
291
|
})),
|
|
284
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
|
+
}
|
|
285
339
|
function decodeMessageBlob(body) {
|
|
286
340
|
if (!body.startsWith(MESSAGE_BLOB_PREFIX)) {
|
|
287
341
|
return { message: body };
|
|
@@ -315,6 +369,9 @@ function decodeMessagePlaintext(plaintext) {
|
|
|
315
369
|
}
|
|
316
370
|
: blob;
|
|
317
371
|
}
|
|
372
|
+
function encodeCallEnvelopePlaintext(envelope) {
|
|
373
|
+
return XUtils.decodeUTF8(CALL_ENVELOPE_PREFIX + JSON.stringify(envelope));
|
|
374
|
+
}
|
|
318
375
|
function encodeMessagePlaintext(message, opts) {
|
|
319
376
|
const body = opts?.extra === undefined
|
|
320
377
|
? message
|
|
@@ -325,6 +382,12 @@ function encodeMessagePlaintext(message, opts) {
|
|
|
325
382
|
});
|
|
326
383
|
return formatVexRetentionEnvelope(body, opts?.retentionHintDays);
|
|
327
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
|
+
}
|
|
328
391
|
function messageFromDecodedPlaintext(decoded) {
|
|
329
392
|
return {
|
|
330
393
|
...(decoded.extra !== undefined ? { extra: decoded.extra } : {}),
|
|
@@ -334,6 +397,9 @@ function messageFromDecodedPlaintext(decoded) {
|
|
|
334
397
|
: {}),
|
|
335
398
|
};
|
|
336
399
|
}
|
|
400
|
+
function normalizeCallEnvelopeBodyForWire(body) {
|
|
401
|
+
return CallEnvelopeBodySchema.parse(jsonWireValue(body));
|
|
402
|
+
}
|
|
337
403
|
function normalizeForwardedMessage(message) {
|
|
338
404
|
const decoded = decodeMessagePlaintext(message.message);
|
|
339
405
|
return {
|
|
@@ -380,6 +446,23 @@ export class Client {
|
|
|
380
446
|
static encryptKeyData = XUtils.encryptKeyData;
|
|
381
447
|
static encryptKeyDataAsync = XUtils.encryptKeyDataAsync;
|
|
382
448
|
static NOT_FOUND_TTL = 30 * 60 * 1000;
|
|
449
|
+
/**
|
|
450
|
+
* Voice-call signaling operations.
|
|
451
|
+
*
|
|
452
|
+
* Platform apps own native media capture/WebRTC. These methods only move
|
|
453
|
+
* authenticated signaling and call state over Spire.
|
|
454
|
+
*/
|
|
455
|
+
calls = {
|
|
456
|
+
accept: (callID, signal) => this.sendEncryptedCallAction("accept", callID, signal),
|
|
457
|
+
active: this.fetchActiveCalls.bind(this),
|
|
458
|
+
cancel: (callID) => this.sendEncryptedCallAction("cancel", callID),
|
|
459
|
+
hangup: (callID) => this.sendEncryptedCallAction("hangup", callID),
|
|
460
|
+
ice: (callID, signal) => this.sendEncryptedCallAction("ice", callID, signal),
|
|
461
|
+
iceServers: this.fetchIceServers.bind(this),
|
|
462
|
+
reject: (callID) => this.sendEncryptedCallAction("reject", callID),
|
|
463
|
+
signal: (callID, signal) => this.sendEncryptedCallAction("signal", callID, signal),
|
|
464
|
+
startDM: (recipientUserID, signal) => this.startEncryptedDmCall(recipientUserID, signal),
|
|
465
|
+
};
|
|
383
466
|
/**
|
|
384
467
|
* Browser-safe NODE_ENV accessor.
|
|
385
468
|
* Uses indirect lookup so the bare `process` global never appears in
|
|
@@ -664,6 +747,7 @@ export class Client {
|
|
|
664
747
|
retrieve: this.fetchUser.bind(this),
|
|
665
748
|
};
|
|
666
749
|
autoReconnectEnabled = false;
|
|
750
|
+
callStates = new Map();
|
|
667
751
|
cryptoProfile;
|
|
668
752
|
database;
|
|
669
753
|
dbPath;
|
|
@@ -1356,6 +1440,86 @@ export class Client {
|
|
|
1356
1440
|
this.emitUndecryptedMessage(mail, timestamp);
|
|
1357
1441
|
this.acknowledgeInboundMail(mail);
|
|
1358
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
|
+
}
|
|
1359
1523
|
async approveDeviceRequest(requestID) {
|
|
1360
1524
|
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
1361
1525
|
if (!req) {
|
|
@@ -1392,6 +1556,35 @@ export class Client {
|
|
|
1392
1556
|
"/passkeys/register/begin", msgpack.encode({ name: args.name, signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1393
1557
|
return decodeHttpResponse(PasskeyOptionsCodec, response.data);
|
|
1394
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
|
+
}
|
|
1395
1588
|
censorPreKey(preKey) {
|
|
1396
1589
|
if (!preKey.index) {
|
|
1397
1590
|
throw new Error("Key index is required.");
|
|
@@ -1494,7 +1687,7 @@ export class Client {
|
|
|
1494
1687
|
* When `readMail` triggers a best-effort session re-establish, key-bundle
|
|
1495
1688
|
* errors should not reject the full read pipeline.
|
|
1496
1689
|
*/
|
|
1497
|
-
allowKeyBundleFailure = false) {
|
|
1690
|
+
allowKeyBundleFailure = false, notify) {
|
|
1498
1691
|
return this.runWithThisCryptoProfile(async () => {
|
|
1499
1692
|
let keyBundle;
|
|
1500
1693
|
try {
|
|
@@ -1575,10 +1768,11 @@ export class Client {
|
|
|
1575
1768
|
recipient: device.deviceID,
|
|
1576
1769
|
sender: this.getDevice().deviceID,
|
|
1577
1770
|
};
|
|
1771
|
+
const wireMail = notify ? { ...mail, notify } : mail;
|
|
1578
1772
|
const hmac = xHMAC(mail, SK);
|
|
1579
1773
|
const msg = {
|
|
1580
1774
|
action: "CREATE",
|
|
1581
|
-
data:
|
|
1775
|
+
data: wireMail,
|
|
1582
1776
|
resourceType: "mail",
|
|
1583
1777
|
transmissionID: uuid.v4(),
|
|
1584
1778
|
type: "resource",
|
|
@@ -1598,32 +1792,38 @@ export class Client {
|
|
|
1598
1792
|
};
|
|
1599
1793
|
await this.database.saveSession(sessionEntry);
|
|
1600
1794
|
this.emitter.emit("session", sessionEntry, user);
|
|
1601
|
-
|
|
1795
|
+
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(message);
|
|
1796
|
+
const callEnvelope = forward
|
|
1797
|
+
? null
|
|
1798
|
+
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
1602
1799
|
const forwardedMsg = forward
|
|
1603
1800
|
? messageSchema.parse(msgpack.decode(message))
|
|
1604
1801
|
: null;
|
|
1605
|
-
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
1606
1802
|
const emitMsg = forwardedMsg
|
|
1607
1803
|
? { ...normalizeForwardedMessage(forwardedMsg), forward: true }
|
|
1608
|
-
:
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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) {
|
|
1623
1823
|
this.emitter.emit("message", emitMsg);
|
|
1624
1824
|
}
|
|
1625
|
-
await this.deliverMailResource(msg, hmac,
|
|
1626
|
-
return
|
|
1825
|
+
await this.deliverMailResource(msg, hmac, wireMail);
|
|
1826
|
+
return emitMsg;
|
|
1627
1827
|
});
|
|
1628
1828
|
}
|
|
1629
1829
|
async deleteChannel(channelID) {
|
|
@@ -1653,6 +1853,49 @@ export class Client {
|
|
|
1653
1853
|
async deleteServer(serverID) {
|
|
1654
1854
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1655
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
|
+
}
|
|
1656
1899
|
deliverMailResource(msg, header, mail) {
|
|
1657
1900
|
if (this.mailBatchUnsupported) {
|
|
1658
1901
|
return this.deliverMailResourceOverSocket(msg, header);
|
|
@@ -1725,6 +1968,59 @@ export class Client {
|
|
|
1725
1968
|
timestamp,
|
|
1726
1969
|
});
|
|
1727
1970
|
}
|
|
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 };
|
|
2015
|
+
}
|
|
2016
|
+
async fetchIceServers() {
|
|
2017
|
+
const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
|
|
2018
|
+
responseType: "json",
|
|
2019
|
+
});
|
|
2020
|
+
return z
|
|
2021
|
+
.object({ iceServers: z.array(IceServerConfigSchema) })
|
|
2022
|
+
.parse(res.data).iceServers;
|
|
2023
|
+
}
|
|
1728
2024
|
/**
|
|
1729
2025
|
* Gets a list of permissions for a server.
|
|
1730
2026
|
*
|
|
@@ -1819,6 +2115,23 @@ export class Client {
|
|
|
1819
2115
|
}
|
|
1820
2116
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
1821
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
|
+
}
|
|
1822
2135
|
/**
|
|
1823
2136
|
* Finish a passkey login and adopt the resulting JWT as the
|
|
1824
2137
|
* client's bearer token. After this call, `client.passkeys.*`
|
|
@@ -2197,6 +2510,22 @@ export class Client {
|
|
|
2197
2510
|
}
|
|
2198
2511
|
async handleNotify(msg) {
|
|
2199
2512
|
switch (msg.event) {
|
|
2513
|
+
case "call":
|
|
2514
|
+
case "callInvite": {
|
|
2515
|
+
const parsed = CallEventSchema.safeParse(msg.data);
|
|
2516
|
+
if (parsed.success) {
|
|
2517
|
+
this.emitter.emit("call", parsed.data);
|
|
2518
|
+
}
|
|
2519
|
+
break;
|
|
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
|
+
}
|
|
2200
2529
|
case "deviceRequest": {
|
|
2201
2530
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
2202
2531
|
if (parsed.success) {
|
|
@@ -2414,6 +2743,67 @@ export class Client {
|
|
|
2414
2743
|
const response = await this.http.get(this.getHost() + "/user/" + userID + "/passkeys");
|
|
2415
2744
|
return decodeHttpResponse(PasskeyArrayCodec, response.data);
|
|
2416
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
|
+
}
|
|
2417
2807
|
async markSessionVerified(sessionID) {
|
|
2418
2808
|
return this.database.markSessionVerified(sessionID);
|
|
2419
2809
|
}
|
|
@@ -2596,6 +2986,29 @@ export class Client {
|
|
|
2596
2986
|
}
|
|
2597
2987
|
}
|
|
2598
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
|
+
}
|
|
2599
3012
|
async publishPendingDeviceRegistration(args) {
|
|
2600
3013
|
const signed = await this.signPendingRegistrationChallenge(args.challenge);
|
|
2601
3014
|
await this.http.post(this.getHost() +
|
|
@@ -2848,39 +3261,52 @@ export class Client {
|
|
|
2848
3261
|
if (!mail.forward) {
|
|
2849
3262
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
2850
3263
|
}
|
|
2851
|
-
const
|
|
3264
|
+
const callEnvelope = mail.forward
|
|
2852
3265
|
? null
|
|
2853
|
-
:
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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);
|
|
2862
3309
|
}
|
|
2863
|
-
: {
|
|
2864
|
-
authorID: mail.authorID,
|
|
2865
|
-
decrypted: true,
|
|
2866
|
-
direction: "incoming",
|
|
2867
|
-
forward: mail.forward,
|
|
2868
|
-
group: mail.group
|
|
2869
|
-
? uuid.stringify(mail.group)
|
|
2870
|
-
: null,
|
|
2871
|
-
mailID: mail.mailID,
|
|
2872
|
-
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
2873
|
-
message: plaintext,
|
|
2874
|
-
}),
|
|
2875
|
-
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
2876
|
-
readerID: mail.readerID,
|
|
2877
|
-
recipient: mail.recipient,
|
|
2878
|
-
sender: mail.sender,
|
|
2879
|
-
timestamp: timestamp,
|
|
2880
|
-
};
|
|
2881
|
-
const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
|
|
2882
|
-
if (shouldEmitIncomingInitial) {
|
|
2883
|
-
this.emitter.emit("message", message);
|
|
2884
3310
|
}
|
|
2885
3311
|
if (libvexDebugDmEnabled()) {
|
|
2886
3312
|
try {
|
|
@@ -3046,33 +3472,47 @@ export class Client {
|
|
|
3046
3472
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
3047
3473
|
: null;
|
|
3048
3474
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
3049
|
-
const
|
|
3475
|
+
const callEnvelope = mail.forward
|
|
3050
3476
|
? null
|
|
3051
|
-
:
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
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);
|
|
3056
3485
|
}
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
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
|
+
}
|
|
3076
3516
|
const sqlPatch = sessionToSqlPatch(session);
|
|
3077
3517
|
const persisted = {
|
|
3078
3518
|
CKr: sqlPatch.CKr,
|
|
@@ -3410,6 +3850,45 @@ export class Client {
|
|
|
3410
3850
|
throw err;
|
|
3411
3851
|
}
|
|
3412
3852
|
}
|
|
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({
|
|
3873
|
+
action,
|
|
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,
|
|
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);
|
|
3891
|
+
}
|
|
3413
3892
|
async sendGroupMessage(channelID, message, opts) {
|
|
3414
3893
|
const userList = await this.getUserList(channelID);
|
|
3415
3894
|
for (const user of userList) {
|
|
@@ -3497,7 +3976,7 @@ export class Client {
|
|
|
3497
3976
|
}
|
|
3498
3977
|
}
|
|
3499
3978
|
/* Sends encrypted mail to a user. */
|
|
3500
|
-
async sendMail(device, user, msg, group, mailID, forward, retry = false) {
|
|
3979
|
+
async sendMail(device, user, msg, group, mailID, forward, retry = false, notify) {
|
|
3501
3980
|
while (this.sending.has(device.deviceID)) {
|
|
3502
3981
|
await sleep(100);
|
|
3503
3982
|
}
|
|
@@ -3512,7 +3991,7 @@ export class Client {
|
|
|
3512
3991
|
retry: String(retry),
|
|
3513
3992
|
});
|
|
3514
3993
|
}
|
|
3515
|
-
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);
|
|
3516
3995
|
if (libvexDebugDmEnabled()) {
|
|
3517
3996
|
debugLibvexDm("sendMail: createSession returned", {
|
|
3518
3997
|
peerDevice: device.deviceID,
|
|
@@ -3551,34 +4030,43 @@ export class Client {
|
|
|
3551
4030
|
recipient: device.deviceID,
|
|
3552
4031
|
sender: this.getDevice().deviceID,
|
|
3553
4032
|
};
|
|
4033
|
+
const wireMail = notify ? { ...mail, notify } : mail;
|
|
3554
4034
|
const msgb = {
|
|
3555
4035
|
action: "CREATE",
|
|
3556
|
-
data:
|
|
4036
|
+
data: wireMail,
|
|
3557
4037
|
resourceType: "mail",
|
|
3558
4038
|
transmissionID: uuid.v4(),
|
|
3559
4039
|
type: "resource",
|
|
3560
4040
|
};
|
|
3561
4041
|
const hmac = xHMAC(mail, messageKey);
|
|
4042
|
+
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
|
|
4043
|
+
const callEnvelope = forward
|
|
4044
|
+
? null
|
|
4045
|
+
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
3562
4046
|
const fwdOut = forward
|
|
3563
4047
|
? messageSchema.parse(msgpack.decode(msg))
|
|
3564
4048
|
: null;
|
|
3565
4049
|
const outMsg = fwdOut
|
|
3566
4050
|
? { ...normalizeForwardedMessage(fwdOut), forward: true }
|
|
3567
|
-
:
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
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
|
+
}
|
|
3582
4070
|
const sqlPatch = sessionToSqlPatch(session);
|
|
3583
4071
|
const persisted = {
|
|
3584
4072
|
CKr: sqlPatch.CKr,
|
|
@@ -3603,22 +4091,22 @@ export class Client {
|
|
|
3603
4091
|
};
|
|
3604
4092
|
await this.database.saveSession(persisted);
|
|
3605
4093
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
3606
|
-
await this.deliverMailResource(msgb, hmac,
|
|
4094
|
+
await this.deliverMailResource(msgb, hmac, wireMail);
|
|
3607
4095
|
return outMsg;
|
|
3608
4096
|
}
|
|
3609
4097
|
finally {
|
|
3610
4098
|
this.sending.delete(device.deviceID);
|
|
3611
4099
|
}
|
|
3612
4100
|
}
|
|
3613
|
-
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false) {
|
|
4101
|
+
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false, notify) {
|
|
3614
4102
|
try {
|
|
3615
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession);
|
|
4103
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession, notify);
|
|
3616
4104
|
}
|
|
3617
4105
|
catch (err) {
|
|
3618
4106
|
if (!this.shouldRetryDeliveryWithFreshSession(err)) {
|
|
3619
4107
|
throw err;
|
|
3620
4108
|
}
|
|
3621
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, true);
|
|
4109
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, true, notify);
|
|
3622
4110
|
}
|
|
3623
4111
|
}
|
|
3624
4112
|
async sendMessage(userID, message, opts) {
|
|
@@ -3753,6 +4241,32 @@ export class Client {
|
|
|
3753
4241
|
// Don't surface a teardown race as an unhandled rejection.
|
|
3754
4242
|
this.send(receipt).catch(ignoreSocketTeardown);
|
|
3755
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
|
+
}
|
|
3756
4270
|
setAlive(status) {
|
|
3757
4271
|
this.isAlive = status;
|
|
3758
4272
|
}
|
|
@@ -3785,6 +4299,66 @@ export class Client {
|
|
|
3785
4299
|
async signPendingRegistrationChallenge(challengeHex) {
|
|
3786
4300
|
return XUtils.encodeHex(await xSignAsync(XUtils.decodeHex(challengeHex), this.signKeys.secretKey));
|
|
3787
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
|
+
}
|
|
3788
4362
|
async submitOTK(amount) {
|
|
3789
4363
|
const otks = [];
|
|
3790
4364
|
for (let i = 0; i < amount; i++) {
|
|
@@ -3882,5 +4456,13 @@ export class Client {
|
|
|
3882
4456
|
return null;
|
|
3883
4457
|
}
|
|
3884
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
|
+
}
|
|
3885
4467
|
}
|
|
3886
4468
|
//# sourceMappingURL=Client.js.map
|