@turnkey/core 1.14.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/__clients__/core.d.ts +86 -33
  2. package/dist/__clients__/core.d.ts.map +1 -1
  3. package/dist/__clients__/core.js +364 -201
  4. package/dist/__clients__/core.js.map +1 -1
  5. package/dist/__clients__/core.mjs +365 -202
  6. package/dist/__clients__/core.mjs.map +1 -1
  7. package/dist/__generated__/sdk-client-base.d.ts +40 -3
  8. package/dist/__generated__/sdk-client-base.d.ts.map +1 -1
  9. package/dist/__generated__/sdk-client-base.js +961 -101
  10. package/dist/__generated__/sdk-client-base.js.map +1 -1
  11. package/dist/__generated__/sdk-client-base.mjs +956 -96
  12. package/dist/__generated__/sdk-client-base.mjs.map +1 -1
  13. package/dist/__generated__/version.d.ts +1 -1
  14. package/dist/__generated__/version.d.ts.map +1 -1
  15. package/dist/__generated__/version.js +1 -1
  16. package/dist/__generated__/version.mjs +1 -1
  17. package/dist/__stampers__/api/base.d.ts +2 -1
  18. package/dist/__stampers__/api/base.d.ts.map +1 -1
  19. package/dist/__stampers__/api/mobile/stamper.d.ts +2 -1
  20. package/dist/__stampers__/api/mobile/stamper.d.ts.map +1 -1
  21. package/dist/__stampers__/api/mobile/stamper.js.map +1 -1
  22. package/dist/__stampers__/api/mobile/stamper.mjs.map +1 -1
  23. package/dist/__stampers__/api/web/stamper.d.ts +2 -1
  24. package/dist/__stampers__/api/web/stamper.d.ts.map +1 -1
  25. package/dist/__stampers__/api/web/stamper.js.map +1 -1
  26. package/dist/__stampers__/api/web/stamper.mjs.map +1 -1
  27. package/dist/__stampers__/passkey/base.d.ts +3 -1
  28. package/dist/__stampers__/passkey/base.d.ts.map +1 -1
  29. package/dist/__stampers__/passkey/base.js +19 -0
  30. package/dist/__stampers__/passkey/base.js.map +1 -1
  31. package/dist/__stampers__/passkey/base.mjs +20 -1
  32. package/dist/__stampers__/passkey/base.mjs.map +1 -1
  33. package/dist/__tests__/poll-transaction-status-test.d.ts +2 -0
  34. package/dist/__tests__/poll-transaction-status-test.d.ts.map +1 -0
  35. package/dist/__tests__/utils-test.d.ts +2 -0
  36. package/dist/__tests__/utils-test.d.ts.map +1 -0
  37. package/dist/__types__/auth.d.ts +2 -41
  38. package/dist/__types__/auth.d.ts.map +1 -1
  39. package/dist/__types__/auth.js.map +1 -1
  40. package/dist/__types__/auth.mjs.map +1 -1
  41. package/dist/__types__/config.d.ts +3 -2
  42. package/dist/__types__/config.d.ts.map +1 -1
  43. package/dist/__types__/enums.d.ts +1 -0
  44. package/dist/__types__/enums.d.ts.map +1 -1
  45. package/dist/__types__/enums.js +1 -0
  46. package/dist/__types__/enums.js.map +1 -1
  47. package/dist/__types__/enums.mjs +1 -0
  48. package/dist/__types__/enums.mjs.map +1 -1
  49. package/dist/__types__/http.d.ts +1 -14
  50. package/dist/__types__/http.d.ts.map +1 -1
  51. package/dist/__types__/index.d.ts +0 -1
  52. package/dist/__types__/index.d.ts.map +1 -1
  53. package/dist/__types__/method-types/shared.d.ts +28 -13
  54. package/dist/__types__/method-types/shared.d.ts.map +1 -1
  55. package/dist/__wallet__/stamper.d.ts +2 -1
  56. package/dist/__wallet__/stamper.d.ts.map +1 -1
  57. package/dist/__wallet__/stamper.js.map +1 -1
  58. package/dist/__wallet__/stamper.mjs.map +1 -1
  59. package/dist/index.d.ts +1 -2
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +8 -6
  62. package/dist/index.js.map +1 -1
  63. package/dist/index.mjs +2 -4
  64. package/dist/index.mjs.map +1 -1
  65. package/dist/turnkey-helpers.d.ts +4 -0
  66. package/dist/turnkey-helpers.d.ts.map +1 -1
  67. package/dist/turnkey-helpers.js +28 -0
  68. package/dist/turnkey-helpers.js.map +1 -1
  69. package/dist/turnkey-helpers.mjs +25 -1
  70. package/dist/turnkey-helpers.mjs.map +1 -1
  71. package/dist/utils.d.ts +41 -18
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +126 -57
  74. package/dist/utils.js.map +1 -1
  75. package/dist/utils.mjs +100 -38
  76. package/dist/utils.mjs.map +1 -1
  77. package/package.json +8 -8
  78. package/dist/__types__/error.d.ts +0 -13
  79. package/dist/__types__/error.d.ts.map +0 -1
  80. package/dist/__types__/error.js +0 -18
  81. package/dist/__types__/error.js.map +0 -1
  82. package/dist/__types__/error.mjs +0 -16
  83. package/dist/__types__/error.mjs.map +0 -1
  84. package/dist/__types__/http.js +0 -11
  85. package/dist/__types__/http.js.map +0 -1
  86. package/dist/__types__/http.mjs +0 -9
  87. package/dist/__types__/http.mjs.map +0 -1
@@ -5,6 +5,7 @@ var sdkTypes = require('@turnkey/sdk-types');
5
5
  var auth = require('../__types__/auth.js');
6
6
  var enums = require('../__types__/enums.js');
7
7
  var utils = require('../utils.js');
8
+ var crypto = require('@turnkey/crypto');
8
9
  var base = require('../__storage__/base.js');
9
10
  var base$1 = require('../__stampers__/api/base.js');
10
11
  var base$2 = require('../__stampers__/passkey/base.js');
@@ -12,26 +13,68 @@ var turnkeyHelpers = require('../turnkey-helpers.js');
12
13
  var jwtDecode = require('jwt-decode');
13
14
  var base$3 = require('../__wallet__/base.js');
14
15
  var ethers = require('ethers');
15
- var crypto = require('@turnkey/crypto');
16
16
  var apiKeyStamper = require('@turnkey/api-key-stamper');
17
17
  var viem = require('viem');
18
18
 
