@weblock-wallet/sdk 0.1.70 → 0.1.71

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/index.js CHANGED
@@ -104111,8 +104111,14 @@ var STORAGE_KEYS = {
104111
104111
  walletAddress: (orgHost) => `${orgHost}:walletAddress`,
104112
104112
  share2: (orgHost) => `${orgHost}:share2`,
104113
104113
  encryptedShare2: (orgHost) => `${orgHost}:encryptedShare2`,
104114
- encryptedShare2Device: (orgHost) => `${orgHost}:encryptedShare2_device`,
104115
- deviceSecret: (orgHost) => `${orgHost}:deviceSecret`,
104114
+ // User-scoped keys to prevent cross-account mixing on the same device.
104115
+ deviceId: (orgHost, firebaseId) => `${orgHost}:${firebaseId}:deviceId`,
104116
+ encryptedShare2Device: (orgHost, firebaseId) => `${orgHost}:${firebaseId}:encryptedShare2_device`,
104117
+ deviceSecret: (orgHost, firebaseId) => `${orgHost}:${firebaseId}:deviceSecret`,
104118
+ // Legacy keys (kept for migration / cleanup)
104119
+ encryptedShare2DeviceLegacy: (orgHost) => `${orgHost}:encryptedShare2_device`,
104120
+ deviceSecretLegacy: (orgHost) => `${orgHost}:deviceSecret`,
104121
+ deviceIdLegacy: (orgHost) => `${orgHost}:deviceId`,
104116
104122
  firebaseId: (orgHost) => `${orgHost}:firebaseId`,
104117
104123
  accessToken: (orgHost) => `${orgHost}:accessToken`,
104118
104124
  isNewUser: (orgHost) => `${orgHost}:isNewUser`
@@ -104144,28 +104150,76 @@ var WalletService = class {
104144
104150
  isSixDigitPin(pin) {
104145
104151
  return /^[0-9]{6}$/.test(pin);
104146
104152
  }
104153
+ normalizeAddr(v5) {
104154
+ const s5 = String(v5 ?? "").trim();
104155
+ return s5 ? s5.toLowerCase() : "";
104156
+ }
104157
+ addressesMismatch(a5, b4) {
104158
+ return !!a5 && !!b4 && a5.toLowerCase() !== b4.toLowerCase();
104159
+ }
104160
+ /**
104161
+ * deviceId: stable identifier for the current device/browser profile.
104162
+ * Used to store/fetch server-side device recovery backup.
104163
+ */
104164
+ async getOrCreateDeviceId(firebaseId) {
104165
+ const scopedKey = STORAGE_KEYS.deviceId(this.orgHost, firebaseId);
104166
+ const existing = await LocalForage.get(scopedKey);
104167
+ if (existing) return existing;
104168
+ const legacy = await LocalForage.get(
104169
+ STORAGE_KEYS.deviceIdLegacy(this.orgHost)
104170
+ );
104171
+ if (legacy) {
104172
+ await LocalForage.save(scopedKey, legacy);
104173
+ return legacy;
104174
+ }
104175
+ const id = randomBytes(16).toString("hex");
104176
+ await LocalForage.save(scopedKey, id);
104177
+ return id;
104178
+ }
104147
104179
  /**
104148
- * deviceSecret: local-only secret for device recovery material.
104149
- * This must never be sent to server.
104180
+ * deviceSecret: device recovery secret. In the original SDK this was local-only.
104181
+ * To enable PIN reset after local storage wipe, we back it up to the server
104182
+ * (server should encrypt-at-rest; transport is TLS).
104150
104183
  */
104151
- async getOrCreateDeviceSecret() {
104152
- const key = STORAGE_KEYS.deviceSecret(this.orgHost);
104153
- const existing = await LocalForage.get(key);
104184
+ async getOrCreateDeviceSecret(firebaseId) {
104185
+ const scopedKey = STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId);
104186
+ const existing = await LocalForage.get(scopedKey);
104154
104187
  if (existing) return existing;
104188
+ const legacy = await LocalForage.get(
104189
+ STORAGE_KEYS.deviceSecretLegacy(this.orgHost)
104190
+ );
104191
+ if (legacy) {
104192
+ await LocalForage.save(scopedKey, legacy);
104193
+ return legacy;
104194
+ }
104155
104195
  const secret = randomBytes(32).toString("hex");
104156
- await LocalForage.save(key, secret);
104196
+ await LocalForage.save(scopedKey, secret);
104157
104197
  return secret;
104158
104198
  }
104159
104199
  /**
104160
- * PATCH APPLIED:
104161
104200
  * Always overwrite encryptedShare2_device when we have a fresh share2.
104162
- * Keeping an old device share causes (share1 new + share2 old) => different derived wallet.
104201
+ * Also upsert to server so that PIN reset can work after local storage wipe.
104202
+ *
104203
+ * Note: Server backup is best-effort; we do not fail the main flow if backup fails.
104163
104204
  */
104164
104205
  async ensureDeviceEncryptedShare2(share2Plain, firebaseId) {
104165
- const encryptedKey = STORAGE_KEYS.encryptedShare2Device(this.orgHost);
104166
- const deviceSecret = await this.getOrCreateDeviceSecret();
104206
+ const deviceId = await this.getOrCreateDeviceId(firebaseId);
104207
+ const deviceSecret = await this.getOrCreateDeviceSecret(firebaseId);
104208
+ const encryptedKey = STORAGE_KEYS.encryptedShare2Device(
104209
+ this.orgHost,
104210
+ firebaseId
104211
+ );
104167
104212
  const encrypted = Crypto.encryptShare(share2Plain, deviceSecret, firebaseId);
104168
104213
  await LocalForage.save(encryptedKey, encrypted);
104214
+ try {
104215
+ await this.walletClient.upsertDeviceRecovery({
104216
+ deviceId,
104217
+ encryptedShare2Device: encrypted,
104218
+ deviceSecret
104219
+ });
104220
+ } catch (e7) {
104221
+ console.warn("[WalletService] device recovery backup upsert failed", e7);
104222
+ }
104169
104223
  }
104170
104224
  async getAddress() {
104171
104225
  try {
@@ -104254,69 +104308,106 @@ var WalletService = class {
104254
104308
  );
104255
104309
  }
104256
104310
  }
104257
- // client/src/core/services/wallet.ts
104258
- // Full version of retrieveWallet() with the address-mismatch guard added.
104259
- // Assumes existing imports/types in this file:
104260
- // - SDKError, SDKErrorCode
104261
- // - LocalForage
104262
- // - STORAGE_KEYS
104263
- // - Crypto (encryptShare/decryptShare)
104264
- // - Secrets (combine)
104265
- // - Wallet (ethers Wallet or equivalent)
104266
- // - this.walletClient.getWallet()
104267
- // - this.orgHost, this.walletAddress
104268
- // - this.isSixDigitPin(password)
104269
104311
  async retrieveWallet(password) {
104270
104312
  try {
104313
+ const accessToken = await LocalForage.get(
104314
+ STORAGE_KEYS.accessToken(this.orgHost)
104315
+ );
104316
+ if (!accessToken) {
104317
+ throw new SDKError("Access token not found", "AUTH_REQUIRED" /* AUTH_REQUIRED */);
104318
+ }
104271
104319
  const firebaseId = await LocalForage.get(
104272
104320
  STORAGE_KEYS.firebaseId(this.orgHost)
104273
104321
  );
104274
104322
  if (!firebaseId) {
104275
104323
  throw new SDKError("Not logged in", "AUTH_REQUIRED" /* AUTH_REQUIRED */);
104276
104324
  }
104277
- if (!password || !this.isSixDigitPin(password)) {
104325
+ if (!this.isSixDigitPin(password)) {
104278
104326
  throw new SDKError(
104279
104327
  "PIN must be a 6-digit number",
104280
104328
  "INVALID_PARAMS" /* INVALID_PARAMS */
104281
104329
  );
104282
104330
  }
104331
+ const decryptShareOrThrow = (encryptedShare) => {
104332
+ try {
104333
+ return Crypto.decryptShare(encryptedShare, password, firebaseId);
104334
+ } catch (e7) {
104335
+ if (this.isInvalidPasswordError(e7)) {
104336
+ throw new SDKError(
104337
+ "Incorrect PIN code",
104338
+ "INVALID_PASSWORD" /* INVALID_PASSWORD */,
104339
+ e7
104340
+ );
104341
+ }
104342
+ throw e7;
104343
+ }
104344
+ };
104283
104345
  const walletInfo = await this.walletClient.getWallet();
104284
- const share1 = walletInfo.share1;
104285
- const serverAddr = walletInfo.address?.toLowerCase?.() ?? "";
104286
- if (!share1) {
104287
- throw new SDKError(
104288
- "Wallet is not initialized on the server",
104289
- "WALLET_NOT_FOUND" /* WALLET_NOT_FOUND */
104290
- );
104291
- }
104292
- const encryptedShare2 = await LocalForage.get(
104293
- STORAGE_KEYS.encryptedShare2(this.orgHost)
104346
+ const serverAddr = this.normalizeAddr(walletInfo?.address);
104347
+ let share2 = await LocalForage.get(
104348
+ STORAGE_KEYS.share2(this.orgHost)
104294
104349
  );
104295
- if (!encryptedShare2) {
104296
- throw new SDKError(
104297
- "Local recovery material is missing on this device",
104298
- "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104299
- );
104300
- }
104301
- let share2;
104302
- try {
104303
- share2 = Crypto.decryptShare(encryptedShare2, password, firebaseId);
104304
- } catch (e7) {
104305
- throw new SDKError(
104306
- "Invalid PIN or corrupted local recovery material",
104307
- "INVALID_PIN" /* INVALID_PIN */,
104308
- e7
104350
+ if (!share2) {
104351
+ const encryptedShare2 = await LocalForage.get(
104352
+ STORAGE_KEYS.encryptedShare2(this.orgHost)
104309
104353
  );
104354
+ if (encryptedShare2) {
104355
+ share2 = decryptShareOrThrow(encryptedShare2);
104356
+ await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), share2);
104357
+ } else {
104358
+ const share3 = decryptShareOrThrow(
104359
+ walletInfo.encryptedShare3
104360
+ );
104361
+ const privateKey2 = await Secrets.combine([
104362
+ walletInfo.share1,
104363
+ share3
104364
+ ]);
104365
+ const wallet2 = new Wallet(privateKey2);
104366
+ const derivedAddr2 = this.normalizeAddr(wallet2.address);
104367
+ if (this.addressesMismatch(serverAddr, derivedAddr2)) {
104368
+ throw new SDKError(
104369
+ `Recovered wallet address mismatch. server=${serverAddr} derived=${derivedAddr2}`,
104370
+ "WALLET_RECOVERY_FAILED" /* WALLET_RECOVERY_FAILED */
104371
+ );
104372
+ }
104373
+ const newShares = await Secrets.split(wallet2.privateKey, 3, 2);
104374
+ const [newShare1, newShare2, newShare3] = newShares;
104375
+ await this.walletClient.updateWalletKey({
104376
+ share1: newShare1,
104377
+ encryptedShare3: Crypto.encryptShare(
104378
+ newShare3,
104379
+ password,
104380
+ firebaseId
104381
+ )
104382
+ });
104383
+ await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), newShare2);
104384
+ await LocalForage.save(
104385
+ STORAGE_KEYS.encryptedShare2(this.orgHost),
104386
+ Crypto.encryptShare(newShare2, password, firebaseId)
104387
+ );
104388
+ await this.ensureDeviceEncryptedShare2(newShare2, firebaseId);
104389
+ this.walletAddress = wallet2.address;
104390
+ await LocalForage.save(
104391
+ STORAGE_KEYS.walletAddress(this.orgHost),
104392
+ wallet2.address
104393
+ );
104394
+ await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104395
+ return wallet2.address;
104396
+ }
104310
104397
  }
104311
- const privateKey = await Secrets.combine([share1, share2]);
104398
+ const privateKey = await Secrets.combine([
104399
+ walletInfo.share1,
104400
+ share2
104401
+ ]);
104312
104402
  const wallet = new Wallet(privateKey);
104313
- const derivedAddr = wallet.address.toLowerCase();
104314
- if (serverAddr && derivedAddr !== serverAddr) {
104403
+ const derivedAddr = this.normalizeAddr(wallet.address);
104404
+ if (this.addressesMismatch(serverAddr, derivedAddr)) {
104315
104405
  throw new SDKError(
104316
104406
  `Recovered wallet address mismatch. server=${serverAddr} derived=${derivedAddr}`,
104317
104407
  "WALLET_RECOVERY_FAILED" /* WALLET_RECOVERY_FAILED */
104318
104408
  );
104319
104409
  }
104410
+ await this.ensureDeviceEncryptedShare2(share2, firebaseId);
104320
104411
  this.walletAddress = wallet.address;
104321
104412
  await LocalForage.save(
104322
104413
  STORAGE_KEYS.walletAddress(this.orgHost),
@@ -104325,6 +104416,8 @@ var WalletService = class {
104325
104416
  await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104326
104417
  return wallet.address;
104327
104418
  } catch (error) {
104419
+ this.walletAddress = null;
104420
+ await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104328
104421
  if (error instanceof SDKError) throw error;
104329
104422
  throw new SDKError(
104330
104423
  "Failed to retrieve wallet",
@@ -104334,7 +104427,8 @@ var WalletService = class {
104334
104427
  }
104335
104428
  }
104336
104429
  /**
104337
- * PIN reset (same private key/address) using encryptedShare2_device.
104430
+ * PIN reset (same private key/address) using device recovery material.
104431
+ * If local recovery material is missing, it attempts to restore it from server backup.
104338
104432
  */
104339
104433
  async resetPin(newPassword) {
104340
104434
  try {
@@ -104356,42 +104450,71 @@ var WalletService = class {
104356
104450
  "INVALID_PARAMS" /* INVALID_PARAMS */
104357
104451
  );
104358
104452
  }
104359
- const encryptedDevice = await LocalForage.get(
104360
- STORAGE_KEYS.encryptedShare2Device(this.orgHost)
104453
+ let encryptedDevice = await LocalForage.get(
104454
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId)
104361
104455
  );
104362
- const deviceSecret = await LocalForage.get(
104363
- STORAGE_KEYS.deviceSecret(this.orgHost)
104456
+ let deviceSecret = await LocalForage.get(
104457
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId)
104364
104458
  );
104365
104459
  if (!encryptedDevice || !deviceSecret) {
104366
- throw new SDKError(
104367
- "PIN reset is not available on this device",
104368
- "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104460
+ let backup;
104461
+ try {
104462
+ backup = await this.walletClient.getDeviceRecovery(void 0);
104463
+ } catch (e7) {
104464
+ throw new SDKError(
104465
+ "PIN reset is not available on this device",
104466
+ "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */,
104467
+ e7
104468
+ );
104469
+ }
104470
+ if (!backup?.found || !backup?.encryptedShare2Device || !backup?.deviceSecret) {
104471
+ throw new SDKError(
104472
+ "PIN reset is not available. No device recovery backup found for this user.",
104473
+ "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104474
+ );
104475
+ }
104476
+ encryptedDevice = backup.encryptedShare2Device;
104477
+ deviceSecret = backup.deviceSecret;
104478
+ await LocalForage.save(
104479
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId),
104480
+ encryptedDevice
104481
+ );
104482
+ await LocalForage.save(
104483
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId),
104484
+ deviceSecret
104369
104485
  );
104486
+ if (backup?.deviceId) {
104487
+ await LocalForage.save(
104488
+ STORAGE_KEYS.deviceId(this.orgHost, firebaseId),
104489
+ backup.deviceId
104490
+ );
104491
+ }
104370
104492
  }
104371
104493
  let share2;
104372
104494
  try {
104373
- share2 = Crypto.decryptShare(encryptedDevice, deviceSecret, firebaseId);
104495
+ share2 = Crypto.decryptShare(
104496
+ encryptedDevice,
104497
+ deviceSecret,
104498
+ firebaseId
104499
+ );
104374
104500
  } catch (e7) {
104375
104501
  throw new SDKError(
104376
- "PIN reset is not available on this device",
104502
+ "PIN reset is not available. Recovery material cannot be decrypted.",
104377
104503
  "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */,
104378
104504
  e7
104379
104505
  );
104380
104506
  }
104381
104507
  const walletInfo = await this.walletClient.getWallet();
104508
+ const serverAddr = this.normalizeAddr(walletInfo?.address);
104382
104509
  const privateKey = await Secrets.combine([
104383
104510
  walletInfo.share1,
104384
104511
  share2
104385
104512
  ]);
104386
104513
  const wallet = new Wallet(privateKey);
104387
- const serverAddr = walletInfo.address?.toLowerCase?.() ?? "";
104388
- const derivedAddr = wallet.address.toLowerCase();
104389
- const cachedAddr = await LocalForage.get(
104390
- STORAGE_KEYS.walletAddress(this.orgHost)
104391
- ) ?? null;
104392
- if (serverAddr && derivedAddr !== serverAddr) {
104514
+ const derivedAddr = this.normalizeAddr(wallet.address);
104515
+ if (this.addressesMismatch(serverAddr, derivedAddr)) {
104393
104516
  throw new SDKError(
104394
- `Device recovery material does not match server wallet. server=${serverAddr} derived=${derivedAddr} cached=${cachedAddr ?? "null"}`,
104517
+ `Device recovery does not match server wallet. server=${serverAddr} derived=${derivedAddr}`,
104395
104518
  "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104396
104519
  );
104397
104520
  }
@@ -104431,8 +104554,23 @@ var WalletService = class {
104431
104554
  await LocalForage.delete(STORAGE_KEYS.walletAddress(this.orgHost));
104432
104555
  await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104433
104556
  await LocalForage.delete(STORAGE_KEYS.encryptedShare2(this.orgHost));
104434
- await LocalForage.delete(STORAGE_KEYS.encryptedShare2Device(this.orgHost));
104435
- await LocalForage.delete(STORAGE_KEYS.deviceSecret(this.orgHost));
104557
+ const firebaseId = await LocalForage.get(
104558
+ STORAGE_KEYS.firebaseId(this.orgHost)
104559
+ );
104560
+ if (firebaseId) {
104561
+ await LocalForage.delete(
104562
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId)
104563
+ );
104564
+ await LocalForage.delete(
104565
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId)
104566
+ );
104567
+ await LocalForage.delete(STORAGE_KEYS.deviceId(this.orgHost, firebaseId));
104568
+ }
104569
+ await LocalForage.delete(
104570
+ STORAGE_KEYS.encryptedShare2DeviceLegacy(this.orgHost)
104571
+ );
104572
+ await LocalForage.delete(STORAGE_KEYS.deviceSecretLegacy(this.orgHost));
104573
+ await LocalForage.delete(STORAGE_KEYS.deviceIdLegacy(this.orgHost));
104436
104574
  }
104437
104575
  async getBalance(address, chainId) {
104438
104576
  const response = await this.rpcClient.sendRpc({
@@ -104569,7 +104707,7 @@ var WalletService = class {
104569
104707
  }
104570
104708
  }
104571
104709
  /**
104572
- * PATCH APPLIED (critical):
104710
+ * Critical safety:
104573
104711
  * - Never overwrite cached walletAddress with derived signer address.
104574
104712
  * - If server/cached address != derived address => throw WALLET_RECOVERY_FAILED.
104575
104713
  */
@@ -104597,19 +104735,21 @@ var WalletService = class {
104597
104735
  const firebaseId = await LocalForage.get(
104598
104736
  STORAGE_KEYS.firebaseId(this.orgHost)
104599
104737
  );
104600
- const encryptedDevice = await LocalForage.get(
104601
- STORAGE_KEYS.encryptedShare2Device(this.orgHost)
104602
- );
104603
- const deviceSecret = await LocalForage.get(
104604
- STORAGE_KEYS.deviceSecret(this.orgHost)
104605
- );
104606
- if (firebaseId && encryptedDevice && deviceSecret) {
104607
- share2 = Crypto.decryptShare(
104608
- encryptedDevice,
104609
- deviceSecret,
104610
- firebaseId
104738
+ if (firebaseId) {
104739
+ const encryptedDevice = await LocalForage.get(
104740
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId)
104611
104741
  );
104612
- await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), share2);
104742
+ const deviceSecret = await LocalForage.get(
104743
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId)
104744
+ );
104745
+ if (encryptedDevice && deviceSecret) {
104746
+ share2 = Crypto.decryptShare(
104747
+ encryptedDevice,
104748
+ deviceSecret,
104749
+ firebaseId
104750
+ );
104751
+ await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), share2);
104752
+ }
104613
104753
  }
104614
104754
  } catch {
104615
104755
  }
@@ -105202,6 +105342,38 @@ var WalletClient = class {
105202
105342
  needsAccessToken: true
105203
105343
  });
105204
105344
  }
105345
+ async getDeviceShare2Backup() {
105346
+ try {
105347
+ return await this.client.get(
105348
+ "/api/v1/wallet/device-share2",
105349
+ { needsAccessToken: true }
105350
+ );
105351
+ } catch (e7) {
105352
+ if (e7?.details?.status === 404 || String(e7?.message || "").includes("404"))
105353
+ return null;
105354
+ throw e7;
105355
+ }
105356
+ }
105357
+ async upsertDeviceShare2Backup(body) {
105358
+ await this.client.put("/api/v1/wallet/device-share2", body, {
105359
+ needsAccessToken: true
105360
+ });
105361
+ }
105362
+ async upsertDeviceRecovery(req) {
105363
+ await this.client.put(`${this.baseUrl}/device-recovery`, req, {
105364
+ needsAccessToken: true
105365
+ });
105366
+ }
105367
+ /**
105368
+ * If deviceId is omitted, server returns latest.
105369
+ * Returns { found:false } if no backup exists.
105370
+ */
105371
+ async getDeviceRecovery(deviceId) {
105372
+ const qs = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
105373
+ return this.client.get(`${this.baseUrl}/device-recovery${qs}`, {
105374
+ needsAccessToken: true
105375
+ });
105376
+ }
105205
105377
  };
105206
105378
 
105207
105379
  // src/clients/api/rpcs.ts