UNPKG

@turnkey/core

Version:

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

803 lines (802 loc) 214 kB
import { TurnkeySDKClientBase } from '../__generated__/sdk-client-base.mjs'; import { TurnkeyErrorCodes, TurnkeyError, AuthAction } from '@turnkey/sdk-types'; import { DEFAULT_SESSION_EXPIRATION_IN_SECONDS } from '../__types__/auth.mjs'; import { SessionKey, StamperType, WalletSource, FilterType, OtpTypeToFilterTypeMap, OtpType, Curve, Chain, SignIntent } from '../__types__/enums.mjs'; 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'; import { createStorageManager } from '../__storage__/base.mjs'; import { CrossPlatformApiKeyStamper } from '../__stampers__/api/base.mjs'; import { CrossPlatformPasskeyStamper } from '../__stampers__/passkey/base.mjs'; import { DEFAULT_ETHEREUM_ACCOUNTS, DEFAULT_SOLANA_ACCOUNTS } from '../turnkey-helpers.mjs'; import { jwtDecode } from 'jwt-decode'; import { createWalletManager } from '../__wallet__/base.mjs'; import { toUtf8Bytes } from 'ethers'; import { verify } from '@turnkey/crypto'; import { SignatureFormat } from '@turnkey/api-key-stamper'; 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 TurnkeySDKClientBase instance with the provided configuration. * This method is used internally to create the HTTP client for making API requests, * but can also be used to create an additional client with different configurations if needed. * By default, it uses the configuration provided during the TurnkeyClient initialization. * * @param params - Optional configuration parameters to override the default client configuration. * @param params.apiBaseUrl - The base URL of the Turnkey API (defaults to `https://api.turnkey.com` if not provided). * @param params.organizationId - The organization ID to associate requests with. * @param params.authProxyUrl - The base URL of the Auth Proxy (defaults to `https://authproxy.turnkey.com` if not provided). * @param params.authProxyConfigId - The configuration ID to use when making Auth Proxy requests. * @param params.defaultStamperType - The default stamper type to use for signing requests * (overrides automatic detection of ApiKey, Passkey, or Wallet stampers). * * @returns A new instance of {@link TurnkeySDKClientBase} configured with the provided parameters. */ this.createHttpClient = (params) => { // We can comfortably default to the prod urls here const apiBaseUrl = params?.apiBaseUrl || this.config.apiBaseUrl || "https://api.turnkey.com"; const authProxyUrl = params?.authProxyUrl || this.config.authProxyUrl || "https://authproxy.turnkey.com"; const organizationId = params?.organizationId || this.config.organizationId; return new TurnkeySDKClientBase({ ...this.config, ...params, apiBaseUrl, authProxyUrl, organizationId, apiKeyStamper: this.apiKeyStamper, passkeyStamper: this.passkeyStamper, walletStamper: this.walletManager?.stamper, storageManager: this.storageManager, }); }; /** * 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 - display name for the passkey (defaults to a generated name based on the current timestamp). * @param params.challenge - challenge string to use for passkey registration. If not provided, a new challenge will be generated. * @returns A promise that resolves to {@link CreatePasskeyResult} * @throws {TurnkeyError} If there is an error during passkey creation, or if the platform is unsupported. */ this.createPasskey = async (params) => { const { name: nameFromParams, challenge } = params || {}; return withTurnkeyErrorHandling(async () => { if (!this.passkeyStamper) { throw new TurnkeyError("Passkey stamper is not initialized", TurnkeyErrorCodes.INTERNAL_ERROR); } const name = isValidPasskeyName(nameFromParams || `passkey-${Date.now()}`); let passkey; if (isWeb()) { const res = await this.passkeyStamper.createWebPasskey({ publicKey: { user: { name, displayName: name, }, ...(challenge && { challenge }), }, }); if (!res) { throw new TurnkeyError("Failed to create Web passkey", TurnkeyErrorCodes.INTERNAL_ERROR); } passkey = { encodedChallenge: res?.encodedChallenge, attestation: res?.attestation, }; } else if (isReactNative()) { const res = await this.passkeyStamper.createReactNativePasskey({ name, displayName: name, }); if (!res) { throw new TurnkeyError("Failed to create React Native passkey", TurnkeyErrorCodes.INTERNAL_ERROR); } passkey = { encodedChallenge: res?.challenge, attestation: res?.attestation, }; } else { throw new TurnkeyError("Unsupported platform for passkey creation", TurnkeyErrorCodes.INVALID_REQUEST); } return passkey; }, { errorMessage: "Failed to create passkey", errorCode: TurnkeyErrorCodes.CREATE_PASSKEY_ERROR, customErrorsByMessages: { "timed out or was not allowed": { message: "Passkey creation was cancelled by the user.", code: 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) => { 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 TurnkeyError("No active session found to log out from.", TurnkeyErrorCodes.NO_SESSION_FOUND); } } }, { errorMessage: "Failed to log out", errorCode: 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). * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID). * @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes: * - `sessionToken`: the signed JWT session token. * - `credentialId`: an empty string. * @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 generatedPublicKey = undefined; return await withTurnkeyErrorHandling(async () => { generatedPublicKey = params?.publicKey || (await this.apiKeyStamper?.createKeyPair()); const sessionKey = params?.sessionKey || SessionKey.DefaultSessionkey; const expirationSeconds = params?.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS; if (!generatedPublicKey) { throw new TurnkeyError("A publickey could not be found or generated.", TurnkeyErrorCodes.INTERNAL_ERROR); } const sessionResponse = await this.httpClient.stampLogin({ publicKey: generatedPublicKey, organizationId: params?.organizationId ?? this.config.organizationId, expirationSeconds, }, StamperType.Passkey); await this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }); generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup return { sessionToken: sessionResponse.session, // TODO: can we return the credentialId here? // from a quick glance this is going to be difficult // for now we return an empty string credentialId: "", }; }, { errorMessage: "Unable to log in with the provided passkey", errorCode: TurnkeyErrorCodes.PASSKEY_LOGIN_AUTH_ERROR, customErrorsByMessages: { "timed out or was not allowed": { message: "Passkey login was cancelled by the user.", code: TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED, }, }, }, { finallyFn: async () => { if (generatedPublicKey) { try { await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey); } catch (cleanupError) { throw new TurnkeyError(`Failed to clean up generated key pair`, 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.passkeyDisplayName - display name for the passkey (defaults to a generated name based on the current timestamp). * @param params.challenge - challenge string to use for passkey registration. If not provided, a new challenge will be generated. * @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default). * @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.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID). * @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes: * - `sessionToken`: the signed JWT session token. * - `credentialId`: the credential ID associated with the passkey created. * @throws {TurnkeyError} If there is an error during passkey creation, sub-organization creation, or session storage. */ this.signUpWithPasskey = async (params) => { const { passkeyDisplayName, challenge, expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, organizationId, } = params || {}; let generatedPublicKey = undefined; return withTurnkeyErrorHandling(async () => { generatedPublicKey = 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, ...(challenge && { challenge }), }); if (!passkey) { throw new TurnkeyError("Failed to create passkey: encoded challenge or attestation is missing", TurnkeyErrorCodes.INTERNAL_ERROR); } const signUpBody = 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-${generatedPublicKey}`, publicKey: generatedPublicKey, curveType: "API_KEY_CURVE_P256", expirationSeconds: "60", }, ], }, }); const res = await this.httpClient.proxySignup(signUpBody); if (!res) { throw new TurnkeyError(`Sign up failed`, TurnkeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR); } const newGeneratedKeyPair = await this.apiKeyStamper?.createKeyPair(); this.apiKeyStamper?.setTemporaryPublicKey(generatedPublicKey); const sessionResponse = await this.httpClient.stampLogin({ publicKey: newGeneratedKeyPair, organizationId: organizationId ?? this.config.organizationId, expirationSeconds, }); await Promise.all([ this.apiKeyStamper?.deleteKeyPair(generatedPublicKey), this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }), ]); generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup return { sessionToken: sessionResponse.session, appProofs: res.appProofs, credentialId: passkey.attestation.credentialId, }; }, { errorCode: TurnkeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR, errorMessage: "Failed to sign up with passkey", }, { finallyFn: async () => { this.apiKeyStamper?.clearTemporaryPublicKey(); if (generatedPublicKey) { try { await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey); } catch (cleanupError) { throw new TurnkeyError(`Failed to clean up generated key pair`, 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.fetchWalletProviders = async (chain) => { return withTurnkeyErrorHandling(async () => { if (!this.walletManager) { throw new TurnkeyError("Wallet manager is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } return await this.walletManager.getProviders(chain); }, { errorMessage: "Unable to get wallet providers", errorCode: 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 with the connected wallet's address. * @throws {TurnkeyError} If the wallet manager is uninitialized or the connection fails. */ this.connectWalletAccount = async (walletProvider) => { return withTurnkeyErrorHandling(async () => { if (!this.walletManager?.connector) { throw new TurnkeyError("Wallet connector is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } return await this.walletManager.connector.connectWalletAccount(walletProvider); }, { errorMessage: "Unable to connect wallet account", errorCode: TurnkeyErrorCodes.CONNECT_WALLET_ACCOUNT_ERROR, customErrorsByMessages: { "WalletConnect: The connection request has expired. Please scan the QR code again.": { message: "Your WalletConnect session expired. Please scan the QR code again.", code: TurnkeyErrorCodes.WALLET_CONNECT_EXPIRED, }, "User rejected the request": { message: "Connect wallet was cancelled by the user.", code: TurnkeyErrorCodes.CONNECT_WALLET_CANCELLED, }, }, }); }; /** * 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 withTurnkeyErrorHandling(async () => { if (!this.walletManager?.connector) { throw new TurnkeyError("Wallet connector is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } await this.walletManager.connector.disconnectWalletAccount(walletProvider); }, { errorMessage: "Unable to disconnect wallet account", errorCode: 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 `fetchWalletProviders()` 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 withTurnkeyErrorHandling(async () => { if (!this.walletManager?.connector) { throw new TurnkeyError("Wallet connector is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } if (walletAccount.source === WalletSource.Embedded) { throw new TurnkeyError("You can only switch chains for connected wallet accounts", TurnkeyErrorCodes.NOT_FOUND); } const providers = walletProviders ?? (await this.fetchWalletProviders()); const walletProvider = findWalletProviderFromAddress(walletAccount.address, providers); if (!walletProvider) { throw new TurnkeyError("Wallet provider not found", 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: TurnkeyErrorCodes.SWITCH_WALLET_CHAIN_ERROR, }); }; /** * Builds and signs a wallet login request without submitting it to Turnkey. * * - This function prepares a signed request for wallet authentication, which can later be used * to log in or sign up a user with Turnkey. * - It initializes the wallet stamper, ensures a valid session public key (generating one if needed), * and signs the login intent with the connected wallet. * - For Ethereum wallets, derives the public key from the stamped request header. * - For Solana wallets, retrieves the public key directly from the connected wallet. * - The signed request is not sent to Turnkey immediately; it is meant to be used in a subsequent flow * (e.g., `loginOrSignupWithWallet`) where sub-organization existence is verified or created first. * * @param params.walletProvider - the wallet provider used for authentication and signing. * @param params.publicKey - optional pre-generated session public key (auto-generated if not provided). * @param params.expirationSeconds - optional session expiration time in seconds (defaults to the configured default). * @returns A promise resolving to an object containing: * - `signedRequest`: the signed wallet login request. * - `publicKey`: the public key associated with the signed request. * @throws {TurnkeyError} If the wallet stamper is not initialized, the signing process fails, * or the public key cannot be derived or generated. */ this.buildWalletLoginRequest = async (params) => { const { walletProvider, publicKey: providedPublicKey } = params; const expirationSeconds = params.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS; let generatedPublicKey = undefined; return withTurnkeyErrorHandling(async () => { if (!this.walletManager?.stamper) { throw new TurnkeyError("Wallet stamper is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED); } const futureSessionPublicKey = providedPublicKey ?? (generatedPublicKey = await this.apiKeyStamper?.createKeyPair()); if (!futureSessionPublicKey) { throw new TurnkeyError("Failed to find or generate a public key for building the wallet login request", TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR); } this.walletManager.stamper.setProvider(walletProvider.interfaceType, walletProvider); // here we sign the request with the wallet, but we don't send it to 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 Turnkey const signedRequest = await withTurnkeyErrorHandling(async () => { return this.httpClient.stampStampLogin({ publicKey: futureSessionPublicKey, organizationId: this.config.organizationId, expirationSeconds, }, StamperType.Wallet); }, { errorMessage: "Failed to create stamped request for wallet login", errorCode: TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR, customErrorsByMessages: { "WalletConnect: The connection request has expired. Please scan the QR code again.": { message: "Your WalletConnect session expired. Please scan the QR code again.", code: TurnkeyErrorCodes.WALLET_CONNECT_EXPIRED, }, "Failed to sign the message": { message: "Wallet auth was cancelled by the user.", code: TurnkeyErrorCodes.CONNECT_WALLET_CANCELLED, }, }, }); if (!signedRequest) { throw new TurnkeyError("Failed to create stamped request for wallet login", TurnkeyErrorCodes.BAD_RESPONSE); } // the wallet's public key is embedded in the stamp header by the wallet stamper // so we extract it from there const publicKey = getPublicKeyFromStampHeader(signedRequest.stamp.stampHeaderValue); return { signedRequest, publicKey, }; }, { errorCode: TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR, errorMessage: "Failed to build wallet login request", catchFn: async () => { if (generatedPublicKey) { try { await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey); } catch (cleanupError) { throw new TurnkeyError(`Failed to clean up generated key pair`, TurnkeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError); } } }, }); }; /** * 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). * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID). * @returns A promise that resolves to a {@link WalletAuthResult}, which includes: * - `sessionToken`: the signed JWT session token. * - `address`: the authenticated wallet address. * @throws {TurnkeyError} If the wallet stamper is uninitialized, a public key cannot be found or generated, or login fails. */ this.loginWithWallet = async (params) => { const { walletProvider, sessionKey = SessionKey.DefaultSessionkey } = params; return withTurnkeyErrorHandling(async () => { const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params); const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest); const sessionToken = sessionResponse.session; if (!sessionToken) { throw new TurnkeyError("Session token not found in the response", TurnkeyErrorCodes.BAD_RESPONSE); } await this.storeSession({ sessionToken: sessionResponse.session, sessionKey, }); return { sessionToken: sessionResponse.session, address: addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey), }; }, { errorMessage: "Unable to log in with the provided wallet", errorCode: TurnkeyErrorCodes.WALLET_LOGIN_AUTH_ERROR, }); }; /** * 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). * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID). * @returns A promise that resolves to a {@link WalletAuthResult}, which includes: * - `sessionToken`: the signed JWT session token. * - `address`: the authenticated wallet address. * @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 = SessionKey.DefaultSessionkey, } = params; return withTurnkeyErrorHandling(async () => { const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params); const signUpBody = buildSignUpBody({ createSubOrgParams: { ...createSubOrgParams, apiKeys: [ { apiKeyName: `wallet-auth:${publicKey}`, publicKey: publicKey, curveType: getCurveTypeFromProvider(walletProvider), }, ], }, }); const res = await this.httpClient.proxySignup(signUpBody); if (!res) { throw new TurnkeyError(`Sign up failed`, TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR); } // now we can send the stamped request to Turnkey const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest); const sessionToken = sessionResponse.session; if (!sessionToken) { throw new TurnkeyError("Session token not found in the response", TurnkeyErrorCodes.BAD_RESPONSE); } await this.storeSession({ sessionToken: sessionToken, sessionKey, }); return { sessionToken: sessionToken, appProofs: res.appProofs, address: addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey), }; }, { errorMessage: "Failed to sign up with wallet", errorCode: TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR, }); }; /** * 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.publicKey - optional public key to associate with the session (generated if not provided). * @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). * @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID). * @returns A promise that resolves to an object containing: * - `sessionToken`: the signed JWT session token. * - `address`: the authenticated wallet address. * - `action`: whether the flow resulted in a login or signup ({@link AuthAction}). * @throws {TurnkeyError} If there is an error during wallet authentication, sub-organization creation, or session storage. */ this.loginOrSignupWithWallet = async (params) => { const { walletProvider, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, } = params; return withTurnkeyErrorHandling(async () => { const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params); // here we check if the subOrg exists and create one // then we send off the stamped request to Turnkey const accountRes = await this.httpClient.proxyGetAccount({ filterType: FilterType.PublicKey, filterValue: publicKey, }); if (!accountRes) { throw new TurnkeyError(`Account fetch failed`, TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR); } const subOrganizationId = accountRes.organizationId; // if there is no subOrganizationId, we create one let signupRes; if (!subOrganizationId) { const signUpBody = buildSignUpBody({ createSubOrgParams: { ...createSubOrgParams, apiKeys: [ { apiKeyName: `wallet-auth:${publicKey}`, publicKey: publicKey, curveType: getCurveTypeFromProvider(walletProvider), }, ], }, }); signupRes = await this.httpClient.proxySignup(signUpBody); if (!signupRes) { throw new TurnkeyError(`Sign up failed`, TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR); } } // now we can send the stamped request to Turnkey const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest); const sessionToken = sessionResponse.session; if (!sessionToken) { throw new TurnkeyError("Session token not found in the response", TurnkeyErrorCodes.BAD_RESPONSE); } await this.storeSession({ sessionToken: sessionToken, sessionKey, }); return { sessionToken: sessionToken, appProofs: signupRes?.appProofs, address: addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey), // if the subOrganizationId exists, it means the user is logging in action: subOrganizationId ? AuthAction.LOGIN : AuthAction.SIGNUP, }; }, { errorCode: TurnkeyErrorCodes.WALLET_LOGIN_OR_SIGNUP_ERROR, errorMessage: "Failed to log in or sign up with wallet", }); }; /** * 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). * @param params.organizationId - optional organization ID to target (defaults to the session's organization ID or the parent organization ID). * @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 withTurnkeyErrorHandling(async () => { const initOtpRes = await this.httpClient.proxyInitOtp(params); if (!initOtpRes || !initOtpRes.otpId) { throw new TurnkeyError("Failed to initialize OTP: otpId is missing", TurnkeyErrorCodes.INIT_OTP_ERROR); } return initOtpRes.otpId; }, { errorMessage: "Failed to initialize OTP", errorCode: TurnkeyErrorCodes.INIT_OTP_ERROR, customErrorsByMessages: { "Max number of OTPs have been initiated": { message: "Maximum number of OTPs has been reached for this contact.", code: 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). * @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. * @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; const resolvedPublicKey = params.publicKey ?? (await this.apiKeyStamper?.createKeyPair()); return withTurnkeyErrorHandling(async () => { const verifyOtpRes = await this.httpClient.proxyVerifyOtp({ otpId: otpId, otpCode: otpCode, publicKey: resolvedPublicKey, }); if (!verifyOtpRes) { throw new TurnkeyError(`OTP verification failed`, TurnkeyErrorCodes.INTERNAL_ERROR); } const accountRes = await this.httpClient.proxyGetAccount({ filterType: OtpTypeToFilterTypeMap[otpType], filterValue: contact, verificationToken: verifyOtpRes.verificationToken, }); if (!accountRes) { throw new TurnkeyError(`Account fetch failed`, TurnkeyErrorCodes.ACCOUNT_FETCH_ERROR); } const subOrganizationId = accountRes.organizationId; return { subOrganizationId: subOrganizationId, verificationToken: verifyOtpRes.verificationToken, }; }, { errorMessage: "Failed to verify OTP", errorCode: TurnkeyErrorCodes.VERIFY_OTP_ERROR, customErrorsByMessages: { "Invalid OTP code": { message: "The provided OTP code is invalid.", code: 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.organizationId - optional organization ID to target (defaults to the verified subOrg ID linked to the verification token contact). * @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 {@link BaseAuthResult}, which includes: * - `sessionToken`: the 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(), organizationId, sessionKey = SessionKey.DefaultSessionkey, } = params; return withTurnkeyErrorHandling(async () => { const { message, publicKey: clientSignaturePublicKey } = getClientSignatureMessageForLogin({ verificationToken, sessionPublicKey: publicKey, }); this.apiKeyStamper?.setTemporaryPublicKey(publicKey); const signature = await this.apiKeyStamper?.sign(message, SignatureFormat.Raw); if (!s