@turnkey/core
Version:
A core JavaScript web and React Native package for interfacing with Turnkey's infrastructure.
804 lines (801 loc) • 217 kB
JavaScript
'use strict';
var sdkClientBase = require('../__generated__/sdk-client-base.js');
var sdkTypes = require('@turnkey/sdk-types');
var auth = require('../__types__/auth.js');
var enums = require('../__types__/enums.js');
var utils = require('../utils.js');
var base = require('../__storage__/base.js');
var base$1 = require('../__stampers__/api/base.js');
var base$2 = require('../__stampers__/passkey/base.js');
var turnkeyHelpers = require('../turnkey-helpers.js');
var jwtDecode = require('jwt-decode');
var base$3 = require('../__wallet__/base.js');
var ethers = require('ethers');
var crypto = require('@turnkey/crypto');
var apiKeyStamper = require('@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$1, 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 sdkClientBase.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 utils.withTurnkeyErrorHandling(async () => {
if (!this.passkeyStamper) {
throw new sdkTypes.TurnkeyError("Passkey stamper is not initialized", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
}
const name = utils.isValidPasskeyName(nameFromParams || `passkey-${Date.now()}`);
let passkey;
if (utils.isWeb()) {
const res = await this.passkeyStamper.createWebPasskey({
publicKey: {
user: {
name,
displayName: name,
},
...(challenge && { challenge }),
},
});
if (!res) {
throw new sdkTypes.TurnkeyError("Failed to create Web passkey", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
}
passkey = {
encodedChallenge: res?.encodedChallenge,
attestation: res?.attestation,
};
}
else if (utils.isReactNative()) {
const res = await this.passkeyStamper.createReactNativePasskey({
name,
displayName: name,
});
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,
customErrorsByMessages: {
"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).
* @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 utils.withTurnkeyErrorHandling(async () => {
generatedPublicKey =
params?.publicKey || (await this.apiKeyStamper?.createKeyPair());
const sessionKey = params?.sessionKey || enums.SessionKey.DefaultSessionkey;
const expirationSeconds = params?.expirationSeconds || auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
if (!generatedPublicKey) {
throw new sdkTypes.TurnkeyError("A publickey could not be found or generated.", sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
}
const sessionResponse = await this.httpClient.stampLogin({
publicKey: generatedPublicKey,
organizationId: params?.organizationId ?? this.config.organizationId,
expirationSeconds,
}, enums.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: sdkTypes.TurnkeyErrorCodes.PASSKEY_LOGIN_AUTH_ERROR,
customErrorsByMessages: {
"timed out or was not allowed": {
message: "Passkey login was cancelled by the user.",
code: sdkTypes.TurnkeyErrorCodes.SELECT_PASSKEY_CANCELLED,
},
},
}, {
finallyFn: async () => {
if (generatedPublicKey) {
try {
await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
}
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.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 = auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = enums.SessionKey.DefaultSessionkey, organizationId, } = params || {};
let generatedPublicKey = undefined;
return utils.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 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-${generatedPublicKey}`,
publicKey: generatedPublicKey,
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?.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: sdkTypes.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 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.fetchWalletProviders = 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 with the connected wallet's address.
* @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);
}
return await this.walletManager.connector.connectWalletAccount(walletProvider);
}, {
errorMessage: "Unable to connect wallet account",
errorCode: sdkTypes.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: sdkTypes.TurnkeyErrorCodes.WALLET_CONNECT_EXPIRED,
},
"User rejected the request": {
message: "Connect wallet was cancelled by the user.",
code: sdkTypes.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 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 `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 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 === enums.WalletSource.Embedded) {
throw new sdkTypes.TurnkeyError("You can only switch chains for connected wallet accounts", sdkTypes.TurnkeyErrorCodes.NOT_FOUND);
}
const providers = walletProviders ?? (await this.fetchWalletProviders());
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,
});
};
/**
* 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 || auth.DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
let generatedPublicKey = 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);
}
const futureSessionPublicKey = providedPublicKey ??
(generatedPublicKey = await this.apiKeyStamper?.createKeyPair());
if (!futureSessionPublicKey) {
throw new sdkTypes.TurnkeyError("Failed to find or generate a public key for building the wallet login request", sdkTypes.TurnkeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR);
}
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 utils.withTurnkeyErrorHandling(async () => {
return this.httpClient.stampStampLogin({
publicKey: futureSessionPublicKey,
organizationId: this.config.organizationId,
expirationSeconds,
}, enums.StamperType.Wallet);
}, {
errorMessage: "Failed to create stamped request for wallet login",
errorCode: sdkTypes.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: sdkTypes.TurnkeyErrorCodes.WALLET_CONNECT_EXPIRED,
},
"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);
}
// the wallet's public key is embedded in the stamp header by the wallet stamper
// so we extract it from there
const publicKey = utils.getPublicKeyFromStampHeader(signedRequest.stamp.stampHeaderValue);
return {
signedRequest,
publicKey,
};
}, {
errorCode: sdkTypes.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 sdkTypes.TurnkeyError(`Failed to clean up generated key pair`, sdkTypes.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 = enums.SessionKey.DefaultSessionkey } = params;
return utils.withTurnkeyErrorHandling(async () => {
const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params);
const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest);
const sessionToken = sessionResponse.session;
if (!sessionToken) {
throw new sdkTypes.TurnkeyError("Session token not found in the response", sdkTypes.TurnkeyErrorCodes.BAD_RESPONSE);
}
await this.storeSession({
sessionToken: sessionResponse.session,
sessionKey,
});
return {
sessionToken: sessionResponse.session,
address: utils.addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey),
};
}, {
errorMessage: "Unable to log in with the provided wallet",
errorCode: sdkTypes.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 = enums.SessionKey.DefaultSessionkey, } = params;
return utils.withTurnkeyErrorHandling(async () => {
const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params);
const signUpBody = utils.buildSignUpBody({
createSubOrgParams: {
...createSubOrgParams,
apiKeys: [
{
apiKeyName: `wallet-auth:${publicKey}`,
publicKey: publicKey,
curveType: utils.getCurveTypeFromProvider(walletProvider),
},
],
},
});
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 Turnkey
const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest);
const sessionToken = sessionResponse.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: sessionToken,
appProofs: res.appProofs,
address: utils.addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey),
};
}, {
errorMessage: "Failed to sign up with wallet",
errorCode: sdkTypes.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 = enums.SessionKey.DefaultSessionkey, } = params;
return utils.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: enums.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
let signupRes;
if (!subOrganizationId) {
const signUpBody = utils.buildSignUpBody({
createSubOrgParams: {
...createSubOrgParams,
apiKeys: [
{
apiKeyName: `wallet-auth:${publicKey}`,
publicKey: publicKey,
curveType: utils.getCurveTypeFromProvider(walletProvider),
},
],
},
});
signupRes = await this.httpClient.proxySignup(signUpBody);
if (!signupRes) {
throw new sdkTypes.TurnkeyError(`Sign up failed`, sdkTypes.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 sdkTypes.TurnkeyError("Session token not found in the response", sdkTypes.TurnkeyErrorCodes.BAD_RESPONSE);
}
await this.storeSession({
sessionToken: sessionToken,
sessionKey,
});
return {
sessionToken: sessionToken,
appProofs: signupRes?.appProofs,
address: utils.addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey),
// if the subOrganizationId exists, it means the user is logging in
action: subOrganizationId ? sdkTypes.AuthAction.LOGIN : sdkTypes.AuthAction.SIGNUP,
};
}, {
errorCode: sdkTypes.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 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,
customErrorsByMessages: {
"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).
* @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 utils.withTurnkeyErrorHandling(async () => {
const verifyOtpRes = await this.httpClient.proxyVerifyOtp({
otpId: otpId,
otpCode: otpCode,
publicKey: resolvedPublicKey,
});
if (!verifyOtpRes) {
throw new sdkTypes.TurnkeyError(`OTP verification failed`, sdkTypes.TurnkeyErrorCodes.INTERNAL_ERROR);
}
const accountRes = await this.httpClient.proxyGetAccount({
filterType: enums.OtpTypeToFilterTypeMap[otpType],
filterValue: contact,
verificationToken: verifyOtpRes.verificationToken,
});
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,
customErrorsByMessages: {
"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.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 = enums.SessionKey.DefaultSessionkey, } = params;
return utils.withTurnkeyErrorHandling(async () => {
const { message, publicKey: clientSignaturePublicKey } = utils.getClientSignatureMessageForLogin({
verificationToken,
sessionPublicKey: publicKey,
});
this.apiKeyStamper?.setTemporaryPublicKey(publicKey);