@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
@@ -15,7 +15,7 @@ import { MinimalNearClient, SignedTransaction, isOffline, openOfflineExport } fr
15
15
  import { AccountRecoveryPhase, AccountRecoveryStatus, ActionPhase, ActionStatus, DEFAULT_WAIT_STATUS, DeviceLinkingPhase, DeviceLinkingStatus, EmailRecoveryPhase, EmailRecoveryStatus, LoginPhase, LoginStatus, RegistrationPhase, RegistrationStatus, init_rpc, init_sdkSentEvents } from "./sdkSentEvents-_jrJLIhw.js";
16
16
  import { SecureConfirmMessageType, SecureConfirmationType, TouchIdPrompt, authenticatorsToAllowCredentials, createRandomVRFChallenge, getIntentDigest, init_accountIds, init_touchIdPrompt, init_vrf_worker, parseTransactionSummary, sanitizeForPostMessage, sendConfirmResponse, toAccountId, validateVRFChallenge } from "./requestHelpers-Dh1hEYL9.js";
17
17
  import { PASSKEY_MANAGER_DEFAULT_CONFIGS, buildConfigsFromEnv, init_defaultConfigs } from "./defaultConfigs-DpslkAQd.js";
18
- import { buildSetRecoveryEmailsActions, checkCanRegisterUserContractCall, executeDeviceLinkingContractCalls, getCredentialIdsContractCall, getDeviceLinkingAccountContractCall, getRecoveryEmailHashesContractCall, init_rpcCalls, syncAuthenticatorsContractCall, verifyAuthenticationResponse } from "./rpcCalls-OhgEeFig.js";
18
+ import { buildSetRecoveryEmailsActions, checkCanRegisterUserContractCall, executeDeviceLinkingContractCalls, getCredentialIdsContractCall, getDeviceLinkingAccountContractCall, getRecoveryEmailHashesContractCall, init_rpcCalls, syncAuthenticatorsContractCall, verifyAuthenticationResponse } from "./rpcCalls-YVeUVMk2.js";
19
19
  import { getLastLoggedInDeviceNumber, init_getDeviceNumber, parseDeviceNumber } from "./getDeviceNumber-zsOHT_Um.js";
20
20
 
21
21
  //#region src/core/sdkPaths/workers.ts
