@vex-chat/libvex 8.0.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,8 +1250,8 @@ 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 - Password for new accounts. Existing-account device approval requests may omit it.
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
@@ -1212,113 +1260,35 @@ export class Client {
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
- : 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.")];
1263
+ const passwordError = registrationPasswordError(password);
1264
+ if (passwordError) {
1265
+ return [null, passwordError];
1316
1266
  }
1267
+ return this.registerAccountOrEnrollment(username, "create-account", password);
1317
1268
  }
1318
1269
  removeAllListeners(event) {
1319
1270
  this.emitter.removeAllListeners(event);
1320
1271
  return this;
1321
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
+ }
1322
1292
  /**
1323
1293
  * Updates the local retention cap (1–30 days) and prunes immediately.
1324
1294
  * Does not affect server-side storage.
@@ -1456,6 +1426,16 @@ export class Client {
1456
1426
  signature: XUtils.decodeHex(preKey.signature),
1457
1427
  };
1458
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
+ }
1459
1439
  async createChannel(name, serverID) {
1460
1440
  const body = { name };
1461
1441
  const res = await this.http.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
@@ -1523,12 +1503,10 @@ export class Client {
1523
1503
  const res = await this.http.post(this.getHost() + "/server/" + serverID + "/invites", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
1524
1504
  return decodeHttpResponse(InviteCodec, res.data);
1525
1505
  }
1526
- async createPreKey() {
1506
+ async createPreKey(kind) {
1527
1507
  return this.runWithThisCryptoProfile(async () => {
1528
1508
  const preKeyPair = await xBoxKeyPairAsync();
1529
- const toSign = this.cryptoProfile === "fips"
1530
- ? fipsP256PreKeySignPayload(preKeyPair.publicKey)
1531
- : xEncode(xConstants.CURVE, preKeyPair.publicKey);
1509
+ const toSign = xPreKeySignaturePayload(preKeyPair.publicKey, kind, this.cryptoProfile);
1532
1510
  return {
1533
1511
  keyPair: preKeyPair,
1534
1512
  signature: await xSignAsync(toSign, this.signKeys.secretKey),
@@ -1604,12 +1582,13 @@ export class Client {
1604
1582
  : XUtils.numberToUint8Arr(0);
1605
1583
  // shared secret key
1606
1584
  const SK = xKDF(IKM);
1585
+ const initialSubkeys = xMessageKeySubkeys(SK);
1607
1586
  const PK = (await xBoxKeyPairFromSecretAsync(SK)).publicKey;
1608
1587
  const AD = fips
1609
1588
  ? fipsP256AdFromIdentityPubs(IK_AP, new Uint8Array(keyBundle.signKey))
1610
1589
  : xConcat(xEncode(xConstants.CURVE, IK_AP), xEncode(xConstants.CURVE, IK_B));
1611
1590
  const nonce = xMakeNonce();
1612
- const cipher = await xSecretboxAsync(message, nonce, SK);
1591
+ const cipher = await xSecretboxAsync(message, nonce, initialSubkeys.encryptionKey);
1613
1592
  const signKeyWire = fips ? IK_AP : this.signKeys.publicKey;
1614
1593
  const ephKeyWire = ephemeralKeys.publicKey;
1615
1594
  const extra = fips
@@ -1628,7 +1607,7 @@ export class Client {
1628
1607
  recipient: device.deviceID,
1629
1608
  sender: this.getDevice().deviceID,
1630
1609
  };
1631
- const hmac = xHMAC(mail, SK);
1610
+ const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
1632
1611
  const msg = {
1633
1612
  action: "CREATE",
1634
1613
  data: mail,
@@ -2122,6 +2101,9 @@ export class Client {
2122
2101
  "/device/" +
2123
2102
  this.getDevice().deviceID +
2124
2103
  "/mail");
2104
+ if (!this.token || this.isManualCloseInFlight()) {
2105
+ return;
2106
+ }
2125
2107
  const mailBuffer = new Uint8Array(res.data);
2126
2108
  const rawInbox = z
2127
2109
  .array(mailInboxEntry)
@@ -2453,9 +2435,7 @@ export class Client {
2453
2435
  }
2454
2436
  async isPreKeySignedByCurrentDevice(preKey) {
2455
2437
  return this.runWithThisCryptoProfile(async () => {
2456
- const payload = this.cryptoProfile === "fips"
2457
- ? fipsP256PreKeySignPayload(preKey.keyPair.publicKey)
2458
- : xEncode(xConstants.CURVE, preKey.keyPair.publicKey);
2438
+ const payload = xPreKeySignaturePayload(preKey.keyPair.publicKey, "signed", this.cryptoProfile);
2459
2439
  const opened = await xSignOpenAsync(preKey.signature, this.signKeys.publicKey);
2460
2440
  return Boolean(opened && XUtils.bytesEqual(opened, payload));
2461
2441
  });
@@ -2627,7 +2607,7 @@ export class Client {
2627
2607
  let preKeys = await this.database.getPreKeys();
2628
2608
  if (!preKeys ||
2629
2609
  !(await this.isPreKeySignedByCurrentDevice(preKeys))) {
2630
- const unsaved = await this.createPreKey();
2610
+ const unsaved = await this.createPreKey("signed");
2631
2611
  const [saved] = await this.database.savePreKeys([unsaved], false);
2632
2612
  if (!saved || saved.index == null)
2633
2613
  throw new Error("Failed to save prekey — no index returned.");
@@ -2897,9 +2877,10 @@ export class Client {
2897
2877
  : xConcat(DH1, DH2, DH3);
2898
2878
  // shared secret key
2899
2879
  const SK = xKDF(IKM);
2880
+ const initialSubkeys = xMessageKeySubkeys(SK);
2900
2881
  const PK = (await xBoxKeyPairFromSecretAsync(SK))
2901
2882
  .publicKey;
2902
- const hmac = xHMAC(mail, SK);
2883
+ const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
2903
2884
  // associated data
2904
2885
  const AD = fipsRead
2905
2886
  ? fipsP256AdFromIdentityPubs(IK_A, IK_BP)
@@ -2930,7 +2911,7 @@ export class Client {
2930
2911
  this.acknowledgeRepeatedDecryptFailure(mail, failureCount, timestamp);
2931
2912
  return;
2932
2913
  }
2933
- 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);
2934
2915
  if (unsealed) {
2935
2916
  let plaintext = "";
2936
2917
  if (!mail.forward) {
@@ -3084,15 +3065,17 @@ export class Client {
3084
3065
  else if (hasRemoteDhChanged(candidateSession.DHr, ratchetHeader.dhPub)) {
3085
3066
  await ratchetStepReceive(candidateSession, ratchetHeader.dhPub, ratchetHeader.pn);
3086
3067
  }
3087
- let messageKey = takeReceiveMessageKey(candidateSession, ratchetHeader.dhPub, ratchetHeader.n);
3088
- 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);
3089
3071
  if (!XUtils.bytesEqual(HMAC, header) &&
3090
3072
  firstInboundFromSubsequent &&
3091
3073
  !originalSession.CKr) {
3092
3074
  const ratchetedCandidate = cloneSessionCrypto(originalSession);
3093
3075
  await ratchetStepReceive(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.pn);
3094
3076
  const ratchetedMessageKey = takeReceiveMessageKey(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.n);
3095
- const ratchetedHMAC = xHMAC(mail, ratchetedMessageKey);
3077
+ const ratchetedSubkeys = xMessageKeySubkeys(ratchetedMessageKey);
3078
+ const ratchetedHMAC = xHMAC(mail, ratchetedSubkeys.authenticationKey);
3096
3079
  if (XUtils.bytesEqual(ratchetedHMAC, header)) {
3097
3080
  if (libvexDebugDmEnabled()) {
3098
3081
  debugLibvexDm("readMail subsequent: first inbound used DH-ratchet fallback", {
@@ -3101,7 +3084,7 @@ export class Client {
3101
3084
  });
3102
3085
  }
3103
3086
  candidateSession = ratchetedCandidate;
3104
- messageKey = ratchetedMessageKey;
3087
+ messageSubkeys = ratchetedSubkeys;
3105
3088
  HMAC = ratchetedHMAC;
3106
3089
  }
3107
3090
  }
@@ -3128,7 +3111,7 @@ export class Client {
3128
3111
  return;
3129
3112
  }
3130
3113
  session = candidateSession;
3131
- 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);
3132
3115
  if (decrypted) {
3133
3116
  const fwdMsg2 = mail.forward
3134
3117
  ? messageSchema.parse(msgpack.decode(decrypted))
@@ -3251,6 +3234,75 @@ export class Client {
3251
3234
  const res = await this.http.patch(this.getHost() + "/invite/" + inviteID);
3252
3235
  return decodeHttpResponse(PermissionCodec, res.data);
3253
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
+ }
3254
3306
  registerDecryptFailure(mail) {
3255
3307
  const count = (this.decryptFailureCounts.get(mail.mailID) ?? 0) + 1;
3256
3308
  this.decryptFailureCounts.set(mail.mailID, count);
@@ -3299,6 +3351,15 @@ export class Client {
3299
3351
  requestID +
3300
3352
  "/reject");
3301
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
+ }
3302
3363
  async respond(msg) {
3303
3364
  const response = {
3304
3365
  signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
@@ -3378,7 +3439,7 @@ export class Client {
3378
3439
  challenge: newDevice.challenge,
3379
3440
  expiresAt: newDevice.expiresAt,
3380
3441
  requestID: newDevice.requestID,
3381
- userID: newDevice.userID ?? null,
3442
+ userID: newDevice.userID,
3382
3443
  });
3383
3444
  }
3384
3445
  else {
@@ -3680,6 +3741,7 @@ export class Client {
3680
3741
  await ratchetStepSend(session);
3681
3742
  }
3682
3743
  const { messageKey, n } = takeSendMessageKey(session);
3744
+ const messageSubkeys = xMessageKeySubkeys(messageKey);
3683
3745
  const ratchetHeader = {
3684
3746
  dhPub: session.DHsPublic,
3685
3747
  n,
@@ -3687,7 +3749,7 @@ export class Client {
3687
3749
  version: 1,
3688
3750
  };
3689
3751
  const nonce = xMakeNonce();
3690
- const cipher = await xSecretboxAsync(msg, nonce, messageKey);
3752
+ const cipher = await xSecretboxAsync(msg, nonce, messageSubkeys.encryptionKey);
3691
3753
  const extra = encodeRatchetHeader(ratchetHeader);
3692
3754
  const mail = {
3693
3755
  authorID: this.getUser().userID,
@@ -3709,7 +3771,7 @@ export class Client {
3709
3771
  transmissionID: uuid.v4(),
3710
3772
  type: "resource",
3711
3773
  };
3712
- const hmac = xHMAC(mail, messageKey);
3774
+ const hmac = xHMAC(mail, messageSubkeys.authenticationKey);
3713
3775
  const fwdOut = forward
3714
3776
  ? messageSchema.parse(msgpack.decode(msg))
3715
3777
  : null;
@@ -3957,7 +4019,7 @@ export class Client {
3957
4019
  async submitOTK(amount) {
3958
4020
  const otks = [];
3959
4021
  for (let i = 0; i < amount; i++) {
3960
- otks.push(await this.createPreKey());
4022
+ otks.push(await this.createPreKey("one-time"));
3961
4023
  }
3962
4024
  const savedKeys = await this.database.savePreKeys(otks, true);
3963
4025
  await this.http.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {