@vex-chat/libvex 7.3.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Client.d.ts +53 -39
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +232 -653
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +18 -0
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +4 -1
- package/dist/codecs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +348 -853
- package/src/__tests__/harness/shared-suite.ts +1 -80
- package/src/codecs.ts +8 -0
- package/src/index.ts +9 -0
package/src/Client.ts
CHANGED
|
@@ -14,10 +14,14 @@ import type {
|
|
|
14
14
|
} from "./types/index.js";
|
|
15
15
|
import type { KeyPair } from "@vex-chat/crypto";
|
|
16
16
|
import type {
|
|
17
|
+
AccountEntitlements,
|
|
18
|
+
AccountTier,
|
|
17
19
|
ActionToken,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
AppleTransactionVerificationRequest,
|
|
21
|
+
BillingAccountState,
|
|
22
|
+
BillingProduct,
|
|
20
23
|
CallEvent,
|
|
24
|
+
CallResourceData,
|
|
21
25
|
CallSession,
|
|
22
26
|
CallSignalPayload,
|
|
23
27
|
ChallMsg,
|
|
@@ -27,10 +31,10 @@ import type {
|
|
|
27
31
|
Emoji,
|
|
28
32
|
FileResponse,
|
|
29
33
|
FileSQL,
|
|
34
|
+
GooglePurchaseVerificationRequest,
|
|
30
35
|
IceServerConfig,
|
|
31
36
|
Invite,
|
|
32
37
|
KeyBundle,
|
|
33
|
-
MailNotificationHint,
|
|
34
38
|
MailWS,
|
|
35
39
|
NotifyMsg,
|
|
36
40
|
Passkey,
|
|
@@ -44,7 +48,6 @@ import type {
|
|
|
44
48
|
Server,
|
|
45
49
|
ServerChannelBootstrap,
|
|
46
50
|
SessionSQL,
|
|
47
|
-
SignedCallEnvelope,
|
|
48
51
|
} from "@vex-chat/types";
|
|
49
52
|
import type { ClientMessage } from "@vex-chat/types";
|
|
50
53
|
|
|
@@ -78,13 +81,12 @@ import {
|
|
|
78
81
|
XUtils,
|
|
79
82
|
} from "@vex-chat/crypto";
|
|
80
83
|
import {
|
|
81
|
-
CallEnvelopeBodySchema,
|
|
82
84
|
CallEventSchema,
|
|
85
|
+
CallSessionSchema,
|
|
83
86
|
IceServerConfigSchema,
|
|
84
87
|
MailType,
|
|
85
88
|
MailWSSchema,
|
|
86
89
|
PermissionSchema,
|
|
87
|
-
SignedCallEnvelopeSchema,
|
|
88
90
|
WSMessageSchema,
|
|
89
91
|
} from "@vex-chat/types";
|
|
90
92
|
|
|
@@ -202,6 +204,16 @@ function debugLibvexDm(
|
|
|
202
204
|
console.error(`[libvex:debug-dm] ${payload}`);
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
function errorFromUnknown(err: unknown): Error {
|
|
208
|
+
if (err instanceof Error) {
|
|
209
|
+
return err;
|
|
210
|
+
}
|
|
211
|
+
if (typeof err === "string") {
|
|
212
|
+
return new Error(err);
|
|
213
|
+
}
|
|
214
|
+
return new Error(JSON.stringify(err));
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
function ignoreSocketTeardown(err: unknown): void {
|
|
206
218
|
if (err instanceof WebSocketNotOpenError) return;
|
|
207
219
|
// Re-throw anything else as a real unhandled rejection so it
|
|
@@ -329,8 +341,11 @@ function spireErrorBodyMessage(data: unknown, max = 8_000): string {
|
|
|
329
341
|
|
|
330
342
|
import { msgpack } from "./codec.js";
|
|
331
343
|
import {
|
|
344
|
+
AccountEntitlementsCodec,
|
|
332
345
|
ActionTokenCodec,
|
|
333
346
|
AuthResponseCodec,
|
|
347
|
+
BillingAccountStateCodec,
|
|
348
|
+
BillingProductArrayCodec,
|
|
334
349
|
ChannelArrayCodec,
|
|
335
350
|
ChannelCodec,
|
|
336
351
|
ConnectResponseCodec,
|
|
@@ -368,6 +383,24 @@ import { uuidToUint8 } from "./utils/uint8uuid.js";
|
|
|
368
383
|
|
|
369
384
|
const _protocolMsgRegex = /��\w+:\w+��/g;
|
|
370
385
|
|
|
386
|
+
/**
|
|
387
|
+
* @ignore
|
|
388
|
+
*/
|
|
389
|
+
export interface Billing {
|
|
390
|
+
/** Returns the configured StoreKit / Play Billing product catalog. */
|
|
391
|
+
products: () => Promise<BillingProduct[]>;
|
|
392
|
+
/** Returns the current billing subscriptions and derived entitlements. */
|
|
393
|
+
retrieve: () => Promise<BillingAccountState>;
|
|
394
|
+
/** Submits an App Store transaction for server-side verification. */
|
|
395
|
+
submitAppleTransaction: (
|
|
396
|
+
request: AppleTransactionVerificationRequest,
|
|
397
|
+
) => Promise<BillingAccountState>;
|
|
398
|
+
/** Submits a Google Play purchase token for server-side verification. */
|
|
399
|
+
submitGooglePurchase: (
|
|
400
|
+
request: GooglePurchaseVerificationRequest,
|
|
401
|
+
) => Promise<BillingAccountState>;
|
|
402
|
+
}
|
|
403
|
+
|
|
371
404
|
/**
|
|
372
405
|
* Voice-call signaling operations.
|
|
373
406
|
*
|
|
@@ -445,6 +478,18 @@ export interface ClientOptions {
|
|
|
445
478
|
|
|
446
479
|
export type DeviceRegistrationResult = Device | PendingDeviceRegistration;
|
|
447
480
|
|
|
481
|
+
/**
|
|
482
|
+
* Permission is a permission to a resource.
|
|
483
|
+
*
|
|
484
|
+
* Common fields:
|
|
485
|
+
* - `permissionID`: unique permission row ID
|
|
486
|
+
* - `userID`: user receiving this grant
|
|
487
|
+
* - `resourceID`: target server/channel/etc.
|
|
488
|
+
* - `resourceType`: type string for the resource
|
|
489
|
+
* - `powerLevel`: authorization level
|
|
490
|
+
*/
|
|
491
|
+
export type { Permission } from "@vex-chat/types";
|
|
492
|
+
|
|
448
493
|
/**
|
|
449
494
|
* @ignore
|
|
450
495
|
*/
|
|
@@ -529,18 +574,6 @@ export interface Devices {
|
|
|
529
574
|
retrieve: (deviceIdentifier: string) => Promise<Device | null>;
|
|
530
575
|
}
|
|
531
576
|
|
|
532
|
-
/**
|
|
533
|
-
* Permission is a permission to a resource.
|
|
534
|
-
*
|
|
535
|
-
* Common fields:
|
|
536
|
-
* - `permissionID`: unique permission row ID
|
|
537
|
-
* - `userID`: user receiving this grant
|
|
538
|
-
* - `resourceID`: target server/channel/etc.
|
|
539
|
-
* - `resourceType`: type string for the resource
|
|
540
|
-
* - `powerLevel`: authorization level
|
|
541
|
-
*/
|
|
542
|
-
export type { Permission } from "@vex-chat/types";
|
|
543
|
-
|
|
544
577
|
/**
|
|
545
578
|
* @ignore
|
|
546
579
|
*/
|
|
@@ -557,6 +590,23 @@ export interface Emojis {
|
|
|
557
590
|
retrieveList: (serverID: string) => Promise<Emoji[]>;
|
|
558
591
|
}
|
|
559
592
|
|
|
593
|
+
/**
|
|
594
|
+
* @ignore
|
|
595
|
+
*/
|
|
596
|
+
export interface Entitlements {
|
|
597
|
+
/** Retrieves the server-authoritative entitlement snapshot. */
|
|
598
|
+
retrieve: () => Promise<AccountEntitlements>;
|
|
599
|
+
/**
|
|
600
|
+
* Local development helper. Spire only serves this endpoint when
|
|
601
|
+
* `VEX_ENABLE_DEV_ENTITLEMENTS=1`, `DEV_API_KEY` is configured, and
|
|
602
|
+
* `NODE_ENV` is not `production`.
|
|
603
|
+
*/
|
|
604
|
+
setDevTier: (
|
|
605
|
+
tier: AccountTier,
|
|
606
|
+
options?: { expiresAt?: null | string },
|
|
607
|
+
) => Promise<AccountEntitlements>;
|
|
608
|
+
}
|
|
609
|
+
|
|
560
610
|
/**
|
|
561
611
|
* Device record associated with a user account.
|
|
562
612
|
*
|
|
@@ -724,7 +774,7 @@ export type { Server } from "@vex-chat/types";
|
|
|
724
774
|
export type { ServerChannelBootstrap } from "@vex-chat/types";
|
|
725
775
|
|
|
726
776
|
export interface NotificationSubscription {
|
|
727
|
-
channel:
|
|
777
|
+
channel: "expo";
|
|
728
778
|
createdAt: string;
|
|
729
779
|
deviceID: string;
|
|
730
780
|
enabled: boolean;
|
|
@@ -736,17 +786,9 @@ export interface NotificationSubscription {
|
|
|
736
786
|
userID: string;
|
|
737
787
|
}
|
|
738
788
|
|
|
739
|
-
export type NotificationSubscriptionChannel = "apnsVoip" | "expo" | "fcmCall";
|
|
740
|
-
|
|
741
|
-
const NotificationSubscriptionChannelSchema = z.enum([
|
|
742
|
-
"apnsVoip",
|
|
743
|
-
"expo",
|
|
744
|
-
"fcmCall",
|
|
745
|
-
]);
|
|
746
|
-
|
|
747
789
|
const NotificationSubscriptionSchema: z.ZodType<NotificationSubscription> =
|
|
748
790
|
z.object({
|
|
749
|
-
channel:
|
|
791
|
+
channel: z.literal("expo"),
|
|
750
792
|
createdAt: z.string(),
|
|
751
793
|
deviceID: z.string(),
|
|
752
794
|
enabled: z.boolean(),
|
|
@@ -759,7 +801,7 @@ const NotificationSubscriptionSchema: z.ZodType<NotificationSubscription> =
|
|
|
759
801
|
});
|
|
760
802
|
|
|
761
803
|
export interface NotificationSubscriptionInput {
|
|
762
|
-
channel:
|
|
804
|
+
channel: "expo";
|
|
763
805
|
events?: string[];
|
|
764
806
|
platform?: "android" | "ios" | "web";
|
|
765
807
|
token: string;
|
|
@@ -941,35 +983,17 @@ const messageSchema: z.ZodType<Message> = z.object({
|
|
|
941
983
|
timestamp: z.string(),
|
|
942
984
|
});
|
|
943
985
|
|
|
944
|
-
const CALL_ENVELOPE_PREFIX = "vex-call:1\n";
|
|
945
|
-
const CALL_INVITE_TTL_MS = 60_000;
|
|
946
|
-
const CALL_MAX_TTL_MS = 2 * 60 * 60 * 1000;
|
|
947
986
|
const MESSAGE_BLOB_PREFIX = "vex-message:1\n";
|
|
948
987
|
const MAIL_FANOUT_CONCURRENCY = 8;
|
|
949
988
|
const MAIL_BATCH_MAX_SIZE = 32;
|
|
950
989
|
const MAIL_BATCH_FLUSH_DELAY_MS = 8;
|
|
951
990
|
|
|
952
|
-
interface CallWakeNotifyData {
|
|
953
|
-
callID: string;
|
|
954
|
-
expiresAt?: string | undefined;
|
|
955
|
-
mailID?: string | undefined;
|
|
956
|
-
mailNonce?: string | undefined;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
991
|
interface DecodedMessagePlaintext {
|
|
960
992
|
extra?: null | string | undefined;
|
|
961
993
|
message: string;
|
|
962
994
|
retentionHintDays?: number | undefined;
|
|
963
995
|
}
|
|
964
996
|
|
|
965
|
-
interface EncryptedCallState {
|
|
966
|
-
peerDeviceID?: string | undefined;
|
|
967
|
-
peerUserID: string;
|
|
968
|
-
pendingPeerDevices: Device[];
|
|
969
|
-
sequence: number;
|
|
970
|
-
session: CallSession;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
997
|
interface PendingMailBatchDelivery {
|
|
974
998
|
header: Uint8Array;
|
|
975
999
|
mail: MailWS;
|
|
@@ -991,62 +1015,6 @@ const mailBatchResponseSchema = z.object({
|
|
|
991
1015
|
),
|
|
992
1016
|
});
|
|
993
1017
|
|
|
994
|
-
const callWakeNotifyData = z.object({
|
|
995
|
-
callID: z.string(),
|
|
996
|
-
expiresAt: z.string().optional(),
|
|
997
|
-
mailID: z.string().optional(),
|
|
998
|
-
mailNonce: z.string().optional(),
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
function canonicalizeJson(value: unknown): unknown {
|
|
1002
|
-
if (Array.isArray(value)) {
|
|
1003
|
-
return value.map((item) => canonicalizeJson(item));
|
|
1004
|
-
}
|
|
1005
|
-
if (!isRecord(value)) {
|
|
1006
|
-
return value;
|
|
1007
|
-
}
|
|
1008
|
-
const out: Record<string, unknown> = {};
|
|
1009
|
-
for (const key of Object.keys(value).sort()) {
|
|
1010
|
-
const item = value[key];
|
|
1011
|
-
if (item !== undefined) {
|
|
1012
|
-
out[key] = canonicalizeJson(item);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return out;
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function canonicalJsonBytes(value: unknown): Uint8Array {
|
|
1019
|
-
return XUtils.decodeUTF8(
|
|
1020
|
-
JSON.stringify(canonicalizeJson(jsonWireValue(value))),
|
|
1021
|
-
);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function cloneCallSession(session: CallSession): CallSession {
|
|
1025
|
-
return {
|
|
1026
|
-
...session,
|
|
1027
|
-
participants: session.participants.map((participant) => ({
|
|
1028
|
-
...participant,
|
|
1029
|
-
})),
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function decodeCallEnvelopePlaintext(
|
|
1034
|
-
plaintext: string,
|
|
1035
|
-
): null | SignedCallEnvelope {
|
|
1036
|
-
if (!plaintext.startsWith(CALL_ENVELOPE_PREFIX)) {
|
|
1037
|
-
return null;
|
|
1038
|
-
}
|
|
1039
|
-
try {
|
|
1040
|
-
const raw = JSON.parse(
|
|
1041
|
-
plaintext.slice(CALL_ENVELOPE_PREFIX.length),
|
|
1042
|
-
) as unknown;
|
|
1043
|
-
const parsed = SignedCallEnvelopeSchema.safeParse(raw);
|
|
1044
|
-
return parsed.success ? parsed.data : null;
|
|
1045
|
-
} catch {
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
1018
|
function decodeMessageBlob(body: string): DecodedMessagePlaintext {
|
|
1051
1019
|
if (!body.startsWith(MESSAGE_BLOB_PREFIX)) {
|
|
1052
1020
|
return { message: body };
|
|
@@ -1084,10 +1052,6 @@ function decodeMessagePlaintext(plaintext: string): DecodedMessagePlaintext {
|
|
|
1084
1052
|
: blob;
|
|
1085
1053
|
}
|
|
1086
1054
|
|
|
1087
|
-
function encodeCallEnvelopePlaintext(envelope: SignedCallEnvelope): Uint8Array {
|
|
1088
|
-
return XUtils.decodeUTF8(CALL_ENVELOPE_PREFIX + JSON.stringify(envelope));
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
1055
|
function encodeMessagePlaintext(
|
|
1092
1056
|
message: string,
|
|
1093
1057
|
opts?: MessageSendOptions,
|
|
@@ -1103,13 +1067,6 @@ function encodeMessagePlaintext(
|
|
|
1103
1067
|
return formatVexRetentionEnvelope(body, opts?.retentionHintDays);
|
|
1104
1068
|
}
|
|
1105
1069
|
|
|
1106
|
-
function jsonWireValue(value: unknown): unknown {
|
|
1107
|
-
// Match the payload JSON.stringify will send, including RTC toJSON() output.
|
|
1108
|
-
const encoded = JSON.stringify(value);
|
|
1109
|
-
const parsed: unknown = JSON.parse(encoded);
|
|
1110
|
-
return parsed;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
1070
|
function messageFromDecodedPlaintext(
|
|
1114
1071
|
decoded: DecodedMessagePlaintext,
|
|
1115
1072
|
): Pick<Message, "extra" | "message" | "retentionHintDays"> {
|
|
@@ -1122,12 +1079,6 @@ function messageFromDecodedPlaintext(
|
|
|
1122
1079
|
};
|
|
1123
1080
|
}
|
|
1124
1081
|
|
|
1125
|
-
function normalizeCallEnvelopeBodyForWire(
|
|
1126
|
-
body: CallEnvelopeBody,
|
|
1127
|
-
): CallEnvelopeBody {
|
|
1128
|
-
return CallEnvelopeBodySchema.parse(jsonWireValue(body));
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
1082
|
function normalizeForwardedMessage(message: Message): Message {
|
|
1132
1083
|
const decoded = decodeMessagePlaintext(message.message);
|
|
1133
1084
|
return {
|
|
@@ -1169,8 +1120,6 @@ const retryRequestNotifyData = z.union([
|
|
|
1169
1120
|
export interface ClientEvents {
|
|
1170
1121
|
/** Voice-call signaling changed or an incoming call was received. */
|
|
1171
1122
|
call: (event: CallEvent) => void;
|
|
1172
|
-
/** Native/mobile call wake hint arrived; clients should sync call mail. */
|
|
1173
|
-
callWake: (wake: CallWakeNotifyData) => void;
|
|
1174
1123
|
/** The client has been shut down (via {@link Client.close}). */
|
|
1175
1124
|
closed: () => void;
|
|
1176
1125
|
/** WebSocket authorized by the server; pre-auth setup begins. */
|
|
@@ -1417,6 +1366,14 @@ export class Client {
|
|
|
1417
1366
|
|
|
1418
1367
|
private static readonly NOT_FOUND_TTL = 30 * 60 * 1000;
|
|
1419
1368
|
|
|
1369
|
+
/** Store subscription billing methods. */
|
|
1370
|
+
public billing: Billing = {
|
|
1371
|
+
products: this.getBillingProducts.bind(this),
|
|
1372
|
+
retrieve: this.getBillingAccountState.bind(this),
|
|
1373
|
+
submitAppleTransaction: this.submitAppleTransaction.bind(this),
|
|
1374
|
+
submitGooglePurchase: this.submitGooglePurchase.bind(this),
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1420
1377
|
/**
|
|
1421
1378
|
* Voice-call signaling operations.
|
|
1422
1379
|
*
|
|
@@ -1425,21 +1382,25 @@ export class Client {
|
|
|
1425
1382
|
*/
|
|
1426
1383
|
public calls: Calls = {
|
|
1427
1384
|
accept: (callID: string, signal?: CallSignalPayload) =>
|
|
1428
|
-
this.
|
|
1385
|
+
this.sendCallResource("ACCEPT", {
|
|
1386
|
+
callID,
|
|
1387
|
+
...(signal ? { signal } : {}),
|
|
1388
|
+
}),
|
|
1429
1389
|
active: this.fetchActiveCalls.bind(this),
|
|
1430
|
-
cancel: (callID: string) =>
|
|
1431
|
-
|
|
1432
|
-
hangup: (callID: string) =>
|
|
1433
|
-
this.sendEncryptedCallAction("hangup", callID),
|
|
1390
|
+
cancel: (callID: string) => this.sendCallResource("CANCEL", { callID }),
|
|
1391
|
+
hangup: (callID: string) => this.sendCallResource("HANGUP", { callID }),
|
|
1434
1392
|
ice: (callID: string, signal: CallSignalPayload) =>
|
|
1435
|
-
this.
|
|
1393
|
+
this.sendCallResource("ICE", { callID, signal }),
|
|
1436
1394
|
iceServers: this.fetchIceServers.bind(this),
|
|
1437
|
-
reject: (callID: string) =>
|
|
1438
|
-
this.sendEncryptedCallAction("reject", callID),
|
|
1395
|
+
reject: (callID: string) => this.sendCallResource("REJECT", { callID }),
|
|
1439
1396
|
signal: (callID: string, signal: CallSignalPayload) =>
|
|
1440
|
-
this.
|
|
1397
|
+
this.sendCallResource("SIGNAL", { callID, signal }),
|
|
1441
1398
|
startDM: (recipientUserID: string, signal?: CallSignalPayload) =>
|
|
1442
|
-
this.
|
|
1399
|
+
this.sendCallResource("INVITE", {
|
|
1400
|
+
conversationType: "dm",
|
|
1401
|
+
recipientUserID,
|
|
1402
|
+
...(signal ? { signal } : {}),
|
|
1403
|
+
}),
|
|
1443
1404
|
};
|
|
1444
1405
|
|
|
1445
1406
|
/**
|
|
@@ -1521,6 +1482,12 @@ export class Client {
|
|
|
1521
1482
|
retrieveList: this.retrieveEmojiList.bind(this),
|
|
1522
1483
|
};
|
|
1523
1484
|
|
|
1485
|
+
/** Account entitlement methods. */
|
|
1486
|
+
public entitlements: Entitlements = {
|
|
1487
|
+
retrieve: this.getAccountEntitlements.bind(this),
|
|
1488
|
+
setDevTier: this.setDevAccountTier.bind(this),
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1524
1491
|
/** File upload/download methods. */
|
|
1525
1492
|
public files: Files = {
|
|
1526
1493
|
/**
|
|
@@ -1753,8 +1720,6 @@ export class Client {
|
|
|
1753
1720
|
|
|
1754
1721
|
private autoReconnectEnabled = false;
|
|
1755
1722
|
|
|
1756
|
-
private readonly callStates = new Map<string, EncryptedCallState>();
|
|
1757
|
-
|
|
1758
1723
|
private readonly cryptoProfile: CryptoProfile;
|
|
1759
1724
|
|
|
1760
1725
|
private readonly database: Storage;
|
|
@@ -1764,12 +1729,13 @@ export class Client {
|
|
|
1764
1729
|
private readonly decryptFailureCounts = new Map<string, number>();
|
|
1765
1730
|
|
|
1766
1731
|
private device?: Device;
|
|
1767
|
-
private deviceRecords: Record<string, Device> = {};
|
|
1768
1732
|
|
|
1733
|
+
private deviceRecords: Record<string, Device> = {};
|
|
1769
1734
|
// ── Event subscription (composition over inheritance) ───────────────
|
|
1770
1735
|
private readonly emitter = new EventEmitter<ClientEvents>();
|
|
1771
1736
|
|
|
1772
1737
|
private fetchingMail: boolean = false;
|
|
1738
|
+
|
|
1773
1739
|
private firstMailFetch = true;
|
|
1774
1740
|
private readonly forwarded = new Set<string>();
|
|
1775
1741
|
private readonly host: string;
|
|
@@ -1777,8 +1743,8 @@ export class Client {
|
|
|
1777
1743
|
/** Cancels in-flight HTTP work on `close()` so `postAuth`/`getMail` cannot hang forever. */
|
|
1778
1744
|
private readonly httpAbortController = new AbortController();
|
|
1779
1745
|
private readonly idKeys: KeyPair | null;
|
|
1780
|
-
|
|
1781
1746
|
private isAlive: boolean = true;
|
|
1747
|
+
|
|
1782
1748
|
private localMessageRetentionDays: number;
|
|
1783
1749
|
private localRetentionPurgeTimer: null | ReturnType<typeof setInterval> =
|
|
1784
1750
|
null;
|
|
@@ -2666,92 +2632,6 @@ export class Client {
|
|
|
2666
2632
|
this.acknowledgeInboundMail(mail);
|
|
2667
2633
|
}
|
|
2668
2634
|
|
|
2669
|
-
private applyCallEnvelopeBody(body: CallEnvelopeBody): CallEvent {
|
|
2670
|
-
const localUserID = this.getUser().userID;
|
|
2671
|
-
const peerUserID =
|
|
2672
|
-
body.fromUserID === localUserID ? body.toUserID : body.fromUserID;
|
|
2673
|
-
const peerDeviceID =
|
|
2674
|
-
body.fromUserID === localUserID
|
|
2675
|
-
? body.toDeviceID
|
|
2676
|
-
: body.fromDeviceID;
|
|
2677
|
-
let state = this.callStates.get(body.callID);
|
|
2678
|
-
if (!state) {
|
|
2679
|
-
state = {
|
|
2680
|
-
peerDeviceID,
|
|
2681
|
-
peerUserID,
|
|
2682
|
-
pendingPeerDevices: [],
|
|
2683
|
-
sequence: 0,
|
|
2684
|
-
session: this.sessionFromCallEnvelope(body),
|
|
2685
|
-
};
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
state.peerUserID = peerUserID;
|
|
2689
|
-
if (body.fromUserID !== localUserID || body.action === "accept") {
|
|
2690
|
-
state.peerDeviceID = peerDeviceID;
|
|
2691
|
-
}
|
|
2692
|
-
state.sequence = Math.max(state.sequence, body.sequence);
|
|
2693
|
-
state.session.expiresAt = body.expiresAt;
|
|
2694
|
-
|
|
2695
|
-
const now = new Date().toISOString();
|
|
2696
|
-
switch (body.action) {
|
|
2697
|
-
case "accept":
|
|
2698
|
-
state.session.status = "active";
|
|
2699
|
-
this.upsertCallParticipant(state.session, {
|
|
2700
|
-
acceptedAt: now,
|
|
2701
|
-
deviceID: body.fromDeviceID,
|
|
2702
|
-
joinedAt: now,
|
|
2703
|
-
state: "accepted",
|
|
2704
|
-
userID: body.fromUserID,
|
|
2705
|
-
});
|
|
2706
|
-
break;
|
|
2707
|
-
case "cancel":
|
|
2708
|
-
case "end":
|
|
2709
|
-
case "hangup":
|
|
2710
|
-
case "reject":
|
|
2711
|
-
case "timeout":
|
|
2712
|
-
state.session.status = "ended";
|
|
2713
|
-
state.session.endedAt = now;
|
|
2714
|
-
this.upsertCallParticipant(state.session, {
|
|
2715
|
-
leftAt: now,
|
|
2716
|
-
state: body.action === "reject" ? "rejected" : "left",
|
|
2717
|
-
userID: body.fromUserID,
|
|
2718
|
-
});
|
|
2719
|
-
break;
|
|
2720
|
-
case "ice":
|
|
2721
|
-
case "signal":
|
|
2722
|
-
break;
|
|
2723
|
-
case "invite":
|
|
2724
|
-
state.session.status = "ringing";
|
|
2725
|
-
this.upsertCallParticipant(state.session, {
|
|
2726
|
-
acceptedAt: body.createdAt,
|
|
2727
|
-
deviceID: body.createdByDeviceID,
|
|
2728
|
-
joinedAt: body.createdAt,
|
|
2729
|
-
state: "accepted",
|
|
2730
|
-
userID: body.createdBy,
|
|
2731
|
-
});
|
|
2732
|
-
this.upsertCallParticipant(state.session, {
|
|
2733
|
-
state: "ringing",
|
|
2734
|
-
userID: body.toUserID,
|
|
2735
|
-
});
|
|
2736
|
-
break;
|
|
2737
|
-
}
|
|
2738
|
-
|
|
2739
|
-
const event: CallEvent = {
|
|
2740
|
-
action: body.action,
|
|
2741
|
-
call: cloneCallSession(state.session),
|
|
2742
|
-
fromDeviceID: body.fromDeviceID,
|
|
2743
|
-
fromUserID: body.fromUserID,
|
|
2744
|
-
...(body.signal ? { signal: body.signal } : {}),
|
|
2745
|
-
};
|
|
2746
|
-
|
|
2747
|
-
if (state.session.status === "ended") {
|
|
2748
|
-
this.callStates.delete(body.callID);
|
|
2749
|
-
} else {
|
|
2750
|
-
this.callStates.set(body.callID, state);
|
|
2751
|
-
}
|
|
2752
|
-
return event;
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
2635
|
private async approveDeviceRequest(requestID: string): Promise<Device> {
|
|
2756
2636
|
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
2757
2637
|
if (!req) {
|
|
@@ -2833,50 +2713,6 @@ export class Client {
|
|
|
2833
2713
|
return decodeHttpResponse(PasskeyOptionsCodec, response.data);
|
|
2834
2714
|
}
|
|
2835
2715
|
|
|
2836
|
-
private async callEnvelopeForBody(
|
|
2837
|
-
body: CallEnvelopeBody,
|
|
2838
|
-
): Promise<SignedCallEnvelope> {
|
|
2839
|
-
const wireBody = normalizeCallEnvelopeBodyForWire(body);
|
|
2840
|
-
const signed = await xSignAsync(
|
|
2841
|
-
canonicalJsonBytes(wireBody),
|
|
2842
|
-
this.signKeys.secretKey,
|
|
2843
|
-
);
|
|
2844
|
-
return {
|
|
2845
|
-
body: wireBody,
|
|
2846
|
-
signed: XUtils.encodeHex(signed),
|
|
2847
|
-
};
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
private async callTargetsForState(
|
|
2851
|
-
state: EncryptedCallState,
|
|
2852
|
-
): Promise<Device[]> {
|
|
2853
|
-
if (state.peerDeviceID) {
|
|
2854
|
-
const cached = this.deviceRecords[state.peerDeviceID];
|
|
2855
|
-
const device =
|
|
2856
|
-
cached ?? (await this.getDeviceByID(state.peerDeviceID));
|
|
2857
|
-
if (!device) {
|
|
2858
|
-
throw new Error(
|
|
2859
|
-
`Call peer device not found: ${state.peerDeviceID}`,
|
|
2860
|
-
);
|
|
2861
|
-
}
|
|
2862
|
-
return [device];
|
|
2863
|
-
}
|
|
2864
|
-
return state.pendingPeerDevices;
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
private callWakeForEnvelope(
|
|
2868
|
-
body: CallEnvelopeBody,
|
|
2869
|
-
): MailNotificationHint | undefined {
|
|
2870
|
-
if (body.action !== "invite") {
|
|
2871
|
-
return undefined;
|
|
2872
|
-
}
|
|
2873
|
-
return {
|
|
2874
|
-
callID: body.callID,
|
|
2875
|
-
event: "callWake",
|
|
2876
|
-
expiresAt: body.expiresAt,
|
|
2877
|
-
};
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
2716
|
private censorPreKey(preKey: PreKeysSQL): PreKeysWS {
|
|
2881
2717
|
if (!preKey.index) {
|
|
2882
2718
|
throw new Error("Key index is required.");
|
|
@@ -3035,7 +2871,6 @@ export class Client {
|
|
|
3035
2871
|
* errors should not reject the full read pipeline.
|
|
3036
2872
|
*/
|
|
3037
2873
|
allowKeyBundleFailure = false,
|
|
3038
|
-
notify?: MailNotificationHint,
|
|
3039
2874
|
): Promise<Message | null> {
|
|
3040
2875
|
return this.runWithThisCryptoProfile(async () => {
|
|
3041
2876
|
let keyBundle: KeyBundle;
|
|
@@ -3153,13 +2988,12 @@ export class Client {
|
|
|
3153
2988
|
recipient: device.deviceID,
|
|
3154
2989
|
sender: this.getDevice().deviceID,
|
|
3155
2990
|
};
|
|
3156
|
-
const wireMail: MailWS = notify ? { ...mail, notify } : mail;
|
|
3157
2991
|
|
|
3158
2992
|
const hmac = xHMAC(mail, SK);
|
|
3159
2993
|
|
|
3160
2994
|
const msg: ResourceMsg = {
|
|
3161
2995
|
action: "CREATE",
|
|
3162
|
-
data:
|
|
2996
|
+
data: mail,
|
|
3163
2997
|
resourceType: "mail",
|
|
3164
2998
|
transmissionID: uuid.v4(),
|
|
3165
2999
|
type: "resource",
|
|
@@ -3183,41 +3017,35 @@ export class Client {
|
|
|
3183
3017
|
|
|
3184
3018
|
this.emitter.emit("session", sessionEntry, user);
|
|
3185
3019
|
|
|
3186
|
-
|
|
3187
|
-
const callEnvelope = forward
|
|
3188
|
-
? null
|
|
3189
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
3020
|
+
// emit the message
|
|
3190
3021
|
const forwardedMsg = forward
|
|
3191
3022
|
? messageSchema.parse(msgpack.decode(message))
|
|
3192
3023
|
: null;
|
|
3193
|
-
const
|
|
3024
|
+
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
3025
|
+
const emitMsg: Message = forwardedMsg
|
|
3194
3026
|
? { ...normalizeForwardedMessage(forwardedMsg), forward: true }
|
|
3195
|
-
:
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
timestamp: new Date().toISOString(),
|
|
3213
|
-
}
|
|
3214
|
-
: null;
|
|
3215
|
-
if (emitMsg) {
|
|
3027
|
+
: {
|
|
3028
|
+
authorID: mail.authorID,
|
|
3029
|
+
decrypted: true,
|
|
3030
|
+
direction: "outgoing",
|
|
3031
|
+
forward: mail.forward,
|
|
3032
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
3033
|
+
mailID: mail.mailID,
|
|
3034
|
+
...messageFromDecodedPlaintext(
|
|
3035
|
+
decodeMessagePlaintext(XUtils.encodeUTF8(message)),
|
|
3036
|
+
),
|
|
3037
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3038
|
+
readerID: mail.readerID,
|
|
3039
|
+
recipient: mail.recipient,
|
|
3040
|
+
sender: mail.sender,
|
|
3041
|
+
timestamp: new Date().toISOString(),
|
|
3042
|
+
};
|
|
3043
|
+
if (shouldEmitHandshakeMessage) {
|
|
3216
3044
|
this.emitter.emit("message", emitMsg);
|
|
3217
3045
|
}
|
|
3218
3046
|
|
|
3219
|
-
await this.deliverMailResource(msg, hmac,
|
|
3220
|
-
return emitMsg;
|
|
3047
|
+
await this.deliverMailResource(msg, hmac, mail);
|
|
3048
|
+
return shouldEmitHandshakeMessage ? emitMsg : null;
|
|
3221
3049
|
});
|
|
3222
3050
|
}
|
|
3223
3051
|
|
|
@@ -3258,68 +3086,6 @@ export class Client {
|
|
|
3258
3086
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
3259
3087
|
}
|
|
3260
3088
|
|
|
3261
|
-
private async deliverCallEnvelopeBatch(args: {
|
|
3262
|
-
bodies: CallEnvelopeBody[];
|
|
3263
|
-
mailID: string;
|
|
3264
|
-
targetUser: User;
|
|
3265
|
-
}): Promise<void> {
|
|
3266
|
-
let failCount = 0;
|
|
3267
|
-
let lastErr: unknown;
|
|
3268
|
-
for (
|
|
3269
|
-
let index = 0;
|
|
3270
|
-
index < args.bodies.length;
|
|
3271
|
-
index += MAIL_FANOUT_CONCURRENCY
|
|
3272
|
-
) {
|
|
3273
|
-
const batch = args.bodies.slice(
|
|
3274
|
-
index,
|
|
3275
|
-
index + MAIL_FANOUT_CONCURRENCY,
|
|
3276
|
-
);
|
|
3277
|
-
const results = await Promise.all(
|
|
3278
|
-
batch.map(async (body): Promise<undefined | unknown> => {
|
|
3279
|
-
try {
|
|
3280
|
-
const targetDevice =
|
|
3281
|
-
this.deviceRecords[body.toDeviceID] ??
|
|
3282
|
-
(await this.getDeviceByID(body.toDeviceID));
|
|
3283
|
-
if (!targetDevice) {
|
|
3284
|
-
throw new Error(
|
|
3285
|
-
`Call target device not found: ${body.toDeviceID}`,
|
|
3286
|
-
);
|
|
3287
|
-
}
|
|
3288
|
-
await this.sendCallEnvelopeMail({
|
|
3289
|
-
body,
|
|
3290
|
-
mailID: args.mailID,
|
|
3291
|
-
notify: this.callWakeForEnvelope(body),
|
|
3292
|
-
targetDevice,
|
|
3293
|
-
targetUser: args.targetUser,
|
|
3294
|
-
});
|
|
3295
|
-
return undefined;
|
|
3296
|
-
} catch (err: unknown) {
|
|
3297
|
-
return err;
|
|
3298
|
-
}
|
|
3299
|
-
}),
|
|
3300
|
-
);
|
|
3301
|
-
for (const result of results) {
|
|
3302
|
-
if (result !== undefined) {
|
|
3303
|
-
lastErr = result;
|
|
3304
|
-
failCount += 1;
|
|
3305
|
-
}
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
if (failCount > 0) {
|
|
3310
|
-
const base =
|
|
3311
|
-
lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
3312
|
-
if (failCount === args.bodies.length) {
|
|
3313
|
-
throw base;
|
|
3314
|
-
}
|
|
3315
|
-
const partial = new Error(
|
|
3316
|
-
`Call signaling failed to reach ${String(failCount)} of ` +
|
|
3317
|
-
`${String(args.bodies.length)} peer device(s).`,
|
|
3318
|
-
);
|
|
3319
|
-
partial.cause = base;
|
|
3320
|
-
throw partial;
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
3089
|
private deliverMailResource(
|
|
3324
3090
|
msg: ResourceMsg,
|
|
3325
3091
|
header: Uint8Array,
|
|
@@ -3405,61 +3171,13 @@ export class Client {
|
|
|
3405
3171
|
});
|
|
3406
3172
|
}
|
|
3407
3173
|
|
|
3408
|
-
private fetchActiveCalls(): Promise<CallSession[]> {
|
|
3409
|
-
const
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
Date.parse(state.session.expiresAt) <= now
|
|
3415
|
-
) {
|
|
3416
|
-
this.callStates.delete(callID);
|
|
3417
|
-
continue;
|
|
3418
|
-
}
|
|
3419
|
-
active.push(cloneCallSession(state.session));
|
|
3420
|
-
}
|
|
3421
|
-
return Promise.resolve(active);
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
|
-
private async fetchCallPeer(args: {
|
|
3425
|
-
userID: string;
|
|
3426
|
-
}): Promise<{ devices: Device[]; user: User }> {
|
|
3427
|
-
const [user, err] = await this.fetchUser(args.userID);
|
|
3428
|
-
if (err) {
|
|
3429
|
-
throw err;
|
|
3430
|
-
}
|
|
3431
|
-
if (!user) {
|
|
3432
|
-
throw new Error("Call peer not found.");
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
|
-
const afterBackoff = await this.fetchUserDeviceListWithBackoff(
|
|
3436
|
-
args.userID,
|
|
3437
|
-
"peer",
|
|
3438
|
-
);
|
|
3439
|
-
let deviceListRaw: Device[];
|
|
3440
|
-
try {
|
|
3441
|
-
const again = await this.fetchUserDeviceListOnce(args.userID);
|
|
3442
|
-
const byID = new Map<string, Device>();
|
|
3443
|
-
for (const device of afterBackoff) {
|
|
3444
|
-
byID.set(device.deviceID, device);
|
|
3445
|
-
}
|
|
3446
|
-
for (const device of again) {
|
|
3447
|
-
byID.set(device.deviceID, device);
|
|
3448
|
-
}
|
|
3449
|
-
deviceListRaw = [...byID.values()];
|
|
3450
|
-
} catch {
|
|
3451
|
-
deviceListRaw = afterBackoff;
|
|
3452
|
-
}
|
|
3453
|
-
|
|
3454
|
-
const devices = deviceListRaw
|
|
3455
|
-
.filter((device) => !device.deleted)
|
|
3456
|
-
.sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
|
|
3457
|
-
if (devices.length === 0) {
|
|
3458
|
-
throw new Error("Call peer has no active devices.");
|
|
3459
|
-
}
|
|
3460
|
-
return { devices, user };
|
|
3174
|
+
private async fetchActiveCalls(): Promise<CallSession[]> {
|
|
3175
|
+
const res = await this.http.get(this.getHost() + "/calls/active", {
|
|
3176
|
+
responseType: "json",
|
|
3177
|
+
});
|
|
3178
|
+
return z.object({ calls: z.array(CallSessionSchema) }).parse(res.data)
|
|
3179
|
+
.calls;
|
|
3461
3180
|
}
|
|
3462
|
-
|
|
3463
3181
|
private async fetchIceServers(): Promise<IceServerConfig[]> {
|
|
3464
3182
|
const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
|
|
3465
3183
|
responseType: "json",
|
|
@@ -3580,24 +3298,6 @@ export class Client {
|
|
|
3580
3298
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
3581
3299
|
}
|
|
3582
3300
|
|
|
3583
|
-
private async fetchUserOrThrow(userID: string): Promise<User> {
|
|
3584
|
-
if (userID === this.getUser().userID) {
|
|
3585
|
-
return this.getUser();
|
|
3586
|
-
}
|
|
3587
|
-
const cached = this.userRecords[userID];
|
|
3588
|
-
if (cached) {
|
|
3589
|
-
return cached;
|
|
3590
|
-
}
|
|
3591
|
-
const [user, err] = await this.fetchUser(userID);
|
|
3592
|
-
if (err) {
|
|
3593
|
-
throw err;
|
|
3594
|
-
}
|
|
3595
|
-
if (!user) {
|
|
3596
|
-
throw new Error(`User not found: ${userID}`);
|
|
3597
|
-
}
|
|
3598
|
-
return user;
|
|
3599
|
-
}
|
|
3600
|
-
|
|
3601
3301
|
/**
|
|
3602
3302
|
* Finish a passkey login and adopt the resulting JWT as the
|
|
3603
3303
|
* client's bearer token. After this call, `client.passkeys.*`
|
|
@@ -3812,6 +3512,23 @@ export class Client {
|
|
|
3812
3512
|
});
|
|
3813
3513
|
}
|
|
3814
3514
|
|
|
3515
|
+
private async getAccountEntitlements(): Promise<AccountEntitlements> {
|
|
3516
|
+
const res = await this.http.get(
|
|
3517
|
+
this.getHost() + "/user/" + this.getUser().userID + "/entitlements",
|
|
3518
|
+
);
|
|
3519
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
private async getBillingAccountState(): Promise<BillingAccountState> {
|
|
3523
|
+
const res = await this.http.get(this.getHost() + "/billing/account");
|
|
3524
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
private async getBillingProducts(): Promise<BillingProduct[]> {
|
|
3528
|
+
const res = await this.http.get(this.getHost() + "/billing/products");
|
|
3529
|
+
return decodeHttpResponse(BillingProductArrayCodec, res.data);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3815
3532
|
private async getChannelByID(channelID: string): Promise<Channel | null> {
|
|
3816
3533
|
try {
|
|
3817
3534
|
const res = await this.http.get(
|
|
@@ -4135,14 +3852,6 @@ export class Client {
|
|
|
4135
3852
|
}
|
|
4136
3853
|
break;
|
|
4137
3854
|
}
|
|
4138
|
-
case "callWake": {
|
|
4139
|
-
const parsed = callWakeNotifyData.safeParse(msg.data);
|
|
4140
|
-
await this.getMail();
|
|
4141
|
-
if (parsed.success) {
|
|
4142
|
-
this.emitter.emit("callWake", parsed.data);
|
|
4143
|
-
}
|
|
4144
|
-
break;
|
|
4145
|
-
}
|
|
4146
3855
|
case "deviceRequest": {
|
|
4147
3856
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
4148
3857
|
if (parsed.success) {
|
|
@@ -4404,84 +4113,6 @@ export class Client {
|
|
|
4404
4113
|
return decodeHttpResponse(PasskeyArrayCodec, response.data);
|
|
4405
4114
|
}
|
|
4406
4115
|
|
|
4407
|
-
private makeCallEnvelopeBody(args: {
|
|
4408
|
-
action: CallAction;
|
|
4409
|
-
expiresAt: string;
|
|
4410
|
-
sequence: number;
|
|
4411
|
-
signal?: CallSignalPayload | undefined;
|
|
4412
|
-
state: EncryptedCallState;
|
|
4413
|
-
toDeviceID: string;
|
|
4414
|
-
toUserID: string;
|
|
4415
|
-
}): CallEnvelopeBody {
|
|
4416
|
-
return {
|
|
4417
|
-
action: args.action,
|
|
4418
|
-
callID: args.state.session.callID,
|
|
4419
|
-
conversationID: args.state.session.conversationID,
|
|
4420
|
-
conversationType: args.state.session.conversationType,
|
|
4421
|
-
createdAt: args.state.session.createdAt,
|
|
4422
|
-
createdBy: args.state.session.createdBy,
|
|
4423
|
-
createdByDeviceID: args.state.session.createdByDeviceID,
|
|
4424
|
-
expiresAt: args.expiresAt,
|
|
4425
|
-
fromDeviceID: this.getDevice().deviceID,
|
|
4426
|
-
fromUserID: this.getUser().userID,
|
|
4427
|
-
media: "audio",
|
|
4428
|
-
sequence: args.sequence,
|
|
4429
|
-
...(args.signal ? { signal: args.signal } : {}),
|
|
4430
|
-
toDeviceID: args.toDeviceID,
|
|
4431
|
-
toUserID: args.toUserID,
|
|
4432
|
-
version: 1,
|
|
4433
|
-
};
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
private markLocalCallAction(
|
|
4437
|
-
state: EncryptedCallState,
|
|
4438
|
-
action: CallAction,
|
|
4439
|
-
signal?: CallSignalPayload,
|
|
4440
|
-
): CallEvent {
|
|
4441
|
-
const now = new Date().toISOString();
|
|
4442
|
-
if (action === "accept") {
|
|
4443
|
-
state.session.status = "active";
|
|
4444
|
-
state.session.expiresAt = new Date(
|
|
4445
|
-
Date.now() + CALL_MAX_TTL_MS,
|
|
4446
|
-
).toISOString();
|
|
4447
|
-
this.upsertCallParticipant(state.session, {
|
|
4448
|
-
acceptedAt: now,
|
|
4449
|
-
deviceID: this.getDevice().deviceID,
|
|
4450
|
-
joinedAt: now,
|
|
4451
|
-
state: "accepted",
|
|
4452
|
-
userID: this.getUser().userID,
|
|
4453
|
-
});
|
|
4454
|
-
} else if (
|
|
4455
|
-
action === "cancel" ||
|
|
4456
|
-
action === "end" ||
|
|
4457
|
-
action === "hangup" ||
|
|
4458
|
-
action === "reject" ||
|
|
4459
|
-
action === "timeout"
|
|
4460
|
-
) {
|
|
4461
|
-
state.session.status = "ended";
|
|
4462
|
-
state.session.endedAt = now;
|
|
4463
|
-
this.upsertCallParticipant(state.session, {
|
|
4464
|
-
leftAt: now,
|
|
4465
|
-
state: action === "reject" ? "rejected" : "left",
|
|
4466
|
-
userID: this.getUser().userID,
|
|
4467
|
-
});
|
|
4468
|
-
}
|
|
4469
|
-
|
|
4470
|
-
const event: CallEvent = {
|
|
4471
|
-
action,
|
|
4472
|
-
call: cloneCallSession(state.session),
|
|
4473
|
-
fromDeviceID: this.getDevice().deviceID,
|
|
4474
|
-
fromUserID: this.getUser().userID,
|
|
4475
|
-
...(signal ? { signal } : {}),
|
|
4476
|
-
};
|
|
4477
|
-
if (state.session.status === "ended") {
|
|
4478
|
-
this.callStates.delete(state.session.callID);
|
|
4479
|
-
} else {
|
|
4480
|
-
this.callStates.set(state.session.callID, state);
|
|
4481
|
-
}
|
|
4482
|
-
return event;
|
|
4483
|
-
}
|
|
4484
|
-
|
|
4485
4116
|
private async markSessionVerified(sessionID: string) {
|
|
4486
4117
|
return this.database.markSessionVerified(sessionID);
|
|
4487
4118
|
}
|
|
@@ -4710,41 +4341,6 @@ export class Client {
|
|
|
4710
4341
|
}
|
|
4711
4342
|
}
|
|
4712
4343
|
|
|
4713
|
-
private async processDecryptedCallEnvelope(args: {
|
|
4714
|
-
envelope: SignedCallEnvelope;
|
|
4715
|
-
mail: MailWS;
|
|
4716
|
-
}): Promise<CallEvent | null> {
|
|
4717
|
-
const body = args.envelope.body;
|
|
4718
|
-
if (
|
|
4719
|
-
body.fromDeviceID !== args.mail.sender ||
|
|
4720
|
-
body.fromUserID !== args.mail.authorID ||
|
|
4721
|
-
body.toDeviceID !== args.mail.recipient ||
|
|
4722
|
-
body.toUserID !== args.mail.readerID ||
|
|
4723
|
-
body.toDeviceID !== this.getDevice().deviceID ||
|
|
4724
|
-
body.toUserID !== this.getUser().userID
|
|
4725
|
-
) {
|
|
4726
|
-
return null;
|
|
4727
|
-
}
|
|
4728
|
-
|
|
4729
|
-
const senderDevice = await this.getDeviceByID(body.fromDeviceID);
|
|
4730
|
-
if (!senderDevice || senderDevice.owner !== body.fromUserID) {
|
|
4731
|
-
return null;
|
|
4732
|
-
}
|
|
4733
|
-
|
|
4734
|
-
const opened = await xSignOpenAsync(
|
|
4735
|
-
XUtils.decodeHex(args.envelope.signed),
|
|
4736
|
-
XUtils.decodeHex(senderDevice.signKey),
|
|
4737
|
-
);
|
|
4738
|
-
if (!opened) {
|
|
4739
|
-
return null;
|
|
4740
|
-
}
|
|
4741
|
-
if (!XUtils.bytesEqual(opened, canonicalJsonBytes(body))) {
|
|
4742
|
-
return null;
|
|
4743
|
-
}
|
|
4744
|
-
|
|
4745
|
-
return this.applyCallEnvelopeBody(body);
|
|
4746
|
-
}
|
|
4747
|
-
|
|
4748
4344
|
private async publishPendingDeviceRegistration(args: {
|
|
4749
4345
|
challenge: string;
|
|
4750
4346
|
requestID: string;
|
|
@@ -5100,61 +4696,46 @@ export class Client {
|
|
|
5100
4696
|
if (!mail.forward) {
|
|
5101
4697
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
5102
4698
|
}
|
|
5103
|
-
const
|
|
4699
|
+
const decodedPlaintext = mail.forward
|
|
5104
4700
|
? null
|
|
5105
|
-
:
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
),
|
|
5144
|
-
nonce: XUtils.encodeHex(
|
|
5145
|
-
new Uint8Array(mail.nonce),
|
|
5146
|
-
),
|
|
5147
|
-
readerID: mail.readerID,
|
|
5148
|
-
recipient: mail.recipient,
|
|
5149
|
-
sender: mail.sender,
|
|
5150
|
-
timestamp: timestamp,
|
|
5151
|
-
};
|
|
5152
|
-
|
|
5153
|
-
const shouldEmitIncomingInitial =
|
|
5154
|
-
mail.forward || plaintext.length > 0;
|
|
5155
|
-
if (shouldEmitIncomingInitial) {
|
|
5156
|
-
this.emitter.emit("message", message);
|
|
5157
|
-
}
|
|
4701
|
+
: decodeMessagePlaintext(plaintext);
|
|
4702
|
+
|
|
4703
|
+
// emit the message
|
|
4704
|
+
const fwdMsg1 = mail.forward
|
|
4705
|
+
? messageSchema.parse(msgpack.decode(unsealed))
|
|
4706
|
+
: null;
|
|
4707
|
+
const message: Message = fwdMsg1
|
|
4708
|
+
? {
|
|
4709
|
+
...normalizeForwardedMessage(fwdMsg1),
|
|
4710
|
+
forward: true,
|
|
4711
|
+
}
|
|
4712
|
+
: {
|
|
4713
|
+
authorID: mail.authorID,
|
|
4714
|
+
decrypted: true,
|
|
4715
|
+
direction: "incoming",
|
|
4716
|
+
forward: mail.forward,
|
|
4717
|
+
group: mail.group
|
|
4718
|
+
? uuid.stringify(mail.group)
|
|
4719
|
+
: null,
|
|
4720
|
+
mailID: mail.mailID,
|
|
4721
|
+
...messageFromDecodedPlaintext(
|
|
4722
|
+
decodedPlaintext ?? {
|
|
4723
|
+
message: plaintext,
|
|
4724
|
+
},
|
|
4725
|
+
),
|
|
4726
|
+
nonce: XUtils.encodeHex(
|
|
4727
|
+
new Uint8Array(mail.nonce),
|
|
4728
|
+
),
|
|
4729
|
+
readerID: mail.readerID,
|
|
4730
|
+
recipient: mail.recipient,
|
|
4731
|
+
sender: mail.sender,
|
|
4732
|
+
timestamp: timestamp,
|
|
4733
|
+
};
|
|
4734
|
+
|
|
4735
|
+
const shouldEmitIncomingInitial =
|
|
4736
|
+
mail.forward || plaintext.length > 0;
|
|
4737
|
+
if (shouldEmitIncomingInitial) {
|
|
4738
|
+
this.emitter.emit("message", message);
|
|
5158
4739
|
}
|
|
5159
4740
|
if (libvexDebugDmEnabled()) {
|
|
5160
4741
|
try {
|
|
@@ -5414,51 +4995,37 @@ export class Client {
|
|
|
5414
4995
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
5415
4996
|
: null;
|
|
5416
4997
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
5417
|
-
const
|
|
4998
|
+
const decodedPlaintext = mail.forward
|
|
5418
4999
|
? null
|
|
5419
|
-
:
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
decodedPlaintext ?? {
|
|
5449
|
-
message: rawIncoming,
|
|
5450
|
-
},
|
|
5451
|
-
),
|
|
5452
|
-
nonce: XUtils.encodeHex(
|
|
5453
|
-
new Uint8Array(mail.nonce),
|
|
5454
|
-
),
|
|
5455
|
-
readerID: mail.readerID,
|
|
5456
|
-
recipient: mail.recipient,
|
|
5457
|
-
sender: mail.sender,
|
|
5458
|
-
timestamp: timestamp,
|
|
5459
|
-
};
|
|
5460
|
-
this.emitter.emit("message", message);
|
|
5461
|
-
}
|
|
5000
|
+
: decodeMessagePlaintext(rawIncoming);
|
|
5001
|
+
const message: Message = fwdMsg2
|
|
5002
|
+
? {
|
|
5003
|
+
...normalizeForwardedMessage(fwdMsg2),
|
|
5004
|
+
forward: true,
|
|
5005
|
+
}
|
|
5006
|
+
: {
|
|
5007
|
+
authorID: mail.authorID,
|
|
5008
|
+
decrypted: true,
|
|
5009
|
+
direction: "incoming",
|
|
5010
|
+
forward: mail.forward,
|
|
5011
|
+
group: mail.group
|
|
5012
|
+
? uuid.stringify(mail.group)
|
|
5013
|
+
: null,
|
|
5014
|
+
mailID: mail.mailID,
|
|
5015
|
+
...messageFromDecodedPlaintext(
|
|
5016
|
+
decodedPlaintext ?? {
|
|
5017
|
+
message: rawIncoming,
|
|
5018
|
+
},
|
|
5019
|
+
),
|
|
5020
|
+
nonce: XUtils.encodeHex(
|
|
5021
|
+
new Uint8Array(mail.nonce),
|
|
5022
|
+
),
|
|
5023
|
+
readerID: mail.readerID,
|
|
5024
|
+
recipient: mail.recipient,
|
|
5025
|
+
sender: mail.sender,
|
|
5026
|
+
timestamp: timestamp,
|
|
5027
|
+
};
|
|
5028
|
+
this.emitter.emit("message", message);
|
|
5462
5029
|
|
|
5463
5030
|
const sqlPatch = sessionToSqlPatch(session);
|
|
5464
5031
|
const persisted: SessionSQL = {
|
|
@@ -5874,71 +5441,87 @@ export class Client {
|
|
|
5874
5441
|
}
|
|
5875
5442
|
}
|
|
5876
5443
|
|
|
5877
|
-
private async
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
notify?: MailNotificationHint | undefined;
|
|
5881
|
-
targetDevice: Device;
|
|
5882
|
-
targetUser: User;
|
|
5883
|
-
}): Promise<void> {
|
|
5884
|
-
const envelope = await this.callEnvelopeForBody(args.body);
|
|
5885
|
-
await this.sendMailWithRecovery(
|
|
5886
|
-
args.targetDevice,
|
|
5887
|
-
args.targetUser,
|
|
5888
|
-
encodeCallEnvelopePlaintext(envelope),
|
|
5889
|
-
null,
|
|
5890
|
-
args.mailID,
|
|
5891
|
-
false,
|
|
5892
|
-
false,
|
|
5893
|
-
args.notify,
|
|
5894
|
-
);
|
|
5895
|
-
}
|
|
5896
|
-
|
|
5897
|
-
private async sendEncryptedCallAction(
|
|
5898
|
-
action: Exclude<CallAction, "end" | "invite" | "timeout">,
|
|
5899
|
-
callID: string,
|
|
5900
|
-
signal?: CallSignalPayload,
|
|
5444
|
+
private async sendCallResource(
|
|
5445
|
+
action: string,
|
|
5446
|
+
data: CallResourceData,
|
|
5901
5447
|
): Promise<CallEvent> {
|
|
5902
|
-
const
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
throw new Error("Call has no reachable peer devices.");
|
|
5910
|
-
}
|
|
5911
|
-
|
|
5912
|
-
const targetUser = await this.fetchUserOrThrow(state.peerUserID);
|
|
5913
|
-
const sequence = state.sequence + 1;
|
|
5914
|
-
state.sequence = sequence;
|
|
5915
|
-
const expiresAt =
|
|
5916
|
-
action === "accept"
|
|
5917
|
-
? new Date(Date.now() + CALL_MAX_TTL_MS).toISOString()
|
|
5918
|
-
: state.session.expiresAt;
|
|
5919
|
-
const bodies = targets.map((target) =>
|
|
5920
|
-
this.makeCallEnvelopeBody({
|
|
5921
|
-
action,
|
|
5922
|
-
expiresAt,
|
|
5923
|
-
sequence,
|
|
5924
|
-
signal,
|
|
5925
|
-
state,
|
|
5926
|
-
toDeviceID: target.deviceID,
|
|
5927
|
-
toUserID: target.owner,
|
|
5928
|
-
}),
|
|
5929
|
-
);
|
|
5448
|
+
const msg: ResourceMsg = {
|
|
5449
|
+
action,
|
|
5450
|
+
data,
|
|
5451
|
+
resourceType: "call",
|
|
5452
|
+
transmissionID: uuid.v4(),
|
|
5453
|
+
type: "resource",
|
|
5454
|
+
};
|
|
5930
5455
|
|
|
5931
|
-
await
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5456
|
+
return await new Promise<CallEvent>((resolve, reject) => {
|
|
5457
|
+
const settle = (err: null | unknown, event?: CallEvent) => {
|
|
5458
|
+
this.socket.off("message", callback);
|
|
5459
|
+
if (err !== null) {
|
|
5460
|
+
reject(errorFromUnknown(err));
|
|
5461
|
+
return;
|
|
5462
|
+
}
|
|
5463
|
+
if (!event) {
|
|
5464
|
+
reject(new Error("Call signaling response was empty."));
|
|
5465
|
+
return;
|
|
5466
|
+
}
|
|
5467
|
+
resolve(event);
|
|
5468
|
+
};
|
|
5936
5469
|
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5470
|
+
const callback = (packedMsg: Uint8Array) => {
|
|
5471
|
+
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
5472
|
+
if (receivedMsg.transmissionID !== msg.transmissionID) {
|
|
5473
|
+
return;
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
5477
|
+
if (!parsed.success) {
|
|
5478
|
+
settle(
|
|
5479
|
+
"Call signaling failed: " + JSON.stringify(receivedMsg),
|
|
5480
|
+
);
|
|
5481
|
+
return;
|
|
5482
|
+
}
|
|
5483
|
+
|
|
5484
|
+
if (parsed.data.type === "success") {
|
|
5485
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
5486
|
+
if (!event.success) {
|
|
5487
|
+
settle(
|
|
5488
|
+
"Invalid call signaling response: " +
|
|
5489
|
+
JSON.stringify(event.error.issues),
|
|
5490
|
+
);
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
settle(null, event.data);
|
|
5494
|
+
return;
|
|
5495
|
+
}
|
|
5496
|
+
|
|
5497
|
+
if (parsed.data.type === "error") {
|
|
5498
|
+
settle(new Error(parsed.data.error));
|
|
5499
|
+
return;
|
|
5500
|
+
}
|
|
5501
|
+
|
|
5502
|
+
if (
|
|
5503
|
+
parsed.data.type === "notify" &&
|
|
5504
|
+
(parsed.data.event === "call" ||
|
|
5505
|
+
parsed.data.event === "callInvite")
|
|
5506
|
+
) {
|
|
5507
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
5508
|
+
if (event.success) {
|
|
5509
|
+
settle(null, event.data);
|
|
5510
|
+
}
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
settle(
|
|
5515
|
+
"Unexpected call signaling response: " +
|
|
5516
|
+
JSON.stringify(parsed.data),
|
|
5517
|
+
);
|
|
5518
|
+
};
|
|
5519
|
+
|
|
5520
|
+
this.socket.on("message", callback);
|
|
5521
|
+
this.send(msg).catch((err: unknown) => {
|
|
5522
|
+
settle(err);
|
|
5523
|
+
});
|
|
5524
|
+
});
|
|
5942
5525
|
}
|
|
5943
5526
|
|
|
5944
5527
|
private async sendGroupMessage(
|
|
@@ -6077,7 +5660,6 @@ export class Client {
|
|
|
6077
5660
|
mailID: null | string,
|
|
6078
5661
|
forward: boolean,
|
|
6079
5662
|
retry = false,
|
|
6080
|
-
notify?: MailNotificationHint,
|
|
6081
5663
|
): Promise<Message | null> {
|
|
6082
5664
|
while (this.sending.has(device.deviceID)) {
|
|
6083
5665
|
await sleep(100);
|
|
@@ -6104,7 +5686,6 @@ export class Client {
|
|
|
6104
5686
|
mailID,
|
|
6105
5687
|
forward,
|
|
6106
5688
|
false,
|
|
6107
|
-
notify,
|
|
6108
5689
|
);
|
|
6109
5690
|
if (libvexDebugDmEnabled()) {
|
|
6110
5691
|
debugLibvexDm("sendMail: createSession returned", {
|
|
@@ -6147,11 +5728,10 @@ export class Client {
|
|
|
6147
5728
|
recipient: device.deviceID,
|
|
6148
5729
|
sender: this.getDevice().deviceID,
|
|
6149
5730
|
};
|
|
6150
|
-
const wireMail: MailWS = notify ? { ...mail, notify } : mail;
|
|
6151
5731
|
|
|
6152
5732
|
const msgb: ResourceMsg = {
|
|
6153
5733
|
action: "CREATE",
|
|
6154
|
-
data:
|
|
5734
|
+
data: mail,
|
|
6155
5735
|
resourceType: "mail",
|
|
6156
5736
|
transmissionID: uuid.v4(),
|
|
6157
5737
|
type: "resource",
|
|
@@ -6159,36 +5739,28 @@ export class Client {
|
|
|
6159
5739
|
|
|
6160
5740
|
const hmac = xHMAC(mail, messageKey);
|
|
6161
5741
|
|
|
6162
|
-
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
|
|
6163
|
-
const callEnvelope = forward
|
|
6164
|
-
? null
|
|
6165
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
6166
5742
|
const fwdOut = forward
|
|
6167
5743
|
? messageSchema.parse(msgpack.decode(msg))
|
|
6168
5744
|
: null;
|
|
6169
|
-
const outMsg: Message
|
|
5745
|
+
const outMsg: Message = fwdOut
|
|
6170
5746
|
? { ...normalizeForwardedMessage(fwdOut), forward: true }
|
|
6171
|
-
:
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
};
|
|
6189
|
-
if (outMsg) {
|
|
6190
|
-
this.emitter.emit("message", outMsg);
|
|
6191
|
-
}
|
|
5747
|
+
: {
|
|
5748
|
+
authorID: mail.authorID,
|
|
5749
|
+
decrypted: true,
|
|
5750
|
+
direction: "outgoing",
|
|
5751
|
+
forward: mail.forward,
|
|
5752
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
5753
|
+
mailID: mail.mailID,
|
|
5754
|
+
...messageFromDecodedPlaintext(
|
|
5755
|
+
decodeMessagePlaintext(XUtils.encodeUTF8(msg)),
|
|
5756
|
+
),
|
|
5757
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
5758
|
+
readerID: mail.readerID,
|
|
5759
|
+
recipient: mail.recipient,
|
|
5760
|
+
sender: mail.sender,
|
|
5761
|
+
timestamp: new Date().toISOString(),
|
|
5762
|
+
};
|
|
5763
|
+
this.emitter.emit("message", outMsg);
|
|
6192
5764
|
|
|
6193
5765
|
const sqlPatch = sessionToSqlPatch(session);
|
|
6194
5766
|
const persisted: SessionSQL = {
|
|
@@ -6215,7 +5787,7 @@ export class Client {
|
|
|
6215
5787
|
await this.database.saveSession(persisted);
|
|
6216
5788
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
6217
5789
|
|
|
6218
|
-
await this.deliverMailResource(msgb, hmac,
|
|
5790
|
+
await this.deliverMailResource(msgb, hmac, mail);
|
|
6219
5791
|
return outMsg;
|
|
6220
5792
|
} finally {
|
|
6221
5793
|
this.sending.delete(device.deviceID);
|
|
@@ -6230,7 +5802,6 @@ export class Client {
|
|
|
6230
5802
|
mailID: null | string,
|
|
6231
5803
|
forward: boolean,
|
|
6232
5804
|
forceFreshSession = false,
|
|
6233
|
-
notify?: MailNotificationHint,
|
|
6234
5805
|
): Promise<Message | null> {
|
|
6235
5806
|
try {
|
|
6236
5807
|
return await this.sendMail(
|
|
@@ -6241,7 +5812,6 @@ export class Client {
|
|
|
6241
5812
|
mailID,
|
|
6242
5813
|
forward,
|
|
6243
5814
|
forceFreshSession,
|
|
6244
|
-
notify,
|
|
6245
5815
|
);
|
|
6246
5816
|
} catch (err: unknown) {
|
|
6247
5817
|
if (!this.shouldRetryDeliveryWithFreshSession(err)) {
|
|
@@ -6255,7 +5825,6 @@ export class Client {
|
|
|
6255
5825
|
mailID,
|
|
6256
5826
|
forward,
|
|
6257
5827
|
true,
|
|
6258
|
-
notify,
|
|
6259
5828
|
);
|
|
6260
5829
|
}
|
|
6261
5830
|
}
|
|
@@ -6433,37 +6002,27 @@ export class Client {
|
|
|
6433
6002
|
this.send(receipt).catch(ignoreSocketTeardown);
|
|
6434
6003
|
}
|
|
6435
6004
|
|
|
6436
|
-
private sessionFromCallEnvelope(body: CallEnvelopeBody): CallSession {
|
|
6437
|
-
const session: CallSession = {
|
|
6438
|
-
callID: body.callID,
|
|
6439
|
-
conversationID: body.conversationID,
|
|
6440
|
-
conversationType: body.conversationType,
|
|
6441
|
-
createdAt: body.createdAt,
|
|
6442
|
-
createdBy: body.createdBy,
|
|
6443
|
-
createdByDeviceID: body.createdByDeviceID,
|
|
6444
|
-
expiresAt: body.expiresAt,
|
|
6445
|
-
media: "audio",
|
|
6446
|
-
participants: [],
|
|
6447
|
-
status: body.action === "invite" ? "ringing" : "active",
|
|
6448
|
-
};
|
|
6449
|
-
this.upsertCallParticipant(session, {
|
|
6450
|
-
acceptedAt: body.createdAt,
|
|
6451
|
-
deviceID: body.createdByDeviceID,
|
|
6452
|
-
joinedAt: body.createdAt,
|
|
6453
|
-
state: "accepted",
|
|
6454
|
-
userID: body.createdBy,
|
|
6455
|
-
});
|
|
6456
|
-
this.upsertCallParticipant(session, {
|
|
6457
|
-
state: "ringing",
|
|
6458
|
-
userID: body.conversationID,
|
|
6459
|
-
});
|
|
6460
|
-
return session;
|
|
6461
|
-
}
|
|
6462
|
-
|
|
6463
6005
|
private setAlive(status: boolean) {
|
|
6464
6006
|
this.isAlive = status;
|
|
6465
6007
|
}
|
|
6466
6008
|
|
|
6009
|
+
private async setDevAccountTier(
|
|
6010
|
+
tier: AccountTier,
|
|
6011
|
+
options?: { expiresAt?: null | string },
|
|
6012
|
+
): Promise<AccountEntitlements> {
|
|
6013
|
+
const res = await this.http.patch(
|
|
6014
|
+
this.getHost() +
|
|
6015
|
+
"/__dev/user/" +
|
|
6016
|
+
this.getUser().userID +
|
|
6017
|
+
"/entitlements",
|
|
6018
|
+
{
|
|
6019
|
+
expiresAt: options?.expiresAt ?? null,
|
|
6020
|
+
tier,
|
|
6021
|
+
},
|
|
6022
|
+
);
|
|
6023
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6467
6026
|
private setUser(user: User): void {
|
|
6468
6027
|
this.user = user;
|
|
6469
6028
|
// Fresh identity / token: drop stale 404 negative-cache entries so a
|
|
@@ -6506,74 +6065,24 @@ export class Client {
|
|
|
6506
6065
|
);
|
|
6507
6066
|
}
|
|
6508
6067
|
|
|
6509
|
-
private async
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
});
|
|
6516
|
-
const now = new Date();
|
|
6517
|
-
const createdAt = now.toISOString();
|
|
6518
|
-
const expiresAt = new Date(
|
|
6519
|
-
now.getTime() + CALL_INVITE_TTL_MS,
|
|
6520
|
-
).toISOString();
|
|
6521
|
-
const session: CallSession = {
|
|
6522
|
-
callID: uuid.v4(),
|
|
6523
|
-
conversationID: recipientUserID,
|
|
6524
|
-
conversationType: "dm",
|
|
6525
|
-
createdAt,
|
|
6526
|
-
createdBy: this.getUser().userID,
|
|
6527
|
-
createdByDeviceID: this.getDevice().deviceID,
|
|
6528
|
-
expiresAt,
|
|
6529
|
-
media: "audio",
|
|
6530
|
-
participants: [
|
|
6531
|
-
{
|
|
6532
|
-
acceptedAt: createdAt,
|
|
6533
|
-
deviceID: this.getDevice().deviceID,
|
|
6534
|
-
joinedAt: createdAt,
|
|
6535
|
-
state: "accepted",
|
|
6536
|
-
userID: this.getUser().userID,
|
|
6537
|
-
},
|
|
6538
|
-
{
|
|
6539
|
-
state: "ringing",
|
|
6540
|
-
userID: recipientUserID,
|
|
6541
|
-
},
|
|
6542
|
-
],
|
|
6543
|
-
status: "ringing",
|
|
6544
|
-
};
|
|
6545
|
-
const state: EncryptedCallState = {
|
|
6546
|
-
peerUserID: recipientUserID,
|
|
6547
|
-
pendingPeerDevices: devices,
|
|
6548
|
-
sequence: 1,
|
|
6549
|
-
session,
|
|
6550
|
-
};
|
|
6551
|
-
this.callStates.set(session.callID, state);
|
|
6552
|
-
|
|
6553
|
-
const bodies = devices.map((device) =>
|
|
6554
|
-
this.makeCallEnvelopeBody({
|
|
6555
|
-
action: "invite",
|
|
6556
|
-
expiresAt,
|
|
6557
|
-
sequence: state.sequence,
|
|
6558
|
-
signal,
|
|
6559
|
-
state,
|
|
6560
|
-
toDeviceID: device.deviceID,
|
|
6561
|
-
toUserID: recipientUserID,
|
|
6562
|
-
}),
|
|
6068
|
+
private async submitAppleTransaction(
|
|
6069
|
+
request: AppleTransactionVerificationRequest,
|
|
6070
|
+
): Promise<BillingAccountState> {
|
|
6071
|
+
const res = await this.http.post(
|
|
6072
|
+
this.getHost() + "/billing/apple/transactions",
|
|
6073
|
+
request,
|
|
6563
6074
|
);
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
mailID: uuid.v4(),
|
|
6567
|
-
targetUser: user,
|
|
6568
|
-
});
|
|
6075
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
6076
|
+
}
|
|
6569
6077
|
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6078
|
+
private async submitGooglePurchase(
|
|
6079
|
+
request: GooglePurchaseVerificationRequest,
|
|
6080
|
+
): Promise<BillingAccountState> {
|
|
6081
|
+
const res = await this.http.post(
|
|
6082
|
+
this.getHost() + "/billing/google/purchases",
|
|
6083
|
+
request,
|
|
6084
|
+
);
|
|
6085
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
6577
6086
|
}
|
|
6578
6087
|
|
|
6579
6088
|
private async submitOTK(amount: number) {
|
|
@@ -6709,18 +6218,4 @@ export class Client {
|
|
|
6709
6218
|
return null;
|
|
6710
6219
|
}
|
|
6711
6220
|
}
|
|
6712
|
-
|
|
6713
|
-
private upsertCallParticipant(
|
|
6714
|
-
session: CallSession,
|
|
6715
|
-
patch: CallSession["participants"][number],
|
|
6716
|
-
): void {
|
|
6717
|
-
const existing = session.participants.find(
|
|
6718
|
-
(participant) => participant.userID === patch.userID,
|
|
6719
|
-
);
|
|
6720
|
-
if (!existing) {
|
|
6721
|
-
session.participants.push({ ...patch });
|
|
6722
|
-
return;
|
|
6723
|
-
}
|
|
6724
|
-
Object.assign(existing, patch);
|
|
6725
|
-
}
|
|
6726
6221
|
}
|