@vex-chat/libvex 7.3.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Client.d.ts +59 -43
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +244 -657
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +18 -0
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +4 -1
- package/dist/codecs.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +366 -859
- package/src/__tests__/harness/shared-suite.ts +1 -80
- package/src/codecs.ts +8 -0
- package/src/index.ts +9 -0
package/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. */
|
|
@@ -1357,8 +1306,10 @@ export interface Users {
|
|
|
1357
1306
|
* const client = await Client.create(privateKey);
|
|
1358
1307
|
*
|
|
1359
1308
|
* // you must register once before you can log in
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1309
|
+
* const username = Client.randomUsername();
|
|
1310
|
+
* const password = "correct horse battery staple";
|
|
1311
|
+
* await client.register(username, password);
|
|
1312
|
+
* await client.login(username, password);
|
|
1362
1313
|
*
|
|
1363
1314
|
* // The ready event fires after connect() finishes post-auth setup.
|
|
1364
1315
|
* // Wait for it before performing messaging or user operations.
|
|
@@ -1417,6 +1368,14 @@ export class Client {
|
|
|
1417
1368
|
|
|
1418
1369
|
private static readonly NOT_FOUND_TTL = 30 * 60 * 1000;
|
|
1419
1370
|
|
|
1371
|
+
/** Store subscription billing methods. */
|
|
1372
|
+
public billing: Billing = {
|
|
1373
|
+
products: this.getBillingProducts.bind(this),
|
|
1374
|
+
retrieve: this.getBillingAccountState.bind(this),
|
|
1375
|
+
submitAppleTransaction: this.submitAppleTransaction.bind(this),
|
|
1376
|
+
submitGooglePurchase: this.submitGooglePurchase.bind(this),
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1420
1379
|
/**
|
|
1421
1380
|
* Voice-call signaling operations.
|
|
1422
1381
|
*
|
|
@@ -1425,21 +1384,25 @@ export class Client {
|
|
|
1425
1384
|
*/
|
|
1426
1385
|
public calls: Calls = {
|
|
1427
1386
|
accept: (callID: string, signal?: CallSignalPayload) =>
|
|
1428
|
-
this.
|
|
1387
|
+
this.sendCallResource("ACCEPT", {
|
|
1388
|
+
callID,
|
|
1389
|
+
...(signal ? { signal } : {}),
|
|
1390
|
+
}),
|
|
1429
1391
|
active: this.fetchActiveCalls.bind(this),
|
|
1430
|
-
cancel: (callID: string) =>
|
|
1431
|
-
|
|
1432
|
-
hangup: (callID: string) =>
|
|
1433
|
-
this.sendEncryptedCallAction("hangup", callID),
|
|
1392
|
+
cancel: (callID: string) => this.sendCallResource("CANCEL", { callID }),
|
|
1393
|
+
hangup: (callID: string) => this.sendCallResource("HANGUP", { callID }),
|
|
1434
1394
|
ice: (callID: string, signal: CallSignalPayload) =>
|
|
1435
|
-
this.
|
|
1395
|
+
this.sendCallResource("ICE", { callID, signal }),
|
|
1436
1396
|
iceServers: this.fetchIceServers.bind(this),
|
|
1437
|
-
reject: (callID: string) =>
|
|
1438
|
-
this.sendEncryptedCallAction("reject", callID),
|
|
1397
|
+
reject: (callID: string) => this.sendCallResource("REJECT", { callID }),
|
|
1439
1398
|
signal: (callID: string, signal: CallSignalPayload) =>
|
|
1440
|
-
this.
|
|
1399
|
+
this.sendCallResource("SIGNAL", { callID, signal }),
|
|
1441
1400
|
startDM: (recipientUserID: string, signal?: CallSignalPayload) =>
|
|
1442
|
-
this.
|
|
1401
|
+
this.sendCallResource("INVITE", {
|
|
1402
|
+
conversationType: "dm",
|
|
1403
|
+
recipientUserID,
|
|
1404
|
+
...(signal ? { signal } : {}),
|
|
1405
|
+
}),
|
|
1443
1406
|
};
|
|
1444
1407
|
|
|
1445
1408
|
/**
|
|
@@ -1521,6 +1484,12 @@ export class Client {
|
|
|
1521
1484
|
retrieveList: this.retrieveEmojiList.bind(this),
|
|
1522
1485
|
};
|
|
1523
1486
|
|
|
1487
|
+
/** Account entitlement methods. */
|
|
1488
|
+
public entitlements: Entitlements = {
|
|
1489
|
+
retrieve: this.getAccountEntitlements.bind(this),
|
|
1490
|
+
setDevTier: this.setDevAccountTier.bind(this),
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1524
1493
|
/** File upload/download methods. */
|
|
1525
1494
|
public files: Files = {
|
|
1526
1495
|
/**
|
|
@@ -1753,8 +1722,6 @@ export class Client {
|
|
|
1753
1722
|
|
|
1754
1723
|
private autoReconnectEnabled = false;
|
|
1755
1724
|
|
|
1756
|
-
private readonly callStates = new Map<string, EncryptedCallState>();
|
|
1757
|
-
|
|
1758
1725
|
private readonly cryptoProfile: CryptoProfile;
|
|
1759
1726
|
|
|
1760
1727
|
private readonly database: Storage;
|
|
@@ -1764,12 +1731,13 @@ export class Client {
|
|
|
1764
1731
|
private readonly decryptFailureCounts = new Map<string, number>();
|
|
1765
1732
|
|
|
1766
1733
|
private device?: Device;
|
|
1767
|
-
private deviceRecords: Record<string, Device> = {};
|
|
1768
1734
|
|
|
1735
|
+
private deviceRecords: Record<string, Device> = {};
|
|
1769
1736
|
// ── Event subscription (composition over inheritance) ───────────────
|
|
1770
1737
|
private readonly emitter = new EventEmitter<ClientEvents>();
|
|
1771
1738
|
|
|
1772
1739
|
private fetchingMail: boolean = false;
|
|
1740
|
+
|
|
1773
1741
|
private firstMailFetch = true;
|
|
1774
1742
|
private readonly forwarded = new Set<string>();
|
|
1775
1743
|
private readonly host: string;
|
|
@@ -1777,8 +1745,8 @@ export class Client {
|
|
|
1777
1745
|
/** Cancels in-flight HTTP work on `close()` so `postAuth`/`getMail` cannot hang forever. */
|
|
1778
1746
|
private readonly httpAbortController = new AbortController();
|
|
1779
1747
|
private readonly idKeys: KeyPair | null;
|
|
1780
|
-
|
|
1781
1748
|
private isAlive: boolean = true;
|
|
1749
|
+
|
|
1782
1750
|
private localMessageRetentionDays: number;
|
|
1783
1751
|
private localRetentionPurgeTimer: null | ReturnType<typeof setInterval> =
|
|
1784
1752
|
null;
|
|
@@ -2400,12 +2368,12 @@ export class Client {
|
|
|
2400
2368
|
* Registers a new account on the server.
|
|
2401
2369
|
*
|
|
2402
2370
|
* @param username - Optional username to register (must be unique when provided).
|
|
2403
|
-
* @param password -
|
|
2371
|
+
* @param password - Password for new accounts. Existing-account device approval requests may omit it.
|
|
2404
2372
|
* @returns `[user, null]` on success, `[null, error]` on failure.
|
|
2405
2373
|
*
|
|
2406
2374
|
* @example
|
|
2407
2375
|
* ```ts
|
|
2408
|
-
* const [user, err] = await client.register("MyUsername");
|
|
2376
|
+
* const [user, err] = await client.register("MyUsername", "correct horse battery staple");
|
|
2409
2377
|
* ```
|
|
2410
2378
|
*/
|
|
2411
2379
|
public async register(
|
|
@@ -2427,7 +2395,7 @@ export class Client {
|
|
|
2427
2395
|
const resolvedPassword =
|
|
2428
2396
|
password?.trim().length !== 0 && password !== undefined
|
|
2429
2397
|
? password
|
|
2430
|
-
:
|
|
2398
|
+
: undefined;
|
|
2431
2399
|
const signKey = XUtils.encodeHex(this.signKeys.publicKey);
|
|
2432
2400
|
const signed = XUtils.encodeHex(
|
|
2433
2401
|
await xSignAsync(
|
|
@@ -2438,7 +2406,6 @@ export class Client {
|
|
|
2438
2406
|
const preKeyIndex = this.xKeyRing.preKeys.index;
|
|
2439
2407
|
const regMsg: RegistrationPayload = {
|
|
2440
2408
|
deviceName: this.options?.deviceName ?? "unknown",
|
|
2441
|
-
password: resolvedPassword,
|
|
2442
2409
|
preKey: XUtils.encodeHex(
|
|
2443
2410
|
this.xKeyRing.preKeys.keyPair.publicKey,
|
|
2444
2411
|
),
|
|
@@ -2450,6 +2417,9 @@ export class Client {
|
|
|
2450
2417
|
signKey,
|
|
2451
2418
|
username: resolvedUsername,
|
|
2452
2419
|
};
|
|
2420
|
+
if (resolvedPassword !== undefined) {
|
|
2421
|
+
regMsg.password = resolvedPassword;
|
|
2422
|
+
}
|
|
2453
2423
|
try {
|
|
2454
2424
|
const res = await this.http.post(
|
|
2455
2425
|
this.getHost() + "/register",
|
|
@@ -2502,6 +2472,14 @@ export class Client {
|
|
|
2502
2472
|
this.setUser(legacyUser);
|
|
2503
2473
|
|
|
2504
2474
|
// Legacy servers require /auth after /register to get a JWT.
|
|
2475
|
+
if (resolvedPassword === undefined) {
|
|
2476
|
+
return [
|
|
2477
|
+
null,
|
|
2478
|
+
new Error(
|
|
2479
|
+
"Legacy register succeeded without a password, so the SDK could not complete login.",
|
|
2480
|
+
),
|
|
2481
|
+
];
|
|
2482
|
+
}
|
|
2505
2483
|
const loginResult = await this.login(
|
|
2506
2484
|
resolvedUsername,
|
|
2507
2485
|
resolvedPassword,
|
|
@@ -2666,92 +2644,6 @@ export class Client {
|
|
|
2666
2644
|
this.acknowledgeInboundMail(mail);
|
|
2667
2645
|
}
|
|
2668
2646
|
|
|
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
2647
|
private async approveDeviceRequest(requestID: string): Promise<Device> {
|
|
2756
2648
|
const req = await this.getDeviceRegistrationRequest(requestID);
|
|
2757
2649
|
if (!req) {
|
|
@@ -2833,50 +2725,6 @@ export class Client {
|
|
|
2833
2725
|
return decodeHttpResponse(PasskeyOptionsCodec, response.data);
|
|
2834
2726
|
}
|
|
2835
2727
|
|
|
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
2728
|
private censorPreKey(preKey: PreKeysSQL): PreKeysWS {
|
|
2881
2729
|
if (!preKey.index) {
|
|
2882
2730
|
throw new Error("Key index is required.");
|
|
@@ -3035,7 +2883,6 @@ export class Client {
|
|
|
3035
2883
|
* errors should not reject the full read pipeline.
|
|
3036
2884
|
*/
|
|
3037
2885
|
allowKeyBundleFailure = false,
|
|
3038
|
-
notify?: MailNotificationHint,
|
|
3039
2886
|
): Promise<Message | null> {
|
|
3040
2887
|
return this.runWithThisCryptoProfile(async () => {
|
|
3041
2888
|
let keyBundle: KeyBundle;
|
|
@@ -3153,13 +3000,12 @@ export class Client {
|
|
|
3153
3000
|
recipient: device.deviceID,
|
|
3154
3001
|
sender: this.getDevice().deviceID,
|
|
3155
3002
|
};
|
|
3156
|
-
const wireMail: MailWS = notify ? { ...mail, notify } : mail;
|
|
3157
3003
|
|
|
3158
3004
|
const hmac = xHMAC(mail, SK);
|
|
3159
3005
|
|
|
3160
3006
|
const msg: ResourceMsg = {
|
|
3161
3007
|
action: "CREATE",
|
|
3162
|
-
data:
|
|
3008
|
+
data: mail,
|
|
3163
3009
|
resourceType: "mail",
|
|
3164
3010
|
transmissionID: uuid.v4(),
|
|
3165
3011
|
type: "resource",
|
|
@@ -3183,41 +3029,35 @@ export class Client {
|
|
|
3183
3029
|
|
|
3184
3030
|
this.emitter.emit("session", sessionEntry, user);
|
|
3185
3031
|
|
|
3186
|
-
|
|
3187
|
-
const callEnvelope = forward
|
|
3188
|
-
? null
|
|
3189
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
3032
|
+
// emit the message
|
|
3190
3033
|
const forwardedMsg = forward
|
|
3191
3034
|
? messageSchema.parse(msgpack.decode(message))
|
|
3192
3035
|
: null;
|
|
3193
|
-
const
|
|
3036
|
+
const shouldEmitHandshakeMessage = forward || message.length > 0;
|
|
3037
|
+
const emitMsg: Message = forwardedMsg
|
|
3194
3038
|
? { ...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) {
|
|
3039
|
+
: {
|
|
3040
|
+
authorID: mail.authorID,
|
|
3041
|
+
decrypted: true,
|
|
3042
|
+
direction: "outgoing",
|
|
3043
|
+
forward: mail.forward,
|
|
3044
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
3045
|
+
mailID: mail.mailID,
|
|
3046
|
+
...messageFromDecodedPlaintext(
|
|
3047
|
+
decodeMessagePlaintext(XUtils.encodeUTF8(message)),
|
|
3048
|
+
),
|
|
3049
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
3050
|
+
readerID: mail.readerID,
|
|
3051
|
+
recipient: mail.recipient,
|
|
3052
|
+
sender: mail.sender,
|
|
3053
|
+
timestamp: new Date().toISOString(),
|
|
3054
|
+
};
|
|
3055
|
+
if (shouldEmitHandshakeMessage) {
|
|
3216
3056
|
this.emitter.emit("message", emitMsg);
|
|
3217
3057
|
}
|
|
3218
3058
|
|
|
3219
|
-
await this.deliverMailResource(msg, hmac,
|
|
3220
|
-
return emitMsg;
|
|
3059
|
+
await this.deliverMailResource(msg, hmac, mail);
|
|
3060
|
+
return shouldEmitHandshakeMessage ? emitMsg : null;
|
|
3221
3061
|
});
|
|
3222
3062
|
}
|
|
3223
3063
|
|
|
@@ -3258,68 +3098,6 @@ export class Client {
|
|
|
3258
3098
|
await this.http.delete(this.getHost() + "/server/" + serverID);
|
|
3259
3099
|
}
|
|
3260
3100
|
|
|
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
3101
|
private deliverMailResource(
|
|
3324
3102
|
msg: ResourceMsg,
|
|
3325
3103
|
header: Uint8Array,
|
|
@@ -3405,61 +3183,13 @@ export class Client {
|
|
|
3405
3183
|
});
|
|
3406
3184
|
}
|
|
3407
3185
|
|
|
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 };
|
|
3186
|
+
private async fetchActiveCalls(): Promise<CallSession[]> {
|
|
3187
|
+
const res = await this.http.get(this.getHost() + "/calls/active", {
|
|
3188
|
+
responseType: "json",
|
|
3189
|
+
});
|
|
3190
|
+
return z.object({ calls: z.array(CallSessionSchema) }).parse(res.data)
|
|
3191
|
+
.calls;
|
|
3461
3192
|
}
|
|
3462
|
-
|
|
3463
3193
|
private async fetchIceServers(): Promise<IceServerConfig[]> {
|
|
3464
3194
|
const res = await this.http.get(this.getHost() + "/calls/ice-servers", {
|
|
3465
3195
|
responseType: "json",
|
|
@@ -3580,24 +3310,6 @@ export class Client {
|
|
|
3580
3310
|
throw new Error(`${base}${this.deviceListFailureDetail(lastErr)}`);
|
|
3581
3311
|
}
|
|
3582
3312
|
|
|
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
3313
|
/**
|
|
3602
3314
|
* Finish a passkey login and adopt the resulting JWT as the
|
|
3603
3315
|
* client's bearer token. After this call, `client.passkeys.*`
|
|
@@ -3812,6 +3524,23 @@ export class Client {
|
|
|
3812
3524
|
});
|
|
3813
3525
|
}
|
|
3814
3526
|
|
|
3527
|
+
private async getAccountEntitlements(): Promise<AccountEntitlements> {
|
|
3528
|
+
const res = await this.http.get(
|
|
3529
|
+
this.getHost() + "/user/" + this.getUser().userID + "/entitlements",
|
|
3530
|
+
);
|
|
3531
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
private async getBillingAccountState(): Promise<BillingAccountState> {
|
|
3535
|
+
const res = await this.http.get(this.getHost() + "/billing/account");
|
|
3536
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
private async getBillingProducts(): Promise<BillingProduct[]> {
|
|
3540
|
+
const res = await this.http.get(this.getHost() + "/billing/products");
|
|
3541
|
+
return decodeHttpResponse(BillingProductArrayCodec, res.data);
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3815
3544
|
private async getChannelByID(channelID: string): Promise<Channel | null> {
|
|
3816
3545
|
try {
|
|
3817
3546
|
const res = await this.http.get(
|
|
@@ -4135,14 +3864,6 @@ export class Client {
|
|
|
4135
3864
|
}
|
|
4136
3865
|
break;
|
|
4137
3866
|
}
|
|
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
3867
|
case "deviceRequest": {
|
|
4147
3868
|
const parsed = deviceRequestNotifyData.safeParse(msg.data);
|
|
4148
3869
|
if (parsed.success) {
|
|
@@ -4404,84 +4125,6 @@ export class Client {
|
|
|
4404
4125
|
return decodeHttpResponse(PasskeyArrayCodec, response.data);
|
|
4405
4126
|
}
|
|
4406
4127
|
|
|
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
4128
|
private async markSessionVerified(sessionID: string) {
|
|
4486
4129
|
return this.database.markSessionVerified(sessionID);
|
|
4487
4130
|
}
|
|
@@ -4710,41 +4353,6 @@ export class Client {
|
|
|
4710
4353
|
}
|
|
4711
4354
|
}
|
|
4712
4355
|
|
|
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
4356
|
private async publishPendingDeviceRegistration(args: {
|
|
4749
4357
|
challenge: string;
|
|
4750
4358
|
requestID: string;
|
|
@@ -5100,61 +4708,46 @@ export class Client {
|
|
|
5100
4708
|
if (!mail.forward) {
|
|
5101
4709
|
plaintext = XUtils.encodeUTF8(unsealed);
|
|
5102
4710
|
}
|
|
5103
|
-
const
|
|
4711
|
+
const decodedPlaintext = mail.forward
|
|
5104
4712
|
? 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
|
-
}
|
|
4713
|
+
: decodeMessagePlaintext(plaintext);
|
|
4714
|
+
|
|
4715
|
+
// emit the message
|
|
4716
|
+
const fwdMsg1 = mail.forward
|
|
4717
|
+
? messageSchema.parse(msgpack.decode(unsealed))
|
|
4718
|
+
: null;
|
|
4719
|
+
const message: Message = fwdMsg1
|
|
4720
|
+
? {
|
|
4721
|
+
...normalizeForwardedMessage(fwdMsg1),
|
|
4722
|
+
forward: true,
|
|
4723
|
+
}
|
|
4724
|
+
: {
|
|
4725
|
+
authorID: mail.authorID,
|
|
4726
|
+
decrypted: true,
|
|
4727
|
+
direction: "incoming",
|
|
4728
|
+
forward: mail.forward,
|
|
4729
|
+
group: mail.group
|
|
4730
|
+
? uuid.stringify(mail.group)
|
|
4731
|
+
: null,
|
|
4732
|
+
mailID: mail.mailID,
|
|
4733
|
+
...messageFromDecodedPlaintext(
|
|
4734
|
+
decodedPlaintext ?? {
|
|
4735
|
+
message: plaintext,
|
|
4736
|
+
},
|
|
4737
|
+
),
|
|
4738
|
+
nonce: XUtils.encodeHex(
|
|
4739
|
+
new Uint8Array(mail.nonce),
|
|
4740
|
+
),
|
|
4741
|
+
readerID: mail.readerID,
|
|
4742
|
+
recipient: mail.recipient,
|
|
4743
|
+
sender: mail.sender,
|
|
4744
|
+
timestamp: timestamp,
|
|
4745
|
+
};
|
|
4746
|
+
|
|
4747
|
+
const shouldEmitIncomingInitial =
|
|
4748
|
+
mail.forward || plaintext.length > 0;
|
|
4749
|
+
if (shouldEmitIncomingInitial) {
|
|
4750
|
+
this.emitter.emit("message", message);
|
|
5158
4751
|
}
|
|
5159
4752
|
if (libvexDebugDmEnabled()) {
|
|
5160
4753
|
try {
|
|
@@ -5414,51 +5007,37 @@ export class Client {
|
|
|
5414
5007
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
5415
5008
|
: null;
|
|
5416
5009
|
const rawIncoming = XUtils.encodeUTF8(decrypted);
|
|
5417
|
-
const
|
|
5010
|
+
const decodedPlaintext = mail.forward
|
|
5418
5011
|
? 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
|
-
}
|
|
5012
|
+
: decodeMessagePlaintext(rawIncoming);
|
|
5013
|
+
const message: Message = fwdMsg2
|
|
5014
|
+
? {
|
|
5015
|
+
...normalizeForwardedMessage(fwdMsg2),
|
|
5016
|
+
forward: true,
|
|
5017
|
+
}
|
|
5018
|
+
: {
|
|
5019
|
+
authorID: mail.authorID,
|
|
5020
|
+
decrypted: true,
|
|
5021
|
+
direction: "incoming",
|
|
5022
|
+
forward: mail.forward,
|
|
5023
|
+
group: mail.group
|
|
5024
|
+
? uuid.stringify(mail.group)
|
|
5025
|
+
: null,
|
|
5026
|
+
mailID: mail.mailID,
|
|
5027
|
+
...messageFromDecodedPlaintext(
|
|
5028
|
+
decodedPlaintext ?? {
|
|
5029
|
+
message: rawIncoming,
|
|
5030
|
+
},
|
|
5031
|
+
),
|
|
5032
|
+
nonce: XUtils.encodeHex(
|
|
5033
|
+
new Uint8Array(mail.nonce),
|
|
5034
|
+
),
|
|
5035
|
+
readerID: mail.readerID,
|
|
5036
|
+
recipient: mail.recipient,
|
|
5037
|
+
sender: mail.sender,
|
|
5038
|
+
timestamp: timestamp,
|
|
5039
|
+
};
|
|
5040
|
+
this.emitter.emit("message", message);
|
|
5462
5041
|
|
|
5463
5042
|
const sqlPatch = sessionToSqlPatch(session);
|
|
5464
5043
|
const persisted: SessionSQL = {
|
|
@@ -5874,71 +5453,87 @@ export class Client {
|
|
|
5874
5453
|
}
|
|
5875
5454
|
}
|
|
5876
5455
|
|
|
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,
|
|
5456
|
+
private async sendCallResource(
|
|
5457
|
+
action: string,
|
|
5458
|
+
data: CallResourceData,
|
|
5901
5459
|
): 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
|
-
);
|
|
5460
|
+
const msg: ResourceMsg = {
|
|
5461
|
+
action,
|
|
5462
|
+
data,
|
|
5463
|
+
resourceType: "call",
|
|
5464
|
+
transmissionID: uuid.v4(),
|
|
5465
|
+
type: "resource",
|
|
5466
|
+
};
|
|
5930
5467
|
|
|
5931
|
-
await
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5468
|
+
return await new Promise<CallEvent>((resolve, reject) => {
|
|
5469
|
+
const settle = (err: null | unknown, event?: CallEvent) => {
|
|
5470
|
+
this.socket.off("message", callback);
|
|
5471
|
+
if (err !== null) {
|
|
5472
|
+
reject(errorFromUnknown(err));
|
|
5473
|
+
return;
|
|
5474
|
+
}
|
|
5475
|
+
if (!event) {
|
|
5476
|
+
reject(new Error("Call signaling response was empty."));
|
|
5477
|
+
return;
|
|
5478
|
+
}
|
|
5479
|
+
resolve(event);
|
|
5480
|
+
};
|
|
5936
5481
|
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5482
|
+
const callback = (packedMsg: Uint8Array) => {
|
|
5483
|
+
const [_header, receivedMsg] = XUtils.unpackMessage(packedMsg);
|
|
5484
|
+
if (receivedMsg.transmissionID !== msg.transmissionID) {
|
|
5485
|
+
return;
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
const parsed = WSMessageSchema.safeParse(receivedMsg);
|
|
5489
|
+
if (!parsed.success) {
|
|
5490
|
+
settle(
|
|
5491
|
+
"Call signaling failed: " + JSON.stringify(receivedMsg),
|
|
5492
|
+
);
|
|
5493
|
+
return;
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
if (parsed.data.type === "success") {
|
|
5497
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
5498
|
+
if (!event.success) {
|
|
5499
|
+
settle(
|
|
5500
|
+
"Invalid call signaling response: " +
|
|
5501
|
+
JSON.stringify(event.error.issues),
|
|
5502
|
+
);
|
|
5503
|
+
return;
|
|
5504
|
+
}
|
|
5505
|
+
settle(null, event.data);
|
|
5506
|
+
return;
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
if (parsed.data.type === "error") {
|
|
5510
|
+
settle(new Error(parsed.data.error));
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5513
|
+
|
|
5514
|
+
if (
|
|
5515
|
+
parsed.data.type === "notify" &&
|
|
5516
|
+
(parsed.data.event === "call" ||
|
|
5517
|
+
parsed.data.event === "callInvite")
|
|
5518
|
+
) {
|
|
5519
|
+
const event = CallEventSchema.safeParse(parsed.data.data);
|
|
5520
|
+
if (event.success) {
|
|
5521
|
+
settle(null, event.data);
|
|
5522
|
+
}
|
|
5523
|
+
return;
|
|
5524
|
+
}
|
|
5525
|
+
|
|
5526
|
+
settle(
|
|
5527
|
+
"Unexpected call signaling response: " +
|
|
5528
|
+
JSON.stringify(parsed.data),
|
|
5529
|
+
);
|
|
5530
|
+
};
|
|
5531
|
+
|
|
5532
|
+
this.socket.on("message", callback);
|
|
5533
|
+
this.send(msg).catch((err: unknown) => {
|
|
5534
|
+
settle(err);
|
|
5535
|
+
});
|
|
5536
|
+
});
|
|
5942
5537
|
}
|
|
5943
5538
|
|
|
5944
5539
|
private async sendGroupMessage(
|
|
@@ -6077,7 +5672,6 @@ export class Client {
|
|
|
6077
5672
|
mailID: null | string,
|
|
6078
5673
|
forward: boolean,
|
|
6079
5674
|
retry = false,
|
|
6080
|
-
notify?: MailNotificationHint,
|
|
6081
5675
|
): Promise<Message | null> {
|
|
6082
5676
|
while (this.sending.has(device.deviceID)) {
|
|
6083
5677
|
await sleep(100);
|
|
@@ -6104,7 +5698,6 @@ export class Client {
|
|
|
6104
5698
|
mailID,
|
|
6105
5699
|
forward,
|
|
6106
5700
|
false,
|
|
6107
|
-
notify,
|
|
6108
5701
|
);
|
|
6109
5702
|
if (libvexDebugDmEnabled()) {
|
|
6110
5703
|
debugLibvexDm("sendMail: createSession returned", {
|
|
@@ -6147,11 +5740,10 @@ export class Client {
|
|
|
6147
5740
|
recipient: device.deviceID,
|
|
6148
5741
|
sender: this.getDevice().deviceID,
|
|
6149
5742
|
};
|
|
6150
|
-
const wireMail: MailWS = notify ? { ...mail, notify } : mail;
|
|
6151
5743
|
|
|
6152
5744
|
const msgb: ResourceMsg = {
|
|
6153
5745
|
action: "CREATE",
|
|
6154
|
-
data:
|
|
5746
|
+
data: mail,
|
|
6155
5747
|
resourceType: "mail",
|
|
6156
5748
|
transmissionID: uuid.v4(),
|
|
6157
5749
|
type: "resource",
|
|
@@ -6159,36 +5751,28 @@ export class Client {
|
|
|
6159
5751
|
|
|
6160
5752
|
const hmac = xHMAC(mail, messageKey);
|
|
6161
5753
|
|
|
6162
|
-
const rawPlaintext = forward ? "" : XUtils.encodeUTF8(msg);
|
|
6163
|
-
const callEnvelope = forward
|
|
6164
|
-
? null
|
|
6165
|
-
: decodeCallEnvelopePlaintext(rawPlaintext);
|
|
6166
5754
|
const fwdOut = forward
|
|
6167
5755
|
? messageSchema.parse(msgpack.decode(msg))
|
|
6168
5756
|
: null;
|
|
6169
|
-
const outMsg: Message
|
|
5757
|
+
const outMsg: Message = fwdOut
|
|
6170
5758
|
? { ...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
|
-
}
|
|
5759
|
+
: {
|
|
5760
|
+
authorID: mail.authorID,
|
|
5761
|
+
decrypted: true,
|
|
5762
|
+
direction: "outgoing",
|
|
5763
|
+
forward: mail.forward,
|
|
5764
|
+
group: mail.group ? uuid.stringify(mail.group) : null,
|
|
5765
|
+
mailID: mail.mailID,
|
|
5766
|
+
...messageFromDecodedPlaintext(
|
|
5767
|
+
decodeMessagePlaintext(XUtils.encodeUTF8(msg)),
|
|
5768
|
+
),
|
|
5769
|
+
nonce: XUtils.encodeHex(new Uint8Array(mail.nonce)),
|
|
5770
|
+
readerID: mail.readerID,
|
|
5771
|
+
recipient: mail.recipient,
|
|
5772
|
+
sender: mail.sender,
|
|
5773
|
+
timestamp: new Date().toISOString(),
|
|
5774
|
+
};
|
|
5775
|
+
this.emitter.emit("message", outMsg);
|
|
6192
5776
|
|
|
6193
5777
|
const sqlPatch = sessionToSqlPatch(session);
|
|
6194
5778
|
const persisted: SessionSQL = {
|
|
@@ -6215,7 +5799,7 @@ export class Client {
|
|
|
6215
5799
|
await this.database.saveSession(persisted);
|
|
6216
5800
|
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
6217
5801
|
|
|
6218
|
-
await this.deliverMailResource(msgb, hmac,
|
|
5802
|
+
await this.deliverMailResource(msgb, hmac, mail);
|
|
6219
5803
|
return outMsg;
|
|
6220
5804
|
} finally {
|
|
6221
5805
|
this.sending.delete(device.deviceID);
|
|
@@ -6230,7 +5814,6 @@ export class Client {
|
|
|
6230
5814
|
mailID: null | string,
|
|
6231
5815
|
forward: boolean,
|
|
6232
5816
|
forceFreshSession = false,
|
|
6233
|
-
notify?: MailNotificationHint,
|
|
6234
5817
|
): Promise<Message | null> {
|
|
6235
5818
|
try {
|
|
6236
5819
|
return await this.sendMail(
|
|
@@ -6241,7 +5824,6 @@ export class Client {
|
|
|
6241
5824
|
mailID,
|
|
6242
5825
|
forward,
|
|
6243
5826
|
forceFreshSession,
|
|
6244
|
-
notify,
|
|
6245
5827
|
);
|
|
6246
5828
|
} catch (err: unknown) {
|
|
6247
5829
|
if (!this.shouldRetryDeliveryWithFreshSession(err)) {
|
|
@@ -6255,7 +5837,6 @@ export class Client {
|
|
|
6255
5837
|
mailID,
|
|
6256
5838
|
forward,
|
|
6257
5839
|
true,
|
|
6258
|
-
notify,
|
|
6259
5840
|
);
|
|
6260
5841
|
}
|
|
6261
5842
|
}
|
|
@@ -6433,37 +6014,27 @@ export class Client {
|
|
|
6433
6014
|
this.send(receipt).catch(ignoreSocketTeardown);
|
|
6434
6015
|
}
|
|
6435
6016
|
|
|
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
6017
|
private setAlive(status: boolean) {
|
|
6464
6018
|
this.isAlive = status;
|
|
6465
6019
|
}
|
|
6466
6020
|
|
|
6021
|
+
private async setDevAccountTier(
|
|
6022
|
+
tier: AccountTier,
|
|
6023
|
+
options?: { expiresAt?: null | string },
|
|
6024
|
+
): Promise<AccountEntitlements> {
|
|
6025
|
+
const res = await this.http.patch(
|
|
6026
|
+
this.getHost() +
|
|
6027
|
+
"/__dev/user/" +
|
|
6028
|
+
this.getUser().userID +
|
|
6029
|
+
"/entitlements",
|
|
6030
|
+
{
|
|
6031
|
+
expiresAt: options?.expiresAt ?? null,
|
|
6032
|
+
tier,
|
|
6033
|
+
},
|
|
6034
|
+
);
|
|
6035
|
+
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
6036
|
+
}
|
|
6037
|
+
|
|
6467
6038
|
private setUser(user: User): void {
|
|
6468
6039
|
this.user = user;
|
|
6469
6040
|
// Fresh identity / token: drop stale 404 negative-cache entries so a
|
|
@@ -6506,74 +6077,24 @@ export class Client {
|
|
|
6506
6077
|
);
|
|
6507
6078
|
}
|
|
6508
6079
|
|
|
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
|
-
}),
|
|
6080
|
+
private async submitAppleTransaction(
|
|
6081
|
+
request: AppleTransactionVerificationRequest,
|
|
6082
|
+
): Promise<BillingAccountState> {
|
|
6083
|
+
const res = await this.http.post(
|
|
6084
|
+
this.getHost() + "/billing/apple/transactions",
|
|
6085
|
+
request,
|
|
6563
6086
|
);
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
mailID: uuid.v4(),
|
|
6567
|
-
targetUser: user,
|
|
6568
|
-
});
|
|
6087
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
6088
|
+
}
|
|
6569
6089
|
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6090
|
+
private async submitGooglePurchase(
|
|
6091
|
+
request: GooglePurchaseVerificationRequest,
|
|
6092
|
+
): Promise<BillingAccountState> {
|
|
6093
|
+
const res = await this.http.post(
|
|
6094
|
+
this.getHost() + "/billing/google/purchases",
|
|
6095
|
+
request,
|
|
6096
|
+
);
|
|
6097
|
+
return decodeHttpResponse(BillingAccountStateCodec, res.data);
|
|
6577
6098
|
}
|
|
6578
6099
|
|
|
6579
6100
|
private async submitOTK(amount: number) {
|
|
@@ -6709,18 +6230,4 @@ export class Client {
|
|
|
6709
6230
|
return null;
|
|
6710
6231
|
}
|
|
6711
6232
|
}
|
|
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
6233
|
}
|