@tatchi-xyz/sdk 0.17.0 → 0.18.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 (122) 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 +519 -448
  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/rpcCalls.js +8 -0
  16. package/dist/cjs/core/rpcCalls.js.map +1 -1
  17. package/dist/cjs/index.js +6 -2
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/react/components/AccountMenuButton/{LinkedDevicesModal-B6api181.css → LinkedDevicesModal-CSSowiHP.css} +1 -1
  20. package/dist/{esm/react/components/AccountMenuButton/LinkedDevicesModal-B6api181.css.map → cjs/react/components/AccountMenuButton/LinkedDevicesModal-CSSowiHP.css.map} +1 -1
  21. package/dist/cjs/react/components/AccountMenuButton/{ProfileDropdown-B-DrG_u5.css → ProfileDropdown-CEPMZ1gY.css} +1 -1
  22. package/dist/{esm/react/components/AccountMenuButton/ProfileDropdown-B-DrG_u5.css.map → cjs/react/components/AccountMenuButton/ProfileDropdown-CEPMZ1gY.css.map} +1 -1
  23. package/dist/cjs/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css → Web3AuthProfileButton-DopOg7Xc.css} +1 -1
  24. package/dist/cjs/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css.map → Web3AuthProfileButton-DopOg7Xc.css.map} +1 -1
  25. package/dist/cjs/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css → TouchIcon-BQWentvJ.css} +1 -1
  26. package/dist/cjs/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css.map → TouchIcon-BQWentvJ.css.map} +1 -1
  27. package/dist/cjs/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css → PasskeyAuthMenu-DwrzWMYx.css} +1 -1
  28. package/dist/cjs/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css.map → PasskeyAuthMenu-DwrzWMYx.css.map} +1 -1
  29. package/dist/cjs/react/components/{ShowQRCode-nZhZSaba.css → ShowQRCode-CCN4h6Uv.css} +1 -1
  30. package/dist/cjs/react/components/{ShowQRCode-nZhZSaba.css.map → ShowQRCode-CCN4h6Uv.css.map} +1 -1
  31. package/dist/cjs/react/hooks/usePreconnectWalletAssets.js +27 -32
  32. package/dist/cjs/react/hooks/usePreconnectWalletAssets.js.map +1 -1
  33. package/dist/cjs/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js +69 -0
  34. package/dist/cjs/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  35. package/dist/cjs/react/sdk/src/core/EmailRecovery/index.js +32 -20
  36. package/dist/cjs/react/sdk/src/core/EmailRecovery/index.js.map +1 -1
  37. package/dist/cjs/react/sdk/src/core/TatchiPasskey/emailRecovery.js +519 -448
  38. package/dist/cjs/react/sdk/src/core/TatchiPasskey/emailRecovery.js.map +1 -1
  39. package/dist/cjs/react/sdk/src/core/TatchiPasskey/index.js +1 -0
  40. package/dist/cjs/react/sdk/src/core/TatchiPasskey/index.js.map +1 -1
  41. package/dist/cjs/react/sdk/src/core/TatchiPasskey/relay.js +23 -1
  42. package/dist/cjs/react/sdk/src/core/TatchiPasskey/relay.js.map +1 -1
  43. package/dist/cjs/react/sdk/src/core/WalletIframe/client/IframeTransport.js +0 -7
  44. package/dist/cjs/react/sdk/src/core/WalletIframe/client/IframeTransport.js.map +1 -1
  45. package/dist/cjs/react/sdk/src/core/WalletIframe/client/router.js +6 -2
  46. package/dist/cjs/react/sdk/src/core/WalletIframe/client/router.js.map +1 -1
  47. package/dist/cjs/react/sdk/src/core/rpcCalls.js +8 -0
  48. package/dist/cjs/react/sdk/src/core/rpcCalls.js.map +1 -1
  49. package/dist/esm/core/EmailRecovery/emailRecoveryPendingStore.js +63 -0
  50. package/dist/esm/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  51. package/dist/esm/core/EmailRecovery/index.js +28 -21
  52. package/dist/esm/core/EmailRecovery/index.js.map +1 -1
  53. package/dist/esm/core/TatchiPasskey/emailRecovery.js +519 -448
  54. package/dist/esm/core/TatchiPasskey/emailRecovery.js.map +1 -1
  55. package/dist/esm/core/TatchiPasskey/index.js +2 -1
  56. package/dist/esm/core/TatchiPasskey/index.js.map +1 -1
  57. package/dist/esm/core/TatchiPasskey/relay.js +23 -1
  58. package/dist/esm/core/TatchiPasskey/relay.js.map +1 -1
  59. package/dist/esm/core/WalletIframe/client/IframeTransport.js +0 -7
  60. package/dist/esm/core/WalletIframe/client/IframeTransport.js.map +1 -1
  61. package/dist/esm/core/WalletIframe/client/router.js +7 -3
  62. package/dist/esm/core/WalletIframe/client/router.js.map +1 -1
  63. package/dist/esm/core/rpcCalls.js +8 -1
  64. package/dist/esm/core/rpcCalls.js.map +1 -1
  65. package/dist/esm/index.js +4 -1
  66. package/dist/esm/index.js.map +1 -1
  67. package/dist/esm/react/components/AccountMenuButton/{LinkedDevicesModal-B6api181.css → LinkedDevicesModal-CSSowiHP.css} +1 -1
  68. package/dist/{cjs/react/components/AccountMenuButton/LinkedDevicesModal-B6api181.css.map → esm/react/components/AccountMenuButton/LinkedDevicesModal-CSSowiHP.css.map} +1 -1
  69. package/dist/esm/react/components/AccountMenuButton/{ProfileDropdown-B-DrG_u5.css → ProfileDropdown-CEPMZ1gY.css} +1 -1
  70. package/dist/{cjs/react/components/AccountMenuButton/ProfileDropdown-B-DrG_u5.css.map → esm/react/components/AccountMenuButton/ProfileDropdown-CEPMZ1gY.css.map} +1 -1
  71. package/dist/esm/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css → Web3AuthProfileButton-DopOg7Xc.css} +1 -1
  72. package/dist/esm/react/components/AccountMenuButton/{Web3AuthProfileButton-BnZDUeCL.css.map → Web3AuthProfileButton-DopOg7Xc.css.map} +1 -1
  73. package/dist/esm/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css → TouchIcon-BQWentvJ.css} +1 -1
  74. package/dist/esm/react/components/AccountMenuButton/icons/{TouchIcon-CAGCi8MY.css.map → TouchIcon-BQWentvJ.css.map} +1 -1
  75. package/dist/esm/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css → PasskeyAuthMenu-DwrzWMYx.css} +1 -1
  76. package/dist/esm/react/components/PasskeyAuthMenu/{PasskeyAuthMenu-CNNxVj4L.css.map → PasskeyAuthMenu-DwrzWMYx.css.map} +1 -1
  77. package/dist/esm/react/components/{ShowQRCode-nZhZSaba.css → ShowQRCode-CCN4h6Uv.css} +1 -1
  78. package/dist/esm/react/components/{ShowQRCode-nZhZSaba.css.map → ShowQRCode-CCN4h6Uv.css.map} +1 -1
  79. package/dist/esm/react/hooks/usePreconnectWalletAssets.js +27 -32
  80. package/dist/esm/react/hooks/usePreconnectWalletAssets.js.map +1 -1
  81. package/dist/esm/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js +63 -0
  82. package/dist/esm/react/sdk/src/core/EmailRecovery/emailRecoveryPendingStore.js.map +1 -0
  83. package/dist/esm/react/sdk/src/core/EmailRecovery/index.js +28 -21
  84. package/dist/esm/react/sdk/src/core/EmailRecovery/index.js.map +1 -1
  85. package/dist/esm/react/sdk/src/core/TatchiPasskey/emailRecovery.js +519 -448
  86. package/dist/esm/react/sdk/src/core/TatchiPasskey/emailRecovery.js.map +1 -1
  87. package/dist/esm/react/sdk/src/core/TatchiPasskey/index.js +2 -1
  88. package/dist/esm/react/sdk/src/core/TatchiPasskey/index.js.map +1 -1
  89. package/dist/esm/react/sdk/src/core/TatchiPasskey/relay.js +23 -1
  90. package/dist/esm/react/sdk/src/core/TatchiPasskey/relay.js.map +1 -1
  91. package/dist/esm/react/sdk/src/core/WalletIframe/client/IframeTransport.js +0 -7
  92. package/dist/esm/react/sdk/src/core/WalletIframe/client/IframeTransport.js.map +1 -1
  93. package/dist/esm/react/sdk/src/core/WalletIframe/client/router.js +7 -3
  94. package/dist/esm/react/sdk/src/core/WalletIframe/client/router.js.map +1 -1
  95. package/dist/esm/react/sdk/src/core/rpcCalls.js +8 -1
  96. package/dist/esm/react/sdk/src/core/rpcCalls.js.map +1 -1
  97. package/dist/esm/sdk/offline-export-app.js.map +1 -1
  98. package/dist/esm/sdk/{router-BLFegW7J.js → router-DuGYOd3G.js} +6 -9
  99. package/dist/esm/sdk/{rpcCalls-DEv9x5-f.js → rpcCalls-BQrJMTdg.js} +2 -2
  100. package/dist/esm/sdk/{rpcCalls-OhgEeFig.js → rpcCalls-YVeUVMk2.js} +8 -1
  101. package/dist/esm/sdk/wallet-iframe-host.js +624 -471
  102. package/dist/esm/wasm_vrf_worker/pkg/wasm_vrf_worker_bg.wasm +0 -0
  103. package/dist/types/src/core/EmailRecovery/emailRecoveryPendingStore.d.ts +25 -0
  104. package/dist/types/src/core/EmailRecovery/emailRecoveryPendingStore.d.ts.map +1 -0
  105. package/dist/types/src/core/EmailRecovery/index.d.ts +1 -0
  106. package/dist/types/src/core/EmailRecovery/index.d.ts.map +1 -1
  107. package/dist/types/src/core/TatchiPasskey/emailRecovery.d.ts +35 -6
  108. package/dist/types/src/core/TatchiPasskey/emailRecovery.d.ts.map +1 -1
  109. package/dist/types/src/core/TatchiPasskey/index.d.ts +2 -2
  110. package/dist/types/src/core/TatchiPasskey/index.d.ts.map +1 -1
  111. package/dist/types/src/core/TatchiPasskey/relay.d.ts +2 -1
  112. package/dist/types/src/core/TatchiPasskey/relay.d.ts.map +1 -1
  113. package/dist/types/src/core/WalletIframe/client/IframeTransport.d.ts.map +1 -1
  114. package/dist/types/src/core/WalletIframe/client/router.d.ts +3 -3
  115. package/dist/types/src/core/WalletIframe/client/router.d.ts.map +1 -1
  116. package/dist/types/src/core/rpcCalls.d.ts +9 -0
  117. package/dist/types/src/core/rpcCalls.d.ts.map +1 -1
  118. package/dist/types/src/index.d.ts +1 -0
  119. package/dist/types/src/index.d.ts.map +1 -1
  120. package/dist/types/src/react/hooks/usePreconnectWalletAssets.d.ts.map +1 -1
  121. package/dist/workers/wasm_vrf_worker_bg.wasm +0 -0
  122. package/package.json +1 -1
