@vex-chat/libvex 7.3.0 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Client.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;
@@ -1440,86 +1401,6 @@ export class Client {
1440
1401
  this.emitUndecryptedMessage(mail, timestamp);
1441
1402
  this.acknowledgeInboundMail(mail);
1442
1403
  }
1443
- applyCallEnvelopeBody(body) {
1444
- const localUserID = this.getUser().userID;
1445
- const peerUserID = body.fromUserID === localUserID ? body.toUserID : body.fromUserID;
1446
- const peerDeviceID = body.fromUserID === localUserID
1447
- ? body.toDeviceID
1448
- : body.fromDeviceID;
1449
- let state = this.callStates.get(body.callID);
1450
- if (!state) {
1451
- state = {
1452
- peerDeviceID,
1453
- peerUserID,
1454
- pendingPeerDevices: [],
1455
- sequence: 0,
1456
- session: this.sessionFromCallEnvelope(body),
1457
- };
1458
- }
1459
- state.peerUserID = peerUserID;
1460
- if (body.fromUserID !== localUserID || body.action === "accept") {
1461
- state.peerDeviceID = peerDeviceID;
1462
- }
1463
- state.sequence = Math.max(state.sequence, body.sequence);
1464
- state.session.expiresAt = body.expiresAt;
1465
- const now = new Date().toISOString();
1466
- switch (body.action) {
1467
- case "accept":
1468
- state.session.status = "active";
1469
- this.upsertCallParticipant(state.session, {
1470
- acceptedAt: now,
1471
- deviceID: body.fromDeviceID,
1472
- joinedAt: now,
1473
- state: "accepted",
1474
- userID: body.fromUserID,
1475
- });
1476
- break;
1477
- case "cancel":
1478
- case "end":
1479
- case "hangup":
1480
- case "reject":
1481
- case "timeout":
1482
- state.session.status = "ended";
1483
- state.session.endedAt = now;
1484
- this.upsertCallParticipant(state.session, {
1485
- leftAt: now,
1486
- state: body.action === "reject" ? "rejected" : "left",
1487
- userID: body.fromUserID,
1488
- });
1489
- break;
1490
- case "ice":
1491
- case "signal":
1492
- break;
1493
- case "invite":
1494
- state.session.status = "ringing";
1495
- this.upsertCallParticipant(state.session, {
1496
- acceptedAt: body.createdAt,
1497
- deviceID: body.createdByDeviceID,
1498
- joinedAt: body.createdAt,
1499
- state: "accepted",
1500
- userID: body.createdBy,
1501
- });
1502
- this.upsertCallParticipant(state.session, {
1503
- state: "ringing",
1504
- userID: body.toUserID,
1505
- });
1506
- break;
1507
- }
1508
- const event = {
1509
- action: body.action,
1510
- call: cloneCallSession(state.session),
1511
- fromDeviceID: body.fromDeviceID,
1512
- fromUserID: body.fromUserID,
1513
- ...(body.signal ? { signal: body.signal } : {}),
1514
- };
1515
- if (state.session.status === "ended") {
1516
- this.callStates.delete(body.callID);
1517
- }
1518
- else {
1519
- this.callStates.set(body.callID, state);
1520
- }
1521
- return event;
1522
- }
1523
1404
  async approveDeviceRequest(requestID) {
1524
1405
  const req = await this.getDeviceRegistrationRequest(requestID);
1525
1406
  if (!req) {
@@ -1556,35 +1437,6 @@ export class Client {
1556
1437
  "/passkeys/register/begin", msgpack.encode({ name: args.name, signed }), { headers: { "Content-Type": "application/msgpack" } });
1557
1438
  return decodeHttpResponse(PasskeyOptionsCodec, response.data);
1558
1439
  }
1559
- async callEnvelopeForBody(body) {
1560
- const wireBody = normalizeCallEnvelopeBodyForWire(body);
1561
- const signed = await xSignAsync(canonicalJsonBytes(wireBody), this.signKeys.secretKey);
1562
- return {
1563
- body: wireBody,
1564
- signed: XUtils.encodeHex(signed),
1565
- };
1566
- }
1567
- async callTargetsForState(state) {
1568
- if (state.peerDeviceID) {
1569
- const cached = this.deviceRecords[state.peerDeviceID];
1570
- const device = cached ?? (await this.getDeviceByID(state.peerDeviceID));
1571
- if (!device) {
1572
- throw new Error(`Call peer device not found: ${state.peerDeviceID}`);
1573
- }
1574
- return [device];
1575
- }
1576
- return state.pendingPeerDevices;
1577
- }
1578
- callWakeForEnvelope(body) {
1579
- if (body.action !== "invite") {
1580
- return undefined;
1581
- }
1582
- return {
1583
- callID: body.callID,
1584
- event: "callWake",
1585
- expiresAt: body.expiresAt,
1586
- };
1587
- }
1588
1440
  censorPreKey(preKey) {
1589
1441
  if (!preKey.index) {
1590
1442
  throw new Error("Key index is required.");
@@ -1687,7 +1539,7 @@ export class Client {
1687
1539
  * When `readMail` triggers a best-effort session re-establish, key-bundle
1688
1540
  * errors should not reject the full read pipeline.
1689
1541
  */
1690
- allowKeyBundleFailure = false, notify) {
1542
+ allowKeyBundleFailure = false) {
1691
1543
  return this.runWithThisCryptoProfile(async () => {
1692
1544
  let keyBundle;
1693
1545
  try {
@@ -1768,11 +1620,10 @@ export class Client {
1768
1620
  recipient: device.deviceID,
1769
1621
  sender: this.getDevice().deviceID,
1770
1622
  };
1771
- const wireMail = notify ? { ...mail, notify } : mail;
1772
1623
  const hmac = xHMAC(mail, SK);
1773
1624
  const msg = {
1774
1625
  action: "CREATE",
1775
- data: wireMail,
1626
+ data: mail,
1776
1627
  resourceType: "mail",
1777
1628
  transmissionID: uuid.v4(),
1778
1629
  type: "resource",
@@ -1792,38 +1643,32 @@ export class Client {
1792
1643
  };
1793
1644
  await this.database.saveSession(sessionEntry);
1794
1645
  this.emitter.emit("session", sessionEntry, user);
1795
- const rawPlaintext = forward ? "" : XUtils.encodeUTF8(message);
1796
- const callEnvelope = forward
1797
- ? null
1798
- : decodeCallEnvelopePlaintext(rawPlaintext);
1646
+ // emit the message
1799
1647
  const forwardedMsg = forward
1800
1648
  ? messageSchema.parse(msgpack.decode(message))
1801
1649
  : null;
1650
+ const shouldEmitHandshakeMessage = forward || message.length > 0;
1802
1651
  const emitMsg = forwardedMsg
1803
1652
  ? { ...normalizeForwardedMessage(forwardedMsg), forward: true }
1804
- : 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) {
1653
+ : {
1654
+ authorID: mail.authorID,
1655
+ decrypted: true,
1656
+ direction: "outgoing",
1657
+ forward: mail.forward,
1658
+ group: mail.group ? uuid.stringify(mail.group) : null,
1659
+ mailID: mail.mailID,
1660
+ ...messageFromDecodedPlaintext(decodeMessagePlaintext(XUtils.encodeUTF8(message))),
1661
+ nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
1662
+ readerID: mail.readerID,
1663
+ recipient: mail.recipient,
1664
+ sender: mail.sender,
1665
+ timestamp: new Date().toISOString(),
1666
+ };
1667
+ if (shouldEmitHandshakeMessage) {
1823
1668
  this.emitter.emit("message", emitMsg);
1824
1669
  }
1825
- await this.deliverMailResource(msg, hmac, wireMail);
1826
- return emitMsg;
1670
+ await this.deliverMailResource(msg, hmac, mail);
1671
+ return shouldEmitHandshakeMessage ? emitMsg : null;
1827
1672
  });
1828
1673
  }
1829
1674
  async deleteChannel(channelID) {
@@ -1853,49 +1698,6 @@ export class Client {
1853
1698
  async deleteServer(serverID) {
1854
1699
  await this.http.delete(this.getHost() + "/server/" + serverID);
1855
1700
  }
1856
- async deliverCallEnvelopeBatch(args) {
1857
- let failCount = 0;
1858
- let lastErr;
1859
- for (let index = 0; index < args.bodies.length; index += MAIL_FANOUT_CONCURRENCY) {
1860
- const batch = args.bodies.slice(index, index + MAIL_FANOUT_CONCURRENCY);
1861
- const results = await Promise.all(batch.map(async (body) => {
1862
- try {
1863
- const targetDevice = this.deviceRecords[body.toDeviceID] ??
1864
- (await this.getDeviceByID(body.toDeviceID));
1865
- if (!targetDevice) {
1866
- throw new Error(`Call target device not found: ${body.toDeviceID}`);
1867
- }
1868
- await this.sendCallEnvelopeMail({
1869
- body,
1870
- mailID: args.mailID,
1871
- notify: this.callWakeForEnvelope(body),
1872
- targetDevice,
1873
- targetUser: args.targetUser,
1874
- });
1875
- return undefined;
1876
- }
1877
- catch (err) {
1878
- return err;
1879
- }
1880
- }));
1881
- for (const result of results) {
1882
- if (result !== undefined) {
1883
- lastErr = result;
1884
- failCount += 1;
1885
- }
1886
- }
1887
- }
1888
- if (failCount > 0) {
1889
- const base = lastErr instanceof Error ? lastErr : new Error(String(lastErr));
1890
- if (failCount === args.bodies.length) {
1891
- throw base;
1892
- }
1893
- const partial = new Error(`Call signaling failed to reach ${String(failCount)} of ` +
1894
- `${String(args.bodies.length)} peer device(s).`);
1895
- partial.cause = base;
1896
- throw partial;
1897
- }
1898
- }
1899
1701
  deliverMailResource(msg, header, mail) {
1900
1702
  if (this.mailBatchUnsupported) {
1901
1703
  return this.deliverMailResourceOverSocket(msg, header);
@@ -1968,50 +1770,12 @@ export class Client {
1968
1770
  timestamp,
1969
1771
  });
1970
1772
  }
1971
- fetchActiveCalls() {
1972
- const 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 };
1773
+ async fetchActiveCalls() {
1774
+ const res = await this.http.get(this.getHost() + "/calls/active", {
1775
+ responseType: "json",
1776
+ });
1777
+ return z.object({ calls: z.array(CallSessionSchema) }).parse(res.data)
1778
+ .calls;
2015
1779
  }
2016
1780
  async fetchIceServers() {
2017
1781
  const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
@@ -2115,23 +1879,6 @@ export class Client {
2115
1879
  }
2116
1880
  throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
2117
1881
  }
2118
- async fetchUserOrThrow(userID) {
2119
- if (userID === this.getUser().userID) {
2120
- return this.getUser();
2121
- }
2122
- const cached = this.userRecords[userID];
2123
- if (cached) {
2124
- return cached;
2125
- }
2126
- const [user, err] = await this.fetchUser(userID);
2127
- if (err) {
2128
- throw err;
2129
- }
2130
- if (!user) {
2131
- throw new Error(`User not found: ${userID}`);
2132
- }
2133
- return user;
2134
- }
2135
1882
  /**
2136
1883
  * Finish a passkey login and adopt the resulting JWT as the
2137
1884
  * client's bearer token. After this call, `client.passkeys.*`
@@ -2264,6 +2011,18 @@ export class Client {
2264
2011
  targets: targetDevices.length,
2265
2012
  });
2266
2013
  }
2014
+ async getAccountEntitlements() {
2015
+ const res = await this.http.get(this.getHost() + "/user/" + this.getUser().userID + "/entitlements");
2016
+ return decodeHttpResponse(AccountEntitlementsCodec, res.data);
2017
+ }
2018
+ async getBillingAccountState() {
2019
+ const res = await this.http.get(this.getHost() + "/billing/account");
2020
+ return decodeHttpResponse(BillingAccountStateCodec, res.data);
2021
+ }
2022
+ async getBillingProducts() {
2023
+ const res = await this.http.get(this.getHost() + "/billing/products");
2024
+ return decodeHttpResponse(BillingProductArrayCodec, res.data);
2025
+ }
2267
2026
  async getChannelByID(channelID) {
2268
2027
  try {
2269
2028
  const res = await this.http.get(this.getHost() + "/channel/" + channelID);
@@ -2518,14 +2277,6 @@ export class Client {
2518
2277
  }
2519
2278
  break;
2520
2279
  }
2521
- case "callWake": {
2522
- const parsed = callWakeNotifyData.safeParse(msg.data);
2523
- await this.getMail();
2524
- if (parsed.success) {
2525
- this.emitter.emit("callWake", parsed.data);
2526
- }
2527
- break;
2528
- }
2529
2280
  case "deviceRequest": {
2530
2281
  const parsed = deviceRequestNotifyData.safeParse(msg.data);
2531
2282
  if (parsed.success) {
@@ -2743,67 +2494,6 @@ export class Client {
2743
2494
  const response = await this.http.get(this.getHost() + "/user/" + userID + "/passkeys");
2744
2495
  return decodeHttpResponse(PasskeyArrayCodec, response.data);
2745
2496
  }
2746
- makeCallEnvelopeBody(args) {
2747
- return {
2748
- action: args.action,
2749
- callID: args.state.session.callID,
2750
- conversationID: args.state.session.conversationID,
2751
- conversationType: args.state.session.conversationType,
2752
- createdAt: args.state.session.createdAt,
2753
- createdBy: args.state.session.createdBy,
2754
- createdByDeviceID: args.state.session.createdByDeviceID,
2755
- expiresAt: args.expiresAt,
2756
- fromDeviceID: this.getDevice().deviceID,
2757
- fromUserID: this.getUser().userID,
2758
- media: "audio",
2759
- sequence: args.sequence,
2760
- ...(args.signal ? { signal: args.signal } : {}),
2761
- toDeviceID: args.toDeviceID,
2762
- toUserID: args.toUserID,
2763
- version: 1,
2764
- };
2765
- }
2766
- markLocalCallAction(state, action, signal) {
2767
- const now = new Date().toISOString();
2768
- if (action === "accept") {
2769
- state.session.status = "active";
2770
- state.session.expiresAt = new Date(Date.now() + CALL_MAX_TTL_MS).toISOString();
2771
- this.upsertCallParticipant(state.session, {
2772
- acceptedAt: now,
2773
- deviceID: this.getDevice().deviceID,
2774
- joinedAt: now,
2775
- state: "accepted",
2776
- userID: this.getUser().userID,
2777
- });
2778
- }
2779
- else if (action === "cancel" ||
2780
- action === "end" ||
2781
- action === "hangup" ||
2782
- action === "reject" ||
2783
- action === "timeout") {
2784
- state.session.status = "ended";
2785
- state.session.endedAt = now;
2786
- this.upsertCallParticipant(state.session, {
2787
- leftAt: now,
2788
- state: action === "reject" ? "rejected" : "left",
2789
- userID: this.getUser().userID,
2790
- });
2791
- }
2792
- const event = {
2793
- action,
2794
- call: cloneCallSession(state.session),
2795
- fromDeviceID: this.getDevice().deviceID,
2796
- fromUserID: this.getUser().userID,
2797
- ...(signal ? { signal } : {}),
2798
- };
2799
- if (state.session.status === "ended") {
2800
- this.callStates.delete(state.session.callID);
2801
- }
2802
- else {
2803
- this.callStates.set(state.session.callID, state);
2804
- }
2805
- return event;
2806
- }
2807
2497
  async markSessionVerified(sessionID) {
2808
2498
  return this.database.markSessionVerified(sessionID);
2809
2499
  }
@@ -2986,29 +2676,6 @@ export class Client {
2986
2676
  }
2987
2677
  }
2988
2678
  }
2989
- async processDecryptedCallEnvelope(args) {
2990
- const body = args.envelope.body;
2991
- if (body.fromDeviceID !== args.mail.sender ||
2992
- body.fromUserID !== args.mail.authorID ||
2993
- body.toDeviceID !== args.mail.recipient ||
2994
- body.toUserID !== args.mail.readerID ||
2995
- body.toDeviceID !== this.getDevice().deviceID ||
2996
- body.toUserID !== this.getUser().userID) {
2997
- return null;
2998
- }
2999
- const senderDevice = await this.getDeviceByID(body.fromDeviceID);
3000
- if (!senderDevice || senderDevice.owner !== body.fromUserID) {
3001
- return null;
3002
- }
3003
- const opened = await xSignOpenAsync(XUtils.decodeHex(args.envelope.signed), XUtils.decodeHex(senderDevice.signKey));
3004
- if (!opened) {
3005
- return null;
3006
- }
3007
- if (!XUtils.bytesEqual(opened, canonicalJsonBytes(body))) {
3008
- return null;
3009
- }
3010
- return this.applyCallEnvelopeBody(body);
3011
- }
3012
2679
  async publishPendingDeviceRegistration(args) {
3013
2680
  const signed = await this.signPendingRegistrationChallenge(args.challenge);
3014
2681
  await this.http.post(this.getHost() +
@@ -3261,52 +2928,39 @@ export class Client {
3261
2928
  if (!mail.forward) {
3262
2929
  plaintext = XUtils.encodeUTF8(unsealed);
3263
2930
  }
3264
- const callEnvelope = mail.forward
2931
+ const decodedPlaintext = mail.forward
3265
2932
  ? 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);
2933
+ : decodeMessagePlaintext(plaintext);
2934
+ // emit the message
2935
+ const fwdMsg1 = mail.forward
2936
+ ? messageSchema.parse(msgpack.decode(unsealed))
2937
+ : null;
2938
+ const message = fwdMsg1
2939
+ ? {
2940
+ ...normalizeForwardedMessage(fwdMsg1),
2941
+ forward: true,
3309
2942
  }
2943
+ : {
2944
+ authorID: mail.authorID,
2945
+ decrypted: true,
2946
+ direction: "incoming",
2947
+ forward: mail.forward,
2948
+ group: mail.group
2949
+ ? uuid.stringify(mail.group)
2950
+ : null,
2951
+ mailID: mail.mailID,
2952
+ ...messageFromDecodedPlaintext(decodedPlaintext ?? {
2953
+ message: plaintext,
2954
+ }),
2955
+ nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
2956
+ readerID: mail.readerID,
2957
+ recipient: mail.recipient,
2958
+ sender: mail.sender,
2959
+ timestamp: timestamp,
2960
+ };
2961
+ const shouldEmitIncomingInitial = mail.forward || plaintext.length > 0;
2962
+ if (shouldEmitIncomingInitial) {
2963
+ this.emitter.emit("message", message);
3310
2964
  }
3311
2965
  if (libvexDebugDmEnabled()) {
3312
2966
  try {
@@ -3472,47 +3126,33 @@ export class Client {
3472
3126
  ? messageSchema.parse(msgpack.decode(decrypted))
3473
3127
  : null;
3474
3128
  const rawIncoming = XUtils.encodeUTF8(decrypted);
3475
- const callEnvelope = mail.forward
3129
+ const decodedPlaintext = mail.forward
3476
3130
  ? 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);
3131
+ : decodeMessagePlaintext(rawIncoming);
3132
+ const message = fwdMsg2
3133
+ ? {
3134
+ ...normalizeForwardedMessage(fwdMsg2),
3135
+ forward: true,
3485
3136
  }
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
- }
3137
+ : {
3138
+ authorID: mail.authorID,
3139
+ decrypted: true,
3140
+ direction: "incoming",
3141
+ forward: mail.forward,
3142
+ group: mail.group
3143
+ ? uuid.stringify(mail.group)
3144
+ : null,
3145
+ mailID: mail.mailID,
3146
+ ...messageFromDecodedPlaintext(decodedPlaintext ?? {
3147
+ message: rawIncoming,
3148
+ }),
3149
+ nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
3150
+ readerID: mail.readerID,
3151
+ recipient: mail.recipient,
3152
+ sender: mail.sender,
3153
+ timestamp: timestamp,
3154
+ };
3155
+ this.emitter.emit("message", message);
3516
3156
  const sqlPatch = sessionToSqlPatch(session);
3517
3157
  const persisted = {
3518
3158
  CKr: sqlPatch.CKr,
@@ -3850,44 +3490,68 @@ export class Client {
3850
3490
  throw err;
3851
3491
  }
3852
3492
  }
3853
- async 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({
3493
+ async sendCallResource(action, data) {
3494
+ const msg = {
3873
3495
  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,
3496
+ data,
3497
+ resourceType: "call",
3498
+ transmissionID: uuid.v4(),
3499
+ type: "resource",
3500
+ };
3501
+ return await new Promise((resolve, reject) => {
3502
+ const settle = (err, event) => {
3503
+ this.socket.off("message", callback);
3504
+ if (err !== null) {
3505
+ reject(errorFromUnknown(err));
3506
+ return;
3507
+ }
3508
+ if (!event) {
3509
+ reject(new Error("Call signaling response was empty."));
3510
+ return;
3511
+ }
3512
+ resolve(event);
3513
+ };
3514
+ const callback = (packedMsg) => {
3515
+ const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
3516
+ if (receivedMsg.transmissionID !== msg.transmissionID) {
3517
+ return;
3518
+ }
3519
+ const parsed = WSMessageSchema.safeParse(receivedMsg);
3520
+ if (!parsed.success) {
3521
+ settle("Call signaling failed: " + JSON.stringify(receivedMsg));
3522
+ return;
3523
+ }
3524
+ if (parsed.data.type === "success") {
3525
+ const event = CallEventSchema.safeParse(parsed.data.data);
3526
+ if (!event.success) {
3527
+ settle("Invalid call signaling response: " +
3528
+ JSON.stringify(event.error.issues));
3529
+ return;
3530
+ }
3531
+ settle(null, event.data);
3532
+ return;
3533
+ }
3534
+ if (parsed.data.type === "error") {
3535
+ settle(new Error(parsed.data.error));
3536
+ return;
3537
+ }
3538
+ if (parsed.data.type === "notify" &&
3539
+ (parsed.data.event === "call" ||
3540
+ parsed.data.event === "callInvite")) {
3541
+ const event = CallEventSchema.safeParse(parsed.data.data);
3542
+ if (event.success) {
3543
+ settle(null, event.data);
3544
+ }
3545
+ return;
3546
+ }
3547
+ settle("Unexpected call signaling response: " +
3548
+ JSON.stringify(parsed.data));
3549
+ };
3550
+ this.socket.on("message", callback);
3551
+ this.send(msg).catch((err) => {
3552
+ settle(err);
3553
+ });
3885
3554
  });
3886
- if (targets.length === 1) {
3887
- state.peerDeviceID = targets[0]?.deviceID;
3888
- }
3889
- state.session.expiresAt = expiresAt;
3890
- return this.markLocalCallAction(state, action, signal);
3891
3555
  }
3892
3556
  async sendGroupMessage(channelID, message, opts) {
3893
3557
  const userList = await this.getUserList(channelID);
@@ -3976,7 +3640,7 @@ export class Client {
3976
3640
  }
3977
3641
  }
3978
3642
  /* Sends encrypted mail to a user. */
3979
- async sendMail(device, user, msg, group, mailID, forward, retry = false, notify) {
3643
+ async sendMail(device, user, msg, group, mailID, forward, retry = false) {
3980
3644
  while (this.sending.has(device.deviceID)) {
3981
3645
  await sleep(100);
3982
3646
  }
@@ -3991,7 +3655,7 @@ export class Client {
3991
3655
  retry: String(retry),
3992
3656
  });
3993
3657
  }
3994
- const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false, notify);
3658
+ const createdMessage = await this.createSession(device, user, msg, group, mailID, forward, false);
3995
3659
  if (libvexDebugDmEnabled()) {
3996
3660
  debugLibvexDm("sendMail: createSession returned", {
3997
3661
  peerDevice: device.deviceID,
@@ -4030,43 +3694,34 @@ export class Client {
4030
3694
  recipient: device.deviceID,
4031
3695
  sender: this.getDevice().deviceID,
4032
3696
  };
4033
- const wireMail = notify ? { ...mail, notify } : mail;
4034
3697
  const msgb = {
4035
3698
  action: "CREATE",
4036
- data: wireMail,
3699
+ data: mail,
4037
3700
  resourceType: "mail",
4038
3701
  transmissionID: uuid.v4(),
4039
3702
  type: "resource",
4040
3703
  };
4041
3704
  const hmac = xHMAC(mail, messageKey);
4042
- const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
4043
- const callEnvelope = forward
4044
- ? null
4045
- : decodeCallEnvelopePlaintext(rawPlaintext);
4046
3705
  const fwdOut = forward
4047
3706
  ? messageSchema.parse(msgpack.decode(msg))
4048
3707
  : null;
4049
3708
  const outMsg = fwdOut
4050
3709
  ? { ...normalizeForwardedMessage(fwdOut), forward: true }
4051
- : 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
- }
3710
+ : {
3711
+ authorID: mail.authorID,
3712
+ decrypted: true,
3713
+ direction: "outgoing",
3714
+ forward: mail.forward,
3715
+ group: mail.group ? uuid.stringify(mail.group) : null,
3716
+ mailID: mail.mailID,
3717
+ ...messageFromDecodedPlaintext(decodeMessagePlaintext(XUtils.encodeUTF8(msg))),
3718
+ nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
3719
+ readerID: mail.readerID,
3720
+ recipient: mail.recipient,
3721
+ sender: mail.sender,
3722
+ timestamp: new Date().toISOString(),
3723
+ };
3724
+ this.emitter.emit("message", outMsg);
4070
3725
  const sqlPatch = sessionToSqlPatch(session);
4071
3726
  const persisted = {
4072
3727
  CKr: sqlPatch.CKr,
@@ -4091,22 +3746,22 @@ export class Client {
4091
3746
  };
4092
3747
  await this.database.saveSession(persisted);
4093
3748
  this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
4094
- await this.deliverMailResource(msgb, hmac, wireMail);
3749
+ await this.deliverMailResource(msgb, hmac, mail);
4095
3750
  return outMsg;
4096
3751
  }
4097
3752
  finally {
4098
3753
  this.sending.delete(device.deviceID);
4099
3754
  }
4100
3755
  }
4101
- async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false, notify) {
3756
+ async sendMailWithRecovery(device, user, msg, group, mailID, forward, forceFreshSession = false) {
4102
3757
  try {
4103
- return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession, notify);
3758
+ return await this.sendMail(device, user, msg, group, mailID, forward, forceFreshSession);
4104
3759
  }
4105
3760
  catch (err) {
4106
3761
  if (!this.shouldRetryDeliveryWithFreshSession(err)) {
4107
3762
  throw err;
4108
3763
  }
4109
- return await this.sendMail(device, user, msg, group, mailID, forward, true, notify);
3764
+ return await this.sendMail(device, user, msg, group, mailID, forward, true);
4110
3765
  }
4111
3766
  }
4112
3767
  async sendMessage(userID, message, opts) {
@@ -4241,35 +3896,19 @@ export class Client {
4241
3896
  // Don't surface a teardown race as an unhandled rejection.
4242
3897
  this.send(receipt).catch(ignoreSocketTeardown);
4243
3898
  }
4244
- sessionFromCallEnvelope(body) {
4245
- const session = {
4246
- callID: body.callID,
4247
- conversationID: body.conversationID,
4248
- conversationType: body.conversationType,
4249
- createdAt: body.createdAt,
4250
- createdBy: body.createdBy,
4251
- createdByDeviceID: body.createdByDeviceID,
4252
- expiresAt: body.expiresAt,
4253
- media: "audio",
4254
- participants: [],
4255
- status: body.action === "invite" ? "ringing" : "active",
4256
- };
4257
- this.upsertCallParticipant(session, {
4258
- acceptedAt: body.createdAt,
4259
- deviceID: body.createdByDeviceID,
4260
- joinedAt: body.createdAt,
4261
- state: "accepted",
4262
- userID: body.createdBy,
4263
- });
4264
- this.upsertCallParticipant(session, {
4265
- state: "ringing",
4266
- userID: body.conversationID,
4267
- });
4268
- return session;
4269
- }
4270
3899
  setAlive(status) {
4271
3900
  this.isAlive = status;
4272
3901
  }
3902
+ async setDevAccountTier(tier, options) {
3903
+ const res = await this.http.patch(this.getHost() +
3904
+ "/__dev/user/" +
3905
+ this.getUser().userID +
3906
+ "/entitlements", {
3907
+ expiresAt: options?.expiresAt ?? null,
3908
+ tier,
3909
+ });
3910
+ return decodeHttpResponse(AccountEntitlementsCodec, res.data);
3911
+ }
4273
3912
  setUser(user) {
4274
3913
  this.user = user;
4275
3914
  // Fresh identity / token: drop stale 404 negative-cache entries so a
@@ -4299,65 +3938,13 @@ export class Client {
4299
3938
  async signPendingRegistrationChallenge(challengeHex) {
4300
3939
  return XUtils.encodeHex(await xSignAsync(XUtils.decodeHex(challengeHex), this.signKeys.secretKey));
4301
3940
  }
4302
- async 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
- };
3941
+ async submitAppleTransaction(request) {
3942
+ const res = await this.http.post(this.getHost() + "/billing/apple/transactions", request);
3943
+ return decodeHttpResponse(BillingAccountStateCodec, res.data);
3944
+ }
3945
+ async submitGooglePurchase(request) {
3946
+ const res = await this.http.post(this.getHost() + "/billing/google/purchases", request);
3947
+ return decodeHttpResponse(BillingAccountStateCodec, res.data);
4361
3948
  }
4362
3949
  async submitOTK(amount) {
4363
3950
  const otks = [];
@@ -4456,13 +4043,5 @@ export class Client {
4456
4043
  return null;
4457
4044
  }
4458
4045
  }
4459
- upsertCallParticipant(session, patch) {
4460
- const existing = session.participants.find((participant) => participant.userID === patch.userID);
4461
- if (!existing) {
4462
- session.participants.push({ ...patch });
4463
- return;
4464
- }
4465
- Object.assign(existing, patch);
4466
- }
4467
4046
  }
4468
4047
  //# sourceMappingURL=Client.js.map