@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
833 lines (831 loc) • 156 kB
JavaScript
import { TurnkeySDKClientBase } from '../__generated__/sdk-client-base.mjs';
import { TurnkeyErrorCodes, TurnkeyError, TurnkeyNetworkError } from '@turnkey/sdk-types';
import { SessionKey, DEFAULT_SESSION_EXPIRATION_IN_SECONDS, StamperType, WalletSource, Chain, FilterType, OtpTypeToFilterTypeMap, OtpType, Curve, SignIntent } from '../__types__/base.mjs';
import { withTurnkeyErrorHandling, isWeb, isReactNative, buildSignUpBody, findWalletProviderFromAddress, isEthereumProvider, getPublicKeyFromStampHeader, toExternalTimestamp, isSolanaProvider, getHashFunction, getEncodingType, getEncodedMessage, splitSignature, broadcastTransaction, 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';
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 withTurnkeyErrorHandling(async () => {
const name = params?.name || "A Passkey";
const displayName = params?.displayName || "A Passkey";
let passkey;
if (isWeb()) {
const res = await this.passkeyStamper?.createWebPasskey({
publicKey: {
user: {
name,
displayName,
},
},
});
if (!res) {
throw new TurnkeyError("Failed to create React Native passkey", TurnkeyErrorCodes.INTERNAL_ERROR);
}
passkey = {
encodedChallenge: res?.encodedChallenge,
attestation: res?.attestation,
};
}
else if (isReactNative()) {
const res = await this.passkeyStamper?.createReactNativePasskey({
name,
displayName,
});
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,
customMessageByMessages: {
"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).
* @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 withTurnkeyErrorHandling(async () => {
generatedKeyPair =
params?.publicKey || (await this.apiKeyStamper?.createKeyPair());
const sessionKey = params?.sessionKey || SessionKey.DefaultSessionkey;
const expirationSeconds = params?.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
if (!generatedKeyPair) {
throw new TurnkeyError("A publickey could not be found or generated.", TurnkeyErrorCodes.INTERNAL_ERROR);
}
const sessionResponse = await this.httpClient.stampLogin({
publicKey: generatedKeyPair,
organizationId: this.config.organizationId,
expirationSeconds,
}, 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: TurnkeyErrorCodes.PASSKEY_LOGIN_AUTH_ERROR,
customMessageByMessages: {
"timed out or was not allowed": {
message: "Passkey login was cancelled by the user.",
code: TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED,
},
},
}, {
finallyFn: async () => {
if (generatedKeyPair) {
try {
await this.apiKeyStamper?.deleteKeyPair(generatedKeyPair);
}
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.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 = SessionKey.DefaultSessionkey, expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, } = params || {};
let generatedKeyPair = undefined;
return 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 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-${generatedKeyPair}`,
publicKey: generatedKeyPair,
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?.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: 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 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.getWalletProviders = 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 once the wallet account is connected.
* @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);
}
await this.walletManager.connector.connectWalletAccount(walletProvider);
}, {
errorMessage: "Unable to connect wallet account",
errorCode: 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 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 `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 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.getWalletProviders());
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,
});
};
/**
* 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 withTurnkeyErrorHandling(async () => {
if (!this.walletManager?.stamper) {
throw new TurnkeyError("Wallet stamper is not initialized", TurnkeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
}
const sessionKey = params.sessionKey || SessionKey.DefaultSessionkey;
const walletProvider = params.walletProvider;
const expirationSeconds = params?.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
if (!publicKey) {
throw new TurnkeyError("A publickey could not be found or generated.", TurnkeyErrorCodes.INTERNAL_ERROR);
}
this.walletManager.stamper.setProvider(walletProvider.interfaceType, walletProvider);
const sessionResponse = await this.httpClient.stampLogin({
publicKey,
organizationId: this.config.organizationId,
expirationSeconds,
}, StamperType.Wallet);
await this.storeSession({
sessionToken: sessionResponse.session,
sessionKey,
});
return sessionResponse.session;
}, {
errorMessage: "Unable to log in with the provided wallet",
errorCode: 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 TurnkeyError("Failed to clean up generated key pair", 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 = SessionKey.DefaultSessionkey, expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, } = params;
let generatedKeyPair = undefined;
return withTurnkeyErrorHandling(async () => {
if (!this.walletManager?.stamper) {
throw new TurnkeyError("Wallet stamper is not initialized", 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 TurnkeyError("Failed to get public key from wallet", TurnkeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
}
const signUpBody = buildSignUpBody({
createSubOrgParams: {
...createSubOrgParams,
apiKeys: [
{
apiKeyName: `wallet-auth:${publicKey}`,
publicKey: publicKey,
curveType: 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 TurnkeyError(`Sign up failed`, 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: 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 TurnkeyError("Failed to clean up generated key pair", 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 || SessionKey.DefaultSessionkey;
const walletProvider = params.walletProvider;
const expirationSeconds = params.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
let generatedKeyPair = undefined;
return withTurnkeyErrorHandling(async () => {
if (!this.walletManager?.stamper) {
throw new TurnkeyError("Wallet stamper is not initialized", 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 withTurnkeyErrorHandling(async () => {
return this.httpClient.stampStampLogin({
publicKey: generatedKeyPair,
organizationId: this.config.organizationId,
expirationSeconds,
}, StamperType.Wallet);
}, {
errorMessage: "Failed to create stamped request for wallet login",
errorCode: TurnkeyErrorCodes.WALLET_LOGIN_OR_SIGNUP_ERROR,
customMessageByMessages: {
"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);
}
let publicKey;
switch (walletProvider.chainInfo.namespace) {
case 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 = getPublicKeyFromStampHeader(signedRequest.stamp.stampHeaderValue);
break;
}
case 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 TurnkeyError(`Unsupported interface type: ${walletProvider.interfaceType}`, 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: 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
if (!subOrganizationId) {
const signUpBody = buildSignUpBody({
createSubOrgParams: {
...createSubOrgParams,
apiKeys: [
{
apiKeyName: `wallet-auth:${publicKey}`,
publicKey: publicKey,
curveType: isEthereumProvider(walletProvider)
? "API_KEY_CURVE_SECP256K1"
: "API_KEY_CURVE_ED25519",
},
],
},
});
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 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 TurnkeyNetworkError(`Stamped request failed`, res.status, TurnkeyErrorCodes.WALLET_LOGIN_AUTH_ERROR, errorText);
}
const sessionResponse = await res.json();
const sessionToken = sessionResponse.activity.result.stampLoginResult?.session;
if (!sessionToken) {
throw new TurnkeyError("Session token not found in the response", TurnkeyErrorCodes.BAD_RESPONSE);
}
await this.storeSession({
sessionToken: sessionToken,
sessionKey,
});
return sessionToken;
}, {
errorCode: 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 TurnkeyError(`Failed to clean up generated key pair`, 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 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,
customMessageByMessages: {
"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).
* @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 withTurnkeyErrorHandling(async () => {
const verifyOtpRes = await this.httpClient.proxyVerifyOtp({
otpId: otpId,
otpCode: otpCode,
});
if (!verifyOtpRes) {
throw new TurnkeyError(`OTP verification failed`, TurnkeyErrorCodes.INTERNAL_ERROR);
}
const accountRes = await this.httpClient.proxyGetAccount({
filterType: OtpTypeToFilterTypeMap[otpType],
filterValue: contact,
});
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,
customMessageByMessages: {
"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.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 = SessionKey.DefaultSessionkey, } = params;
return withTurnkeyErrorHandling(async () => {
const res = await this.httpClient.proxyOtpLogin({
verificationToken,
publicKey: publicKey,
invalidateExisting,
});
if (!res) {
throw new TurnkeyError(`Auth proxy OTP login failed`, TurnkeyErrorCodes.OTP_LOGIN_ERROR);
}
const loginRes = await res;
if (!loginRes.session) {
throw new TurnkeyError("No session returned from OTP login", TurnkeyErrorCodes.OTP_LOGIN_ERROR);
}
await this.storeSession({
sessionToken: loginRes.session,
sessionKey,
});
return loginRes.session;
}, {
errorMessage: "Failed to log in with OTP",
errorCode: 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 TurnkeyError(`Failed to clean up generated key pair`, 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 = buildSignUpBody({
createSubOrgParams: {
...createSubOrgParams,
...(otpType === OtpType.Email
? { userEmail: contact }
: { userPhoneNumber: contact }),
verificationToken,
},
});
return withTurnkeyErrorHandling(async () => {
const generatedKeyPair = await this.apiKeyStamper?.createKeyPair();
const res = await this.httpClient.proxySignup(signUpBody);
if (!res) {
throw new TurnkeyError(`Auth proxy OTP sign up failed`, TurnkeyErrorCodes.OTP_SIGNUP_ERROR);
}
return await this.loginWithOtp({
verificationToken,
publicKey: generatedKeyPair,
...(invalidateExisting && { invalidateExisting }),
...(sessionKey && { sessionKey }),
});
}, {
errorCode: 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 with a sub-organization, it will complete the login flow.
* - Supports passing a custom public key for authentication, invalidating existing session, specifying a session key, and providing additional sub-organization creation parameters.
* - Handles both email and SMS OTP types.
*
* @param params.otpId - ID of the OTP to complete (returned from `initOtp`).
* @param params.otpCode - OTP code entered by the user.