@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.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 { CallEnvelopeBodySchema, CallEventSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, SignedCallEnvelopeSchema, WSMessageSchema, } from "@vex-chat/types";
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: NotificationSubscriptionChannelSchema,
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.sendEncryptedCallAction("accept", callID, signal),
406
+ accept: (callID, signal) => this.sendCallResource("ACCEPT", {
407
+ callID,
408
+ ...(signal ? { signal } : {}),
409
+ }),
457
410
  active: this.fetchActiveCalls.bind(this),
458
- cancel: (callID) => this.sendEncryptedCallAction("cancel", callID),
459
- hangup: (callID) => this.sendEncryptedCallAction("hangup", callID),
460
- ice: (callID, signal) => this.sendEncryptedCallAction("ice", callID, signal),
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.sendEncryptedCallAction("reject", callID),
463
- signal: (callID, signal) => this.sendEncryptedCallAction("signal", callID, signal),
464
- startDM: (recipientUserID, signal) => this.startEncryptedDmCall(recipientUserID, signal),
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 - Optional legacy password used when talking to pre-keycluster servers.
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
- : uuid.v4();
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, notify) {
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: wireMail,
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
- const rawPlaintext = forward ? "" : XUtils.encodeUTF8(message);
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
- : callEnvelope
1805
- ? null
1806
- : message.length > 0
1807
- ? {
1808
- authorID: mail.authorID,
1809
- decrypted: true,
1810
- direction: "outgoing",
1811
- forward: mail.forward,
1812
- group: mail.group ? uuid.stringify(mail.group) : null,
1813
- mailID: mail.mailID,
1814
- ...messageFromDecodedPlaintext(decodeMessagePlaintext(rawPlaintext)),
1815
- nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
1816
- readerID: mail.readerID,
1817
- recipient: mail.recipient,
1818
- sender: mail.sender,
1819
- timestamp: new Date().toISOString(),
1820
- }
1821
- : null;
1822
- if (emitMsg) {
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, wireMail);
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 now = Date.now();
1973
- const active = [];
1974
- for (const [callID, state] of this.callStates.entries()) {
1975
- if (state.session.status === "ended" ||
1976
- Date.parse(state.session.expiresAt) <= now) {
1977
- this.callStates.delete(callID);
1978
- continue;
1979
- }
1980
- active.push(cloneCallSession(state.session));
1981
- }
1982
- return Promise.resolve(active);
1983
- }
1984
- async fetchCallPeer(args) {
1985
- const [user, err] = await this.fetchUser(args.userID);
1986
- if (err) {
1987
- throw err;
1988
- }
1989
- if (!user) {
1990
- throw new Error("Call peer not found.");
1991
- }
1992
- const afterBackoff = await this.fetchUserDeviceListWithBackoff(args.userID, "peer");
1993
- let deviceListRaw;
1994
- try {
1995
- const again = await this.fetchUserDeviceListOnce(args.userID);
1996
- const byID = new Map();
1997
- for (const device of afterBackoff) {
1998
- byID.set(device.deviceID, device);
1999
- }
2000
- for (const device of again) {
2001
- byID.set(device.deviceID, device);
2002
- }
2003
- deviceListRaw = [...byID.values()];
2004
- }
2005
- catch {
2006
- deviceListRaw = afterBackoff;
2007
- }
2008
- const devices = deviceListRaw
2009
- .filter((device) => !device.deleted)
2010
- .sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
2011
- if (devices.length === 0) {
2012
- throw new Error("Call peer has no active devices.");
2013
- }
2014
- return { devices, user };
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 callEnvelope = mail.forward
2939
+ const decodedPlaintext = mail.forward
3265
2940
  ? null
3266
- : decodeCallEnvelopePlaintext(plaintext);
3267
- if (callEnvelope) {
3268
- const event = await this.processDecryptedCallEnvelope({
3269
- envelope: callEnvelope,
3270
- mail,
3271
- });
3272
- if (event) {
3273
- this.emitter.emit("call", event);
3274
- }
3275
- }
3276
- else {
3277
- const decodedPlaintext = mail.forward
3278
- ? null
3279
- : decodeMessagePlaintext(plaintext);
3280
- const fwdMsg1 = mail.forward
3281
- ? messageSchema.parse(msgpack.decode(unsealed))
3282
- : null;
3283
- const message = fwdMsg1
3284
- ? {
3285
- ...normalizeForwardedMessage(fwdMsg1),
3286
- forward: true,
3287
- }
3288
- : {
3289
- authorID: mail.authorID,
3290
- decrypted: true,
3291
- direction: "incoming",
3292
- forward: mail.forward,
3293
- group: mail.group
3294
- ? uuid.stringify(mail.group)
3295
- : null,
3296
- mailID: mail.mailID,
3297
- ...messageFromDecodedPlaintext(decodedPlaintext ?? {
3298
- message: plaintext,
3299
- }),
3300
- nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
3301
- readerID: mail.readerID,
3302
- recipient: mail.recipient,
3303
- sender: mail.sender,
3304
- timestamp: timestamp,
3305
- };
3306
- const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
3307
- if (shouldEmitIncomingInitial) {
3308
- this.emitter.emit("message", message);
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 callEnvelope = mail.forward
3137
+ const decodedPlaintext = mail.forward
3476
3138
  ? null
3477
- : decodeCallEnvelopePlaintext(rawIncoming);
3478
- if (callEnvelope) {
3479
- const event = await this.processDecryptedCallEnvelope({
3480
- envelope: callEnvelope,
3481
- mail,
3482
- });
3483
- if (event) {
3484
- this.emitter.emit("call", event);
3139
+ : decodeMessagePlaintext(rawIncoming);
3140
+ const message = fwdMsg2
3141
+ ? {
3142
+ ...normalizeForwardedMessage(fwdMsg2),
3143
+ forward: true,
3485
3144
  }
3486
- }
3487
- else {
3488
- const decodedPlaintext = mail.forward
3489
- ? null
3490
- : decodeMessagePlaintext(rawIncoming);
3491
- const message = fwdMsg2
3492
- ? {
3493
- ...normalizeForwardedMessage(fwdMsg2),
3494
- forward: true,
3495
- }
3496
- : {
3497
- authorID: mail.authorID,
3498
- decrypted: true,
3499
- direction: "incoming",
3500
- forward: mail.forward,
3501
- group: mail.group
3502
- ? uuid.stringify(mail.group)
3503
- : null,
3504
- mailID: mail.mailID,
3505
- ...messageFromDecodedPlaintext(decodedPlaintext ?? {
3506
- message: rawIncoming,
3507
- }),
3508
- nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
3509
- readerID: mail.readerID,
3510
- recipient: mail.recipient,
3511
- sender: mail.sender,
3512
- timestamp: timestamp,
3513
- };
3514
- this.emitter.emit("message", message);
3515
- }
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 sendCallEnvelopeMail(args) {
3854
- const envelope = await this.callEnvelopeForBody(args.body);
3855
- await this.sendMailWithRecovery(args.targetDevice, args.targetUser, encodeCallEnvelopePlaintext(envelope), null, args.mailID, false, false, args.notify);
3856
- }
3857
- async sendEncryptedCallAction(action, callID, signal) {
3858
- const state = this.callStates.get(callID);
3859
- if (!state) {
3860
- throw new Error("Unknown encrypted call: " + callID);
3861
- }
3862
- const targets = await this.callTargetsForState(state);
3863
- if (targets.length === 0) {
3864
- throw new Error("Call has no reachable peer devices.");
3865
- }
3866
- const targetUser = await this.fetchUserOrThrow(state.peerUserID);
3867
- const sequence = state.sequence + 1;
3868
- state.sequence = sequence;
3869
- const expiresAt = action === "accept"
3870
- ? new Date(Date.now() + CALL_MAX_TTL_MS).toISOString()
3871
- : state.session.expiresAt;
3872
- const bodies = targets.map((target) => this.makeCallEnvelopeBody({
3501
+ async sendCallResource(action, data) {
3502
+ const msg = {
3873
3503
  action,
3874
- expiresAt,
3875
- sequence,
3876
- signal,
3877
- state,
3878
- toDeviceID: target.deviceID,
3879
- toUserID: target.owner,
3880
- }));
3881
- await this.deliverCallEnvelopeBatch({
3882
- bodies,
3883
- mailID: uuid.v4(),
3884
- targetUser,
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, notify) {
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, notify);
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: wireMail,
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
- : callEnvelope
4052
- ? null
4053
- : {
4054
- authorID: mail.authorID,
4055
- decrypted: true,
4056
- direction: "outgoing",
4057
- forward: mail.forward,
4058
- group: mail.group ? uuid.stringify(mail.group) : null,
4059
- mailID: mail.mailID,
4060
- ...messageFromDecodedPlaintext(decodeMessagePlaintext(rawPlaintext)),
4061
- nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
4062
- readerID: mail.readerID,
4063
- recipient: mail.recipient,
4064
- sender: mail.sender,
4065
- timestamp: new Date().toISOString(),
4066
- };
4067
- if (outMsg) {
4068
- this.emitter.emit("message", outMsg);
4069
- }
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, wireMail);
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, notify) {
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, notify);
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, notify);
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 startEncryptedDmCall(recipientUserID, signal) {
4303
- const { devices, user } = await this.fetchCallPeer({
4304
- userID: recipientUserID,
4305
- });
4306
- const now = new Date();
4307
- const createdAt = now.toISOString();
4308
- const expiresAt = new Date(now.getTime() + CALL_INVITE_TTL_MS).toISOString();
4309
- const session = {
4310
- callID: uuid.v4(),
4311
- conversationID: recipientUserID,
4312
- conversationType: "dm",
4313
- createdAt,
4314
- createdBy: this.getUser().userID,
4315
- createdByDeviceID: this.getDevice().deviceID,
4316
- expiresAt,
4317
- media: "audio",
4318
- participants: [
4319
- {
4320
- acceptedAt: createdAt,
4321
- deviceID: this.getDevice().deviceID,
4322
- joinedAt: createdAt,
4323
- state: "accepted",
4324
- userID: this.getUser().userID,
4325
- },
4326
- {
4327
- state: "ringing",
4328
- userID: recipientUserID,
4329
- },
4330
- ],
4331
- status: "ringing",
4332
- };
4333
- const state = {
4334
- peerUserID: recipientUserID,
4335
- pendingPeerDevices: devices,
4336
- sequence: 1,
4337
- session,
4338
- };
4339
- this.callStates.set(session.callID, state);
4340
- const bodies = devices.map((device) => this.makeCallEnvelopeBody({
4341
- action: "invite",
4342
- expiresAt,
4343
- sequence: state.sequence,
4344
- signal,
4345
- state,
4346
- toDeviceID: device.deviceID,
4347
- toUserID: recipientUserID,
4348
- }));
4349
- await this.deliverCallEnvelopeBatch({
4350
- bodies,
4351
- mailID: uuid.v4(),
4352
- targetUser: user,
4353
- });
4354
- return {
4355
- action: "invite",
4356
- call: cloneCallSession(session),
4357
- fromDeviceID: this.getDevice().deviceID,
4358
- fromUserID: this.getUser().userID,
4359
- ...(signal ? { signal } : {}),
4360
- };
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