@turnkey/core 1.14.1 → 2.0.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 (83) 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 +354 -195
  4. package/dist/__clients__/core.js.map +1 -1
  5. package/dist/__clients__/core.mjs +355 -196
  6. package/dist/__clients__/core.mjs.map +1 -1
  7. package/dist/__generated__/sdk-client-base.d.ts +8 -3
  8. package/dist/__generated__/sdk-client-base.d.ts.map +1 -1
  9. package/dist/__generated__/sdk-client-base.js +117 -55
  10. package/dist/__generated__/sdk-client-base.js.map +1 -1
  11. package/dist/__generated__/sdk-client-base.mjs +112 -50
  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/__types__/auth.d.ts +2 -41
  34. package/dist/__types__/auth.d.ts.map +1 -1
  35. package/dist/__types__/auth.js.map +1 -1
  36. package/dist/__types__/auth.mjs.map +1 -1
  37. package/dist/__types__/config.d.ts +3 -2
  38. package/dist/__types__/config.d.ts.map +1 -1
  39. package/dist/__types__/enums.d.ts +1 -0
  40. package/dist/__types__/enums.d.ts.map +1 -1
  41. package/dist/__types__/enums.js +1 -0
  42. package/dist/__types__/enums.js.map +1 -1
  43. package/dist/__types__/enums.mjs +1 -0
  44. package/dist/__types__/enums.mjs.map +1 -1
  45. package/dist/__types__/http.d.ts +1 -14
  46. package/dist/__types__/http.d.ts.map +1 -1
  47. package/dist/__types__/index.d.ts +0 -1
  48. package/dist/__types__/index.d.ts.map +1 -1
  49. package/dist/__types__/method-types/shared.d.ts +28 -13
  50. package/dist/__types__/method-types/shared.d.ts.map +1 -1
  51. package/dist/__wallet__/stamper.d.ts +2 -1
  52. package/dist/__wallet__/stamper.d.ts.map +1 -1
  53. package/dist/__wallet__/stamper.js.map +1 -1
  54. package/dist/__wallet__/stamper.mjs.map +1 -1
  55. package/dist/index.d.ts +1 -2
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +8 -6
  58. package/dist/index.js.map +1 -1
  59. package/dist/index.mjs +2 -4
  60. package/dist/index.mjs.map +1 -1
  61. package/dist/turnkey-helpers.d.ts +4 -0
  62. package/dist/turnkey-helpers.d.ts.map +1 -1
  63. package/dist/turnkey-helpers.js +28 -0
  64. package/dist/turnkey-helpers.js.map +1 -1
  65. package/dist/turnkey-helpers.mjs +25 -1
  66. package/dist/turnkey-helpers.mjs.map +1 -1
  67. package/dist/utils.d.ts +41 -18
  68. package/dist/utils.d.ts.map +1 -1
  69. package/dist/utils.js +126 -57
  70. package/dist/utils.js.map +1 -1
  71. package/dist/utils.mjs +100 -38
  72. package/dist/utils.mjs.map +1 -1
  73. package/package.json +7 -7
  74. package/dist/__types__/error.d.ts +0 -13
  75. package/dist/__types__/error.d.ts.map +0 -1
  76. package/dist/__types__/error.js +0 -18
  77. package/dist/__types__/error.js.map +0 -1
  78. package/dist/__types__/error.mjs +0 -16
  79. package/dist/__types__/error.mjs.map +0 -1
  80. package/dist/__types__/http.js +0 -11
  81. package/dist/__types__/http.js.map +0 -1
  82. package/dist/__types__/http.mjs +0 -9
  83. package/dist/__types__/http.mjs.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { TurnkeySDKClientBase } from '../__generated__/sdk-client-base.mjs';
2
2
  import { TurnkeyErrorCodes, TurnkeyError, AuthAction } from '@turnkey/sdk-types';
3
3
  import { DEFAULT_SESSION_EXPIRATION_IN_SECONDS } from '../__types__/auth.mjs';
4
- import { SessionKey, StamperType, WalletSource, FilterType, OtpTypeToFilterTypeMap, OtpType, Curve, Chain, SignIntent } from '../__types__/enums.mjs';
5
- import { withTurnkeyErrorHandling, isValidPasskeyName, isWeb, isReactNative, buildSignUpBody, findWalletProviderFromAddress, getPublicKeyFromStampHeader, addressFromPublicKey, getCurveTypeFromProvider, getClientSignatureMessageForLogin, getClientSignatureMessageForSignup, getAuthenticatorAddresses, fetchAllWalletAccountsWithCursor, mapAccountsToWallet, toExternalTimestamp, isEthereumProvider, isSolanaProvider, getActiveSessionOrThrowIfRequired, getHashFunction, getEncodingType, getEncodedMessage, splitSignature, broadcastTransaction, getPolicySignature, googleISS, isWalletAccountArray, generateWalletAccountsFromAddressFormat } from '../utils.mjs';
4
+ import { SessionKey, StamperType, WalletSource, FilterType, OtpType, OtpTypeToFilterTypeMap, Curve, Chain, SignIntent } from '../__types__/enums.mjs';
5
+ import { withTurnkeyErrorHandling, isValidPasskeyName, isWeb, isReactNative, buildSignUpBody, findWalletProviderFromAddress, getPublicKeyFromStampHeader, addressFromPublicKey, getCurveTypeFromProvider, getClientSignatureMessageForLogin, getClientSignatureMessageForSignup, getAuthenticatorAddresses, fetchAllWalletAccountsWithCursor, mapAccountsToWallet, toExternalTimestamp, isEthereumProvider, isSolanaProvider, getActiveSessionOrThrowIfRequired, getHashFunction, getEncodingType, getEncodedMessage, splitSignature, broadcastTransaction, ERC20_TRANSFER_ABI, getPolicySignature, googleISS, isWalletAccountArray, generateWalletAccountsFromAddressFormat } from '../utils.mjs';
6
+ import { encryptOtpCodeToBundle, verify } from '@turnkey/crypto';
6
7
  import { createStorageManager } from '../__storage__/base.mjs';
7
8
  import { CrossPlatformApiKeyStamper } from '../__stampers__/api/base.mjs';
8
9
  import { CrossPlatformPasskeyStamper } from '../__stampers__/passkey/base.mjs';
@@ -10,26 +11,68 @@ import { DEFAULT_ETHEREUM_ACCOUNTS, DEFAULT_SOLANA_ACCOUNTS } from '../turnkey-h
10
11
  import { jwtDecode } from 'jwt-decode';
11
12
  import { createWalletManager } from '../__wallet__/base.mjs';
12
13
  import { toUtf8Bytes } from 'ethers';
13
- import { verify } from '@turnkey/crypto';
14
14
  import { SignatureFormat } from '@turnkey/api-key-stamper';
15
15
  import { encodeFunctionData } from 'viem';
16
16
 
