@vex-chat/libvex 7.4.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Client.js CHANGED
@@ -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, fipsP256PreKeySignPayload, isFipsInitialExtraV1, } from "./utils/fipsMailExtra.js";
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
  }
@@ -529,6 +539,7 @@ export class Client {
529
539
  * Helpers for information/actions related to the currently authenticated account.
530
540
  */
531
541
  me = {
542
+ changePassword: this.changePassword.bind(this),
532
543
  /**
533
544
  * Retrieves current device details.
534
545
  *
@@ -614,6 +625,7 @@ export class Client {
614
625
  listDevices: this.passkeyListDevices.bind(this),
615
626
  recoverDeviceRequest: this.passkeyRecoverDeviceRequest.bind(this),
616
627
  rejectDeviceRequest: this.passkeyRejectDeviceRequest.bind(this),
628
+ resetPassword: this.resetPasswordWithPasskey.bind(this),
617
629
  };
618
630
  /**
619
631
  * Permission-management methods for the current user.
@@ -849,14 +861,23 @@ export class Client {
849
861
  const atRestAes = XUtils.deriveLocalAtRestAesKey(idKeys.secretKey, profile);
850
862
  let resolvedStorage = storage;
851
863
  if (!resolvedStorage) {
852
- const { createNodeStorage } = await import("./storage/node.js");
864
+ const nodeStorageModule = "./storage/node.js";
865
+ const isNodeStorageModule = (value) => typeof value === "object" &&
866
+ value !== null &&
867
+ "createNodeStorage" in value &&
868
+ typeof value.createNodeStorage === "function";
869
+ const nodeStorage = await import(
870
+ /* @vite-ignore */ nodeStorageModule);
871
+ if (!isNodeStorageModule(nodeStorage)) {
872
+ throw new Error("Node storage adapter is unavailable.");
873
+ }
853
874
  const dbFileName = options?.inMemoryDb
854
875
  ? ":memory:"
855
876
  : XUtils.encodeHex(signKeys.publicKey) + ".sqlite";
856
877
  const dbPath = options?.dbFolder
857
878
  ? options.dbFolder + "/" + dbFileName
858
879
  : dbFileName;
859
- resolvedStorage = createNodeStorage(dbPath, atRestAes);
880
+ resolvedStorage = nodeStorage.createNodeStorage(dbPath, atRestAes);
860
881
  }
861
882
  await resolvedStorage.init();
