@vex-chat/libvex 7.3.0 → 7.4.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 +53 -39
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +232 -653
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +18 -0
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +4 -1
- package/dist/codecs.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 +348 -853
- package/src/__tests__/harness/shared-suite.ts +1 -80
- package/src/codecs.ts +8 -0
- package/src/index.ts +9 -0
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 { CallEventSchema, CallSessionSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, 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,6 +74,15 @@ 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
|
+
}
|
|
77
86
|
function ignoreSocketTeardown(err) {
|
|
78
87
|
if (err instanceof WebSocketNotOpenError)
|
|
79
88
|
return;
|
|
@@ -195,17 +204,12 @@ function spireErrorBodyMessage(data, max = 8_000) {
|
|
|
195
204
|
return t.length > max ? t.slice(0, max) + "…" : t;
|
|
196
205
|
}
|
|
197
206
|
import { msgpack } from "./codec.js";
|
|
198
|
-
import { ActionTokenCodec, AuthResponseCodec, ChannelArrayCodec, ChannelCodec, ConnectResponseCodec, decodeHttpResponse, DeviceArrayCodec, DeviceChallengeCodec, DeviceCodec, DeviceRegistrationResultCodec, EmojiArrayCodec, EmojiCodec, FileSQLCodec, InviteArrayCodec, InviteCodec, KeyBundleCodec, OtkCountCodec, PasskeyArrayCodec, PasskeyAuthFinishResponseCodec, PasskeyCodec, PasskeyOptionsCodec, PendingDeviceRequestArrayCodec, PendingDeviceRequestCodec, PermissionArrayCodec, PermissionCodec, RegisterPendingApprovalCodec, RegisterResponseCodec, ServerArrayCodec, ServerChannelBootstrapCodec, ServerCodec, UserArrayCodec, UserCodec, WhoamiCodec, } from "./codecs.js";
|
|
207
|
+
import { AccountEntitlementsCodec, ActionTokenCodec, AuthResponseCodec, BillingAccountStateCodec, BillingProductArrayCodec, ChannelArrayCodec, ChannelCodec, ConnectResponseCodec, decodeHttpResponse, DeviceArrayCodec, DeviceChallengeCodec, DeviceCodec, DeviceRegistrationResultCodec, EmojiArrayCodec, EmojiCodec, FileSQLCodec, InviteArrayCodec, InviteCodec, KeyBundleCodec, OtkCountCodec, PasskeyArrayCodec, PasskeyAuthFinishResponseCodec, PasskeyCodec, PasskeyOptionsCodec, PendingDeviceRequestArrayCodec, PendingDeviceRequestCodec, PermissionArrayCodec, PermissionCodec, RegisterPendingApprovalCodec, RegisterResponseCodec, ServerArrayCodec, ServerChannelBootstrapCodec, ServerCodec, UserArrayCodec, UserCodec, WhoamiCodec, } from "./codecs.js";
|
|
199
208
|
import { sqlSessionToCrypto } from "./utils/sqlSessionToCrypto.js";
|
|
200
209
|
import { uuidToUint8 } from "./utils/uint8uuid.js";
|
|
201
210
|
const _protocolMsgRegex = /��\w+:\w+��/g;
|
|
202
|
-
const NotificationSubscriptionChannelSchema = z.enum([
|
|
203
|
-
"apnsVoip",
|
|
204
|
-
"expo",
|
|
205
|
-
"fcmCall",
|
|
206
|
-
]);
|
|
207
211
|
const NotificationSubscriptionSchema = z.object({
|
|
208
|
-
channel:
|
|
212
|
+
channel: z.literal("expo"),
|
|
209
213
|
createdAt: z.string(),
|
|
210
214
|
deviceID: z.string(),
|
|
211
215
|
enabled: z.boolean(),
|
|
@@ -273,9 +277,6 @@ const messageSchema = z.object({
|
|
|
273
277
|
sender: z.string(),
|
|
274
278
|
timestamp: z.string(),
|
|
275
279
|
});
|
|
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;
|
|
279
280
|
const MESSAGE_BLOB_PREFIX = "vex-message:1\n";
|
|
280
281
|
const MAIL_FANOUT_CONCURRENCY = 8;
|
|
281
282
|
const MAIL_BATCH_MAX_SIZE = 32;
|
|
@@ -290,52 +291,6 @@ const mailBatchResponseSchema = z.object({
|
|
|
290
291
|
status: z.number().int().optional(),
|
|
291
292
|
})),
|
|
292
293
|
});
|
|
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
|
-
}
|
|
339
294
|
function decodeMessageBlob(body) {
|
|
340
295
|
if (!body.startsWith(MESSAGE_BLOB_PREFIX)) {
|
|
341
296
|
return { message: body };
|
|
@@ -369,9 +324,6 @@ function decodeMessagePlaintext(plaintext) {
|
|
|
369
324
|
}
|
|
370
325
|
: blob;
|
|
371
326
|
}
|
|
372
|
-
function encodeCallEnvelopePlaintext(envelope) {
|
|
373
|
-
return XUtils.decodeUTF8(CALL_ENVELOPE_PREFIX + JSON.stringify(envelope));
|
|
374
|
-
}
|
|
375
327
|
function encodeMessagePlaintext(message, opts) {
|
|
376
328
|
const body = opts?.extra === undefined
|
|
377
329
|
? message
|
|
@@ -382,12 +334,6 @@ function encodeMessagePlaintext(message, opts) {
|
|
|
382
334
|
});
|
|
383
335
|
return formatVexRetentionEnvelope(body, opts?.retentionHintDays);
|
|
384
336
|
}
|
|
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
|
-
}
|
|
391
337
|
function messageFromDecodedPlaintext(decoded) {
|
|
392
338
|
return {
|
|
393
339
|
...(decoded.extra !== undefined ? { extra: decoded.extra } : {}),
|
|
@@ -397,9 +343,6 @@ function messageFromDecodedPlaintext(decoded) {
|
|
|
397
343
|
: {}),
|
|
398
344
|
};
|
|
399
345
|
}
|
|
400
|
-
function normalizeCallEnvelopeBodyForWire(body) {
|
|
401
|
-
return CallEnvelopeBodySchema.parse(jsonWireValue(body));
|
|
402
|
-
}
|
|
403
346
|
function normalizeForwardedMessage(message) {
|
|
404
347
|
const decoded = decodeMessagePlaintext(message.message);
|
|
405
348
|
return {
|
|
@@ -446,6 +389,13 @@ export class Client {
|
|
|
446
389
|
static encryptKeyData = XUtils.encryptKeyData;
|
|
447
390
|
static encryptKeyDataAsync = XUtils.encryptKeyDataAsync;
|
|
448
391
|
static NOT_FOUND_TTL = 30 * 60 * 1000;
|
|
392
|
+
/** Store subscription billing methods. */
|
|
393
|
+
billing = {
|
|
394
|
+
products: this.getBillingProducts.bind(this),
|
|
395
|
+
retrieve: this.getBillingAccountState.bind(this),
|
|
396
|
+
submitAppleTransaction: this.submitAppleTransaction.bind(this),
|
|
397
|
+
submitGooglePurchase: this.submitGooglePurchase.bind(this),
|
|
398
|
+
};
|
|
449
399
|
/**
|
|
450
400
|
* Voice-call signaling operations.
|
|
451
401
|
*
|
|
@@ -453,15 +403,22 @@ export class Client {
|
|
|
453
403
|
* authenticated signaling and call state over Spire.
|
|
454
404
|
*/
|
|
455
405
|
calls = {
|
|
456
|
-
accept: (callID, signal) => this.
|
|
406
|
+
accept: (callID, signal) => this.sendCallResource("ACCEPT", {
|
|
407
|
+
callID,
|
|
408
|
+
...(signal ? { signal } : {}),
|
|
409
|
+
}),
|
|
457
410
|
active: this.fetchActiveCalls.bind(this),
|
|
458
|
-
cancel: (callID) => this.
|
|
459
|
-
hangup: (callID) => this.
|
|
460
|
-
ice: (callID, signal) => this.
|
|
411
|
+
cancel: (callID) => this.sendCallResource("CANCEL", { callID }),
|
|
412
|
+
hangup: (callID) => this.sendCallResource("HANGUP", { callID }),
|
|
413
|
+
ice: (callID, signal) => this.sendCallResource("ICE", { callID, signal }),
|
|
461
414
|
iceServers: this.fetchIceServers.bind(this),
|
|
462
|
-
reject: (callID) => this.
|
|
463
|
-
signal: (callID, signal) => this.
|
|
464
|
-
startDM: (recipientUserID, signal) => this.
|
|
415
|
+
reject: (callID) => this.sendCallResource("REJECT", { callID }),
|
|
416
|
+
signal: (callID, signal) => this.sendCallResource("SIGNAL", { callID, signal }),
|
|
417
|
+
startDM: (recipientUserID, signal) => this.sendCallResource("INVITE", {
|
|
418
|
+
conversationType: "dm",
|
|
419
|
+
recipientUserID,
|
|
420
|
+
...(signal ? { signal } : {}),
|
|
421
|
+
}),
|
|
465
422
|
};
|
|
466
423
|
/**
|
|
467
424
|
* Browser-safe NODE_ENV accessor.
|
|
@@ -535,6 +492,11 @@ export class Client {
|
|
|
535
492
|
retrieve: this.retrieveEmojiByID.bind(this),
|
|
536
493
|
retrieveList: this.retrieveEmojiList.bind(this),
|
|
537
494
|
};
|
|
495
|
+
/** Account entitlement methods. */
|
|
496
|
+
entitlements = {
|
|
497
|
+
retrieve: this.getAccountEntitlements.bind(this),
|
|
498
|
+
setDevTier: this.setDevAccountTier.bind(this),
|
|
499
|
+
};
|
|
538
500
|
/** File upload/download methods. */
|
|
539
501
|
files = {
|
|
540
502
|
/**
|
|
@@ -747,7 +709,6 @@ export class Client {
|
|
|
747
709
|
retrieve: this.fetchUser.bind(this),
|
|
748
710
|
};
|
|
749
711
|
autoReconnectEnabled = false;
|
|
750
|
-
callStates = new Map();
|
|
751
712
|
cryptoProfile;
|
|
752
713
|
database;
|
|
753
714
|
dbPath;
|
|
@@ -1440,86 +1401,6 @@ export class Client {
|
|
|
1440
1401
|
this.emitUndecryptedMessage(mail, timestamp);
|
|
1441
1402
|
this.acknowledgeInboundMail(mail);
|
|
1442
1403
|
}
|
|
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
|
-
}
|
|
1523
1404
|
async approveDeviceRequest(requestID) {
|
|
1524
1405
|
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
1525
1406
|
if (!req) {
|
|
@@ -1556,35 +1437,6 @@ export class Client {
|
|
|
1556
1437
|
"/passkeys/register/begin", msgpack.encode({ name: args.name, signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1557
1438
|
return decodeHttpResponse(PasskeyOptionsCodec, response.data);
|
|
1558
1439
|
}
|
|
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
|
-
}
|
|
1588
1440
|
censorPreKey(preKey) {
|
|
1589
1441
|
if (!preKey.index) {
|
|
1590
1442
|
throw new Error("Key index is required.");
|
|
@@ -1687,7 +1539,7 @@ export class Client {
|
|
|
1687
1539
|
* When `readMail` triggers a best-effort session re-establish, key-bundle
|
|
1688
1540
|
* errors should not reject the full read pipeline.
|
|
1689
1541
|
*/
|
|
1690
|
-
allowKeyBundleFailure = false
|
|
1542
|
+
allowKeyBundleFailure = false) {
|
|
1691
1543
|
return this.runWithThisCryptoProfile(async () => {
|
|
1692
1544
|
let keyBundle;
|
|
1693
1545
|
try {
|
|
@@ -1768,11 +1620,10 @@ export class Client {
|
|
|
1768
1620
|
recipient: device.deviceID,
|
|
1769
1621
|
sender: this.getDevice().deviceID,
|
|
1770
1622
|
};
|
|
1771
|
-
const wireMail = notify ? { ...mail, notify } : mail;
|
|
1772
1623
|
const hmac = xHMAC(mail, SK);
|
|
1773
1624
|
const msg = {
|
|
1774
1625
|
action: "CREATE",
|
|
1775
|
-
data:
|
|
1626
|
+
data: mail,
|
|
1776
1627
|
resourceType: "mail",
|
|
1777
1628
|
transmissionID: uuid.v4(),
|
|
1778
1629
|
type: "resource",
|
|
@@ -1792,38 +1643,32 @@ export class Client {
|
|
|
1792
1643
|
};
|
|
1793
1644
|
await this.database.saveSession(sessionEntry);
|
|
1794
1645
|
this.emitter.emit("session", sessionEntry, user);
|
|
1795
|
-
|
|
1796
|
-
const callEnvelope = forward
|
|
1797
|
-
? null
|
|
1798
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
1646
|
+
// emit the message
|
|
1799
1647
|
const forwardedMsg = forward
|
|
1800
1648
|
? messageSchema.parse(msgpack.decode(message))
|
|
1801
1649
|
: null;
|
|
1650
|
+
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
1802
1651
|
const emitMsg = forwardedMsg
|
|
1803
1652
|
? { ...normalizeForwardedMessage(forwardedMsg), forward: true }
|
|
1804
|
-
:
|
|
1805
|
-
|
|
1806
|
-
:
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
timestamp: new Date().toISOString(),
|
|
1820
|
-
}
|
|
1821
|
-
: null;
|
|
1822
|
-
if (emitMsg) {
|
|
1653
|
+
: {
|
|
1654
|
+
authorID: mail.authorID,
|
|
1655
|
+
decrypted: true,
|
|
1656
|
+
direction: "outgoing",
|
|
1657
|
+
forward: mail.forward,
|
|
1658
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
1659
|
+
mailID: mail.mailID,
|
|
1660
|
+
...messageFromDecodedPlaintext(decodeMessagePlaintext(XUtils.encodeUTF8(message))),
|
|
1661
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
1662
|
+
readerID: mail.readerID,
|
|
1663
|
+
recipient: mail.recipient,
|
|
1664
|
+
sender: mail.sender,
|
|
1665
|
+
timestamp: new Date().toISOString(),
|
|
1666
|
+
};
|
|
1667
|
+
if (shouldEmitHandshakeMessage) {
|
|
1823
1668
|
this.emitter.emit("message", emitMsg);
|
|
1824
1669
|
}
|
|
1825
|
-
await this.deliverMailResource(msg, hmac,
|
|
1826
|
-
return emitMsg;
|
|
1670
|
+
await this.deliverMailResource(msg, hmac, mail);
|
|
1671
|
+
return shouldEmitHandshakeMessage ? emitMsg : null;
|
|
1827
1672
|
});
|
|
1828
1673
|
}
|
|
1829
1674
|
async deleteChannel(channelID) {
|
|
@@ -1853,49 +1698,6 @@ export class Client {
|
|
|
1853
1698
|
async deleteServer(serverID) {
|
|
1854
1699
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1855
1700
|
}
|
|
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
|
-
}
|
|
1899
1701
|
deliverMailResource(msg, header, mail) {
|
|
1900
1702
|
if (this.mailBatchUnsupported) {
|
|
1901
1703
|
return this.deliverMailResourceOverSocket(msg, header);
|
|
@@ -1968,50 +1770,12 @@ export class Client {
|
|
|
1968
1770
|
timestamp,
|
|
1969
1771
|
});
|
|
1970
1772
|
}
|
|
1971
|
-
fetchActiveCalls() {
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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 };
|
|
1773
|
+
async fetchActiveCalls() {
|
|
1774
|
+
const res = await this.http.get(this.getHost() + "/calls/active", {
|
|
1775
|
+
responseType: "json",
|
|
1776
|
+
});
|
|
1777
|
+
return z.object({ calls: z.array(CallSessionSchema) }).parse(res.data)
|
|
1778
|
+
.calls;
|
|
2015
1779
|
}
|
|
2016
1780
|
async fetchIceServers() {
|
|
2017
1781
|
const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
|
|
@@ -2115,23 +1879,6 @@ export class Client {
|
|
|
2115
1879
|
}
|
|
2116
1880
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
2117
1881
|
}
|
|
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
|
-
}
|
|
2135
1882
|
/**
|
|
2136
1883
|
* Finish a passkey login and adopt the resulting JWT as the
|
|
2137
1884
|
* client's bearer token. After this call, `client.passkeys.*`
|
|
@@ -2264,6 +2011,18 @@ export class Client {
|
|
|
2264
2011
|
targets: targetDevices.length,
|
|
2265
2012
|
});
|
|
2266
2013
|
}
|
|
2014
|
+
async getAccountEntitlements() {
|
|
2015
|
+
const res = await this.http.get(this.getHost() + "/user/" + this.getUser().userID + "/entitlements");
|
|
2016
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
2017
|
+
}
|
|
2018
|
+
async getBillingAccountState() {
|
|
2019
|
+
const res = await this.http.get(this.getHost() + "/billing/account");
|
|
2020
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
2021
|
+
}
|
|
2022
|
+
async getBillingProducts() {
|
|
2023
|
+
const res = await this.http.get(this.getHost() + "/billing/products");
|
|
2024
|
+
return decodeHttpResponse(BillingProductArrayCodec, res.data);
|
|
2025
|
+
}
|
|
2267
2026
|
async getChannelByID(channelID) {
|
|
2268
2027
|
try {
|
|
2269
2028
|
const res = await this.http.get(this.getHost() + "/channel/" + channelID);
|
|
@@ -2518,14 +2277,6 @@ export class Client {
|
|
|
2518
2277
|
}
|
|
2519
2278
|
break;
|
|
2520
2279
|
}
|
|
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
|
-
}
|
|
2529
2280
|
case "deviceRequest": {
|
|
2530
2281
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
2531
2282
|
if (parsed.success) {
|
|
@@ -2743,67 +2494,6 @@ export class Client {
|
|
|
2743
2494
|
const response = await this.http.get(this.getHost() + "/user/" + userID + "/passkeys");
|
|
2744
2495
|
return decodeHttpResponse(PasskeyArrayCodec, response.data);
|
|
2745
2496
|
}
|
|
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
|
-
}
|
|
2807
2497
|
async markSessionVerified(sessionID) {
|
|
2808
2498
|
return this.database.markSessionVerified(sessionID);
|
|
2809
2499
|
}
|
|
@@ -2986,29 +2676,6 @@ export class Client {
|
|
|
2986
2676
|
}
|
|
2987
2677
|
}
|
|
2988
2678
|
}
|
|
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
|
-
}
|
|
3012
2679
|
async publishPendingDeviceRegistration(args) {
|
|
3013
2680
|
const signed = await this.signPendingRegistrationChallenge(args.challenge);
|
|
3014
2681
|
await this.http.post(this.getHost() +
|
|
@@ -3261,52 +2928,39 @@ export class Client {
|
|
|
3261
2928
|
if (!mail.forward) {
|
|
3262
2929
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
3263
2930
|
}
|
|
3264
|
-
const
|
|
2931
|
+
const decodedPlaintext = mail.forward
|
|
3265
2932
|
? null
|
|
3266
|
-
:
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
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);
|
|
2933
|
+
: decodeMessagePlaintext(plaintext);
|
|
2934
|
+
// emit the message
|
|
2935
|
+
const fwdMsg1 = mail.forward
|
|
2936
|
+
? messageSchema.parse(msgpack.decode(unsealed))
|
|
2937
|
+
: null;
|
|
2938
|
+
const message = fwdMsg1
|
|
2939
|
+
? {
|
|
2940
|
+
...normalizeForwardedMessage(fwdMsg1),
|
|
2941
|
+
forward: true,
|
|
3309
2942
|
}
|
|
2943
|
+
: {
|
|
2944
|
+
authorID: mail.authorID,
|
|
2945
|
+
decrypted: true,
|
|
2946
|
+
direction: "incoming",
|
|
2947
|
+
forward: mail.forward,
|
|
2948
|
+
group: mail.group
|
|
2949
|
+
? uuid.stringify(mail.group)
|
|
2950
|
+
: null,
|
|
2951
|
+
mailID: mail.mailID,
|
|
2952
|
+
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
2953
|
+
message: plaintext,
|
|
2954
|
+
}),
|
|
2955
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
2956
|
+
readerID: mail.readerID,
|
|
2957
|
+
recipient: mail.recipient,
|
|
2958
|
+
sender: mail.sender,
|
|
2959
|
+
timestamp: timestamp,
|
|
2960
|
+
};
|
|
2961
|
+
const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
|
|
2962
|
+
if (shouldEmitIncomingInitial) {
|
|
2963
|
+
this.emitter.emit("message", message);
|
|
3310
2964
|
}
|
|
3311
2965
|
if (libvexDebugDmEnabled()) {
|
|
3312
2966
|
try {
|
|
@@ -3472,47 +3126,33 @@ export class Client {
|
|
|
3472
3126
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
3473
3127
|
: null;
|
|
3474
3128
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
3475
|
-
const
|
|
3129
|
+
const decodedPlaintext = mail.forward
|
|
3476
3130
|
? null
|
|
3477
|
-
:
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
});
|
|
3483
|
-
if (event) {
|
|
3484
|
-
this.emitter.emit("call", event);
|
|
3131
|
+
: decodeMessagePlaintext(rawIncoming);
|
|
3132
|
+
const message = fwdMsg2
|
|
3133
|
+
? {
|
|
3134
|
+
...normalizeForwardedMessage(fwdMsg2),
|
|
3135
|
+
forward: true,
|
|
3485
3136
|
}
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
:
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
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
|
-
}
|
|
3137
|
+
: {
|
|
3138
|
+
authorID: mail.authorID,
|
|
3139
|
+
decrypted: true,
|
|
3140
|
+
direction: "incoming",
|
|
3141
|
+
forward: mail.forward,
|
|
3142
|
+
group: mail.group
|
|
3143
|
+
? uuid.stringify(mail.group)
|
|
3144
|
+
: null,
|
|
3145
|
+
mailID: mail.mailID,
|
|
3146
|
+
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
3147
|
+
message: rawIncoming,
|
|
3148
|
+
}),
|
|
3149
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3150
|
+
readerID: mail.readerID,
|
|
3151
|
+
recipient: mail.recipient,
|
|
3152
|
+
sender: mail.sender,
|
|
3153
|
+
timestamp: timestamp,
|
|
3154
|
+
};
|
|
3155
|
+
this.emitter.emit("message", message);
|
|
3516
3156
|
const sqlPatch = sessionToSqlPatch(session);
|
|
3517
3157
|
const persisted = {
|
|
3518
3158
|
CKr: sqlPatch.CKr,
|
|
@@ -3850,44 +3490,68 @@ export class Client {
|
|
|
3850
3490
|
throw err;
|
|
3851
3491
|
}
|
|
3852
3492
|
}
|
|
3853
|
-
async
|
|
3854
|
-
const
|
|
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({
|
|
3493
|
+
async sendCallResource(action, data) {
|
|
3494
|
+
const msg = {
|
|
3873
3495
|
action,
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3496
|
+
data,
|
|
3497
|
+
resourceType: "call",
|
|
3498
|
+
transmissionID: uuid.v4(),
|
|
3499
|
+
type: "resource",
|
|
3500
|
+
};
|
|
3501
|
+
return await new Promise((resolve, reject) => {
|
|
3502
|
+
const settle = (err, event) => {
|
|
3503
|
+
this.socket.off("message", callback);
|
|
3504
|
+
if (err !== null) {
|
|
3505
|
+
reject(errorFromUnknown(err));
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
if (!event) {
|
|
3509
|
+
reject(new Error("Call signaling response was empty."));
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
resolve(event);
|
|
3513
|
+
};
|
|
3514
|
+
const callback = (packedMsg) => {
|
|
3515
|
+
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
3516
|
+
if (receivedMsg.transmissionID !== msg.transmissionID) {
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
3520
|
+
if (!parsed.success) {
|
|
3521
|
+
settle("Call signaling failed: " + JSON.stringify(receivedMsg));
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
if (parsed.data.type === "success") {
|
|
3525
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
3526
|
+
if (!event.success) {
|
|
3527
|
+
settle("Invalid call signaling response: " +
|
|
3528
|
+
JSON.stringify(event.error.issues));
|
|
3529
|
+
return;
|
|
3530
|
+
}
|
|
3531
|
+
settle(null, event.data);
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (parsed.data.type === "error") {
|
|
3535
|
+
settle(new Error(parsed.data.error));
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
if (parsed.data.type === "notify" &&
|
|
3539
|
+
(parsed.data.event === "call" ||
|
|
3540
|
+
parsed.data.event === "callInvite")) {
|
|
3541
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
3542
|
+
if (event.success) {
|
|
3543
|
+
settle(null, event.data);
|
|
3544
|
+
}
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
settle("Unexpected call signaling response: " +
|
|
3548
|
+
JSON.stringify(parsed.data));
|
|
3549
|
+
};
|
|
3550
|
+
this.socket.on("message", callback);
|
|
3551
|
+
this.send(msg).catch((err) => {
|
|
3552
|
+
settle(err);
|
|
3553
|
+
});
|
|
3885
3554
|
});
|
|
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
3555
|
}
|
|
3892
3556
|
async sendGroupMessage(channelID, message, opts) {
|
|
3893
3557
|
const userList = await this.getUserList(channelID);
|
|
@@ -3976,7 +3640,7 @@ export class Client {
|
|
|
3976
3640
|
}
|
|
3977
3641
|
}
|
|
3978
3642
|
/* Sends encrypted mail to a user. */
|
|
3979
|
-
async sendMail(device, user, msg, group, mailID, forward, retry = false
|
|
3643
|
+
async sendMail(device, user, msg, group, mailID, forward, retry = false) {
|
|
3980
3644
|
while (this.sending.has(device.deviceID)) {
|
|
3981
3645
|
await sleep(100);
|
|
3982
3646
|
}
|
|
@@ -3991,7 +3655,7 @@ export class Client {
|
|
|
3991
3655
|
retry: String(retry),
|
|
3992
3656
|
});
|
|
3993
3657
|
}
|
|
3994
|
-
const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false
|
|
3658
|
+
const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false);
|
|
3995
3659
|
if (libvexDebugDmEnabled()) {
|
|
3996
3660
|
debugLibvexDm("sendMail: createSession returned", {
|
|
3997
3661
|
peerDevice: device.deviceID,
|
|
@@ -4030,43 +3694,34 @@ export class Client {
|
|
|
4030
3694
|
recipient: device.deviceID,
|
|
4031
3695
|
sender: this.getDevice().deviceID,
|
|
4032
3696
|
};
|
|
4033
|
-
const wireMail = notify ? { ...mail, notify } : mail;
|
|
4034
3697
|
const msgb = {
|
|
4035
3698
|
action: "CREATE",
|
|
4036
|
-
data:
|
|
3699
|
+
data: mail,
|
|
4037
3700
|
resourceType: "mail",
|
|
4038
3701
|
transmissionID: uuid.v4(),
|
|
4039
3702
|
type: "resource",
|
|
4040
3703
|
};
|
|
4041
3704
|
const hmac = xHMAC(mail, messageKey);
|
|
4042
|
-
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
|
|
4043
|
-
const callEnvelope = forward
|
|
4044
|
-
? null
|
|
4045
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
4046
3705
|
const fwdOut = forward
|
|
4047
3706
|
? messageSchema.parse(msgpack.decode(msg))
|
|
4048
3707
|
: null;
|
|
4049
3708
|
const outMsg = fwdOut
|
|
4050
3709
|
? { ...normalizeForwardedMessage(fwdOut), forward: true }
|
|
4051
|
-
:
|
|
4052
|
-
|
|
4053
|
-
:
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
};
|
|
4067
|
-
if (outMsg) {
|
|
4068
|
-
this.emitter.emit("message", outMsg);
|
|
4069
|
-
}
|
|
3710
|
+
: {
|
|
3711
|
+
authorID: mail.authorID,
|
|
3712
|
+
decrypted: true,
|
|
3713
|
+
direction: "outgoing",
|
|
3714
|
+
forward: mail.forward,
|
|
3715
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
3716
|
+
mailID: mail.mailID,
|
|
3717
|
+
...messageFromDecodedPlaintext(decodeMessagePlaintext(XUtils.encodeUTF8(msg))),
|
|
3718
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3719
|
+
readerID: mail.readerID,
|
|
3720
|
+
recipient: mail.recipient,
|
|
3721
|
+
sender: mail.sender,
|
|
3722
|
+
timestamp: new Date().toISOString(),
|
|
3723
|
+
};
|
|
3724
|
+
this.emitter.emit("message", outMsg);
|
|
4070
3725
|
const sqlPatch = sessionToSqlPatch(session);
|
|
4071
3726
|
const persisted = {
|
|
4072
3727
|
CKr: sqlPatch.CKr,
|
|
@@ -4091,22 +3746,22 @@ export class Client {
|
|
|
4091
3746
|
};
|
|
4092
3747
|
await this.database.saveSession(persisted);
|
|
4093
3748
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
4094
|
-
await this.deliverMailResource(msgb, hmac,
|
|
3749
|
+
await this.deliverMailResource(msgb, hmac, mail);
|
|
4095
3750
|
return outMsg;
|
|
4096
3751
|
}
|
|
4097
3752
|
finally {
|
|
4098
3753
|
this.sending.delete(device.deviceID);
|
|
4099
3754
|
}
|
|
4100
3755
|
}
|
|
4101
|
-
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false
|
|
3756
|
+
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false) {
|
|
4102
3757
|
try {
|
|
4103
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession
|
|
3758
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession);
|
|
4104
3759
|
}
|
|
4105
3760
|
catch (err) {
|
|
4106
3761
|
if (!this.shouldRetryDeliveryWithFreshSession(err)) {
|
|
4107
3762
|
throw err;
|
|
4108
3763
|
}
|
|
4109
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, true
|
|
3764
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, true);
|
|
4110
3765
|
}
|
|
4111
3766
|
}
|
|
4112
3767
|
async sendMessage(userID, message, opts) {
|
|
@@ -4241,35 +3896,19 @@ export class Client {
|
|
|
4241
3896
|
// Don't surface a teardown race as an unhandled rejection.
|
|
4242
3897
|
this.send(receipt).catch(ignoreSocketTeardown);
|
|
4243
3898
|
}
|
|
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
|
-
}
|
|
4270
3899
|
setAlive(status) {
|
|
4271
3900
|
this.isAlive = status;
|
|
4272
3901
|
}
|
|
3902
|
+
async setDevAccountTier(tier, options) {
|
|
3903
|
+
const res = await this.http.patch(this.getHost() +
|
|
3904
|
+
"/__dev/user/" +
|
|
3905
|
+
this.getUser().userID +
|
|
3906
|
+
"/entitlements", {
|
|
3907
|
+
expiresAt: options?.expiresAt ?? null,
|
|
3908
|
+
tier,
|
|
3909
|
+
});
|
|
3910
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
3911
|
+
}
|
|
4273
3912
|
setUser(user) {
|
|
4274
3913
|
this.user = user;
|
|
4275
3914
|
// Fresh identity / token: drop stale 404 negative-cache entries so a
|
|
@@ -4299,65 +3938,13 @@ export class Client {
|
|
|
4299
3938
|
async signPendingRegistrationChallenge(challengeHex) {
|
|
4300
3939
|
return XUtils.encodeHex(await xSignAsync(XUtils.decodeHex(challengeHex), this.signKeys.secretKey));
|
|
4301
3940
|
}
|
|
4302
|
-
async
|
|
4303
|
-
const
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
const
|
|
4308
|
-
|
|
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
|
-
};
|
|
3941
|
+
async submitAppleTransaction(request) {
|
|
3942
|
+
const res = await this.http.post(this.getHost() + "/billing/apple/transactions", request);
|
|
3943
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
3944
|
+
}
|
|
3945
|
+
async submitGooglePurchase(request) {
|
|
3946
|
+
const res = await this.http.post(this.getHost() + "/billing/google/purchases", request);
|
|
3947
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
4361
3948
|
}
|
|
4362
3949
|
async submitOTK(amount) {
|
|
4363
3950
|
const otks = [];
|
|
@@ -4456,13 +4043,5 @@ export class Client {
|
|
|
4456
4043
|
return null;
|
|
4457
4044
|
}
|
|
4458
4045
|
}
|
|
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
|
-
}
|
|
4467
4046
|
}
|
|
4468
4047
|
//# sourceMappingURL=Client.js.map
|