@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.cjs CHANGED
@@ -104156,8 +104156,14 @@ var STORAGE_KEYS = {
104156
104156
  walletAddress: (orgHost) => `${orgHost}:walletAddress`,
104157
104157
  share2: (orgHost) => `${orgHost}:share2`,
104158
104158
  encryptedShare2: (orgHost) => `${orgHost}:encryptedShare2`,
104159
- encryptedShare2Device: (orgHost) => `${orgHost}:encryptedShare2_device`,
104160
- deviceSecret: (orgHost) => `${orgHost}:deviceSecret`,
104159
+ // User-scoped keys to prevent cross-account mixing on the same device.
104160
+ deviceId: (orgHost, firebaseId) => `${orgHost}:${firebaseId}:deviceId`,
104161
+ encryptedShare2Device: (orgHost, firebaseId) => `${orgHost}:${firebaseId}:encryptedShare2_device`,
104162
+ deviceSecret: (orgHost, firebaseId) => `${orgHost}:${firebaseId}:deviceSecret`,
104163
+ // Legacy keys (kept for migration / cleanup)
104164
+ encryptedShare2DeviceLegacy: (orgHost) => `${orgHost}:encryptedShare2_device`,
104165
+ deviceSecretLegacy: (orgHost) => `${orgHost}:deviceSecret`,
104166
+ deviceIdLegacy: (orgHost) => `${orgHost}:deviceId`,
104161
104167
  firebaseId: (orgHost) => `${orgHost}:firebaseId`,
104162
104168
  accessToken: (orgHost) => `${orgHost}:accessToken`,
104163
104169
  isNewUser: (orgHost) => `${orgHost}:isNewUser`
@@ -104189,28 +104195,76 @@ var WalletService = class {
104189
104195
  isSixDigitPin(pin) {
104190
104196
  return /^[0-9]{6}$/.test(pin);
104191
104197
  }
104198
+ normalizeAddr(v5) {
104199
+ const s5 = String(v5 ?? "").trim();
104200
+ return s5 ? s5.toLowerCase() : "";
104201
+ }
104202
+ addressesMismatch(a5, b4) {
104203
+ return !!a5 && !!b4 && a5.toLowerCase() !== b4.toLowerCase();
104204
+ }
104205
+ /**
104206
+ * deviceId: stable identifier for the current device/browser profile.
104207
+ * Used to store/fetch server-side device recovery backup.
104208
+ */
104209
+ async getOrCreateDeviceId(firebaseId) {
104210
+ const scopedKey = STORAGE_KEYS.deviceId(this.orgHost, firebaseId);
104211
+ const existing = await LocalForage.get(scopedKey);
104212
+ if (existing) return existing;
104213
+ const legacy = await LocalForage.get(
104214
+ STORAGE_KEYS.deviceIdLegacy(this.orgHost)
104215
+ );
104216
+ if (legacy) {
104217
+ await LocalForage.save(scopedKey, legacy);
104218
+ return legacy;
104219
+ }
104220
+ const id = randomBytes(16).toString("hex");
104221
+ await LocalForage.save(scopedKey, id);
104222
+ return id;
104223
+ }
104192
104224
  /**
104193
- * deviceSecret: local-only secret for device recovery material.
104194
- * This must never be sent to server.
104225
+ * deviceSecret: device recovery secret. In the original SDK this was local-only.
104226
+ * To enable PIN reset after local storage wipe, we back it up to the server
104227
+ * (server should encrypt-at-rest; transport is TLS).
104195
104228
  */
104196
- async getOrCreateDeviceSecret() {
104197
- const key = STORAGE_KEYS.deviceSecret(this.orgHost);
104198
- const existing = await LocalForage.get(key);
104229
+ async getOrCreateDeviceSecret(firebaseId) {
104230
+ const scopedKey = STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId);
104231
+ const existing = await LocalForage.get(scopedKey);
104199
104232
  if (existing) return existing;
104233
+ const legacy = await LocalForage.get(
104234
+ STORAGE_KEYS.deviceSecretLegacy(this.orgHost)
104235
+ );
104236
+ if (legacy) {
104237
+ await LocalForage.save(scopedKey, legacy);
104238
+ return legacy;
104239
+ }
104200
104240
  const secret = randomBytes(32).toString("hex");
104201
- await LocalForage.save(key, secret);
104241
+ await LocalForage.save(scopedKey, secret);
104202
104242
  return secret;
104203
104243
  }
104204
104244
  /**
104205
- * PATCH APPLIED:
104206
104245
  * Always overwrite encryptedShare2_device when we have a fresh share2.
104207
- * Keeping an old device share causes (share1 new + share2 old) => different derived wallet.
104246
+ * Also upsert to server so that PIN reset can work after local storage wipe.
104247
+ *
104248
+ * Note: Server backup is best-effort; we do not fail the main flow if backup fails.
104208
104249
  */
104209
104250
  async ensureDeviceEncryptedShare2(share2Plain, firebaseId) {
104210
- const encryptedKey = STORAGE_KEYS.encryptedShare2Device(this.orgHost);
104211
- const deviceSecret = await this.getOrCreateDeviceSecret();
104251
+ const deviceId = await this.getOrCreateDeviceId(firebaseId);
104252
+ const deviceSecret = await this.getOrCreateDeviceSecret(firebaseId);
104253
+ const encryptedKey = STORAGE_KEYS.encryptedShare2Device(
104254
+ this.orgHost,
104255
+ firebaseId
104256
+ );
104212
104257
  const encrypted = Crypto.encryptShare(share2Plain, deviceSecret, firebaseId);
104213
104258
  await LocalForage.save(encryptedKey, encrypted);
104259
+ try {
104260
+ await this.walletClient.upsertDeviceRecovery({
104261
+ deviceId,
104262
+ encryptedShare2Device: encrypted,
104263
+ deviceSecret
104264
+ });
104265
+ } catch (e7) {
104266
+ console.warn("[WalletService] device recovery backup upsert failed", e7);
104267
+ }
104214
104268
  }
104215
104269
  async getAddress() {
104216
104270
  try {
@@ -104299,69 +104353,106 @@ var WalletService = class {
104299
104353
  );
104300
104354
  }
104301
104355
  }
104302
- // client/src/core/services/wallet.ts
104303
- // Full version of retrieveWallet() with the address-mismatch guard added.
104304
- // Assumes existing imports/types in this file:
104305
- // - SDKError, SDKErrorCode
104306
- // - LocalForage
104307
- // - STORAGE_KEYS
104308
- // - Crypto (encryptShare/decryptShare)
104309
- // - Secrets (combine)
104310
- // - Wallet (ethers Wallet or equivalent)
104311
- // - this.walletClient.getWallet()
104312
- // - this.orgHost, this.walletAddress
104313
- // - this.isSixDigitPin(password)
104314
104356
  async retrieveWallet(password) {
104315
104357
  try {
104358
+ const accessToken = await LocalForage.get(
104359
+ STORAGE_KEYS.accessToken(this.orgHost)
104360
+ );
104361
+ if (!accessToken) {
104362
+ throw new SDKError("Access token not found", "AUTH_REQUIRED" /* AUTH_REQUIRED */);
104363
+ }
104316
104364
  const firebaseId = await LocalForage.get(
104317
104365
  STORAGE_KEYS.firebaseId(this.orgHost)
104318
104366
  );
104319
104367
  if (!firebaseId) {
104320
104368
  throw new SDKError("Not logged in", "AUTH_REQUIRED" /* AUTH_REQUIRED */);
104321
104369
  }
104322
- if (!password || !this.isSixDigitPin(password)) {
104370
+ if (!this.isSixDigitPin(password)) {
104323
104371
  throw new SDKError(
104324
104372
  "PIN must be a 6-digit number",
104325
104373
  "INVALID_PARAMS" /* INVALID_PARAMS */
104326
104374
  );
104327
104375
  }
104376
+ const decryptShareOrThrow = (encryptedShare) => {
104377
+ try {
104378
+ return Crypto.decryptShare(encryptedShare, password, firebaseId);
104379
+ } catch (e7) {
104380
+ if (this.isInvalidPasswordError(e7)) {
104381
+ throw new SDKError(
104382
+ "Incorrect PIN code",
104383
+ "INVALID_PASSWORD" /* INVALID_PASSWORD */,
104384
+ e7
104385
+ );
104386
+ }
104387
+ throw e7;
104388
+ }
104389
+ };
104328
104390
  const walletInfo = await this.walletClient.getWallet();
104329
- const share1 = walletInfo.share1;
104330
- const serverAddr = walletInfo.address?.toLowerCase?.() ?? "";
104331
- if (!share1) {
104332
- throw new SDKError(
104333
- "Wallet is not initialized on the server",
104334
- "WALLET_NOT_FOUND" /* WALLET_NOT_FOUND */
104335
- );
104336
- }
104337
- const encryptedShare2 = await LocalForage.get(
104338
- STORAGE_KEYS.encryptedShare2(this.orgHost)
104391
+ const serverAddr = this.normalizeAddr(walletInfo?.address);
104392
+ let share2 = await LocalForage.get(
104393
+ STORAGE_KEYS.share2(this.orgHost)
104339
104394
  );
104340
- if (!encryptedShare2) {
104341
- throw new SDKError(
104342
- "Local recovery material is missing on this device",
104343
- "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104344
- );
104345
- }
104346
- let share2;
104347
- try {
104348
- share2 = Crypto.decryptShare(encryptedShare2, password, firebaseId);
104349
- } catch (e7) {
104350
- throw new SDKError(
104351
- "Invalid PIN or corrupted local recovery material",
104352
- "INVALID_PIN" /* INVALID_PIN */,
104353
- e7
104395
+ if (!share2) {
104396
+ const encryptedShare2 = await LocalForage.get(
104397
+ STORAGE_KEYS.encryptedShare2(this.orgHost)
104354
104398
  );
104399
+ if (encryptedShare2) {
104400
+ share2 = decryptShareOrThrow(encryptedShare2);
104401
+ await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), share2);
104402
+ } else {
104403
+ const share3 = decryptShareOrThrow(
104404
+ walletInfo.encryptedShare3
104405
+ );
104406
+ const privateKey2 = await Secrets.combine([
104407
+ walletInfo.share1,
104408
+ share3
104409
+ ]);
104410
+ const wallet2 = new import_ethers2.Wallet(privateKey2);
104411
+ const derivedAddr2 = this.normalizeAddr(wallet2.address);
104412
+ if (this.addressesMismatch(serverAddr, derivedAddr2)) {
104413
+ throw new SDKError(
104414
+ `Recovered wallet address mismatch. server=${serverAddr} derived=${derivedAddr2}`,
104415
+ "WALLET_RECOVERY_FAILED" /* WALLET_RECOVERY_FAILED */
104416
+ );
104417
+ }
104418
+ const newShares = await Secrets.split(wallet2.privateKey, 3, 2);
104419
+ const [newShare1, newShare2, newShare3] = newShares;
104420
+ await this.walletClient.updateWalletKey({
104421
+ share1: newShare1,
104422
+ encryptedShare3: Crypto.encryptShare(
104423
+ newShare3,
104424
+ password,
104425
+ firebaseId
104426
+ )
104427
+ });
104428
+ await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), newShare2);
104429
+ await LocalForage.save(
104430
+ STORAGE_KEYS.encryptedShare2(this.orgHost),
104431
+ Crypto.encryptShare(newShare2, password, firebaseId)
104432
+ );
104433
+ await this.ensureDeviceEncryptedShare2(newShare2, firebaseId);
104434
+ this.walletAddress = wallet2.address;
104435
+ await LocalForage.save(
104436
+ STORAGE_KEYS.walletAddress(this.orgHost),
104437
+ wallet2.address
104438
+ );
104439
+ await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104440
+ return wallet2.address;
104441
+ }
104355
104442
  }
104356
- const privateKey = await Secrets.combine([share1, share2]);
104443
+ const privateKey = await Secrets.combine([
104444
+ walletInfo.share1,
104445
+ share2
104446
+ ]);
104357
104447
  const wallet = new import_ethers2.Wallet(privateKey);
104358
- const derivedAddr = wallet.address.toLowerCase();
104359
- if (serverAddr && derivedAddr !== serverAddr) {
104448
+ const derivedAddr = this.normalizeAddr(wallet.address);
104449
+ if (this.addressesMismatch(serverAddr, derivedAddr)) {
104360
104450
  throw new SDKError(
104361
104451
  `Recovered wallet address mismatch. server=${serverAddr} derived=${derivedAddr}`,
104362
104452
  "WALLET_RECOVERY_FAILED" /* WALLET_RECOVERY_FAILED */
104363
104453
  );
104364
104454
  }
104455
+ await this.ensureDeviceEncryptedShare2(share2, firebaseId);
104365
104456
  this.walletAddress = wallet.address;
104366
104457
  await LocalForage.save(
104367
104458
  STORAGE_KEYS.walletAddress(this.orgHost),
@@ -104370,6 +104461,8 @@ var WalletService = class {
104370
104461
  await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104371
104462
  return wallet.address;
104372
104463
  } catch (error) {
104464
+ this.walletAddress = null;
104465
+ await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104373
104466
  if (error instanceof SDKError) throw error;
104374
104467
  throw new SDKError(
104375
104468
  "Failed to retrieve wallet",
@@ -104379,7 +104472,8 @@ var WalletService = class {
104379
104472
  }
104380
104473
  }
104381
104474
  /**
104382
- * PIN reset (same private key/address) using encryptedShare2_device.
104475
+ * PIN reset (same private key/address) using device recovery material.
104476
+ * If local recovery material is missing, it attempts to restore it from server backup.
104383
104477
  */
104384
104478
  async resetPin(newPassword) {
104385
104479
  try {
@@ -104401,42 +104495,71 @@ var WalletService = class {
104401
104495
  "INVALID_PARAMS" /* INVALID_PARAMS */
104402
104496
  );
104403
104497
  }
104404
- const encryptedDevice = await LocalForage.get(
104405
- STORAGE_KEYS.encryptedShare2Device(this.orgHost)
104498
+ let encryptedDevice = await LocalForage.get(
104499
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId)
104406
104500
  );
104407
- const deviceSecret = await LocalForage.get(
104408
- STORAGE_KEYS.deviceSecret(this.orgHost)
104501
+ let deviceSecret = await LocalForage.get(
104502
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId)
104409
104503
  );
104410
104504
  if (!encryptedDevice || !deviceSecret) {
104411
- throw new SDKError(
104412
- "PIN reset is not available on this device",
104413
- "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104505
+ let backup;
104506
+ try {
104507
+ backup = await this.walletClient.getDeviceRecovery(void 0);
104508
+ } catch (e7) {
104509
+ throw new SDKError(
104510
+ "PIN reset is not available on this device",
104511
+ "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */,
104512
+ e7
104513
+ );
104514
+ }
104515
+ if (!backup?.found || !backup?.encryptedShare2Device || !backup?.deviceSecret) {
104516
+ throw new SDKError(
104517
+ "PIN reset is not available. No device recovery backup found for this user.",
104518
+ "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104519
+ );
104520
+ }
104521
+ encryptedDevice = backup.encryptedShare2Device;
104522
+ deviceSecret = backup.deviceSecret;
104523
+ await LocalForage.save(
104524
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId),
104525
+ encryptedDevice
104526
+ );
104527
+ await LocalForage.save(
104528
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId),
104529
+ deviceSecret
104414
104530
  );
104531
+ if (backup?.deviceId) {
104532
+ await LocalForage.save(
104533
+ STORAGE_KEYS.deviceId(this.orgHost, firebaseId),
104534
+ backup.deviceId
104535
+ );
104536
+ }
104415
104537
  }
104416
104538
  let share2;
104417
104539
  try {
104418
- share2 = Crypto.decryptShare(encryptedDevice, deviceSecret, firebaseId);
104540
+ share2 = Crypto.decryptShare(
104541
+ encryptedDevice,
104542
+ deviceSecret,
104543
+ firebaseId
104544
+ );
104419
104545
  } catch (e7) {
104420
104546
  throw new SDKError(
104421
- "PIN reset is not available on this device",
104547
+ "PIN reset is not available. Recovery material cannot be decrypted.",
104422
104548
  "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */,
104423
104549
  e7
104424
104550
  );
104425
104551
  }
104426
104552
  const walletInfo = await this.walletClient.getWallet();
104553
+ const serverAddr = this.normalizeAddr(walletInfo?.address);
104427
104554
  const privateKey = await Secrets.combine([
104428
104555
  walletInfo.share1,
104429
104556
  share2
104430
104557
  ]);
104431
104558
  const wallet = new import_ethers2.Wallet(privateKey);
104432
- const serverAddr = walletInfo.address?.toLowerCase?.() ?? "";
104433
- const derivedAddr = wallet.address.toLowerCase();
104434
- const cachedAddr = await LocalForage.get(
104435
- STORAGE_KEYS.walletAddress(this.orgHost)
104436
- ) ?? null;
104437
- if (serverAddr && derivedAddr !== serverAddr) {
104559
+ const derivedAddr = this.normalizeAddr(wallet.address);
104560
+ if (this.addressesMismatch(serverAddr, derivedAddr)) {
104438
104561
  throw new SDKError(
104439
- `Device recovery material does not match server wallet. server=${serverAddr} derived=${derivedAddr} cached=${cachedAddr ?? "null"}`,
104562
+ `Device recovery does not match server wallet. server=${serverAddr} derived=${derivedAddr}`,
104440
104563
  "RECOVERY_NOT_AVAILABLE" /* RECOVERY_NOT_AVAILABLE */
104441
104564
  );
104442
104565
  }
@@ -104476,8 +104599,23 @@ var WalletService = class {
104476
104599
  await LocalForage.delete(STORAGE_KEYS.walletAddress(this.orgHost));
104477
104600
  await LocalForage.delete(STORAGE_KEYS.share2(this.orgHost));
104478
104601
  await LocalForage.delete(STORAGE_KEYS.encryptedShare2(this.orgHost));
104479
- await LocalForage.delete(STORAGE_KEYS.encryptedShare2Device(this.orgHost));
104480
- await LocalForage.delete(STORAGE_KEYS.deviceSecret(this.orgHost));
104602
+ const firebaseId = await LocalForage.get(
104603
+ STORAGE_KEYS.firebaseId(this.orgHost)
104604
+ );
104605
+ if (firebaseId) {
104606
+ await LocalForage.delete(
104607
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId)
104608
+ );
104609
+ await LocalForage.delete(
104610
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId)
104611
+ );
104612
+ await LocalForage.delete(STORAGE_KEYS.deviceId(this.orgHost, firebaseId));
104613
+ }
104614
+ await LocalForage.delete(
104615
+ STORAGE_KEYS.encryptedShare2DeviceLegacy(this.orgHost)
104616
+ );
104617
+ await LocalForage.delete(STORAGE_KEYS.deviceSecretLegacy(this.orgHost));
104618
+ await LocalForage.delete(STORAGE_KEYS.deviceIdLegacy(this.orgHost));
104481
104619
  }
104482
104620
  async getBalance(address, chainId) {
104483
104621
  const response = await this.rpcClient.sendRpc({
@@ -104614,7 +104752,7 @@ var WalletService = class {
104614
104752
  }
104615
104753
  }
104616
104754
  /**
104617
- * PATCH APPLIED (critical):
104755
+ * Critical safety:
104618
104756
  * - Never overwrite cached walletAddress with derived signer address.
104619
104757
  * - If server/cached address != derived address => throw WALLET_RECOVERY_FAILED.
104620
104758
  */
@@ -104642,19 +104780,21 @@ var WalletService = class {
104642
104780
  const firebaseId = await LocalForage.get(
104643
104781
  STORAGE_KEYS.firebaseId(this.orgHost)
104644
104782
  );
104645
- const encryptedDevice = await LocalForage.get(
104646
- STORAGE_KEYS.encryptedShare2Device(this.orgHost)
104647
- );
104648
- const deviceSecret = await LocalForage.get(
104649
- STORAGE_KEYS.deviceSecret(this.orgHost)
104650
- );
104651
- if (firebaseId && encryptedDevice && deviceSecret) {
104652
- share2 = Crypto.decryptShare(
104653
- encryptedDevice,
104654
- deviceSecret,
104655
- firebaseId
104783
+ if (firebaseId) {
104784
+ const encryptedDevice = await LocalForage.get(
104785
+ STORAGE_KEYS.encryptedShare2Device(this.orgHost, firebaseId)
104656
104786
  );
104657
- await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), share2);
104787
+ const deviceSecret = await LocalForage.get(
104788
+ STORAGE_KEYS.deviceSecret(this.orgHost, firebaseId)
104789
+ );
104790
+ if (encryptedDevice && deviceSecret) {
104791
+ share2 = Crypto.decryptShare(
104792
+ encryptedDevice,
104793
+ deviceSecret,
104794
+ firebaseId
104795
+ );
104796
+ await LocalForage.save(STORAGE_KEYS.share2(this.orgHost), share2);
104797
+ }
104658
104798
  }
104659
104799
  } catch {
104660
104800
  }
@@ -105243,6 +105383,38 @@ var WalletClient = class {
105243
105383
  needsAccessToken: true
105244
105384
  });
105245
105385
  }
105386
+ async getDeviceShare2Backup() {
105387
+ try {
105388
+ return await this.client.get(
105389
+ "/api/v1/wallet/device-share2",
105390
+ { needsAccessToken: true }
105391
+ );
105392
+ } catch (e7) {
105393
+ if (e7?.details?.status === 404 || String(e7?.message || "").includes("404"))
105394
+ return null;
105395
+ throw e7;
105396
+ }
105397
+ }
105398
+ async upsertDeviceShare2Backup(body) {
105399
+ await this.client.put("/api/v1/wallet/device-share2", body, {
105400
+ needsAccessToken: true
105401
+ });
105402
+ }
105403
+ async upsertDeviceRecovery(req) {
105404
+ await this.client.put(`${this.baseUrl}/device-recovery`, req, {
105405
+ needsAccessToken: true
105406
+ });
105407
+ }
105408
+ /**
105409
+ * If deviceId is omitted, server returns latest.
105410
+ * Returns { found:false } if no backup exists.
105411
+ */
105412
+ async getDeviceRecovery(deviceId) {
105413
+ const qs = deviceId ? `?deviceId=${encodeURIComponent(deviceId)}` : "";
105414
+ return this.client.get(`${this.baseUrl}/device-recovery${qs}`, {
105415
+ needsAccessToken: true
105416
+ });
105417
+ }
105246
105418
  };
105247
105419
 
105248
105420
  // src/clients/api/rpcs.ts