19
- const ERC20_TRANSFER_ABI = [
20
- {
21
- type: "function",
22
- name: "transfer",
23
- stateMutability: "nonpayable",
24
- inputs: [
25
- { name: "to", type: "address" },
26
- { name: "amount", type: "uint256" },
27
- ],
28
- outputs: [{ name: "success", type: "bool" }],
29
- },
30
- ];
31
19
  class TurnkeyClient {
32
20
  constructor(config,
33
21
  // Users can pass in their own stampers, or we will create them. Should we remove this?
34
22
  apiKeyStamper$1, passkeyStamper, walletManager) {
23
+ /**
24
+ * Overrides the API key stamper configuration.
25
+ *
26
+ * - Set `temporaryPublicKey` to a non-empty string to override the default API key.
27
+ * - Set `temporaryPublicKey` to an empty string ("") to clear the temporary key and restore default behavior.
28
+ * - Omit `temporaryPublicKey` to leave it unchanged.
29
+ * - Useful for dynamically changing the API key used for signing requests.
30
+ *
31
+ * @param params.temporaryPublicKey - temporary public key to set (non-empty string), clear (empty string), or leave unchanged (omit).
32
+ * @returns A promise that resolves when the stamper has been updated.
33
+ * @throws {TurnkeyError} If the API key stamper is not initialized or if there is an error updating it.
34
+ */
35
+ this.overrideApiKeyStamper = async (params) => {
36
+ return utils.withTurnkeyErrorHandling(async () => {
37
+ if (!this.apiKeyStamper) {
38
+ throw new sdkTypes.TurnkeyError("API key stamper is not initialized", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
39
+ }
40
+ const { temporaryPublicKey } = params;
41
+ // Only process if temporaryPublicKey is explicitly passed
42
+ if (temporaryPublicKey !== undefined) {
43
+ if (temporaryPublicKey === "") {
44
+ this.apiKeyStamper.clearTemporaryPublicKey();
45
+ }
46
+ else {
47
+ this.apiKeyStamper.setTemporaryPublicKey(temporaryPublicKey);
48
+ }
49
+ }
50
+ }, {
51
+ errorMessage: "Failed to override API key stamper",
52
+ errorCode: sdkTypes.TurnkeyErrorCodes.INITIALIZE_API_KEY_STAMPER_ERROR,
53
+ });
54
+ };
55
+ /**
56
+ * Overrides the passkey stamper with a new configuration.
57
+ *
58
+ * - This function updates the passkey stamper configuration without reinitializing the entire stamper.
59
+ * - Updates the stamper's configuration properties (e.g., allowCredentials, rpId, timeout, userVerification).
60
+ * - Useful for dynamically changing passkey configuration during runtime.
61
+ *
62
+ * @param params.config - new passkey stamper configuration to use.
63
+ * @returns A promise that resolves when the stamper configuration has been updated.
64
+ * @throws {TurnkeyError} If there is an error updating the stamper configuration or if the passkey stamper is not initialized.
65
+ */
66
+ this.overridePasskeyStamper = async (params) => {
67
+ const { config } = params;
68
+ return utils.withTurnkeyErrorHandling(async () => {
69
+ if (!this.passkeyStamper) {
70
+ throw new sdkTypes.TurnkeyError("Passkey stamper is not initialized", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
71
+ }
72
+ this.passkeyStamper.updateConfig(config);
73
+ }, {
74
+ errorMessage: "Failed to override passkey stamper",
75
+ errorCode: sdkTypes.TurnkeyErrorCodes.INITIALIZE_PASSKEY_STAMPER_ERROR,
76
+ });
77
+ };
35
78
  /**
36
79
  * Creates a new TurnkeySDKClientBase instance with the provided configuration.
37
80
  * This method is used internally to create the HTTP client for making API requests,
@@ -149,14 +192,14 @@ class TurnkeyClient {
149
192
  if (params?.sessionKey) {
150
193
  const session = await this.storageManager.getSession(params.sessionKey);
151
194
  this.storageManager.clearSession(params.sessionKey);
152
- this.apiKeyStamper?.deleteKeyPair(session?.publicKey);
195
+ await this.deleteApiKeyPair({ publicKey: session?.publicKey });
153
196
  }
154
197
  else {
155
198
  const sessionKey = await this.storageManager.getActiveSessionKey();
156
199
  const session = await this.storageManager.getActiveSession();
157
200
  if (sessionKey) {
158
201
  this.storageManager.clearSession(sessionKey);
159
- this.apiKeyStamper?.deleteKeyPair(session?.publicKey);
202
+ await this.deleteApiKeyPair({ publicKey: session?.publicKey });
160
203
  }
161
204
  else {
162
205
  throw new sdkTypes.TurnkeyError("No active session found to log out from.", sdkTypes.TurnkeyErrorCodes.NO_SESSION_FOUND);
@@ -180,37 +223,52 @@ class TurnkeyClient {
180
223
  * @param params.sessionKey - session key to use for session creation (defaults to the default session key).
181
224
  * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
182
225
  * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
226
+ * @param params.allowCredentials - optional list of allowed credentials for passkey authentication. This allows you to restrict which passkeys can be used for login.
183
227
  * @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes:
184
228
  * - `sessionToken`: the signed JWT session token.
185
229
  * - `credentialId`: an empty string.
186
230
  * @throws {TurnkeyError} If there is an error during the passkey login process or if the user cancels the passkey prompt.
187
231
  */
188
232
  this.loginWithPasskey = async (params) => {
189
- let generatedPublicKey = undefined;
233
+ const shouldOverrideConfig = params?.allowCredentials && this.passkeyStamper;
234
+ const currentConfig = this.config.passkeyConfig;
235
+ const generatedPublicKey = params?.publicKey || (await this.createApiKeyPair());
190
236
  return await utils.withTurnkeyErrorHandling(async () => {
191
- generatedPublicKey =
192
- params?.publicKey || (await this.apiKeyStamper?.createKeyPair());
237
+ if (!this.passkeyStamper) {
238
+ throw new sdkTypes.TurnkeyError("Passkey stamper is not initialized", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
239
+ }
193
240
  const sessionKey = params?.sessionKey || enums.SessionKey.DefaultSessionkey;
194
241
  const expirationSeconds = params?.expirationSeconds || auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
242
+ if (shouldOverrideConfig) {
243
+ // Override passkey stamper config to include allowCredentials
244
+ const mergedConfig = {
245
+ ...currentConfig,
246
+ allowCredentials: params?.allowCredentials, // Can safely assert non-null due to check above
247
+ };
248
+ await this.overridePasskeyStamper({ config: mergedConfig });
249
+ }
195
250
  if (!generatedPublicKey) {
196
251
  throw new sdkTypes.TurnkeyError("A publickey could not be found or generated.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
197
252
  }
198
- const sessionResponse = await this.httpClient.stampLogin({
253
+ const loginPayload = {
199
254
  publicKey: generatedPublicKey,
200
255
  organizationId: params?.organizationId ?? this.config.organizationId,
201
256
  expirationSeconds,
202
- }, enums.StamperType.Passkey);
257
+ };
258
+ const passkeyLoginRequest = await this.httpClient.stampStampLogin(loginPayload, enums.StamperType.Passkey);
259
+ if (!passkeyLoginRequest) {
260
+ throw new sdkTypes.TurnkeyError("Failed to generate login payload for passkey login.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
261
+ }
262
+ // For passkey stamps, the stamp header value is JSON that includes the credentialId used to sign. Extract it here so we can return it to the caller.
263
+ const credentialId = JSON.parse(passkeyLoginRequest.stamp.stampHeaderValue).credentialId;
264
+ const sessionResponse = await this.httpClient.sendSignedRequest(passkeyLoginRequest);
203
265
  await this.storeSession({
204
266
  sessionToken: sessionResponse.session,
205
267
  sessionKey,
206
268
  });
207
- generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
208
269
  return {
209
270
  sessionToken: sessionResponse.session,
210
- // TODO: can we return the credentialId here?
211
- // from a quick glance this is going to be difficult
212
- // for now we return an empty string
213
- credentialId: "",
271
+ credentialId,
214
272
  };
215
273
  }, {
216
274
  errorMessage: "Unable to log in with the provided passkey",
@@ -221,15 +279,23 @@ class TurnkeyClient {
221
279
  code: sdkTypes.TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED,
222
280
  },
223
281
  },
282
+ catchFn: async () => {
283
+ try {
284
+ await this.deleteApiKeyPair({ publicKey: generatedPublicKey });
285
+ }
286
+ catch (cleanupError) {
287
+ throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
288
+ }
289
+ },
224
290
  }, {
225
291
  finallyFn: async () => {
226
- if (generatedPublicKey) {
227
- try {
228
- await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
229
- }
230
- catch (cleanupError) {
231
- throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
232
- }
292
+ if (shouldOverrideConfig) {
293
+ // we restore previous stamper after login attempt
294
+ await this.overridePasskeyStamper({
295
+ // we can assert this safely, since if this statement runs we are certian the passkey stamper
296
+ // is initialized
297
+ config: currentConfig,
298
+ });
233
299
  }
234
300
  },
235
301
  });
@@ -248,17 +314,18 @@ class TurnkeyClient {
248
314
  * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
249
315
  * @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata).
250
316
  * @param params.sessionKey - session key to use for storing the session (defaults to the default session key).
251
- * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
252
317
  * @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes:
253
318
  * - `sessionToken`: the signed JWT session token.
254
319
  * - `credentialId`: the credential ID associated with the passkey created.
255
320
  * @throws {TurnkeyError} If there is an error during passkey creation, sub-organization creation, or session storage.
256
321
  */
257
322
  this.signUpWithPasskey = async (params) => {
258
- const { passkeyDisplayName, challenge, expirationSeconds = auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = enums.SessionKey.DefaultSessionkey, organizationId, } = params || {};
259
- let generatedPublicKey = undefined;
323
+ const { passkeyDisplayName, challenge, expirationSeconds = auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = enums.SessionKey.DefaultSessionkey, } = params || {};
324
+ const generatedPublicKey = await this.createApiKeyPair();
260
325
  return utils.withTurnkeyErrorHandling(async () => {
261
- generatedPublicKey = await this.apiKeyStamper?.createKeyPair();
326
+ if (!this.passkeyStamper) {
327
+ throw new sdkTypes.TurnkeyError("Passkey stamper is not initialized", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
328
+ }
262
329
  const passkeyName = passkeyDisplayName || `passkey-${Date.now()}`;
263
330
  // A passkey will be created automatically when you call this function. The name is passed in
264
331
  const passkey = await this.createPasskey({
@@ -290,25 +357,23 @@ class TurnkeyClient {
290
357
  ],
291
358
  },
292
359
  });
293
- const res = await this.httpClient.proxySignup(signUpBody);
360
+ const res = await this.httpClient.proxySignupV2(signUpBody);
294
361
  if (!res) {
295
362
  throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.TurnkeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR);
296
363
  }
297
- const newGeneratedKeyPair = await this.apiKeyStamper?.createKeyPair();
298
- this.apiKeyStamper?.setTemporaryPublicKey(generatedPublicKey);
364
+ const newGeneratedKeyPair = await this.createApiKeyPair();
365
+ await this.overrideApiKeyStamper({
366
+ temporaryPublicKey: generatedPublicKey,
367
+ });
299
368
  const sessionResponse = await this.httpClient.stampLogin({
300
369
  publicKey: newGeneratedKeyPair,
301
- organizationId: organizationId ?? this.config.organizationId,
370
+ organizationId: res.organizationId,
302
371
  expirationSeconds,
303
372
  });
304
- await Promise.all([
305
- this.apiKeyStamper?.deleteKeyPair(generatedPublicKey),
306
- this.storeSession({
307
- sessionToken: sessionResponse.session,
308
- sessionKey,
309
- }),
310
- ]);
311
- generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
373
+ await this.storeSession({
374
+ sessionToken: sessionResponse.session,
375
+ sessionKey,
376
+ });
312
377
  return {
313
378
  sessionToken: sessionResponse.session,
314
379
  appProofs: res.appProofs,
@@ -319,14 +384,12 @@ class TurnkeyClient {
319
384
  errorMessage: "Failed to sign up with passkey",
320
385
  }, {
321
386
  finallyFn: async () => {
322
- this.apiKeyStamper?.clearTemporaryPublicKey();
323
- if (generatedPublicKey) {
324
- try {
325
- await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
326
- }
327
- catch (cleanupError) {
328
- throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
329
- }
387
+ try {
388
+ await this.overrideApiKeyStamper({ temporaryPublicKey: "" });
389
+ await this.deleteApiKeyPair({ publicKey: generatedPublicKey });
390
+ }
391
+ catch (cleanupError) {
392
+ throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
330
393
  }
331
394
  },
332
395
  });
@@ -466,13 +529,11 @@ class TurnkeyClient {
466
529
  this.buildWalletLoginRequest = async (params) => {
467
530
  const { walletProvider, publicKey: providedPublicKey } = params;
468
531
  const expirationSeconds = params.expirationSeconds || auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
469
- let generatedPublicKey = undefined;
532
+ const futureSessionPublicKey = providedPublicKey ?? (await this.createApiKeyPair());
470
533
  return utils.withTurnkeyErrorHandling(async () => {
471
534
  if (!this.walletManager?.stamper) {
472
535
  throw new sdkTypes.TurnkeyError("Wallet stamper is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
473
536
  }
474
- const futureSessionPublicKey = providedPublicKey ??
475
- (generatedPublicKey = await this.apiKeyStamper?.createKeyPair());
476
537
  if (!futureSessionPublicKey) {
477
538
  throw new sdkTypes.TurnkeyError("Failed to find or generate a public key for building the wallet login request", sdkTypes.TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR);
478
539
  }
@@ -514,9 +575,12 @@ class TurnkeyClient {
514
575
  errorCode: sdkTypes.TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR,
515
576
  errorMessage: "Failed to build wallet login request",
516
577
  catchFn: async () => {
517
- if (generatedPublicKey) {
578
+ // we only clean up the generated key pair if we created it here
579
+ if (!providedPublicKey) {
518
580
  try {
519
- await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
581
+ await this.deleteApiKeyPair({
582
+ publicKey: futureSessionPublicKey,
583
+ });
520
584
  }
521
585
  catch (cleanupError) {
522
586
  throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
@@ -601,7 +665,7 @@ class TurnkeyClient {
601
665
  ],
602
666
  },
603
667
  });
604
- const res = await this.httpClient.proxySignup(signUpBody);
668
+ const res = await this.httpClient.proxySignupV2(signUpBody);
605
669
  if (!res) {
606
670
  throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
607
671
  }
@@ -676,7 +740,7 @@ class TurnkeyClient {
676
740
  ],
677
741
  },
678
742
  });
679
- signupRes = await this.httpClient.proxySignup(signUpBody);
743
+ signupRes = await this.httpClient.proxySignupV2(signUpBody);
680
744
  if (!signupRes) {
681
745
  throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
682
746
  }
@@ -708,21 +772,29 @@ class TurnkeyClient {
708
772
  *
709
773
  * - This function initiates the OTP flow by sending a one-time password (OTP) code to the user's contact information (email address or phone number) via the auth proxy.
710
774
  * - Supports both email and SMS OTP types.
711
- * - Returns an OTP ID that is required for subsequent OTP verification.
775
+ * - Returns an OTP ID and an encryption target bundle required for subsequent OTP verification.
776
+ * - The `otpEncryptionTargetBundle` must be used to encrypt the OTP code and a client-generated public key before calling `verifyOtp`.
712
777
  *
713
778
  * @param params.otpType - type of OTP to initialize (OtpType.Email or OtpType.Sms).
714
779
  * @param params.contact - contact information for the user (e.g., email address or phone number).
715
780
  * @param params.organizationId - optional organization ID to target (defaults to the session's organization ID or the parent organization ID).
716
- * @returns A promise that resolves to the OTP ID required for verification.
781
+ * @returns A promise that resolves to an {@link InitOtpResult}, which includes:
782
+ * - `otpId`: the ID of the initiated OTP.
783
+ * - `otpEncryptionTargetBundle`: the signed encryption target bundle for encrypting the OTP code.
717
784
  * @throws {TurnkeyError} If there is an error during the OTP initialization process or if the maximum number of OTPs has been reached.
718
785
  */
719
786
  this.initOtp = async (params) => {
720
787
  return utils.withTurnkeyErrorHandling(async () => {
721
- const initOtpRes = await this.httpClient.proxyInitOtp(params);
722
- if (!initOtpRes || !initOtpRes.otpId) {
723
- throw new sdkTypes.TurnkeyError("Failed to initialize OTP: otpId is missing", sdkTypes.TurnkeyErrorCodes.INIT_OTP_ERROR);
788
+ const initOtpRes = await this.httpClient.proxyInitOtpV2(params);
789
+ if (!initOtpRes ||
790
+ !initOtpRes.otpId ||
791
+ !initOtpRes.otpEncryptionTargetBundle) {
792
+ throw new sdkTypes.TurnkeyError("Failed to initialize OTP: otpId or otpEncryptionTargetBundle is missing", sdkTypes.TurnkeyErrorCodes.INIT_OTP_ERROR);
724
793
  }
725
- return initOtpRes.otpId;
794
+ return {
795
+ otpId: initOtpRes.otpId,
796
+ otpEncryptionTargetBundle: initOtpRes.otpEncryptionTargetBundle,
797
+ };
726
798
  }, {
727
799
  errorMessage: "Failed to initialize OTP",
728
800
  errorCode: sdkTypes.TurnkeyErrorCodes.INIT_OTP_ERROR,
@@ -737,45 +809,38 @@ class TurnkeyClient {
737
809
  /**
738
810
  * Verifies the OTP code sent to the user.
739
811
  *
740
- * - This function verifies the OTP code entered by the user against the OTP sent to their contact information (email or phone) using the auth proxy.
741
- * - If verification is successful, it returns the sub-organization ID associated with the contact (if it exists) and a verification token.
812
+ * - Under the hood, the OTP code and an ephemeral client public key are encrypted to the enclave's target key (from `initOtp`) before being sent for verification.
813
+ * - If verification is successful, it returns a verification token bound to the public key.
742
814
  * - The verification token can be used for subsequent login or sign-up flows.
743
- * - Handles both email and SMS OTP types.
744
815
  *
745
816
  * @param params.otpId - ID of the OTP to verify (returned from `initOtp`).
746
- * @param params.otpCode - OTP code entered by the user.
747
- * @param params.contact - contact information for the user (e.g., email address or phone number).
748
- * @param params.otpType - type of OTP being verified (OtpType.Email or OtpType.Sms).
749
- * @param params.publicKey - public key the verification token is bound to for ownership verification (client signature verification during login/signup). This public key is optional; if not provided, a new key pair will be generated.
817
+ * @param params.otpCode - the OTP code entered by the user.
818
+ * @param params.otpEncryptionTargetBundle - the encryption target bundle returned from `initOtp`.
819
+ * @param params.publicKey - optional public key to bind to the verification token. If not provided, a new key pair will be generated via the configured `apiKeyStamper`.
750
820
  * @returns A promise that resolves to an object containing:
751
- * - subOrganizationId: sub-organization ID if the contact is already associated with a sub-organization, or an empty string if not.
752
821
  * - verificationToken: verification token to be used for login or sign-up.
822
+ * - publicKey: the public key bound to the verification token (either the one provided or the auto-generated one).
753
823
  * @throws {TurnkeyError} If there is an error during the OTP verification process, such as an invalid code or network failure.
754
824
  */
755
825
  this.verifyOtp = async (params) => {
756
- const { otpId, otpCode, contact, otpType } = params;
757
- const resolvedPublicKey = params.publicKey ?? (await this.apiKeyStamper?.createKeyPair());
826
+ const { otpId, otpCode, otpEncryptionTargetBundle } = params;
827
+ // we track auto-generated keys so we can clean them up on errors
828
+ const publicKey = params.publicKey ?? (await this.createApiKeyPair());
829
+ if (!publicKey) {
830
+ throw new sdkTypes.TurnkeyError("No public key available. Either pass a publicKey or ensure apiKeyStamper is configured.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST);
831
+ }
758
832
  return utils.withTurnkeyErrorHandling(async () => {
759
- const verifyOtpRes = await this.httpClient.proxyVerifyOtp({
833
+ const encryptedOtpBundle = await crypto.encryptOtpCodeToBundle(otpCode, otpEncryptionTargetBundle, publicKey);
834
+ const verifyOtpRes = await this.httpClient.proxyVerifyOtpV2({
760
835
  otpId: otpId,
761
- otpCode: otpCode,
762
- publicKey: resolvedPublicKey,
836
+ encryptedOtpBundle: encryptedOtpBundle,
763
837
  });
764
838
  if (!verifyOtpRes) {
765
839
  throw new sdkTypes.TurnkeyError(`OTP verification failed`, sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
766
840
  }
767
- const accountRes = await this.httpClient.proxyGetAccount({
768
- filterType: enums.OtpTypeToFilterTypeMap[otpType],
769
- filterValue: contact,
770
- verificationToken: verifyOtpRes.verificationToken,
771
- });
772
- if (!accountRes) {
773
- throw new sdkTypes.TurnkeyError(`Account fetch failed`, sdkTypes.TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
774
- }
775
- const subOrganizationId = accountRes.organizationId;
776
841
  return {
777
- subOrganizationId: subOrganizationId,
778
842
  verificationToken: verifyOtpRes.verificationToken,
843
+ publicKey,
779
844
  };
780
845
  }, {
781
846
  errorMessage: "Failed to verify OTP",
@@ -786,19 +851,29 @@ class TurnkeyClient {
786
851
  code: sdkTypes.TurnkeyErrorCodes.INVALID_OTP_CODE,
787
852
  },
788
853
  },
854
+ catchFn: async () => {
855
+ // we only clean up the generated key pair if we created it here
856
+ if (!params.publicKey) {
857
+ try {
858
+ await this.deleteApiKeyPair({ publicKey });
859
+ }
860
+ catch (cleanupError) {
861
+ throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
862
+ }
863
+ }
864
+ },
789
865
  });
790
866
  };
791
867
  /**
792
868
  * Logs in a user using an OTP verification token.
793
869
  *
794
870
  * - This function logs in a user using the verification token received after OTP verification (from email or SMS).
795
- * - If a public key is not provided, a new API key pair will be generated for authentication.
871
+ * - The client signature is always produced using the private key bound to the verification token during `verifyOtp`.
872
+ * - The verification token's embedded key becomes the session public key.
796
873
  * - Optionally invalidates any existing sessions for the user if `invalidateExisting` is set to true.
797
874
  * - Stores the resulting session token under the specified session key, or the default session key if not provided.
798
- * - Handles cleanup of unused key pairs if login fails.
799
875
  *
800
876
  * @param params.verificationToken - verification token received after OTP verification.
801
- * @param params.publicKey - public key to use for authentication. If not provided, a new key pair will be generated.
802
877
  * @param params.organizationId - optional organization ID to target (defaults to the verified subOrg ID linked to the verification token contact).
803
878
  * @param params.invalidateExisting - flag to invalidate existing session for the user.
804
879
  * @param params.sessionKey - session key to use for session creation (defaults to the default session key).
@@ -807,34 +882,35 @@ class TurnkeyClient {
807
882
  * @throws {TurnkeyError} If there is an error during the OTP login process or if key pair cleanup fails.
808
883
  */
809
884
  this.loginWithOtp = async (params) => {
810
- const { verificationToken, invalidateExisting = false, publicKey = await this.apiKeyStamper?.createKeyPair(), organizationId, sessionKey = enums.SessionKey.DefaultSessionkey, } = params;
885
+ const { verificationToken, invalidateExisting = false, organizationId, sessionKey = enums.SessionKey.DefaultSessionkey, } = params;
811
886
  return utils.withTurnkeyErrorHandling(async () => {
812
- const { message, publicKey: clientSignaturePublicKey } = utils.getClientSignatureMessageForLogin({
813
- verificationToken,
814
- sessionPublicKey: publicKey,
887
+ const { message, publicKey: verificationPublicKey } = utils.getClientSignatureMessageForLogin({ verificationToken });
888
+ // we sign with the verification token key. This is the key bound during
889
+ // verifyOtp() and is what Turnkey expects to sign the client signature for login
890
+ const signature = await this.signWithApiKey({
891
+ message,
892
+ publicKey: verificationPublicKey,
815
893
  });
816
- this.apiKeyStamper?.setTemporaryPublicKey(publicKey);
817
- const signature = await this.apiKeyStamper?.sign(message, apiKeyStamper.SignatureFormat.Raw);
818
894
  if (!signature) {
819
895
  throw new sdkTypes.TurnkeyError(`Failed to sign client signature for OTP login`, sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
820
896
  }
821
897
  const clientSignature = {
822
898
  message: message,
823
- publicKey: clientSignaturePublicKey,
899
+ publicKey: verificationPublicKey,
824
900
  scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
825
901
  signature: signature,
826
902
  };
827
- const res = await this.httpClient.proxyOtpLogin({
903
+ // the verification token's public key is also used as the session public key for login
904
+ const loginRes = await this.httpClient.proxyOtpLoginV2({
828
905
  verificationToken,
829
- publicKey: publicKey,
830
- invalidateExisting,
906
+ publicKey: verificationPublicKey,
831
907
  clientSignature,
908
+ invalidateExisting,
832
909
  ...(organizationId && { organizationId }),
833
910
  });
834
- if (!res) {
911
+ if (!loginRes) {
835
912
  throw new sdkTypes.TurnkeyError(`Auth proxy OTP login failed`, sdkTypes.TurnkeyErrorCodes.OTP_LOGIN_ERROR);
836
913
  }
837
- const loginRes = await res;
838
914
  if (!loginRes.session) {
839
915
  throw new sdkTypes.TurnkeyError("No session returned from OTP login", sdkTypes.TurnkeyErrorCodes.OTP_LOGIN_ERROR);
840
916
  }
@@ -848,21 +924,6 @@ class TurnkeyClient {
848
924
  }, {
849
925
  errorMessage: "Failed to log in with OTP",
850
926
  errorCode: sdkTypes.TurnkeyErrorCodes.OTP_LOGIN_ERROR,
851
- catchFn: async () => {
852
- // Clean up the generated key pair if it wasn't successfully used
853
- if (publicKey) {
854
- try {
855
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
856
- }
857
- catch (cleanupError) {
858
- throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
859
- }
860
- }
861
- },
862
- }, {
863
- finallyFn: async () => {
864
- this.apiKeyStamper?.clearTemporaryPublicKey();
865
- },
866
927
  });
867
928
  };
868
929
  /**
@@ -870,7 +931,7 @@ class TurnkeyClient {
870
931
  *
871
932
  * - This function signs up a user using the verification token received after OTP verification (from email or SMS).
872
933
  * - Creates a new sub-organization for the user with the provided parameters and associates the contact (email or phone) with the sub-organization.
873
- * - Automatically generates a new API key pair for authentication and session management.
934
+ * - The verification token's embedded key becomes the session public key.
874
935
  * - Stores the resulting session token under the specified session key, or the default session key if not provided.
875
936
  * - Handles both email and SMS OTP types, and supports additional sub-organization creation parameters.
876
937
  *
@@ -885,7 +946,7 @@ class TurnkeyClient {
885
946
  * @throws {TurnkeyError} If there is an error during the OTP sign-up process or session storage.
886
947
  */
887
948
  this.signUpWithOtp = async (params) => {
888
- const { verificationToken, contact, otpType, createSubOrgParams, invalidateExisting, sessionKey, publicKey = await this.apiKeyStamper?.createKeyPair(), } = params;
949
+ const { verificationToken, contact, otpType, createSubOrgParams, invalidateExisting, sessionKey, } = params;
889
950
  // build sign up body without client signature first
890
951
  const signUpBody = utils.buildSignUpBody({
891
952
  createSubOrgParams: {
@@ -897,7 +958,7 @@ class TurnkeyClient {
897
958
  },
898
959
  });
899
960
  return utils.withTurnkeyErrorHandling(async () => {
900
- const { message, publicKey: clientSignaturePublicKey } = utils.getClientSignatureMessageForSignup({
961
+ const { message, publicKey: verificationPublicKey } = utils.getClientSignatureMessageForSignup({
901
962
  verificationToken,
902
963
  ...(signUpBody.userEmail && { email: signUpBody.userEmail }),
903
964
  ...(signUpBody.userPhoneNumber && {
@@ -907,18 +968,22 @@ class TurnkeyClient {
907
968
  authenticators: signUpBody.authenticators,
908
969
  oauthProviders: signUpBody.oauthProviders,
909
970
  });
910
- this.apiKeyStamper?.setTemporaryPublicKey(publicKey);
911
- const signature = await this.apiKeyStamper?.sign(message, apiKeyStamper.SignatureFormat.Raw);
971
+ // we sign with the verification token key. This is the key bound during
972
+ // verifyOtp() and is what Turnkey expects to sign the client signature for signup
973
+ const signature = await this.signWithApiKey({
974
+ message,
975
+ publicKey: verificationPublicKey,
976
+ });
912
977
  if (!signature) {
913
978
  throw new sdkTypes.TurnkeyError(`Failed to sign client signature for OTP sign up`, sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
914
979
  }
915
980
  const clientSignature = {
916
981
  message: message,
917
- publicKey: clientSignaturePublicKey,
982
+ publicKey: verificationPublicKey,
918
983
  scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
919
984
  signature: signature,
920
985
  };
921
- const signupRes = await this.httpClient.proxySignup({
986
+ const signupRes = await this.httpClient.proxySignupV2({
922
987
  ...signUpBody,
923
988
  clientSignature,
924
989
  });
@@ -927,7 +992,6 @@ class TurnkeyClient {
927
992
  }
928
993
  const otpRes = await this.loginWithOtp({
929
994
  verificationToken,
930
- publicKey: publicKey,
931
995
  ...(invalidateExisting && { invalidateExisting }),
932
996
  ...(sessionKey && { sessionKey }),
933
997
  });
@@ -936,39 +1000,26 @@ class TurnkeyClient {
936
1000
  appProofs: signupRes.appProofs,
937
1001
  };
938
1002
  }, {
939
- catchFn: async () => {
940
- // Clean up the generated key pair if it wasn't successfully used
941
- if (publicKey) {
942
- try {
943
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
944
- }
945
- catch (cleanupError) {
946
- throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
947
- }
948
- }
949
- },
950
1003
  errorCode: sdkTypes.TurnkeyErrorCodes.OTP_SIGNUP_ERROR,
951
1004
  errorMessage: "Failed to sign up with OTP",
952
- }, {
953
- finallyFn: async () => {
954
- this.apiKeyStamper?.clearTemporaryPublicKey();
955
- },
956
1005
  });
957
1006
  };
958
1007
  /**
959
- * Completes the OTP authentication flow by verifying the OTP code and then either signing up or logging in the user.
1008
+ * Completes the OTP authentication flow by verifying the encrypted OTP bundle and then either signing up or logging in the user.
960
1009
  *
961
- * - This function first verifies the OTP code for the provided contact and OTP type.
1010
+ * - This function encrypts the OTP code and the session public key to the enclave's target key, verifies the OTP, and then either signs up or logs in the user.
962
1011
  * - If the contact is not associated with an existing sub-organization, it will automatically create a new sub-organization and complete the sign-up flow.
963
1012
  * - If the contact is already associated with a sub-organization, it will complete the login flow.
964
- * - Supports passing a custom public key for authentication, invalidating existing session, specifying a session key, and providing additional sub-organization creation parameters.
1013
+ * - The key bound to the verification token during `verifyOtp` is always reused as the session public key.
1014
+ * - Supports passing a custom public key for binding into the verification token, invalidating existing session, specifying a session key, and providing additional sub-organization creation parameters.
965
1015
  * - Handles both email and SMS OTP types.
966
1016
  *
967
1017
  * @param params.otpId - ID of the OTP to complete (returned from `initOtp`).
968
- * @param params.otpCode - OTP code entered by the user.
1018
+ * @param params.otpCode - the OTP code entered by the user.
1019
+ * @param params.otpEncryptionTargetBundle - the encryption target bundle returned from `initOtp`.
969
1020
  * @param params.contact - contact information for the user (e.g., email address or phone number).
970
1021
  * @param params.otpType - type of OTP being completed (OtpType.Email or OtpType.Sms).
971
- * @param params.publicKey - public key to use for authentication. If not provided, a new key pair may be generated.
1022
+ * @param params.publicKey - public key to bind to the verification token via `verifyOtp`. If not provided, a new key pair will be generated. This key becomes the session public key.
972
1023
  * @param params.invalidateExisting - flag to invalidate existing sessions for the user.
973
1024
  * @param params.sessionKey - session key to use for session creation (defaults to the default session key).
974
1025
  * @param params.createSubOrgParams - parameters for sub-organization creation (e.g., authenticators, user metadata).
@@ -979,18 +1030,30 @@ class TurnkeyClient {
979
1030
  * @throws {TurnkeyError} If there is an error during OTP verification, sign-up, or login.
980
1031
  */
981
1032
  this.completeOtp = async (params) => {
982
- const { otpId, otpCode, contact, otpType, publicKey = await this.apiKeyStamper?.createKeyPair(), invalidateExisting = false, sessionKey, createSubOrgParams, } = params;
1033
+ const { otpId, otpCode, otpEncryptionTargetBundle, contact, otpType, invalidateExisting = false, sessionKey, createSubOrgParams, } = params;
1034
+ const publicKey = params.publicKey ?? (await this.createApiKeyPair());
1035
+ if (!publicKey) {
1036
+ throw new sdkTypes.TurnkeyError("No public key available. Either pass a publicKey or ensure apiKeyStamper is configured.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST);
1037
+ }
983
1038
  return utils.withTurnkeyErrorHandling(async () => {
984
- const { subOrganizationId, verificationToken } = await this.verifyOtp({
985
- otpId: otpId,
986
- otpCode: otpCode,
987
- contact: contact,
988
- otpType: otpType,
989
- publicKey: publicKey,
1039
+ const { verificationToken } = await this.verifyOtp({
1040
+ otpId,
1041
+ otpCode,
1042
+ otpEncryptionTargetBundle,
1043
+ publicKey,
990
1044
  });
991
1045
  if (!verificationToken) {
992
1046
  throw new sdkTypes.TurnkeyError("No verification token returned from OTP verification", sdkTypes.TurnkeyErrorCodes.VERIFY_OTP_ERROR);
993
1047
  }
1048
+ const accountRes = await this.httpClient.proxyGetAccount({
1049
+ filterType: enums.OtpTypeToFilterTypeMap[otpType],
1050
+ filterValue: contact,
1051
+ verificationToken: verificationToken,
1052
+ });
1053
+ if (!accountRes) {
1054
+ throw new sdkTypes.TurnkeyError(`Account fetch failed`, sdkTypes.TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
1055
+ }
1056
+ const subOrganizationId = accountRes.organizationId;
994
1057
  if (!subOrganizationId) {
995
1058
  const signUpRes = await this.signUpWithOtp({
996
1059
  verificationToken,
@@ -999,7 +1062,6 @@ class TurnkeyClient {
999
1062
  ...(createSubOrgParams && { createSubOrgParams }),
1000
1063
  ...(invalidateExisting && { invalidateExisting }),
1001
1064
  ...(sessionKey && { sessionKey }),
1002
- publicKey: publicKey,
1003
1065
  });
1004
1066
  return {
1005
1067
  ...signUpRes,
@@ -1010,7 +1072,6 @@ class TurnkeyClient {
1010
1072
  else {
1011
1073
  const loginRes = await this.loginWithOtp({
1012
1074
  verificationToken,
1013
- publicKey: publicKey,
1014
1075
  ...(invalidateExisting && { invalidateExisting }),
1015
1076
  ...(sessionKey && { sessionKey }),
1016
1077
  });
@@ -1023,6 +1084,16 @@ class TurnkeyClient {
1023
1084
  }, {
1024
1085
  errorMessage: "Failed to complete OTP process",
1025
1086
  errorCode: sdkTypes.TurnkeyErrorCodes.OTP_COMPLETION_ERROR,
1087
+ catchFn: async () => {
1088
+ if (!params.publicKey) {
1089
+ try {
1090
+ await this.deleteApiKeyPair({ publicKey });
1091
+ }
1092
+ catch (cleanupError) {
1093
+ throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
1094
+ }
1095
+ }
1096
+ },
1026
1097
  });
1027
1098
  };
1028
1099
  /**
@@ -1147,7 +1218,7 @@ class TurnkeyClient {
1147
1218
  // Clean up the generated key pair if it wasn't successfully used
1148
1219
  if (publicKey) {
1149
1220
  try {
1150
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
1221
+ await this.deleteApiKeyPair({ publicKey });
1151
1222
  }
1152
1223
  catch (cleanupError) {
1153
1224
  throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
@@ -1184,10 +1255,11 @@ class TurnkeyClient {
1184
1255
  providerName,
1185
1256
  oidcToken,
1186
1257
  },
1258
+ ...(createSubOrgParams?.oauthProviders ?? []),
1187
1259
  ],
1188
1260
  },
1189
1261
  });
1190
- const signupRes = await this.httpClient.proxySignup(signUpBody);
1262
+ const signupRes = await this.httpClient.proxySignupV2(signUpBody);
1191
1263
  if (!signupRes) {
1192
1264
  throw new sdkTypes.TurnkeyError(`Auth proxy OAuth signup failed`, sdkTypes.TurnkeyErrorCodes.OAUTH_SIGNUP_ERROR);
1193
1265
  }
@@ -1766,7 +1838,7 @@ class TurnkeyClient {
1766
1838
  throw new sdkTypes.TurnkeyError("Invalid ERC20 amount. Use a base-unit integer string.", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST);
1767
1839
  }
1768
1840
  const data = viem.encodeFunctionData({
1769
- abi: ERC20_TRANSFER_ABI,
1841
+ abi: utils.ERC20_TRANSFER_ABI,
1770
1842
  functionName: "transfer",
1771
1843
  args: [to, parsedAmount],
1772
1844
  });
@@ -2349,37 +2421,71 @@ class TurnkeyClient {
2349
2421
  });
2350
2422
  };
2351
2423
  /**
2352
- * Adds an OAuth provider to the user.
2424
+ * Adds one or more OAuth provider audiences to the user.
2353
2425
  *
2354
- * - This function adds an OAuth provider (e.g., Google, Apple) to the user account.
2355
- * - If a userId is provided, it adds the provider for that specific user; otherwise, it uses the current session's userId.
2356
- * - Automatically checks if an account already exists for the provided OIDC token and prevents duplicate associations.
2357
- * - If the user's email is not set or not verified, attempts to update and verify the email using the email from the OIDC token.
2358
- * - Handles session management and error reporting for the add provider flow.
2426
+ * - Accepts either an `oidcToken`, a list of `oidcClaims`, or both. At least one must be provided.
2427
+ * - The issuer (`iss`) of the `oidcToken` and all `oidcClaims` must match, ensuring they belong to the same OAuth provider.
2428
+ * - Checks for an existing account for the `oidcToken` (if provided) and for each `oidcClaims` entry, and throws if any of them are already associated with a different sub-organization.
2429
+ * - If the user's email is not set or not verified and an `oidcToken` is provided, attempts to update and verify the email using the email from the token.
2430
+ * - Submits a single `createOauthProviders` request containing the token (if any) and all additional claims.
2359
2431
  * - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
2360
2432
  *
2361
2433
  * @param params.providerName - name of the OAuth provider to add (e.g., "Google", "Apple").
2362
- * @param params.oidcToken - OIDC token for the OAuth provider.
2434
+ * @param params.oidcToken - OIDC token for the OAuth provider. Either `oidcToken` or `oidcClaims` (or both) must be provided.
2435
+ * @param params.oidcClaims - additional `iss`/`sub`/`aud` claims to register as additional audiences for the same identity.
2363
2436
  * @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
2364
2437
  * @param params.userId - user ID to add the provider for a specific user (defaults to current session's userId).
2365
2438
  * @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
2366
2439
  * @returns A promise that resolves to an array of provider IDs associated with the user.
2367
- * @throws {TurnkeyError} If there is no active session, if the account already exists, or if there is an error adding the OAuth provider.
2440
+ * @throws {TurnkeyError} If there is no active session, if neither `oidcToken` nor `oidcClaims` was provided, if the issuer claims do not all match, if any of the audiences are already associated with another account, or if there is an error adding the OAuth provider.
2368
2441
  */
2369
2442
  this.addOauthProvider = async (params) => {
2370
- const { providerName, oidcToken, stampWith = this.config.defaultStamperType, } = params;
2443
+ const { providerName, oidcToken, oidcClaims = [], stampWith = this.config.defaultStamperType, } = params;
2444
+ if (!oidcToken && oidcClaims.length === 0) {
2445
+ throw new sdkTypes.TurnkeyError("At least one of `oidcToken` or `oidcClaims` must be provided to add an OAuth provider", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST);
2446
+ }
2371
2447
  const session = await utils.getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
2372
2448
  return utils.withTurnkeyErrorHandling(async () => {
2373
- const accountRes = await this.httpClient.proxyGetAccount({
2374
- filterType: "OIDC_TOKEN",
2375
- filterValue: oidcToken,
2376
- });
2377
- if (!accountRes) {
2378
- throw new sdkTypes.TurnkeyError(`Account fetch failed`, sdkTypes.TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
2379
- }
2380
- if (accountRes.organizationId &&
2381
- accountRes.organizationId !== session?.organizationId) {
2382
- throw new sdkTypes.TurnkeyError("Account already exists with this OIDC token", sdkTypes.TurnkeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
2449
+ // Parse the oidc token so we can get the email/iss. The email is used to update the user's email for social linking.
2450
+ // The iss is used to ensure all passed-in claims share the same issuer.
2451
+ const { email: oidcEmail, iss: tokenIss } = oidcToken
2452
+ ? jwtDecode.jwtDecode(oidcToken) || {}
2453
+ : { email: undefined, iss: undefined };
2454
+ // Verify that all passed-in claims share the same issuer (and match the token's issuer if a token was provided).
2455
+ const issuers = new Set();
2456
+ if (tokenIss)
2457
+ issuers.add(tokenIss);
2458
+ for (const claim of oidcClaims) {
2459
+ issuers.add(claim.iss);
2460
+ }
2461
+ if (issuers.size > 1) {
2462
+ throw new sdkTypes.TurnkeyError("All `oidcToken` and `oidcClaims` entries must share the same issuer (`iss`)", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST);
2463
+ }
2464
+ // Check that none of the audiences are already associated with a different sub-organization.
2465
+ const accountChecks = [];
2466
+ if (oidcToken) {
2467
+ // Look up the sub-organization associated with passed in oidc token
2468
+ accountChecks.push(this.httpClient.proxyGetAccount({
2469
+ filterType: enums.FilterType.OidcToken,
2470
+ filterValue: oidcToken,
2471
+ }));
2472
+ }
2473
+ // Also look up the sub-organization for each oidcClaim
2474
+ for (const claim of oidcClaims) {
2475
+ accountChecks.push(this.httpClient.proxyGetAccount({
2476
+ filterType: enums.FilterType.OidcClaims,
2477
+ filterValue: JSON.stringify(claim),
2478
+ }));
2479
+ }
2480
+ const accountResults = await Promise.all(accountChecks);
2481
+ for (const accountRes of accountResults) {
2482
+ if (!accountRes) {
2483
+ throw new sdkTypes.TurnkeyError(`Account fetch failed`, sdkTypes.TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
2484
+ }
2485
+ if (accountRes.organizationId &&
2486
+ accountRes.organizationId !== session?.organizationId) {
2487
+ throw new sdkTypes.TurnkeyError("Account already exists with this OIDC token or claims", sdkTypes.TurnkeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
2488
+ }
2383
2489
  }
2384
2490
  const userId = params?.userId || session?.userId;
2385
2491
  if (!userId) {
@@ -2389,9 +2495,7 @@ class TurnkeyClient {
2389
2495
  if (!organizationId) {
2390
2496
  throw new sdkTypes.TurnkeyError("Organization ID is required to add OAuth provider", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST);
2391
2497
  }
2392
- // parse the oidc token so we can get the email. Pass it in to updateUser then call createOauthProviders. This will be verified by Turnkey.
2393
- const { email: oidcEmail, iss } = jwtDecode.jwtDecode(oidcToken) || {};
2394
- if (iss === utils.googleISS) {
2498
+ if (oidcToken && tokenIss === utils.googleISS) {
2395
2499
  const verifiedSuborg = await this.httpClient.proxyGetAccount({
2396
2500
  filterType: "EMAIL",
2397
2501
  filterValue: oidcEmail,
@@ -2424,14 +2528,13 @@ class TurnkeyClient {
2424
2528
  }, stampWith);
2425
2529
  }
2426
2530
  }
2531
+ const oauthProviders = [
2532
+ ...(oidcToken ? [{ providerName, oidcToken }] : []),
2533
+ ...oidcClaims.map((claim) => ({ providerName, oidcClaims: claim })),
2534
+ ];
2427
2535
  const createProviderRes = await this.httpClient.createOauthProviders({
2428
2536
  userId,
2429
- oauthProviders: [
2430
- {
2431
- providerName,
2432
- oidcToken,
2433
- },
2434
- ],
2537
+ oauthProviders,
2435
2538
  }, stampWith);
2436
2539
  if (!createProviderRes) {
2437
2540
  throw new sdkTypes.TurnkeyError("Failed to create OAuth provider", sdkTypes.TurnkeyErrorCodes.ADD_OAUTH_PROVIDER_ERROR);
@@ -2975,7 +3078,7 @@ class TurnkeyClient {
2975
3078
  const session = await this.storageManager.getSession(sessionKey);
2976
3079
  if (session) {
2977
3080
  await Promise.all([
2978
- this.apiKeyStamper?.deleteKeyPair(session.publicKey),
3081
+ this.deleteApiKeyPair({ publicKey: session.publicKey }),
2979
3082
  this.storageManager.clearSession(sessionKey),
2980
3083
  ]);
2981
3084
  }
@@ -3043,9 +3146,8 @@ class TurnkeyClient {
3043
3146
  if (!this.httpClient) {
3044
3147
  throw new sdkTypes.TurnkeyError("HTTP client is not initialized. Please initialize the client before refreshing the session.", sdkTypes.TurnkeyErrorCodes.CLIENT_NOT_INITIALIZED);
3045
3148
  }
3046
- let keyPair;
3149
+ const keyPair = publicKey ?? (await this.createApiKeyPair());
3047
3150
  return utils.withTurnkeyErrorHandling(async () => {
3048
- keyPair = publicKey ?? (await this.apiKeyStamper?.createKeyPair());
3049
3151
  if (!keyPair) {
3050
3152
  throw new sdkTypes.TurnkeyError("Failed to create new key pair.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
3051
3153
  }
@@ -3065,6 +3167,17 @@ class TurnkeyClient {
3065
3167
  }, {
3066
3168
  errorMessage: "Failed to refresh session",
3067
3169
  errorCode: sdkTypes.TurnkeyErrorCodes.REFRESH_SESSION_ERROR,
3170
+ catchFn: async () => {
3171
+ // we only delete the keypair if we generated it here
3172
+ if (!publicKey) {
3173
+ try {
3174
+ await this.deleteApiKeyPair({ publicKey: keyPair });
3175
+ }
3176
+ catch (cleanupError) {
3177
+ throw new sdkTypes.TurnkeyError("Failed to clean up generated key pair", sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
3178
+ }
3179
+ }
3180
+ },
3068
3181
  });
3069
3182
  };
3070
3183
  /**
@@ -3185,7 +3298,7 @@ class TurnkeyClient {
3185
3298
  for (const publicKey of publicKeys) {
3186
3299
  if (!sessionTokensMap[publicKey]) {
3187
3300
  try {
3188
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
3301
+ await this.deleteApiKeyPair({ publicKey });
3189
3302
  }
3190
3303
  catch (error) {
3191
3304
  throw new sdkTypes.TurnkeyError(`Failed to delete unused key pair ${publicKey}`, sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR, error);
@@ -3200,34 +3313,84 @@ class TurnkeyClient {
3200
3313
  /**
3201
3314
  * Creates a new API key pair and returns the public key.
3202
3315
  *
3203
- * - This function generates a new API key pair and stores it in the underlying key store (IndexedDB).
3316
+ * - This function generates a new API key pair and stores it in the underlying key store.
3204
3317
  * - If an external key pair is provided, it will use that key pair for creation instead of generating a new one.
3205
- * - If `storeOverride` is set to true, the generated or provided public key will be set as the override key in the API key stamper, making it the active key for subsequent signing operations.
3206
3318
  * - Ensures the API key stamper is initialized before proceeding.
3207
3319
  * - Handles both native CryptoKeyPair objects and raw key material.
3208
3320
  *
3209
3321
  * @param params.externalKeyPair - An externally generated key pair (either a CryptoKeyPair or an object with publicKey/privateKey strings) to use instead of generating a new one.
3210
- * @param params.storeOverride - If true, sets the generated or provided public key as the override key in the API key stamper (defaults to false).
3211
3322
  * @return A promise that resolves to the public key of the created or provided API key pair as a string.
3212
3323
  * @throws {TurnkeyError} If the API key stamper is not initialized or if there is an error during key pair creation or storage.
3213
3324
  */
3214
3325
  this.createApiKeyPair = async (params) => {
3215
3326
  return utils.withTurnkeyErrorHandling(async () => {
3216
3327
  const externalKeyPair = params?.externalKeyPair;
3217
- const storeOverride = params?.storeOverride ?? false;
3218
3328
  if (!this.apiKeyStamper) {
3219
3329
  throw new sdkTypes.TurnkeyError("API Key Stamper is not initialized.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
3220
3330
  }
3221
3331
  const publicKey = await this.apiKeyStamper.createKeyPair(externalKeyPair ? externalKeyPair : undefined);
3222
- if (storeOverride && publicKey) {
3223
- this.apiKeyStamper.setTemporaryPublicKey(publicKey);
3224
- }
3225
3332
  return publicKey;
3226
3333
  }, {
3227
3334
  errorMessage: "Failed to create API key pair",
3228
3335
  errorCode: sdkTypes.TurnkeyErrorCodes.CREATE_API_KEY_PAIR_ERROR,
3229
3336
  });
3230
3337
  };
3338
+ /**
3339
+ * Deletes an API key pair from persistent storage.
3340
+ *
3341
+ * - This function removes a stored API key pair from the underlying key store (IndexedDB) based on its public key.
3342
+ * - Ensures that the API key stamper is initialized before proceeding with deletion.
3343
+ * - Intended for cleaning up key pairs that are not associated with active sessions, such as after failed authentication attempts where a temporary key pair was created but never used.
3344
+ * - Should NOT be used to delete key pairs that are associated with active sessions; use `clearSession` or `clearAllSessions` instead.
3345
+ *
3346
+ * @param params.publicKey - The public key of the API key pair to delete.
3347
+ * @returns A promise that resolves when the key pair is successfully deleted.
3348
+ * @throws {TurnkeyError} If the API key stamper is not initialized or if there is an error during key pair deletion.
3349
+ */
3350
+ this.deleteApiKeyPair = async (params) => {
3351
+ const { publicKey } = params;
3352
+ return utils.withTurnkeyErrorHandling(async () => {
3353
+ if (!this.apiKeyStamper) {
3354
+ throw new sdkTypes.TurnkeyError("API Key Stamper is not initialized.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
3355
+ }
3356
+ await this.apiKeyStamper.deleteKeyPair(publicKey);
3357
+ }, {
3358
+ errorMessage: "Failed to delete API key pair",
3359
+ errorCode: sdkTypes.TurnkeyErrorCodes.DELETE_API_KEY_PAIR_ERROR,
3360
+ });
3361
+ };
3362
+ /**
3363
+ * Signs a message using a key pair stored in the API key stamper.
3364
+ *
3365
+ * - This function signs the provided message string with the private key identified by `publicKey`.
3366
+ * - The message is SHA-256 hashed internally before signing (ECDSA P-256).
3367
+ * - Returns a compact hex signature (r || s) suitable for use as a client signature.
3368
+ * - The key pair must already exist in the key store (e.g., created via `createApiKeyPair`).
3369
+ *
3370
+ * @param params.message - the message string to sign.
3371
+ * @param params.publicKey - the public key identifying which key pair to sign with.
3372
+ * @returns A promise that resolves to the compact hex signature string.
3373
+ * @throws {TurnkeyError} If the API key stamper is not initialized or the key pair is not found.
3374
+ */
3375
+ this.signWithApiKey = async (params) => {
3376
+ const { message, publicKey } = params;
3377
+ let previousTemporaryKey;
3378
+ return utils.withTurnkeyErrorHandling(async () => {
3379
+ if (!this.apiKeyStamper) {
3380
+ throw new sdkTypes.TurnkeyError("API Key Stamper is not initialized.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
3381
+ }
3382
+ previousTemporaryKey = this.apiKeyStamper.getTemporaryPublicKey();
3383
+ this.apiKeyStamper.setTemporaryPublicKey(publicKey);
3384
+ return await this.apiKeyStamper.sign(message, apiKeyStamper.SignatureFormat.Raw);
3385
+ }, {
3386
+ errorMessage: "Failed to sign with API key",
3387
+ errorCode: sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR,
3388
+ }, {
3389
+ finallyFn: async () => {
3390
+ this.apiKeyStamper?.setTemporaryPublicKey(previousTemporaryKey);
3391
+ },
3392
+ });
3393
+ };
3231
3394
  /**
3232
3395
  * Fetches the WalletKit proxy authentication configuration from the auth proxy.
3233
3396
  *