@ideem/zsm-react-native
Version:
ZSM makes 2FA easy and invisible for everyone, all the time, using advanced cryptography like MPC to establish cryptographic proof of the origin of any transaction or login attempt, while eliminating opportunities for social engineering. ZSM has no relian
611 lines (502 loc) • 30.7 kB
JavaScript
import RelyingParty from './relying-party';
import ZSMLogger from './zsm-logger';
import { version as packageVersion } from '../package.json';
class WebAuthnClientBase {
static get version() {
return packageVersion;
}
#config = {
"application_id" : "",
"api_key" : "",
"host_url" : "https://zsm-authenticator-demo.useideem.com/",
"application_environment" : "TEST",
"installation_description" : (typeof window !== 'undefined' ? window?.navigator?.userAgent : ''),
"log_level" : 'Error',
"request_timeout_ms" : 30000,
"retry_count" : 0,
"consumer_id" : "",
};
#relyingParty = undefined;
#zsmApi = undefined;
#identityId = undefined;
#useIdentityIdForMpc = false; // Track if this credential was created with identity_id
/**
* Constructs a WebAuthnClient object.
* @param {Object} config - The configuration for ZSM initialization.
*/
constructor(config) {
// Handle different naming conventions for authenticator host
if(!config?.authenticator_host_url) {
config.authenticator_host_url = config.authentication_host || config.host_url;
}
if(!config?.zsm_host_url) {
config.zsm_host_url = config.host_url.replace("-authenticator", "");
}
config.host_url = config.zsm_host_url;
this.#config = Object.assign(this.#config, config);
// Configure ZSMLogger with log level from config
ZSMLogger.setLogLevel(this.#config.log_level);
ZSMLogger.info(`ZSM JavaScript client v${ZSMLogger.version} initialized successfully`);
ZSMLogger.info(`ZSM JavaScript client configured with log level: ${this.#config.log_level}`);
if(typeof zsmAppConfig !== 'undefined') zsmAppConfig = this.#config;
this.#relyingParty = new RelyingParty(this.#config.authenticator_host_url, this.#config.api_key, this.#config.application_id);
this.#zsmApi
this.initializationMutex = false;
// Perform automatic health check if enabled
if (this.#config.health_check === true) {
ZSMLogger.info('Automatic health check enabled, running on initialization...');
this.healthCheck().then(result => {
if (result.overall_status !== 'HEALTHY') {
ZSMLogger.warn(`Health check failed during initialization: ${JSON.stringify(result)}`);
} else {
ZSMLogger.info('Initialization health check passed');
}
}).catch(error => {
ZSMLogger.error(`Health check error during initialization: ${error.message}`);
});
}
}
get userIdentifier() { return this.#relyingParty.customerDefinedIdentifier; }
set userIdentifier(v) { return this.#relyingParty.customerDefinedIdentifier = v; }
get customerDefinedIdentifier() { return this.#relyingParty.customerDefinedIdentifier; }
set customerDefinedIdentifier(v) { return this.#relyingParty.customerDefinedIdentifier = v; }
get username() { return this.userIdentifier; }
set username(v) { return this.userIdentifier = v; }
get userID() { return this.userIdentifier; }
get token() { return this.#relyingParty.token; }
get credentialID() { return this.#relyingParty.credentialID; }
set credentialID(v) { return this.#relyingParty.credentialID = v; }
get identityId() { return this.#identityId; }
// Expose RelyingParty for testing
get _relyingParty() { return this.#relyingParty; }
get isEnrolled() { return !!this.credentialID }
// Get the appropriate consumer_id for MPC operations
// CRITICAL: Use identity_id for MPC operations when available (required for Android)
// iOS handles this mapping natively, but Android needs identity_id as consumer_id for MPC
#getMpcConsumerId() {
const result = this.#identityId || this.#config.consumer_id;
ZSMLogger.trace(`[MPC-CONSUMER-ID] identity_id: ${this.#identityId}, consumer_id: ${this.#config.consumer_id}, returning: ${result} (using identity_id for MPC when available)`);
return result;
}
// Get a config object with the correct consumer_id for MPC operations
#getMpcConfig() {
return { ...this.#config, consumer_id: this.#getMpcConsumerId() };
}
/**
* Initializes the ZSM module for the specified username.
* @param {string} userIdentifier - The username to initialize ZSM for.
* @returns {Promise<void>} Resolves when ZSM is initialized.
*/
initializeZsm = async (userIdentifier=this.userIdentifier) => {
const traceId = ZSMLogger.generateTraceId();
ZSMLogger.trace(`initializeZsm called with userIdentifier: ${userIdentifier}`, traceId);
ZSMLogger.trace(`[INIT-BEFORE] userIdentifier: ${this.userIdentifier}`, traceId);
ZSMLogger.trace(`[INIT-BEFORE] relyingParty.customerDefinedIdentifier: ${this.#relyingParty.customerDefinedIdentifier}`, traceId);
// Update the base consumer_id (used for storage) but keep identity_id separate
this.#config.consumer_id = this.userIdentifier = userIdentifier;
// CRITICAL: When switching users, lookup the correct identity_id for the new user
const storedIdentityId = await this.#relyingParty.lookupIdentityMapping(userIdentifier);
if (storedIdentityId !== userIdentifier) {
// Found a stored identity mapping - use it
this.#identityId = storedIdentityId;
ZSMLogger.trace(`[INIT-USER-SWITCH] Found stored identity_id: ${storedIdentityId} for user: ${userIdentifier}`, traceId);
} else {
// No stored mapping - clear any cached identity_id
this.#identityId = undefined;
ZSMLogger.trace(`[INIT-USER-SWITCH] No stored identity_id for user: ${userIdentifier}, cleared cache`, traceId);
}
ZSMLogger.trace(`[INIT-AFTER] userIdentifier: ${this.userIdentifier}`, traceId);
ZSMLogger.trace(`[INIT-AFTER] relyingParty.customerDefinedIdentifier: ${this.#relyingParty.customerDefinedIdentifier}`, traceId);
ZSMLogger.trace(`[INIT-AFTER] MPC consumer_id will be: ${this.#getMpcConsumerId()}`, traceId);
ZSMLogger.trace(`[INIT-AFTER] Identity ID is: ${this.#identityId}`, traceId);
ZSMLogger.trace(`[INIT-AFTER] Storage consumer_id (preserved): ${this.#config.consumer_id}`, traceId);
if(typeof zsmAppConfig !== 'undefined') zsmAppConfig = this.#config;
// Just configure if ZSM API already exists, otherwise initialize fully
// Use MPC config for native layer (includes identity_id when available)
if (this.#zsmApi) {
await this.#zsmApi.configure(this.#getMpcConfig());
return Promise.resolve(this.#zsmApi);
} else {
this.status="ZSM_INITIALIZING";
let rawAPI = await this.getZsmApi(this.#getMpcConfig());
return Promise.resolve(this.#zsmApi = rawAPI);
}
}
//override INHERITED METHODS
getZsmApi = async (config) => {
Promise.reject(new Error('Method not implemented'));
}
/**
* Performs a comprehensive health check on both ZSM and authenticator hosts.
* @returns {Promise<Object>} Resolves with health status for all configured endpoints.
*/
healthCheck = async () => {
const traceId = ZSMLogger.generateTraceId();
try {
ZSMLogger.info('Starting comprehensive health check...', traceId);
const results = {
timestamp: new Date().toISOString(),
version: this.version,
configuration: {
zsm_host_url: this.#config.host_url,
authenticator_host_url: this.#config.authenticator_host_url,
application_id: this.#config.application_id || 'NOT_SET',
api_key: this.#config.api_key ? '***configured***' : 'NOT_SET',
application_environment: this.#config.application_environment
},
health_checks: {}
};
// Health check for authenticator host (via RelyingParty)
ZSMLogger.trace('Checking authenticator host...', traceId);
results.health_checks.authenticator = await this.#relyingParty.healthCheck();
// Health check for main ZSM host
ZSMLogger.trace('Checking main ZSM host...', traceId);
try {
const zsmHealthUrl = `${this.#config.host_url.endsWith('/') ? this.#config.host_url.slice(0,-1) : this.#config.host_url}/api/health`;
const zsmResponse = await fetch(zsmHealthUrl, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
let zsmHealthData;
try {
const responseText = await zsmResponse.text();
if (!responseText || responseText.trim() === '') {
zsmHealthData = { error: 'Empty response from ZSM health endpoint' };
} else {
zsmHealthData = JSON.parse(responseText);
}
} catch (e) {
ZSMLogger.error(`Failed to parse ZSM health response: ${e.message}`);
zsmHealthData = { error: `Failed to parse response: ${e.message}` };
}
results.health_checks.zsm_main = {
host: this.#config.host_url,
healthEndpoint: zsmHealthUrl,
healthStatus: zsmResponse.ok ? 'OK' : 'FAILED',
healthResponse: zsmHealthData,
httpStatus: zsmResponse.status,
timestamp: new Date().toISOString()
};
} catch (error) {
results.health_checks.zsm_main = {
host: this.#config.host_url,
healthStatus: 'ERROR',
error: error.message,
timestamp: new Date().toISOString()
};
}
// Test API key and app ID validation
ZSMLogger.trace('Testing API credentials...', traceId);
try {
const testResult = await this.#relyingParty.checkServerSideIdentity(false);
results.health_checks.api_credentials = {
status: 'OK',
test_endpoint: 'api/umfa/check-identity',
response: testResult
};
} catch (error) {
results.health_checks.api_credentials = {
status: 'FAILED',
test_endpoint: 'api/umfa/check-identity',
error: error.message
};
}
// Overall health summary
const allChecks = Object.values(results.health_checks);
const failedChecks = allChecks.filter(check =>
check.healthStatus === 'FAILED' || check.healthStatus === 'ERROR' || check.status === 'FAILED'
);
results.overall_status = failedChecks.length === 0 ? 'HEALTHY' : 'UNHEALTHY';
results.failed_checks = failedChecks.length;
results.total_checks = allChecks.length;
ZSMLogger.info(`Health check complete: ${results.overall_status}`, traceId);
ZSMLogger.trace(`Full health check results: ${JSON.stringify(results)}`, traceId);
return results;
} catch (error) {
const errorResult = {
timestamp: new Date().toISOString(),
overall_status: 'ERROR',
error: error.message,
configuration: {
zsm_host_url: this.#config.host_url,
authenticator_host_url: this.#config.authenticator_host_url
}
};
ZSMLogger.error(`Health check failed: ${error.message}`);
return errorResult;
}
}
toObject = (inp) => {
const traceId = ZSMLogger.generateTraceId();
if (inp instanceof Map) {
let obj = {};
inp.forEach((v, k) => obj[k] = this.toObject(v));
return obj;
} else if (Array.isArray(inp)) {
return inp.map(this.toObject.bind(this));
} else if (inp instanceof Object) {
let obj = {};
for (let k in inp) obj[k] = this.toObject(inp[k]);
return obj;
} else {
return inp;
}
}
/**
* Retrieves a user's identity_id and can create a new one if specified
* @param {string} userIdentifier - The userIdentifier.
* @param {boolean} createNewIdentity Create a new identity if one does not already exist
* @returns {Promise<Object>} Resolves with the retrieved identity if it exists.
*/
checkIdentity = async (userIdentifier, createNewIdentity=false) => {
try {
this.status='CHECKING_IDENTITY_STARTING';
await this.initializeZsm(userIdentifier);
const resultOfCheck = await this.#relyingParty.checkServerSideIdentity(createNewIdentity);
if(!resultOfCheck) throw new Error('Unable to communicate with identity server!');
const identityID = resultOfCheck.identity_id;
if(!identityID) throw new Error('Unable to acquire identity_id!');
// Store identity_id for MPC operations while preserving original consumer_id for storage
this.#identityId = identityID;
// CRITICAL: Store identity mapping to persistent storage (this was missing!)
await this.#relyingParty.handleServerIdentityResponse(resultOfCheck, userIdentifier);
const traceId = ZSMLogger.generateTraceId();
ZSMLogger.trace(`[CHECK-IDENTITY] Stored identity_id: ${this.#identityId}`, traceId);
ZSMLogger.trace(`[CHECK-IDENTITY] MPC config will use consumer_id: ${this.#getMpcConsumerId()}`, traceId);
ZSMLogger.trace(`[CHECK-IDENTITY] Storage config preserves consumer_id: ${this.#config.consumer_id}`, traceId);
// Reconfigure ZSM API with the new MPC config that includes identity_id
if (this.#zsmApi) {
await this.#zsmApi.configure(this.#getMpcConfig());
ZSMLogger.trace(`[CHECK-IDENTITY] ZSM API reconfigured with identity_id`, traceId);
}
return identityID;
} catch(err) {
this.status='CHECKING_IDENTITY_FAILED';
return Promise.reject(err);
}
}
/**
* Creates a new WebAuthn credential.
* @param {string} userIdentifier - The user identifier.
* @returns {Promise<Object>} Resolves with the created credential.
*/
webauthnCreate = async (userIdentifier, traceId = null) => {
this.status = 'ENROLLMENT_STARTING';
await this.initializeZsm(userIdentifier);
// Ensure identity exists (like web SDK does) before creating credentials
const actualTraceId = traceId || ZSMLogger.generateTraceId();
ZSMLogger.debug(`Ensuring identity exists for user: ${userIdentifier}`, actualTraceId);
await this.checkIdentity(userIdentifier, true);
const publicKey = await this.#relyingParty.registrationStart(actualTraceId);
if (!publicKey) throw new Error('[registration_start] webauthnCreate failed :: Unable to retrieve public key from Relying Party Server!');
ZSMLogger.trace('About to call #zsmApi.webauthn_create...', actualTraceId);
ZSMLogger.trace(`publicKey: ${JSON.stringify(publicKey)}`, actualTraceId);
ZSMLogger.trace(`traceId: ${actualTraceId}`, actualTraceId);
let createResponse = await this.#zsmApi.webauthn_create(publicKey, actualTraceId);
ZSMLogger.trace('Raw ZSM API response received', actualTraceId);
ZSMLogger.trace(`Raw response type: ${typeof createResponse}`, actualTraceId);
ZSMLogger.trace(`Raw response: ${JSON.stringify(createResponse)}`, actualTraceId);
if (!createResponse) throw new Error('[mpc_round] webauthnCreate failed :: Unable to obtain response from Crypto Server!');
ZSMLogger.trace('About to call toObject with createResponse...', actualTraceId);
createResponse = this.toObject(createResponse);
ZSMLogger.trace(`toObject result: ${JSON.stringify(createResponse)}`, actualTraceId);
if (!createResponse?.result) throw new Error('[mpc_round] webauthnCreate failed :: Unable to obtain credential from Crypto Server!');
if (!createResponse?.result?.response) throw new Error('[mpc_round] webauthnCreate failed :: Unable to acquire attestation object!');
const credential = createResponse.result;
const registrationResult = await this.#relyingParty.registrationFinish(credential, actualTraceId);
if (!registrationResult) throw new Error('[registration_finish] webauthnCreate failed :: Unable to verify credential Relying Party Server!');
return registrationResult;
};
/**
* Authenticates a user via WebAuthn.
* @param {string} userIdentifier - The user identifier.
* @returns {Promise<Object>} Resolves with the authentication result.
*/
webauthnGet = async (userIdentifier, traceId = null) => {
this.status = 'AUTHENTICATION_STARTING';
// Don't reinitialize - just ensure we have the right user set
if (this.userIdentifier !== userIdentifier) {
this.userIdentifier = userIdentifier;
}
const actualTraceId = traceId || ZSMLogger.generateTraceId();
ZSMLogger.debug(`webauthnGet called for user: ${userIdentifier}`, actualTraceId);
// webauthnGet retrieves credential ID before authentication (matches working web SDK pattern)
await this.webauthnRetrieve(userIdentifier, true, actualTraceId);
if (!this.isEnrolled) {
throw new Error(`Credential is missing for ${userIdentifier}`);
}
const authStartResponse = await this.#relyingParty.authenticationStart(this.credentialID, actualTraceId);
if (!authStartResponse) throw new Error('[authentication_start] webauthnGet failed :: Unable to start authentication with Relying Party Server!');
const pubKey = authStartResponse.rcr.publicKey;
if (!pubKey || !pubKey.challenge) throw new Error('[authentication_start] webauthnGet failed :: Unable to acquire challenge from public key!');
const response = this.toObject(await this.#zsmApi.webauthn_get(pubKey, actualTraceId));
if (!response?.result) throw new Error('[mpc_round] webauthnGet failed :: Unable to obtain credential from Crypto Server!');
const credential = response.result;
const authFinishResponse = await this.#relyingParty.authenticationFinish(credential, actualTraceId);
if (!authFinishResponse) throw new Error('[authentication_finish] webauthnGet failed :: Unable to verify credential Relying Party Server!');
if (!authFinishResponse.user_id || !authFinishResponse.token) throw new Error('[authentication_finish] webauthnGet failed :: User ID or token is missing from response body!');
return {
signature: credential.response.signature,
signedChallenge: credential.response.clientDataJSON,
user_id: authFinishResponse.user_id,
token: authFinishResponse.token,
challenge: pubKey.challenge,
credential,
};
};
/**
* Retrieves the credential ID using WebAuthn.
* @param {string} userIdentifier - The user identifier.
* @param {boolean} [silent=false] - Whether to suppress status updates.
* @returns {Promise<string|false>} Resolves with the credential ID or `false` if retrieval fails.
*/
webauthnRetrieve = async (userIdentifier, silent = false, traceId = null) => {
this.credentialID = null;
if (!silent) {
this.status = 'CREDENTIAL_RETRIEVING';
}
try {
// Don't reinitialize - just ensure we have the right user set
if (this.userIdentifier !== userIdentifier) {
this.userIdentifier = userIdentifier;
}
// CRITICAL: Implement iOS native pattern - always use original consumerId for storage retrieval
// This matches iOS webauthn_retrieve behavior (lines 181-186 in ZSMNativeModule.m)
const actualTraceId = traceId || ZSMLogger.generateTraceId();
const currentConsumerId = this.#config.consumer_id; // Original consumer_id for storage
ZSMLogger.trace(`[RETRIEVE] Using current consumerId for storage retrieval: ${currentConsumerId}`, actualTraceId);
ZSMLogger.trace(`[RETRIEVE] userIdentifier parameter: ${userIdentifier}`, actualTraceId);
ZSMLogger.trace(`[RETRIEVE] Current identity_id (for MPC only): ${this.#identityId}`, actualTraceId);
// Try retrieval with current consumerId first (like iOS does)
const response = this.toObject(await this.#zsmApi.webauthn_retrieve(currentConsumerId, actualTraceId));
if (response.error || !response?.result?.rawId) {
ZSMLogger.debug(`No credential found for userIdentifier: ${userIdentifier}`, actualTraceId);
return false;
}
this.credentialID = response.result.rawId;
ZSMLogger.debug(`Found credential with rawId: ${this.credentialID}`, actualTraceId);
if (!silent) {
this.status = 'CREDENTIAL_RETRIEVED';
}
return this.credentialID;
} catch (error) {
const actualTraceId = traceId || ZSMLogger.generateTraceId();
ZSMLogger.error(`webauthnRetrieve error: ${error.message}`, actualTraceId);
if (!silent) {
this.status = 'CREDENTIAL_RETRIEVAL_FAILED';
}
throw error;
}
};
/**
* Creates a new WebAuthn credential and immediately performs a partial get (optimized UMFA enrollment flow).
* @param {string} userIdentifier - The user identifier.
* @param {*} trace - Optional trace parameter.
* @returns {Promise<Object>} Resolves with the credential and authentication result.
*/
webauthnCreateThenPartialGet = async (userIdentifier, traceId = null) => {
this.status = 'ENROLLMENT_STARTING';
await this.initializeZsm(userIdentifier);
const actualTraceId = traceId || ZSMLogger.generateTraceId();
ZSMLogger.debug(`[OPTIMIZED-FLOW] Starting webauthnCreateThenPartialGet for user: ${userIdentifier}`, actualTraceId);
// OPTIMIZED FLOW: Use combined identity creation and registration start
const returnObj = await this.#relyingParty.createIdentityThenRegistrationStart(actualTraceId);
if (!returnObj) throw new Error('[createIdentityThenRegistrationStart] webauthnCreateThenPartialGet failed :: Unable to retrieve public key from Relying Party Server!');
const {publicKey, identity_id} = returnObj;
ZSMLogger.debug(`[OPTIMIZED-FLOW] Received identity_id: ${identity_id}`, actualTraceId);
// Store identity_id for MPC operations while preserving original consumer_id for storage
this.#identityId = identity_id;
ZSMLogger.trace(`[OPTIMIZED-FLOW] Storage consumer_id (preserved): ${this.#config.consumer_id}`, actualTraceId);
ZSMLogger.trace(`[OPTIMIZED-FLOW] MPC consumer_id will be: ${this.#getMpcConsumerId()}`, actualTraceId);
// Configure ZSM API with the new consumer_id (identity_id) for MPC operations
if (this.#zsmApi) {
await this.#zsmApi.configure(this.#getMpcConfig());
ZSMLogger.trace(`[OPTIMIZED-FLOW] ZSM API configured with identity_id: ${this.#identityId}`, actualTraceId);
}
ZSMLogger.trace('[OPTIMIZED-FLOW] About to call #zsmApi.webauthn_create...', actualTraceId);
ZSMLogger.trace(`[OPTIMIZED-FLOW] publicKey: ${JSON.stringify(publicKey)}`, actualTraceId);
let createResponse = await this.#zsmApi.webauthn_create(publicKey, actualTraceId);
ZSMLogger.trace('[OPTIMIZED-FLOW] Raw ZSM API response received', actualTraceId);
if (!createResponse) throw new Error('[mpc_round] webauthnCreateThenPartialGet failed :: Unable to obtain response from Crypto Server!');
createResponse = this.toObject(createResponse);
if (!createResponse?.result) throw new Error('[mpc_round] webauthnCreateThenPartialGet failed :: Unable to obtain credential from Crypto Server!');
const createCredential = createResponse.result;
// OPTIMIZED FLOW: Use combined registration finish and authentication start
let regFinishAuthStartResponse = await this.#relyingParty.registrationFinishAuthenticationStart(createCredential, actualTraceId);
if (!regFinishAuthStartResponse) throw new Error('[registrationFinishAuthenticationStart] webauthnCreateThenPartialGet failed :: Unable to retrieve response from Relying Party Server!');
regFinishAuthStartResponse = this.toObject(regFinishAuthStartResponse);
if (!regFinishAuthStartResponse?.rcr || !regFinishAuthStartResponse?.rcr?.publicKey) throw new Error('[registrationFinishAuthenticationStart] webauthnCreateThenPartialGet failed :: Unable to retrieve public key from Relying Party Server!');
const getChallenge = regFinishAuthStartResponse.rcr.publicKey;
// Store the credential ID from the created credential
this.credentialID = createCredential.rawId || createCredential.id;
ZSMLogger.trace('[OPTIMIZED-FLOW] About to call #zsmApi.webauthn_get...', actualTraceId);
let getResponse = await this.#zsmApi.webauthn_get(getChallenge, actualTraceId);
if (!getResponse) throw new Error('[mpc_round] webauthnGet failed :: Unable to obtain response from Crypto Server!');
getResponse = this.toObject(getResponse);
if (!getResponse?.result) throw new Error('[mpc_round] webauthnGet failed :: Unable to obtain credential from Crypto Server!');
const getCredential = getResponse.result;
// OPTIMIZED FLOW: Return partial result (no authenticationFinish call)
return {
registrationResult: createCredential,
credential: {
signature: getCredential.response.signature,
signedChallenge: getCredential.response.clientDataJSON,
user_id: userIdentifier,
challenge: getChallenge.challenge,
credential: getCredential,
}
};
};
/**
* Performs a partial WebAuthn get (optimized UMFA authentication flow).
* @param {string} userIdentifier - The user identifier.
* @param {*} trace - Optional trace parameter.
* @returns {Promise<Object>} Resolves with the authentication result.
*/
webauthnPartialGet = async (userIdentifier, traceId = null) => {
this.status = 'AUTHENTICATION_STARTING';
// Don't reinitialize - credential should already be set from enrollment
// Just ensure we have the right user set
if (this.userIdentifier !== userIdentifier) {
this.userIdentifier = userIdentifier;
}
const actualTraceId = traceId || ZSMLogger.generateTraceId();
ZSMLogger.debug(`[OPTIMIZED-AUTH] webauthnPartialGet called for user: ${userIdentifier}`, actualTraceId);
ZSMLogger.trace(`[OPTIMIZED-AUTH] Current identity_id: ${this.#identityId}`, actualTraceId);
ZSMLogger.trace(`[OPTIMIZED-AUTH] MPC consumer_id will be: ${this.#getMpcConsumerId()}`, actualTraceId);
// Ensure ZSM API is configured with correct consumer_id (identity_id if available)
if (this.#zsmApi && this.#identityId) {
await this.#zsmApi.configure(this.#getMpcConfig());
ZSMLogger.trace(`[OPTIMIZED-AUTH] ZSM API configured with identity_id: ${this.#identityId}`, actualTraceId);
}
// webauthnPartialGet does NOT call webauthnRetrieve (matches working web SDK pattern)
if (!this.isEnrolled) {
throw new Error(`Credential is missing for ${userIdentifier}`);
}
const getChallenge = await this.#relyingParty.authenticationStart(this.credentialID, actualTraceId);
if (!getChallenge) throw new Error('[authentication_start] webauthnPartialGet failed :: Unable to start authentication with Relying Party Server!');
const pubKey = getChallenge.rcr.publicKey;
if (!pubKey || !pubKey.challenge) throw new Error('[authentication_start] webauthnPartialGet failed :: Unable to retrieve public key from Relying Party Server!');
ZSMLogger.trace('[OPTIMIZED-AUTH] About to call #zsmApi.webauthn_get...', actualTraceId);
const getResponse = this.toObject(await this.#zsmApi.webauthn_get(pubKey, actualTraceId));
if (!getResponse?.result) throw new Error('[mpc_round] webauthnPartialGet failed :: Unable to obtain credential from Crypto Server!');
const getCredential = getResponse.result;
// webauthnPartialGet does NOT call authenticationFinish (returns partial results)
return {
signature: getCredential.response.signature,
signedChallenge: getCredential.response.clientDataJSON,
user_id: userIdentifier, // Return input userIdentifier, not from server response
challenge: getChallenge.challenge,
credential: getCredential,
};
};
/**
* Resets the WebAuthn credential for the user.
* @param {string} userIdentifier - The userIdentifier.
* @returns {Promise<void>} Resolves when the credential is reset.
*/
webauthnReset = () => this.#relyingParty.resetAll();
resetDevice = this.webauthnReset
}
// Export WebAuthnClient for browser fallback or custom usage
export default WebAuthnClientBase;