@vex-chat/libvex 8.0.0 → 9.1.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 +54 -21
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +292 -164
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +8 -4
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +2 -5
- package/dist/codecs.js.map +1 -1
- package/dist/http.js +15 -1
- package/dist/http.js.map +1 -1
- package/dist/keystore/node.d.ts.map +1 -1
- package/dist/keystore/node.js +9 -11
- package/dist/keystore/node.js.map +1 -1
- package/dist/storage/sqlite.d.ts +6 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +139 -113
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/utils/verifyKeyBundle.d.ts.map +1 -1
- package/dist/utils/verifyKeyBundle.js +7 -10
- package/dist/utils/verifyKeyBundle.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +476 -219
- package/src/__tests__/harness/shared-suite.ts +110 -11
- package/src/__tests__/http.test.ts +22 -0
- package/src/__tests__/multi-device-delivery.test.ts +147 -0
- package/src/__tests__/node-keystore.test.ts +69 -0
- package/src/__tests__/prekey-reuse.test.ts +13 -13
- package/src/__tests__/storage-sqlite.test.ts +117 -1
- package/src/__tests__/verifyKeyBundle.test.ts +11 -8
- package/src/codecs.ts +2 -5
- package/src/http.ts +16 -1
- package/src/keystore/node.ts +17 -19
- package/src/storage/sqlite.ts +162 -139
- package/src/utils/verifyKeyBundle.ts +13 -13
package/dist/Client.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
-
import { enterCryptoProfileScope, getCryptoProfile, leaveCryptoProfileScope, setCryptoProfile, xBoxKeyPairAsync, xBoxKeyPairFromSecretAsync, xConcat, xConstants, xDHAsync, xEcdhKeyPairFromEcdsaKeyPairAsync, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMnemonic, xRandomBytes, xSecretboxAsync, xSecretboxOpenAsync, xSignAsync, xSignKeyPair, xSignKeyPairAsync, xSignKeyPairFromSecret, xSignKeyPairFromSecretAsync, xSignOpenAsync, XUtils, } from "@vex-chat/crypto";
|
|
7
|
-
import { CallEventSchema, CallSessionSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, WSMessageSchema, } from "@vex-chat/types";
|
|
6
|
+
import { enterCryptoProfileScope, getCryptoProfile, leaveCryptoProfileScope, setCryptoProfile, xBoxKeyPairAsync, xBoxKeyPairFromSecretAsync, xConcat, xConstants, xDHAsync, xEcdhKeyPairFromEcdsaKeyPairAsync, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMessageKeySubkeys, xMnemonic, xPreKeySignaturePayload, xRandomBytes, xSecretboxAsync, xSecretboxOpenAsync, xSignAsync, xSignKeyPair, xSignKeyPairAsync, xSignKeyPairFromSecret, xSignKeyPairFromSecretAsync, xSignOpenAsync, XUtils, } from "@vex-chat/crypto";
|
|
7
|
+
import { ACCOUNT_PASSWORD_MAX_LENGTH, ACCOUNT_PASSWORD_MIN_LENGTH, CallEventSchema, CallSessionSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, WSMessageSchema, } from "@vex-chat/types";
|
|
8
8
|
import { EventEmitter } from "eventemitter3";
|
|
9
9
|
import * as uuid from "uuid";
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
11
|
import { createFetchHttpClient, isHttpError, } from "./http.js";
|
|
12
12
|
import { clampLocalMessageRetentionDays, formatVexRetentionEnvelope, stripVexRetentionEnvelope, } from "./retention.js";
|
|
13
13
|
import { WebSocketAdapter, WebSocketNotOpenError, } from "./transport/websocket.js";
|
|
14
|
-
import { decodeFipsInitialExtraV1, encodeFipsInitialExtraV1, fipsP256AdFromIdentityPubs,
|
|
14
|
+
import { decodeFipsInitialExtraV1, encodeFipsInitialExtraV1, fipsP256AdFromIdentityPubs, isFipsInitialExtraV1, } from "./utils/fipsMailExtra.js";
|
|
15
15
|
import { decodeRatchetHeader, deriveBootstrapSendChain, encodeRatchetHeader, hasRemoteDhChanged, initRatchetSession, ratchetStepReceive, ratchetStepSend, sessionToSqlPatch, takeReceiveMessageKey, takeSendMessageKey, } from "./utils/ratchet.js";
|
|
16
16
|
import { verifyKeyBundleSignatures } from "./utils/verifyKeyBundle.js";
|
|
17
17
|
/**
|
|
@@ -148,6 +148,16 @@ function libvexDebugLevel() {
|
|
|
148
148
|
return "debug";
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
function registrationPasswordError(password) {
|
|
152
|
+
if (password.trim().length === 0 ||
|
|
153
|
+
password.length < ACCOUNT_PASSWORD_MIN_LENGTH) {
|
|
154
|
+
return new Error(`Password must be at least ${String(ACCOUNT_PASSWORD_MIN_LENGTH)} characters.`);
|
|
155
|
+
}
|
|
156
|
+
if (password.length > ACCOUNT_PASSWORD_MAX_LENGTH) {
|
|
157
|
+
return new Error(`Password must be at most ${String(ACCOUNT_PASSWORD_MAX_LENGTH)} characters.`);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
151
161
|
function sleep(ms) {
|
|
152
162
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
153
163
|
}
|
|
@@ -373,6 +383,7 @@ const retryRequestNotifyData = z.union([
|
|
|
373
383
|
mailID: z.string(),
|
|
374
384
|
}),
|
|
375
385
|
]);
|
|
386
|
+
const serverChangeNotifyData = z.string().min(1).max(128);
|
|
376
387
|
export class Client {
|
|
377
388
|
/**
|
|
378
389
|
* Decrypts a secret key from encrypted data produced by encryptKeyData().
|
|
@@ -454,6 +465,8 @@ export class Client {
|
|
|
454
465
|
* @returns The Channel object, or null.
|
|
455
466
|
*/
|
|
456
467
|
retrieveByID: this.getChannelByID.bind(this),
|
|
468
|
+
/** Changes a channel's display name. */
|
|
469
|
+
update: this.updateChannel.bind(this),
|
|
457
470
|
/**
|
|
458
471
|
* Retrieves a channel's userlist.
|
|
459
472
|
* @param channelID - The channel to retrieve the userlist for.
|
|
@@ -529,6 +542,7 @@ export class Client {
|
|
|
529
542
|
* Helpers for information/actions related to the currently authenticated account.
|
|
530
543
|
*/
|
|
531
544
|
me = {
|
|
545
|
+
changePassword: this.changePassword.bind(this),
|
|
532
546
|
/**
|
|
533
547
|
* Retrieves current device details.
|
|
534
548
|
*
|
|
@@ -590,6 +604,7 @@ export class Client {
|
|
|
590
604
|
moderation = {
|
|
591
605
|
fetchPermissionList: this.fetchPermissionList.bind(this),
|
|
592
606
|
kick: this.kickUser.bind(this),
|
|
607
|
+
setRole: this.setServerMemberRole.bind(this),
|
|
593
608
|
};
|
|
594
609
|
/**
|
|
595
610
|
* Passkey ("recovery credential") methods.
|
|
@@ -614,6 +629,7 @@ export class Client {
|
|
|
614
629
|
listDevices: this.passkeyListDevices.bind(this),
|
|
615
630
|
recoverDeviceRequest: this.passkeyRecoverDeviceRequest.bind(this),
|
|
616
631
|
rejectDeviceRequest: this.passkeyRejectDeviceRequest.bind(this),
|
|
632
|
+
resetPassword: this.resetPasswordWithPasskey.bind(this),
|
|
617
633
|
};
|
|
618
634
|
/**
|
|
619
635
|
* Permission-management methods for the current user.
|
|
@@ -645,7 +661,9 @@ export class Client {
|
|
|
645
661
|
* @param serverID - The server to delete.
|
|
646
662
|
*/
|
|
647
663
|
delete: this.deleteServer.bind(this),
|
|
664
|
+
iconURL: this.getServerIconURL.bind(this),
|
|
648
665
|
leave: this.leaveServer.bind(this),
|
|
666
|
+
removeIcon: this.removeServerIcon.bind(this),
|
|
649
667
|
/**
|
|
650
668
|
* Retrieves all servers the logged in user has access to.
|
|
651
669
|
*
|
|
@@ -659,6 +677,8 @@ export class Client {
|
|
|
659
677
|
*/
|
|
660
678
|
retrieveByID: this.getServerByID.bind(this),
|
|
661
679
|
retrieveWithChannels: this.getServerChannelBootstrap.bind(this),
|
|
680
|
+
setIcon: this.uploadServerIcon.bind(this),
|
|
681
|
+
update: this.updateServer.bind(this),
|
|
662
682
|
};
|
|
663
683
|
/**
|
|
664
684
|
* Encryption-session helpers.
|
|
@@ -849,14 +869,23 @@ export class Client {
|
|
|
849
869
|
const atRestAes = XUtils.deriveLocalAtRestAesKey(idKeys.secretKey, profile);
|
|
850
870
|
let resolvedStorage = storage;
|
|
851
871
|
if (!resolvedStorage) {
|
|
852
|
-
const
|
|
872
|
+
const nodeStorageModule = "./storage/node.js";
|
|
873
|
+
const isNodeStorageModule = (value) => typeof value === "object" &&
|
|
874
|
+
value !== null &&
|
|
875
|
+
"createNodeStorage" in value &&
|
|
876
|
+
typeof value.createNodeStorage === "function";
|
|
877
|
+
const nodeStorage = await import(
|
|
878
|
+
/* @vite-ignore */ nodeStorageModule);
|
|
879
|
+
if (!isNodeStorageModule(nodeStorage)) {
|
|
880
|
+
throw new Error("Node storage adapter is unavailable.");
|
|
881
|
+
}
|
|
853
882
|
const dbFileName = options?.inMemoryDb
|
|
854
883
|
? ":memory:"
|
|
855
884
|
: XUtils.encodeHex(signKeys.publicKey) + ".sqlite";
|
|
856
885
|
const dbPath = options?.dbFolder
|
|
857
886
|
? options.dbFolder + "/" + dbFileName
|
|
858
887
|
: dbFileName;
|
|
859
|
-
resolvedStorage = createNodeStorage(dbPath, atRestAes);
|
|
888
|
+
resolvedStorage = nodeStorage.createNodeStorage(dbPath, atRestAes);
|
|
860
889
|
}
|
|
861
890
|
await resolvedStorage.init();
|
|
862
891
|
const client = new Client({
|
|
@@ -1147,10 +1176,37 @@ export class Client {
|
|
|
1147
1176
|
return null;
|
|
1148
1177
|
}
|
|
1149
1178
|
/**
|
|
1150
|
-
*
|
|
1179
|
+
* Ends the local authenticated session and disconnects realtime transport.
|
|
1180
|
+
* Spire access tokens are stateless, so local credential removal is the
|
|
1181
|
+
* authoritative logout boundary.
|
|
1151
1182
|
*/
|
|
1152
1183
|
async logout() {
|
|
1153
|
-
|
|
1184
|
+
try {
|
|
1185
|
+
if (this.token) {
|
|
1186
|
+
await this.http.post(this.getHost() + "/goodbye");
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
catch {
|
|
1190
|
+
// Logout must still complete locally when the server is unreachable.
|
|
1191
|
+
}
|
|
1192
|
+
finally {
|
|
1193
|
+
this.token = null;
|
|
1194
|
+
this.user = undefined;
|
|
1195
|
+
this.device = undefined;
|
|
1196
|
+
delete this.http.defaults.headers.common.Authorization;
|
|
1197
|
+
delete this.http.defaults.headers.common["X-Device-Token"];
|
|
1198
|
+
this.autoReconnectEnabled = false;
|
|
1199
|
+
this.postAuthVersion++;
|
|
1200
|
+
if (this.reconnectTimer) {
|
|
1201
|
+
clearTimeout(this.reconnectTimer);
|
|
1202
|
+
this.reconnectTimer = null;
|
|
1203
|
+
}
|
|
1204
|
+
if (this.pingInterval) {
|
|
1205
|
+
clearInterval(this.pingInterval);
|
|
1206
|
+
this.pingInterval = null;
|
|
1207
|
+
}
|
|
1208
|
+
this.socket.close();
|
|
1209
|
+
}
|
|
1154
1210
|
}
|
|
1155
1211
|
/** Removes an event listener. See {@link ClientEvents} for available events. */
|
|
1156
1212
|
off(event, fn, context) {
|
|
@@ -1202,8 +1258,8 @@ export class Client {
|
|
|
1202
1258
|
/**
|
|
1203
1259
|
* Registers a new account on the server.
|
|
1204
1260
|
*
|
|
1205
|
-
* @param username -
|
|
1206
|
-
* @param password - Password for
|
|
1261
|
+
* @param username - Username to register.
|
|
1262
|
+
* @param password - Password for the account and device approval request.
|
|
1207
1263
|
* @returns `[user, null]` on success, `[null, error]` on failure.
|
|
1208
1264
|
*
|
|
1209
1265
|
* @example
|
|
@@ -1212,113 +1268,35 @@ export class Client {
|
|
|
1212
1268
|
* ```
|
|
1213
1269
|
*/
|
|
1214
1270
|
async register(username, password) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
const regKey = await this.getToken("register");
|
|
1219
|
-
if (regKey) {
|
|
1220
|
-
// Usernames are case-insensitive at the protocol level;
|
|
1221
|
-
// lowercase before sending so the local SDK view matches
|
|
1222
|
-
// what the server canonicalizes and persists.
|
|
1223
|
-
const resolvedUsername = username?.trim().length !== 0 && username !== undefined
|
|
1224
|
-
? username.trim().toLowerCase()
|
|
1225
|
-
: Client.randomUsername();
|
|
1226
|
-
const resolvedPassword = password?.trim().length !== 0 && password !== undefined
|
|
1227
|
-
? password
|
|
1228
|
-
: undefined;
|
|
1229
|
-
const signKey = XUtils.encodeHex(this.signKeys.publicKey);
|
|
1230
|
-
const signed = XUtils.encodeHex(await xSignAsync(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
|
|
1231
|
-
const preKeyIndex = this.xKeyRing.preKeys.index;
|
|
1232
|
-
const regMsg = {
|
|
1233
|
-
deviceName: this.options?.deviceName ?? "unknown",
|
|
1234
|
-
preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
|
|
1235
|
-
preKeyIndex,
|
|
1236
|
-
preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
|
|
1237
|
-
signed,
|
|
1238
|
-
signKey,
|
|
1239
|
-
username: resolvedUsername,
|
|
1240
|
-
};
|
|
1241
|
-
if (resolvedPassword !== undefined) {
|
|
1242
|
-
regMsg.password = resolvedPassword;
|
|
1243
|
-
}
|
|
1244
|
-
try {
|
|
1245
|
-
const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
1246
|
-
// New key-cluster server response: { device, token, user }.
|
|
1247
|
-
// Legacy response (still deployed in some environments): user only.
|
|
1248
|
-
let didDecodeRegisterResponse = false;
|
|
1249
|
-
let pendingApproval = null;
|
|
1250
|
-
try {
|
|
1251
|
-
const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
|
|
1252
|
-
this.device = device;
|
|
1253
|
-
this.setUser(user);
|
|
1254
|
-
this.token = token;
|
|
1255
|
-
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
1256
|
-
didDecodeRegisterResponse = true;
|
|
1257
|
-
}
|
|
1258
|
-
catch {
|
|
1259
|
-
// fall through to legacy decode path
|
|
1260
|
-
}
|
|
1261
|
-
if (!didDecodeRegisterResponse) {
|
|
1262
|
-
try {
|
|
1263
|
-
pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
|
|
1264
|
-
}
|
|
1265
|
-
catch {
|
|
1266
|
-
// fall through to legacy decode path
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
if (!didDecodeRegisterResponse) {
|
|
1270
|
-
if (pendingApproval !== null) {
|
|
1271
|
-
return [
|
|
1272
|
-
null,
|
|
1273
|
-
new DeviceApprovalRequiredError({
|
|
1274
|
-
challenge: pendingApproval.challenge,
|
|
1275
|
-
expiresAt: pendingApproval.expiresAt,
|
|
1276
|
-
requestID: pendingApproval.requestID,
|
|
1277
|
-
userID: pendingApproval.userID ?? null,
|
|
1278
|
-
}),
|
|
1279
|
-
];
|
|
1280
|
-
}
|
|
1281
|
-
const legacyUser = decodeHttpResponse(UserCodec, res.data);
|
|
1282
|
-
this.setUser(legacyUser);
|
|
1283
|
-
// Legacy servers require /auth after /register to get a JWT.
|
|
1284
|
-
if (resolvedPassword === undefined) {
|
|
1285
|
-
return [
|
|
1286
|
-
null,
|
|
1287
|
-
new Error("Legacy register succeeded without a password, so the SDK could not complete login."),
|
|
1288
|
-
];
|
|
1289
|
-
}
|
|
1290
|
-
const loginResult = await this.login(resolvedUsername, resolvedPassword);
|
|
1291
|
-
if (!loginResult.ok) {
|
|
1292
|
-
return [
|
|
1293
|
-
null,
|
|
1294
|
-
new Error(loginResult.error ??
|
|
1295
|
-
"Legacy register succeeded but login failed."),
|
|
1296
|
-
];
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
return [this.getUser(), null];
|
|
1300
|
-
}
|
|
1301
|
-
catch (err) {
|
|
1302
|
-
if (isHttpError(err) && err.response) {
|
|
1303
|
-
return [
|
|
1304
|
-
null,
|
|
1305
|
-
new Error(spireErrorBodyMessage(err.response.data)),
|
|
1306
|
-
];
|
|
1307
|
-
}
|
|
1308
|
-
return [
|
|
1309
|
-
null,
|
|
1310
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
1311
|
-
];
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
return [null, new Error("Couldn't get regkey from server.")];
|
|
1271
|
+
const passwordError = registrationPasswordError(password);
|
|
1272
|
+
if (passwordError) {
|
|
1273
|
+
return [null, passwordError];
|
|
1316
1274
|
}
|
|
1275
|
+
return this.registerAccountOrEnrollment(username, "create-account", password);
|
|
1317
1276
|
}
|
|
1318
1277
|
removeAllListeners(event) {
|
|
1319
1278
|
this.emitter.removeAllListeners(event);
|
|
1320
1279
|
return this;
|
|
1321
1280
|
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Requests approval for a new device using the account password. This
|
|
1283
|
+
* enrollment flow never creates an account when the username is unknown.
|
|
1284
|
+
*/
|
|
1285
|
+
async requestDeviceEnrollment(username, password) {
|
|
1286
|
+
const passwordError = registrationPasswordError(password);
|
|
1287
|
+
if (passwordError) {
|
|
1288
|
+
return [null, passwordError];
|
|
1289
|
+
}
|
|
1290
|
+
return this.registerAccountOrEnrollment(username, "enroll-device", password);
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Requests enrollment for this device after a successful passkey ceremony.
|
|
1294
|
+
* This is not account registration: the passkey bearer must identify an
|
|
1295
|
+
* existing account, and the server returns a pending device request.
|
|
1296
|
+
*/
|
|
1297
|
+
async requestDeviceEnrollmentWithPasskey(username) {
|
|
1298
|
+
return this.registerAccountOrEnrollment(username, "enroll-device");
|
|
1299
|
+
}
|
|
1322
1300
|
/**
|
|
1323
1301
|
* Updates the local retention cap (1–30 days) and prunes immediately.
|
|
1324
1302
|
* Does not affect server-side storage.
|
|
@@ -1456,6 +1434,16 @@ export class Client {
|
|
|
1456
1434
|
signature: XUtils.decodeHex(preKey.signature),
|
|
1457
1435
|
};
|
|
1458
1436
|
}
|
|
1437
|
+
async changePassword(currentPassword, newPassword) {
|
|
1438
|
+
const passwordError = registrationPasswordError(newPassword);
|
|
1439
|
+
if (passwordError)
|
|
1440
|
+
throw passwordError;
|
|
1441
|
+
if (currentPassword.length === 0 ||
|
|
1442
|
+
currentPassword.length > ACCOUNT_PASSWORD_MAX_LENGTH) {
|
|
1443
|
+
throw new Error("Current password is invalid.");
|
|
1444
|
+
}
|
|
1445
|
+
await this.replaceAccountPassword({ currentPassword, newPassword });
|
|
1446
|
+
}
|
|
1459
1447
|
async createChannel(name, serverID) {
|
|
1460
1448
|
const body = { name };
|
|
1461
1449
|
const res = await this.http.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
|
|
@@ -1523,12 +1511,10 @@ export class Client {
|
|
|
1523
1511
|
const res = await this.http.post(this.getHost() + "/server/" + serverID + "/invites", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
|
|
1524
1512
|
return decodeHttpResponse(InviteCodec, res.data);
|
|
1525
1513
|
}
|
|
1526
|
-
async createPreKey() {
|
|
1514
|
+
async createPreKey(kind) {
|
|
1527
1515
|
return this.runWithThisCryptoProfile(async () => {
|
|
1528
1516
|
const preKeyPair = await xBoxKeyPairAsync();
|
|
1529
|
-
const toSign = this.cryptoProfile
|
|
1530
|
-
? fipsP256PreKeySignPayload(preKeyPair.publicKey)
|
|
1531
|
-
: xEncode(xConstants.CURVE, preKeyPair.publicKey);
|
|
1517
|
+
const toSign = xPreKeySignaturePayload(preKeyPair.publicKey, kind, this.cryptoProfile);
|
|
1532
1518
|
return {
|
|
1533
1519
|
keyPair: preKeyPair,
|
|
1534
1520
|
signature: await xSignAsync(toSign, this.signKeys.secretKey),
|
|
@@ -1536,7 +1522,7 @@ export class Client {
|
|
|
1536
1522
|
});
|
|
1537
1523
|
}
|
|
1538
1524
|
async createServer(name) {
|
|
1539
|
-
const res = await this.http.post(this.getHost() + "/
|
|
1525
|
+
const res = await this.http.post(this.getHost() + "/servers", msgpack.encode({ name }), { headers: { "Content-Type": "application/msgpack" } });
|
|
1540
1526
|
return decodeHttpResponse(ServerCodec, res.data);
|
|
1541
1527
|
}
|
|
1542
1528
|
async createSession(device, user, message, group,
|
|
@@ -1604,12 +1590,13 @@ export class Client {
|
|
|
1604
1590
|
: XUtils.numberToUint8Arr(0);
|
|
1605
1591
|
// shared secret key
|
|
1606
1592
|
const SK = xKDF(IKM);
|
|
1593
|
+
const initialSubkeys = xMessageKeySubkeys(SK);
|
|
1607
1594
|
const PK = (await xBoxKeyPairFromSecretAsync(SK)).publicKey;
|
|
1608
1595
|
const AD = fips
|
|
1609
1596
|
? fipsP256AdFromIdentityPubs(IK_AP, new Uint8Array(keyBundle.signKey))
|
|
1610
1597
|
: xConcat(xEncode(xConstants.CURVE, IK_AP), xEncode(xConstants.CURVE, IK_B));
|
|
1611
1598
|
const nonce = xMakeNonce();
|
|
1612
|
-
const cipher = await xSecretboxAsync(message, nonce,
|
|
1599
|
+
const cipher = await xSecretboxAsync(message, nonce, initialSubkeys.encryptionKey);
|
|
1613
1600
|
const signKeyWire = fips ? IK_AP : this.signKeys.publicKey;
|
|
1614
1601
|
const ephKeyWire = ephemeralKeys.publicKey;
|
|
1615
1602
|
const extra = fips
|
|
@@ -1628,7 +1615,7 @@ export class Client {
|
|
|
1628
1615
|
recipient: device.deviceID,
|
|
1629
1616
|
sender: this.getDevice().deviceID,
|
|
1630
1617
|
};
|
|
1631
|
-
const hmac = xHMAC(mail,
|
|
1618
|
+
const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
|
|
1632
1619
|
const msg = {
|
|
1633
1620
|
action: "CREATE",
|
|
1634
1621
|
data: mail,
|
|
@@ -2122,6 +2109,9 @@ export class Client {
|
|
|
2122
2109
|
"/device/" +
|
|
2123
2110
|
this.getDevice().deviceID +
|
|
2124
2111
|
"/mail");
|
|
2112
|
+
if (!this.token || this.isManualCloseInFlight()) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
2125
2115
|
const mailBuffer = new Uint8Array(res.data);
|
|
2126
2116
|
const rawInbox = z
|
|
2127
2117
|
.array(mailInboxEntry)
|
|
@@ -2220,6 +2210,9 @@ export class Client {
|
|
|
2220
2210
|
"/servers/bootstrap");
|
|
2221
2211
|
return decodeHttpResponse(ServerChannelBootstrapCodec, res.data);
|
|
2222
2212
|
}
|
|
2213
|
+
getServerIconURL(iconID) {
|
|
2214
|
+
return this.getHost() + "/server-icon/" + encodeURIComponent(iconID);
|
|
2215
|
+
}
|
|
2223
2216
|
async getServerList() {
|
|
2224
2217
|
const res = await this.http.get(this.getHost() + "/user/" + this.getUser().userID + "/servers");
|
|
2225
2218
|
return decodeHttpResponse(ServerArrayCodec, res.data);
|
|
@@ -2312,6 +2305,13 @@ export class Client {
|
|
|
2312
2305
|
}
|
|
2313
2306
|
}
|
|
2314
2307
|
break;
|
|
2308
|
+
case "serverChange": {
|
|
2309
|
+
const parsed = serverChangeNotifyData.safeParse(msg.data);
|
|
2310
|
+
if (parsed.success) {
|
|
2311
|
+
this.emitter.emit("serverChange", parsed.data);
|
|
2312
|
+
}
|
|
2313
|
+
break;
|
|
2314
|
+
}
|
|
2315
2315
|
default:
|
|
2316
2316
|
break;
|
|
2317
2317
|
}
|
|
@@ -2453,9 +2453,7 @@ export class Client {
|
|
|
2453
2453
|
}
|
|
2454
2454
|
async isPreKeySignedByCurrentDevice(preKey) {
|
|
2455
2455
|
return this.runWithThisCryptoProfile(async () => {
|
|
2456
|
-
const payload =
|
|
2457
|
-
? fipsP256PreKeySignPayload(preKey.keyPair.publicKey)
|
|
2458
|
-
: xEncode(xConstants.CURVE, preKey.keyPair.publicKey);
|
|
2456
|
+
const payload = xPreKeySignaturePayload(preKey.keyPair.publicKey, "signed", this.cryptoProfile);
|
|
2459
2457
|
const opened = await xSignOpenAsync(preKey.signature, this.signKeys.publicKey);
|
|
2460
2458
|
return Boolean(opened && XUtils.bytesEqual(opened, payload));
|
|
2461
2459
|
});
|
|
@@ -2627,7 +2625,7 @@ export class Client {
|
|
|
2627
2625
|
let preKeys = await this.database.getPreKeys();
|
|
2628
2626
|
if (!preKeys ||
|
|
2629
2627
|
!(await this.isPreKeySignedByCurrentDevice(preKeys))) {
|
|
2630
|
-
const unsaved = await this.createPreKey();
|
|
2628
|
+
const unsaved = await this.createPreKey("signed");
|
|
2631
2629
|
const [saved] = await this.database.savePreKeys([unsaved], false);
|
|
2632
2630
|
if (!saved || saved.index == null)
|
|
2633
2631
|
throw new Error("Failed to save prekey — no index returned.");
|
|
@@ -2897,9 +2895,10 @@ export class Client {
|
|
|
2897
2895
|
: xConcat(DH1, DH2, DH3);
|
|
2898
2896
|
// shared secret key
|
|
2899
2897
|
const SK = xKDF(IKM);
|
|
2898
|
+
const initialSubkeys = xMessageKeySubkeys(SK);
|
|
2900
2899
|
const PK = (await xBoxKeyPairFromSecretAsync(SK))
|
|
2901
2900
|
.publicKey;
|
|
2902
|
-
const hmac = xHMAC(mail,
|
|
2901
|
+
const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
|
|
2903
2902
|
// associated data
|
|
2904
2903
|
const AD = fipsRead
|
|
2905
2904
|
? fipsP256AdFromIdentityPubs(IK_A, IK_BP)
|
|
@@ -2930,7 +2929,7 @@ export class Client {
|
|
|
2930
2929
|
this.acknowledgeRepeatedDecryptFailure(mail, failureCount, timestamp);
|
|
2931
2930
|
return;
|
|
2932
2931
|
}
|
|
2933
|
-
const unsealed = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce),
|
|
2932
|
+
const unsealed = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), initialSubkeys.encryptionKey);
|
|
2934
2933
|
if (unsealed) {
|
|
2935
2934
|
let plaintext = "";
|
|
2936
2935
|
if (!mail.forward) {
|
|
@@ -3084,15 +3083,17 @@ export class Client {
|
|
|
3084
3083
|
else if (hasRemoteDhChanged(candidateSession.DHr, ratchetHeader.dhPub)) {
|
|
3085
3084
|
await ratchetStepReceive(candidateSession, ratchetHeader.dhPub, ratchetHeader.pn);
|
|
3086
3085
|
}
|
|
3087
|
-
|
|
3088
|
-
let
|
|
3086
|
+
const messageKey = takeReceiveMessageKey(candidateSession, ratchetHeader.dhPub, ratchetHeader.n);
|
|
3087
|
+
let messageSubkeys = xMessageKeySubkeys(messageKey);
|
|
3088
|
+
let HMAC = xHMAC(mail, messageSubkeys.authenticationKey);
|
|
3089
3089
|
if (!XUtils.bytesEqual(HMAC, header) &&
|
|
3090
3090
|
firstInboundFromSubsequent &&
|
|
3091
3091
|
!originalSession.CKr) {
|
|
3092
3092
|
const ratchetedCandidate = cloneSessionCrypto(originalSession);
|
|
3093
3093
|
await ratchetStepReceive(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.pn);
|
|
3094
3094
|
const ratchetedMessageKey = takeReceiveMessageKey(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.n);
|
|
3095
|
-
const
|
|
3095
|
+
const ratchetedSubkeys = xMessageKeySubkeys(ratchetedMessageKey);
|
|
3096
|
+
const ratchetedHMAC = xHMAC(mail, ratchetedSubkeys.authenticationKey);
|
|
3096
3097
|
if (XUtils.bytesEqual(ratchetedHMAC, header)) {
|
|
3097
3098
|
if (libvexDebugDmEnabled()) {
|
|
3098
3099
|
debugLibvexDm("readMail subsequent: first inbound used DH-ratchet fallback", {
|
|
@@ -3101,7 +3102,7 @@ export class Client {
|
|
|
3101
3102
|
});
|
|
3102
3103
|
}
|
|
3103
3104
|
candidateSession = ratchetedCandidate;
|
|
3104
|
-
|
|
3105
|
+
messageSubkeys = ratchetedSubkeys;
|
|
3105
3106
|
HMAC = ratchetedHMAC;
|
|
3106
3107
|
}
|
|
3107
3108
|
}
|
|
@@ -3128,7 +3129,7 @@ export class Client {
|
|
|
3128
3129
|
return;
|
|
3129
3130
|
}
|
|
3130
3131
|
session = candidateSession;
|
|
3131
|
-
const decrypted = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce),
|
|
3132
|
+
const decrypted = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), messageSubkeys.encryptionKey);
|
|
3132
3133
|
if (decrypted) {
|
|
3133
3134
|
const fwdMsg2 = mail.forward
|
|
3134
3135
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
@@ -3251,6 +3252,75 @@ export class Client {
|
|
|
3251
3252
|
const res = await this.http.patch(this.getHost() + "/invite/" + inviteID);
|
|
3252
3253
|
return decodeHttpResponse(PermissionCodec, res.data);
|
|
3253
3254
|
}
|
|
3255
|
+
async registerAccountOrEnrollment(username, intent, password) {
|
|
3256
|
+
const resolvedUsername = username.trim().toLowerCase();
|
|
3257
|
+
if (!/^\w{3,19}$/.test(resolvedUsername)) {
|
|
3258
|
+
return [
|
|
3259
|
+
null,
|
|
3260
|
+
new Error("Username must be between three and nineteen letters, digits, or underscores."),
|
|
3261
|
+
];
|
|
3262
|
+
}
|
|
3263
|
+
while (!this.xKeyRing) {
|
|
3264
|
+
await sleep(100);
|
|
3265
|
+
}
|
|
3266
|
+
const regKey = await this.getToken("register");
|
|
3267
|
+
if (regKey) {
|
|
3268
|
+
const signKey = XUtils.encodeHex(this.signKeys.publicKey);
|
|
3269
|
+
const signed = XUtils.encodeHex(await xSignAsync(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
|
|
3270
|
+
const preKeyIndex = this.xKeyRing.preKeys.index;
|
|
3271
|
+
const regMsg = {
|
|
3272
|
+
deviceName: this.options?.deviceName ?? "unknown",
|
|
3273
|
+
intent,
|
|
3274
|
+
preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
|
|
3275
|
+
preKeyIndex,
|
|
3276
|
+
preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
|
|
3277
|
+
signed,
|
|
3278
|
+
signKey,
|
|
3279
|
+
username: resolvedUsername,
|
|
3280
|
+
};
|
|
3281
|
+
if (password !== undefined) {
|
|
3282
|
+
regMsg.password = password;
|
|
3283
|
+
}
|
|
3284
|
+
try {
|
|
3285
|
+
const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
3286
|
+
try {
|
|
3287
|
+
const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
|
|
3288
|
+
this.device = device;
|
|
3289
|
+
this.setUser(user);
|
|
3290
|
+
this.token = token;
|
|
3291
|
+
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
3292
|
+
}
|
|
3293
|
+
catch {
|
|
3294
|
+
const pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
|
|
3295
|
+
return [
|
|
3296
|
+
null,
|
|
3297
|
+
new DeviceApprovalRequiredError({
|
|
3298
|
+
challenge: pendingApproval.challenge,
|
|
3299
|
+
expiresAt: pendingApproval.expiresAt,
|
|
3300
|
+
requestID: pendingApproval.requestID,
|
|
3301
|
+
userID: pendingApproval.userID,
|
|
3302
|
+
}),
|
|
3303
|
+
];
|
|
3304
|
+
}
|
|
3305
|
+
return [this.getUser(), null];
|
|
3306
|
+
}
|
|
3307
|
+
catch (err) {
|
|
3308
|
+
if (isHttpError(err) && err.response) {
|
|
3309
|
+
return [
|
|
3310
|
+
null,
|
|
3311
|
+
new Error(spireErrorBodyMessage(err.response.data)),
|
|
3312
|
+
];
|
|
3313
|
+
}
|
|
3314
|
+
return [
|
|
3315
|
+
null,
|
|
3316
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
3317
|
+
];
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
else {
|
|
3321
|
+
return [null, new Error("Couldn't get regkey from server.")];
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3254
3324
|
registerDecryptFailure(mail) {
|
|
3255
3325
|
const count = (this.decryptFailureCounts.get(mail.mailID) ?? 0) + 1;
|
|
3256
3326
|
this.decryptFailureCounts.set(mail.mailID, count);
|
|
@@ -3299,6 +3369,19 @@ export class Client {
|
|
|
3299
3369
|
requestID +
|
|
3300
3370
|
"/reject");
|
|
3301
3371
|
}
|
|
3372
|
+
async removeServerIcon(serverID) {
|
|
3373
|
+
const res = await this.http.delete(this.getHost() + "/server-icon/" + serverID);
|
|
3374
|
+
return decodeHttpResponse(ServerCodec, res.data);
|
|
3375
|
+
}
|
|
3376
|
+
async replaceAccountPassword(payload) {
|
|
3377
|
+
await this.http.patch(this.getHost() + "/user/" + this.getUser().userID + "/password", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
|
|
3378
|
+
}
|
|
3379
|
+
async resetPasswordWithPasskey(newPassword) {
|
|
3380
|
+
const passwordError = registrationPasswordError(newPassword);
|
|
3381
|
+
if (passwordError)
|
|
3382
|
+
throw passwordError;
|
|
3383
|
+
await this.replaceAccountPassword({ newPassword });
|
|
3384
|
+
}
|
|
3302
3385
|
async respond(msg) {
|
|
3303
3386
|
const response = {
|
|
3304
3387
|
signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
|
|
@@ -3378,7 +3461,7 @@ export class Client {
|
|
|
3378
3461
|
challenge: newDevice.challenge,
|
|
3379
3462
|
expiresAt: newDevice.expiresAt,
|
|
3380
3463
|
requestID: newDevice.requestID,
|
|
3381
|
-
userID: newDevice.userID
|
|
3464
|
+
userID: newDevice.userID,
|
|
3382
3465
|
});
|
|
3383
3466
|
}
|
|
3384
3467
|
else {
|
|
@@ -3606,8 +3689,8 @@ export class Client {
|
|
|
3606
3689
|
return;
|
|
3607
3690
|
}
|
|
3608
3691
|
const stableDevices = [...targetDevices.values()].sort((a, b) => a.deviceID.localeCompare(b.deviceID, "en"));
|
|
3609
|
-
|
|
3610
|
-
|
|
3692
|
+
const successfulOwnerIDs = new Set();
|
|
3693
|
+
const lastErrorByOwnerID = new Map();
|
|
3611
3694
|
for (let index = 0; index < stableDevices.length; index += MAIL_FANOUT_CONCURRENCY) {
|
|
3612
3695
|
const batch = stableDevices.slice(index, index + MAIL_FANOUT_CONCURRENCY);
|
|
3613
3696
|
const results = await Promise.all(batch.map(async (device) => {
|
|
@@ -3615,36 +3698,40 @@ export class Client {
|
|
|
3615
3698
|
? this.getUser()
|
|
3616
3699
|
: this.userRecords[device.owner];
|
|
3617
3700
|
if (!ownerRecord) {
|
|
3618
|
-
return
|
|
3701
|
+
return {
|
|
3702
|
+
device,
|
|
3703
|
+
error: new Error(`Missing owner record for device ${device.deviceID}.`),
|
|
3704
|
+
ok: false,
|
|
3705
|
+
};
|
|
3619
3706
|
}
|
|
3620
3707
|
try {
|
|
3621
3708
|
await this.sendMailWithRecovery(device, ownerRecord, msgBytes, uuidToUint8(channelID), mailID, false);
|
|
3622
|
-
return
|
|
3709
|
+
return { device, ok: true };
|
|
3623
3710
|
}
|
|
3624
|
-
catch (
|
|
3625
|
-
return
|
|
3711
|
+
catch (error) {
|
|
3712
|
+
return { device, error, ok: false };
|
|
3626
3713
|
}
|
|
3627
3714
|
}));
|
|
3628
3715
|
for (const result of results) {
|
|
3629
|
-
if (result
|
|
3630
|
-
|
|
3631
|
-
|
|
3716
|
+
if (result.ok) {
|
|
3717
|
+
successfulOwnerIDs.add(result.device.owner);
|
|
3718
|
+
}
|
|
3719
|
+
else {
|
|
3720
|
+
lastErrorByOwnerID.set(result.device.owner, result.error);
|
|
3632
3721
|
}
|
|
3633
3722
|
}
|
|
3634
|
-
if (failCount === stableDevices.length) {
|
|
3635
|
-
break;
|
|
3636
|
-
}
|
|
3637
|
-
}
|
|
3638
|
-
if (failCount === stableDevices.length) {
|
|
3639
|
-
throw lastErr instanceof Error
|
|
3640
|
-
? lastErr
|
|
3641
|
-
: new Error(String(lastErr));
|
|
3642
3723
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3724
|
+
const requiredOwnerIDs = peerUserIDs.length > 0 ? peerUserIDs : [myUserID];
|
|
3725
|
+
const unreachableOwnerIDs = requiredOwnerIDs.filter((ownerID) => !successfulOwnerIDs.has(ownerID));
|
|
3726
|
+
if (unreachableOwnerIDs.length > 0) {
|
|
3727
|
+
const lastError = lastErrorByOwnerID.get(unreachableOwnerIDs.at(-1) ?? "");
|
|
3728
|
+
if (unreachableOwnerIDs.length === requiredOwnerIDs.length &&
|
|
3729
|
+
lastError instanceof Error) {
|
|
3730
|
+
throw lastError;
|
|
3731
|
+
}
|
|
3732
|
+
const deliveryError = new Error(`Message could not be delivered to ${String(unreachableOwnerIDs.length)} channel member(s).`);
|
|
3733
|
+
deliveryError.cause = lastError;
|
|
3734
|
+
throw deliveryError;
|
|
3648
3735
|
}
|
|
3649
3736
|
}
|
|
3650
3737
|
/* Sends encrypted mail to a user. */
|
|
@@ -3680,6 +3767,7 @@ export class Client {
|
|
|
3680
3767
|
await ratchetStepSend(session);
|
|
3681
3768
|
}
|
|
3682
3769
|
const { messageKey, n } = takeSendMessageKey(session);
|
|
3770
|
+
const messageSubkeys = xMessageKeySubkeys(messageKey);
|
|
3683
3771
|
const ratchetHeader = {
|
|
3684
3772
|
dhPub: session.DHsPublic,
|
|
3685
3773
|
n,
|
|
@@ -3687,7 +3775,7 @@ export class Client {
|
|
|
3687
3775
|
version: 1,
|
|
3688
3776
|
};
|
|
3689
3777
|
const nonce = xMakeNonce();
|
|
3690
|
-
const cipher = await xSecretboxAsync(msg, nonce,
|
|
3778
|
+
const cipher = await xSecretboxAsync(msg, nonce, messageSubkeys.encryptionKey);
|
|
3691
3779
|
const extra = encodeRatchetHeader(ratchetHeader);
|
|
3692
3780
|
const mail = {
|
|
3693
3781
|
authorID: this.getUser().userID,
|
|
@@ -3709,7 +3797,7 @@ export class Client {
|
|
|
3709
3797
|
transmissionID: uuid.v4(),
|
|
3710
3798
|
type: "resource",
|
|
3711
3799
|
};
|
|
3712
|
-
const hmac = xHMAC(mail,
|
|
3800
|
+
const hmac = xHMAC(mail, messageSubkeys.authenticationKey);
|
|
3713
3801
|
const fwdOut = forward
|
|
3714
3802
|
? messageSchema.parse(msgpack.decode(msg))
|
|
3715
3803
|
: null;
|
|
@@ -3822,6 +3910,7 @@ export class Client {
|
|
|
3822
3910
|
}
|
|
3823
3911
|
let lastErr;
|
|
3824
3912
|
let failCount = 0;
|
|
3913
|
+
let successCount = 0;
|
|
3825
3914
|
// One logical DM fan-outs to multiple recipient devices. Reuse a
|
|
3826
3915
|
// single mailID so local/UI dedupe treats it as one message.
|
|
3827
3916
|
const messageMailID = uuid.v4();
|
|
@@ -3865,24 +3954,19 @@ export class Client {
|
|
|
3865
3954
|
lastErr = result;
|
|
3866
3955
|
failCount += 1;
|
|
3867
3956
|
}
|
|
3957
|
+
else {
|
|
3958
|
+
successCount += 1;
|
|
3959
|
+
}
|
|
3868
3960
|
}
|
|
3869
3961
|
if (failCount === deviceList.length) {
|
|
3870
3962
|
break;
|
|
3871
3963
|
}
|
|
3872
3964
|
}
|
|
3873
|
-
if (
|
|
3965
|
+
if (successCount === 0) {
|
|
3874
3966
|
const base = lastErr instanceof Error
|
|
3875
3967
|
? lastErr
|
|
3876
3968
|
: new Error(String(lastErr));
|
|
3877
|
-
|
|
3878
|
-
throw base;
|
|
3879
|
-
}
|
|
3880
|
-
// Multi-device: do not “succeed” when only one device of several got mail —
|
|
3881
|
-
// callers and tests have no per-device result and the other copy times out.
|
|
3882
|
-
const partial = new Error(`Direct message failed to reach ${String(failCount)} of ` +
|
|
3883
|
-
`${String(deviceList.length)} peer device(s) (X3DH/post).`);
|
|
3884
|
-
partial.cause = base;
|
|
3885
|
-
throw partial;
|
|
3969
|
+
throw base;
|
|
3886
3970
|
}
|
|
3887
3971
|
if (userID !== this.getUser().userID &&
|
|
3888
3972
|
firstOutgoingMessage.current !== null) {
|
|
@@ -3917,6 +4001,10 @@ export class Client {
|
|
|
3917
4001
|
});
|
|
3918
4002
|
return decodeHttpResponse(AccountEntitlementsCodec, res.data);
|
|
3919
4003
|
}
|
|
4004
|
+
async setServerMemberRole(permissionID, powerLevel) {
|
|
4005
|
+
const res = await this.http.patch(this.getHost() + "/permission/" + permissionID, msgpack.encode({ powerLevel }), { headers: { "Content-Type": "application/msgpack" } });
|
|
4006
|
+
return decodeHttpResponse(PermissionCodec, res.data);
|
|
4007
|
+
}
|
|
3920
4008
|
setUser(user) {
|
|
3921
4009
|
this.user = user;
|
|
3922
4010
|
// Fresh identity / token: drop stale 404 negative-cache entries so a
|
|
@@ -3957,13 +4045,21 @@ export class Client {
|
|
|
3957
4045
|
async submitOTK(amount) {
|
|
3958
4046
|
const otks = [];
|
|
3959
4047
|
for (let i = 0; i < amount; i++) {
|
|
3960
|
-
otks.push(await this.createPreKey());
|
|
4048
|
+
otks.push(await this.createPreKey("one-time"));
|
|
3961
4049
|
}
|
|
3962
4050
|
const savedKeys = await this.database.savePreKeys(otks, true);
|
|
3963
4051
|
await this.http.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {
|
|
3964
4052
|
headers: { "Content-Type": "application/msgpack" },
|
|
3965
4053
|
});
|
|
3966
4054
|
}
|
|
4055
|
+
async updateChannel(channelID, name) {
|
|
4056
|
+
const res = await this.http.patch(this.getHost() + "/channel/" + channelID, msgpack.encode({ name }), { headers: { "Content-Type": "application/msgpack" } });
|
|
4057
|
+
return decodeHttpResponse(ChannelCodec, res.data);
|
|
4058
|
+
}
|
|
4059
|
+
async updateServer(serverID, name) {
|
|
4060
|
+
const res = await this.http.patch(this.getHost() + "/server/" + serverID, msgpack.encode({ name }), { headers: { "Content-Type": "application/msgpack" } });
|
|
4061
|
+
return decodeHttpResponse(ServerCodec, res.data);
|
|
4062
|
+
}
|
|
3967
4063
|
async uploadAvatar(avatar) {
|
|
3968
4064
|
const canUseMultipart = typeof FormData !== "undefined" &&
|
|
3969
4065
|
(() => {
|
|
@@ -4051,5 +4147,37 @@ export class Client {
|
|
|
4051
4147
|
return null;
|
|
4052
4148
|
}
|
|
4053
4149
|
}
|
|
4150
|
+
async uploadServerIcon(serverID, icon) {
|
|
4151
|
+
const canUseMultipart = typeof FormData !== "undefined" &&
|
|
4152
|
+
(() => {
|
|
4153
|
+
try {
|
|
4154
|
+
void new Blob([new Uint8Array([1, 2, 3])]);
|
|
4155
|
+
return true;
|
|
4156
|
+
}
|
|
4157
|
+
catch {
|
|
4158
|
+
return false;
|
|
4159
|
+
}
|
|
4160
|
+
})();
|
|
4161
|
+
if (canUseMultipart) {
|
|
4162
|
+
const payload = new FormData();
|
|
4163
|
+
payload.set("icon", new Blob([new Uint8Array(icon)]));
|
|
4164
|
+
const res = await this.http.post(this.getHost() + "/server-icon/" + serverID, payload, {
|
|
4165
|
+
headers: { "Content-Type": "multipart/form-data" },
|
|
4166
|
+
onUploadProgress: (progressEvent) => {
|
|
4167
|
+
const { loaded, total = 0 } = progressEvent;
|
|
4168
|
+
this.emitter.emit("fileProgress", {
|
|
4169
|
+
direction: "upload",
|
|
4170
|
+
loaded,
|
|
4171
|
+
progress: Math.round((loaded * 100) / (progressEvent.total ?? 1)),
|
|
4172
|
+
token: serverID,
|
|
4173
|
+
total,
|
|
4174
|
+
});
|
|
4175
|
+
},
|
|
4176
|
+
});
|
|
4177
|
+
return decodeHttpResponse(ServerCodec, res.data);
|
|
4178
|
+
}
|
|
4179
|
+
const res = await this.http.post(this.getHost() + "/server-icon/" + serverID + "/json", msgpack.encode({ file: XUtils.encodeBase64(icon) }), { headers: { "Content-Type": "application/msgpack" } });
|
|
4180
|
+
return decodeHttpResponse(ServerCodec, res.data);
|
|
4181
|
+
}
|
|
4054
4182
|
}
|
|
4055
4183
|
//# sourceMappingURL=Client.js.map
|