862
883
  const client = new Client({
@@ -1147,10 +1168,37 @@ export class Client {
1147
1168
  return null;
1148
1169
  }
1149
1170
  /**
1150
- * Logs out the current authenticated session from the server.
1171
+ * Ends the local authenticated session and disconnects realtime transport.
1172
+ * Spire access tokens are stateless, so local credential removal is the
1173
+ * authoritative logout boundary.
1151
1174
  */
1152
1175
  async logout() {
1153
- await this.http.post(this.getHost() + "/goodbye");
1176
+ try {
1177
+ if (this.token) {
1178
+ await this.http.post(this.getHost() + "/goodbye");
1179
+ }
1180
+ }
1181
+ catch {
1182
+ // Logout must still complete locally when the server is unreachable.
1183
+ }
1184
+ finally {
1185
+ this.token = null;
1186
+ this.user = undefined;
1187
+ this.device = undefined;
1188
+ delete this.http.defaults.headers.common.Authorization;
1189
+ delete this.http.defaults.headers.common["X-Device-Token"];
1190
+ this.autoReconnectEnabled = false;
1191
+ this.postAuthVersion++;
1192
+ if (this.reconnectTimer) {
1193
+ clearTimeout(this.reconnectTimer);
1194
+ this.reconnectTimer = null;
1195
+ }
1196
+ if (this.pingInterval) {
1197
+ clearInterval(this.pingInterval);
1198
+ this.pingInterval = null;
1199
+ }
1200
+ this.socket.close();
1201
+ }
1154
1202
  }
1155
1203
  /** Removes an event listener. See {@link ClientEvents} for available events. */
1156
1204
  off(event, fn, context) {
@@ -1202,115 +1250,45 @@ export class Client {
1202
1250
  /**
1203
1251
  * Registers a new account on the server.
1204
1252
  *
1205
- * @param username - Optional username to register (must be unique when provided).
1206
- * @param password - Optional legacy password used when talking to pre-keycluster servers.
1253
+ * @param username - Username to register.
1254
+ * @param password - Password for the account and device approval request.
1207
1255
  * @returns `[user, null]` on success, `[null, error]` on failure.
1208
1256
  *
1209
1257
  * @example
1210
1258
  * ```ts
1211
- * const [user, err] = await client.register("MyUsername");
1259
+ * const [user, err] = await client.register("MyUsername", "correct horse battery staple");
1212
1260
  * ```
1213
1261
  */
1214
1262
  async register(username, password) {
1215
- while (!this.xKeyRing) {
1216
- await sleep(100);
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
- : uuid.v4();
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
- password: resolvedPassword,
1235
- preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
1236
- preKeyIndex,
1237
- preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
1238
- signed,
1239
- signKey,
1240
- username: resolvedUsername,
1241
- };
1242
- try {
1243
- const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
1244
- // New key-cluster server response: { device, token, user }.
1245
- // Legacy response (still deployed in some environments): user only.
1246
- let didDecodeRegisterResponse = false;
1247
- let pendingApproval = null;
1248
- try {
1249
- const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
1250
- this.device = device;
1251
- this.setUser(user);
1252
- this.token = token;
1253
- this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
1254
- didDecodeRegisterResponse = true;
1255
- }
1256
- catch {
1257
- // fall through to legacy decode path
1258
- }
1259
- if (!didDecodeRegisterResponse) {
1260
- try {
1261
- pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
1262
- }
1263
- catch {
1264
- // fall through to legacy decode path
1265
- }
1266
- }
1267
- if (!didDecodeRegisterResponse) {
1268
- if (pendingApproval !== null) {
1269
- return [
1270
- null,
1271
- new DeviceApprovalRequiredError({
1272
- challenge: pendingApproval.challenge,
1273
- expiresAt: pendingApproval.expiresAt,
1274
- requestID: pendingApproval.requestID,
1275
- userID: pendingApproval.userID ?? null,
1276
- }),
1277
- ];
1278
- }
1279
- const legacyUser = decodeHttpResponse(UserCodec, res.data);
1280
- this.setUser(legacyUser);
1281
- // Legacy servers require /auth after /register to get a JWT.
1282
- const loginResult = await this.login(resolvedUsername, resolvedPassword);
1283
- if (!loginResult.ok) {
1284
- return [
1285
- null,
1286
- new Error(loginResult.error ??
1287
- "Legacy register succeeded but login failed."),
1288
- ];
1289
- }
1290
- }
1291
- return [this.getUser(), null];
1292
- }
1293
- catch (err) {
1294
- if (isHttpError(err) && err.response) {
1295
- return [
1296
- null,
1297
- new Error(spireErrorBodyMessage(err.response.data)),
1298
- ];
1299
- }
1300
- return [
1301
- null,
1302
- err instanceof Error ? err : new Error(String(err)),
1303
- ];
1304
- }
1305
- }
1306
- else {
1307
- return [null, new Error("Couldn't get regkey from server.")];
1263
+ const passwordError = registrationPasswordError(password);
1264
+ if (passwordError) {
1265
+ return [null, passwordError];
1308
1266
  }
1267
+ return this.registerAccountOrEnrollment(username, "create-account", password);
1309
1268
  }
1310
1269
  removeAllListeners(event) {
1311
1270
  this.emitter.removeAllListeners(event);
1312
1271
  return this;
1313
1272
  }
1273
+ /**
1274
+ * Requests approval for a new device using the account password. This
1275
+ * enrollment flow never creates an account when the username is unknown.
1276
+ */
1277
+ async requestDeviceEnrollment(username, password) {
1278
+ const passwordError = registrationPasswordError(password);
1279
+ if (passwordError) {
1280
+ return [null, passwordError];
1281
+ }
1282
+ return this.registerAccountOrEnrollment(username, "enroll-device", password);
1283
+ }
1284
+ /**
1285
+ * Requests enrollment for this device after a successful passkey ceremony.
1286
+ * This is not account registration: the passkey bearer must identify an
1287
+ * existing account, and the server returns a pending device request.
1288
+ */
1289
+ async requestDeviceEnrollmentWithPasskey(username) {
1290
+ return this.registerAccountOrEnrollment(username, "enroll-device");
1291
+ }
1314
1292
  /**
1315
1293
  * Updates the local retention cap (1–30 days) and prunes immediately.
1316
1294
  * Does not affect server-side storage.
@@ -1448,6 +1426,16 @@ export class Client {
1448
1426
  signature: XUtils.decodeHex(preKey.signature),
1449
1427
  };
1450
1428
  }
1429
+ async changePassword(currentPassword, newPassword) {
1430
+ const passwordError = registrationPasswordError(newPassword);
1431
+ if (passwordError)
1432
+ throw passwordError;
1433
+ if (currentPassword.length === 0 ||
1434
+ currentPassword.length > ACCOUNT_PASSWORD_MAX_LENGTH) {
1435
+ throw new Error("Current password is invalid.");
1436
+ }
1437
+ await this.replaceAccountPassword({ currentPassword, newPassword });
1438
+ }
1451
1439
  async createChannel(name, serverID) {
1452
1440
  const body = { name };
1453
1441
  const res = await this.http.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
@@ -1515,12 +1503,10 @@ export class Client {
1515
1503
  const res = await this.http.post(this.getHost() + "/server/" + serverID + "/invites", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
1516
1504
  return decodeHttpResponse(InviteCodec, res.data);
1517
1505
  }
1518
- async createPreKey() {
1506
+ async createPreKey(kind) {
1519
1507
  return this.runWithThisCryptoProfile(async () => {
1520
1508
  const preKeyPair = await xBoxKeyPairAsync();
1521
- const toSign = this.cryptoProfile === "fips"
1522
- ? fipsP256PreKeySignPayload(preKeyPair.publicKey)
1523
- : xEncode(xConstants.CURVE, preKeyPair.publicKey);
1509
+ const toSign = xPreKeySignaturePayload(preKeyPair.publicKey, kind, this.cryptoProfile);
1524
1510
  return {
1525
1511
  keyPair: preKeyPair,
1526
1512
  signature: await xSignAsync(toSign, this.signKeys.secretKey),
@@ -1596,12 +1582,13 @@ export class Client {
1596
1582
  : XUtils.numberToUint8Arr(0);
1597
1583
  // shared secret key
1598
1584
  const SK = xKDF(IKM);
1585
+ const initialSubkeys = xMessageKeySubkeys(SK);
1599
1586
  const PK = (await xBoxKeyPairFromSecretAsync(SK)).publicKey;
1600
1587
  const AD = fips
1601
1588
  ? fipsP256AdFromIdentityPubs(IK_AP, new Uint8Array(keyBundle.signKey))
1602
1589
  : xConcat(xEncode(xConstants.CURVE, IK_AP), xEncode(xConstants.CURVE, IK_B));
1603
1590
  const nonce = xMakeNonce();
1604
- const cipher = await xSecretboxAsync(message, nonce, SK);
1591
+ const cipher = await xSecretboxAsync(message, nonce, initialSubkeys.encryptionKey);
1605
1592
  const signKeyWire = fips ? IK_AP : this.signKeys.publicKey;
1606
1593
  const ephKeyWire = ephemeralKeys.publicKey;
1607
1594
  const extra = fips
@@ -1620,7 +1607,7 @@ export class Client {
1620
1607
  recipient: device.deviceID,
1621
1608
  sender: this.getDevice().deviceID,
1622
1609
  };
1623
- const hmac = xHMAC(mail, SK);
1610
+ const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
1624
1611
  const msg = {
1625
1612
  action: "CREATE",
1626
1613
  data: mail,
@@ -2114,6 +2101,9 @@ export class Client {
2114
2101
  "/device/" +
2115
2102
  this.getDevice().deviceID +
2116
2103
  "/mail");
2104
+ if (!this.token || this.isManualCloseInFlight()) {
2105
+ return;
2106
+ }
2117
2107
  const mailBuffer = new Uint8Array(res.data);
2118
2108
  const rawInbox = z
2119
2109
  .array(mailInboxEntry)
@@ -2445,9 +2435,7 @@ export class Client {
2445
2435
  }
2446
2436
  async isPreKeySignedByCurrentDevice(preKey) {
2447
2437
  return this.runWithThisCryptoProfile(async () => {
2448
- const payload = this.cryptoProfile === "fips"
2449
- ? fipsP256PreKeySignPayload(preKey.keyPair.publicKey)
2450
- : xEncode(xConstants.CURVE, preKey.keyPair.publicKey);
2438
+ const payload = xPreKeySignaturePayload(preKey.keyPair.publicKey, "signed", this.cryptoProfile);
2451
2439
  const opened = await xSignOpenAsync(preKey.signature, this.signKeys.publicKey);
2452
2440
  return Boolean(opened && XUtils.bytesEqual(opened, payload));
2453
2441
  });
@@ -2619,7 +2607,7 @@ export class Client {
2619
2607
  let preKeys = await this.database.getPreKeys();
2620
2608
  if (!preKeys ||
2621
2609
  !(await this.isPreKeySignedByCurrentDevice(preKeys))) {
2622
- const unsaved = await this.createPreKey();
2610
+ const unsaved = await this.createPreKey("signed");
2623
2611
  const [saved] = await this.database.savePreKeys([unsaved], false);
2624
2612
  if (!saved || saved.index == null)
2625
2613
  throw new Error("Failed to save prekey — no index returned.");
@@ -2889,9 +2877,10 @@ export class Client {
2889
2877
  : xConcat(DH1, DH2, DH3);
2890
2878
  // shared secret key
2891
2879
  const SK = xKDF(IKM);
2880
+ const initialSubkeys = xMessageKeySubkeys(SK);
2892
2881
  const PK = (await xBoxKeyPairFromSecretAsync(SK))
2893
2882
  .publicKey;
2894
- const hmac = xHMAC(mail, SK);
2883
+ const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
2895
2884
  // associated data
2896
2885
  const AD = fipsRead
2897
2886
  ? fipsP256AdFromIdentityPubs(IK_A, IK_BP)
@@ -2922,7 +2911,7 @@ export class Client {
2922
2911
  this.acknowledgeRepeatedDecryptFailure(mail, failureCount, timestamp);
2923
2912
  return;
2924
2913
  }
2925
- const unsealed = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), SK);
2914
+ const unsealed = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), initialSubkeys.encryptionKey);
2926
2915
  if (unsealed) {
2927
2916
  let plaintext = "";
2928
2917
  if (!mail.forward) {
@@ -3076,15 +3065,17 @@ export class Client {
3076
3065
  else if (hasRemoteDhChanged(candidateSession.DHr, ratchetHeader.dhPub)) {
3077
3066
  await ratchetStepReceive(candidateSession, ratchetHeader.dhPub, ratchetHeader.pn);
3078
3067
  }
3079
- let messageKey = takeReceiveMessageKey(candidateSession, ratchetHeader.dhPub, ratchetHeader.n);
3080
- let HMAC = xHMAC(mail, messageKey);
3068
+ const messageKey = takeReceiveMessageKey(candidateSession, ratchetHeader.dhPub, ratchetHeader.n);
3069
+ let messageSubkeys = xMessageKeySubkeys(messageKey);
3070
+ let HMAC = xHMAC(mail, messageSubkeys.authenticationKey);
3081
3071
  if (!XUtils.bytesEqual(HMAC, header) &&
3082
3072
  firstInboundFromSubsequent &&
3083
3073
  !originalSession.CKr) {
3084
3074
  const ratchetedCandidate = cloneSessionCrypto(originalSession);
3085
3075
  await ratchetStepReceive(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.pn);
3086
3076
  const ratchetedMessageKey = takeReceiveMessageKey(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.n);
3087
- const ratchetedHMAC = xHMAC(mail, ratchetedMessageKey);
3077
+ const ratchetedSubkeys = xMessageKeySubkeys(ratchetedMessageKey);
3078
+ const ratchetedHMAC = xHMAC(mail, ratchetedSubkeys.authenticationKey);
3088
3079
  if (XUtils.bytesEqual(ratchetedHMAC, header)) {
3089
3080
  if (libvexDebugDmEnabled()) {
3090
3081
  debugLibvexDm("readMail subsequent: first inbound used DH-ratchet fallback", {
@@ -3093,7 +3084,7 @@ export class Client {
3093
3084
  });
3094
3085
  }
3095
3086
  candidateSession = ratchetedCandidate;
3096
- messageKey = ratchetedMessageKey;
3087
+ messageSubkeys = ratchetedSubkeys;
3097
3088
  HMAC = ratchetedHMAC;
3098
3089
  }
3099
3090
  }
@@ -3120,7 +3111,7 @@ export class Client {
3120
3111
  return;
3121
3112
  }
3122
3113
  session = candidateSession;
3123
- const decrypted = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), messageKey);
3114
+ const decrypted = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), messageSubkeys.encryptionKey);
3124
3115
  if (decrypted) {
3125
3116
  const fwdMsg2 = mail.forward
3126
3117
  ? messageSchema.parse(msgpack.decode(decrypted))
@@ -3243,6 +3234,75 @@ export class Client {
3243
3234
  const res = await this.http.patch(this.getHost() + "/invite/" + inviteID);
3244
3235
  return decodeHttpResponse(PermissionCodec, res.data);
3245
3236
  }
3237
+ async registerAccountOrEnrollment(username, intent, password) {
3238
+ const resolvedUsername = username.trim().toLowerCase();
3239
+ if (!/^\w{3,19}$/.test(resolvedUsername)) {
3240
+ return [
3241
+ null,
3242
+ new Error("Username must be between three and nineteen letters, digits, or underscores."),
3243
+ ];
3244
+ }
3245
+ while (!this.xKeyRing) {
3246
+ await sleep(100);
3247
+ }
3248
+ const regKey = await this.getToken("register");
3249
+ if (regKey) {
3250
+ const signKey = XUtils.encodeHex(this.signKeys.publicKey);
3251
+ const signed = XUtils.encodeHex(await xSignAsync(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
3252
+ const preKeyIndex = this.xKeyRing.preKeys.index;
3253
+ const regMsg = {
3254
+ deviceName: this.options?.deviceName ?? "unknown",
3255
+ intent,
3256
+ preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
3257
+ preKeyIndex,
3258
+ preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
3259
+ signed,
3260
+ signKey,
3261
+ username: resolvedUsername,
3262
+ };
3263
+ if (password !== undefined) {
3264
+ regMsg.password = password;
3265
+ }
3266
+ try {
3267
+ const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
3268
+ try {
3269
+ const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
3270
+ this.device = device;
3271
+ this.setUser(user);
3272
+ this.token = token;
3273
+ this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
3274
+ }
3275
+ catch {
3276
+ const pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
3277
+ return [
3278
+ null,
3279
+ new DeviceApprovalRequiredError({
3280
+ challenge: pendingApproval.challenge,
3281
+ expiresAt: pendingApproval.expiresAt,
3282
+ requestID: pendingApproval.requestID,
3283
+ userID: pendingApproval.userID,
3284
+ }),
3285
+ ];
3286
+ }
3287
+ return [this.getUser(), null];
3288
+ }
3289
+ catch (err) {
3290
+ if (isHttpError(err) && err.response) {
3291
+ return [
3292
+ null,
3293
+ new Error(spireErrorBodyMessage(err.response.data)),
3294
+ ];
3295
+ }
3296
+ return [
3297
+ null,
3298
+ err instanceof Error ? err : new Error(String(err)),
3299
+ ];
3300
+ }
3301
+ }
3302
+ else {
3303
+ return [null, new Error("Couldn't get regkey from server.")];
3304
+ }
3305
+ }
3246
3306
  registerDecryptFailure(mail) {
3247
3307
  const count = (this.decryptFailureCounts.get(mail.mailID) ?? 0) + 1;
3248
3308
  this.decryptFailureCounts.set(mail.mailID, count);
@@ -3291,6 +3351,15 @@ export class Client {
3291
3351
  requestID +
3292
3352
  "/reject");
3293
3353
  }
3354
+ async replaceAccountPassword(payload) {
3355
+ await this.http.patch(this.getHost() + "/user/" + this.getUser().userID + "/password", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
3356
+ }
3357
+ async resetPasswordWithPasskey(newPassword) {
3358
+ const passwordError = registrationPasswordError(newPassword);
3359
+ if (passwordError)
3360
+ throw passwordError;
3361
+ await this.replaceAccountPassword({ newPassword });
3362
+ }
3294
3363
  async respond(msg) {
3295
3364
  const response = {
3296
3365
  signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
@@ -3370,7 +3439,7 @@ export class Client {
3370
3439
  challenge: newDevice.challenge,
3371
3440
  expiresAt: newDevice.expiresAt,
3372
3441
  requestID: newDevice.requestID,
3373
- userID: newDevice.userID ?? null,
3442
+ userID: newDevice.userID,
3374
3443
  });
3375
3444
  }
3376
3445
  else {
@@ -3672,6 +3741,7 @@ export class Client {
3672
3741
  await ratchetStepSend(session);
3673
3742
  }
3674
3743
  const { messageKey, n } = takeSendMessageKey(session);
3744
+ const messageSubkeys = xMessageKeySubkeys(messageKey);
3675
3745
  const ratchetHeader = {
3676
3746
  dhPub: session.DHsPublic,
3677
3747
  n,
@@ -3679,7 +3749,7 @@ export class Client {
3679
3749
  version: 1,
3680
3750
  };
3681
3751
  const nonce = xMakeNonce();
3682
- const cipher = await xSecretboxAsync(msg, nonce, messageKey);
3752
+ const cipher = await xSecretboxAsync(msg, nonce, messageSubkeys.encryptionKey);
3683
3753
  const extra = encodeRatchetHeader(ratchetHeader);
3684
3754
  const mail = {
3685
3755
  authorID: this.getUser().userID,
@@ -3701,7 +3771,7 @@ export class Client {
3701
3771
  transmissionID: uuid.v4(),
3702
3772
  type: "resource",
3703
3773
  };
3704
- const hmac = xHMAC(mail, messageKey);
3774
+ const hmac = xHMAC(mail, messageSubkeys.authenticationKey);
3705
3775
  const fwdOut = forward
3706
3776
  ? messageSchema.parse(msgpack.decode(msg))
3707
3777
  : null;
@@ -3949,7 +4019,7 @@ export class Client {
3949
4019
  async submitOTK(amount) {
3950
4020
  const otks = [];
3951
4021
  for (let i = 0; i < amount; i++) {
3952
- otks.push(await this.createPreKey());
4022
+ otks.push(await this.createPreKey("one-time"));
3953
4023
  }
3954
4024
  const savedKeys = await this.database.savePreKeys(otks, true);
3955
4025
  await this.http.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {