@tatchi-xyz/sdk 0.17.0 → 0.19.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.
Files changed (162) hide show
  1. package/dist/cjs/core/EmailRecovery/emailRecoveryPendingStore.js +69 -0
  2. package/dist/cjs/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  3. package/dist/cjs/core/EmailRecovery/index.js +32 -20
  4. package/dist/cjs/core/EmailRecovery/index.js.map +1 -1
  5. package/dist/cjs/core/TatchiPasskey/emailRecovery.js +507 -452
  6. package/dist/cjs/core/TatchiPasskey/emailRecovery.js.map +1 -1
  7. package/dist/cjs/core/TatchiPasskey/index.js +1 -0
  8. package/dist/cjs/core/TatchiPasskey/index.js.map +1 -1
  9. package/dist/cjs/core/TatchiPasskey/relay.js +23 -1
  10. package/dist/cjs/core/TatchiPasskey/relay.js.map +1 -1
  11. package/dist/cjs/core/WalletIframe/client/IframeTransport.js +0 -7
  12. package/dist/cjs/core/WalletIframe/client/IframeTransport.js.map +1 -1
  13. package/dist/cjs/core/WalletIframe/client/router.js +6 -2
  14. package/dist/cjs/core/WalletIframe/client/router.js.map +1 -1
  15. package/dist/cjs/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js +1 -1
  16. package/dist/cjs/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js.map +1 -1
  17. package/dist/cjs/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js +1 -1
  18. package/dist/cjs/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js.map +1 -1
  19. package/dist/cjs/core/WebAuthnManager/index.js +23 -0
  20. package/dist/cjs/core/WebAuthnManager/index.js.map +1 -1
  21. package/dist/cjs/core/rpcCalls.js +8 -0
  22. package/dist/cjs/core/rpcCalls.js.map +1 -1
  23. package/dist/cjs/index.js +6 -2
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/react/components/AccountMenuButton/{LinkedDevicesModal-B6api181.css → LinkedDevicesModal-CSSowiHP.css} +1 -1
  26. package/dist/{esm/react/components/AccountMenuButton/LinkedDevicesModal-B6api181.css.map → cjs/react/components/AccountMenuButton/LinkedDevicesModal-CSSowiHP.css.map} +1 -1
  27. package/dist/cjs/react/components/AccountMenuButton/{ProfileDropdown-B-DrG_u5.css → ProfileDropdown-CEPMZ1gY.css} +1 -1
  28. package/dist/{esm/react/components/AccountMenuButton/ProfileDropdown-B-DrG_u5.css.map → cjs/react/components/AccountMenuButton/ProfileDropdown-CEPMZ1gY.css.map} +1 -1
  29. package/dist/cjs/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css → Web3AuthProfileButton-DopOg7Xc.css} +1 -1
  30. package/dist/cjs/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css.map → Web3AuthProfileButton-DopOg7Xc.css.map} +1 -1
  31. package/dist/cjs/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css → TouchIcon-BQWentvJ.css} +1 -1
  32. package/dist/cjs/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css.map → TouchIcon-BQWentvJ.css.map} +1 -1
  33. package/dist/cjs/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css → PasskeyAuthMenu-DwrzWMYx.css} +1 -1
  34. package/dist/cjs/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css.map → PasskeyAuthMenu-DwrzWMYx.css.map} +1 -1
  35. package/dist/cjs/react/components/{ShowQRCode-nZhZSaba.css → ShowQRCode-CCN4h6Uv.css} +1 -1
  36. package/dist/cjs/react/components/{ShowQRCode-nZhZSaba.css.map → ShowQRCode-CCN4h6Uv.css.map} +1 -1
  37. package/dist/cjs/react/hooks/usePreconnectWalletAssets.js +27 -32
  38. package/dist/cjs/react/hooks/usePreconnectWalletAssets.js.map +1 -1
  39. package/dist/cjs/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js +69 -0
  40. package/dist/cjs/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  41. package/dist/cjs/react/sdk/src/core/EmailRecovery/index.js +32 -20
  42. package/dist/cjs/react/sdk/src/core/EmailRecovery/index.js.map +1 -1
  43. package/dist/cjs/react/sdk/src/core/TatchiPasskey/emailRecovery.js +507 -452
  44. package/dist/cjs/react/sdk/src/core/TatchiPasskey/emailRecovery.js.map +1 -1
  45. package/dist/cjs/react/sdk/src/core/TatchiPasskey/index.js +1 -0
  46. package/dist/cjs/react/sdk/src/core/TatchiPasskey/index.js.map +1 -1
  47. package/dist/cjs/react/sdk/src/core/TatchiPasskey/relay.js +23 -1
  48. package/dist/cjs/react/sdk/src/core/TatchiPasskey/relay.js.map +1 -1
  49. package/dist/cjs/react/sdk/src/core/WalletIframe/client/IframeTransport.js +0 -7
  50. package/dist/cjs/react/sdk/src/core/WalletIframe/client/IframeTransport.js.map +1 -1
  51. package/dist/cjs/react/sdk/src/core/WalletIframe/client/router.js +6 -2
  52. package/dist/cjs/react/sdk/src/core/WalletIframe/client/router.js.map +1 -1
  53. package/dist/cjs/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js +1 -1
  54. package/dist/cjs/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js.map +1 -1
  55. package/dist/cjs/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js +1 -1
  56. package/dist/cjs/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js.map +1 -1
  57. package/dist/cjs/react/sdk/src/core/WebAuthnManager/index.js +23 -0
  58. package/dist/cjs/react/sdk/src/core/WebAuthnManager/index.js.map +1 -1
  59. package/dist/cjs/react/sdk/src/core/rpcCalls.js +8 -0
  60. package/dist/cjs/react/sdk/src/core/rpcCalls.js.map +1 -1
  61. package/dist/esm/core/EmailRecovery/emailRecoveryPendingStore.js +63 -0
  62. package/dist/esm/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  63. package/dist/esm/core/EmailRecovery/index.js +28 -21
  64. package/dist/esm/core/EmailRecovery/index.js.map +1 -1
  65. package/dist/esm/core/TatchiPasskey/emailRecovery.js +507 -452
  66. package/dist/esm/core/TatchiPasskey/emailRecovery.js.map +1 -1
  67. package/dist/esm/core/TatchiPasskey/index.js +2 -1
  68. package/dist/esm/core/TatchiPasskey/index.js.map +1 -1
  69. package/dist/esm/core/TatchiPasskey/relay.js +23 -1
  70. package/dist/esm/core/TatchiPasskey/relay.js.map +1 -1
  71. package/dist/esm/core/WalletIframe/client/IframeTransport.js +0 -7
  72. package/dist/esm/core/WalletIframe/client/IframeTransport.js.map +1 -1
  73. package/dist/esm/core/WalletIframe/client/router.js +7 -3
  74. package/dist/esm/core/WalletIframe/client/router.js.map +1 -1
  75. package/dist/esm/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js +1 -1
  76. package/dist/esm/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js.map +1 -1
  77. package/dist/esm/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js +1 -1
  78. package/dist/esm/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js.map +1 -1
  79. package/dist/esm/core/WebAuthnManager/index.js +23 -0
  80. package/dist/esm/core/WebAuthnManager/index.js.map +1 -1
  81. package/dist/esm/core/rpcCalls.js +8 -1
  82. package/dist/esm/core/rpcCalls.js.map +1 -1
  83. package/dist/esm/index.js +4 -1
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/react/components/AccountMenuButton/{LinkedDevicesModal-B6api181.css → LinkedDevicesModal-CSSowiHP.css} +1 -1
  86. package/dist/{cjs/react/components/AccountMenuButton/LinkedDevicesModal-B6api181.css.map → esm/react/components/AccountMenuButton/LinkedDevicesModal-CSSowiHP.css.map} +1 -1
  87. package/dist/esm/react/components/AccountMenuButton/{ProfileDropdown-B-DrG_u5.css → ProfileDropdown-CEPMZ1gY.css} +1 -1
  88. package/dist/{cjs/react/components/AccountMenuButton/ProfileDropdown-B-DrG_u5.css.map → esm/react/components/AccountMenuButton/ProfileDropdown-CEPMZ1gY.css.map} +1 -1
  89. package/dist/esm/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css → Web3AuthProfileButton-DopOg7Xc.css} +1 -1
  90. package/dist/esm/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css.map → Web3AuthProfileButton-DopOg7Xc.css.map} +1 -1
  91. package/dist/esm/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css → TouchIcon-BQWentvJ.css} +1 -1
  92. package/dist/esm/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css.map → TouchIcon-BQWentvJ.css.map} +1 -1
  93. package/dist/esm/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css → PasskeyAuthMenu-DwrzWMYx.css} +1 -1
  94. package/dist/esm/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css.map → PasskeyAuthMenu-DwrzWMYx.css.map} +1 -1
  95. package/dist/esm/react/components/{ShowQRCode-nZhZSaba.css → ShowQRCode-CCN4h6Uv.css} +1 -1
  96. package/dist/esm/react/components/{ShowQRCode-nZhZSaba.css.map → ShowQRCode-CCN4h6Uv.css.map} +1 -1
  97. package/dist/esm/react/hooks/usePreconnectWalletAssets.js +27 -32
  98. package/dist/esm/react/hooks/usePreconnectWalletAssets.js.map +1 -1
  99. package/dist/esm/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js +63 -0
  100. package/dist/esm/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  101. package/dist/esm/react/sdk/src/core/EmailRecovery/index.js +28 -21
  102. package/dist/esm/react/sdk/src/core/EmailRecovery/index.js.map +1 -1
  103. package/dist/esm/react/sdk/src/core/TatchiPasskey/emailRecovery.js +507 -452
  104. package/dist/esm/react/sdk/src/core/TatchiPasskey/emailRecovery.js.map +1 -1
  105. package/dist/esm/react/sdk/src/core/TatchiPasskey/index.js +2 -1
  106. package/dist/esm/react/sdk/src/core/TatchiPasskey/index.js.map +1 -1
  107. package/dist/esm/react/sdk/src/core/TatchiPasskey/relay.js +23 -1
  108. package/dist/esm/react/sdk/src/core/TatchiPasskey/relay.js.map +1 -1
  109. package/dist/esm/react/sdk/src/core/WalletIframe/client/IframeTransport.js +0 -7
  110. package/dist/esm/react/sdk/src/core/WalletIframe/client/IframeTransport.js.map +1 -1
  111. package/dist/esm/react/sdk/src/core/WalletIframe/client/router.js +7 -3
  112. package/dist/esm/react/sdk/src/core/WalletIframe/client/router.js.map +1 -1
  113. package/dist/esm/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js +1 -1
  114. package/dist/esm/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.js.map +1 -1
  115. package/dist/esm/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js +1 -1
  116. package/dist/esm/react/sdk/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/flows/localOnly.js.map +1 -1
  117. package/dist/esm/react/sdk/src/core/WebAuthnManager/index.js +23 -0
  118. package/dist/esm/react/sdk/src/core/WebAuthnManager/index.js.map +1 -1
  119. package/dist/esm/react/sdk/src/core/rpcCalls.js +8 -1
  120. package/dist/esm/react/sdk/src/core/rpcCalls.js.map +1 -1
  121. package/dist/esm/sdk/{createAdapters-qVGD6i0g.js → createAdapters-DIRR8_Z9.js} +1 -1
  122. package/dist/esm/sdk/{createAdapters-BumKM2ft.js → createAdapters-Yga6W0en.js} +2 -2
  123. package/dist/esm/sdk/{createAdapters-BumKM2ft.js.map → createAdapters-Yga6W0en.js.map} +1 -1
  124. package/dist/esm/sdk/{localOnly-pXMTqh1m.js → localOnly-BHScJasw.js} +2 -2
  125. package/dist/esm/sdk/{localOnly-Byi3AK7A.js → localOnly-VevCI7H0.js} +3 -3
  126. package/dist/esm/sdk/{localOnly-Byi3AK7A.js.map → localOnly-VevCI7H0.js.map} +1 -1
  127. package/dist/esm/sdk/offline-export-app.js +29 -6
  128. package/dist/esm/sdk/offline-export-app.js.map +1 -1
  129. package/dist/esm/sdk/{registration-CBiS4Ua_.js → registration-bKEg9Zr2.js} +2 -2
  130. package/dist/esm/sdk/{registration-CBiS4Ua_.js.map → registration-bKEg9Zr2.js.map} +1 -1
  131. package/dist/esm/sdk/{registration-DLPLsGCz.js → registration-lDD60Ytt.js} +1 -1
  132. package/dist/esm/sdk/{router-BLFegW7J.js → router-DuGYOd3G.js} +6 -9
  133. package/dist/esm/sdk/{rpcCalls-DEv9x5-f.js → rpcCalls-BQrJMTdg.js} +2 -2
  134. package/dist/esm/sdk/{rpcCalls-OhgEeFig.js → rpcCalls-YVeUVMk2.js} +8 -1
  135. package/dist/esm/sdk/{transactions-Bk-VavcV.js → transactions-BalIhtJ9.js} +1 -1
  136. package/dist/esm/sdk/{transactions-BIqKZeR0.js → transactions-bqaAwL4k.js} +2 -2
  137. package/dist/esm/sdk/{transactions-BIqKZeR0.js.map → transactions-bqaAwL4k.js.map} +1 -1
  138. package/dist/esm/sdk/wallet-iframe-host.js +641 -481
  139. package/dist/esm/wasm_vrf_worker/pkg/wasm_vrf_worker_bg.wasm +0 -0
  140. package/dist/types/src/core/EmailRecovery/emailRecoveryPendingStore.d.ts +25 -0
  141. package/dist/types/src/core/EmailRecovery/emailRecoveryPendingStore.d.ts.map +1 -0
  142. package/dist/types/src/core/EmailRecovery/index.d.ts +1 -0
  143. package/dist/types/src/core/EmailRecovery/index.d.ts.map +1 -1
  144. package/dist/types/src/core/TatchiPasskey/emailRecovery.d.ts +38 -6
  145. package/dist/types/src/core/TatchiPasskey/emailRecovery.d.ts.map +1 -1
  146. package/dist/types/src/core/TatchiPasskey/index.d.ts +2 -2
  147. package/dist/types/src/core/TatchiPasskey/index.d.ts.map +1 -1
  148. package/dist/types/src/core/TatchiPasskey/relay.d.ts +2 -1
  149. package/dist/types/src/core/TatchiPasskey/relay.d.ts.map +1 -1
  150. package/dist/types/src/core/WalletIframe/client/IframeTransport.d.ts.map +1 -1
  151. package/dist/types/src/core/WalletIframe/client/router.d.ts +3 -3
  152. package/dist/types/src/core/WalletIframe/client/router.d.ts.map +1 -1
  153. package/dist/types/src/core/WebAuthnManager/VrfWorkerManager/confirmTxFlow/adapters/webauthn.d.ts.map +1 -1
  154. package/dist/types/src/core/WebAuthnManager/index.d.ts +7 -0
  155. package/dist/types/src/core/WebAuthnManager/index.d.ts.map +1 -1
  156. package/dist/types/src/core/rpcCalls.d.ts +9 -0
  157. package/dist/types/src/core/rpcCalls.d.ts.map +1 -1
  158. package/dist/types/src/index.d.ts +1 -0
  159. package/dist/types/src/index.d.ts.map +1 -1
  160. package/dist/types/src/react/hooks/usePreconnectWalletAssets.d.ts.map +1 -1
  161. package/dist/workers/wasm_vrf_worker_bg.wasm +0 -0
  162. package/package.json +1 -1
@@ -7,6 +7,8 @@ import { EmailRecoveryPhase, EmailRecoveryStatus, init_sdkSentEvents } from "../
7
7
  import { DEFAULT_WAIT_STATUS, init_rpc } from "../types/rpc.js";
8
8
  import { init_getDeviceNumber, parseDeviceNumber } from "../WebAuthnManager/SignerWorkerManager/getDeviceNumber.js";
9
9
  import { getLoginSession, init_login } from "./login.js";
10
+ import { EmailRecoveryPendingStore } from "../EmailRecovery/emailRecoveryPendingStore.js";
11
+ import { init_EmailRecovery } from "../EmailRecovery/index.js";
10
12
 
11
13
  //#region src/core/TatchiPasskey/emailRecovery.ts
12
14
  var emailRecovery_exports = {};
@@ -52,9 +54,11 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
52
54
  init_rpc();
53
55
  init_getDeviceNumber();
54
56
  init_login();
57
+ init_EmailRecovery();
55
58
  EmailRecoveryFlow = class {
56
59
  context;
57
60
  options;
61
+ pendingStore;
58
62
  pending = null;
59
63
  phase = EmailRecoveryPhase.STEP_1_PREPARATION;
60
64
  pollingTimer;
@@ -65,6 +69,7 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
65
69
  constructor(context, options) {
66
70
  this.context = context;
67
71
  this.options = options;
72
+ this.pendingStore = options?.pendingStore ?? new EmailRecoveryPendingStore({ getPendingTtlMs: () => this.getConfig().pendingTtlMs });
68
73
  }
69
74
  setOptions(options) {
70
75
  if (!options) return;
@@ -72,6 +77,7 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
72
77
  ...this.options || {},
73
78
  ...options
74
79
  };
80
+ if (options.pendingStore) this.pendingStore = options.pendingStore;
75
81
  }
76
82
  emit(event) {
77
83
  this.options?.onEvent?.(event);
@@ -90,24 +96,139 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
90
96
  this.options?.onError?.(err);
91
97
  return err;
92
98
  }
99
+ async fail(step, message) {
100
+ const err = this.emitError(step, message);
101
+ await this.options?.afterCall?.(false);
102
+ throw err;
103
+ }
104
+ async assertValidAccountIdOrFail(step, accountId) {
105
+ const validation = validateNearAccountId(accountId);
106
+ if (!validation.valid) await this.fail(step, `Invalid NEAR account ID: ${validation.error}`);
107
+ return toAccountId(accountId);
108
+ }
109
+ async resolvePendingOrFail(step, args, options) {
110
+ const { allowErrorStatus = true, missingMessage = "No pending email recovery record found for this account", errorStatusMessage = "Pending email recovery is in an error state; please restart the flow" } = options ?? {};
111
+ let rec = this.pending;
112
+ if (!rec || rec.accountId !== args.accountId || args.nearPublicKey && rec.nearPublicKey !== args.nearPublicKey) {
113
+ rec = await this.loadPending(args.accountId, args.nearPublicKey);
114
+ this.pending = rec;
115
+ }
116
+ if (!rec) await this.fail(step, missingMessage);
117
+ const resolved = rec;
118
+ if (!allowErrorStatus && resolved.status === "error") await this.fail(step, errorStatusMessage);
119
+ return resolved;
120
+ }
93
121
  getConfig() {
94
122
  return getEmailRecoveryConfig(this.context.configs);
95
123
  }
96
- getPendingIndexKey(accountId) {
97
- return `pendingEmailRecovery:${accountId}`;
124
+ toBigInt(value) {
125
+ if (typeof value === "bigint") return value;
126
+ if (typeof value === "number") return BigInt(value);
127
+ if (typeof value === "string" && value.length > 0) return BigInt(value);
128
+ return BigInt(0);
129
+ }
130
+ computeAvailableBalance(accountView) {
131
+ const STORAGE_PRICE_PER_BYTE = BigInt("10000000000000000000");
132
+ const amount = this.toBigInt(accountView.amount);
133
+ const locked = this.toBigInt(accountView.locked);
134
+ const storageUsage = this.toBigInt(accountView.storage_usage);
135
+ const storageCost = storageUsage * STORAGE_PRICE_PER_BYTE;
136
+ const rawAvailable = amount - locked - storageCost;
137
+ return rawAvailable > 0 ? rawAvailable : BigInt(0);
138
+ }
139
+ async assertSufficientBalance(nearAccountId) {
140
+ const { minBalanceYocto } = this.getConfig();
141
+ try {
142
+ const accountView = await this.context.nearClient.viewAccount(nearAccountId);
143
+ const available = this.computeAvailableBalance(accountView);
144
+ if (available < BigInt(minBalanceYocto)) await this.fail(1, `This account does not have enough NEAR to finalize recovery. Available: ${available.toString()} yocto; required: ${String(minBalanceYocto)}. Please top up and try again.`);
145
+ } catch (e) {
146
+ await this.fail(1, e?.message || "Failed to fetch account balance for recovery");
147
+ }
148
+ }
149
+ async getCanonicalRecoveryEmailOrFail(recoveryEmail) {
150
+ const canonicalEmail = String(recoveryEmail || "").trim().toLowerCase();
151
+ if (!canonicalEmail) await this.fail(1, "Recovery email is required for email-based account recovery");
152
+ return canonicalEmail;
153
+ }
154
+ async getNextDeviceNumberFromContract(nearAccountId) {
155
+ try {
156
+ const { syncAuthenticatorsContractCall } = await import("../rpcCalls.js");
157
+ const authenticators = await syncAuthenticatorsContractCall(this.context.nearClient, this.context.configs.contractId, nearAccountId);
158
+ const numbers = authenticators.map((a) => a?.authenticator?.deviceNumber).filter((n) => typeof n === "number" && Number.isFinite(n));
159
+ const max = numbers.length > 0 ? Math.max(...numbers) : 0;
160
+ return max + 1;
161
+ } catch {
162
+ return 1;
163
+ }
164
+ }
165
+ async collectRecoveryCredentialOrFail(nearAccountId, deviceNumber) {
166
+ const confirmerText = {
167
+ title: this.options?.confirmerText?.title ?? "Register New Recovery Account",
168
+ body: this.options?.confirmerText?.body ?? "Create a recovery account and send an encrypted email to recover your account."
169
+ };
170
+ const confirm = await this.context.webAuthnManager.requestRegistrationCredentialConfirmation({
171
+ nearAccountId,
172
+ deviceNumber,
173
+ confirmerText,
174
+ confirmationConfigOverride: this.options?.confirmationConfig
175
+ });
176
+ if (!confirm.confirmed || !confirm.credential) await this.fail(2, "User cancelled email recovery TouchID confirmation");
177
+ return {
178
+ credential: confirm.credential,
179
+ vrfChallenge: confirm.vrfChallenge || void 0
180
+ };
181
+ }
182
+ async deriveRecoveryKeysOrFail(nearAccountId, deviceNumber, credential) {
183
+ const vrfDerivationResult = await this.context.webAuthnManager.deriveVrfKeypair({
184
+ credential,
185
+ nearAccountId
186
+ });
187
+ if (!vrfDerivationResult.success || !vrfDerivationResult.encryptedVrfKeypair) await this.fail(2, "Failed to derive VRF keypair from PRF for email recovery");
188
+ const nearKeyResult = await this.context.webAuthnManager.deriveNearKeypairAndEncryptFromSerialized({
189
+ nearAccountId,
190
+ credential,
191
+ options: { deviceNumber }
192
+ });
193
+ if (!nearKeyResult.success || !nearKeyResult.publicKey) await this.fail(2, "Failed to derive NEAR keypair for email recovery");
194
+ return {
195
+ encryptedVrfKeypair: vrfDerivationResult.encryptedVrfKeypair,
196
+ serverEncryptedVrfKeypair: vrfDerivationResult.serverEncryptedVrfKeypair || null,
197
+ vrfPublicKey: vrfDerivationResult.vrfPublicKey,
198
+ nearPublicKey: nearKeyResult.publicKey
199
+ };
200
+ }
201
+ emitAwaitEmail(rec, mailtoUrl) {
202
+ this.phase = EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
203
+ this.emit({
204
+ step: 3,
205
+ phase: EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
206
+ status: EmailRecoveryStatus.PROGRESS,
207
+ message: "New device key created; please send the recovery email from your registered address.",
208
+ data: {
209
+ accountId: rec.accountId,
210
+ recoveryEmail: rec.recoveryEmail,
211
+ nearPublicKey: rec.nearPublicKey,
212
+ requestId: rec.requestId,
213
+ mailtoUrl
214
+ }
215
+ });
98
216
  }
99
- getPendingRecordKey(accountId, nearPublicKey) {
100
- return `${this.getPendingIndexKey(accountId)}:${nearPublicKey}`;
217
+ emitAutoLoginEvent(status, message, data) {
218
+ this.emit({
219
+ step: 5,
220
+ phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
221
+ status,
222
+ message,
223
+ data
224
+ });
101
225
  }
102
- async checkVerificationStatus(rec) {
226
+ async checkViaDkimViewMethod(rec) {
103
227
  const { dkimVerifierAccountId, verificationViewMethod } = this.getConfig();
104
228
  if (!dkimVerifierAccountId) return null;
105
229
  try {
106
- const result = await this.context.nearClient.view({
107
- account: dkimVerifierAccountId,
108
- method: verificationViewMethod,
109
- args: { request_id: rec.requestId }
110
- });
230
+ const { getEmailRecoveryVerificationResult } = await import("../rpcCalls.js");
231
+ const result = await getEmailRecoveryVerificationResult(this.context.nearClient, dkimVerifierAccountId, verificationViewMethod, rec.requestId);
111
232
  if (!result) return {
112
233
  completed: false,
113
234
  success: false
@@ -139,43 +260,78 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
139
260
  transactionHash: result.transaction_hash
140
261
  };
141
262
  } catch (err) {
142
- console.warn("[EmailRecoveryFlow] get_verification_result view failed; falling back to access key polling", err);
263
+ console.warn("[EmailRecoveryFlow] get_verification_result view failed; will retry", err);
143
264
  return null;
144
265
  }
145
266
  }
146
- async loadPending(accountId, nearPublicKey) {
147
- const { pendingTtlMs } = this.getConfig();
148
- const indexKey = this.getPendingIndexKey(accountId);
149
- const indexedNearPublicKey = await IndexedDBManager.clientDB.getAppState(indexKey);
150
- const resolvedNearPublicKey = nearPublicKey ?? indexedNearPublicKey;
151
- if (!resolvedNearPublicKey) return null;
152
- const recordKey = this.getPendingRecordKey(accountId, resolvedNearPublicKey);
153
- const record = await IndexedDBManager.clientDB.getAppState(recordKey);
154
- const shouldClearIndex = indexedNearPublicKey === resolvedNearPublicKey;
155
- if (!record) {
156
- if (shouldClearIndex) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
157
- return null;
158
- }
159
- if (Date.now() - record.createdAt > pendingTtlMs) {
160
- await IndexedDBManager.clientDB.setAppState(recordKey, void 0).catch(() => {});
161
- if (shouldClearIndex) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
162
- return null;
267
+ buildPollingEventData(rec, details) {
268
+ return {
269
+ accountId: rec.accountId,
270
+ requestId: rec.requestId,
271
+ nearPublicKey: rec.nearPublicKey,
272
+ transactionHash: details.transactionHash,
273
+ elapsedMs: details.elapsedMs,
274
+ pollCount: details.pollCount
275
+ };
276
+ }
277
+ async sleepForPollInterval(ms) {
278
+ await new Promise((resolve) => {
279
+ this.pollIntervalResolver = resolve;
280
+ this.pollingTimer = setTimeout(() => {
281
+ this.pollIntervalResolver = void 0;
282
+ this.pollingTimer = void 0;
283
+ resolve();
284
+ }, ms);
285
+ }).finally(() => {
286
+ this.pollIntervalResolver = void 0;
287
+ });
288
+ }
289
+ async pollUntil(args) {
290
+ const now = args.now ?? Date.now;
291
+ const sleep = args.sleep ?? this.sleepForPollInterval.bind(this);
292
+ const startedAt = now();
293
+ let pollCount = 0;
294
+ while (!args.isCancelled()) {
295
+ pollCount += 1;
296
+ const elapsedMs$1 = now() - startedAt;
297
+ if (elapsedMs$1 > args.timeoutMs) return {
298
+ status: "timedOut",
299
+ elapsedMs: elapsedMs$1,
300
+ pollCount
301
+ };
302
+ const result = await args.tick({
303
+ elapsedMs: elapsedMs$1,
304
+ pollCount
305
+ });
306
+ if (result.done) return {
307
+ status: "completed",
308
+ value: result.value,
309
+ elapsedMs: elapsedMs$1,
310
+ pollCount
311
+ };
312
+ if (args.isCancelled()) return {
313
+ status: "cancelled",
314
+ elapsedMs: elapsedMs$1,
315
+ pollCount
316
+ };
317
+ await sleep(args.intervalMs);
163
318
  }
164
- await IndexedDBManager.clientDB.setAppState(indexKey, record.nearPublicKey).catch(() => {});
165
- return record;
319
+ const elapsedMs = now() - startedAt;
320
+ return {
321
+ status: "cancelled",
322
+ elapsedMs,
323
+ pollCount
324
+ };
325
+ }
326
+ async loadPending(accountId, nearPublicKey) {
327
+ return this.pendingStore.get(accountId, nearPublicKey);
166
328
  }
167
329
  async savePending(rec) {
168
- const key = this.getPendingRecordKey(rec.accountId, rec.nearPublicKey);
169
- await IndexedDBManager.clientDB.setAppState(key, rec);
170
- await IndexedDBManager.clientDB.setAppState(this.getPendingIndexKey(rec.accountId), rec.nearPublicKey).catch(() => {});
330
+ await this.pendingStore.set(rec);
171
331
  this.pending = rec;
172
332
  }
173
333
  async clearPending(accountId, nearPublicKey) {
174
- const indexKey = this.getPendingIndexKey(accountId);
175
- const idx = await IndexedDBManager.clientDB.getAppState(indexKey).catch(() => void 0);
176
- const resolvedNearPublicKey = nearPublicKey || idx || "";
177
- if (resolvedNearPublicKey) await IndexedDBManager.clientDB.setAppState(this.getPendingRecordKey(accountId, resolvedNearPublicKey), void 0).catch(() => {});
178
- if (!nearPublicKey || idx === nearPublicKey) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
334
+ await this.pendingStore.clear(accountId, nearPublicKey);
179
335
  if (this.pending && this.pending.accountId === accountId && (!nearPublicKey || this.pending.nearPublicKey === nearPublicKey)) this.pending = null;
180
336
  }
181
337
  getState() {
@@ -189,48 +345,14 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
189
345
  const { accountId, nearPublicKey } = args;
190
346
  this.cancelled = false;
191
347
  this.error = void 0;
192
- const validation = validateNearAccountId(accountId);
193
- if (!validation.valid) {
194
- const err = this.emitError(3, `Invalid NEAR account ID: ${validation.error}`);
195
- await this.options?.afterCall?.(false);
196
- throw err;
197
- }
198
- const nearAccountId = toAccountId(accountId);
199
- let rec = this.pending;
200
- if (!rec || rec.accountId !== nearAccountId || nearPublicKey && rec.nearPublicKey !== nearPublicKey) {
201
- rec = await this.loadPending(nearAccountId, nearPublicKey);
202
- this.pending = rec;
203
- }
204
- if (!rec) {
205
- const err = this.emitError(3, "No pending email recovery record found for this account");
206
- await this.options?.afterCall?.(false);
207
- throw err;
208
- }
209
- if (rec.status === "error") {
210
- const err = this.emitError(3, "Pending email recovery is in an error state; please restart the flow");
211
- await this.options?.afterCall?.(false);
212
- throw err;
213
- }
214
- if (rec.status === "finalizing" || rec.status === "complete") {
215
- const err = this.emitError(3, "Recovery email has already been processed on-chain for this request");
216
- await this.options?.afterCall?.(false);
217
- throw err;
218
- }
348
+ const nearAccountId = await this.assertValidAccountIdOrFail(3, accountId);
349
+ const rec = await this.resolvePendingOrFail(3, {
350
+ accountId: nearAccountId,
351
+ nearPublicKey
352
+ }, { allowErrorStatus: false });
353
+ if (rec.status === "finalizing" || rec.status === "complete") await this.fail(3, "Recovery email has already been processed on-chain for this request");
219
354
  const mailtoUrl = rec.status === "awaiting-email" ? await this.buildMailtoUrlAndUpdateStatus(rec) : this.buildMailtoUrlInternal(rec);
220
- this.phase = EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
221
- this.emit({
222
- step: 3,
223
- phase: EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
224
- status: EmailRecoveryStatus.PROGRESS,
225
- message: "New device key created; please send the recovery email from your registered address.",
226
- data: {
227
- accountId: rec.accountId,
228
- recoveryEmail: rec.recoveryEmail,
229
- nearPublicKey: rec.nearPublicKey,
230
- requestId: rec.requestId,
231
- mailtoUrl
232
- }
233
- });
355
+ this.emitAwaitEmail(rec, mailtoUrl);
234
356
  await this.options?.afterCall?.(true, void 0);
235
357
  return mailtoUrl;
236
358
  }
@@ -245,49 +367,10 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
245
367
  status: EmailRecoveryStatus.PROGRESS,
246
368
  message: "Preparing email recovery..."
247
369
  });
248
- const validation = validateNearAccountId(accountId);
249
- if (!validation.valid) {
250
- const err = this.emitError(1, `Invalid NEAR account ID: ${validation.error}`);
251
- await this.options?.afterCall?.(false);
252
- throw err;
253
- }
254
- const nearAccountId = toAccountId(accountId);
255
- const { minBalanceYocto } = this.getConfig();
256
- const STORAGE_PRICE_PER_BYTE = BigInt("10000000000000000000");
257
- try {
258
- const accountView = await this.context.nearClient.viewAccount(nearAccountId);
259
- const amount = BigInt(accountView.amount || "0");
260
- const locked = BigInt(accountView.locked || "0");
261
- const storageUsage = BigInt(accountView.storage_usage || 0);
262
- const storageCost = storageUsage * STORAGE_PRICE_PER_BYTE;
263
- const rawAvailable = amount - locked - storageCost;
264
- const available = rawAvailable > 0 ? rawAvailable : BigInt(0);
265
- if (available < BigInt(minBalanceYocto)) {
266
- const err = this.emitError(1, `This account does not have enough NEAR to finalize recovery. Available: ${available.toString()} yocto; required: ${String(minBalanceYocto)}. Please top up and try again.`);
267
- await this.options?.afterCall?.(false);
268
- throw err;
269
- }
270
- } catch (e) {
271
- const err = this.emitError(1, e?.message || "Failed to fetch account balance for recovery");
272
- await this.options?.afterCall?.(false);
273
- throw err;
274
- }
275
- const canonicalEmail = String(recoveryEmail || "").trim().toLowerCase();
276
- if (!canonicalEmail) {
277
- const err = this.emitError(1, "Recovery email is required for email-based account recovery");
278
- await this.options?.afterCall?.(false);
279
- throw err;
280
- }
281
- let deviceNumber = 1;
282
- try {
283
- const { syncAuthenticatorsContractCall } = await import("../rpcCalls.js");
284
- const authenticators = await syncAuthenticatorsContractCall(this.context.nearClient, this.context.configs.contractId, nearAccountId);
285
- const numbers = authenticators.map((a) => a?.authenticator?.deviceNumber).filter((n) => typeof n === "number" && Number.isFinite(n));
286
- const max = numbers.length > 0 ? Math.max(...numbers) : 0;
287
- deviceNumber = max + 1;
288
- } catch {
289
- deviceNumber = 1;
290
- }
370
+ const nearAccountId = await this.assertValidAccountIdOrFail(1, accountId);
371
+ await this.assertSufficientBalance(nearAccountId);
372
+ const canonicalEmail = await this.getCanonicalRecoveryEmailOrFail(recoveryEmail);
373
+ const deviceNumber = await this.getNextDeviceNumberFromContract(nearAccountId);
291
374
  this.phase = EmailRecoveryPhase.STEP_2_TOUCH_ID_REGISTRATION;
292
375
  this.emit({
293
376
  step: 2,
@@ -296,69 +379,24 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
296
379
  message: "Collecting passkey for email recovery..."
297
380
  });
298
381
  try {
299
- const confirmerText = {
300
- title: this.options?.confirmerText?.title ?? "Register New Recovery Account",
301
- body: this.options?.confirmerText?.body ?? "Create a recovery account and send an encrypted email to recover your account."
302
- };
303
- const confirm = await this.context.webAuthnManager.requestRegistrationCredentialConfirmation({
304
- nearAccountId,
305
- deviceNumber,
306
- confirmerText,
307
- confirmationConfigOverride: this.options?.confirmationConfig
308
- });
309
- if (!confirm.confirmed || !confirm.credential) {
310
- const err = this.emitError(2, "User cancelled email recovery TouchID confirmation");
311
- await this.options?.afterCall?.(false);
312
- throw err;
313
- }
314
- const vrfDerivationResult = await this.context.webAuthnManager.deriveVrfKeypair({
315
- credential: confirm.credential,
316
- nearAccountId
317
- });
318
- if (!vrfDerivationResult.success || !vrfDerivationResult.encryptedVrfKeypair) {
319
- const err = this.emitError(2, "Failed to derive VRF keypair from PRF for email recovery");
320
- await this.options?.afterCall?.(false);
321
- throw err;
322
- }
323
- const nearKeyResult = await this.context.webAuthnManager.deriveNearKeypairAndEncryptFromSerialized({
324
- nearAccountId,
325
- credential: confirm.credential,
326
- options: { deviceNumber }
327
- });
328
- if (!nearKeyResult.success || !nearKeyResult.publicKey) {
329
- const err = this.emitError(2, "Failed to derive NEAR keypair for email recovery");
330
- await this.options?.afterCall?.(false);
331
- throw err;
332
- }
382
+ const confirm = await this.collectRecoveryCredentialOrFail(nearAccountId, deviceNumber);
383
+ const derivedKeys = await this.deriveRecoveryKeysOrFail(nearAccountId, deviceNumber, confirm.credential);
333
384
  const rec = {
334
385
  accountId: nearAccountId,
335
386
  recoveryEmail: canonicalEmail,
336
387
  deviceNumber,
337
- nearPublicKey: nearKeyResult.publicKey,
388
+ nearPublicKey: derivedKeys.nearPublicKey,
338
389
  requestId: generateEmailRecoveryRequestId(),
339
- encryptedVrfKeypair: vrfDerivationResult.encryptedVrfKeypair,
340
- serverEncryptedVrfKeypair: vrfDerivationResult.serverEncryptedVrfKeypair || null,
341
- vrfPublicKey: vrfDerivationResult.vrfPublicKey,
390
+ encryptedVrfKeypair: derivedKeys.encryptedVrfKeypair,
391
+ serverEncryptedVrfKeypair: derivedKeys.serverEncryptedVrfKeypair,
392
+ vrfPublicKey: derivedKeys.vrfPublicKey,
342
393
  credential: confirm.credential,
343
394
  vrfChallenge: confirm.vrfChallenge || void 0,
344
395
  createdAt: Date.now(),
345
396
  status: "awaiting-email"
346
397
  };
347
398
  const mailtoUrl = await this.buildMailtoUrlAndUpdateStatus(rec);
348
- this.phase = EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
349
- this.emit({
350
- step: 3,
351
- phase: EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
352
- status: EmailRecoveryStatus.PROGRESS,
353
- message: "New device key created; please send the recovery email from your registered address.",
354
- data: {
355
- accountId: rec.accountId,
356
- recoveryEmail: rec.recoveryEmail,
357
- nearPublicKey: rec.nearPublicKey,
358
- requestId: rec.requestId,
359
- mailtoUrl
360
- }
361
- });
399
+ this.emitAwaitEmail(rec, mailtoUrl);
362
400
  await this.options?.afterCall?.(true, void 0);
363
401
  return {
364
402
  mailtoUrl,
@@ -386,28 +424,11 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
386
424
  const { accountId, nearPublicKey } = args;
387
425
  this.cancelled = false;
388
426
  this.error = void 0;
389
- const validation = validateNearAccountId(accountId);
390
- if (!validation.valid) {
391
- const err = this.emitError(4, `Invalid NEAR account ID: ${validation.error}`);
392
- await this.options?.afterCall?.(false);
393
- throw err;
394
- }
395
- const nearAccountId = toAccountId(accountId);
396
- let rec = this.pending;
397
- if (!rec || rec.accountId !== nearAccountId || nearPublicKey && rec.nearPublicKey !== nearPublicKey) {
398
- rec = await this.loadPending(nearAccountId, nearPublicKey);
399
- this.pending = rec;
400
- }
401
- if (!rec) {
402
- const err = this.emitError(4, "No pending email recovery record found for this account");
403
- await this.options?.afterCall?.(false);
404
- throw err;
405
- }
406
- if (rec.status === "error") {
407
- const err = this.emitError(4, "Pending email recovery is in an error state; please restart the flow");
408
- await this.options?.afterCall?.(false);
409
- throw err;
410
- }
427
+ const nearAccountId = await this.assertValidAccountIdOrFail(4, accountId);
428
+ const rec = await this.resolvePendingOrFail(4, {
429
+ accountId: nearAccountId,
430
+ nearPublicKey
431
+ }, { allowErrorStatus: false });
411
432
  if (rec.status === "complete" || rec.status === "finalizing") {
412
433
  await this.options?.afterCall?.(true, void 0);
413
434
  return;
@@ -447,23 +468,11 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
447
468
  const { accountId, nearPublicKey } = args;
448
469
  this.cancelled = false;
449
470
  this.error = void 0;
450
- const validation = validateNearAccountId(accountId);
451
- if (!validation.valid) {
452
- const err = this.emitError(4, `Invalid NEAR account ID: ${validation.error}`);
453
- await this.options?.afterCall?.(false);
454
- throw err;
455
- }
456
- const nearAccountId = toAccountId(accountId);
457
- let rec = this.pending;
458
- if (!rec || rec.accountId !== nearAccountId || nearPublicKey && rec.nearPublicKey !== nearPublicKey) {
459
- rec = await this.loadPending(nearAccountId, nearPublicKey);
460
- this.pending = rec;
461
- }
462
- if (!rec) {
463
- const err = this.emitError(4, "No pending email recovery record found for this account");
464
- await this.options?.afterCall?.(false);
465
- throw err;
466
- }
471
+ const nearAccountId = await this.assertValidAccountIdOrFail(4, accountId);
472
+ const rec = await this.resolvePendingOrFail(4, {
473
+ accountId: nearAccountId,
474
+ nearPublicKey
475
+ }, { allowErrorStatus: true });
467
476
  this.emit({
468
477
  step: 0,
469
478
  phase: EmailRecoveryPhase.RESUMED_FROM_PENDING,
@@ -499,245 +508,230 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
499
508
  }
500
509
  this.phase = EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT;
501
510
  this.pollingStartedAt = Date.now();
502
- let pollCount = 0;
503
- while (!this.cancelled) {
504
- pollCount += 1;
505
- const elapsed = Date.now() - (this.pollingStartedAt || 0);
506
- if (elapsed > maxPollingDurationMs) {
507
- const err$1 = this.emitError(4, "Timed out waiting for recovery email to be processed on-chain");
511
+ const pollResult = await this.pollUntil({
512
+ intervalMs: pollingIntervalMs,
513
+ timeoutMs: maxPollingDurationMs,
514
+ isCancelled: () => this.cancelled,
515
+ tick: async ({ elapsedMs, pollCount }) => {
516
+ const verification = await this.checkViaDkimViewMethod(rec);
517
+ const completed = verification?.completed === true;
518
+ const success = verification?.success === true;
519
+ this.emit({
520
+ step: 4,
521
+ phase: EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT,
522
+ status: EmailRecoveryStatus.PROGRESS,
523
+ message: completed && success ? `Email verified for request ${rec.requestId}; finalizing registration` : `Waiting for email verification for request ${rec.requestId}`,
524
+ data: this.buildPollingEventData(rec, {
525
+ transactionHash: verification?.transactionHash,
526
+ elapsedMs,
527
+ pollCount
528
+ })
529
+ });
530
+ if (!completed) return { done: false };
531
+ if (!success) return {
532
+ done: true,
533
+ value: {
534
+ outcome: "failed",
535
+ errorMessage: verification?.errorMessage || "Email verification failed"
536
+ }
537
+ };
538
+ return {
539
+ done: true,
540
+ value: { outcome: "verified" }
541
+ };
542
+ }
543
+ });
544
+ if (pollResult.status === "completed") {
545
+ if (pollResult.value.outcome === "failed") {
546
+ const err$1 = this.emitError(4, pollResult.value.errorMessage);
508
547
  rec.status = "error";
509
548
  await this.savePending(rec);
510
549
  await this.options?.afterCall?.(false);
511
550
  throw err$1;
512
551
  }
513
- const verification = await this.checkVerificationStatus(rec);
514
- const completed = verification?.completed === true;
515
- const success = verification?.success === true;
516
- this.emit({
517
- step: 4,
518
- phase: EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT,
519
- status: EmailRecoveryStatus.PROGRESS,
520
- message: completed && success ? `Email verified for request ${rec.requestId}; finalizing registration` : `Waiting for email verification for request ${rec.requestId}`,
521
- data: {
522
- accountId: rec.accountId,
523
- requestId: rec.requestId,
524
- nearPublicKey: rec.nearPublicKey,
525
- transactionHash: verification?.transactionHash,
526
- elapsedMs: elapsed,
527
- pollCount
528
- }
529
- });
530
- if (completed) {
531
- if (!success) {
532
- const err$1 = this.emitError(4, verification?.errorMessage || "Email verification failed");
533
- rec.status = "error";
534
- await this.savePending(rec);
535
- await this.options?.afterCall?.(false);
536
- throw err$1;
537
- }
538
- rec.status = "finalizing";
539
- await this.savePending(rec);
540
- return;
541
- }
542
- if (this.cancelled) break;
543
- await new Promise((resolve) => {
544
- this.pollIntervalResolver = resolve;
545
- this.pollingTimer = setTimeout(() => {
546
- this.pollIntervalResolver = void 0;
547
- this.pollingTimer = void 0;
548
- resolve();
549
- }, pollingIntervalMs);
550
- }).finally(() => {
551
- this.pollIntervalResolver = void 0;
552
- });
552
+ rec.status = "finalizing";
553
+ await this.savePending(rec);
554
+ return;
555
+ }
556
+ if (pollResult.status === "timedOut") {
557
+ const err$1 = this.emitError(4, "Timed out waiting for recovery email to be processed on-chain");
558
+ rec.status = "error";
559
+ await this.savePending(rec);
560
+ await this.options?.afterCall?.(false);
561
+ throw err$1;
553
562
  }
554
563
  const err = this.emitError(4, "Email recovery polling was cancelled");
555
564
  await this.options?.afterCall?.(false);
556
565
  throw err;
557
566
  }
558
- async finalizeRegistration(rec) {
559
- this.phase = EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
560
- this.emit({
561
- step: 5,
562
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
563
- status: EmailRecoveryStatus.PROGRESS,
564
- message: "Finalizing email recovery registration...",
565
- data: {
566
- accountId: rec.accountId,
567
- nearPublicKey: rec.nearPublicKey
568
- }
569
- });
567
+ initializeNonceManager(rec) {
570
568
  const nonceManager = this.context.webAuthnManager.getNonceManager();
571
569
  const accountId = toAccountId(rec.accountId);
572
570
  nonceManager.initializeUser(accountId, rec.nearPublicKey);
571
+ return {
572
+ nonceManager,
573
+ accountId
574
+ };
575
+ }
576
+ async signRegistrationTx(rec, accountId) {
577
+ const vrfChallenge = rec.vrfChallenge;
578
+ if (!vrfChallenge) return this.fail(5, "Missing VRF challenge for email recovery registration");
579
+ const registrationResult = await this.context.webAuthnManager.signDevice2RegistrationWithStoredKey({
580
+ nearAccountId: accountId,
581
+ credential: rec.credential,
582
+ vrfChallenge,
583
+ deterministicVrfPublicKey: rec.vrfPublicKey,
584
+ deviceNumber: rec.deviceNumber
585
+ });
586
+ if (!registrationResult.success || !registrationResult.signedTransaction) await this.fail(5, registrationResult.error || "Failed to sign email recovery registration transaction");
587
+ return registrationResult.signedTransaction;
588
+ }
589
+ async broadcastRegistrationTxAndWaitFinal(rec, signedTx) {
573
590
  try {
574
- if (!rec.vrfChallenge) {
575
- const err = this.emitError(5, "Missing VRF challenge for email recovery registration");
576
- await this.options?.afterCall?.(false);
577
- throw err;
578
- }
579
- const registrationResult = await this.context.webAuthnManager.signDevice2RegistrationWithStoredKey({
580
- nearAccountId: accountId,
581
- credential: rec.credential,
582
- vrfChallenge: rec.vrfChallenge,
583
- deterministicVrfPublicKey: rec.vrfPublicKey,
584
- deviceNumber: rec.deviceNumber
585
- });
586
- if (!registrationResult.success || !registrationResult.signedTransaction) {
587
- const err = this.emitError(5, registrationResult.error || "Failed to sign email recovery registration transaction");
588
- await this.options?.afterCall?.(false);
589
- throw err;
590
- }
591
- const signedTx = registrationResult.signedTransaction;
591
+ const txResult = await this.context.nearClient.sendTransaction(signedTx, DEFAULT_WAIT_STATUS.linkDeviceRegistration);
592
592
  try {
593
- const txResult = await this.context.nearClient.sendTransaction(signedTx, DEFAULT_WAIT_STATUS.linkDeviceRegistration);
594
- try {
595
- const txHash = txResult?.transaction?.hash || txResult?.transaction_hash;
596
- if (txHash) {
597
- this.emit({
598
- step: 5,
599
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
600
- status: EmailRecoveryStatus.PROGRESS,
601
- message: "Registration transaction confirmed",
602
- data: {
603
- accountId: rec.accountId,
604
- nearPublicKey: rec.nearPublicKey,
605
- transactionHash: txHash
606
- }
607
- });
608
- try {
609
- await IndexedDBManager.clientDB.storeWebAuthnUserData({
610
- nearAccountId: accountId,
611
- deviceNumber: rec.deviceNumber,
612
- clientNearPublicKey: rec.nearPublicKey,
613
- passkeyCredential: {
614
- id: rec.credential.id,
615
- rawId: rec.credential.rawId
616
- },
617
- encryptedVrfKeypair: rec.encryptedVrfKeypair,
618
- serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
619
- });
620
- const { syncAuthenticatorsContractCall } = await import("../rpcCalls.js");
621
- const authenticators = await syncAuthenticatorsContractCall(this.context.nearClient, this.context.configs.contractId, accountId);
622
- const mappedAuthenticators = authenticators.map(({ authenticator }) => ({
623
- credentialId: authenticator.credentialId,
624
- credentialPublicKey: authenticator.credentialPublicKey,
625
- transports: authenticator.transports,
626
- name: authenticator.name,
627
- registered: authenticator.registered.toISOString(),
628
- vrfPublicKey: authenticator.vrfPublicKeys?.[0] || "",
629
- deviceNumber: authenticator.deviceNumber
630
- }));
631
- await IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
632
- await IndexedDBManager.clientDB.setLastUser(accountId, rec.deviceNumber);
633
- } catch (syncErr) {
634
- console.warn("[EmailRecoveryFlow] Failed to sync authenticators after recovery:", syncErr);
635
- }
593
+ const txHash = txResult?.transaction?.hash || txResult?.transaction_hash;
594
+ if (txHash) this.emit({
595
+ step: 5,
596
+ phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
597
+ status: EmailRecoveryStatus.PROGRESS,
598
+ message: "Registration transaction confirmed",
599
+ data: {
600
+ accountId: rec.accountId,
601
+ nearPublicKey: rec.nearPublicKey,
602
+ transactionHash: txHash
636
603
  }
637
- } catch {}
638
- } catch (e) {
639
- const msg = String(e?.message || "");
640
- const err = this.emitError(5, msg || "Failed to broadcast email recovery registration transaction (insufficient funds or RPC error)");
641
- await this.options?.afterCall?.(false);
642
- throw err;
643
- }
644
- try {
645
- const txNonce = signedTx.transaction?.nonce;
646
- if (txNonce != null) await nonceManager.updateNonceFromBlockchain(this.context.nearClient, String(txNonce));
604
+ });
605
+ return txHash;
647
606
  } catch {}
607
+ } catch (e) {
608
+ const msg = String(e?.message || "");
609
+ await this.fail(5, msg || "Failed to broadcast email recovery registration transaction (insufficient funds or RPC error)");
610
+ }
611
+ return void 0;
612
+ }
613
+ mapAuthenticatorsFromContract(authenticators) {
614
+ return authenticators.map(({ authenticator }) => ({
615
+ credentialId: authenticator.credentialId,
616
+ credentialPublicKey: authenticator.credentialPublicKey,
617
+ transports: authenticator.transports,
618
+ name: authenticator.name,
619
+ registered: authenticator.registered.toISOString(),
620
+ vrfPublicKey: authenticator.vrfPublicKeys?.[0] || "",
621
+ deviceNumber: authenticator.deviceNumber
622
+ }));
623
+ }
624
+ async syncAuthenticatorsBestEffort(accountId) {
625
+ try {
626
+ const { syncAuthenticatorsContractCall } = await import("../rpcCalls.js");
627
+ const authenticators = await syncAuthenticatorsContractCall(this.context.nearClient, this.context.configs.contractId, accountId);
628
+ const mappedAuthenticators = this.mapAuthenticatorsFromContract(authenticators);
629
+ await IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
630
+ return true;
631
+ } catch (err) {
632
+ console.warn("[EmailRecoveryFlow] Failed to sync authenticators after recovery:", err);
633
+ return false;
634
+ }
635
+ }
636
+ async setLastUserBestEffort(accountId, deviceNumber) {
637
+ try {
638
+ await IndexedDBManager.clientDB.setLastUser(accountId, deviceNumber);
639
+ return true;
640
+ } catch (err) {
641
+ console.warn("[EmailRecoveryFlow] Failed to set last user after recovery:", err);
642
+ return false;
643
+ }
644
+ }
645
+ async updateNonceBestEffort(nonceManager, signedTx) {
646
+ try {
647
+ const txNonce = signedTx.transaction?.nonce;
648
+ if (txNonce != null) await nonceManager.updateNonceFromBlockchain(this.context.nearClient, String(txNonce));
649
+ } catch {}
650
+ }
651
+ async persistRecoveredUserData(rec, accountId) {
652
+ const { webAuthnManager } = this.context;
653
+ const payload = {
654
+ nearAccountId: accountId,
655
+ deviceNumber: rec.deviceNumber,
656
+ clientNearPublicKey: rec.nearPublicKey,
657
+ lastUpdated: Date.now(),
658
+ passkeyCredential: {
659
+ id: rec.credential.id,
660
+ rawId: rec.credential.rawId
661
+ },
662
+ encryptedVrfKeypair: {
663
+ encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
664
+ chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
665
+ },
666
+ serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
667
+ };
668
+ await webAuthnManager.storeUserData(payload);
669
+ }
670
+ /**
671
+ * Explicitly persist the authenticator from the recovery record into the local cache.
672
+ * This ensures the key is available immediately, bridging the gap before RPC sync sees it.
673
+ */
674
+ async persistAuthenticatorBestEffort(rec, accountId) {
675
+ try {
648
676
  const { webAuthnManager } = this.context;
649
- await webAuthnManager.storeUserData({
677
+ const attestationB64u = rec.credential.response.attestationObject;
678
+ const credentialPublicKey = await webAuthnManager.extractCosePublicKey(attestationB64u);
679
+ await webAuthnManager.storeAuthenticator({
650
680
  nearAccountId: accountId,
651
681
  deviceNumber: rec.deviceNumber,
652
- clientNearPublicKey: rec.nearPublicKey,
653
- lastUpdated: Date.now(),
654
- passkeyCredential: {
655
- id: rec.credential.id,
656
- rawId: rec.credential.rawId
657
- },
658
- encryptedVrfKeypair: {
659
- encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
660
- chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
661
- },
662
- serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
663
- });
664
- try {
665
- const attestationB64u = rec.credential.response.attestationObject;
666
- const credentialPublicKey = await webAuthnManager.extractCosePublicKey(attestationB64u);
667
- await webAuthnManager.storeAuthenticator({
668
- nearAccountId: accountId,
669
- deviceNumber: rec.deviceNumber,
670
- credentialId: rec.credential.rawId,
671
- credentialPublicKey,
672
- transports: ["internal"],
673
- name: `Device ${rec.deviceNumber} Passkey for ${rec.accountId.split(".")[0]}`,
674
- registered: (/* @__PURE__ */ new Date()).toISOString(),
675
- syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
676
- vrfPublicKey: rec.vrfPublicKey
677
- });
678
- } catch {}
679
- await this.attemptAutoLogin(rec);
680
- rec.status = "complete";
681
- await this.savePending(rec);
682
- await this.clearPending(rec.accountId, rec.nearPublicKey);
683
- this.phase = EmailRecoveryPhase.STEP_6_COMPLETE;
684
- this.emit({
685
- step: 6,
686
- phase: EmailRecoveryPhase.STEP_6_COMPLETE,
687
- status: EmailRecoveryStatus.SUCCESS,
688
- message: "Email recovery completed successfully",
689
- data: {
690
- accountId: rec.accountId,
691
- nearPublicKey: rec.nearPublicKey
692
- }
682
+ credentialId: rec.credential.rawId,
683
+ credentialPublicKey,
684
+ transports: ["internal"],
685
+ name: `Device ${rec.deviceNumber} Passkey for ${rec.accountId.split(".")[0]}`,
686
+ registered: (/* @__PURE__ */ new Date()).toISOString(),
687
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
688
+ vrfPublicKey: rec.vrfPublicKey
693
689
  });
690
+ console.log("[EmailRecoveryFlow] Locally persisted recovered authenticator for immediate use.");
694
691
  } catch (e) {
695
- const err = this.emitError(5, e?.message || "Email recovery finalization failed");
696
- await this.options?.afterCall?.(false);
697
- throw err;
692
+ console.error("[EmailRecoveryFlow] Failed to locally persist authenticator (critical for immediate export):", e);
698
693
  }
699
694
  }
700
- async attemptAutoLogin(rec) {
695
+ async markCompleteAndClearPending(rec) {
696
+ rec.status = "complete";
697
+ await this.savePending(rec);
698
+ await this.clearPending(rec.accountId, rec.nearPublicKey);
699
+ }
700
+ async assertVrfActiveForAccount(accountId, message) {
701
+ const vrfStatus = await this.context.webAuthnManager.checkVrfStatus();
702
+ const vrfActiveForAccount = vrfStatus.active && vrfStatus.nearAccountId && String(vrfStatus.nearAccountId) === String(accountId);
703
+ if (!vrfActiveForAccount) throw new Error(message);
704
+ }
705
+ async finalizeLocalLoginState(accountId, deviceNumber) {
706
+ const { webAuthnManager } = this.context;
707
+ await webAuthnManager.setLastUser(accountId, deviceNumber);
708
+ await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
701
709
  try {
702
- this.emit({
703
- step: 5,
704
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
705
- status: EmailRecoveryStatus.PROGRESS,
706
- message: "Attempting auto-login with recovered device...",
707
- data: { autoLogin: "progress" }
710
+ await getLoginSession(this.context, accountId);
711
+ } catch {}
712
+ }
713
+ async tryShamirUnlock(rec, accountId, deviceNumber) {
714
+ if (!rec.serverEncryptedVrfKeypair || !rec.serverEncryptedVrfKeypair.serverKeyId || !this.context.configs.vrfWorkerConfigs?.shamir3pass?.relayServerUrl) return false;
715
+ try {
716
+ const { webAuthnManager } = this.context;
717
+ const unlockResult = await webAuthnManager.shamir3PassDecryptVrfKeypair({
718
+ nearAccountId: accountId,
719
+ kek_s_b64u: rec.serverEncryptedVrfKeypair.kek_s_b64u,
720
+ ciphertextVrfB64u: rec.serverEncryptedVrfKeypair.ciphertextVrfB64u,
721
+ serverKeyId: rec.serverEncryptedVrfKeypair.serverKeyId
708
722
  });
723
+ if (!unlockResult.success) return false;
724
+ await this.assertVrfActiveForAccount(accountId, "VRF session inactive after Shamir3Pass unlock");
725
+ await this.finalizeLocalLoginState(accountId, deviceNumber);
726
+ return true;
727
+ } catch (err) {
728
+ console.warn("[EmailRecoveryFlow] Shamir 3-pass unlock failed, falling back to TouchID", err);
729
+ return false;
730
+ }
731
+ }
732
+ async tryTouchIdUnlock(rec, accountId, deviceNumber) {
733
+ try {
709
734
  const { webAuthnManager } = this.context;
710
- const accountId = toAccountId(rec.accountId);
711
- const deviceNumber = parseDeviceNumber(rec.deviceNumber, { min: 1 });
712
- if (deviceNumber === null) throw new Error(`Invalid deviceNumber for auto-login: ${String(rec.deviceNumber)}`);
713
- if (rec.serverEncryptedVrfKeypair && rec.serverEncryptedVrfKeypair.serverKeyId && this.context.configs.vrfWorkerConfigs?.shamir3pass?.relayServerUrl) try {
714
- const unlockResult = await webAuthnManager.shamir3PassDecryptVrfKeypair({
715
- nearAccountId: accountId,
716
- kek_s_b64u: rec.serverEncryptedVrfKeypair.kek_s_b64u,
717
- ciphertextVrfB64u: rec.serverEncryptedVrfKeypair.ciphertextVrfB64u,
718
- serverKeyId: rec.serverEncryptedVrfKeypair.serverKeyId
719
- });
720
- if (unlockResult.success) {
721
- const vrfStatus$1 = await webAuthnManager.checkVrfStatus();
722
- const vrfActiveForAccount$1 = vrfStatus$1.active && vrfStatus$1.nearAccountId && String(vrfStatus$1.nearAccountId) === String(accountId);
723
- if (!vrfActiveForAccount$1) throw new Error("VRF session inactive after Shamir3Pass unlock");
724
- await webAuthnManager.setLastUser(accountId, deviceNumber);
725
- await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
726
- try {
727
- await getLoginSession(this.context, accountId);
728
- } catch {}
729
- this.emit({
730
- step: 5,
731
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
732
- status: EmailRecoveryStatus.SUCCESS,
733
- message: `Welcome ${accountId}`,
734
- data: { autoLogin: "success" }
735
- });
736
- return;
737
- }
738
- } catch (err) {
739
- console.warn("[EmailRecoveryFlow] Shamir 3-pass unlock failed, falling back to TouchID", err);
740
- }
741
735
  const authChallenge = createRandomVRFChallenge();
742
736
  const storedCredentialId = String(rec.credential?.rawId || rec.credential?.id || "").trim();
743
737
  const credentialIds = storedCredentialId ? [storedCredentialId] : [];
@@ -747,43 +741,104 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
747
741
  challenge: authChallenge,
748
742
  credentialIds: credentialIds.length > 0 ? credentialIds : authenticators.map((a) => a.credentialId)
749
743
  });
750
- if (storedCredentialId && authCredential.rawId !== storedCredentialId) throw new Error("Wrong passkey selected during recovery auto-login; please use the newly recovered passkey.");
744
+ if (storedCredentialId && authCredential.rawId !== storedCredentialId) return {
745
+ success: false,
746
+ reason: "Wrong passkey selected during recovery auto-login; please use the newly recovered passkey."
747
+ };
751
748
  const vrfUnlockResult = await webAuthnManager.unlockVRFKeypair({
752
749
  nearAccountId: accountId,
753
750
  encryptedVrfKeypair: rec.encryptedVrfKeypair,
754
751
  credential: authCredential
755
752
  });
756
- if (!vrfUnlockResult.success) throw new Error(vrfUnlockResult.error || "VRF unlock failed during auto-login");
757
- const vrfStatus = await webAuthnManager.checkVrfStatus();
758
- const vrfActiveForAccount = vrfStatus.active && vrfStatus.nearAccountId && String(vrfStatus.nearAccountId) === String(accountId);
759
- if (!vrfActiveForAccount) throw new Error("VRF session inactive after TouchID unlock");
760
- await webAuthnManager.setLastUser(accountId, deviceNumber);
761
- await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
762
- try {
763
- await getLoginSession(this.context, accountId);
764
- } catch {}
765
- this.emit({
766
- step: 5,
767
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
768
- status: EmailRecoveryStatus.SUCCESS,
769
- message: `Welcome ${accountId}`,
770
- data: { autoLogin: "success" }
771
- });
753
+ if (!vrfUnlockResult.success) return {
754
+ success: false,
755
+ reason: vrfUnlockResult.error || "VRF unlock failed during auto-login"
756
+ };
757
+ await this.assertVrfActiveForAccount(accountId, "VRF session inactive after TouchID unlock");
758
+ await this.finalizeLocalLoginState(accountId, deviceNumber);
759
+ return { success: true };
772
760
  } catch (err) {
773
- console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err);
774
- try {
775
- await this.context.webAuthnManager.clearVrfSession();
776
- } catch {}
761
+ return {
762
+ success: false,
763
+ reason: err?.message || String(err)
764
+ };
765
+ }
766
+ }
767
+ async handleAutoLoginFailure(reason, err) {
768
+ console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err ?? reason);
769
+ try {
770
+ await this.context.webAuthnManager.clearVrfSession();
771
+ } catch {}
772
+ return {
773
+ success: false,
774
+ reason
775
+ };
776
+ }
777
+ async finalizeRegistration(rec) {
778
+ this.phase = EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
779
+ this.emit({
780
+ step: 5,
781
+ phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
782
+ status: EmailRecoveryStatus.PROGRESS,
783
+ message: "Finalizing email recovery registration...",
784
+ data: {
785
+ accountId: rec.accountId,
786
+ nearPublicKey: rec.nearPublicKey
787
+ }
788
+ });
789
+ try {
790
+ const { nonceManager, accountId } = this.initializeNonceManager(rec);
791
+ const signedTx = await this.signRegistrationTx(rec, accountId);
792
+ const txHash = await this.broadcastRegistrationTxAndWaitFinal(rec, signedTx);
793
+ if (!txHash) console.warn("[EmailRecoveryFlow] Registration transaction confirmed without hash; continuing local persistence");
794
+ await this.persistRecoveredUserData(rec, accountId);
795
+ await this.syncAuthenticatorsBestEffort(accountId);
796
+ await this.persistAuthenticatorBestEffort(rec, accountId);
797
+ await this.setLastUserBestEffort(accountId, rec.deviceNumber);
798
+ await this.updateNonceBestEffort(nonceManager, signedTx);
799
+ this.emitAutoLoginEvent(EmailRecoveryStatus.PROGRESS, "Attempting auto-login with recovered device...", { autoLogin: "progress" });
800
+ const autoLoginResult = await this.attemptAutoLogin(rec);
801
+ if (autoLoginResult.success) this.emitAutoLoginEvent(EmailRecoveryStatus.SUCCESS, `Welcome ${accountId}`, { autoLogin: "success" });
802
+ else this.emitAutoLoginEvent(EmailRecoveryStatus.ERROR, "Auto-login failed; please log in manually on this device.", {
803
+ error: autoLoginResult.reason,
804
+ autoLogin: "error"
805
+ });
806
+ await this.markCompleteAndClearPending(rec);
807
+ this.phase = EmailRecoveryPhase.STEP_6_COMPLETE;
777
808
  this.emit({
778
- step: 5,
779
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
780
- status: EmailRecoveryStatus.ERROR,
781
- message: "Auto-login failed; please log in manually on this device.",
809
+ step: 6,
810
+ phase: EmailRecoveryPhase.STEP_6_COMPLETE,
811
+ status: EmailRecoveryStatus.SUCCESS,
812
+ message: "Email recovery completed successfully",
782
813
  data: {
783
- error: err?.message || String(err),
784
- autoLogin: "error"
814
+ accountId: rec.accountId,
815
+ nearPublicKey: rec.nearPublicKey
785
816
  }
786
817
  });
818
+ } catch (e) {
819
+ const err = this.emitError(5, e?.message || "Email recovery finalization failed");
820
+ await this.options?.afterCall?.(false);
821
+ throw err;
822
+ }
823
+ }
824
+ async attemptAutoLogin(rec) {
825
+ try {
826
+ const accountId = toAccountId(rec.accountId);
827
+ const deviceNumber = parseDeviceNumber(rec.deviceNumber, { min: 1 });
828
+ if (deviceNumber === null) return this.handleAutoLoginFailure(`Invalid deviceNumber for auto-login: ${String(rec.deviceNumber)}`);
829
+ const shamirUnlocked = await this.tryShamirUnlock(rec, accountId, deviceNumber);
830
+ if (shamirUnlocked) return {
831
+ success: true,
832
+ method: "shamir"
833
+ };
834
+ const touchIdResult = await this.tryTouchIdUnlock(rec, accountId, deviceNumber);
835
+ if (touchIdResult.success) return {
836
+ success: true,
837
+ method: "touchid"
838
+ };
839
+ return this.handleAutoLoginFailure(touchIdResult.reason || "Auto-login failed");
840
+ } catch (err) {
841
+ return this.handleAutoLoginFailure(err?.message || String(err), err);
787
842
  }
788
843
  }
789
844
  };