@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.
- 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 +519 -448
- 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/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 +519 -448
- 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/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 +519 -448
- 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/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 +519 -448
- 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/rpcCalls.js +8 -1
- package/dist/esm/react/sdk/src/core/rpcCalls.js.map +1 -1
- package/dist/esm/sdk/offline-export-app.js.map +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/wallet-iframe-host.js +624 -471
- 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 +35 -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/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);
|
|
98
129
|
}
|
|
99
|
-
|
|
100
|
-
|
|
130
|
+
computeAvailableBalance(accountView) {
|
|
131
|
+
const STORAGE_PRICE_PER_BYTE = BigInt("10000000000000000000");
|
|
132
|
+
const amount = this.toBigInt(accountView.amount);
|
|
133
|
+
const locked = this.toBigInt(accountView.locked);
|
|
134
|
+
const storageUsage = this.toBigInt(accountView.storage_usage);
|
|
135
|
+
const storageCost = storageUsage * STORAGE_PRICE_PER_BYTE;
|
|
136
|
+
const rawAvailable = amount - locked - storageCost;
|
|
137
|
+
return rawAvailable > 0 ? rawAvailable : BigInt(0);
|
|
101
138
|
}
|
|
102
|
-
async
|
|
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
|
+
});
|
|
216
|
+
}
|
|
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
|
+
});
|
|
225
|
+
}
|
|
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,242 @@ 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 {}
|
|
648
|
-
|
|
649
|
-
|
|
607
|
+
} catch (e) {
|
|
608
|
+
const msg = String(e?.message || "");
|
|
609
|
+
await this.fail(5, msg || "Failed to broadcast email recovery registration transaction (insufficient funds or RPC error)");
|
|
610
|
+
}
|
|
611
|
+
return void 0;
|
|
612
|
+
}
|
|
613
|
+
async persistRecoveredUserRecordBestEffort(rec, accountId) {
|
|
614
|
+
try {
|
|
615
|
+
await IndexedDBManager.clientDB.storeWebAuthnUserData({
|
|
650
616
|
nearAccountId: accountId,
|
|
651
617
|
deviceNumber: rec.deviceNumber,
|
|
652
618
|
clientNearPublicKey: rec.nearPublicKey,
|
|
653
|
-
lastUpdated: Date.now(),
|
|
654
619
|
passkeyCredential: {
|
|
655
620
|
id: rec.credential.id,
|
|
656
621
|
rawId: rec.credential.rawId
|
|
657
622
|
},
|
|
658
|
-
encryptedVrfKeypair:
|
|
659
|
-
encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
|
|
660
|
-
chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
|
|
661
|
-
},
|
|
623
|
+
encryptedVrfKeypair: rec.encryptedVrfKeypair,
|
|
662
624
|
serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
|
|
663
625
|
});
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
}
|
|
693
|
-
});
|
|
694
|
-
} catch (e) {
|
|
695
|
-
const err = this.emitError(5, e?.message || "Email recovery finalization failed");
|
|
696
|
-
await this.options?.afterCall?.(false);
|
|
697
|
-
throw err;
|
|
626
|
+
return true;
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.warn("[EmailRecoveryFlow] Failed to store recovery user record:", err);
|
|
629
|
+
return false;
|
|
698
630
|
}
|
|
699
631
|
}
|
|
700
|
-
|
|
632
|
+
mapAuthenticatorsFromContract(authenticators) {
|
|
633
|
+
return authenticators.map(({ authenticator }) => ({
|
|
634
|
+
credentialId: authenticator.credentialId,
|
|
635
|
+
credentialPublicKey: authenticator.credentialPublicKey,
|
|
636
|
+
transports: authenticator.transports,
|
|
637
|
+
name: authenticator.name,
|
|
638
|
+
registered: authenticator.registered.toISOString(),
|
|
639
|
+
vrfPublicKey: authenticator.vrfPublicKeys?.[0] || "",
|
|
640
|
+
deviceNumber: authenticator.deviceNumber
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
async syncAuthenticatorsBestEffort(accountId) {
|
|
701
644
|
try {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
645
|
+
const { syncAuthenticatorsContractCall } = await import("../rpcCalls.js");
|
|
646
|
+
const authenticators = await syncAuthenticatorsContractCall(this.context.nearClient, this.context.configs.contractId, accountId);
|
|
647
|
+
const mappedAuthenticators = this.mapAuthenticatorsFromContract(authenticators);
|
|
648
|
+
await IndexedDBManager.clientDB.syncAuthenticatorsFromContract(accountId, mappedAuthenticators);
|
|
649
|
+
return true;
|
|
650
|
+
} catch (err) {
|
|
651
|
+
console.warn("[EmailRecoveryFlow] Failed to sync authenticators after recovery:", err);
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
async setLastUserBestEffort(accountId, deviceNumber) {
|
|
656
|
+
try {
|
|
657
|
+
await IndexedDBManager.clientDB.setLastUser(accountId, deviceNumber);
|
|
658
|
+
return true;
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.warn("[EmailRecoveryFlow] Failed to set last user after recovery:", err);
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async updateNonceBestEffort(nonceManager, signedTx) {
|
|
665
|
+
try {
|
|
666
|
+
const txNonce = signedTx.transaction?.nonce;
|
|
667
|
+
if (txNonce != null) await nonceManager.updateNonceFromBlockchain(this.context.nearClient, String(txNonce));
|
|
668
|
+
} catch {}
|
|
669
|
+
}
|
|
670
|
+
async persistRecoveredUserData(rec, accountId) {
|
|
671
|
+
const { webAuthnManager } = this.context;
|
|
672
|
+
const payload = {
|
|
673
|
+
nearAccountId: accountId,
|
|
674
|
+
deviceNumber: rec.deviceNumber,
|
|
675
|
+
clientNearPublicKey: rec.nearPublicKey,
|
|
676
|
+
lastUpdated: Date.now(),
|
|
677
|
+
passkeyCredential: {
|
|
678
|
+
id: rec.credential.id,
|
|
679
|
+
rawId: rec.credential.rawId
|
|
680
|
+
},
|
|
681
|
+
encryptedVrfKeypair: {
|
|
682
|
+
encryptedVrfDataB64u: rec.encryptedVrfKeypair.encryptedVrfDataB64u,
|
|
683
|
+
chacha20NonceB64u: rec.encryptedVrfKeypair.chacha20NonceB64u
|
|
684
|
+
},
|
|
685
|
+
serverEncryptedVrfKeypair: rec.serverEncryptedVrfKeypair || void 0
|
|
686
|
+
};
|
|
687
|
+
await webAuthnManager.storeUserData(payload);
|
|
688
|
+
}
|
|
689
|
+
async persistAuthenticatorBestEffort(rec, accountId) {
|
|
690
|
+
try {
|
|
691
|
+
const { webAuthnManager } = this.context;
|
|
692
|
+
const attestationB64u = rec.credential.response.attestationObject;
|
|
693
|
+
const credentialPublicKey = await webAuthnManager.extractCosePublicKey(attestationB64u);
|
|
694
|
+
await webAuthnManager.storeAuthenticator({
|
|
695
|
+
nearAccountId: accountId,
|
|
696
|
+
deviceNumber: rec.deviceNumber,
|
|
697
|
+
credentialId: rec.credential.rawId,
|
|
698
|
+
credentialPublicKey,
|
|
699
|
+
transports: ["internal"],
|
|
700
|
+
name: `Device ${rec.deviceNumber} Passkey for ${rec.accountId.split(".")[0]}`,
|
|
701
|
+
registered: (/* @__PURE__ */ new Date()).toISOString(),
|
|
702
|
+
syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
703
|
+
vrfPublicKey: rec.vrfPublicKey
|
|
708
704
|
});
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
707
|
+
async markCompleteAndClearPending(rec) {
|
|
708
|
+
rec.status = "complete";
|
|
709
|
+
await this.savePending(rec);
|
|
710
|
+
await this.clearPending(rec.accountId, rec.nearPublicKey);
|
|
711
|
+
}
|
|
712
|
+
async assertVrfActiveForAccount(accountId, message) {
|
|
713
|
+
const vrfStatus = await this.context.webAuthnManager.checkVrfStatus();
|
|
714
|
+
const vrfActiveForAccount = vrfStatus.active && vrfStatus.nearAccountId && String(vrfStatus.nearAccountId) === String(accountId);
|
|
715
|
+
if (!vrfActiveForAccount) throw new Error(message);
|
|
716
|
+
}
|
|
717
|
+
async finalizeLocalLoginState(accountId, deviceNumber) {
|
|
718
|
+
const { webAuthnManager } = this.context;
|
|
719
|
+
await webAuthnManager.setLastUser(accountId, deviceNumber);
|
|
720
|
+
await webAuthnManager.initializeCurrentUser(accountId, this.context.nearClient);
|
|
721
|
+
try {
|
|
722
|
+
await getLoginSession(this.context, accountId);
|
|
723
|
+
} catch {}
|
|
724
|
+
}
|
|
725
|
+
async tryShamirUnlock(rec, accountId, deviceNumber) {
|
|
726
|
+
if (!rec.serverEncryptedVrfKeypair || !rec.serverEncryptedVrfKeypair.serverKeyId || !this.context.configs.vrfWorkerConfigs?.shamir3pass?.relayServerUrl) return false;
|
|
727
|
+
try {
|
|
728
|
+
const { webAuthnManager } = this.context;
|
|
729
|
+
const unlockResult = await webAuthnManager.shamir3PassDecryptVrfKeypair({
|
|
730
|
+
nearAccountId: accountId,
|
|
731
|
+
kek_s_b64u: rec.serverEncryptedVrfKeypair.kek_s_b64u,
|
|
732
|
+
ciphertextVrfB64u: rec.serverEncryptedVrfKeypair.ciphertextVrfB64u,
|
|
733
|
+
serverKeyId: rec.serverEncryptedVrfKeypair.serverKeyId
|
|
734
|
+
});
|
|
735
|
+
if (!unlockResult.success) return false;
|
|
736
|
+
await this.assertVrfActiveForAccount(accountId, "VRF session inactive after Shamir3Pass unlock");
|
|
737
|
+
await this.finalizeLocalLoginState(accountId, deviceNumber);
|
|
738
|
+
return true;
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.warn("[EmailRecoveryFlow] Shamir 3-pass unlock failed, falling back to TouchID", err);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async tryTouchIdUnlock(rec, accountId, deviceNumber) {
|
|
745
|
+
try {
|
|
709
746
|
const { webAuthnManager } = this.context;
|
|
710
|
-
const accountId = 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
747
|
const authChallenge = createRandomVRFChallenge();
|
|
742
748
|
const storedCredentialId = String(rec.credential?.rawId || rec.credential?.id || "").trim();
|
|
743
749
|
const credentialIds = storedCredentialId ? [storedCredentialId] : [];
|
|
@@ -747,43 +753,108 @@ var init_emailRecovery = __esm({ "src/core/TatchiPasskey/emailRecovery.ts": (()
|
|
|
747
753
|
challenge: authChallenge,
|
|
748
754
|
credentialIds: credentialIds.length > 0 ? credentialIds : authenticators.map((a) => a.credentialId)
|
|
749
755
|
});
|
|
750
|
-
if (storedCredentialId && authCredential.rawId !== storedCredentialId)
|
|
756
|
+
if (storedCredentialId && authCredential.rawId !== storedCredentialId) return {
|
|
757
|
+
success: false,
|
|
758
|
+
reason: "Wrong passkey selected during recovery auto-login; please use the newly recovered passkey."
|
|
759
|
+
};
|
|
751
760
|
const vrfUnlockResult = await webAuthnManager.unlockVRFKeypair({
|
|
752
761
|
nearAccountId: accountId,
|
|
753
762
|
encryptedVrfKeypair: rec.encryptedVrfKeypair,
|
|
754
763
|
credential: authCredential
|
|
755
764
|
});
|
|
756
|
-
if (!vrfUnlockResult.success)
|
|
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
|
-
});
|
|
765
|
+
if (!vrfUnlockResult.success) return {
|
|
766
|
+
success: false,
|
|
767
|
+
reason: vrfUnlockResult.error || "VRF unlock failed during auto-login"
|
|
768
|
+
};
|
|
769
|
+
await this.assertVrfActiveForAccount(accountId, "VRF session inactive after TouchID unlock");
|
|
770
|
+
await this.finalizeLocalLoginState(accountId, deviceNumber);
|
|
771
|
+
return { success: true };
|
|
772
772
|
} catch (err) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
773
|
+
return {
|
|
774
|
+
success: false,
|
|
775
|
+
reason: err?.message || String(err)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async handleAutoLoginFailure(reason, err) {
|
|
780
|
+
console.warn("[EmailRecoveryFlow] Auto-login failed after recovery", err ?? reason);
|
|
781
|
+
try {
|
|
782
|
+
await this.context.webAuthnManager.clearVrfSession();
|
|
783
|
+
} catch {}
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
reason
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async finalizeRegistration(rec) {
|
|
790
|
+
this.phase = EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION;
|
|
791
|
+
this.emit({
|
|
792
|
+
step: 5,
|
|
793
|
+
phase: EmailRecoveryPhase.STEP_5_FINALIZING_REGISTRATION,
|
|
794
|
+
status: EmailRecoveryStatus.PROGRESS,
|
|
795
|
+
message: "Finalizing email recovery registration...",
|
|
796
|
+
data: {
|
|
797
|
+
accountId: rec.accountId,
|
|
798
|
+
nearPublicKey: rec.nearPublicKey
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
try {
|
|
802
|
+
const { nonceManager, accountId } = this.initializeNonceManager(rec);
|
|
803
|
+
const signedTx = await this.signRegistrationTx(rec, accountId);
|
|
804
|
+
const txHash = await this.broadcastRegistrationTxAndWaitFinal(rec, signedTx);
|
|
805
|
+
if (txHash) {
|
|
806
|
+
const storedUser = await this.persistRecoveredUserRecordBestEffort(rec, accountId);
|
|
807
|
+
if (storedUser) {
|
|
808
|
+
const syncedAuthenticators = await this.syncAuthenticatorsBestEffort(accountId);
|
|
809
|
+
if (syncedAuthenticators) await this.setLastUserBestEffort(accountId, rec.deviceNumber);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
await this.updateNonceBestEffort(nonceManager, signedTx);
|
|
813
|
+
await this.persistRecoveredUserData(rec, accountId);
|
|
814
|
+
await this.persistAuthenticatorBestEffort(rec, accountId);
|
|
815
|
+
this.emitAutoLoginEvent(EmailRecoveryStatus.PROGRESS, "Attempting auto-login with recovered device...", { autoLogin: "progress" });
|
|
816
|
+
const autoLoginResult = await this.attemptAutoLogin(rec);
|
|
817
|
+
if (autoLoginResult.success) this.emitAutoLoginEvent(EmailRecoveryStatus.SUCCESS, `Welcome ${accountId}`, { autoLogin: "success" });
|
|
818
|
+
else this.emitAutoLoginEvent(EmailRecoveryStatus.ERROR, "Auto-login failed; please log in manually on this device.", {
|
|
819
|
+
error: autoLoginResult.reason,
|
|
820
|
+
autoLogin: "error"
|
|
821
|
+
});
|
|
822
|
+
await this.markCompleteAndClearPending(rec);
|
|
823
|
+
this.phase = EmailRecoveryPhase.STEP_6_COMPLETE;
|
|
777
824
|
this.emit({
|
|
778
|
-
step:
|
|
779
|
-
phase: EmailRecoveryPhase.
|
|
780
|
-
status: EmailRecoveryStatus.
|
|
781
|
-
message: "
|
|
825
|
+
step: 6,
|
|
826
|
+
phase: EmailRecoveryPhase.STEP_6_COMPLETE,
|
|
827
|
+
status: EmailRecoveryStatus.SUCCESS,
|
|
828
|
+
message: "Email recovery completed successfully",
|
|
782
829
|
data: {
|
|
783
|
-
|
|
784
|
-
|
|
830
|
+
accountId: rec.accountId,
|
|
831
|
+
nearPublicKey: rec.nearPublicKey
|
|
785
832
|
}
|
|
786
833
|
});
|
|
834
|
+
} catch (e) {
|
|
835
|
+
const err = this.emitError(5, e?.message || "Email recovery finalization failed");
|
|
836
|
+
await this.options?.afterCall?.(false);
|
|
837
|
+
throw err;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async attemptAutoLogin(rec) {
|
|
841
|
+
try {
|
|
842
|
+
const accountId = toAccountId(rec.accountId);
|
|
843
|
+
const deviceNumber = parseDeviceNumber(rec.deviceNumber, { min: 1 });
|
|
844
|
+
if (deviceNumber === null) return this.handleAutoLoginFailure(`Invalid deviceNumber for auto-login: ${String(rec.deviceNumber)}`);
|
|
845
|
+
const shamirUnlocked = await this.tryShamirUnlock(rec, accountId, deviceNumber);
|
|
846
|
+
if (shamirUnlocked) return {
|
|
847
|
+
success: true,
|
|
848
|
+
method: "shamir"
|
|
849
|
+
};
|
|
850
|
+
const touchIdResult = await this.tryTouchIdUnlock(rec, accountId, deviceNumber);
|
|
851
|
+
if (touchIdResult.success) return {
|
|
852
|
+
success: true,
|
|
853
|
+
method: "touchid"
|
|
854
|
+
};
|
|
855
|
+
return this.handleAutoLoginFailure(touchIdResult.reason || "Auto-login failed");
|
|
856
|
+
} catch (err) {
|
|
857
|
+
return this.handleAutoLoginFailure(err?.message || String(err), err);
|
|
787
858
|
}
|
|
788
859
|
}
|
|
789
860
|
};
|