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