@@ -7,6 +7,8 @@ const require_sdkSentEvents = require('../types/sdkSentEvents.js');
7
7
  const require_rpc = require('../types/rpc.js');
8
8
  const require_getDeviceNumber = require('../WebAuthnManager/SignerWorkerManager/getDeviceNumber.js');
9
9
  const require_login = require('./login.js');
10
+ const require_emailRecoveryPendingStore = require('../EmailRecovery/emailRecoveryPendingStore.js');
11
+ const require_index$1 = require('../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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
52
54
  require_rpc.init_rpc();
53
55
  require_getDeviceNumber.init_getDeviceNumber();
54
56
  require_login.init_login();
57
+ require_index$1.init_EmailRecovery();
55
58
  EmailRecoveryFlow = class {
56
59
  context;
57
60
  options;
61
+ pendingStore;
58
62
  pending = null;
59
63
  phase = require_sdkSentEvents.EmailRecoveryPhase.STEP_1_PREPARATION;
60
64
  pollingTimer;
@@ -65,6 +69,7 @@ var init_emailRecovery = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
65
69
  constructor(context, options) {
66
70
  this.context = context;
67
71
  this.options = options;
72
+ this.pendingStore = options?.pendingStore ?? new require_emailRecoveryPendingStore.EmailRecoveryPendingStore({ getPendingTtlMs: () => this.getConfig().pendingTtlMs });
68
73
  }
69
74
  setOptions(options) {
70
75
  if (!options) return;
@@ -72,6 +77,7 @@ var init_emailRecovery = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
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 = require_validation.validateNearAccountId(accountId);
106
+ if (!validation.valid) await this.fail(step, `Invalid NEAR account ID: ${validation.error}`);
107
+ return require_accountIds.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);
98
129
  }
99
- getPendingRecordKey(accountId, nearPublicKey) {
100
- return `${this.getPendingIndexKey(accountId)}:${nearPublicKey}`;
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);
101
138
  }