@@ -9845,8 +9845,30 @@ async function signDelegateAction(args) {
9845
9845
  //#endregion
9846
9846
  //#region src/core/TatchiPasskey/relay.ts
9847
9847
  init_sdkSentEvents();
9848
+ const toNumberArray = (value) => Array.isArray(value) ? value : Array.from(value);
9849
+ const normalizeSignedDelegateForRelay = (signedDelegate) => {
9850
+ const delegateAction = signedDelegate.delegateAction;
9851
+ const signature = signedDelegate.signature;
9852
+ return {
9853
+ delegateAction: {
9854
+ ...delegateAction,
9855
+ publicKey: {
9856
+ ...delegateAction.publicKey,
9857
+ keyData: toNumberArray(delegateAction.publicKey.keyData)
9858
+ }
9859
+ },
9860
+ signature: {
9861
+ ...signature,
9862
+ signatureData: toNumberArray(signature.signatureData)
9863
+ }
9864
+ };
9865
+ };
9848
9866
  async function sendDelegateActionViaRelayer(args) {
9849
9867
  const { url, payload, signal, options } = args;
9868
+ const normalizedPayload = {
9869
+ ...payload,
9870
+ signedDelegate: normalizeSignedDelegateForRelay(payload.signedDelegate)
9871
+ };
9850
9872
  const emit = (event) => options?.onEvent?.(event);
9851
9873
  const emitError = (message) => {
9852
9874
  emit({
@@ -9868,7 +9890,7 @@ async function sendDelegateActionViaRelayer(args) {
9868
9890
  res = await fetch(url, {
9869
9891
  method: "POST",
9870
9892
  headers: { "content-type": "application/json" },
9871
- body: JSON.stringify(payload),
9893
+ body: JSON.stringify(normalizedPayload),
9872
9894
  signal
9873
9895
  });
9874
9896
  } catch (err$1) {
@@ -9933,28 +9955,65 @@ const OFFLINE_EXPORT_FALLBACK = "OFFLINE_EXPORT_FALLBACK";
9933
9955
  const WALLET_UI_CLOSED = "WALLET_UI_CLOSED";
9934
9956
  const EXPORT_NEAR_KEYPAIR_CANCELLED = "EXPORT_NEAR_KEYPAIR_CANCELLED";
9935
9957
 
9958
+ //#endregion
9959
+ //#region src/core/EmailRecovery/emailRecoveryPendingStore.ts
9960
+ var EmailRecoveryPendingStore;
9961
+ var init_emailRecoveryPendingStore = __esm({ "src/core/EmailRecovery/emailRecoveryPendingStore.ts": (() => {
9962
+ init_IndexedDBManager();
9963
+ EmailRecoveryPendingStore = class {
9964
+ getPendingTtlMs;
9965
+ now;
9966
+ constructor(options) {
9967
+ this.getPendingTtlMs = options.getPendingTtlMs;
9968
+ this.now = options.now ?? Date.now;
9969
+ }
9970
+ getPendingIndexKey(accountId) {
9971
+ return `pendingEmailRecovery:${accountId}`;
9972
+ }
9973
+ getPendingRecordKey(accountId, nearPublicKey) {
9974
+ return `${this.getPendingIndexKey(accountId)}:${nearPublicKey}`;
9975
+ }
9976
+ async get(accountId, nearPublicKey) {
9977
+ const pendingTtlMs = this.getPendingTtlMs();
9978
+ const indexKey = this.getPendingIndexKey(accountId);
9979
+ const indexedNearPublicKey = await IndexedDBManager.clientDB.getAppState(indexKey);
9980
+ const resolvedNearPublicKey = nearPublicKey ?? indexedNearPublicKey;
9981
+ if (!resolvedNearPublicKey) return null;
9982
+ const recordKey = this.getPendingRecordKey(accountId, resolvedNearPublicKey);
9983
+ const record = await IndexedDBManager.clientDB.getAppState(recordKey);
9984
+ const shouldClearIndex = indexedNearPublicKey === resolvedNearPublicKey;
9985
+ if (!record) {
9986
+ if (shouldClearIndex) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
9987
+ return null;
9988
+ }
9989
+ if (this.now() - record.createdAt > pendingTtlMs) {
9990
+ await IndexedDBManager.clientDB.setAppState(recordKey, void 0).catch(() => {});
9991
+ if (shouldClearIndex) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
9992
+ return null;
9993
+ }
9994
+ await this.touchIndex(accountId, record.nearPublicKey);
9995
+ return record;
9996
+ }
9997
+ async set(record) {
9998
+ const key = this.getPendingRecordKey(record.accountId, record.nearPublicKey);
9999
+ await IndexedDBManager.clientDB.setAppState(key, record);
10000
+ await this.touchIndex(record.accountId, record.nearPublicKey);
10001
+ }
10002
+ async clear(accountId, nearPublicKey) {
10003
+ const indexKey = this.getPendingIndexKey(accountId);
10004
+ const idx = await IndexedDBManager.clientDB.getAppState(indexKey).catch(() => void 0);
10005
+ const resolvedNearPublicKey = nearPublicKey || idx || "";
10006
+ if (resolvedNearPublicKey) await IndexedDBManager.clientDB.setAppState(this.getPendingRecordKey(accountId, resolvedNearPublicKey), void 0).catch(() => {});
10007
+ if (!nearPublicKey || idx === nearPublicKey) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
10008
+ }
10009
+ async touchIndex(accountId, nearPublicKey) {
10010
+ await IndexedDBManager.clientDB.setAppState(this.getPendingIndexKey(accountId), nearPublicKey).catch(() => {});
10011
+ }
10012
+ };
10013
+ }) });
10014
+
9936
10015
  //#endregion
9937
10016
  //#region src/core/EmailRecovery/index.ts
9938
- init_accountIds();
9939
- init_IndexedDBManager();
9940
- const canonicalizeEmail = (email) => {
9941
- const raw = String(email || "").trim();
9942
- if (!raw) return "";
9943
- const withoutHeaderName = raw.replace(/^[a-z0-9-]+\s*:\s*/i, "").trim();
9944
- const emailRegex = /([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)/;
9945
- const angleMatch = withoutHeaderName.match(/<([^>]+)>/);
9946
- const candidates = [angleMatch?.[1], withoutHeaderName].filter((v) => typeof v === "string" && v.length > 0);
9947
- for (const candidate of candidates) {
9948
- const cleaned = candidate.replace(/^mailto:\s*/i, "");
9949
- const match = cleaned.match(emailRegex);
9950
- if (match?.[1]) return match[1].trim().toLowerCase();
9951
- }
9952
- return withoutHeaderName.toLowerCase();
9953
- };
9954
- const bytesToHex = (bytes) => {
9955
- const arr = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
9956
- return `0x${Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
9957
- };
9958
10017
  async function hashRecoveryEmails(emails, accountId) {
9959
10018
  const encoder = new TextEncoder();
9960
10019
  const salt = (accountId || "").trim().toLowerCase();
@@ -10001,6 +10060,30 @@ async function prepareRecoveryEmails(nearAccountId, recoveryEmails) {
10001
10060
  async function getLocalRecoveryEmails(nearAccountId) {
10002
10061
  return IndexedDBManager.getRecoveryEmails(nearAccountId);
10003
10062
  }
10063
+ var canonicalizeEmail, bytesToHex;
10064
+ var init_EmailRecovery = __esm({ "src/core/EmailRecovery/index.ts": (() => {
10065
+ init_accountIds();
10066
+ init_IndexedDBManager();
10067
+ init_emailRecoveryPendingStore();
10068
+ canonicalizeEmail = (email) => {
10069
+ const raw = String(email || "").trim();
10070
+ if (!raw) return "";
10071
+ const withoutHeaderName = raw.replace(/^[a-z0-9-]+\s*:\s*/i, "").trim();
10072
+ const emailRegex = /([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)/;
10073
+ const angleMatch = withoutHeaderName.match(/<([^>]+)>/);
10074
+ const candidates = [angleMatch?.[1], withoutHeaderName].filter((v) => typeof v === "string" && v.length > 0);
10075
+ for (const candidate of candidates) {
10076
+ const cleaned = candidate.replace(/^mailto:\s*/i, "");
10077
+ const match = cleaned.match(emailRegex);
10078
+ if (match?.[1]) return match[1].trim().toLowerCase();
10079
+ }
10080
+ return withoutHeaderName.toLowerCase();
10081
+ };
10082
+ bytesToHex = (bytes) => {
10083
+ const arr = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
10084
+ return `0x${Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
10085
+ };
10086
+ }) });
10004
10087
 
10005
10088
  //#endregion
10006
10089
  //#region src/core/TatchiPasskey/emailRecovery.ts
@@ -10047,9 +10130,11 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10047
10130
  init_rpc();
10048
10131
  init_getDeviceNumber();
10049
10132
  init_login();
10133
+ init_EmailRecovery();
10050
10134
  EmailRecoveryFlow = class {
10051
10135
  context;
10052
10136
  options;
10137
+ pendingStore;
10053
10138
  pending = null;
10054
10139
  phase = EmailRecoveryPhase.STEP_1_PREPARATION;
10055
10140
  pollingTimer;
@@ -10060,6 +10145,7 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10060
10145
  constructor(context, options) {
10061
10146
  this.context = context;
10062
10147
  this.options = options;
10148
+ this.pendingStore = options?.pendingStore ?? new EmailRecoveryPendingStore({ getPendingTtlMs: () => this.getConfig().pendingTtlMs });
10063
10149
  }
10064
10150
  setOptions(options) {
10065
10151
  if (!options) return;
@@ -10067,6 +10153,7 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10067
10153
  ...this.options || {},
10068
10154
  ...options
10069
10155
  };
10156
+ if (options.pendingStore) this.pendingStore = options.pendingStore;
10070
10157
  }
10071
10158
  emit(event) {
10072
10159
  this.options?.onEvent?.(event);
@@ -10085,24 +10172,139 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10085
10172
  this.options?.onError?.(err$1);
10086
10173
  return err$1;
10087
10174
  }
10175
+ async fail(step, message) {
10176
+ const err$1 = this.emitError(step, message);
10177
+ await this.options?.afterCall?.(false);
10178
+ throw err$1;
10179
+ }
10180
+ async assertValidAccountIdOrFail(step, accountId) {
10181
+ const validation = validateNearAccountId(accountId);
10182
+ if (!validation.valid) await this.fail(step, `Invalid NEAR account ID: ${validation.error}`);
10183
+ return toAccountId(accountId);
10184
+ }
10185
+ async resolvePendingOrFail(step, args, options) {
10186
+ 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 ?? {};
10187
+ let rec = this.pending;
10188
+ if (!rec || rec.accountId !== args.accountId || args.nearPublicKey && rec.nearPublicKey !== args.nearPublicKey) {
10189
+ rec = await this.loadPending(args.accountId, args.nearPublicKey);
10190
+ this.pending = rec;
10191
+ }
10192
+ if (!rec) await this.fail(step, missingMessage);
10193
+ const resolved = rec;
10194
+ if (!allowErrorStatus && resolved.status === "error") await this.fail(step, errorStatusMessage);
10195
+ return resolved;
10196
+ }
10088
10197
  getConfig() {
10089
10198
  return getEmailRecoveryConfig(this.context.configs);
10090
10199
  }
10091
- getPendingIndexKey(accountId) {
10092
- return `pendingEmailRecovery:${accountId}`;
10200
+ toBigInt(value) {
10201
+ if (typeof value === "bigint") return value;
10202
+ if (typeof value === "number") return BigInt(value);
10203
+ if (typeof value === "string" && value.length > 0) return BigInt(value);
10204
+ return BigInt(0);
10093
10205
  }
10094
- getPendingRecordKey(accountId, nearPublicKey) {
10095
- return `${this.getPendingIndexKey(accountId)}:${nearPublicKey}`;
10206
+ computeAvailableBalance(accountView) {
10207
+ const STORAGE_PRICE_PER_BYTE = BigInt("10000000000000000000");
10208
+ const amount = this.toBigInt(accountView.amount);
10209
+ const locked = this.toBigInt(accountView.locked);
10210
+ const storageUsage = this.toBigInt(accountView.storage_usage);
10211
+ const storageCost = storageUsage * STORAGE_PRICE_PER_BYTE;
10212
+ const rawAvailable = amount - locked - storageCost;
10213
+ return rawAvailable > 0 ? rawAvailable : BigInt(0);
10214
+ }
10215
+ async assertSufficientBalance(nearAccountId) {
10216
+ const { minBalanceYocto } = this.getConfig();
10217
+ try {
10218
+ const accountView = await this.context.nearClient.viewAccount(nearAccountId);
10219
+ const available = this.computeAvailableBalance(accountView);
10220
+ 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.`);
10221
+ } catch (e) {
10222
+ await this.fail(1, e?.message || "Failed to fetch account balance for recovery");
10223
+ }
10224
+ }
10225
+ async getCanonicalRecoveryEmailOrFail(recoveryEmail) {
10226
+ const canonicalEmail = String(recoveryEmail || "").trim().toLowerCase();
10227
+ if (!canonicalEmail) await this.fail(1, "Recovery email is required for email-based account recovery");
10228
+ return canonicalEmail;
10229
+ }
10230
+ async getNextDeviceNumberFromContract(nearAccountId) {
10231
+ try {
10232
+ const { syncAuthenticatorsContractCall: syncAuthenticatorsContractCall$1 } = await import("./rpcCalls-BQrJMTdg.js");
10233
+ const authenticators = await syncAuthenticatorsContractCall$1(this.context.nearClient, this.context.configs.contractId, nearAccountId);
10234
+ const numbers = authenticators.map((a) => a?.authenticator?.deviceNumber).filter((n) => typeof n === "number" && Number.isFinite(n));
10235
+ const max = numbers.length > 0 ? Math.max(...numbers) : 0;
10236
+ return max + 1;
10237
+ } catch {
10238
+ return 1;
10239
+ }
10240
+ }
10241
+ async collectRecoveryCredentialOrFail(nearAccountId, deviceNumber) {
10242
+ const confirmerText = {
10243
+ title: this.options?.confirmerText?.title ?? "Register New Recovery Account",
10244
+ body: this.options?.confirmerText?.body ?? "Create a recovery account and send an encrypted email to recover your account."
10245
+ };
10246
+ const confirm = await this.context.webAuthnManager.requestRegistrationCredentialConfirmation({
10247
+ nearAccountId,
10248
+ deviceNumber,
10249
+ confirmerText,
10250
+ confirmationConfigOverride: this.options?.confirmationConfig
10251
+ });
10252
+ if (!confirm.confirmed || !confirm.credential) await this.fail(2, "User cancelled email recovery TouchID confirmation");
10253
+ return {
10254
+ credential: confirm.credential,
10255
+ vrfChallenge: confirm.vrfChallenge || void 0
10256
+ };
10257
+ }
10258
+ async deriveRecoveryKeysOrFail(nearAccountId, deviceNumber, credential) {
10259
+ const vrfDerivationResult = await this.context.webAuthnManager.deriveVrfKeypair({
10260
+ credential,
10261
+ nearAccountId
10262
+ });
10263
+ if (!vrfDerivationResult.success || !vrfDerivationResult.encryptedVrfKeypair) await this.fail(2, "Failed to derive VRF keypair from PRF for email recovery");
10264
+ const nearKeyResult = await this.context.webAuthnManager.deriveNearKeypairAndEncryptFromSerialized({
10265
+ nearAccountId,
10266
+ credential,
10267
+ options: { deviceNumber }
10268
+ });
10269
+ if (!nearKeyResult.success || !nearKeyResult.publicKey) await this.fail(2, "Failed to derive NEAR keypair for email recovery");
10270
+ return {
10271
+ encryptedVrfKeypair: vrfDerivationResult.encryptedVrfKeypair,
10272
+ serverEncryptedVrfKeypair: vrfDerivationResult.serverEncryptedVrfKeypair || null,
10273
+ vrfPublicKey: vrfDerivationResult.vrfPublicKey,
10274
+ nearPublicKey: nearKeyResult.publicKey
10275
+ };
10276
+ }
10277
+ emitAwaitEmail(rec, mailtoUrl) {
10278
+ this.phase = EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
10279
+ this.emit({
10280
+ step: 3,
10281
+ phase: EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
10282
+ status: EmailRecoveryStatus.PROGRESS,
10283
+ message: "New device key created; please send the recovery email from your registered address.",
10284
+ data: {
10285
+ accountId: rec.accountId,
10286
+ recoveryEmail: rec.recoveryEmail,
10287
+ nearPublicKey: rec.nearPublicKey,
10288
+ requestId: rec.requestId,
10289
+ mailtoUrl
10290
+ }
10291
+ });
10096
10292
  }
10097
- async checkVerificationStatus(rec) {
10293
+ emitAutoLoginEvent(status, message, data) {
10294
+ this.emit({
10295
+ step: 5,
10296
+ phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10297
+ status,
10298
+ message,
10299
+ data
10300
+ });
10301
+ }
10302
+ async checkViaDkimViewMethod(rec) {
10098
10303
  const { dkimVerifierAccountId, verificationViewMethod } = this.getConfig();
10099
10304
  if (!dkimVerifierAccountId) return null;
10100
10305
  try {
10101
- const result = await this.context.nearClient.view({
10102
- account: dkimVerifierAccountId,
10103
- method: verificationViewMethod,
10104
- args: { request_id: rec.requestId }
10105
- });
10306
+ const { getEmailRecoveryVerificationResult } = await import("./rpcCalls-BQrJMTdg.js");
10307
+ const result = await getEmailRecoveryVerificationResult(this.context.nearClient, dkimVerifierAccountId, verificationViewMethod, rec.requestId);
10106
10308
  if (!result) return {
10107
10309
  completed: false,
10108
10310
  success: false
@@ -10134,43 +10336,78 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10134
10336
  transactionHash: result.transaction_hash
10135
10337
  };
10136
10338
  } catch (err$1) {
10137
- console.warn("[EmailRecoveryFlow] get_verification_result view failed; falling back to access key polling", err$1);
10339
+ console.warn("[EmailRecoveryFlow] get_verification_result view failed; will retry", err$1);
10138
10340
  return null;
10139
10341
  }
10140
10342
  }
10141
- async loadPending(accountId, nearPublicKey) {
10142
- const { pendingTtlMs } = this.getConfig();
10143
- const indexKey = this.getPendingIndexKey(accountId);
10144
- const indexedNearPublicKey = await IndexedDBManager.clientDB.getAppState(indexKey);
10145
- const resolvedNearPublicKey = nearPublicKey ?? indexedNearPublicKey;
10146
- if (!resolvedNearPublicKey) return null;
10147
- const recordKey = this.getPendingRecordKey(accountId, resolvedNearPublicKey);
10148
- const record = await IndexedDBManager.clientDB.getAppState(recordKey);
10149
- const shouldClearIndex = indexedNearPublicKey === resolvedNearPublicKey;
10150
- if (!record) {
10151
- if (shouldClearIndex) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
10152
- return null;
10153
- }
10154
- if (Date.now() - record.createdAt > pendingTtlMs) {
10155
- await IndexedDBManager.clientDB.setAppState(recordKey, void 0).catch(() => {});
10156
- if (shouldClearIndex) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
10157
- return null;
10343
+ buildPollingEventData(rec, details) {
10344
+ return {
10345
+ accountId: rec.accountId,
10346
+ requestId: rec.requestId,
10347
+ nearPublicKey: rec.nearPublicKey,
10348
+ transactionHash: details.transactionHash,
10349
+ elapsedMs: details.elapsedMs,
10350
+ pollCount: details.pollCount
10351
+ };
10352
+ }
10353
+ async sleepForPollInterval(ms) {
10354
+ await new Promise((resolve) => {
10355
+ this.pollIntervalResolver = resolve;
10356
+ this.pollingTimer = setTimeout(() => {
10357
+ this.pollIntervalResolver = void 0;
10358
+ this.pollingTimer = void 0;
10359
+ resolve();
10360
+ }, ms);
10361
+ }).finally(() => {
10362
+ this.pollIntervalResolver = void 0;
10363
+ });
10364
+ }
10365
+ async pollUntil(args) {
10366
+ const now = args.now ?? Date.now;
10367
+ const sleep = args.sleep ?? this.sleepForPollInterval.bind(this);
10368
+ const startedAt = now();
10369
+ let pollCount = 0;
10370
+ while (!args.isCancelled()) {
10371
+ pollCount += 1;
10372
+ const elapsedMs$1 = now() - startedAt;
10373
+ if (elapsedMs$1 > args.timeoutMs) return {
10374
+ status: "timedOut",
10375
+ elapsedMs: elapsedMs$1,
10376
+ pollCount
10377
+ };
10378
+ const result = await args.tick({
10379
+ elapsedMs: elapsedMs$1,
10380
+ pollCount
10381
+ });
10382
+ if (result.done) return {
10383
+ status: "completed",
10384
+ value: result.value,
10385
+ elapsedMs: elapsedMs$1,
10386
+ pollCount
10387
+ };
10388
+ if (args.isCancelled()) return {
10389
+ status: "cancelled",
10390
+ elapsedMs: elapsedMs$1,
10391
+ pollCount
10392
+ };
10393
+ await sleep(args.intervalMs);
10158
10394
  }
10159
- await IndexedDBManager.clientDB.setAppState(indexKey, record.nearPublicKey).catch(() => {});
10160
- return record;
10395
+ const elapsedMs = now() - startedAt;
10396
+ return {
10397
+ status: "cancelled",
10398
+ elapsedMs,
10399
+ pollCount
10400
+ };
10401
+ }
10402
+ async loadPending(accountId, nearPublicKey) {
10403
+ return this.pendingStore.get(accountId, nearPublicKey);
10161
10404
  }
10162
10405
  async savePending(rec) {
10163
- const key = this.getPendingRecordKey(rec.accountId, rec.nearPublicKey);
10164
- await IndexedDBManager.clientDB.setAppState(key, rec);
10165
- await IndexedDBManager.clientDB.setAppState(this.getPendingIndexKey(rec.accountId), rec.nearPublicKey).catch(() => {});
10406
+ await this.pendingStore.set(rec);
10166
10407
  this.pending = rec;
10167
10408
  }
10168
10409
  async clearPending(accountId, nearPublicKey) {
10169
- const indexKey = this.getPendingIndexKey(accountId);
10170
- const idx = await IndexedDBManager.clientDB.getAppState(indexKey).catch(() => void 0);
10171
- const resolvedNearPublicKey = nearPublicKey || idx || "";
10172
- if (resolvedNearPublicKey) await IndexedDBManager.clientDB.setAppState(this.getPendingRecordKey(accountId, resolvedNearPublicKey), void 0).catch(() => {});
10173
- if (!nearPublicKey || idx === nearPublicKey) await IndexedDBManager.clientDB.setAppState(indexKey, void 0).catch(() => {});
10410
+ await this.pendingStore.clear(accountId, nearPublicKey);
10174
10411
  if (this.pending && this.pending.accountId === accountId && (!nearPublicKey || this.pending.nearPublicKey === nearPublicKey)) this.pending = null;
10175
10412
  }
10176
10413
  getState() {
@@ -10184,48 +10421,14 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10184
10421
  const { accountId, nearPublicKey } = args;
10185
10422
  this.cancelled = false;
10186
10423
  this.error = void 0;
10187
- const validation = validateNearAccountId(accountId);
10188
- if (!validation.valid) {
10189
- const err$1 = this.emitError(3, `Invalid NEAR account ID: ${validation.error}`);
10190
- await this.options?.afterCall?.(false);
10191
- throw err$1;
10192
- }
10193
- const nearAccountId = toAccountId(accountId);
10194
- let rec = this.pending;
10195
- if (!rec || rec.accountId !== nearAccountId || nearPublicKey && rec.nearPublicKey !== nearPublicKey) {
10196
- rec = await this.loadPending(nearAccountId, nearPublicKey);
10197
- this.pending = rec;
10198
- }
10199
- if (!rec) {
10200
- const err$1 = this.emitError(3, "No pending email recovery record found for this account");
10201
- await this.options?.afterCall?.(false);
10202
- throw err$1;
10203
- }
10204
- if (rec.status === "error") {
10205
- const err$1 = this.emitError(3, "Pending email recovery is in an error state; please restart the flow");
10206
- await this.options?.afterCall?.(false);
10207
- throw err$1;
10208
- }
10209
- if (rec.status === "finalizing" || rec.status === "complete") {
10210
- const err$1 = this.emitError(3, "Recovery email has already been processed on-chain for this request");
10211
- await this.options?.afterCall?.(false);
10212
- throw err$1;
10213
- }
10424
+ const nearAccountId = await this.assertValidAccountIdOrFail(3, accountId);
10425
+ const rec = await this.resolvePendingOrFail(3, {
10426
+ accountId: nearAccountId,
10427
+ nearPublicKey
10428
+ }, { allowErrorStatus: false });
10429
+ if (rec.status === "finalizing" || rec.status === "complete") await this.fail(3, "Recovery email has already been processed on-chain for this request");
10214
10430
  const mailtoUrl = rec.status === "awaiting-email" ? await this.buildMailtoUrlAndUpdateStatus(rec) : this.buildMailtoUrlInternal(rec);
10215
- this.phase = EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
10216
- this.emit({
10217
- step: 3,
10218
- phase: EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
10219
- status: EmailRecoveryStatus.PROGRESS,
10220
- message: "New device key created; please send the recovery email from your registered address.",
10221
- data: {
10222
- accountId: rec.accountId,
10223
- recoveryEmail: rec.recoveryEmail,
10224
- nearPublicKey: rec.nearPublicKey,
10225
- requestId: rec.requestId,
10226
- mailtoUrl
10227
- }
10228
- });
10431
+ this.emitAwaitEmail(rec, mailtoUrl);
10229
10432
  await this.options?.afterCall?.(true, void 0);
10230
10433
  return mailtoUrl;
10231
10434
  }
@@ -10240,49 +10443,10 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10240
10443
  status: EmailRecoveryStatus.PROGRESS,
10241
10444
  message: "Preparing email recovery..."
10242
10445
  });
10243
- const validation = validateNearAccountId(accountId);
10244
- if (!validation.valid) {
10245
- const err$1 = this.emitError(1, `Invalid NEAR account ID: ${validation.error}`);
10246
- await this.options?.afterCall?.(false);
10247
- throw err$1;
10248
- }
10249
- const nearAccountId = toAccountId(accountId);
10250
- const { minBalanceYocto } = this.getConfig();
10251
- const STORAGE_PRICE_PER_BYTE = BigInt("10000000000000000000");
10252
- try {
10253
- const accountView = await this.context.nearClient.viewAccount(nearAccountId);
10254
- const amount = BigInt(accountView.amount || "0");
10255
- const locked = BigInt(accountView.locked || "0");
10256
- const storageUsage = BigInt(accountView.storage_usage || 0);
10257
- const storageCost = storageUsage * STORAGE_PRICE_PER_BYTE;
10258
- const rawAvailable = amount - locked - storageCost;
10259
- const available = rawAvailable > 0 ? rawAvailable : BigInt(0);
10260
- if (available < BigInt(minBalanceYocto)) {
10261
- const err$1 = 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.`);
10262
- await this.options?.afterCall?.(false);
10263
- throw err$1;
10264
- }
10265
- } catch (e) {
10266
- const err$1 = this.emitError(1, e?.message || "Failed to fetch account balance for recovery");
10267
- await this.options?.afterCall?.(false);
10268
- throw err$1;
10269
- }
10270
- const canonicalEmail = String(recoveryEmail || "").trim().toLowerCase();
10271
- if (!canonicalEmail) {
10272
- const err$1 = this.emitError(1, "Recovery email is required for email-based account recovery");
10273
- await this.options?.afterCall?.(false);
10274
- throw err$1;
10275
- }
10276
- let deviceNumber = 1;
10277
- try {
10278
- const { syncAuthenticatorsContractCall: syncAuthenticatorsContractCall$1 } = await import("./rpcCalls-DEv9x5-f.js");
10279
- const authenticators = await syncAuthenticatorsContractCall$1(this.context.nearClient, this.context.configs.contractId, nearAccountId);
10280
- const numbers = authenticators.map((a) => a?.authenticator?.deviceNumber).filter((n) => typeof n === "number" && Number.isFinite(n));
10281
- const max = numbers.length > 0 ? Math.max(...numbers) : 0;
10282
- deviceNumber = max + 1;
10283
- } catch {
10284
- deviceNumber = 1;
10285
- }
10446
+ const nearAccountId = await this.assertValidAccountIdOrFail(1, accountId);
10447
+ await this.assertSufficientBalance(nearAccountId);
10448
+ const canonicalEmail = await this.getCanonicalRecoveryEmailOrFail(recoveryEmail);
10449
+ const deviceNumber = await this.getNextDeviceNumberFromContract(nearAccountId);
10286
10450
  this.phase = EmailRecoveryPhase.STEP_2_TOUCH_ID_REGISTRATION;
10287
10451
  this.emit({
10288
10452
  step: 2,
@@ -10291,69 +10455,24 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10291
10455
  message: "Collecting passkey for email recovery..."
10292
10456
  });
10293
10457
  try {
10294
- const confirmerText = {
10295
- title: this.options?.confirmerText?.title ?? "Register New Recovery Account",
10296
- body: this.options?.confirmerText?.body ?? "Create a recovery account and send an encrypted email to recover your account."
10297
- };
10298
- const confirm = await this.context.webAuthnManager.requestRegistrationCredentialConfirmation({
10299
- nearAccountId,
10300
- deviceNumber,
10301
- confirmerText,
10302
- confirmationConfigOverride: this.options?.confirmationConfig
10303
- });
10304
- if (!confirm.confirmed || !confirm.credential) {
10305
- const err$1 = this.emitError(2, "User cancelled email recovery TouchID confirmation");
10306
- await this.options?.afterCall?.(false);
10307
- throw err$1;
10308
- }
10309
- const vrfDerivationResult = await this.context.webAuthnManager.deriveVrfKeypair({
10310
- credential: confirm.credential,
10311
- nearAccountId
10312
- });
10313
- if (!vrfDerivationResult.success || !vrfDerivationResult.encryptedVrfKeypair) {
10314
- const err$1 = this.emitError(2, "Failed to derive VRF keypair from PRF for email recovery");
10315
- await this.options?.afterCall?.(false);
10316
- throw err$1;
10317
- }
10318
- const nearKeyResult = await this.context.webAuthnManager.deriveNearKeypairAndEncryptFromSerialized({
10319
- nearAccountId,
10320
- credential: confirm.credential,
10321
- options: { deviceNumber }
10322
- });
10323
- if (!nearKeyResult.success || !nearKeyResult.publicKey) {
10324
- const err$1 = this.emitError(2, "Failed to derive NEAR keypair for email recovery");
10325
- await this.options?.afterCall?.(false);
10326
- throw err$1;
10327
- }
10458
+ const confirm = await this.collectRecoveryCredentialOrFail(nearAccountId, deviceNumber);
10459
+ const derivedKeys = await this.deriveRecoveryKeysOrFail(nearAccountId, deviceNumber, confirm.credential);
10328
10460
  const rec = {
10329
10461
  accountId: nearAccountId,
10330
10462
  recoveryEmail: canonicalEmail,
10331
10463
  deviceNumber,
10332
- nearPublicKey: nearKeyResult.publicKey,
10464
+ nearPublicKey: derivedKeys.nearPublicKey,
10333
10465
  requestId: generateEmailRecoveryRequestId(),
10334
- encryptedVrfKeypair: vrfDerivationResult.encryptedVrfKeypair,
10335
- serverEncryptedVrfKeypair: vrfDerivationResult.serverEncryptedVrfKeypair || null,
10336
- vrfPublicKey: vrfDerivationResult.vrfPublicKey,
10466
+ encryptedVrfKeypair: derivedKeys.encryptedVrfKeypair,
10467
+ serverEncryptedVrfKeypair: derivedKeys.serverEncryptedVrfKeypair,
10468
+ vrfPublicKey: derivedKeys.vrfPublicKey,
10337
10469
  credential: confirm.credential,
10338
10470
  vrfChallenge: confirm.vrfChallenge || void 0,
10339
10471
  createdAt: Date.now(),
10340
10472
  status: "awaiting-email"
10341
10473
  };
10342
10474
  const mailtoUrl = await this.buildMailtoUrlAndUpdateStatus(rec);
10343
- this.phase = EmailRecoveryPhase.STEP_3_AWAIT_EMAIL;
10344
- this.emit({
10345
- step: 3,
10346
- phase: EmailRecoveryPhase.STEP_3_AWAIT_EMAIL,
10347
- status: EmailRecoveryStatus.PROGRESS,
10348
- message: "New device key created; please send the recovery email from your registered address.",
10349
- data: {
10350
- accountId: rec.accountId,
10351
- recoveryEmail: rec.recoveryEmail,
10352
- nearPublicKey: rec.nearPublicKey,
10353
- requestId: rec.requestId,
10354
- mailtoUrl
10355
- }
10356
- });
10475
+ this.emitAwaitEmail(rec, mailtoUrl);
10357
10476
  await this.options?.afterCall?.(true, void 0);
10358
10477
  return {
10359
10478
  mailtoUrl,
@@ -10381,28 +10500,11 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10381
10500
  const { accountId, nearPublicKey } = args;
10382
10501
  this.cancelled = false;
10383
10502
  this.error = void 0;
10384
- const validation = validateNearAccountId(accountId);
10385
- if (!validation.valid) {
10386
- const err$1 = this.emitError(4, `Invalid NEAR account ID: ${validation.error}`);
10387
- await this.options?.afterCall?.(false);
10388
- throw err$1;
10389
- }
10390
- const nearAccountId = toAccountId(accountId);
10391
- let rec = this.pending;
10392
- if (!rec || rec.accountId !== nearAccountId || nearPublicKey && rec.nearPublicKey !== nearPublicKey) {
10393
- rec = await this.loadPending(nearAccountId, nearPublicKey);
10394
- this.pending = rec;
10395
- }
10396
- if (!rec) {
10397
- const err$1 = this.emitError(4, "No pending email recovery record found for this account");
10398
- await this.options?.afterCall?.(false);
10399
- throw err$1;
10400
- }
10401
- if (rec.status === "error") {
10402
- const err$1 = this.emitError(4, "Pending email recovery is in an error state; please restart the flow");
10403
- await this.options?.afterCall?.(false);
10404
- throw err$1;
10405
- }
10503
+ const nearAccountId = await this.assertValidAccountIdOrFail(4, accountId);
10504
+ const rec = await this.resolvePendingOrFail(4, {
10505
+ accountId: nearAccountId,
10506
+ nearPublicKey
10507
+ }, { allowErrorStatus: false });
10406
10508
  if (rec.status === "complete" || rec.status === "finalizing") {
10407
10509
  await this.options?.afterCall?.(true, void 0);
10408
10510
  return;
@@ -10442,23 +10544,11 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10442
10544
  const { accountId, nearPublicKey } = args;
10443
10545
  this.cancelled = false;
10444
10546
  this.error = void 0;
10445
- const validation = validateNearAccountId(accountId);
10446
- if (!validation.valid) {
10447
- const err$1 = this.emitError(4, `Invalid NEAR account ID: ${validation.error}`);
10448
- await this.options?.afterCall?.(false);
10449
- throw err$1;
10450
- }
10451
- const nearAccountId = toAccountId(accountId);
10452
- let rec = this.pending;
10453
- if (!rec || rec.accountId !== nearAccountId || nearPublicKey && rec.nearPublicKey !== nearPublicKey) {
10454
- rec = await this.loadPending(nearAccountId, nearPublicKey);
10455
- this.pending = rec;
10456
- }
10457
- if (!rec) {
10458
- const err$1 = this.emitError(4, "No pending email recovery record found for this account");
10459
- await this.options?.afterCall?.(false);
10460
- throw err$1;
10461
- }
10547
+ const nearAccountId = await this.assertValidAccountIdOrFail(4, accountId);
10548
+ const rec = await this.resolvePendingOrFail(4, {
10549
+ accountId: nearAccountId,
10550
+ nearPublicKey
10551
+ }, { allowErrorStatus: true });
10462
10552
  this.emit({
10463
10553
  step: 0,
10464
10554
  phase: EmailRecoveryPhase.RESUMED_FROM_PENDING,
@@ -10494,245 +10584,242 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10494
10584
  }
10495
10585
  this.phase = EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT;
10496
10586
  this.pollingStartedAt = Date.now();
10497
- let pollCount = 0;
10498
- while (!this.cancelled) {
10499
- pollCount += 1;
10500
- const elapsed = Date.now() - (this.pollingStartedAt || 0);
10501
- if (elapsed > maxPollingDurationMs) {
10502
- const err$2 = this.emitError(4, "Timed out waiting for recovery email to be processed on-chain");
10587
+ const pollResult = await this.pollUntil({
10588
+ intervalMs: pollingIntervalMs,
10589
+ timeoutMs: maxPollingDurationMs,
10590
+ isCancelled: () => this.cancelled,
10591
+ tick: async ({ elapsedMs, pollCount }) => {
10592
+ const verification = await this.checkViaDkimViewMethod(rec);
10593
+ const completed = verification?.completed === true;
10594
+ const success = verification?.success === true;
10595
+ this.emit({
10596
+ step: 4,
10597
+ phase: EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT,
10598
+ status: EmailRecoveryStatus.PROGRESS,
10599
+ message: completed && success ? `Email verified for request ${rec.requestId}; finalizing registration` : `Waiting for email verification for request ${rec.requestId}`,
10600
+ data: this.buildPollingEventData(rec, {
10601
+ transactionHash: verification?.transactionHash,
10602
+ elapsedMs,
10603
+ pollCount
10604
+ })
10605
+ });
10606
+ if (!completed) return { done: false };
10607
+ if (!success) return {
10608
+ done: true,
10609
+ value: {
10610
+ outcome: "failed",
10611
+ errorMessage: verification?.errorMessage || "Email verification failed"
10612
+ }
10613
+ };
10614
+ return {
10615
+ done: true,
10616
+ value: { outcome: "verified" }
10617
+ };
10618
+ }
10619
+ });
10620
+ if (pollResult.status === "completed") {
10621
+ if (pollResult.value.outcome === "failed") {
10622
+ const err$2 = this.emitError(4, pollResult.value.errorMessage);
10503
10623
  rec.status = "error";
10504
10624
  await this.savePending(rec);
10505
10625
  await this.options?.afterCall?.(false);
10506
10626
  throw err$2;
10507
10627
  }
10508
- const verification = await this.checkVerificationStatus(rec);
10509
- const completed = verification?.completed === true;
10510
- const success = verification?.success === true;
10511
- this.emit({
10512
- step: 4,
10513
- phase: EmailRecoveryPhase.STEP_4_POLLING_VERIFICATION_RESULT,
10514
- status: EmailRecoveryStatus.PROGRESS,
10515
- message: completed && success ? `Email verified for request ${rec.requestId}; finalizing registration` : `Waiting for email verification for request ${rec.requestId}`,
10516
- data: {
10517
- accountId: rec.accountId,
10518
- requestId: rec.requestId,
10519
- nearPublicKey: rec.nearPublicKey,
10520
- transactionHash: verification?.transactionHash,
10521
- elapsedMs: elapsed,
10522
- pollCount
10523
- }
10524
- });
10525
- if (completed) {
10526
- if (!success) {
10527
- const err$2 = this.emitError(4, verification?.errorMessage || "Email verification failed");
10528
- rec.status = "error";
10529
- await this.savePending(rec);
10530
- await this.options?.afterCall?.(false);
10531
- throw err$2;
10532
- }
10533
- rec.status = "finalizing";
10534
- await this.savePending(rec);
10535
- return;
10536
- }
10537
- if (this.cancelled) break;
10538
- await new Promise((resolve) => {
10539
- this.pollIntervalResolver = resolve;
10540
- this.pollingTimer = setTimeout(() => {
10541
- this.pollIntervalResolver = void 0;
10542
- this.pollingTimer = void 0;
10543
- resolve();
10544
- }, pollingIntervalMs);
10545
- }).finally(() => {
10546
- this.pollIntervalResolver = void 0;
10547
- });
10628
+ rec.status = "finalizing";
10629
+ await this.savePending(rec);
10630
+ return;
10631
+ }
10632
+ if (pollResult.status === "timedOut") {
10633
+ const err$2 = this.emitError(4, "Timed out waiting for recovery email to be processed on-chain");
10634
+ rec.status = "error";
10635
+ await this.savePending(rec);
10636
+ await this.options?.afterCall?.(false);
10637
+ throw err$2;
10548
10638
  }
10549
10639
  const err$1 = this.emitError(4, "Email recovery polling was cancelled");
10550
10640
  await this.options?.afterCall?.(false);
10551
10641
  throw err$1;
10552
10642
  }
10553
- async finalizeRegistration(rec) {
10554
- this.phase = EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
10555
- this.emit({
10556
- step: 5,
10557
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10558
- status: EmailRecoveryStatus.PROGRESS,
10559
- message: "Finalizing email recovery registration...",
10560
- data: {
10561
- accountId: rec.accountId,
10562
- nearPublicKey: rec.nearPublicKey
10563
- }
10564
- });
10643
+ initializeNonceManager(rec) {
10565
10644
  const nonceManager = this.context.webAuthnManager.getNonceManager();
10566
10645
  const accountId = toAccountId(rec.accountId);
10567
10646
  nonceManager.initializeUser(accountId, rec.nearPublicKey);
10647
+ return {
10648
+ nonceManager,
10649
+ accountId
10650
+ };
10651
+ }
10652
+ async signRegistrationTx(rec, accountId) {
10653
+ const vrfChallenge = rec.vrfChallenge;
10654
+ if (!vrfChallenge) return this.fail(5, "Missing VRF challenge for email recovery registration");
10655
+ const registrationResult = await this.context.webAuthnManager.signDevice2RegistrationWithStoredKey({
10656
+ nearAccountId: accountId,
10657
+ credential: rec.credential,
10658
+ vrfChallenge,
10659
+ deterministicVrfPublicKey: rec.vrfPublicKey,
10660
+ deviceNumber: rec.deviceNumber
10661
+ });
10662
+ if (!registrationResult.success || !registrationResult.signedTransaction) await this.fail(5, registrationResult.error || "Failed to sign email recovery registration transaction");
10663
+ return registrationResult.signedTransaction;
10664
+ }
10665
+ async broadcastRegistrationTxAndWaitFinal(rec, signedTx) {
10568
10666
  try {
10569
- if (!rec.vrfChallenge) {
10570
- const err$1 = this.emitError(5, "Missing VRF challenge for email recovery registration");
10571
- await this.options?.afterCall?.(false);
10572
- throw err$1;
10573
- }
10574
- const registrationResult = await this.context.webAuthnManager.signDevice2RegistrationWithStoredKey({
10575
- nearAccountId: accountId,
10576
- credential: rec.credential,
10577
- vrfChallenge: rec.vrfChallenge,
10578
- deterministicVrfPublicKey: rec.vrfPublicKey,
10579
- deviceNumber: rec.deviceNumber
10580
- });
10581
- if (!registrationResult.success || !registrationResult.signedTransaction) {
10582
- const err$1 = this.emitError(5, registrationResult.error || "Failed to sign email recovery registration transaction");
10583
- await this.options?.afterCall?.(false);
10584
- throw err$1;
10585
- }
10586
- const signedTx = registrationResult.signedTransaction;
10667
+ const txResult = await this.context.nearClient.sendTransaction(signedTx, DEFAULT_WAIT_STATUS.linkDeviceRegistration);
10587
10668
  try {
10588
- const txResult = await this.context.nearClient.sendTransaction(signedTx, DEFAULT_WAIT_STATUS.linkDeviceRegistration);
10589
- try {
10590
- const txHash = txResult?.transaction?.hash || txResult?.transaction_hash;
10591
- if (txHash) {
10592
- this.emit({
10593
- step: 5,
10594
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10595
- status: EmailRecoveryStatus.PROGRESS,
10596
- message: "Registration transaction confirmed",
10597
- data: {
10598
- accountId: rec.accountId,
10599
- nearPublicKey: rec.nearPublicKey,
10600
- transactionHash: txHash
10601
- }
10602
- });
10603
- try {
10604
- await IndexedDBManager.clientDB.storeWebAuthnUserData({
10605
- nearAccountId: accountId,
10606
- deviceNumber: rec.deviceNumber,
10607
- clientNearPublicKey: rec.nearPublicKey,
10608
- passkeyCredential: {
10609
- id: rec.credential.id,
10610
- rawId: rec.credential.rawId
10611
- },
10612
- encryptedVrfKeypair: rec.encryptedVrfKeypair,
10613
- serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
10614
- });
10615
- const { syncAuthenticatorsContractCall: syncAuthenticatorsContractCall$1 } = await import("./rpcCalls-DEv9x5-f.js");
10616
- const authenticators = await syncAuthenticatorsContractCall$1(this.context.nearClient, this.context.configs.contractId, accountId);
10617
- const mappedAuthenticators = authenticators.map(({ authenticator }) => ({
10618
- credentialId: authenticator.credentialId,
10619
- credentialPublicKey: authenticator.credentialPublicKey,
10620
- transports: authenticator.transports,
10621
- name: authenticator.name,
10622
- registered: authenticator.registered.toISOString(),
10623
- vrfPublicKey: authenticator.vrfPublicKeys?.[0] || "",
10624
- deviceNumber: authenticator.deviceNumber
10625
- }));
10626
- await IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
10627
- await IndexedDBManager.clientDB.setLastUser(accountId, rec.deviceNumber);
10628
- } catch (syncErr) {
10629
- console.warn("[EmailRecoveryFlow] Failed to sync authenticators after recovery:", syncErr);
10630
- }
10669
+ const txHash = txResult?.transaction?.hash || txResult?.transaction_hash;
10670
+ if (txHash) this.emit({
10671
+ step: 5,
10672
+ phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10673
+ status: EmailRecoveryStatus.PROGRESS,
10674
+ message: "Registration transaction confirmed",
10675
+ data: {
10676
+ accountId: rec.accountId,
10677
+ nearPublicKey: rec.nearPublicKey,
10678
+ transactionHash: txHash
10631
10679
  }
10632
- } catch {}
10633
- } catch (e) {
10634
- const msg = String(e?.message || "");
10635
- const err$1 = this.emitError(5, msg || "Failed to broadcast email recovery registration transaction (insufficient funds or RPC error)");
10636
- await this.options?.afterCall?.(false);
10637
- throw err$1;
10638
- }
10639
- try {
10640
- const txNonce = signedTx.transaction?.nonce;
10641
- if (txNonce != null) await nonceManager.updateNonceFromBlockchain(this.context.nearClient, String(txNonce));
10680
+ });
10681
+ return txHash;
10642
10682
  } catch {}
10643
- const { webAuthnManager } = this.context;
10644
- await webAuthnManager.storeUserData({
10683
+ } catch (e) {
10684
+ const msg = String(e?.message || "");
10685
+ await this.fail(5, msg || "Failed to broadcast email recovery registration transaction (insufficient funds or RPC error)");
10686
+ }
10687
+ return void 0;
10688
+ }
10689
+ async persistRecoveredUserRecordBestEffort(rec, accountId) {
10690
+ try {
10691
+ await IndexedDBManager.clientDB.storeWebAuthnUserData({
10645
10692
  nearAccountId: accountId,
10646
10693
  deviceNumber: rec.deviceNumber,
10647
10694
  clientNearPublicKey: rec.nearPublicKey,
10648
- lastUpdated: Date.now(),
10649
10695
  passkeyCredential: {
10650
10696
  id: rec.credential.id,
10651
10697
  rawId: rec.credential.rawId
10652
10698
  },
10653
- encryptedVrfKeypair: {
10654
- encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
10655
- chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
10656
- },
10699
+ encryptedVrfKeypair: rec.encryptedVrfKeypair,
10657
10700
  serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
10658
10701
  });
10659
- try {
10660
- const attestationB64u = rec.credential.response.attestationObject;
10661
- const credentialPublicKey = await webAuthnManager.extractCosePublicKey(attestationB64u);
10662
- await webAuthnManager.storeAuthenticator({
10663
- nearAccountId: accountId,
10664
- deviceNumber: rec.deviceNumber,
10665
- credentialId: rec.credential.rawId,
10666
- credentialPublicKey,
10667
- transports: ["internal"],
10668
- name: `Device ${rec.deviceNumber} Passkey for ${rec.accountId.split(".")[0]}`,
10669
- registered: (/* @__PURE__ */ new Date()).toISOString(),
10670
- syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
10671
- vrfPublicKey: rec.vrfPublicKey
10672
- });
10673
- } catch {}
10674
- await this.attemptAutoLogin(rec);
10675
- rec.status = "complete";
10676
- await this.savePending(rec);
10677
- await this.clearPending(rec.accountId, rec.nearPublicKey);
10678
- this.phase = EmailRecoveryPhase.STEP_6_COMPLETE;
10679
- this.emit({
10680
- step: 6,
10681
- phase: EmailRecoveryPhase.STEP_6_COMPLETE,
10682
- status: EmailRecoveryStatus.SUCCESS,
10683
- message: "Email recovery completed successfully",
10684
- data: {
10685
- accountId: rec.accountId,
10686
- nearPublicKey: rec.nearPublicKey
10687
- }
10688
- });
10689
- } catch (e) {
10690
- const err$1 = this.emitError(5, e?.message || "Email recovery finalization failed");
10691
- await this.options?.afterCall?.(false);
10692
- throw err$1;
10702
+ return true;
10703
+ } catch (err$1) {
10704
+ console.warn("[EmailRecoveryFlow] Failed to store recovery user record:", err$1);
10705
+ return false;
10693
10706
  }
10694
10707
  }
10695
- async attemptAutoLogin(rec) {
10708
+ mapAuthenticatorsFromContract(authenticators) {
10709
+ return authenticators.map(({ authenticator }) => ({
10710
+ credentialId: authenticator.credentialId,
10711
+ credentialPublicKey: authenticator.credentialPublicKey,
10712
+ transports: authenticator.transports,
10713
+ name: authenticator.name,
10714
+ registered: authenticator.registered.toISOString(),
10715
+ vrfPublicKey: authenticator.vrfPublicKeys?.[0] || "",
10716
+ deviceNumber: authenticator.deviceNumber
10717
+ }));
10718
+ }
10719
+ async syncAuthenticatorsBestEffort(accountId) {
10696
10720
  try {
10697
- this.emit({
10698
- step: 5,
10699
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10700
- status: EmailRecoveryStatus.PROGRESS,
10701
- message: "Attempting auto-login with recovered device...",
10702
- data: { autoLogin: "progress" }
10721
+ const { syncAuthenticatorsContractCall: syncAuthenticatorsContractCall$1 } = await import("./rpcCalls-BQrJMTdg.js");
10722
+ const authenticators = await syncAuthenticatorsContractCall$1(this.context.nearClient, this.context.configs.contractId, accountId);
10723
+ const mappedAuthenticators = this.mapAuthenticatorsFromContract(authenticators);
10724
+ await IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
10725
+ return true;
10726
+ } catch (err$1) {
10727
+ console.warn("[EmailRecoveryFlow] Failed to sync authenticators after recovery:", err$1);
10728
+ return false;
10729
+ }
10730
+ }
10731
+ async setLastUserBestEffort(accountId, deviceNumber) {
10732
+ try {
10733
+ await IndexedDBManager.clientDB.setLastUser(accountId, deviceNumber);
10734
+ return true;
10735
+ } catch (err$1) {
10736
+ console.warn("[EmailRecoveryFlow] Failed to set last user after recovery:", err$1);
10737
+ return false;
10738
+ }
10739
+ }
10740
+ async updateNonceBestEffort(nonceManager, signedTx) {
10741
+ try {
10742
+ const txNonce = signedTx.transaction?.nonce;
10743
+ if (txNonce != null) await nonceManager.updateNonceFromBlockchain(this.context.nearClient, String(txNonce));
10744
+ } catch {}
10745
+ }
10746
+ async persistRecoveredUserData(rec, accountId) {
10747
+ const { webAuthnManager } = this.context;
10748
+ const payload = {
10749
+ nearAccountId: accountId,
10750
+ deviceNumber: rec.deviceNumber,
10751
+ clientNearPublicKey: rec.nearPublicKey,
10752
+ lastUpdated: Date.now(),
10753
+ passkeyCredential: {
10754
+ id: rec.credential.id,
10755
+ rawId: rec.credential.rawId
10756
+ },
10757
+ encryptedVrfKeypair: {
10758
+ encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
10759
+ chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
10760
+ },
10761
+ serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
10762
+ };
10763
+ await webAuthnManager.storeUserData(payload);
10764
+ }
10765
+ async persistAuthenticatorBestEffort(rec, accountId) {
10766
+ try {
10767
+ const { webAuthnManager } = this.context;
10768
+ const attestationB64u = rec.credential.response.attestationObject;
10769
+ const credentialPublicKey = await webAuthnManager.extractCosePublicKey(attestationB64u);
10770
+ await webAuthnManager.storeAuthenticator({
10771
+ nearAccountId: accountId,
10772
+ deviceNumber: rec.deviceNumber,
10773
+ credentialId: rec.credential.rawId,
10774
+ credentialPublicKey,
10775
+ transports: ["internal"],
10776
+ name: `Device ${rec.deviceNumber} Passkey for ${rec.accountId.split(".")[0]}`,
10777
+ registered: (/* @__PURE__ */ new Date()).toISOString(),
10778
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
10779
+ vrfPublicKey: rec.vrfPublicKey
10703
10780
  });
10781
+ } catch {}
10782
+ }
10783
+ async markCompleteAndClearPending(rec) {
10784
+ rec.status = "complete";
10785
+ await this.savePending(rec);
10786
+ await this.clearPending(rec.accountId, rec.nearPublicKey);
10787
+ }
10788
+ async assertVrfActiveForAccount(accountId, message) {
10789
+ const vrfStatus = await this.context.webAuthnManager.checkVrfStatus();
10790
+ const vrfActiveForAccount = vrfStatus.active && vrfStatus.nearAccountId && String(vrfStatus.nearAccountId) === String(accountId);
10791
+ if (!vrfActiveForAccount) throw new Error(message);
10792
+ }
10793
+ async finalizeLocalLoginState(accountId, deviceNumber) {
10794
+ const { webAuthnManager } = this.context;
10795
+ await webAuthnManager.setLastUser(accountId, deviceNumber);
10796
+ await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
10797
+ try {
10798
+ await getLoginSession(this.context, accountId);
10799
+ } catch {}
10800
+ }
10801
+ async tryShamirUnlock(rec, accountId, deviceNumber) {
10802
+ if (!rec.serverEncryptedVrfKeypair || !rec.serverEncryptedVrfKeypair.serverKeyId || !this.context.configs.vrfWorkerConfigs?.shamir3pass?.relayServerUrl) return false;
10803
+ try {
10804
+ const { webAuthnManager } = this.context;
10805
+ const unlockResult = await webAuthnManager.shamir3PassDecryptVrfKeypair({
10806
+ nearAccountId: accountId,
10807
+ kek_s_b64u: rec.serverEncryptedVrfKeypair.kek_s_b64u,
10808
+ ciphertextVrfB64u: rec.serverEncryptedVrfKeypair.ciphertextVrfB64u,
10809
+ serverKeyId: rec.serverEncryptedVrfKeypair.serverKeyId
10810
+ });
10811
+ if (!unlockResult.success) return false;
10812
+ await this.assertVrfActiveForAccount(accountId, "VRF session inactive after Shamir3Pass unlock");
10813
+ await this.finalizeLocalLoginState(accountId, deviceNumber);
10814
+ return true;
10815
+ } catch (err$1) {
10816
+ console.warn("[EmailRecoveryFlow] Shamir 3-pass unlock failed, falling back to TouchID", err$1);
10817
+ return false;
10818
+ }
10819
+ }
10820
+ async tryTouchIdUnlock(rec, accountId, deviceNumber) {
10821
+ try {
10704
10822
  const { webAuthnManager } = this.context;
10705
- const accountId = toAccountId(rec.accountId);
10706
- const deviceNumber = parseDeviceNumber(rec.deviceNumber, { min: 1 });
10707
- if (deviceNumber === null) throw new Error(`Invalid deviceNumber for auto-login: ${String(rec.deviceNumber)}`);
10708
- if (rec.serverEncryptedVrfKeypair && rec.serverEncryptedVrfKeypair.serverKeyId && this.context.configs.vrfWorkerConfigs?.shamir3pass?.relayServerUrl) try {
10709
- const unlockResult = await webAuthnManager.shamir3PassDecryptVrfKeypair({
10710
- nearAccountId: accountId,
10711
- kek_s_b64u: rec.serverEncryptedVrfKeypair.kek_s_b64u,
10712
- ciphertextVrfB64u: rec.serverEncryptedVrfKeypair.ciphertextVrfB64u,
10713
- serverKeyId: rec.serverEncryptedVrfKeypair.serverKeyId
10714
- });
10715
- if (unlockResult.success) {
10716
- const vrfStatus$1 = await webAuthnManager.checkVrfStatus();
10717
- const vrfActiveForAccount$1 = vrfStatus$1.active && vrfStatus$1.nearAccountId && String(vrfStatus$1.nearAccountId) === String(accountId);
10718
- if (!vrfActiveForAccount$1) throw new Error("VRF session inactive after Shamir3Pass unlock");
10719
- await webAuthnManager.setLastUser(accountId, deviceNumber);
10720
- await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
10721
- try {
10722
- await getLoginSession(this.context, accountId);
10723
- } catch {}
10724
- this.emit({
10725
- step: 5,
10726
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10727
- status: EmailRecoveryStatus.SUCCESS,
10728
- message: `Welcome ${accountId}`,
10729
- data: { autoLogin: "success" }
10730
- });
10731
- return;
10732
- }
10733
- } catch (err$1) {
10734
- console.warn("[EmailRecoveryFlow] Shamir 3-pass unlock failed, falling back to TouchID", err$1);
10735
- }
10736
10823
  const authChallenge = createRandomVRFChallenge();
10737
10824
  const storedCredentialId = String(rec.credential?.rawId || rec.credential?.id || "").trim();
10738
10825
  const credentialIds = storedCredentialId ? [storedCredentialId] : [];
@@ -10742,43 +10829,108 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
10742
10829
  challenge: authChallenge,
10743
10830
  credentialIds: credentialIds.length > 0 ? credentialIds : authenticators.map((a) => a.credentialId)
10744
10831
  });
10745
- if (storedCredentialId && authCredential.rawId !== storedCredentialId) throw new Error("Wrong passkey selected during recovery auto-login; please use the newly recovered passkey.");
10832
+ if (storedCredentialId && authCredential.rawId !== storedCredentialId) return {
10833
+ success: false,
10834
+ reason: "Wrong passkey selected during recovery auto-login; please use the newly recovered passkey."
10835
+ };
10746
10836
  const vrfUnlockResult = await webAuthnManager.unlockVRFKeypair({
10747
10837
  nearAccountId: accountId,
10748
10838
  encryptedVrfKeypair: rec.encryptedVrfKeypair,
10749
10839
  credential: authCredential
10750
10840
  });
10751
- if (!vrfUnlockResult.success) throw new Error(vrfUnlockResult.error || "VRF unlock failed during auto-login");
10752
- const vrfStatus = await webAuthnManager.checkVrfStatus();
10753
- const vrfActiveForAccount = vrfStatus.active && vrfStatus.nearAccountId && String(vrfStatus.nearAccountId) === String(accountId);
10754
- if (!vrfActiveForAccount) throw new Error("VRF session inactive after TouchID unlock");
10755
- await webAuthnManager.setLastUser(accountId, deviceNumber);
10756
- await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
10757
- try {
10758
- await getLoginSession(this.context, accountId);
10759
- } catch {}
10760
- this.emit({
10761
- step: 5,
10762
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10763
- status: EmailRecoveryStatus.SUCCESS,
10764
- message: `Welcome ${accountId}`,
10765
- data: { autoLogin: "success" }
10766
- });
10841
+ if (!vrfUnlockResult.success) return {
10842
+ success: false,
10843
+ reason: vrfUnlockResult.error || "VRF unlock failed during auto-login"
10844
+ };
10845
+ await this.assertVrfActiveForAccount(accountId, "VRF session inactive after TouchID unlock");
10846
+ await this.finalizeLocalLoginState(accountId, deviceNumber);
10847
+ return { success: true };
10767
10848
  } catch (err$1) {
10768
- console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err$1);
10769
- try {
10770
- await this.context.webAuthnManager.clearVrfSession();
10771
- } catch {}
10849
+ return {
10850
+ success: false,
10851
+ reason: err$1?.message || String(err$1)
10852
+ };
10853
+ }
10854
+ }
10855
+ async handleAutoLoginFailure(reason, err$1) {
10856
+ console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err$1 ?? reason);
10857
+ try {
10858
+ await this.context.webAuthnManager.clearVrfSession();
10859
+ } catch {}
10860
+ return {
10861
+ success: false,
10862
+ reason
10863
+ };
10864
+ }
10865
+ async finalizeRegistration(rec) {
10866
+ this.phase = EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
10867
+ this.emit({
10868
+ step: 5,
10869
+ phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10870
+ status: EmailRecoveryStatus.PROGRESS,
10871
+ message: "Finalizing email recovery registration...",
10872
+ data: {
10873
+ accountId: rec.accountId,
10874
+ nearPublicKey: rec.nearPublicKey
10875
+ }
10876
+ });
10877
+ try {
10878
+ const { nonceManager, accountId } = this.initializeNonceManager(rec);
10879
+ const signedTx = await this.signRegistrationTx(rec, accountId);
10880
+ const txHash = await this.broadcastRegistrationTxAndWaitFinal(rec, signedTx);
10881
+ if (txHash) {
10882
+ const storedUser = await this.persistRecoveredUserRecordBestEffort(rec, accountId);
10883
+ if (storedUser) {
10884
+ const syncedAuthenticators = await this.syncAuthenticatorsBestEffort(accountId);
10885
+ if (syncedAuthenticators) await this.setLastUserBestEffort(accountId, rec.deviceNumber);
10886
+ }
10887
+ }
10888
+ await this.updateNonceBestEffort(nonceManager, signedTx);
10889
+ await this.persistRecoveredUserData(rec, accountId);
10890
+ await this.persistAuthenticatorBestEffort(rec, accountId);
10891
+ this.emitAutoLoginEvent(EmailRecoveryStatus.PROGRESS, "Attempting auto-login with recovered device...", { autoLogin: "progress" });
10892
+ const autoLoginResult = await this.attemptAutoLogin(rec);
10893
+ if (autoLoginResult.success) this.emitAutoLoginEvent(EmailRecoveryStatus.SUCCESS, `Welcome ${accountId}`, { autoLogin: "success" });
10894
+ else this.emitAutoLoginEvent(EmailRecoveryStatus.ERROR, "Auto-login failed; please log in manually on this device.", {
10895
+ error: autoLoginResult.reason,
10896
+ autoLogin: "error"
10897
+ });
10898
+ await this.markCompleteAndClearPending(rec);
10899
+ this.phase = EmailRecoveryPhase.STEP_6_COMPLETE;
10772
10900
  this.emit({
10773
- step: 5,
10774
- phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
10775
- status: EmailRecoveryStatus.ERROR,
10776
- message: "Auto-login failed; please log in manually on this device.",
10901
+ step: 6,
10902
+ phase: EmailRecoveryPhase.STEP_6_COMPLETE,
10903
+ status: EmailRecoveryStatus.SUCCESS,
10904
+ message: "Email recovery completed successfully",
10777
10905
  data: {
10778
- error: err$1?.message || String(err$1),
10779
- autoLogin: "error"
10906
+ accountId: rec.accountId,
10907
+ nearPublicKey: rec.nearPublicKey
10780
10908
  }
10781
10909
  });
10910
+ } catch (e) {
10911
+ const err$1 = this.emitError(5, e?.message || "Email recovery finalization failed");
10912
+ await this.options?.afterCall?.(false);
10913
+ throw err$1;
10914
+ }
10915
+ }
10916
+ async attemptAutoLogin(rec) {
10917
+ try {
10918
+ const accountId = toAccountId(rec.accountId);
10919
+ const deviceNumber = parseDeviceNumber(rec.deviceNumber, { min: 1 });
10920
+ if (deviceNumber === null) return this.handleAutoLoginFailure(`Invalid deviceNumber for auto-login: ${String(rec.deviceNumber)}`);
10921
+ const shamirUnlocked = await this.tryShamirUnlock(rec, accountId, deviceNumber);
10922
+ if (shamirUnlocked) return {
10923
+ success: true,
10924
+ method: "shamir"
10925
+ };
10926
+ const touchIdResult = await this.tryTouchIdUnlock(rec, accountId, deviceNumber);
10927
+ if (touchIdResult.success) return {
10928
+ success: true,
10929
+ method: "touchid"
10930
+ };
10931
+ return this.handleAutoLoginFailure(touchIdResult.reason || "Auto-login failed");
10932
+ } catch (err$1) {
10933
+ return this.handleAutoLoginFailure(err$1?.message || String(err$1), err$1);
10782
10934
  }
10783
10935
  }
10784
10936
  };
@@ -10793,6 +10945,7 @@ init_IndexedDBManager();
10793
10945
  init_actions();
10794
10946
  init_errors();
10795
10947
  init_rpcCalls();
10948
+ init_EmailRecovery();
10796
10949
  init_defaultConfigs();
10797
10950
  let warnedAboutSameOriginWallet = false;
10798
10951
  /**
@@ -10849,7 +11002,7 @@ var TatchiPasskey = class {
10849
11002
  } catch {}
10850
11003
  if (!this.iframeRouter) {
10851
11004
  if (!this.walletIframeInitInFlight) this.walletIframeInitInFlight = (async () => {
10852
- const { WalletIframeRouter } = await import("./router-BLFegW7J.js");
11005
+ const { WalletIframeRouter } = await import("./router-DuGYOd3G.js");
10853
11006
  this.iframeRouter = new WalletIframeRouter({
10854
11007
  walletOrigin,
10855
11008
  servicePath: walletIframeConfig?.walletServicePath || "/wallet-service",