@authon/react-native-wallet 0.5.1 → 0.5.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @authon/react-native-wallet
2
2
 
3
+ ## 0.5.2
4
+
5
+ - Signing, `accounts()`, `protect()` and `upgradeBackup()` fail with `NEEDS_RESTORE` when this device holds a share of an older generation than the server, instead of `ADDRESS_MISMATCH` (Swift and Kotlin). This happens when a backup was saved on the server but the device could not store its new share, or after another device upgraded the backup. `status()` already reports `needsRestore` in that case; `restore()` with the existing backup passkey brings the device back. Calling `protect()` again on a device already in that state no longer reports success. This detects the state on later calls; it does not make the server and device writes atomic. `revokeDevice()` skips the local share check, and the server's own device checks still apply (a device left behind by another device's upgrade is refused there).
6
+
3
7
  ## 0.5.1
4
8
 
5
9
  - `AuthonWalletError` gains `reason`, `phase` and `clientAttemptId` (null when not reported). Android: when the passkey provider says it cannot validate the RP for this app (a DataError of the "RP ID cannot be validated" family), `protect()`, `upgradeBackup()` and `restore()` fail with `ORIGIN_NOT_ALLOWED`, `reason: 'PASSKEY_RP_VALIDATION_FAILED'`, `phase: 'passkey.create.rpValidation'` (or `passkey.get.rpValidation`) and `retryable: false` instead of `INTERNAL`. Other provider data errors stay `INTERNAL`. `phase` is set on Android Credential Manager failures and `clientAttemptId` (a random device-side ID) on those other than a cancel or a missing passkey; all three are null on iOS and from older natives. `diagnosticId` stays the server request ID only.
@@ -171,11 +171,18 @@ internal class WalletOps(private val ctx: WalletContext) {
171
171
 
172
172
  private class Local(val record: DeviceRecord, val wallet: ServerWallet)
173
173
 
174
- private suspend fun requireLocal(session: String): Local {
174
+ /**
175
+ * The local record for this user's wallet. With [currentShare] (every path that opens the device
176
+ * share) a share from an older generation is refused: the server rotated the shares (another
177
+ * device's upgrade, or a backup that committed before this device could save its new share), and
178
+ * combining the old share with the new company share would only fail later as ADDRESS_MISMATCH.
179
+ */
180
+ private suspend fun requireLocal(session: String, currentShare: Boolean = true): Local {
175
181
  val wallet = ctx.api.getMe(session)
176
182
  if (wallet == null || wallet.status == "pending") throw walletError(WalletErrorCode.NOT_FOUND)
177
183
  val record = ctx.store.get(recordKey(session))
178
184
  if (record == null || record.walletId != wallet.id) throw walletError(WalletErrorCode.NEEDS_RESTORE)
185
+ if (currentShare && record.shareVersion != wallet.shareVersion) throw walletError(WalletErrorCode.NEEDS_RESTORE)
179
186
  return Local(record, wallet)
180
187
  }
181
188
 
@@ -527,7 +534,7 @@ internal class WalletOps(private val ctx: WalletContext) {
527
534
 
528
535
  suspend fun revokeDevice(session: String, deviceId: String): Boolean {
529
536
  if (deviceId.isEmpty()) throw walletError(WalletErrorCode.INVALID_REQUEST, "deviceId required")
530
- val local = requireLocal(session)
537
+ val local = requireLocal(session, currentShare = false)
531
538
  ctx.api.revokeDevice(session, deviceId, deviceProof(session, local.record))
532
539
  if (local.record.deviceId == deviceId) ctx.store.delete(recordKey(session))
533
540
  return true
@@ -261,11 +261,16 @@ struct WalletOps {
261
261
  return try await status(session)
262
262
  }
263
263
 
264
- private func requireLocal(_ session: String) async throws -> (record: DeviceRecord, wallet: ServerWallet) {
264
+ /// The local record for this user's wallet. With `currentShare` (every path that opens the device
265
+ /// share) a share from an older generation is refused: the server rotated the shares (another
266
+ /// device's upgrade, or a backup that committed before this device could save its new share), and
267
+ /// combining the old share with the new company share would only fail later as addressMismatch.
268
+ private func requireLocal(_ session: String, currentShare: Bool = true) async throws -> (record: DeviceRecord, wallet: ServerWallet) {
265
269
  guard let wallet = try await ctx.api.getMe(session), wallet.status != "pending" else { throw WalletError(.notFound) }
266
270
  guard let record = try ctx.store.get(try recordKey(session)), record.walletId == wallet.id else {
267
271
  throw WalletError(.needsRestore)
268
272
  }
273
+ if currentShare && record.shareVersion != wallet.shareVersion { throw WalletError(.needsRestore) }
269
274
  return (record, wallet)
270
275
  }
271
276
 
@@ -647,7 +652,7 @@ struct WalletOps {
647
652
  /// Revokes a device with a request signed by this device's key.
648
653
  func revokeDevice(_ session: String, deviceId: String) async throws {
649
654
  guard !deviceId.isEmpty else { throw WalletError(.invalidRequest, "deviceId required") }
650
- let (record, _) = try await requireLocal(session)
655
+ let (record, _) = try await requireLocal(session, currentShare: false)
651
656
  try await ctx.api.revokeDevice(session, deviceId: deviceId, proof: try await deviceProof(session, record))
652
657
  if record.deviceId == deviceId { try ctx.store.delete(try recordKey(session)) }
653
658
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authon/react-native-wallet",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Authon embedded wallet for React Native (Expo native module wrapping the Swift and Kotlin wallet SDKs)",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",