@vex-chat/libvex 7.3.0 → 8.0.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 +59 -43
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +244 -657
- 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 +366 -859
- 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;
|
|
@@ -1242,12 +1203,12 @@ export class Client {
|
|
|
1242
1203
|
* Registers a new account on the server.
|
|
1243
1204
|
*
|
|
1244
1205
|
* @param username - Optional username to register (must be unique when provided).
|
|
1245
|
-
* @param password -
|
|
1206
|
+
* @param password - Password for new accounts. Existing-account device approval requests may omit it.
|
|
1246
1207
|
* @returns `[user, null]` on success, `[null, error]` on failure.
|
|
1247
1208
|
*
|
|
1248
1209
|
* @example
|
|
1249
1210
|
* ```ts
|
|
1250
|
-
* const [user, err] = await client.register("MyUsername");
|
|
1211
|
+
* const [user, err] = await client.register("MyUsername", "correct horse battery staple");
|
|
1251
1212
|
* ```
|
|
1252
1213
|
*/
|
|
1253
1214
|
async register(username, password) {
|
|
@@ -1264,13 +1225,12 @@ export class Client {
|
|
|
1264
1225
|
: Client.randomUsername();
|
|
1265
1226
|
const resolvedPassword = password?.trim().length !== 0 && password !== undefined
|
|
1266
1227
|
? password
|
|
1267
|
-
:
|
|
1228
|
+
: undefined;
|
|
1268
1229
|
const signKey = XUtils.encodeHex(this.signKeys.publicKey);
|
|
1269
1230
|
const signed = XUtils.encodeHex(await xSignAsync(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
|
|
1270
1231
|
const preKeyIndex = this.xKeyRing.preKeys.index;
|
|
1271
1232
|
const regMsg = {
|
|
1272
1233
|
deviceName: this.options?.deviceName ?? "unknown",
|
|
1273
|
-
password: resolvedPassword,
|
|
1274
1234
|
preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
|
|
1275
1235
|
preKeyIndex,
|
|
1276
1236
|
preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
|
|
@@ -1278,6 +1238,9 @@ export class Client {
|
|
|
1278
1238
|
signKey,
|
|
1279
1239
|
username: resolvedUsername,
|
|
1280
1240
|
};
|
|
1241
|
+
if (resolvedPassword !== undefined) {
|
|
1242
|
+
regMsg.password = resolvedPassword;
|
|
1243
|
+
}
|
|
1281
1244
|
try {
|
|
1282
1245
|
const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
1283
1246
|
// New key-cluster server response: { device, token, user }.
|
|
@@ -1318,6 +1281,12 @@ export class Client {
|
|
|
1318
1281
|
const legacyUser = decodeHttpResponse(UserCodec, res.data);
|
|
1319
1282
|
this.setUser(legacyUser);
|
|
1320
1283
|
// Legacy servers require /auth after /register to get a JWT.
|
|
1284
|
+
if (resolvedPassword === undefined) {
|
|
1285
|
+
return [
|
|
1286
|
+
null,
|
|
1287
|
+
new Error("Legacy register succeeded without a password, so the SDK could not complete login."),
|
|
1288
|
+
];
|
|
1289
|
+
}
|
|
1321
1290
|
const loginResult = await this.login(resolvedUsername, resolvedPassword);
|
|
1322
1291
|
if (!loginResult.ok) {
|
|
1323
1292
|
return [
|
|
@@ -1440,86 +1409,6 @@ export class Client {
|
|
|
1440
1409
|
this.emitUndecryptedMessage(mail, timestamp);
|
|
1441
1410
|
this.acknowledgeInboundMail(mail);
|
|
1442
1411
|
}
|
|
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
1412
|
async approveDeviceRequest(requestID) {
|
|
1524
1413
|
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
1525
1414
|
if (!req) {
|
|
@@ -1556,35 +1445,6 @@ export class Client {
|
|
|
1556
1445
|
"/passkeys/register/begin", msgpack.encode({ name: args.name, signed }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1557
1446
|
return decodeHttpResponse(PasskeyOptionsCodec, response.data);
|
|
1558
1447
|
}
|
|
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
1448
|
censorPreKey(preKey) {
|
|
1589
1449
|
if (!preKey.index) {
|
|
1590
1450
|
throw new Error("Key index is required.");
|
|
@@ -1687,7 +1547,7 @@ export class Client {
|
|
|
1687
1547
|
* When `readMail` triggers a best-effort session re-establish, key-bundle
|
|
1688
1548
|
* errors should not reject the full read pipeline.
|
|
1689
1549
|
*/
|
|
1690
|
-
allowKeyBundleFailure = false
|
|
1550
|
+
allowKeyBundleFailure = false) {
|
|
1691
1551
|
return this.runWithThisCryptoProfile(async () => {
|
|
1692
1552
|
let keyBundle;
|
|
1693
1553
|
try {
|
|
@@ -1768,11 +1628,10 @@ export class Client {
|
|
|
1768
1628
|
recipient: device.deviceID,
|
|
1769
1629
|
sender: this.getDevice().deviceID,
|
|
1770
1630
|
};
|
|
1771
|
-
const wireMail = notify ? { ...mail, notify } : mail;
|
|
1772
1631
|
const hmac = xHMAC(mail, SK);
|
|
1773
1632
|
const msg = {
|
|
1774
1633
|
action: "CREATE",
|
|
1775
|
-
data:
|
|
1634
|
+
data: mail,
|
|
1776
1635
|
resourceType: "mail",
|
|
1777
1636
|
transmissionID: uuid.v4(),
|
|
1778
1637
|
type: "resource",
|
|
@@ -1792,38 +1651,32 @@ export class Client {
|
|
|
1792
1651
|
};
|
|
1793
1652
|
await this.database.saveSession(sessionEntry);
|
|
1794
1653
|
this.emitter.emit("session", sessionEntry, user);
|
|
1795
|
-
|
|
1796
|
-
const callEnvelope = forward
|
|
1797
|
-
? null
|
|
1798
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
1654
|
+
// emit the message
|
|
1799
1655
|
const forwardedMsg = forward
|
|
1800
1656
|
? messageSchema.parse(msgpack.decode(message))
|
|
1801
1657
|
: null;
|
|
1658
|
+
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
1802
1659
|
const emitMsg = forwardedMsg
|
|
1803
1660
|
? { ...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) {
|
|
1661
|
+
: {
|
|
1662
|
+
authorID: mail.authorID,
|
|
1663
|
+
decrypted: true,
|
|
1664
|
+
direction: "outgoing",
|
|
1665
|
+
forward: mail.forward,
|
|
1666
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
1667
|
+
mailID: mail.mailID,
|
|
1668
|
+
...messageFromDecodedPlaintext(decodeMessagePlaintext(XUtils.encodeUTF8(message))),
|
|
1669
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
1670
|
+
readerID: mail.readerID,
|
|
1671
|
+
recipient: mail.recipient,
|
|
1672
|
+
sender: mail.sender,
|
|
1673
|
+
timestamp: new Date().toISOString(),
|
|
1674
|
+
};
|
|
1675
|
+
if (shouldEmitHandshakeMessage) {
|
|
1823
1676
|
this.emitter.emit("message", emitMsg);
|
|
1824
1677
|
}
|
|
1825
|
-
await this.deliverMailResource(msg, hmac,
|
|
1826
|
-
return emitMsg;
|
|
1678
|
+
await this.deliverMailResource(msg, hmac, mail);
|
|
1679
|
+
return shouldEmitHandshakeMessage ? emitMsg : null;
|
|
1827
1680
|
});
|
|
1828
1681
|
}
|
|
1829
1682
|
async deleteChannel(channelID) {
|
|
@@ -1853,49 +1706,6 @@ export class Client {
|
|
|
1853
1706
|
async deleteServer(serverID) {
|
|
1854
1707
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
1855
1708
|
}
|
|
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
1709
|
deliverMailResource(msg, header, mail) {
|
|
1900
1710
|
if (this.mailBatchUnsupported) {
|
|
1901
1711
|
return this.deliverMailResourceOverSocket(msg, header);
|
|
@@ -1968,50 +1778,12 @@ export class Client {
|
|
|
1968
1778
|
timestamp,
|
|
1969
1779
|
});
|
|
1970
1780
|
}
|
|
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 };
|
|
1781
|
+
async fetchActiveCalls() {
|
|
1782
|
+
const res = await this.http.get(this.getHost() + "/calls/active", {
|
|
1783
|
+
responseType: "json",
|
|
1784
|
+
});
|
|
1785
|
+
return z.object({ calls: z.array(CallSessionSchema) }).parse(res.data)
|
|
1786
|
+
.calls;
|
|
2015
1787
|
}
|
|
2016
1788
|
async fetchIceServers() {
|
|
2017
1789
|
const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
|
|
@@ -2115,23 +1887,6 @@ export class Client {
|
|
|
2115
1887
|
}
|
|
2116
1888
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
2117
1889
|
}
|
|
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
1890
|
/**
|
|
2136
1891
|
* Finish a passkey login and adopt the resulting JWT as the
|
|
2137
1892
|
* client's bearer token. After this call, `client.passkeys.*`
|
|
@@ -2264,6 +2019,18 @@ export class Client {
|
|
|
2264
2019
|
targets: targetDevices.length,
|
|
2265
2020
|
});
|
|
2266
2021
|
}
|
|
2022
|
+
async getAccountEntitlements() {
|
|
2023
|
+
const res = await this.http.get(this.getHost() + "/user/" + this.getUser().userID + "/entitlements");
|
|
2024
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
2025
|
+
}
|
|
2026
|
+
async getBillingAccountState() {
|
|
2027
|
+
const res = await this.http.get(this.getHost() + "/billing/account");
|
|
2028
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
2029
|
+
}
|
|
2030
|
+
async getBillingProducts() {
|
|
2031
|
+
const res = await this.http.get(this.getHost() + "/billing/products");
|
|
2032
|
+
return decodeHttpResponse(BillingProductArrayCodec, res.data);
|
|
2033
|
+
}
|
|
2267
2034
|
async getChannelByID(channelID) {
|
|
2268
2035
|
try {
|
|
2269
2036
|
const res = await this.http.get(this.getHost() + "/channel/" + channelID);
|
|
@@ -2518,14 +2285,6 @@ export class Client {
|
|
|
2518
2285
|
}
|
|
2519
2286
|
break;
|
|
2520
2287
|
}
|
|
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
2288
|
case "deviceRequest": {
|
|
2530
2289
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
2531
2290
|
if (parsed.success) {
|
|
@@ -2743,67 +2502,6 @@ export class Client {
|
|
|
2743
2502
|
const response = await this.http.get(this.getHost() + "/user/" + userID + "/passkeys");
|
|
2744
2503
|
return decodeHttpResponse(PasskeyArrayCodec, response.data);
|
|
2745
2504
|
}
|
|
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
2505
|
async markSessionVerified(sessionID) {
|
|
2808
2506
|
return this.database.markSessionVerified(sessionID);
|
|
2809
2507
|
}
|
|
@@ -2986,29 +2684,6 @@ export class Client {
|
|
|
2986
2684
|
}
|
|
2987
2685
|
}
|
|
2988
2686
|
}
|
|
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
2687
|
async publishPendingDeviceRegistration(args) {
|
|
3013
2688
|
const signed = await this.signPendingRegistrationChallenge(args.challenge);
|
|
3014
2689
|
await this.http.post(this.getHost() +
|
|
@@ -3261,52 +2936,39 @@ export class Client {
|
|
|
3261
2936
|
if (!mail.forward) {
|
|
3262
2937
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
3263
2938
|
}
|
|
3264
|
-
const
|
|
2939
|
+
const decodedPlaintext = mail.forward
|
|
3265
2940
|
? 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);
|
|
2941
|
+
: decodeMessagePlaintext(plaintext);
|
|
2942
|
+
// emit the message
|
|
2943
|
+
const fwdMsg1 = mail.forward
|
|
2944
|
+
? messageSchema.parse(msgpack.decode(unsealed))
|
|
2945
|
+
: null;
|
|
2946
|
+
const message = fwdMsg1
|
|
2947
|
+
? {
|
|
2948
|
+
...normalizeForwardedMessage(fwdMsg1),
|
|
2949
|
+
forward: true,
|
|
3309
2950
|
}
|
|
2951
|
+
: {
|
|
2952
|
+
authorID: mail.authorID,
|
|
2953
|
+
decrypted: true,
|
|
2954
|
+
direction: "incoming",
|
|
2955
|
+
forward: mail.forward,
|
|
2956
|
+
group: mail.group
|
|
2957
|
+
? uuid.stringify(mail.group)
|
|
2958
|
+
: null,
|
|
2959
|
+
mailID: mail.mailID,
|
|
2960
|
+
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
2961
|
+
message: plaintext,
|
|
2962
|
+
}),
|
|
2963
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
2964
|
+
readerID: mail.readerID,
|
|
2965
|
+
recipient: mail.recipient,
|
|
2966
|
+
sender: mail.sender,
|
|
2967
|
+
timestamp: timestamp,
|
|
2968
|
+
};
|
|
2969
|
+
const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
|
|
2970
|
+
if (shouldEmitIncomingInitial) {
|
|
2971
|
+
this.emitter.emit("message", message);
|
|
3310
2972
|
}
|
|
3311
2973
|
if (libvexDebugDmEnabled()) {
|
|
3312
2974
|
try {
|
|
@@ -3472,47 +3134,33 @@ export class Client {
|
|
|
3472
3134
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
3473
3135
|
: null;
|
|
3474
3136
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
3475
|
-
const
|
|
3137
|
+
const decodedPlaintext = mail.forward
|
|
3476
3138
|
? null
|
|
3477
|
-
:
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
});
|
|
3483
|
-
if (event) {
|
|
3484
|
-
this.emitter.emit("call", event);
|
|
3139
|
+
: decodeMessagePlaintext(rawIncoming);
|
|
3140
|
+
const message = fwdMsg2
|
|
3141
|
+
? {
|
|
3142
|
+
...normalizeForwardedMessage(fwdMsg2),
|
|
3143
|
+
forward: true,
|
|
3485
3144
|
}
|
|
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
|
-
}
|
|
3145
|
+
: {
|
|
3146
|
+
authorID: mail.authorID,
|
|
3147
|
+
decrypted: true,
|
|
3148
|
+
direction: "incoming",
|
|
3149
|
+
forward: mail.forward,
|
|
3150
|
+
group: mail.group
|
|
3151
|
+
? uuid.stringify(mail.group)
|
|
3152
|
+
: null,
|
|
3153
|
+
mailID: mail.mailID,
|
|
3154
|
+
...messageFromDecodedPlaintext(decodedPlaintext ?? {
|
|
3155
|
+
message: rawIncoming,
|
|
3156
|
+
}),
|
|
3157
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3158
|
+
readerID: mail.readerID,
|
|
3159
|
+
recipient: mail.recipient,
|
|
3160
|
+
sender: mail.sender,
|
|
3161
|
+
timestamp: timestamp,
|
|
3162
|
+
};
|
|
3163
|
+
this.emitter.emit("message", message);
|
|
3516
3164
|
const sqlPatch = sessionToSqlPatch(session);
|
|
3517
3165
|
const persisted = {
|
|
3518
3166
|
CKr: sqlPatch.CKr,
|
|
@@ -3850,44 +3498,68 @@ export class Client {
|
|
|
3850
3498
|
throw err;
|
|
3851
3499
|
}
|
|
3852
3500
|
}
|
|
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({
|
|
3501
|
+
async sendCallResource(action, data) {
|
|
3502
|
+
const msg = {
|
|
3873
3503
|
action,
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3504
|
+
data,
|
|
3505
|
+
resourceType: "call",
|
|
3506
|
+
transmissionID: uuid.v4(),
|
|
3507
|
+
type: "resource",
|
|
3508
|
+
};
|
|
3509
|
+
return await new Promise((resolve, reject) => {
|
|
3510
|
+
const settle = (err, event) => {
|
|
3511
|
+
this.socket.off("message", callback);
|
|
3512
|
+
if (err !== null) {
|
|
3513
|
+
reject(errorFromUnknown(err));
|
|
3514
|
+
return;
|
|
3515
|
+
}
|
|
3516
|
+
if (!event) {
|
|
3517
|
+
reject(new Error("Call signaling response was empty."));
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
resolve(event);
|
|
3521
|
+
};
|
|
3522
|
+
const callback = (packedMsg) => {
|
|
3523
|
+
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
3524
|
+
if (receivedMsg.transmissionID !== msg.transmissionID) {
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
3528
|
+
if (!parsed.success) {
|
|
3529
|
+
settle("Call signaling failed: " + JSON.stringify(receivedMsg));
|
|
3530
|
+
return;
|
|
3531
|
+
}
|
|
3532
|
+
if (parsed.data.type === "success") {
|
|
3533
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
3534
|
+
if (!event.success) {
|
|
3535
|
+
settle("Invalid call signaling response: " +
|
|
3536
|
+
JSON.stringify(event.error.issues));
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
settle(null, event.data);
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
3542
|
+
if (parsed.data.type === "error") {
|
|
3543
|
+
settle(new Error(parsed.data.error));
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (parsed.data.type === "notify" &&
|
|
3547
|
+
(parsed.data.event === "call" ||
|
|
3548
|
+
parsed.data.event === "callInvite")) {
|
|
3549
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
3550
|
+
if (event.success) {
|
|
3551
|
+
settle(null, event.data);
|
|
3552
|
+
}
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
settle("Unexpected call signaling response: " +
|
|
3556
|
+
JSON.stringify(parsed.data));
|
|
3557
|
+
};
|
|
3558
|
+
this.socket.on("message", callback);
|
|
3559
|
+
this.send(msg).catch((err) => {
|
|
3560
|
+
settle(err);
|
|
3561
|
+
});
|
|
3885
3562
|
});
|
|
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
3563
|
}
|
|
3892
3564
|
async sendGroupMessage(channelID, message, opts) {
|
|
3893
3565
|
const userList = await this.getUserList(channelID);
|
|
@@ -3976,7 +3648,7 @@ export class Client {
|
|
|
3976
3648
|
}
|
|
3977
3649
|
}
|
|
3978
3650
|
/* Sends encrypted mail to a user. */
|
|
3979
|
-
async sendMail(device, user, msg, group, mailID, forward, retry = false
|
|
3651
|
+
async sendMail(device, user, msg, group, mailID, forward, retry = false) {
|
|
3980
3652
|
while (this.sending.has(device.deviceID)) {
|
|
3981
3653
|
await sleep(100);
|
|
3982
3654
|
}
|
|
@@ -3991,7 +3663,7 @@ export class Client {
|
|
|
3991
3663
|
retry: String(retry),
|
|
3992
3664
|
});
|
|
3993
3665
|
}
|
|
3994
|
-
const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false
|
|
3666
|
+
const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false);
|
|
3995
3667
|
if (libvexDebugDmEnabled()) {
|
|
3996
3668
|
debugLibvexDm("sendMail: createSession returned", {
|
|
3997
3669
|
peerDevice: device.deviceID,
|
|
@@ -4030,43 +3702,34 @@ export class Client {
|
|
|
4030
3702
|
recipient: device.deviceID,
|
|
4031
3703
|
sender: this.getDevice().deviceID,
|
|
4032
3704
|
};
|
|
4033
|
-
const wireMail = notify ? { ...mail, notify } : mail;
|
|
4034
3705
|
const msgb = {
|
|
4035
3706
|
action: "CREATE",
|
|
4036
|
-
data:
|
|
3707
|
+
data: mail,
|
|
4037
3708
|
resourceType: "mail",
|
|
4038
3709
|
transmissionID: uuid.v4(),
|
|
4039
3710
|
type: "resource",
|
|
4040
3711
|
};
|
|
4041
3712
|
const hmac = xHMAC(mail, messageKey);
|
|
4042
|
-
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
|
|
4043
|
-
const callEnvelope = forward
|
|
4044
|
-
? null
|
|
4045
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
4046
3713
|
const fwdOut = forward
|
|
4047
3714
|
? messageSchema.parse(msgpack.decode(msg))
|
|
4048
3715
|
: null;
|
|
4049
3716
|
const outMsg = fwdOut
|
|
4050
3717
|
? { ...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
|
-
}
|
|
3718
|
+
: {
|
|
3719
|
+
authorID: mail.authorID,
|
|
3720
|
+
decrypted: true,
|
|
3721
|
+
direction: "outgoing",
|
|
3722
|
+
forward: mail.forward,
|
|
3723
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
3724
|
+
mailID: mail.mailID,
|
|
3725
|
+
...messageFromDecodedPlaintext(decodeMessagePlaintext(XUtils.encodeUTF8(msg))),
|
|
3726
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3727
|
+
readerID: mail.readerID,
|
|
3728
|
+
recipient: mail.recipient,
|
|
3729
|
+
sender: mail.sender,
|
|
3730
|
+
timestamp: new Date().toISOString(),
|
|
3731
|
+
};
|
|
3732
|
+
this.emitter.emit("message", outMsg);
|
|
4070
3733
|
const sqlPatch = sessionToSqlPatch(session);
|
|
4071
3734
|
const persisted = {
|
|
4072
3735
|
CKr: sqlPatch.CKr,
|
|
@@ -4091,22 +3754,22 @@ export class Client {
|
|
|
4091
3754
|
};
|
|
4092
3755
|
await this.database.saveSession(persisted);
|
|
4093
3756
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
4094
|
-
await this.deliverMailResource(msgb, hmac,
|
|
3757
|
+
await this.deliverMailResource(msgb, hmac, mail);
|
|
4095
3758
|
return outMsg;
|
|
4096
3759
|
}
|
|
4097
3760
|
finally {
|
|
4098
3761
|
this.sending.delete(device.deviceID);
|
|
4099
3762
|
}
|
|
4100
3763
|
}
|
|
4101
|
-
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false
|
|
3764
|
+
async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false) {
|
|
4102
3765
|
try {
|
|
4103
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession
|
|
3766
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession);
|
|
4104
3767
|
}
|
|
4105
3768
|
catch (err) {
|
|
4106
3769
|
if (!this.shouldRetryDeliveryWithFreshSession(err)) {
|
|
4107
3770
|
throw err;
|
|
4108
3771
|
}
|
|
4109
|
-
return await this.sendMail(device, user, msg, group, mailID, forward, true
|
|
3772
|
+
return await this.sendMail(device, user, msg, group, mailID, forward, true);
|
|
4110
3773
|
}
|
|
4111
3774
|
}
|
|
4112
3775
|
async sendMessage(userID, message, opts) {
|
|
@@ -4241,35 +3904,19 @@ export class Client {
|
|
|
4241
3904
|
// Don't surface a teardown race as an unhandled rejection.
|
|
4242
3905
|
this.send(receipt).catch(ignoreSocketTeardown);
|
|
4243
3906
|
}
|
|
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
3907
|
setAlive(status) {
|
|
4271
3908
|
this.isAlive = status;
|
|
4272
3909
|
}
|
|
3910
|
+
async setDevAccountTier(tier, options) {
|
|
3911
|
+
const res = await this.http.patch(this.getHost() +
|
|
3912
|
+
"/__dev/user/" +
|
|
3913
|
+
this.getUser().userID +
|
|
3914
|
+
"/entitlements", {
|
|
3915
|
+
expiresAt: options?.expiresAt ?? null,
|
|
3916
|
+
tier,
|
|
3917
|
+
});
|
|
3918
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
3919
|
+
}
|
|
4273
3920
|
setUser(user) {
|
|
4274
3921
|
this.user = user;
|
|
4275
3922
|
// Fresh identity / token: drop stale 404 negative-cache entries so a
|
|
@@ -4299,65 +3946,13 @@ export class Client {
|
|
|
4299
3946
|
async signPendingRegistrationChallenge(challengeHex) {
|
|
4300
3947
|
return XUtils.encodeHex(await xSignAsync(XUtils.decodeHex(challengeHex), this.signKeys.secretKey));
|
|
4301
3948
|
}
|
|
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
|
-
};
|
|
3949
|
+
async submitAppleTransaction(request) {
|
|
3950
|
+
const res = await this.http.post(this.getHost() + "/billing/apple/transactions", request);
|
|
3951
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
3952
|
+
}
|
|
3953
|
+
async submitGooglePurchase(request) {
|
|
3954
|
+
const res = await this.http.post(this.getHost() + "/billing/google/purchases", request);
|
|
3955
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
4361
3956
|
}
|
|
4362
3957
|
async submitOTK(amount) {
|
|
4363
3958
|
const otks = [];
|
|
@@ -4456,13 +4051,5 @@ export class Client {
|
|
|
4456
4051
|
return null;
|
|
4457
4052
|
}
|
|
4458
4053
|
}
|
|
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
4054
|
}
|
|
4468
4055
|
//# sourceMappingURL=Client.js.map
|