signer-test-sdk-core 0.0.1
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/README.md +81 -0
- package/dist/src/components/OnboardingUI/auth.d.ts +158 -0
- package/dist/src/components/OnboardingUI/auth.js +230 -0
- package/dist/src/components/OnboardingUI/auth.js.map +1 -0
- package/dist/src/components/OnboardingUI/index.d.ts +9 -0
- package/dist/src/components/OnboardingUI/index.js +9 -0
- package/dist/src/components/OnboardingUI/index.js.map +1 -0
- package/dist/src/components/OnboardingUI/types.d.ts +159 -0
- package/dist/src/components/OnboardingUI/types.js +5 -0
- package/dist/src/components/OnboardingUI/types.js.map +1 -0
- package/dist/src/core/AbstraxnWallet.d.ts +162 -0
- package/dist/src/core/AbstraxnWallet.js +593 -0
- package/dist/src/core/AbstraxnWallet.js.map +1 -0
- package/dist/src/core/AuthManager.d.ts +112 -0
- package/dist/src/core/AuthManager.js +685 -0
- package/dist/src/core/AuthManager.js.map +1 -0
- package/dist/src/core/Signer.d.ts +35 -0
- package/dist/src/core/Signer.js +156 -0
- package/dist/src/core/Signer.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interfaces/IAuth.d.ts +26 -0
- package/dist/src/interfaces/IAuth.js +2 -0
- package/dist/src/interfaces/IAuth.js.map +1 -0
- package/dist/src/interfaces/ISigner.d.ts +14 -0
- package/dist/src/interfaces/ISigner.js +2 -0
- package/dist/src/interfaces/ISigner.js.map +1 -0
- package/dist/src/interfaces/IStorage.d.ts +15 -0
- package/dist/src/interfaces/IStorage.js +2 -0
- package/dist/src/interfaces/IStorage.js.map +1 -0
- package/dist/src/interfaces/IWallet.d.ts +45 -0
- package/dist/src/interfaces/IWallet.js +2 -0
- package/dist/src/interfaces/IWallet.js.map +1 -0
- package/dist/src/services/ApiService.d.ts +131 -0
- package/dist/src/services/ApiService.js +626 -0
- package/dist/src/services/ApiService.js.map +1 -0
- package/dist/src/services/TokenService.d.ts +29 -0
- package/dist/src/services/TokenService.js +40 -0
- package/dist/src/services/TokenService.js.map +1 -0
- package/dist/src/services/TurnkeyService.d.ts +54 -0
- package/dist/src/services/TurnkeyService.js +91 -0
- package/dist/src/services/TurnkeyService.js.map +1 -0
- package/dist/src/storage/IndexedDBStorage.d.ts +39 -0
- package/dist/src/storage/IndexedDBStorage.js +280 -0
- package/dist/src/storage/IndexedDBStorage.js.map +1 -0
- package/dist/src/utils/constants.d.ts +52 -0
- package/dist/src/utils/constants.js +78 -0
- package/dist/src/utils/constants.js.map +1 -0
- package/dist/src/utils/errors.d.ts +23 -0
- package/dist/src/utils/errors.js +48 -0
- package/dist/src/utils/errors.js.map +1 -0
- package/dist/src/utils/helpers.d.ts +12 -0
- package/dist/src/utils/helpers.js +30 -0
- package/dist/src/utils/helpers.js.map +1 -0
- package/dist/src/utils/passkey.d.ts +33 -0
- package/dist/src/utils/passkey.js +122 -0
- package/dist/src/utils/passkey.js.map +1 -0
- package/dist/src/utils/types.d.ts +182 -0
- package/dist/src/utils/types.js +5 -0
- package/dist/src/utils/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { STORAGE_KEYS, TURNKEY_CONFIG, TURNKEY_ENDPOINTS } from '../utils/constants';
|
|
2
|
+
import { AuthenticationError } from '../utils/errors';
|
|
3
|
+
import { generateChallenge, createPasskeyCredential, generateAuthenticatorName, generateUserName, generateOrganizationName, } from '../utils/passkey';
|
|
4
|
+
import { WebauthnStamper } from '@turnkey/webauthn-stamper';
|
|
5
|
+
import { TurnkeyClient } from '@turnkey/http';
|
|
6
|
+
export class AuthManager {
|
|
7
|
+
constructor(apiService, turnkeyService, tokenService, storage) {
|
|
8
|
+
this.apiService = apiService;
|
|
9
|
+
this.turnkeyService = turnkeyService;
|
|
10
|
+
this.tokenService = tokenService;
|
|
11
|
+
this.storage = storage;
|
|
12
|
+
this.currentUser = null;
|
|
13
|
+
this.whoamiPromise = null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Initialize OTP authentication
|
|
17
|
+
*/
|
|
18
|
+
async loginWithOTP(email) {
|
|
19
|
+
// Clear previous IndexedDB data and reset TurnkeyService before starting new login
|
|
20
|
+
await this.turnkeyService.reset();
|
|
21
|
+
// Initialize storage if not already initialized
|
|
22
|
+
await this.turnkeyService.init();
|
|
23
|
+
// Get public key from storage (internal - users don't see this)
|
|
24
|
+
const publicKey = await this.turnkeyService.getPublicKey();
|
|
25
|
+
if (!publicKey) {
|
|
26
|
+
throw new AuthenticationError('Failed to get public key from storage');
|
|
27
|
+
}
|
|
28
|
+
// Initialize OTP with backend
|
|
29
|
+
const response = await this.apiService.initOtp(email);
|
|
30
|
+
return { otpId: response.otpId };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Verify OTP and complete authentication
|
|
34
|
+
*/
|
|
35
|
+
async verifyOTP(otpId, otpCode) {
|
|
36
|
+
// Clear previous IndexedDB data and reset TurnkeyService before verifying OTP
|
|
37
|
+
await this.turnkeyService.reset();
|
|
38
|
+
// Ensure storage is initialized
|
|
39
|
+
await this.turnkeyService.init();
|
|
40
|
+
// Get public key from storage (internal)
|
|
41
|
+
const publicKey = await this.turnkeyService.getPublicKey();
|
|
42
|
+
if (!publicKey) {
|
|
43
|
+
throw new AuthenticationError('Failed to get public key from storage');
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
// Verify OTP with backend
|
|
47
|
+
const response = await this.apiService.verifyOtp(otpId, otpCode, publicKey);
|
|
48
|
+
if (!response.result?.user) {
|
|
49
|
+
throw new AuthenticationError('Failed to get user information');
|
|
50
|
+
}
|
|
51
|
+
// Set user in memory (not storing to storage)
|
|
52
|
+
this.currentUser = response.result.user;
|
|
53
|
+
// Store user in storage
|
|
54
|
+
await this.storage.setItem(STORAGE_KEYS.USER, this.currentUser);
|
|
55
|
+
// Handle whoami call based on isPolicy
|
|
56
|
+
// If isPolicy is true, ONLY call whoami (do NOT call PROXY)
|
|
57
|
+
// If isPolicy is false, call PROXY first, then whoami
|
|
58
|
+
const isPolicy = Boolean(this.currentUser.isPolicy);
|
|
59
|
+
if (isPolicy) {
|
|
60
|
+
// If isPolicy is true, call whoami directly (NO PROXY call)
|
|
61
|
+
try {
|
|
62
|
+
await this.callWhoami();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Log error but don't fail the verification if whoami call fails
|
|
66
|
+
console.warn('Whoami call after OTP verification failed:', error);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
// If isPolicy is false, call PROXY first, then whoami
|
|
71
|
+
try {
|
|
72
|
+
await this.callProxyAfterVerification('otp');
|
|
73
|
+
// Call whoami after PROXY succeeds
|
|
74
|
+
await this.callWhoami();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// Log error but don't fail the verification if PROXY or whoami call fails
|
|
78
|
+
console.warn('PROXY or whoami call after OTP verification failed:', error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return this.currentUser;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// If verify OTP API fails (e.g., duplicate public key error), reset IndexedDB
|
|
85
|
+
// This allows the user to retry with a fresh key
|
|
86
|
+
try {
|
|
87
|
+
await this.turnkeyService.reset();
|
|
88
|
+
}
|
|
89
|
+
catch (resetError) {
|
|
90
|
+
// Log reset error but don't fail - we still want to throw the original error
|
|
91
|
+
console.warn('Failed to reset IndexedDB after verify error:', resetError);
|
|
92
|
+
}
|
|
93
|
+
// Re-throw the original error
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Call PROXY endpoint after successful authentication
|
|
99
|
+
* This is called internally after user is authenticated
|
|
100
|
+
* Calls Turnkey create_policy endpoint via PROXY
|
|
101
|
+
* @param authMethod - 'otp' for OTP login, 'google' for Google OAuth login
|
|
102
|
+
*/
|
|
103
|
+
async callProxyAfterVerification(authMethod = 'otp') {
|
|
104
|
+
if (!this.currentUser?.subOrganizationId) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Different policies for OTP vs OAuth login
|
|
108
|
+
const isOauth = authMethod !== 'otp';
|
|
109
|
+
const policyConfig = isOauth
|
|
110
|
+
? {
|
|
111
|
+
policyName: 'Allow user to verify OTP',
|
|
112
|
+
effect: 'EFFECT_ALLOW',
|
|
113
|
+
condition: 'activity.resource in [\'AUTH\', \'OAUTH\'] && activity.action in [\'CREATE\',\'VERIFY\']',
|
|
114
|
+
consensus: 'true',
|
|
115
|
+
notes: 'Allow user to verify OTP',
|
|
116
|
+
}
|
|
117
|
+
: {
|
|
118
|
+
policyName: 'Allow user to verify OTP',
|
|
119
|
+
effect: 'EFFECT_ALLOW',
|
|
120
|
+
condition: 'activity.resource in [\'AUTH\', \'OTP\'] && activity.action in [\'CREATE\',\'VERIFY\']',
|
|
121
|
+
consensus: 'true',
|
|
122
|
+
notes: 'Allow user to verify OTP',
|
|
123
|
+
};
|
|
124
|
+
// Create request body for PROXY call (matching Turnkey stampCreatePolicy format)
|
|
125
|
+
const body = JSON.stringify({
|
|
126
|
+
timestampMs: new Date().getTime().toString(),
|
|
127
|
+
organizationId: this.currentUser.subOrganizationId,
|
|
128
|
+
parameters: policyConfig,
|
|
129
|
+
type: 'ACTIVITY_TYPE_CREATE_POLICY_V3',
|
|
130
|
+
});
|
|
131
|
+
// Create stamp for the request
|
|
132
|
+
const stamp = await this.turnkeyService.createStamp(body);
|
|
133
|
+
// Call PROXY endpoint to proxy the request to Turnkey
|
|
134
|
+
const turnkeyUrl = `${TURNKEY_CONFIG.BASE_URL}${TURNKEY_ENDPOINTS.CREATE_POLICY}`;
|
|
135
|
+
await this.apiService.callProxy(turnkeyUrl, 'POST', body, stamp);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Login with Google OAuth
|
|
139
|
+
*/
|
|
140
|
+
async loginWithGoogle() {
|
|
141
|
+
// Initialize storage
|
|
142
|
+
await this.turnkeyService.init();
|
|
143
|
+
// Get public key from storage (internal) - force refresh to ensure we get latest from IndexedDB
|
|
144
|
+
// This is important in case user deleted keys from IndexedDB without reloading
|
|
145
|
+
const publicKey = await this.turnkeyService.getPublicKey(true);
|
|
146
|
+
const originUrl = window.location.href;
|
|
147
|
+
if (!publicKey) {
|
|
148
|
+
throw new AuthenticationError('Failed to get public key from storage');
|
|
149
|
+
}
|
|
150
|
+
// Redirect to Google OAuth
|
|
151
|
+
const authUrl = this.apiService.getGoogleAuthUrl(publicKey, originUrl);
|
|
152
|
+
window.location.href = authUrl;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Login with Discord OAuth
|
|
156
|
+
*/
|
|
157
|
+
async loginWithDiscord() {
|
|
158
|
+
await this.turnkeyService.init();
|
|
159
|
+
// Force refresh to ensure we get latest public key from IndexedDB
|
|
160
|
+
const publicKey = await this.turnkeyService.getPublicKey(true);
|
|
161
|
+
const originUrl = window.location.href;
|
|
162
|
+
if (!publicKey) {
|
|
163
|
+
throw new AuthenticationError('Failed to get public key from storage');
|
|
164
|
+
}
|
|
165
|
+
const authUrl = this.apiService.getDiscordAuthUrl(publicKey, originUrl);
|
|
166
|
+
window.location.href = authUrl;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Login with X (Twitter) OAuth
|
|
170
|
+
*/
|
|
171
|
+
async loginWithTwitter() {
|
|
172
|
+
await this.turnkeyService.init();
|
|
173
|
+
// Force refresh to ensure we get latest public key from IndexedDB
|
|
174
|
+
const publicKey = await this.turnkeyService.getPublicKey(true);
|
|
175
|
+
const originUrl = window.location.href;
|
|
176
|
+
if (!publicKey) {
|
|
177
|
+
throw new AuthenticationError('Failed to get public key from storage');
|
|
178
|
+
}
|
|
179
|
+
const authUrl = this.apiService.getTwitterAuthUrl(publicKey, originUrl);
|
|
180
|
+
window.location.href = authUrl;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Handle Google OAuth callback
|
|
184
|
+
* This is called when user returns from Google OAuth
|
|
185
|
+
* Calls the backend API to get accessToken, refreshToken, and user data
|
|
186
|
+
*/
|
|
187
|
+
async handleGoogleCallback() {
|
|
188
|
+
// Initialize storage WITHOUT resetting - this ensures we use the same public key
|
|
189
|
+
// that was used during loginWithGoogle()
|
|
190
|
+
// Turnkey's IndexedDbStamper will automatically reuse existing keys from IndexedDB
|
|
191
|
+
await this.turnkeyService.init();
|
|
192
|
+
const params = new URLSearchParams(window.location.search);
|
|
193
|
+
// Check for errors in URL
|
|
194
|
+
if (params.get('error')) {
|
|
195
|
+
throw new AuthenticationError(params.get('error') || 'Google authentication failed');
|
|
196
|
+
}
|
|
197
|
+
// Extract code and state from URL (standard OAuth flow)
|
|
198
|
+
const code = params.get('code');
|
|
199
|
+
const state = params.get('state');
|
|
200
|
+
// Fallback: Check if tokens are in URL params (legacy flow)
|
|
201
|
+
const accessToken = params.get('accessToken');
|
|
202
|
+
const refreshToken = params.get('refreshToken');
|
|
203
|
+
const userJson = params.get('user');
|
|
204
|
+
// If code and state are present, call the API endpoint
|
|
205
|
+
if (code && state) {
|
|
206
|
+
try {
|
|
207
|
+
const result = await this.apiService.handleGoogleCallback(code, state);
|
|
208
|
+
// Store user
|
|
209
|
+
this.currentUser = result.user;
|
|
210
|
+
await this.storage.setItem(STORAGE_KEYS.USER, result.user);
|
|
211
|
+
// Tokens are already stored by ApiService.handleGoogleCallback
|
|
212
|
+
// Now call whoami API to get user details and store the response
|
|
213
|
+
// whoami MUST succeed for the login to be considered successful
|
|
214
|
+
await this.callWhoami();
|
|
215
|
+
// After whoami succeeds, check if isPolicy is false and call PROXY if needed
|
|
216
|
+
const isPolicy = Boolean(this.currentUser.isPolicy);
|
|
217
|
+
if (!isPolicy) {
|
|
218
|
+
try {
|
|
219
|
+
await this.callProxyAfterVerification('google');
|
|
220
|
+
}
|
|
221
|
+
catch (proxyError) {
|
|
222
|
+
// Log error but don't fail the login if PROXY call fails
|
|
223
|
+
console.warn('PROXY call after Google login failed:', proxyError);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Verify whoami response exists before returning user
|
|
227
|
+
const whoamiResponse = await this.getWhoami();
|
|
228
|
+
if (!whoamiResponse) {
|
|
229
|
+
throw new AuthenticationError('Whoami response not available after successful call');
|
|
230
|
+
}
|
|
231
|
+
return result.user;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
throw new AuthenticationError(error instanceof Error ? error.message : 'Failed to handle Google callback');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Legacy flow: tokens in URL params
|
|
238
|
+
if (accessToken && refreshToken && userJson) {
|
|
239
|
+
// Store tokens
|
|
240
|
+
await this.tokenService.setTokens({ accessToken, refreshToken });
|
|
241
|
+
// Parse and store user
|
|
242
|
+
try {
|
|
243
|
+
const user = JSON.parse(userJson);
|
|
244
|
+
this.currentUser = user;
|
|
245
|
+
await this.storage.setItem(STORAGE_KEYS.USER, user);
|
|
246
|
+
// Call whoami API to get user details and store the response
|
|
247
|
+
// whoami MUST succeed for the login to be considered successful
|
|
248
|
+
await this.callWhoami();
|
|
249
|
+
// After whoami succeeds, check if isPolicy is false and call PROXY if needed
|
|
250
|
+
const isPolicy = Boolean(this.currentUser.isPolicy);
|
|
251
|
+
if (!isPolicy) {
|
|
252
|
+
try {
|
|
253
|
+
await this.callProxyAfterVerification('google');
|
|
254
|
+
}
|
|
255
|
+
catch (proxyError) {
|
|
256
|
+
// Log error but don't fail the login if PROXY call fails
|
|
257
|
+
console.warn('PROXY call after Google login failed (legacy flow):', proxyError);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Verify whoami response exists before returning user
|
|
261
|
+
const whoamiResponse = await this.getWhoami();
|
|
262
|
+
if (!whoamiResponse) {
|
|
263
|
+
throw new AuthenticationError('Whoami response not available after successful call');
|
|
264
|
+
}
|
|
265
|
+
return user;
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
269
|
+
throw new AuthenticationError(`Failed to parse user data: ${errorMessage}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// No valid authentication data found
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Handle Discord OAuth callback
|
|
277
|
+
*/
|
|
278
|
+
async handleDiscordCallback() {
|
|
279
|
+
await this.turnkeyService.init();
|
|
280
|
+
const params = new URLSearchParams(window.location.search);
|
|
281
|
+
if (params.get('error')) {
|
|
282
|
+
throw new AuthenticationError(params.get('error') || 'Discord authentication failed');
|
|
283
|
+
}
|
|
284
|
+
const code = params.get('code');
|
|
285
|
+
const state = params.get('state');
|
|
286
|
+
const accessToken = params.get('accessToken');
|
|
287
|
+
const refreshToken = params.get('refreshToken');
|
|
288
|
+
const userJson = params.get('user');
|
|
289
|
+
if (code && state) {
|
|
290
|
+
try {
|
|
291
|
+
const result = await this.apiService.handleDiscordCallback(code, state);
|
|
292
|
+
this.currentUser = result.user;
|
|
293
|
+
await this.storage.setItem(STORAGE_KEYS.USER, result.user);
|
|
294
|
+
const isPolicy = Boolean(this.currentUser.isPolicy);
|
|
295
|
+
if (!isPolicy) {
|
|
296
|
+
try {
|
|
297
|
+
await this.callProxyAfterVerification('discord');
|
|
298
|
+
}
|
|
299
|
+
catch (proxyError) {
|
|
300
|
+
console.warn('PROXY call after Discord login failed:', proxyError);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
try {
|
|
305
|
+
await this.callWhoami();
|
|
306
|
+
}
|
|
307
|
+
catch (whoamiError) {
|
|
308
|
+
console.warn('Whoami call after Discord login failed:', whoamiError);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const whoamiResponse = await this.getWhoami();
|
|
312
|
+
if (!whoamiResponse) {
|
|
313
|
+
throw new AuthenticationError('Whoami response not available after successful call');
|
|
314
|
+
}
|
|
315
|
+
return result.user;
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
throw new AuthenticationError(error instanceof Error ? error.message : 'Failed to handle Discord callback');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (accessToken && refreshToken && userJson) {
|
|
322
|
+
await this.tokenService.setTokens({ accessToken, refreshToken });
|
|
323
|
+
try {
|
|
324
|
+
const user = JSON.parse(userJson);
|
|
325
|
+
this.currentUser = user;
|
|
326
|
+
await this.storage.setItem(STORAGE_KEYS.USER, user);
|
|
327
|
+
await this.callWhoami();
|
|
328
|
+
const whoamiResponse = await this.getWhoami();
|
|
329
|
+
if (!whoamiResponse) {
|
|
330
|
+
throw new AuthenticationError('Whoami response not available after successful call');
|
|
331
|
+
}
|
|
332
|
+
return user;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
336
|
+
throw new AuthenticationError(`Failed to parse user data: ${errorMessage}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Handle X (Twitter) OAuth callback
|
|
343
|
+
*/
|
|
344
|
+
async handleTwitterCallback() {
|
|
345
|
+
await this.turnkeyService.init();
|
|
346
|
+
const params = new URLSearchParams(window.location.search);
|
|
347
|
+
if (params.get('error')) {
|
|
348
|
+
throw new AuthenticationError(params.get('error') || 'X (Twitter) authentication failed');
|
|
349
|
+
}
|
|
350
|
+
const code = params.get('code');
|
|
351
|
+
const state = params.get('state');
|
|
352
|
+
const accessToken = params.get('accessToken');
|
|
353
|
+
const refreshToken = params.get('refreshToken');
|
|
354
|
+
const userJson = params.get('user');
|
|
355
|
+
if (code && state) {
|
|
356
|
+
try {
|
|
357
|
+
const result = await this.apiService.handleTwitterCallback(code, state);
|
|
358
|
+
this.currentUser = result.user;
|
|
359
|
+
await this.storage.setItem(STORAGE_KEYS.USER, result.user);
|
|
360
|
+
const isPolicy = Boolean(this.currentUser.isPolicy);
|
|
361
|
+
if (!isPolicy) {
|
|
362
|
+
try {
|
|
363
|
+
await this.callProxyAfterVerification('twitter');
|
|
364
|
+
}
|
|
365
|
+
catch (proxyError) {
|
|
366
|
+
console.warn('PROXY call after Twitter login failed:', proxyError);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
try {
|
|
371
|
+
await this.callWhoami();
|
|
372
|
+
}
|
|
373
|
+
catch (whoamiError) {
|
|
374
|
+
console.warn('Whoami call after Twitter login failed:', whoamiError);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const whoamiResponse = await this.getWhoami();
|
|
378
|
+
if (!whoamiResponse) {
|
|
379
|
+
throw new AuthenticationError('Whoami response not available after successful call');
|
|
380
|
+
}
|
|
381
|
+
return result.user;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
throw new AuthenticationError(error instanceof Error ? error.message : 'Failed to handle X (Twitter) callback');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (accessToken && refreshToken && userJson) {
|
|
388
|
+
await this.tokenService.setTokens({ accessToken, refreshToken });
|
|
389
|
+
try {
|
|
390
|
+
const user = JSON.parse(userJson);
|
|
391
|
+
this.currentUser = user;
|
|
392
|
+
await this.storage.setItem(STORAGE_KEYS.USER, user);
|
|
393
|
+
await this.callWhoami();
|
|
394
|
+
const whoamiResponse = await this.getWhoami();
|
|
395
|
+
if (!whoamiResponse) {
|
|
396
|
+
throw new AuthenticationError('Whoami response not available after successful call');
|
|
397
|
+
}
|
|
398
|
+
return user;
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
402
|
+
throw new AuthenticationError(`Failed to parse user data: ${errorMessage}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Logout
|
|
409
|
+
*/
|
|
410
|
+
async logout() {
|
|
411
|
+
this.currentUser = null;
|
|
412
|
+
await this.tokenService.clearTokens();
|
|
413
|
+
// Clear all storage items
|
|
414
|
+
await this.storage.removeItem(STORAGE_KEYS.USER);
|
|
415
|
+
await this.storage.removeItem(STORAGE_KEYS.WHOAMI);
|
|
416
|
+
await this.storage.removeItem(STORAGE_KEYS.VERIFY_RESPONSE);
|
|
417
|
+
await this.storage.removeItem(STORAGE_KEYS.WALLET_CONFIG);
|
|
418
|
+
// Clear all localStorage
|
|
419
|
+
await this.storage.clear();
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Refresh access token
|
|
423
|
+
*/
|
|
424
|
+
async refreshToken() {
|
|
425
|
+
return await this.apiService.refreshToken();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get current user
|
|
429
|
+
*/
|
|
430
|
+
getCurrentUser() {
|
|
431
|
+
return this.currentUser;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Check if authenticated
|
|
435
|
+
*/
|
|
436
|
+
async isAuthenticated() {
|
|
437
|
+
if (this.currentUser) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
// Try to load user from storage
|
|
441
|
+
const user = await this.storage.getItem(STORAGE_KEYS.USER);
|
|
442
|
+
if (user) {
|
|
443
|
+
this.currentUser = user;
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Load user from storage
|
|
450
|
+
*/
|
|
451
|
+
async loadUserFromStorage() {
|
|
452
|
+
const user = await this.storage.getItem(STORAGE_KEYS.USER);
|
|
453
|
+
if (user) {
|
|
454
|
+
this.currentUser = user;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Get stored verification response
|
|
459
|
+
* Returns the full OTP verification response that was saved
|
|
460
|
+
*/
|
|
461
|
+
async getVerifyResponse() {
|
|
462
|
+
return await this.storage.getItem(STORAGE_KEYS.VERIFY_RESPONSE);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Call whoami API and store the response
|
|
466
|
+
* This is called after successful OTP verification
|
|
467
|
+
* Always calls backend endpoint with access token (backend handles Turnkey call)
|
|
468
|
+
*/
|
|
469
|
+
async callWhoami() {
|
|
470
|
+
if (!this.currentUser?.subOrganizationId) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
// If whoami is already being called, wait for the existing call to complete
|
|
474
|
+
// This prevents duplicate API calls
|
|
475
|
+
if (this.whoamiPromise) {
|
|
476
|
+
try {
|
|
477
|
+
await this.whoamiPromise;
|
|
478
|
+
// If the existing call succeeded, we're done
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
// If the existing call failed, clear the promise and continue to retry
|
|
483
|
+
this.whoamiPromise = null;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Check if we already have a cached response (avoid unnecessary API call)
|
|
487
|
+
const cachedWhoami = await this.getWhoami();
|
|
488
|
+
if (cachedWhoami) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
// Create and store the promise for this call
|
|
492
|
+
this.whoamiPromise = (async () => {
|
|
493
|
+
try {
|
|
494
|
+
// Ensure storage is initialized before calling whoami
|
|
495
|
+
// This is critical for createStamp to work in getWhoami
|
|
496
|
+
await this.turnkeyService.init();
|
|
497
|
+
// Call whoami via backend endpoint with access token
|
|
498
|
+
// Backend will handle the Turnkey call internally
|
|
499
|
+
const whoamiResponse = await this.apiService.getWhoami(this.currentUser.subOrganizationId);
|
|
500
|
+
// Store whoami response in localStorage
|
|
501
|
+
await this.storage.setItem(STORAGE_KEYS.WHOAMI, whoamiResponse);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
console.error('Failed to get whoami information:', error);
|
|
505
|
+
// Clear promise on error so it can be retried
|
|
506
|
+
this.whoamiPromise = null;
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
// Only clear if it's still the same promise (in case of concurrent calls)
|
|
511
|
+
if (this.whoamiPromise) {
|
|
512
|
+
this.whoamiPromise = null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
})();
|
|
516
|
+
return await this.whoamiPromise;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Get stored whoami response
|
|
520
|
+
*/
|
|
521
|
+
async getWhoami() {
|
|
522
|
+
return await this.storage.getItem(STORAGE_KEYS.WHOAMI);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Refresh whoami by making a fresh API call
|
|
526
|
+
* This bypasses the cache and always calls the API
|
|
527
|
+
*/
|
|
528
|
+
async refreshWhoami() {
|
|
529
|
+
if (!this.currentUser?.subOrganizationId) {
|
|
530
|
+
throw new AuthenticationError('User not authenticated');
|
|
531
|
+
}
|
|
532
|
+
// Clear any existing promise to force a fresh call
|
|
533
|
+
this.whoamiPromise = null;
|
|
534
|
+
try {
|
|
535
|
+
// Ensure storage is initialized before calling whoami
|
|
536
|
+
await this.turnkeyService.init();
|
|
537
|
+
// Call whoami via backend endpoint with access token
|
|
538
|
+
// Backend will handle the Turnkey call internally
|
|
539
|
+
const whoamiResponse = await this.apiService.getWhoami(this.currentUser.subOrganizationId);
|
|
540
|
+
// Store whoami response in localStorage
|
|
541
|
+
await this.storage.setItem(STORAGE_KEYS.WHOAMI, whoamiResponse);
|
|
542
|
+
return whoamiResponse;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
console.error('Failed to refresh whoami information:', error);
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Signup with passkey
|
|
551
|
+
* Creates a WebAuthn credential and registers the user
|
|
552
|
+
*/
|
|
553
|
+
async signupWithPasskey() {
|
|
554
|
+
try {
|
|
555
|
+
// Clear previous IndexedDB data and reset TurnkeyService before starting new signup
|
|
556
|
+
await this.turnkeyService.reset();
|
|
557
|
+
// Initialize storage if not already initialized
|
|
558
|
+
await this.turnkeyService.init();
|
|
559
|
+
// Get public key from storage (internal - users don't see this)
|
|
560
|
+
const targetPublicKey = await this.turnkeyService.getPublicKey();
|
|
561
|
+
if (!targetPublicKey) {
|
|
562
|
+
throw new AuthenticationError('Failed to get public key from storage');
|
|
563
|
+
}
|
|
564
|
+
// Get rpId from current hostname
|
|
565
|
+
const rpId = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
|
|
566
|
+
// Generate challenge
|
|
567
|
+
const challengeBase64 = generateChallenge();
|
|
568
|
+
// Create passkey credential
|
|
569
|
+
const passkeyData = await createPasskeyCredential(rpId, challengeBase64);
|
|
570
|
+
// Generate authenticator name
|
|
571
|
+
const authenticatorName = generateAuthenticatorName(rpId);
|
|
572
|
+
// Generate user and organization names
|
|
573
|
+
const userName = generateUserName();
|
|
574
|
+
const organizationName = generateOrganizationName();
|
|
575
|
+
// Format the attestation data
|
|
576
|
+
const authenticator = {
|
|
577
|
+
authenticatorName,
|
|
578
|
+
challenge: passkeyData.challenge,
|
|
579
|
+
attestation: {
|
|
580
|
+
credentialId: passkeyData.credentialId,
|
|
581
|
+
attestationObject: passkeyData.attestationObject,
|
|
582
|
+
clientDataJson: passkeyData.clientDataJson,
|
|
583
|
+
transports: passkeyData.transports,
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
// Call passkey signup API
|
|
587
|
+
const response = await this.apiService.passkeySignup({
|
|
588
|
+
userName,
|
|
589
|
+
authenticators: [authenticator],
|
|
590
|
+
organizationName,
|
|
591
|
+
targetPublicKey,
|
|
592
|
+
});
|
|
593
|
+
if (!response.result?.user) {
|
|
594
|
+
throw new AuthenticationError('Failed to get user information from passkey signup');
|
|
595
|
+
}
|
|
596
|
+
// Set user in memory
|
|
597
|
+
this.currentUser = response.result.user;
|
|
598
|
+
// Store user in storage
|
|
599
|
+
await this.storage.setItem(STORAGE_KEYS.USER, this.currentUser);
|
|
600
|
+
// For passkey signup, skip proxy and call whoami directly
|
|
601
|
+
try {
|
|
602
|
+
await this.callWhoami();
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
console.warn('Whoami call after passkey signup failed:', error);
|
|
606
|
+
}
|
|
607
|
+
return this.currentUser;
|
|
608
|
+
}
|
|
609
|
+
catch (error) {
|
|
610
|
+
// If passkey signup API fails, reset IndexedDB
|
|
611
|
+
// This allows the user to retry with a fresh key
|
|
612
|
+
try {
|
|
613
|
+
await this.turnkeyService.reset();
|
|
614
|
+
}
|
|
615
|
+
catch (resetError) {
|
|
616
|
+
// Log reset error but don't fail - we still want to throw the original error
|
|
617
|
+
console.warn('Failed to reset IndexedDB after passkey signup error:', resetError);
|
|
618
|
+
}
|
|
619
|
+
// Re-throw the original error
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Login with passkey
|
|
625
|
+
* Uses WebauthnStamper to authenticate with existing passkey
|
|
626
|
+
*/
|
|
627
|
+
async loginWithPasskey() {
|
|
628
|
+
try {
|
|
629
|
+
const rpId = typeof window !== 'undefined' ? window.location.hostname : 'localhost';
|
|
630
|
+
// Create WebauthnStamper
|
|
631
|
+
const stamper = new WebauthnStamper({
|
|
632
|
+
rpId: rpId,
|
|
633
|
+
userVerification: 'required',
|
|
634
|
+
});
|
|
635
|
+
// Create Turnkey client with WebauthnStamper
|
|
636
|
+
const httpClient = new TurnkeyClient({ baseUrl: TURNKEY_CONFIG.BASE_URL }, stamper);
|
|
637
|
+
// Root organization ID for stamping (provided)
|
|
638
|
+
const rootOrganizationId = '0ba77025-ceba-4093-b5b3-a22c838d2e28';
|
|
639
|
+
// Initialize storage and try to get public key from Turnkey IndexedDB stamper
|
|
640
|
+
await this.turnkeyService.init();
|
|
641
|
+
const storagePublicKey = await this.turnkeyService.getPublicKey();
|
|
642
|
+
const storedUser = await this.storage.getItem(STORAGE_KEYS.USER);
|
|
643
|
+
const publicKey = storagePublicKey || storedUser?.publicKey || this.currentUser?.publicKey;
|
|
644
|
+
if (!publicKey) {
|
|
645
|
+
throw new AuthenticationError('Failed to get public key for passkey login. Please complete passkey signup first.');
|
|
646
|
+
}
|
|
647
|
+
// Create stamp + body using Turnkey stampStampLogin
|
|
648
|
+
const { body, stamp } = await httpClient.stampStampLogin({
|
|
649
|
+
type: 'ACTIVITY_TYPE_STAMP_LOGIN',
|
|
650
|
+
timestampMs: new Date().getTime().toString(),
|
|
651
|
+
organizationId: rootOrganizationId,
|
|
652
|
+
parameters: {
|
|
653
|
+
publicKey,
|
|
654
|
+
expirationSeconds: '86400',
|
|
655
|
+
invalidateExisting: true,
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
// Call backend PASSKEY_LOGIN endpoint with stamp + body
|
|
659
|
+
const parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
|
|
660
|
+
const stampValue = stamp?.stampHeaderValue ?? stamp;
|
|
661
|
+
const response = await this.apiService.passkeyLogin({ body: parsedBody, stamp: stampValue });
|
|
662
|
+
if (!response.result?.user) {
|
|
663
|
+
throw new AuthenticationError('Failed to get user information from passkey login');
|
|
664
|
+
}
|
|
665
|
+
// Set user and store
|
|
666
|
+
this.currentUser = response.result.user;
|
|
667
|
+
await this.storage.setItem(STORAGE_KEYS.USER, this.currentUser);
|
|
668
|
+
// Fetch whoami directly (no proxy for passkey)
|
|
669
|
+
try {
|
|
670
|
+
await this.callWhoami();
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
console.warn('Whoami call after passkey login failed:', error);
|
|
674
|
+
}
|
|
675
|
+
return this.currentUser;
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (error instanceof AuthenticationError) {
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
throw new AuthenticationError(error instanceof Error ? error.message : 'Failed to login with passkey');
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
//# sourceMappingURL=AuthManager.js.map
|