17
- const ERC20_TRANSFER_ABI = [
18
- {
19
- type: "function",
20
- name: "transfer",
21
- stateMutability: "nonpayable",
22
- inputs: [
23
- { name: "to", type: "address" },
24
- { name: "amount", type: "uint256" },
25
- ],
26
- outputs: [{ name: "success", type: "bool" }],
27
- },
28
- ];
29
17
  class TurnkeyClient {
30
18
  constructor(config,
31
19
  // Users can pass in their own stampers, or we will create them. Should we remove this?
32
20
  apiKeyStamper, passkeyStamper, walletManager) {
21
+ /**
22
+ * Overrides the API key stamper configuration.
23
+ *
24
+ * - Set `temporaryPublicKey` to a non-empty string to override the default API key.
25
+ * - Set `temporaryPublicKey` to an empty string ("") to clear the temporary key and restore default behavior.
26
+ * - Omit `temporaryPublicKey` to leave it unchanged.
27
+ * - Useful for dynamically changing the API key used for signing requests.
28
+ *
29
+ * @param params.temporaryPublicKey - temporary public key to set (non-empty string), clear (empty string), or leave unchanged (omit).
30
+ * @returns A promise that resolves when the stamper has been updated.
31
+ * @throws {TurnkeyError} If the API key stamper is not initialized or if there is an error updating it.
32
+ */
33
+ this.overrideApiKeyStamper = async (params) => {
34
+ return withTurnkeyErrorHandling(async () => {
35
+ if (!this.apiKeyStamper) {
36
+ throw new TurnkeyError("API key stamper is not initialized", TurnkeyErrorCodes.INTERNAL_ERROR);
37
+ }
38
+ const { temporaryPublicKey } = params;
39
+ // Only process if temporaryPublicKey is explicitly passed
40
+ if (temporaryPublicKey !== undefined) {
41
+ if (temporaryPublicKey === "") {
42
+ this.apiKeyStamper.clearTemporaryPublicKey();
43
+ }
44
+ else {
45
+ this.apiKeyStamper.setTemporaryPublicKey(temporaryPublicKey);
46
+ }
47
+ }
48
+ }, {
49
+ errorMessage: "Failed to override API key stamper",
50
+ errorCode: TurnkeyErrorCodes.INITIALIZE_API_KEY_STAMPER_ERROR,
51
+ });
52
+ };
53
+ /**
54
+ * Overrides the passkey stamper with a new configuration.
55
+ *
56
+ * - This function updates the passkey stamper configuration without reinitializing the entire stamper.
57
+ * - Updates the stamper's configuration properties (e.g., allowCredentials, rpId, timeout, userVerification).
58
+ * - Useful for dynamically changing passkey configuration during runtime.
59
+ *
60
+ * @param params.config - new passkey stamper configuration to use.
61
+ * @returns A promise that resolves when the stamper configuration has been updated.
62
+ * @throws {TurnkeyError} If there is an error updating the stamper configuration or if the passkey stamper is not initialized.
63
+ */
64
+ this.overridePasskeyStamper = async (params) => {
65
+ const { config } = params;
66
+ return withTurnkeyErrorHandling(async () => {
67
+ if (!this.passkeyStamper) {
68
+ throw new TurnkeyError("Passkey stamper is not initialized", TurnkeyErrorCodes.INTERNAL_ERROR);
69
+ }
70
+ this.passkeyStamper.updateConfig(config);
71
+ }, {
72
+ errorMessage: "Failed to override passkey stamper",
73
+ errorCode: TurnkeyErrorCodes.INITIALIZE_PASSKEY_STAMPER_ERROR,
74
+ });
75
+ };
33
76
  /**
34
77
  * Creates a new TurnkeySDKClientBase instance with the provided configuration.
35
78
  * This method is used internally to create the HTTP client for making API requests,
@@ -147,14 +190,14 @@ class TurnkeyClient {
147
190
  if (params?.sessionKey) {
148
191
  const session = await this.storageManager.getSession(params.sessionKey);
149
192
  this.storageManager.clearSession(params.sessionKey);
150
- this.apiKeyStamper?.deleteKeyPair(session?.publicKey);
193
+ await this.deleteApiKeyPair({ publicKey: session?.publicKey });
151
194
  }
152
195
  else {
153
196
  const sessionKey = await this.storageManager.getActiveSessionKey();
154
197
  const session = await this.storageManager.getActiveSession();
155
198
  if (sessionKey) {
156
199
  this.storageManager.clearSession(sessionKey);
157
- this.apiKeyStamper?.deleteKeyPair(session?.publicKey);
200
+ await this.deleteApiKeyPair({ publicKey: session?.publicKey });
158
201
  }
159
202
  else {
160
203
  throw new TurnkeyError("No active session found to log out from.", TurnkeyErrorCodes.NO_SESSION_FOUND);
@@ -178,18 +221,30 @@ class TurnkeyClient {
178
221
  * @param params.sessionKey - session key to use for session creation (defaults to the default session key).
179
222
  * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
180
223
  * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
224
+ * @param params.allowCredentials - optional list of allowed credentials for passkey authentication. This allows you to restrict which passkeys can be used for login.
181
225
  * @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes:
182
226
  * - `sessionToken`: the signed JWT session token.
183
227
  * - `credentialId`: an empty string.
184
228
  * @throws {TurnkeyError} If there is an error during the passkey login process or if the user cancels the passkey prompt.
185
229
  */
186
230
  this.loginWithPasskey = async (params) => {
187
- let generatedPublicKey = undefined;
231
+ const shouldOverrideConfig = params?.allowCredentials && this.passkeyStamper;
232
+ const currentConfig = this.config.passkeyConfig;
233
+ const generatedPublicKey = params?.publicKey || (await this.createApiKeyPair());
188
234
  return await withTurnkeyErrorHandling(async () => {
189
- generatedPublicKey =
190
- params?.publicKey || (await this.apiKeyStamper?.createKeyPair());
235
+ if (!this.passkeyStamper) {
236
+ throw new TurnkeyError("Passkey stamper is not initialized", TurnkeyErrorCodes.INTERNAL_ERROR);
237
+ }
191
238
  const sessionKey = params?.sessionKey || SessionKey.DefaultSessionkey;
192
239
  const expirationSeconds = params?.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
240
+ if (shouldOverrideConfig) {
241
+ // Override passkey stamper config to include allowCredentials
242
+ const mergedConfig = {
243
+ ...currentConfig,
244
+ allowCredentials: params?.allowCredentials, // Can safely assert non-null due to check above
245
+ };
246
+ await this.overridePasskeyStamper({ config: mergedConfig });
247
+ }
193
248
  if (!generatedPublicKey) {
194
249
  throw new TurnkeyError("A publickey could not be found or generated.", TurnkeyErrorCodes.INTERNAL_ERROR);
195
250
  }
@@ -202,7 +257,6 @@ class TurnkeyClient {
202
257
  sessionToken: sessionResponse.session,
203
258
  sessionKey,
204
259
  });
205
- generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
206
260
  return {
207
261
  sessionToken: sessionResponse.session,
208
262
  // TODO: can we return the credentialId here?
@@ -219,15 +273,23 @@ class TurnkeyClient {
219
273
  code: TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED,
220
274
  },
221
275
  },
276
+ catchFn: async () => {
277
+ try {
278
+ await this.deleteApiKeyPair({ publicKey: generatedPublicKey });
279
+ }
280
+ catch (cleanupError) {
281
+ throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
282
+ }
283
+ },
222
284
  }, {
223
285
  finallyFn: async () => {
224
- if (generatedPublicKey) {
225
- try {
226
- await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
227
- }
228
- catch (cleanupError) {
229
- throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
230
- }
286
+ if (shouldOverrideConfig) {
287
+ // we restore previous stamper after login attempt
288
+ await this.overridePasskeyStamper({
289
+ // we can assert this safely, since if this statement runs we are certian the passkey stamper
290
+ // is initialized
291
+ config: currentConfig,
292
+ });
231
293
  }
232
294
  },
233
295
  });
@@ -246,17 +308,18 @@ class TurnkeyClient {
246
308
  * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
247
309
  * @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata).
248
310
  * @param params.sessionKey - session key to use for storing the session (defaults to the default session key).
249
- * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
250
311
  * @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes:
251
312
  * - `sessionToken`: the signed JWT session token.
252
313
  * - `credentialId`: the credential ID associated with the passkey created.
253
314
  * @throws {TurnkeyError} If there is an error during passkey creation, sub-organization creation, or session storage.
254
315
  */
255
316
  this.signUpWithPasskey = async (params) => {
256
- const { passkeyDisplayName, challenge, expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, organizationId, } = params || {};
257
- let generatedPublicKey = undefined;
317
+ const { passkeyDisplayName, challenge, expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, } = params || {};
318
+ const generatedPublicKey = await this.createApiKeyPair();
258
319
  return withTurnkeyErrorHandling(async () => {
259
- generatedPublicKey = await this.apiKeyStamper?.createKeyPair();
320
+ if (!this.passkeyStamper) {
321
+ throw new TurnkeyError("Passkey stamper is not initialized", TurnkeyErrorCodes.INTERNAL_ERROR);
322
+ }
260
323
  const passkeyName = passkeyDisplayName || `passkey-${Date.now()}`;
261
324
  // A passkey will be created automatically when you call this function. The name is passed in
262
325
  const passkey = await this.createPasskey({
@@ -288,25 +351,23 @@ class TurnkeyClient {
288
351
  ],
289
352
  },
290
353
  });
291
- const res = await this.httpClient.proxySignup(signUpBody);
354
+ const res = await this.httpClient.proxySignupV2(signUpBody);
292
355
  if (!res) {
293
356
  throw new TurnkeyError(`Sign up failed`, TurnkeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR);
294
357
  }
295
- const newGeneratedKeyPair = await this.apiKeyStamper?.createKeyPair();
296
- this.apiKeyStamper?.setTemporaryPublicKey(generatedPublicKey);
358
+ const newGeneratedKeyPair = await this.createApiKeyPair();
359
+ await this.overrideApiKeyStamper({
360
+ temporaryPublicKey: generatedPublicKey,
361
+ });
297
362
  const sessionResponse = await this.httpClient.stampLogin({
298
363
  publicKey: newGeneratedKeyPair,
299
- organizationId: organizationId ?? this.config.organizationId,
364
+ organizationId: res.organizationId,
300
365
  expirationSeconds,
301
366
  });
302
- await Promise.all([
303
- this.apiKeyStamper?.deleteKeyPair(generatedPublicKey),
304
- this.storeSession({
305
- sessionToken: sessionResponse.session,
306
- sessionKey,
307
- }),
308
- ]);
309
- generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
367
+ await this.storeSession({
368
+ sessionToken: sessionResponse.session,
369
+ sessionKey,
370
+ });
310
371
  return {
311
372
  sessionToken: sessionResponse.session,
312
373
  appProofs: res.appProofs,
@@ -317,14 +378,12 @@ class TurnkeyClient {
317
378
  errorMessage: "Failed to sign up with passkey",
318
379
  }, {
319
380
  finallyFn: async () => {
320
- this.apiKeyStamper?.clearTemporaryPublicKey();
321
- if (generatedPublicKey) {
322
- try {
323
- await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
324
- }
325
- catch (cleanupError) {
326
- throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
327
- }
381
+ try {
382
+ await this.overrideApiKeyStamper({ temporaryPublicKey: "" });
383
+ await this.deleteApiKeyPair({ publicKey: generatedPublicKey });
384
+ }
385
+ catch (cleanupError) {
386
+ throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
328
387
  }
329
388
  },
330
389
  });
@@ -464,13 +523,11 @@ class TurnkeyClient {
464
523
  this.buildWalletLoginRequest = async (params) => {
465
524
  const { walletProvider, publicKey: providedPublicKey } = params;
466
525
  const expirationSeconds = params.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
467
- let generatedPublicKey = undefined;
526
+ const futureSessionPublicKey = providedPublicKey ?? (await this.createApiKeyPair());
468
527
  return withTurnkeyErrorHandling(async () => {
469
528
  if (!this.walletManager?.stamper) {
470
529
  throw new TurnkeyError("Wallet stamper is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
471
530
  }
472
- const futureSessionPublicKey = providedPublicKey ??
473
- (generatedPublicKey = await this.apiKeyStamper?.createKeyPair());
474
531
  if (!futureSessionPublicKey) {
475
532
  throw new TurnkeyError("Failed to find or generate a public key for building the wallet login request", TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR);
476
533
  }
@@ -512,9 +569,12 @@ class TurnkeyClient {
512
569
  errorCode: TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR,
513
570
  errorMessage: "Failed to build wallet login request",
514
571
  catchFn: async () => {
515
- if (generatedPublicKey) {
572
+ // we only clean up the generated key pair if we created it here
573
+ if (!providedPublicKey) {
516
574
  try {
517
- await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
575
+ await this.deleteApiKeyPair({
576
+ publicKey: futureSessionPublicKey,
577
+ });
518
578
  }
519
579
  catch (cleanupError) {
520
580
  throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
@@ -599,7 +659,7 @@ class TurnkeyClient {
599
659
  ],
600
660
  },
601
661
  });
602
- const res = await this.httpClient.proxySignup(signUpBody);
662
+ const res = await this.httpClient.proxySignupV2(signUpBody);
603
663
  if (!res) {
604
664
  throw new TurnkeyError(`Sign up failed`, TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
605
665
  }
@@ -674,7 +734,7 @@ class TurnkeyClient {
674
734
  ],
675
735
  },
676
736
  });
677
- signupRes = await this.httpClient.proxySignup(signUpBody);
737
+ signupRes = await this.httpClient.proxySignupV2(signUpBody);
678
738
  if (!signupRes) {
679
739
  throw new TurnkeyError(`Sign up failed`, TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
680
740
  }
@@ -706,21 +766,29 @@ class TurnkeyClient {
706
766
  *
707
767
  * - 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.
708
768
  * - Supports both email and SMS OTP types.
709
- * - Returns an OTP ID that is required for subsequent OTP verification.
769
+ * - Returns an OTP ID and an encryption target bundle required for subsequent OTP verification.
770
+ * - The `otpEncryptionTargetBundle` must be used to encrypt the OTP code and a client-generated public key before calling `verifyOtp`.
710
771
  *
711
772
  * @param params.otpType - type of OTP to initialize (OtpType.Email or OtpType.Sms).
712
773
  * @param params.contact - contact information for the user (e.g., email address or phone number).
713
774
  * @param params.organizationId - optional organization ID to target (defaults to the session's organization ID or the parent organization ID).
714
- * @returns A promise that resolves to the OTP ID required for verification.
775
+ * @returns A promise that resolves to an {@link InitOtpResult}, which includes:
776
+ * - `otpId`: the ID of the initiated OTP.
777
+ * - `otpEncryptionTargetBundle`: the signed encryption target bundle for encrypting the OTP code.
715
778
  * @throws {TurnkeyError} If there is an error during the OTP initialization process or if the maximum number of OTPs has been reached.
716
779
  */
717
780
  this.initOtp = async (params) => {
718
781
  return withTurnkeyErrorHandling(async () => {
719
- const initOtpRes = await this.httpClient.proxyInitOtp(params);
720
- if (!initOtpRes || !initOtpRes.otpId) {
721
- throw new TurnkeyError("Failed to initialize OTP: otpId is missing", TurnkeyErrorCodes.INIT_OTP_ERROR);
782
+ const initOtpRes = await this.httpClient.proxyInitOtpV2(params);
783
+ if (!initOtpRes ||
784
+ !initOtpRes.otpId ||
785
+ !initOtpRes.otpEncryptionTargetBundle) {
786
+ throw new TurnkeyError("Failed to initialize OTP: otpId or otpEncryptionTargetBundle is missing", TurnkeyErrorCodes.INIT_OTP_ERROR);
722
787
  }
723
- return initOtpRes.otpId;
788
+ return {
789
+ otpId: initOtpRes.otpId,
790
+ otpEncryptionTargetBundle: initOtpRes.otpEncryptionTargetBundle,
791
+ };
724
792
  }, {
725
793
  errorMessage: "Failed to initialize OTP",
726
794
  errorCode: TurnkeyErrorCodes.INIT_OTP_ERROR,
@@ -735,45 +803,38 @@ class TurnkeyClient {
735
803
  /**
736
804
  * Verifies the OTP code sent to the user.
737
805
  *
738
- * - 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.
739
- * - If verification is successful, it returns the sub-organization ID associated with the contact (if it exists) and a verification token.
806
+ * - 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.
807
+ * - If verification is successful, it returns a verification token bound to the public key.
740
808
  * - The verification token can be used for subsequent login or sign-up flows.
741
- * - Handles both email and SMS OTP types.
742
809
  *
743
810
  * @param params.otpId - ID of the OTP to verify (returned from `initOtp`).
744
- * @param params.otpCode - OTP code entered by the user.
745
- * @param params.contact - contact information for the user (e.g., email address or phone number).
746
- * @param params.otpType - type of OTP being verified (OtpType.Email or OtpType.Sms).
747
- * @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.
811
+ * @param params.otpCode - the OTP code entered by the user.
812
+ * @param params.otpEncryptionTargetBundle - the encryption target bundle returned from `initOtp`.
813
+ * @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`.
748
814
  * @returns A promise that resolves to an object containing:
749
- * - subOrganizationId: sub-organization ID if the contact is already associated with a sub-organization, or an empty string if not.
750
815
  * - verificationToken: verification token to be used for login or sign-up.
816
+ * - publicKey: the public key bound to the verification token (either the one provided or the auto-generated one).
751
817
  * @throws {TurnkeyError} If there is an error during the OTP verification process, such as an invalid code or network failure.
752
818
  */
753
819
  this.verifyOtp = async (params) => {
754
- const { otpId, otpCode, contact, otpType } = params;
755
- const resolvedPublicKey = params.publicKey ?? (await this.apiKeyStamper?.createKeyPair());
820
+ const { otpId, otpCode, otpEncryptionTargetBundle } = params;
821
+ // we track auto-generated keys so we can clean them up on errors
822
+ const publicKey = params.publicKey ?? (await this.createApiKeyPair());
823
+ if (!publicKey) {
824
+ throw new TurnkeyError("No public key available. Either pass a publicKey or ensure apiKeyStamper is configured.", TurnkeyErrorCodes.INVALID_REQUEST);
825
+ }
756
826
  return withTurnkeyErrorHandling(async () => {
757
- const verifyOtpRes = await this.httpClient.proxyVerifyOtp({
827
+ const encryptedOtpBundle = await encryptOtpCodeToBundle(otpCode, otpEncryptionTargetBundle, publicKey);
828
+ const verifyOtpRes = await this.httpClient.proxyVerifyOtpV2({
758
829
  otpId: otpId,
759
- otpCode: otpCode,
760
- publicKey: resolvedPublicKey,
830
+ encryptedOtpBundle: encryptedOtpBundle,
761
831
  });
762
832
  if (!verifyOtpRes) {
763
833
  throw new TurnkeyError(`OTP verification failed`, TurnkeyErrorCodes.INTERNAL_ERROR);
764
834
  }
765
- const accountRes = await this.httpClient.proxyGetAccount({
766
- filterType: OtpTypeToFilterTypeMap[otpType],
767
- filterValue: contact,
768
- verificationToken: verifyOtpRes.verificationToken,
769
- });
770
- if (!accountRes) {
771
- throw new TurnkeyError(`Account fetch failed`, TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
772
- }
773
- const subOrganizationId = accountRes.organizationId;
774
835
  return {
775
- subOrganizationId: subOrganizationId,
776
836
  verificationToken: verifyOtpRes.verificationToken,
837
+ publicKey,
777
838
  };
778
839
  }, {
779
840
  errorMessage: "Failed to verify OTP",
@@ -784,19 +845,29 @@ class TurnkeyClient {
784
845
  code: TurnkeyErrorCodes.INVALID_OTP_CODE,
785
846
  },
786
847
  },
848
+ catchFn: async () => {
849
+ // we only clean up the generated key pair if we created it here
850
+ if (!params.publicKey) {
851
+ try {
852
+ await this.deleteApiKeyPair({ publicKey });
853
+ }
854
+ catch (cleanupError) {
855
+ throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
856
+ }
857
+ }
858
+ },
787
859
  });
788
860
  };
789
861
  /**
790
862
  * Logs in a user using an OTP verification token.
791
863
  *
792
864
  * - This function logs in a user using the verification token received after OTP verification (from email or SMS).
793
- * - If a public key is not provided, a new API key pair will be generated for authentication.
865
+ * - The client signature is always produced using the private key bound to the verification token during `verifyOtp`.
866
+ * - The verification token's embedded key becomes the session public key.
794
867
  * - Optionally invalidates any existing sessions for the user if `invalidateExisting` is set to true.
795
868
  * - Stores the resulting session token under the specified session key, or the default session key if not provided.
796
- * - Handles cleanup of unused key pairs if login fails.
797
869
  *
798
870
  * @param params.verificationToken - verification token received after OTP verification.
799
- * @param params.publicKey - public key to use for authentication. If not provided, a new key pair will be generated.
800
871
  * @param params.organizationId - optional organization ID to target (defaults to the verified subOrg ID linked to the verification token contact).
801
872
  * @param params.invalidateExisting - flag to invalidate existing session for the user.
802
873
  * @param params.sessionKey - session key to use for session creation (defaults to the default session key).
@@ -805,34 +876,35 @@ class TurnkeyClient {
805
876
  * @throws {TurnkeyError} If there is an error during the OTP login process or if key pair cleanup fails.
806
877
  */
807
878
  this.loginWithOtp = async (params) => {
808
- const { verificationToken, invalidateExisting = false, publicKey = await this.apiKeyStamper?.createKeyPair(), organizationId, sessionKey = SessionKey.DefaultSessionkey, } = params;
879
+ const { verificationToken, invalidateExisting = false, organizationId, sessionKey = SessionKey.DefaultSessionkey, } = params;
809
880
  return withTurnkeyErrorHandling(async () => {
810
- const { message, publicKey: clientSignaturePublicKey } = getClientSignatureMessageForLogin({
811
- verificationToken,
812
- sessionPublicKey: publicKey,
881
+ const { message, publicKey: verificationPublicKey } = getClientSignatureMessageForLogin({ verificationToken });
882
+ // we sign with the verification token key. This is the key bound during
883
+ // verifyOtp() and is what Turnkey expects to sign the client signature for login
884
+ const signature = await this.signWithApiKey({
885
+ message,
886
+ publicKey: verificationPublicKey,
813
887
  });
814
- this.apiKeyStamper?.setTemporaryPublicKey(publicKey);
815
- const signature = await this.apiKeyStamper?.sign(message, SignatureFormat.Raw);
816
888
  if (!signature) {
817
889
  throw new TurnkeyError(`Failed to sign client signature for OTP login`, TurnkeyErrorCodes.INTERNAL_ERROR);
818
890
  }
819
891
  const clientSignature = {
820
892
  message: message,
821
- publicKey: clientSignaturePublicKey,
893
+ publicKey: verificationPublicKey,
822
894
  scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
823
895
  signature: signature,
824
896
  };
825
- const res = await this.httpClient.proxyOtpLogin({
897
+ // the verification token's public key is also used as the session public key for login
898
+ const loginRes = await this.httpClient.proxyOtpLoginV2({
826
899
  verificationToken,
827
- publicKey: publicKey,
828
- invalidateExisting,
900
+ publicKey: verificationPublicKey,
829
901
  clientSignature,
902
+ invalidateExisting,
830
903
  ...(organizationId && { organizationId }),
831
904
  });
832
- if (!res) {
905
+ if (!loginRes) {
833
906
  throw new TurnkeyError(`Auth proxy OTP login failed`, TurnkeyErrorCodes.OTP_LOGIN_ERROR);
834
907
  }
835
- const loginRes = await res;
836
908
  if (!loginRes.session) {
837
909
  throw new TurnkeyError("No session returned from OTP login", TurnkeyErrorCodes.OTP_LOGIN_ERROR);
838
910
  }
@@ -846,21 +918,6 @@ class TurnkeyClient {
846
918
  }, {
847
919
  errorMessage: "Failed to log in with OTP",
848
920
  errorCode: TurnkeyErrorCodes.OTP_LOGIN_ERROR,
849
- catchFn: async () => {
850
- // Clean up the generated key pair if it wasn't successfully used
851
- if (publicKey) {
852
- try {
853
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
854
- }
855
- catch (cleanupError) {
856
- throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
857
- }
858
- }
859
- },
860
- }, {
861
- finallyFn: async () => {
862
- this.apiKeyStamper?.clearTemporaryPublicKey();
863
- },
864
921
  });
865
922
  };
866
923
  /**
@@ -868,7 +925,7 @@ class TurnkeyClient {
868
925
  *
869
926
  * - This function signs up a user using the verification token received after OTP verification (from email or SMS).
870
927
  * - Creates a new sub-organization for the user with the provided parameters and associates the contact (email or phone) with the sub-organization.
871
- * - Automatically generates a new API key pair for authentication and session management.
928
+ * - The verification token's embedded key becomes the session public key.
872
929
  * - Stores the resulting session token under the specified session key, or the default session key if not provided.
873
930
  * - Handles both email and SMS OTP types, and supports additional sub-organization creation parameters.
874
931
  *
@@ -883,7 +940,7 @@ class TurnkeyClient {
883
940
  * @throws {TurnkeyError} If there is an error during the OTP sign-up process or session storage.
884
941
  */
885
942
  this.signUpWithOtp = async (params) => {
886
- const { verificationToken, contact, otpType, createSubOrgParams, invalidateExisting, sessionKey, publicKey = await this.apiKeyStamper?.createKeyPair(), } = params;
943
+ const { verificationToken, contact, otpType, createSubOrgParams, invalidateExisting, sessionKey, } = params;
887
944
  // build sign up body without client signature first
888
945
  const signUpBody = buildSignUpBody({
889
946
  createSubOrgParams: {
@@ -895,7 +952,7 @@ class TurnkeyClient {
895
952
  },
896
953
  });
897
954
  return withTurnkeyErrorHandling(async () => {
898
- const { message, publicKey: clientSignaturePublicKey } = getClientSignatureMessageForSignup({
955
+ const { message, publicKey: verificationPublicKey } = getClientSignatureMessageForSignup({
899
956
  verificationToken,
900
957
  ...(signUpBody.userEmail && { email: signUpBody.userEmail }),
901
958
  ...(signUpBody.userPhoneNumber && {
@@ -905,18 +962,22 @@ class TurnkeyClient {
905
962
  authenticators: signUpBody.authenticators,
906
963
  oauthProviders: signUpBody.oauthProviders,
907
964
  });
908
- this.apiKeyStamper?.setTemporaryPublicKey(publicKey);
909
- const signature = await this.apiKeyStamper?.sign(message, SignatureFormat.Raw);
965
+ // we sign with the verification token key. This is the key bound during
966
+ // verifyOtp() and is what Turnkey expects to sign the client signature for signup
967
+ const signature = await this.signWithApiKey({
968
+ message,
969
+ publicKey: verificationPublicKey,
970
+ });
910
971
  if (!signature) {
911
972
  throw new TurnkeyError(`Failed to sign client signature for OTP sign up`, TurnkeyErrorCodes.INTERNAL_ERROR);
912
973
  }
913
974
  const clientSignature = {
914
975
  message: message,
915
- publicKey: clientSignaturePublicKey,
976
+ publicKey: verificationPublicKey,
916
977
  scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
917
978
  signature: signature,
918
979
  };
919
- const signupRes = await this.httpClient.proxySignup({
980
+ const signupRes = await this.httpClient.proxySignupV2({
920
981
  ...signUpBody,
921
982
  clientSignature,
922
983
  });
@@ -925,7 +986,6 @@ class TurnkeyClient {
925
986
  }
926
987
  const otpRes = await this.loginWithOtp({
927
988
  verificationToken,
928
- publicKey: publicKey,
929
989
  ...(invalidateExisting && { invalidateExisting }),
930
990
  ...(sessionKey && { sessionKey }),
931
991
  });
@@ -934,39 +994,26 @@ class TurnkeyClient {
934
994
  appProofs: signupRes.appProofs,
935
995
  };
936
996
  }, {
937
- catchFn: async () => {
938
- // Clean up the generated key pair if it wasn't successfully used
939
- if (publicKey) {
940
- try {
941
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
942
- }
943
- catch (cleanupError) {
944
- throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
945
- }
946
- }
947
- },
948
997
  errorCode: TurnkeyErrorCodes.OTP_SIGNUP_ERROR,
949
998
  errorMessage: "Failed to sign up with OTP",
950
- }, {
951
- finallyFn: async () => {
952
- this.apiKeyStamper?.clearTemporaryPublicKey();
953
- },
954
999
  });
955
1000
  };
956
1001
  /**
957
- * Completes the OTP authentication flow by verifying the OTP code and then either signing up or logging in the user.
1002
+ * Completes the OTP authentication flow by verifying the encrypted OTP bundle and then either signing up or logging in the user.
958
1003
  *
959
- * - This function first verifies the OTP code for the provided contact and OTP type.
1004
+ * - 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.
960
1005
  * - 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.
961
1006
  * - If the contact is already associated with a sub-organization, it will complete the login flow.
962
- * - Supports passing a custom public key for authentication, invalidating existing session, specifying a session key, and providing additional sub-organization creation parameters.
1007
+ * - The key bound to the verification token during `verifyOtp` is always reused as the session public key.
1008
+ * - 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.
963
1009
  * - Handles both email and SMS OTP types.
964
1010
  *
965
1011
  * @param params.otpId - ID of the OTP to complete (returned from `initOtp`).
966
- * @param params.otpCode - OTP code entered by the user.
1012
+ * @param params.otpCode - the OTP code entered by the user.
1013
+ * @param params.otpEncryptionTargetBundle - the encryption target bundle returned from `initOtp`.
967
1014
  * @param params.contact - contact information for the user (e.g., email address or phone number).
968
1015
  * @param params.otpType - type of OTP being completed (OtpType.Email or OtpType.Sms).
969
- * @param params.publicKey - public key to use for authentication. If not provided, a new key pair may be generated.
1016
+ * @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.
970
1017
  * @param params.invalidateExisting - flag to invalidate existing sessions for the user.
971
1018
  * @param params.sessionKey - session key to use for session creation (defaults to the default session key).
972
1019
  * @param params.createSubOrgParams - parameters for sub-organization creation (e.g., authenticators, user metadata).
@@ -977,18 +1024,30 @@ class TurnkeyClient {
977
1024
  * @throws {TurnkeyError} If there is an error during OTP verification, sign-up, or login.
978
1025
  */
979
1026
  this.completeOtp = async (params) => {
980
- const { otpId, otpCode, contact, otpType, publicKey = await this.apiKeyStamper?.createKeyPair(), invalidateExisting = false, sessionKey, createSubOrgParams, } = params;
1027
+ const { otpId, otpCode, otpEncryptionTargetBundle, contact, otpType, invalidateExisting = false, sessionKey, createSubOrgParams, } = params;
1028
+ const publicKey = params.publicKey ?? (await this.createApiKeyPair());
1029
+ if (!publicKey) {
1030
+ throw new TurnkeyError("No public key available. Either pass a publicKey or ensure apiKeyStamper is configured.", TurnkeyErrorCodes.INVALID_REQUEST);
1031
+ }
981
1032
  return withTurnkeyErrorHandling(async () => {
982
- const { subOrganizationId, verificationToken } = await this.verifyOtp({
983
- otpId: otpId,
984
- otpCode: otpCode,
985
- contact: contact,
986
- otpType: otpType,
987
- publicKey: publicKey,
1033
+ const { verificationToken } = await this.verifyOtp({
1034
+ otpId,
1035
+ otpCode,
1036
+ otpEncryptionTargetBundle,
1037
+ publicKey,
988
1038
  });
989
1039
  if (!verificationToken) {
990
1040
  throw new TurnkeyError("No verification token returned from OTP verification", TurnkeyErrorCodes.VERIFY_OTP_ERROR);
991
1041
  }
1042
+ const accountRes = await this.httpClient.proxyGetAccount({
1043
+ filterType: OtpTypeToFilterTypeMap[otpType],
1044
+ filterValue: contact,
1045
+ verificationToken: verificationToken,
1046
+ });
1047
+ if (!accountRes) {
1048
+ throw new TurnkeyError(`Account fetch failed`, TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
1049
+ }
1050
+ const subOrganizationId = accountRes.organizationId;
992
1051
  if (!subOrganizationId) {
993
1052
  const signUpRes = await this.signUpWithOtp({
994
1053
  verificationToken,
@@ -997,7 +1056,6 @@ class TurnkeyClient {
997
1056
  ...(createSubOrgParams && { createSubOrgParams }),
998
1057
  ...(invalidateExisting && { invalidateExisting }),
999
1058
  ...(sessionKey && { sessionKey }),
1000
- publicKey: publicKey,
1001
1059
  });
1002
1060
  return {
1003
1061
  ...signUpRes,
@@ -1008,7 +1066,6 @@ class TurnkeyClient {
1008
1066
  else {
1009
1067
  const loginRes = await this.loginWithOtp({
1010
1068
  verificationToken,
1011
- publicKey: publicKey,
1012
1069
  ...(invalidateExisting && { invalidateExisting }),
1013
1070
  ...(sessionKey && { sessionKey }),
1014
1071
  });
@@ -1021,6 +1078,16 @@ class TurnkeyClient {
1021
1078
  }, {
1022
1079
  errorMessage: "Failed to complete OTP process",
1023
1080
  errorCode: TurnkeyErrorCodes.OTP_COMPLETION_ERROR,
1081
+ catchFn: async () => {
1082
+ if (!params.publicKey) {
1083
+ try {
1084
+ await this.deleteApiKeyPair({ publicKey });
1085
+ }
1086
+ catch (cleanupError) {
1087
+ throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
1088
+ }
1089
+ }
1090
+ },
1024
1091
  });
1025
1092
  };
1026
1093
  /**
@@ -1145,7 +1212,7 @@ class TurnkeyClient {
1145
1212
  // Clean up the generated key pair if it wasn't successfully used
1146
1213
  if (publicKey) {
1147
1214
  try {
1148
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
1215
+ await this.deleteApiKeyPair({ publicKey });
1149
1216
  }
1150
1217
  catch (cleanupError) {
1151
1218
  throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
@@ -1182,10 +1249,11 @@ class TurnkeyClient {
1182
1249
  providerName,
1183
1250
  oidcToken,
1184
1251
  },
1252
+ ...(createSubOrgParams?.oauthProviders ?? []),
1185
1253
  ],
1186
1254
  },
1187
1255
  });
1188
- const signupRes = await this.httpClient.proxySignup(signUpBody);
1256
+ const signupRes = await this.httpClient.proxySignupV2(signUpBody);
1189
1257
  if (!signupRes) {
1190
1258
  throw new TurnkeyError(`Auth proxy OAuth signup failed`, TurnkeyErrorCodes.OAUTH_SIGNUP_ERROR);
1191
1259
  }
@@ -2347,37 +2415,71 @@ class TurnkeyClient {
2347
2415
  });
2348
2416
  };
2349
2417
  /**
2350
- * Adds an OAuth provider to the user.
2418
+ * Adds one or more OAuth provider audiences to the user.
2351
2419
  *
2352
- * - This function adds an OAuth provider (e.g., Google, Apple) to the user account.
2353
- * - If a userId is provided, it adds the provider for that specific user; otherwise, it uses the current session's userId.
2354
- * - Automatically checks if an account already exists for the provided OIDC token and prevents duplicate associations.
2355
- * - 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.
2356
- * - Handles session management and error reporting for the add provider flow.
2420
+ * - Accepts either an `oidcToken`, a list of `oidcClaims`, or both. At least one must be provided.
2421
+ * - The issuer (`iss`) of the `oidcToken` and all `oidcClaims` must match, ensuring they belong to the same OAuth provider.
2422
+ * - 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.
2423
+ * - 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.
2424
+ * - Submits a single `createOauthProviders` request containing the token (if any) and all additional claims.
2357
2425
  * - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
2358
2426
  *
2359
2427
  * @param params.providerName - name of the OAuth provider to add (e.g., "Google", "Apple").
2360
- * @param params.oidcToken - OIDC token for the OAuth provider.
2428
+ * @param params.oidcToken - OIDC token for the OAuth provider. Either `oidcToken` or `oidcClaims` (or both) must be provided.
2429
+ * @param params.oidcClaims - additional `iss`/`sub`/`aud` claims to register as additional audiences for the same identity.
2361
2430
  * @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
2362
2431
  * @param params.userId - user ID to add the provider for a specific user (defaults to current session's userId).
2363
2432
  * @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
2364
2433
  * @returns A promise that resolves to an array of provider IDs associated with the user.
2365
- * @throws {TurnkeyError} If there is no active session, if the account already exists, or if there is an error adding the OAuth provider.
2434
+ * @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.
2366
2435
  */
2367
2436
  this.addOauthProvider = async (params) => {
2368
- const { providerName, oidcToken, stampWith = this.config.defaultStamperType, } = params;
2437
+ const { providerName, oidcToken, oidcClaims = [], stampWith = this.config.defaultStamperType, } = params;
2438
+ if (!oidcToken && oidcClaims.length === 0) {
2439
+ throw new TurnkeyError("At least one of `oidcToken` or `oidcClaims` must be provided to add an OAuth provider", TurnkeyErrorCodes.INVALID_REQUEST);
2440
+ }
2369
2441
  const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
2370
2442
  return withTurnkeyErrorHandling(async () => {
2371
- const accountRes = await this.httpClient.proxyGetAccount({
2372
- filterType: "OIDC_TOKEN",
2373
- filterValue: oidcToken,
2374
- });
2375
- if (!accountRes) {
2376
- throw new TurnkeyError(`Account fetch failed`, TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
2377
- }
2378
- if (accountRes.organizationId &&
2379
- accountRes.organizationId !== session?.organizationId) {
2380
- throw new TurnkeyError("Account already exists with this OIDC token", TurnkeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
2443
+ // Parse the oidc token so we can get the email/iss. The email is used to update the user's email for social linking.
2444
+ // The iss is used to ensure all passed-in claims share the same issuer.
2445
+ const { email: oidcEmail, iss: tokenIss } = oidcToken
2446
+ ? jwtDecode(oidcToken) || {}
2447
+ : { email: undefined, iss: undefined };
2448
+ // Verify that all passed-in claims share the same issuer (and match the token's issuer if a token was provided).
2449
+ const issuers = new Set();
2450
+ if (tokenIss)
2451
+ issuers.add(tokenIss);
2452
+ for (const claim of oidcClaims) {
2453
+ issuers.add(claim.iss);
2454
+ }
2455
+ if (issuers.size > 1) {
2456
+ throw new TurnkeyError("All `oidcToken` and `oidcClaims` entries must share the same issuer (`iss`)", TurnkeyErrorCodes.INVALID_REQUEST);
2457
+ }
2458
+ // Check that none of the audiences are already associated with a different sub-organization.
2459
+ const accountChecks = [];
2460
+ if (oidcToken) {
2461
+ // Look up the sub-organization associated with passed in oidc token
2462
+ accountChecks.push(this.httpClient.proxyGetAccount({
2463
+ filterType: FilterType.OidcToken,
2464
+ filterValue: oidcToken,
2465
+ }));
2466
+ }
2467
+ // Also look up the sub-organization for each oidcClaim
2468
+ for (const claim of oidcClaims) {
2469
+ accountChecks.push(this.httpClient.proxyGetAccount({
2470
+ filterType: FilterType.OidcClaims,
2471
+ filterValue: JSON.stringify(claim),
2472
+ }));
2473
+ }
2474
+ const accountResults = await Promise.all(accountChecks);
2475
+ for (const accountRes of accountResults) {
2476
+ if (!accountRes) {
2477
+ throw new TurnkeyError(`Account fetch failed`, TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR);
2478
+ }
2479
+ if (accountRes.organizationId &&
2480
+ accountRes.organizationId !== session?.organizationId) {
2481
+ throw new TurnkeyError("Account already exists with this OIDC token or claims", TurnkeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
2482
+ }
2381
2483
  }
2382
2484
  const userId = params?.userId || session?.userId;
2383
2485
  if (!userId) {
@@ -2387,9 +2489,7 @@ class TurnkeyClient {
2387
2489
  if (!organizationId) {
2388
2490
  throw new TurnkeyError("Organization ID is required to add OAuth provider", TurnkeyErrorCodes.INVALID_REQUEST);
2389
2491
  }
2390
- // parse the oidc token so we can get the email. Pass it in to updateUser then call createOauthProviders. This will be verified by Turnkey.
2391
- const { email: oidcEmail, iss } = jwtDecode(oidcToken) || {};
2392
- if (iss === googleISS) {
2492
+ if (oidcToken && tokenIss === googleISS) {
2393
2493
  const verifiedSuborg = await this.httpClient.proxyGetAccount({
2394
2494
  filterType: "EMAIL",
2395
2495
  filterValue: oidcEmail,
@@ -2422,14 +2522,13 @@ class TurnkeyClient {
2422
2522
  }, stampWith);
2423
2523
  }
2424
2524
  }
2525
+ const oauthProviders = [
2526
+ ...(oidcToken ? [{ providerName, oidcToken }] : []),
2527
+ ...oidcClaims.map((claim) => ({ providerName, oidcClaims: claim })),
2528
+ ];
2425
2529
  const createProviderRes = await this.httpClient.createOauthProviders({
2426
2530
  userId,
2427
- oauthProviders: [
2428
- {
2429
- providerName,
2430
- oidcToken,
2431
- },
2432
- ],
2531
+ oauthProviders,
2433
2532
  }, stampWith);
2434
2533
  if (!createProviderRes) {
2435
2534
  throw new TurnkeyError("Failed to create OAuth provider", TurnkeyErrorCodes.ADD_OAUTH_PROVIDER_ERROR);
@@ -2973,7 +3072,7 @@ class TurnkeyClient {
2973
3072
  const session = await this.storageManager.getSession(sessionKey);
2974
3073
  if (session) {
2975
3074
  await Promise.all([
2976
- this.apiKeyStamper?.deleteKeyPair(session.publicKey),
3075
+ this.deleteApiKeyPair({ publicKey: session.publicKey }),
2977
3076
  this.storageManager.clearSession(sessionKey),
2978
3077
  ]);
2979
3078
  }
@@ -3041,9 +3140,8 @@ class TurnkeyClient {
3041
3140
  if (!this.httpClient) {
3042
3141
  throw new TurnkeyError("HTTP client is not initialized. Please initialize the client before refreshing the session.", TurnkeyErrorCodes.CLIENT_NOT_INITIALIZED);
3043
3142
  }
3044
- let keyPair;
3143
+ const keyPair = publicKey ?? (await this.createApiKeyPair());
3045
3144
  return withTurnkeyErrorHandling(async () => {
3046
- keyPair = publicKey ?? (await this.apiKeyStamper?.createKeyPair());
3047
3145
  if (!keyPair) {
3048
3146
  throw new TurnkeyError("Failed to create new key pair.", TurnkeyErrorCodes.INTERNAL_ERROR);
3049
3147
  }
@@ -3063,6 +3161,17 @@ class TurnkeyClient {
3063
3161
  }, {
3064
3162
  errorMessage: "Failed to refresh session",
3065
3163
  errorCode: TurnkeyErrorCodes.REFRESH_SESSION_ERROR,
3164
+ catchFn: async () => {
3165
+ // we only delete the keypair if we generated it here
3166
+ if (!publicKey) {
3167
+ try {
3168
+ await this.deleteApiKeyPair({ publicKey: keyPair });
3169
+ }
3170
+ catch (cleanupError) {
3171
+ throw new TurnkeyError("Failed to clean up generated key pair", TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
3172
+ }
3173
+ }
3174
+ },
3066
3175
  });
3067
3176
  };
3068
3177
  /**
@@ -3183,7 +3292,7 @@ class TurnkeyClient {
3183
3292
  for (const publicKey of publicKeys) {
3184
3293
  if (!sessionTokensMap[publicKey]) {
3185
3294
  try {
3186
- await this.apiKeyStamper?.deleteKeyPair(publicKey);
3295
+ await this.deleteApiKeyPair({ publicKey });
3187
3296
  }
3188
3297
  catch (error) {
3189
3298
  throw new TurnkeyError(`Failed to delete unused key pair ${publicKey}`, TurnkeyErrorCodes.INTERNAL_ERROR, error);
@@ -3198,34 +3307,84 @@ class TurnkeyClient {
3198
3307
  /**
3199
3308
  * Creates a new API key pair and returns the public key.
3200
3309
  *
3201
- * - This function generates a new API key pair and stores it in the underlying key store (IndexedDB).
3310
+ * - This function generates a new API key pair and stores it in the underlying key store.
3202
3311
  * - If an external key pair is provided, it will use that key pair for creation instead of generating a new one.
3203
- * - 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.
3204
3312
  * - Ensures the API key stamper is initialized before proceeding.
3205
3313
  * - Handles both native CryptoKeyPair objects and raw key material.
3206
3314
  *
3207
3315
  * @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.
3208
- * @param params.storeOverride - If true, sets the generated or provided public key as the override key in the API key stamper (defaults to false).
3209
3316
  * @return A promise that resolves to the public key of the created or provided API key pair as a string.
3210
3317
  * @throws {TurnkeyError} If the API key stamper is not initialized or if there is an error during key pair creation or storage.
3211
3318
  */
3212
3319
  this.createApiKeyPair = async (params) => {
3213
3320
  return withTurnkeyErrorHandling(async () => {
3214
3321
  const externalKeyPair = params?.externalKeyPair;
3215
- const storeOverride = params?.storeOverride ?? false;
3216
3322
  if (!this.apiKeyStamper) {
3217
3323
  throw new TurnkeyError("API Key Stamper is not initialized.", TurnkeyErrorCodes.INTERNAL_ERROR);
3218
3324
  }
3219
3325
  const publicKey = await this.apiKeyStamper.createKeyPair(externalKeyPair ? externalKeyPair : undefined);
3220
- if (storeOverride && publicKey) {
3221
- this.apiKeyStamper.setTemporaryPublicKey(publicKey);
3222
- }
3223
3326
  return publicKey;
3224
3327
  }, {
3225
3328
  errorMessage: "Failed to create API key pair",
3226
3329
  errorCode: TurnkeyErrorCodes.CREATE_API_KEY_PAIR_ERROR,
3227
3330
  });
3228
3331
  };
3332
+ /**
3333
+ * Deletes an API key pair from persistent storage.
3334
+ *
3335
+ * - This function removes a stored API key pair from the underlying key store (IndexedDB) based on its public key.
3336
+ * - Ensures that the API key stamper is initialized before proceeding with deletion.
3337
+ * - 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.
3338
+ * - Should NOT be used to delete key pairs that are associated with active sessions; use `clearSession` or `clearAllSessions` instead.
3339
+ *
3340
+ * @param params.publicKey - The public key of the API key pair to delete.
3341
+ * @returns A promise that resolves when the key pair is successfully deleted.
3342
+ * @throws {TurnkeyError} If the API key stamper is not initialized or if there is an error during key pair deletion.
3343
+ */
3344
+ this.deleteApiKeyPair = async (params) => {
3345
+ const { publicKey } = params;
3346
+ return withTurnkeyErrorHandling(async () => {
3347
+ if (!this.apiKeyStamper) {
3348
+ throw new TurnkeyError("API Key Stamper is not initialized.", TurnkeyErrorCodes.INTERNAL_ERROR);
3349
+ }
3350
+ await this.apiKeyStamper.deleteKeyPair(publicKey);
3351
+ }, {
3352
+ errorMessage: "Failed to delete API key pair",
3353
+ errorCode: TurnkeyErrorCodes.DELETE_API_KEY_PAIR_ERROR,
3354
+ });
3355
+ };
3356
+ /**
3357
+ * Signs a message using a key pair stored in the API key stamper.
3358
+ *
3359
+ * - This function signs the provided message string with the private key identified by `publicKey`.
3360
+ * - The message is SHA-256 hashed internally before signing (ECDSA P-256).
3361
+ * - Returns a compact hex signature (r || s) suitable for use as a client signature.
3362
+ * - The key pair must already exist in the key store (e.g., created via `createApiKeyPair`).
3363
+ *
3364
+ * @param params.message - the message string to sign.
3365
+ * @param params.publicKey - the public key identifying which key pair to sign with.
3366
+ * @returns A promise that resolves to the compact hex signature string.
3367
+ * @throws {TurnkeyError} If the API key stamper is not initialized or the key pair is not found.
3368
+ */
3369
+ this.signWithApiKey = async (params) => {
3370
+ const { message, publicKey } = params;
3371
+ let previousTemporaryKey;
3372
+ return withTurnkeyErrorHandling(async () => {
3373
+ if (!this.apiKeyStamper) {
3374
+ throw new TurnkeyError("API Key Stamper is not initialized.", TurnkeyErrorCodes.INTERNAL_ERROR);
3375
+ }
3376
+ previousTemporaryKey = this.apiKeyStamper.getTemporaryPublicKey();
3377
+ this.apiKeyStamper.setTemporaryPublicKey(publicKey);
3378
+ return await this.apiKeyStamper.sign(message, SignatureFormat.Raw);
3379
+ }, {
3380
+ errorMessage: "Failed to sign with API key",
3381
+ errorCode: TurnkeyErrorCodes.INTERNAL_ERROR,
3382
+ }, {
3383
+ finallyFn: async () => {
3384
+ this.apiKeyStamper?.setTemporaryPublicKey(previousTemporaryKey);
3385
+ },
3386
+ });
3387
+ };
3229
3388
  /**
3230
3389
  * Fetches the WalletKit proxy authentication configuration from the auth proxy.
3231
3390
  *