UNPKG

@turnkey/core

Version:

A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.

829 lines (827 loc) 159 kB
'use strict'; var sdkClientBase = require('../__generated__/sdk-client-base.js'); var sdkTypes = require('@turnkey/sdk-types'); var base = require('../__types__/base.js'); var utils = require('../utils.js'); var base$1 = require('../__storage__/base.js'); var base$2 = require('../__stampers__/api/base.js'); var base$3 = require('../__stampers__/passkey/base.js'); var turnkeyHelpers = require('../turnkey-helpers.js'); var jwtDecode = require('jwt-decode'); var base$4 = require('../__wallet__/base.js'); var ethers = require('ethers'); class TurnkeyClient { constructor(config, // Users can pass in their own stampers, or we will create them. Should we remove this? apiKeyStamper, passkeyStamper, walletManager) { /** * Creates a new passkey authenticator for the user. * * - This function generates a new passkey attestation and challenge, suitable for registration with the user's device. * - Handles both web and React Native environments, automatically selecting the appropriate passkey creation flow. * - The resulting attestation and challenge can be used to register the passkey with Turnkey. * * @param params.name - name of the passkey. If not provided, defaults to "A Passkey". * @param params.displayName - display name for the passkey. If not provided, defaults to "A Passkey". * @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet). * @returns A promise that resolves to an object containing: * - attestation: attestation object returned from the passkey creation process. * - encodedChallenge: encoded challenge string used for passkey registration. * @throws {TurnkeyError} If there is an error during passkey creation, or if the platform is unsupported. */ this.createPasskey = async (params) => { return utils.withTurnkeyErrorHandling(async () => { const name = params?.name || "A Passkey"; const displayName = params?.displayName || "A Passkey"; let passkey; if (utils.isWeb()) { const res = await this.passkeyStamper?.createWebPasskey({ publicKey: { user: { name, displayName, }, }, }); if (!res) { throw new sdkTypes.TurnkeyError("Failed to create React Native passkey", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR); } passkey = { encodedChallenge: res?.encodedChallenge, attestation: res?.attestation, }; } else if (utils.isReactNative()) { const res = await this.passkeyStamper?.createReactNativePasskey({ name, displayName, }); if (!res) { throw new sdkTypes.TurnkeyError("Failed to create React Native passkey", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR); } passkey = { encodedChallenge: res?.challenge, attestation: res?.attestation, }; } else { throw new sdkTypes.TurnkeyError("Unsupported platform for passkey creation", sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } return passkey; }, { errorMessage: "Failed to create passkey", errorCode: sdkTypes.TurnkeyErrorCodes.CREATE_PASSKEY_ERROR, customMessageByMessages: { "timed out or was not allowed": { message: "Passkey creation was cancelled by the user.", code: sdkTypes.TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED, }, }, }); }; /** * Logs out the current client session. * * - This function clears the specified session and removes any associated key pairs from storage. * - If a sessionKey is provided, it logs out from that session; otherwise, it logs out from the active session. * - Cleans up any api keys associated with the session. * * @param params.sessionKey - session key to specify which session to log out from (defaults to the active session). * @returns A promise that resolves when the logout process is complete. * @throws {TurnkeyError} If there is no active session or if there is an error during the logout process. */ this.logout = async (params) => { utils.withTurnkeyErrorHandling(async () => { if (params?.sessionKey) { const session = await this.storageManager.getSession(params.sessionKey); this.storageManager.clearSession(params.sessionKey); this.apiKeyStamper?.deleteKeyPair(session?.publicKey); } else { const sessionKey = await this.storageManager.getActiveSessionKey(); const session = await this.storageManager.getActiveSession(); if (sessionKey) { this.storageManager.clearSession(sessionKey); this.apiKeyStamper?.deleteKeyPair(session?.publicKey); } else { throw new sdkTypes.TurnkeyError("No active session found to log out from.", sdkTypes.TurnkeyErrorCodes.NO_SESSION_FOUND); } } }, { errorMessage: "Failed to log out", errorCode: sdkTypes.TurnkeyErrorCodes.LOGOUT_ERROR, }); }; /** * Logs in a user using a passkey, optionally specifying the public key, session key, and session expiration. * * - This function initiates the login process with a passkey and handles session creation and storage. * - If a public key is not provided, a new key pair will be generated for authentication. * - If a session key is not provided, the default session key will be used. * - The session expiration can be customized via the expirationSeconds parameter. * - Handles cleanup of unused key pairs if login fails. * * @param params.publicKey - public key to use for authentication. If not provided, a new key pair will be generated. * @param params.sessionKey - session key to use for session creation (defaults to the default session key). * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default). * @returns A promise that resolves to a signed JWT session token. * @throws {TurnkeyError} If there is an error during the passkey login process or if the user cancels the passkey prompt. */ this.loginWithPasskey = async (params) => { let generatedKeyPair = undefined; return await utils.withTurnkeyErrorHandling(async () => { generatedKeyPair = params?.publicKey || (await this.apiKeyStamper?.createKeyPair()); const sessionKey = params?.sessionKey || base.SessionKey.DefaultSessionkey; const expirationSeconds = params?.expirationSeconds || base.DEFAULT_SESSION_EXPIRATION_IN_SECONDS; if (!generatedKeyPair) { throw new sdkTypes.TurnkeyError("A publickey could not be found or generated.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR); } const sessionResponse = await this.httpClient.stampLogin({ publicKey: generatedKeyPair, organizationId: this.config.organizationId, expirationSeconds, }, base.StamperType.Passkey); await this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }); generatedKeyPair = undefined; // Key pair was successfully used, set to null to prevent cleanup return sessionResponse.session; }, { errorMessage: "Unable to log in with the provided passkey", errorCode: sdkTypes.TurnkeyErrorCodes.PASSKEY_LOGIN_AUTH_ERROR, customMessageByMessages: { "timed out or was not allowed": { message: "Passkey login was cancelled by the user.", code: sdkTypes.TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED, }, }, }, { finallyFn: async () => { if (generatedKeyPair) { try { await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair); } catch (cleanupError) { throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * Signs up a user using a passkey, creating a new sub-organization and session. * * - This function creates a new passkey authenticator and uses it to register a new sub-organization for the user. * - Handles both passkey creation and sub-organization creation in a single flow. * - Optionally accepts additional sub-organization parameters, a custom session key, a custom passkey display name, and a custom session expiration. * - Automatically generates a new API key pair for authentication and session management. * - Stores the resulting session token and manages cleanup of unused key pairs. * * @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata). * @param params.sessionKey - session key to use for storing the session (defaults to the default session key). * @param params.passkeyDisplayName - display name for the passkey (defaults to a generated name based on the current timestamp). * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default). * @returns A promise that resolves to a signed JWT session token for the new sub-organization. * @throws {TurnkeyError} If there is an error during passkey creation, sub-organization creation, or session storage. */ this.signUpWithPasskey = async (params) => { const { createSubOrgParams, passkeyDisplayName, sessionKey = base.SessionKey.DefaultSessionkey, expirationSeconds = base.DEFAULT_SESSION_EXPIRATION_IN_SECONDS, } = params || {}; let generatedKeyPair = undefined; return utils.withTurnkeyErrorHandling(async () => { generatedKeyPair = await this.apiKeyStamper?.createKeyPair(); const passkeyName = passkeyDisplayName || `passkey-${Date.now()}`; // A passkey will be created automatically when you call this function. The name is passed in const passkey = await this.createPasskey({ name: passkeyName, displayName: passkeyName, }); if (!passkey) { throw new sdkTypes.TurnkeyError("Failed to create passkey: encoded challenge or attestation is missing", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR); } const signUpBody = utils.buildSignUpBody({ createSubOrgParams: { ...createSubOrgParams, authenticators: [ ...(createSubOrgParams?.authenticators ?? []), // Any extra authenticators can be passed into createSubOrgParams { // Add our passkey that we made earlier. authenticatorName: passkeyName, // Ensure the name in orgData is the same name as the created passkey challenge: passkey.encodedChallenge, attestation: passkey.attestation, }, ], apiKeys: [ { apiKeyName: `passkey-auth-${generatedKeyPair}`, publicKey: generatedKeyPair, curveType: "API_KEY_CURVE_P256", expirationSeconds: "60", }, ], }, }); const res = await this.httpClient.proxySignup(signUpBody); if (!res) { throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.TurnkeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR); } const newGeneratedKeyPair = await this.apiKeyStamper?.createKeyPair(); this.apiKeyStamper?.setPublicKeyOverride(generatedKeyPair); const sessionResponse = await this.httpClient.stampLogin({ publicKey: newGeneratedKeyPair, organizationId: this.config.organizationId, expirationSeconds, }); await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair); await this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }); generatedKeyPair = undefined; // Key pair was successfully used, set to null to prevent cleanup return sessionResponse.session; }, { errorCode: sdkTypes.TurnkeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR, errorMessage: "Failed to sign up with passkey", }, { finallyFn: async () => { this.apiKeyStamper?.clearPublicKeyOverride(); if (generatedKeyPair) { try { await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair); } catch (cleanupError) { throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * Retrieves wallet providers from the initialized wallet manager. * * - Optionally filters providers by the specified blockchain chain. * - Throws an error if the wallet manager is not initialized. * * @param chain - optional blockchain chain to filter the returned providers. * @returns A promise that resolves to an array of wallet providers. * @throws {TurnkeyError} If the wallet manager is uninitialized or provider retrieval fails. */ this.getWalletProviders = async (chain) => { return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager) { throw new sdkTypes.TurnkeyError("Wallet manager is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } return await this.walletManager.getProviders(chain); }, { errorMessage: "Unable to get wallet providers", errorCode: sdkTypes.TurnkeyErrorCodes.FETCH_WALLETS_ERROR, }); }; /** * Connects the specified wallet account. * * - Requires the wallet manager and its connector to be initialized. * * @param walletProvider - wallet provider to connect. * @returns A promise that resolves once the wallet account is connected. * @throws {TurnkeyError} If the wallet manager is uninitialized or the connection fails. */ this.connectWalletAccount = async (walletProvider) => { return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager?.connector) { throw new sdkTypes.TurnkeyError("Wallet connector is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } await this.walletManager.connector.connectWalletAccount(walletProvider); }, { errorMessage: "Unable to connect wallet account", errorCode: sdkTypes.TurnkeyErrorCodes.CONNECT_WALLET_ACCOUNT_ERROR, }); }; /** * Disconnects the specified wallet account. * * - Requires the wallet manager and its connector to be initialized. * * @param walletProvider - wallet provider to disconnect. * @returns A promise that resolves once the wallet account is disconnected. * @throws {TurnkeyError} If the wallet manager is uninitialized or the disconnection fails. */ this.disconnectWalletAccount = async (walletProvider) => { return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager?.connector) { throw new sdkTypes.TurnkeyError("Wallet connector is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } await this.walletManager.connector.disconnectWalletAccount(walletProvider); }, { errorMessage: "Unable to disconnect wallet account", errorCode: sdkTypes.TurnkeyErrorCodes.DISCONNECT_WALLET_ACCOUNT_ERROR, }); }; /** * Switches the wallet provider associated with a given wallet account * to a different chain. * * - Requires the wallet manager and its connector to be initialized * - Only works for connected wallet accounts * - Looks up the provider for the given account address * - Does nothing if the provider is already on the desired chain. * * @param params.walletAccount - The wallet account whose provider should be switched. * @param params.chainOrId - The target chain, specified as a chain ID string or a SwitchableChain object. * @param params.walletProviders - Optional list of wallet providers to search; falls back to `getWalletProviders()` if omitted. * @returns A promise that resolves once the chain switch is complete. * * @throws {TurnkeyError} If the wallet manager is uninitialized, the provider is not connected, or the switch fails. */ this.switchWalletAccountChain = async (params) => { const { walletAccount, chainOrId, walletProviders } = params; return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager?.connector) { throw new sdkTypes.TurnkeyError("Wallet connector is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } if (walletAccount.source === base.WalletSource.Embedded) { throw new sdkTypes.TurnkeyError("You can only switch chains for connected wallet accounts", sdkTypes.TurnkeyErrorCodes.NOT_FOUND); } const providers = walletProviders ?? (await this.getWalletProviders()); const walletProvider = utils.findWalletProviderFromAddress(walletAccount.address, providers); if (!walletProvider) { throw new sdkTypes.TurnkeyError("Wallet provider not found", sdkTypes.TurnkeyErrorCodes.SWITCH_WALLET_CHAIN_ERROR); } // if the wallet provider is already on the desired chain, do nothing if (walletProvider.chainInfo.namespace === chainOrId) { return; } await this.walletManager.connector.switchChain(walletProvider, chainOrId); }, { errorMessage: "Unable to switch wallet account chain", errorCode: sdkTypes.TurnkeyErrorCodes.SWITCH_WALLET_CHAIN_ERROR, }); }; /** * Logs in a user using the specified wallet provider. * * - This function logs in a user by authenticating with the provided wallet provider via a wallet-based signature. * - If a public key is not provided, a new one will be generated for authentication. * - Optionally accepts a custom session key and session expiration time. * - Stores the resulting session token under the specified session key, or the default session key if not provided. * - Throws an error if a public key cannot be found or generated, or if the login process fails. * * @param params.walletProvider - wallet provider to use for authentication. * @param params.publicKey - optional public key to associate with the session (generated if not provided). * @param params.sessionKey - optional key to store the session under (defaults to the default session key). * @param params.expirationSeconds - optional session expiration time in seconds (defaults to the configured default). * @returns A promise that resolves to the created session token. * @throws {TurnkeyError} If the wallet stamper is uninitialized, a public key cannot be found or generated, or login fails. */ this.loginWithWallet = async (params) => { let publicKey = params.publicKey || (await this.apiKeyStamper?.createKeyPair()); return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager?.stamper) { throw new sdkTypes.TurnkeyError("Wallet stamper is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } const sessionKey = params.sessionKey || base.SessionKey.DefaultSessionkey; const walletProvider = params.walletProvider; const expirationSeconds = params?.expirationSeconds || base.DEFAULT_SESSION_EXPIRATION_IN_SECONDS; if (!publicKey) { throw new sdkTypes.TurnkeyError("A publickey could not be found or generated.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR); } this.walletManager.stamper.setProvider(walletProvider.interfaceType, walletProvider); const sessionResponse = await this.httpClient.stampLogin({ publicKey, organizationId: this.config.organizationId, expirationSeconds, }, base.StamperType.Wallet); await this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }); return sessionResponse.session; }, { errorMessage: "Unable to log in with the provided wallet", errorCode: sdkTypes.TurnkeyErrorCodes.WALLET_LOGIN_AUTH_ERROR, }, { finallyFn: async () => { // Clean up the generated key pair if it wasn't successfully used this.apiKeyStamper?.clearPublicKeyOverride(); if (publicKey) { try { await this.apiKeyStamper?.deleteKeyPair(publicKey); } catch (cleanupError) { throw new sdkTypes.TurnkeyError("Failed to clean up generated key pair", sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * Signs up a user using a wallet, creating a new sub-organization and session. * * - This function creates a new wallet authenticator and uses it to register a new sub-organization for the user. * - Handles both wallet authentication and sub-organization creation in a single flow. * - Optionally accepts additional sub-organization parameters, a custom session key, and a custom session expiration. * - Automatically generates additional API key pairs for authentication and session management. * - Stores the resulting session token under the specified session key, or the default session key if not provided, and manages cleanup of unused key pairs. * * @param params.walletProvider - wallet provider to use for authentication. * @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata). * @param params.sessionKey - session key to use for storing the session (defaults to the default session key). * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default). * @returns A promise that resolves to a signed JWT session token for the new sub-organization. * @throws {TurnkeyError} If there is an error during wallet authentication, sub-organization creation, session storage, or cleanup. */ this.signUpWithWallet = async (params) => { const { walletProvider, createSubOrgParams, sessionKey = base.SessionKey.DefaultSessionkey, expirationSeconds = base.DEFAULT_SESSION_EXPIRATION_IN_SECONDS, } = params; let generatedKeyPair = undefined; return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager?.stamper) { throw new sdkTypes.TurnkeyError("Wallet stamper is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } generatedKeyPair = await this.apiKeyStamper?.createKeyPair(); this.walletManager.stamper.setProvider(walletProvider.interfaceType, walletProvider); const publicKey = await this.walletManager.stamper.getPublicKey(walletProvider.interfaceType, walletProvider); if (!publicKey) { throw new sdkTypes.TurnkeyError("Failed to get public key from wallet", sdkTypes.TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR); } const signUpBody = utils.buildSignUpBody({ createSubOrgParams: { ...createSubOrgParams, apiKeys: [ { apiKeyName: `wallet-auth:${publicKey}`, publicKey: publicKey, curveType: utils.isEthereumProvider(walletProvider) ? "API_KEY_CURVE_SECP256K1" : "API_KEY_CURVE_ED25519", }, { apiKeyName: `wallet-auth-${generatedKeyPair}`, publicKey: generatedKeyPair, curveType: "API_KEY_CURVE_P256", expirationSeconds: "60", }, ], }, }); const res = await this.httpClient.proxySignup(signUpBody); if (!res) { throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR); } const newGeneratedKeyPair = await this.apiKeyStamper?.createKeyPair(); this.apiKeyStamper?.setPublicKeyOverride(generatedKeyPair); const sessionResponse = await this.httpClient.stampLogin({ publicKey: newGeneratedKeyPair, organizationId: this.config.organizationId, expirationSeconds, }); await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair); await this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }); generatedKeyPair = undefined; // Key pair was successfully used, set to null to prevent cleanup return sessionResponse.session; }, { errorMessage: "Failed to sign up with wallet", errorCode: sdkTypes.TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR, }, { finallyFn: async () => { // Clean up the generated key pair if it wasn't successfully used this.apiKeyStamper?.clearPublicKeyOverride(); if (generatedKeyPair) { try { await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair); } catch (cleanupError) { throw new sdkTypes.TurnkeyError("Failed to clean up generated key pair", sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * Logs in an existing user or signs up a new user using a wallet, creating a new sub-organization if needed. * * - This function attempts to log in the user by stamping a login request with the provided wallet. * - If the wallet’s public key is not associated with an existing sub-organization, a new one is created. * - Handles both wallet authentication and sub-organization creation in a single flow. * - For Ethereum wallets, derives the public key from the signed request header; for Solana wallets, retrieves it directly from the wallet. * - Optionally accepts additional sub-organization parameters, a custom session key, and a custom session expiration. * - Stores the resulting session token under the specified session key, or the default session key if not provided. * * @param params.walletProvider - wallet provider to use for authentication. * @param params.createSubOrgParams - optional parameters for creating a sub-organization (e.g., authenticators, user metadata). * @param params.sessionKey - session key to use for storing the session (defaults to the default session key). * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default). * @returns A promise that resolves to a signed JWT session token for the sub-organization (new or existing). * @throws {TurnkeyError} If there is an error during wallet authentication, sub-organization creation, or session storage. */ this.loginOrSignupWithWallet = async (params) => { const createSubOrgParams = params.createSubOrgParams; const sessionKey = params.sessionKey || base.SessionKey.DefaultSessionkey; const walletProvider = params.walletProvider; const expirationSeconds = params.expirationSeconds || base.DEFAULT_SESSION_EXPIRATION_IN_SECONDS; let generatedKeyPair = undefined; return utils.withTurnkeyErrorHandling(async () => { if (!this.walletManager?.stamper) { throw new sdkTypes.TurnkeyError("Wallet stamper is not initialized", sdkTypes.TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } generatedKeyPair = await this.apiKeyStamper?.createKeyPair(); this.walletManager.stamper.setProvider(walletProvider.interfaceType, walletProvider); // here we sign the request with the wallet, but we don't send it to the Turnkey yet // this is because we need to check if the subOrg exists first, and create one if it doesn't // once we have the subOrg for the publicKey, we then can send the request to the Turnkey const signedRequest = await utils.withTurnkeyErrorHandling(async () => { return this.httpClient.stampStampLogin({ publicKey: generatedKeyPair, organizationId: this.config.organizationId, expirationSeconds, }, base.StamperType.Wallet); }, { errorMessage: "Failed to create stamped request for wallet login", errorCode: sdkTypes.TurnkeyErrorCodes.WALLET_LOGIN_OR_SIGNUP_ERROR, customMessageByMessages: { "Failed to sign the message": { message: "Wallet auth was cancelled by the user.", code: sdkTypes.TurnkeyErrorCodes.CONNECT_WALLET_CANCELLED, }, }, }); if (!signedRequest) { throw new sdkTypes.TurnkeyError("Failed to create stamped request for wallet login", sdkTypes.TurnkeyErrorCodes.BAD_RESPONSE); } let publicKey; switch (walletProvider.chainInfo.namespace) { case base.Chain.Ethereum: { // for Ethereum, there is no way to get the public key from the wallet address // so we derive it from the signed request publicKey = utils.getPublicKeyFromStampHeader(signedRequest.stamp.stampHeaderValue); break; } case base.Chain.Solana: { // for Solana, we can get the public key from the wallet address // since the wallet address is the public key // this doesn't require any action from the user as long as the wallet is connected // which it has to be since they just called stampStampLogin() publicKey = await this.walletManager.stamper.getPublicKey(walletProvider.interfaceType, walletProvider); break; } default: throw new sdkTypes.TurnkeyError(`Unsupported interface type: ${walletProvider.interfaceType}`, sdkTypes.TurnkeyErrorCodes.INVALID_REQUEST); } // here we check if the subOrg exists and create one // then we send off the stamped request to the Turnkey const accountRes = await this.httpClient.proxyGetAccount({ filterType: base.FilterType.PublicKey, filterValue: publicKey, }); if (!accountRes) { throw new sdkTypes.TurnkeyError(`Account fetch failed`, sdkTypes.TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR); } const subOrganizationId = accountRes.organizationId; // if there is no subOrganizationId, we create one if (!subOrganizationId) { const signUpBody = utils.buildSignUpBody({ createSubOrgParams: { ...createSubOrgParams, apiKeys: [ { apiKeyName: `wallet-auth:${publicKey}`, publicKey: publicKey, curveType: utils.isEthereumProvider(walletProvider) ? "API_KEY_CURVE_SECP256K1" : "API_KEY_CURVE_ED25519", }, ], }, }); const res = await this.httpClient.proxySignup(signUpBody); if (!res) { throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR); } } // now we can send the stamped request to the Turnkey const headers = { "Content-Type": "application/json", [signedRequest.stamp.stampHeaderName]: signedRequest.stamp.stampHeaderValue, }; const res = await fetch(signedRequest.url, { method: "POST", headers, body: signedRequest.body, }); if (!res.ok) { const errorText = await res.text(); throw new sdkTypes.TurnkeyNetworkError(`Stamped request failed`, res.status, sdkTypes.TurnkeyErrorCodes.WALLET_LOGIN_AUTH_ERROR, errorText); } const sessionResponse = await res.json(); const sessionToken = sessionResponse.activity.result.stampLoginResult?.session; if (!sessionToken) { throw new sdkTypes.TurnkeyError("Session token not found in the response", sdkTypes.TurnkeyErrorCodes.BAD_RESPONSE); } await this.storeSession({ sessionToken: sessionToken, sessionKey, }); return sessionToken; }, { errorCode: sdkTypes.TurnkeyErrorCodes.WALLET_LOGIN_OR_SIGNUP_ERROR, errorMessage: "Failed to log in or sign up with wallet", catchFn: async () => { if (generatedKeyPair) { try { await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair); } catch (cleanupError) { throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * Initializes the OTP process by sending an OTP code to the provided contact. * * - 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. * - Supports both email and SMS OTP types. * - Returns an OTP ID that is required for subsequent OTP verification. * * @param params.otpType - type of OTP to initialize (OtpType.Email or OtpType.Sms). * @param params.contact - contact information for the user (e.g., email address or phone number). * @returns A promise that resolves to the OTP ID required for verification. * @throws {TurnkeyError} If there is an error during the OTP initialization process or if the maximum number of OTPs has been reached. */ this.initOtp = async (params) => { return utils.withTurnkeyErrorHandling(async () => { const initOtpRes = await this.httpClient.proxyInitOtp(params); if (!initOtpRes || !initOtpRes.otpId) { throw new sdkTypes.TurnkeyError("Failed to initialize OTP: otpId is missing", sdkTypes.TurnkeyErrorCodes.INIT_OTP_ERROR); } return initOtpRes.otpId; }, { errorMessage: "Failed to initialize OTP", errorCode: sdkTypes.TurnkeyErrorCodes.INIT_OTP_ERROR, customMessageByMessages: { "Max number of OTPs have been initiated": { message: "Maximum number of OTPs has been reached for this contact.", code: sdkTypes.TurnkeyErrorCodes.MAX_OTP_INITIATED_ERROR, }, }, }); }; /** * Verifies the OTP code sent to the user. * * - 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. * - If verification is successful, it returns the sub-organization ID associated with the contact (if it exists) and a verification token. * - The verification token can be used for subsequent login or sign-up flows. * - Handles both email and SMS OTP types. * * @param params.otpId - ID of the OTP to verify (returned from `initOtp`). * @param params.otpCode - OTP code entered by the user. * @param params.contact - contact information for the user (e.g., email address or phone number). * @param params.otpType - type of OTP being verified (OtpType.Email or OtpType.Sms). * @returns A promise that resolves to an object containing: * - subOrganizationId: sub-organization ID if the contact is already associated with a sub-organization, or an empty string if not. * - verificationToken: verification token to be used for login or sign-up. * @throws {TurnkeyError} If there is an error during the OTP verification process, such as an invalid code or network failure. */ this.verifyOtp = async (params) => { const { otpId, otpCode, contact, otpType } = params; return utils.withTurnkeyErrorHandling(async () => { const verifyOtpRes = await this.httpClient.proxyVerifyOtp({ otpId: otpId, otpCode: otpCode, }); if (!verifyOtpRes) { throw new sdkTypes.TurnkeyError(`OTP verification failed`, sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR); } const accountRes = await this.httpClient.proxyGetAccount({ filterType: base.OtpTypeToFilterTypeMap[otpType], filterValue: contact, }); if (!accountRes) { throw new sdkTypes.TurnkeyError(`Account fetch failed`, sdkTypes.TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR); } const subOrganizationId = accountRes.organizationId; return { subOrganizationId: subOrganizationId, verificationToken: verifyOtpRes.verificationToken, }; }, { errorMessage: "Failed to verify OTP", errorCode: sdkTypes.TurnkeyErrorCodes.VERIFY_OTP_ERROR, customMessageByMessages: { "Invalid OTP code": { message: "The provided OTP code is invalid.", code: sdkTypes.TurnkeyErrorCodes.INVALID_OTP_CODE, }, }, }); }; /** * Logs in a user using an OTP verification token. * * - This function logs in a user using the verification token received after OTP verification (from email or SMS). * - If a public key is not provided, a new API key pair will be generated for authentication. * - Optionally invalidates any existing sessions for the user if `invalidateExisting` is set to true. * - Stores the resulting session token under the specified session key, or the default session key if not provided. * - Handles cleanup of unused key pairs if login fails. * * @param params.verificationToken - verification token received after OTP verification. * @param params.publicKey - public key to use for authentication. If not provided, a new key pair will be generated. * @param params.invalidateExisting - flag to invalidate existing session for the user. * @param params.sessionKey - session key to use for session creation (defaults to the default session key). * @returns A promise that resolves to a signed JWT session token. * @throws {TurnkeyError} If there is an error during the OTP login process or if key pair cleanup fails. */ this.loginWithOtp = async (params) => { const { verificationToken, invalidateExisting = false, publicKey = await this.apiKeyStamper?.createKeyPair(), sessionKey = base.SessionKey.DefaultSessionkey, } = params; return utils.withTurnkeyErrorHandling(async () => { const res = await this.httpClient.proxyOtpLogin({ verificationToken, publicKey: publicKey, invalidateExisting, }); if (!res) { throw new sdkTypes.TurnkeyError(`Auth proxy OTP login failed`, sdkTypes.TurnkeyErrorCodes.OTP_LOGIN_ERROR); } const loginRes = await res; if (!loginRes.session) { throw new sdkTypes.TurnkeyError("No session returned from OTP login", sdkTypes.TurnkeyErrorCodes.OTP_LOGIN_ERROR); } await this.storeSession({ sessionToken: loginRes.session, sessionKey, }); return loginRes.session; }, { errorMessage: "Failed to log in with OTP", errorCode: sdkTypes.TurnkeyErrorCodes.OTP_LOGIN_ERROR, catchFn: async () => { // Clean up the generated key pair if it wasn't successfully used if (publicKey) { try { await this.apiKeyStamper?.deleteKeyPair(publicKey); } catch (cleanupError) { throw new sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * Signs up a user using an OTP verification token. * * - This function signs up a user using the verification token received after OTP verification (from email or SMS). * - Creates a new sub-organization for the user with the provided parameters and associates the contact (email or phone) with the sub-organization. * - Automatically generates a new API key pair for authentication and session management. * - Stores the resulting session token under the specified session key, or the default session key if not provided. * - Handles both email and SMS OTP types, and supports additional sub-organization creation parameters. * * @param params.verificationToken - verification token received after OTP verification. * @param params.contact - contact information for the user (e.g., email address or phone number). * @param params.otpType - type of OTP being used (OtpType.Email or OtpType.Sms). * @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata). * @param params.invalidateExisting - flag to invalidate existing session for the user. * @param params.sessionKey - session key to use for session creation (defaults to the default session key). * @returns A promise that resolves to a signed JWT session token for the new sub-organization. * @throws {TurnkeyError} If there is an error during the OTP sign-up process or session storage. */ this.signUpWithOtp = async (params) => { const { verificationToken, contact, otpType, createSubOrgParams, invalidateExisting, sessionKey, } = params; const signUpBody = utils.buildSignUpBody({ createSubOrgParams: { ...createSubOrgParams, ...(otpType === base.OtpType.Email ? { userEmail: contact } : { userPhoneNumber: contact }), verificationToken, }, }); return utils.withTurnkeyErrorHandling(async () => { const generatedKeyPair = await this.apiKeyStamper?.createKeyPair(); const res = await this.httpClient.proxySignup(signUpBody); if (!res) { throw new sdkTypes.TurnkeyError(`Auth proxy OTP sign up failed`, sdkTypes.TurnkeyErrorCodes.OTP_SIGNUP_ERROR); } return await this.loginWithOtp({ verificationToken, publicKey: generatedKeyPair, ...(invalidateExisting && { invalidateExisting }), ...(sessionKey && { sessionKey }), }); }, { errorCode: sdkTypes.TurnkeyErrorCodes.OTP_SIGNUP_ERROR, errorMessage: "Failed to sign up with OTP", }); }; /** * Completes the OTP authentication flow by verifying the OTP code and then either signing up or logging in the user. * * - This function first verifies the OTP code for the provided contact and OTP type. * - 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. * - If the contact is already associated