102
- async checkVerificationStatus(rec) {
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 Promise.resolve().then(() => require("../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 = require_sdkSentEvents.EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
203
+ this.emit({
204
+ step: 3,
205
+ phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
206
+ status: require_sdkSentEvents.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
+ });
216
+ }
217
+ emitAutoLoginEvent(status, message, data) {
218
+ this.emit({
219
+ step: 5,
220
+ phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
221
+ status,
222
+ message,
223
+ data
224
+ });
225
+ }
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 Promise.resolve().then(() => require("../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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
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 require_index.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 require_index.IndexedDBManager.clientDB.getAppState(recordKey);
154
- const shouldClearIndex = indexedNearPublicKey === resolvedNearPublicKey;
155
- if (!record) {
156
- if (shouldClearIndex) await require_index.IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
157
- return null;
158
- }
159
- if (Date.now() - record.createdAt > pendingTtlMs) {
160
- await require_index.IndexedDBManager.clientDB.setAppState(recordKey, void 0).catch(() => {});
161
- if (shouldClearIndex) await require_index.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 require_index.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 require_index.IndexedDBManager.clientDB.setAppState(key, rec);
170
- await require_index.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 require_index.IndexedDBManager.clientDB.getAppState(indexKey).catch(() => void 0);
176
- const resolvedNearPublicKey = nearPublicKey || idx || "";
177
- if (resolvedNearPublicKey) await require_index.IndexedDBManager.clientDB.setAppState(this.getPendingRecordKey(accountId, resolvedNearPublicKey), void 0).catch(() => {});
178
- if (!nearPublicKey || idx === nearPublicKey) await require_index.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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
189
345
  const { accountId, nearPublicKey } = args;
190
346
  this.cancelled = false;
191
347
  this.error = void 0;
192
- const validation = require_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 = require_accountIds.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 = require_sdkSentEvents.EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
221
- this.emit({
222
- step: 3,
223
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
224
- status: require_sdkSentEvents.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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
245
367
  status: require_sdkSentEvents.EmailRecoveryStatus.PROGRESS,
246
368
  message: "Preparing email recovery..."
247
369
  });
248
- const validation = require_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 = require_accountIds.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 Promise.resolve().then(() => require("../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 = require_sdkSentEvents.EmailRecoveryPhase.STEP_2_TOUCH_ID_REGISTRATION;
292
375
  this.emit({
293
376
  step: 2,
@@ -296,69 +379,24 @@ var init_emailRecovery = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
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 = require_sdkSentEvents.EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
349
- this.emit({
350
- step: 3,
351
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
352
- status: require_sdkSentEvents.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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
386
424
  const { accountId, nearPublicKey } = args;
387
425
  this.cancelled = false;
388
426
  this.error = void 0;
389
- const validation = require_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 = require_accountIds.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 = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
447
468
  const { accountId, nearPublicKey } = args;
448
469
  this.cancelled = false;
449
470
  this.error = void 0;
450
- const validation = require_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 = require_accountIds.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: require_sdkSentEvents.EmailRecoveryPhase.RESUMED_FROM_PENDING,
@@ -499,245 +508,242 @@ var init_emailRecovery = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
499
508
  }
500
509
  this.phase = require_sdkSentEvents.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: require_sdkSentEvents.EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT,
522
+ status: require_sdkSentEvents.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: require_sdkSentEvents.EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT,
519
- status: require_sdkSentEvents.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 = require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
560
- this.emit({
561
- step: 5,
562
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
563
- status: require_sdkSentEvents.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 = require_accountIds.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, require_rpc.DEFAULT_WAIT_STATUS.linkDeviceRegistration);
592
592
  try {
593
- const txResult = await this.context.nearClient.sendTransaction(signedTx, require_rpc.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: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
600
- status: require_sdkSentEvents.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 require_index.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 Promise.resolve().then(() => require("../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 require_index.IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
632
- await require_index.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: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
597
+ status: require_sdkSentEvents.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 {}
648
- const { webAuthnManager } = this.context;
649
- await webAuthnManager.storeUserData({
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
+ async persistRecoveredUserRecordBestEffort(rec, accountId) {
614
+ try {
615
+ await require_index.IndexedDBManager.clientDB.storeWebAuthnUserData({
650
616
  nearAccountId: accountId,
651
617
  deviceNumber: rec.deviceNumber,
652
618
  clientNearPublicKey: rec.nearPublicKey,
653
- lastUpdated: Date.now(),
654
619
  passkeyCredential: {
655
620
  id: rec.credential.id,
656
621
  rawId: rec.credential.rawId
657
622
  },
658
- encryptedVrfKeypair: {
659
- encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
660
- chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
661
- },
623
+ encryptedVrfKeypair: rec.encryptedVrfKeypair,
662
624
  serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
663
625
  });
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 = require_sdkSentEvents.EmailRecoveryPhase.STEP_6_COMPLETE;
684
- this.emit({
685
- step: 6,
686
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_6_COMPLETE,
687
- status: require_sdkSentEvents.EmailRecoveryStatus.SUCCESS,
688
- message: "Email recovery completed successfully",
689
- data: {
690
- accountId: rec.accountId,
691
- nearPublicKey: rec.nearPublicKey
692
- }
693
- });
694
- } catch (e) {
695
- const err = this.emitError(5, e?.message || "Email recovery finalization failed");
696
- await this.options?.afterCall?.(false);
697
- throw err;
626
+ return true;
627
+ } catch (err) {
628
+ console.warn("[EmailRecoveryFlow] Failed to store recovery user record:", err);
629
+ return false;
698
630
  }
699
631
  }
700
- async attemptAutoLogin(rec) {
632
+ mapAuthenticatorsFromContract(authenticators) {
633
+ return authenticators.map(({ authenticator }) => ({
634
+ credentialId: authenticator.credentialId,
635
+ credentialPublicKey: authenticator.credentialPublicKey,
636
+ transports: authenticator.transports,
637
+ name: authenticator.name,
638
+ registered: authenticator.registered.toISOString(),
639
+ vrfPublicKey: authenticator.vrfPublicKeys?.[0] || "",
640
+ deviceNumber: authenticator.deviceNumber
641
+ }));
642
+ }
643
+ async syncAuthenticatorsBestEffort(accountId) {
701
644
  try {
702
- this.emit({
703
- step: 5,
704
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
705
- status: require_sdkSentEvents.EmailRecoveryStatus.PROGRESS,
706
- message: "Attempting auto-login with recovered device...",
707
- data: { autoLogin: "progress" }
645
+ const { syncAuthenticatorsContractCall } = await Promise.resolve().then(() => require("../rpcCalls.js"));
646
+ const authenticators = await syncAuthenticatorsContractCall(this.context.nearClient, this.context.configs.contractId, accountId);
647
+ const mappedAuthenticators = this.mapAuthenticatorsFromContract(authenticators);
648
+ await require_index.IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
649
+ return true;
650
+ } catch (err) {
651
+ console.warn("[EmailRecoveryFlow] Failed to sync authenticators after recovery:", err);
652
+ return false;
653
+ }
654
+ }
655
+ async setLastUserBestEffort(accountId, deviceNumber) {
656
+ try {
657
+ await require_index.IndexedDBManager.clientDB.setLastUser(accountId, deviceNumber);
658
+ return true;
659
+ } catch (err) {
660
+ console.warn("[EmailRecoveryFlow] Failed to set last user after recovery:", err);
661
+ return false;
662
+ }
663
+ }
664
+ async updateNonceBestEffort(nonceManager, signedTx) {
665
+ try {
666
+ const txNonce = signedTx.transaction?.nonce;
667
+ if (txNonce != null) await nonceManager.updateNonceFromBlockchain(this.context.nearClient, String(txNonce));
668
+ } catch {}
669
+ }
670
+ async persistRecoveredUserData(rec, accountId) {
671
+ const { webAuthnManager } = this.context;
672
+ const payload = {
673
+ nearAccountId: accountId,
674
+ deviceNumber: rec.deviceNumber,
675
+ clientNearPublicKey: rec.nearPublicKey,
676
+ lastUpdated: Date.now(),
677
+ passkeyCredential: {
678
+ id: rec.credential.id,
679
+ rawId: rec.credential.rawId
680
+ },
681
+ encryptedVrfKeypair: {
682
+ encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
683
+ chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
684
+ },
685
+ serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
686
+ };
687
+ await webAuthnManager.storeUserData(payload);
688
+ }
689
+ async persistAuthenticatorBestEffort(rec, accountId) {
690
+ try {
691
+ const { webAuthnManager } = this.context;
692
+ const attestationB64u = rec.credential.response.attestationObject;
693
+ const credentialPublicKey = await webAuthnManager.extractCosePublicKey(attestationB64u);
694
+ await webAuthnManager.storeAuthenticator({
695
+ nearAccountId: accountId,
696
+ deviceNumber: rec.deviceNumber,
697
+ credentialId: rec.credential.rawId,
698
+ credentialPublicKey,
699
+ transports: ["internal"],
700
+ name: `Device ${rec.deviceNumber} Passkey for ${rec.accountId.split(".")[0]}`,
701
+ registered: (/* @__PURE__ */ new Date()).toISOString(),
702
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
703
+ vrfPublicKey: rec.vrfPublicKey
708
704
  });
705
+ } catch {}
706
+ }
707
+ async markCompleteAndClearPending(rec) {
708
+ rec.status = "complete";
709
+ await this.savePending(rec);
710
+ await this.clearPending(rec.accountId, rec.nearPublicKey);
711
+ }
712
+ async assertVrfActiveForAccount(accountId, message) {
713
+ const vrfStatus = await this.context.webAuthnManager.checkVrfStatus();
714
+ const vrfActiveForAccount = vrfStatus.active && vrfStatus.nearAccountId && String(vrfStatus.nearAccountId) === String(accountId);
715
+ if (!vrfActiveForAccount) throw new Error(message);
716
+ }
717
+ async finalizeLocalLoginState(accountId, deviceNumber) {
718
+ const { webAuthnManager } = this.context;
719
+ await webAuthnManager.setLastUser(accountId, deviceNumber);
720
+ await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
721
+ try {
722
+ await require_login.getLoginSession(this.context, accountId);
723
+ } catch {}
724
+ }
725
+ async tryShamirUnlock(rec, accountId, deviceNumber) {
726
+ if (!rec.serverEncryptedVrfKeypair || !rec.serverEncryptedVrfKeypair.serverKeyId || !this.context.configs.vrfWorkerConfigs?.shamir3pass?.relayServerUrl) return false;
727
+ try {
728
+ const { webAuthnManager } = this.context;
729
+ const unlockResult = await webAuthnManager.shamir3PassDecryptVrfKeypair({
730
+ nearAccountId: accountId,
731
+ kek_s_b64u: rec.serverEncryptedVrfKeypair.kek_s_b64u,
732
+ ciphertextVrfB64u: rec.serverEncryptedVrfKeypair.ciphertextVrfB64u,
733
+ serverKeyId: rec.serverEncryptedVrfKeypair.serverKeyId
734
+ });
735
+ if (!unlockResult.success) return false;
736
+ await this.assertVrfActiveForAccount(accountId, "VRF session inactive after Shamir3Pass unlock");
737
+ await this.finalizeLocalLoginState(accountId, deviceNumber);
738
+ return true;
739
+ } catch (err) {
740
+ console.warn("[EmailRecoveryFlow] Shamir 3-pass unlock failed, falling back to TouchID", err);
741
+ return false;
742
+ }
743
+ }
744
+ async tryTouchIdUnlock(rec, accountId, deviceNumber) {
745
+ try {
709
746
  const { webAuthnManager } = this.context;
710
- const accountId = require_accountIds.toAccountId(rec.accountId);
711
- const deviceNumber = require_getDeviceNumber.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 require_login.getLoginSession(this.context, accountId);
728
- } catch {}
729
- this.emit({
730
- step: 5,
731
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
732
- status: require_sdkSentEvents.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
747
  const authChallenge = require_vrf_worker.createRandomVRFChallenge();
742
748
  const storedCredentialId = String(rec.credential?.rawId || rec.credential?.id || "").trim();
743
749
  const credentialIds = storedCredentialId ? [storedCredentialId] : [];
@@ -747,43 +753,108 @@ var init_emailRecovery = require_rolldown_runtime.__esm({ "src/core/TatchiPasske
747
753
  challenge: authChallenge,
748
754
  credentialIds: credentialIds.length > 0 ? credentialIds : authenticators.map((a) => a.credentialId)
749
755
  });
750
- if (storedCredentialId && authCredential.rawId !== storedCredentialId) throw new Error("Wrong passkey selected during recovery auto-login; please use the newly recovered passkey.");
756
+ if (storedCredentialId && authCredential.rawId !== storedCredentialId) return {
757
+ success: false,
758
+ reason: "Wrong passkey selected during recovery auto-login; please use the newly recovered passkey."
759
+ };
751
760
  const vrfUnlockResult = await webAuthnManager.unlockVRFKeypair({
752
761
  nearAccountId: accountId,
753
762
  encryptedVrfKeypair: rec.encryptedVrfKeypair,
754
763
  credential: authCredential
755
764
  });
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 require_login.getLoginSession(this.context, accountId);
764
- } catch {}
765
- this.emit({
766
- step: 5,
767
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
768
- status: require_sdkSentEvents.EmailRecoveryStatus.SUCCESS,
769
- message: `Welcome ${accountId}`,
770
- data: { autoLogin: "success" }
771
- });
765
+ if (!vrfUnlockResult.success) return {
766
+ success: false,
767
+ reason: vrfUnlockResult.error || "VRF unlock failed during auto-login"
768
+ };
769
+ await this.assertVrfActiveForAccount(accountId, "VRF session inactive after TouchID unlock");
770
+ await this.finalizeLocalLoginState(accountId, deviceNumber);
771
+ return { success: true };
772
772
  } catch (err) {
773
- console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err);
774
- try {
775
- await this.context.webAuthnManager.clearVrfSession();
776
- } catch {}
773
+ return {
774
+ success: false,
775
+ reason: err?.message || String(err)
776
+ };
777
+ }
778
+ }
779
+ async handleAutoLoginFailure(reason, err) {
780
+ console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err ?? reason);
781
+ try {
782
+ await this.context.webAuthnManager.clearVrfSession();
783
+ } catch {}
784
+ return {
785
+ success: false,
786
+ reason
787
+ };
788
+ }
789
+ async finalizeRegistration(rec) {
790
+ this.phase = require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
791
+ this.emit({
792
+ step: 5,
793
+ phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
794
+ status: require_sdkSentEvents.EmailRecoveryStatus.PROGRESS,
795
+ message: "Finalizing email recovery registration...",
796
+ data: {
797
+ accountId: rec.accountId,
798
+ nearPublicKey: rec.nearPublicKey
799
+ }
800
+ });
801
+ try {
802
+ const { nonceManager, accountId } = this.initializeNonceManager(rec);
803
+ const signedTx = await this.signRegistrationTx(rec, accountId);
804
+ const txHash = await this.broadcastRegistrationTxAndWaitFinal(rec, signedTx);
805
+ if (txHash) {
806
+ const storedUser = await this.persistRecoveredUserRecordBestEffort(rec, accountId);
807
+ if (storedUser) {
808
+ const syncedAuthenticators = await this.syncAuthenticatorsBestEffort(accountId);
809
+ if (syncedAuthenticators) await this.setLastUserBestEffort(accountId, rec.deviceNumber);
810
+ }
811
+ }
812
+ await this.updateNonceBestEffort(nonceManager, signedTx);
813
+ await this.persistRecoveredUserData(rec, accountId);
814
+ await this.persistAuthenticatorBestEffort(rec, accountId);
815
+ this.emitAutoLoginEvent(require_sdkSentEvents.EmailRecoveryStatus.PROGRESS, "Attempting auto-login with recovered device...", { autoLogin: "progress" });
816
+ const autoLoginResult = await this.attemptAutoLogin(rec);
817
+ if (autoLoginResult.success) this.emitAutoLoginEvent(require_sdkSentEvents.EmailRecoveryStatus.SUCCESS, `Welcome ${accountId}`, { autoLogin: "success" });
818
+ else this.emitAutoLoginEvent(require_sdkSentEvents.EmailRecoveryStatus.ERROR, "Auto-login failed; please log in manually on this device.", {
819
+ error: autoLoginResult.reason,
820
+ autoLogin: "error"
821
+ });
822
+ await this.markCompleteAndClearPending(rec);
823
+ this.phase = require_sdkSentEvents.EmailRecoveryPhase.STEP_6_COMPLETE;
777
824
  this.emit({
778
- step: 5,
779
- phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
780
- status: require_sdkSentEvents.EmailRecoveryStatus.ERROR,
781
- message: "Auto-login failed; please log in manually on this device.",
825
+ step: 6,
826
+ phase: require_sdkSentEvents.EmailRecoveryPhase.STEP_6_COMPLETE,
827
+ status: require_sdkSentEvents.EmailRecoveryStatus.SUCCESS,
828
+ message: "Email recovery completed successfully",
782
829
  data: {
783
- error: err?.message || String(err),
784
- autoLogin: "error"
830
+ accountId: rec.accountId,
831
+ nearPublicKey: rec.nearPublicKey
785
832
  }
786
833
  });
834
+ } catch (e) {
835
+ const err = this.emitError(5, e?.message || "Email recovery finalization failed");
836
+ await this.options?.afterCall?.(false);
837
+ throw err;
838
+ }
839
+ }
840
+ async attemptAutoLogin(rec) {
841
+ try {
842
+ const accountId = require_accountIds.toAccountId(rec.accountId);
843
+ const deviceNumber = require_getDeviceNumber.parseDeviceNumber(rec.deviceNumber, { min: 1 });
844
+ if (deviceNumber === null) return this.handleAutoLoginFailure(`Invalid deviceNumber for auto-login: ${String(rec.deviceNumber)}`);
845
+ const shamirUnlocked = await this.tryShamirUnlock(rec, accountId, deviceNumber);
846
+ if (shamirUnlocked) return {
847
+ success: true,
848
+ method: "shamir"
849
+ };
850
+ const touchIdResult = await this.tryTouchIdUnlock(rec, accountId, deviceNumber);
851
+ if (touchIdResult.success) return {
852
+ success: true,
853
+ method: "touchid"
854
+ };
855
+ return this.handleAutoLoginFailure(touchIdResult.reason || "Auto-login failed");
856
+ } catch (err) {
857
+ return this.handleAutoLoginFailure(err?.message || String(err), err);
787
858
  }
788
859
  }
789
860
  };