UNPKG

@ideem/zsm-client-sdk

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

155 lines (146 loc) 15 kB
/** * @file zsm-client-sdk/ErrorHandler.js * @name ErrorHandler * @description A centralized error handling singleton for managing sequential errors in ZSM client SDK operations, intended to facilitate self-healing from repeated Relying Party server operation errors. * @param {ZSMAPI} zsmAPIInstance An optional instance of a ZSM API (e.g., WebAuthnClient) to facilitate local credential purging. * @setter {ZSMAPI} ERR.zsmAPI Sets the ZSM API instance (WebAuthnClient, etc.) to facilitate local credential purging. * @setter {string} ERR.userIdentifier Sets the active userIdentifier (used for local credential purging). * @method ERR.error(errType, message, options) Generates & returns an Error object, increments sequential error count, and triggers individual or global IDB record purging (by count) * @method ERR.reset() Resets the sequential error count to zero. * @exports ERR An instance of the ErrorHandler singleton. * * @notes IMPORTANT: If unable to unbind the active user at 3+ errors, or either setter value is unset, OR the count reaches 5, the entire ideem IndexedDB gets purged. * * @notes IMPORTANT: the `reset()` method MUST be called to reset the sequential error count upon successful trannsaction with the RP (see: makePostRequest in RelyingPartyBase.js). * * @notes IMPORTANT: This class relies on a valid userIdentifier and zsmAPIInstance being set PRIOR to invoking the error() method to facilitate local credential purging when necessary. * @example ERR.zsmAPI = webAuthnClientInstance; // Set the ZSM API instance (e.g., WebAuthnClient) * @example ERR.userIdentifier = 'user123'; // Set the active userIdentifier * @example throw new Error("An example error."); // Generate and throw an error via the ErrorHandler * * @notes The sequential error count is persisted in sessionStorage to survive page reloads within the same browser session. * */ class ErrorHandler { #sequentialErrorCount; // Number of sequential errors encountered #zsmAPIInstance = null; // Instance of ZSM API (e.g., WebAuthnClient) #activeUserIdentifier = null; // Currently active userIdentifier constructor(zsmAPIInstance=null) { try { this.#sequentialErrorCount = Number(sessionStorage.getItem('zsmSequentialErrorCt') || 0); } // Try to retrieve sequential error count from sessionStorage... catch { this.#sequentialErrorCount = 0; } // ...and roll over to 0 if unable to do so (or it's absent) this.#zsmAPIInstance = zsmAPIInstance || null; // Set ZSM API instance if provided, else null } get #count() { return this.#sequentialErrorCount; } // Get sequential error count set #count(v) { try { sessionStorage.setItem('zsmSequentialErrorCt', (this.#sequentialErrorCount = v)); }catch(e){} } // Set sequential error count and store in sessionStorage get #userIdentifier() { return this.#activeUserIdentifier; } // Get active userIdentifier set userIdentifier(id) { return (this.#activeUserIdentifier = id); } // Set active userIdentifier get #zsmAPI() { return this.#zsmAPIInstance; } // Get ZSM API instance set zsmAPI(instance) { return (this.#zsmAPIInstance = instance); } // Set ZSM API instance /** * @name purgeLocalCredentialForActiveUser * @description Attempts to purge the local ZSM credential data for the currently active userIdentifier via the associated ZSM API instance. Failing that, purges the entire local IndexedDB database. * @throws {Promise<void>} Rollover functionality (full reset) is triggered if userIdentifier or zsmAPIInstance is unset, or unable to unbind active user * @returns {Promise<void>} * @memberOf ErrorHandler * @private * @async */ async #purgeLocalCredentialForActiveUser() { try { const uid = this.#userIdentifier; // Use the active userIdentifier... if(!uid || uid === '') throw new Error(`No active userIdentifier is set on this ErrorHandler.`); // ...or throw an error if not set if(this.#zsmAPI == null) throw new Error(`No ZSM API instance is associated with this ErrorHandler.`); // Throw an error if no WebAuthnClient instance is yet set await this.#zsmAPI.unbindFromDevice(uid); // Attempt to unenroll the active user from the device via the WebAuthnClient console.info(`[ErrorHandler] :: Local ZSM credential data purged for ${uid}`); } catch (err) { console.warn(new Error(`[ErrorHandler] :: purgeLocalCredentialForActiveUser :: ${err.message}.`, err)); console.warn(`\n\nBecause local credential purge failed, attempting to purge entire local IndexedDB instead.`); this.#purgeLocalIDB(); // If unable to unbind the active user, purge the entire IndexedDB instead } } /** * @name purgeLocalIDB * @description Attempts to purge the entire local IndexedDB database named 'ideem', removing all credential and ZSM data. * @returns {void} * @memberOf ErrorHandler * @private */ #purgeLocalIDB() { try { indexedDB.deleteDatabase('ideem'); // Delete the entire 'ideem' IndexedDB database console.info(`[ErrorHandler] :: Local IndexedDB purged of all credential and ZSM data.`); } catch (err) { console.error(`[ErrorHandler] :: purgeLocalIDB :: Unable to purge local IndexedDB (usually this is due to the local store being absent).\nDetails:\n`, err); } } /** * @name processErrorCountsForGeneratedError * @description Processes the sequential error count after an error has been generated, triggering local credential or IndexedDB purging as necessary. * @param {Error} errOP The newly-generated or amended error object. * @returns {Error} The same error object passed in, for chaining. * @memberOf ErrorHandler * @private */ #processErrorCountsForGeneratedError(errOP) { if(this.#count === 3) { // IF sequential error count is exactly 3... this.#purgeLocalCredentialForActiveUser(); // ...purge local credential for active user } else if(this.#count >= 5) { // ELSE IF sequential error count is 5 or more... this.#purgeLocalIDB(); // ...purge entire local IndexedDB this.reset(); // ...reset the sequential error count } return errOP; // Return the [new|amended] error object } /** * @name reset * @description Resets the sequential error count to zero and persists the change in sessionStorage. * @param {...any} args Optional arguments to pass through transparently. * @returns {void|any|Array<any>} For 0 args: undefined; 1 arg: that arg; multiple args: array of those args. * @memberOf ErrorHandler */ reset(...args) { this.#count = 0; // Reset the sequential error count and store it in sessionStorage console.info(`[ErrorHandler] :: reset :: Sequential Error Count reset to 0.`); // Log the reset action return (typeof args === 'array' && args.length === 0) ? void(0) // Pass through received arguments transparently, if any : (typeof args === 'array' && args.length === 1) ? args[0] : args; } /** * @name error * @description Generates & returns an Error object, increments sequential error count, and triggers individual or global IDB record purging (by count) * @param {function|Error} errType Either an Error constructor function (e.g., Error, TypeError; counted) or existing Error object to amend (not counted). * @param {string} message The error message to associate with the generated or amended Error object. * @param {Object} options Optional additional options to pass to the Error constructor. * @returns {Error} The newly-generated or amended Error object. * @memberOf ErrorHandler * @notes This method is designed to be resilient as hell; if anything's wrong with its invocation, it will STILL return a valid Error object (albeit a generic one). */ error(errType=Error, message="An error occurred.", options={}) { let errOP; try { if(typeof message !== 'string') message = "An error occurred."; // Ensure message is a string (or supplant it with a default) if(options == null || typeof options !== 'object') options = {}; // Ensure options is an object (or supplant it with an empty one) if(typeof errType === 'string') { // [NEW ERROR] Only the error message was provided as a string... message = errType; // ...so shift it into the message... errType = Error; // ...and set errType to the generic Error constructor } else if(typeof errType === 'function') { // [NEW ERROR] A constructor function was provided errOP = new errType(message, options); // Attempt to create the error object if(!(errOP instanceof Error)) { // IF the newly-created object is NOT an Error... errOP = new Error(message, options); // ...replace it with a generic Error object (still with message & options) } this.#count = this.#count + 1; // Increment the sequential error count } else if (errType instanceof Error) { // [EXISTING ERROR] An existing error object was provided errOP = new errType.constructor(message, options); // Create a new Error object of the previous's prototype... errOP.cause = errType; // ...and leverage its cause to preserve the causal stack trace } else { // [INVALID INPUT] Neither a constructor function nor an existing error object was provided throw new Error(`Invalid errType provided to ErrorHandler.error(): must be either an Error constructor function or an existing Error object.`); } return this.#processErrorCountsForGeneratedError(errOP); // Post-process the count and return the [new|amended] error object }catch(e) { this.#count = this.#count + 1; // Increment the sequential error count errOP = new Error(`[ErrorHandler] :: error :: An error occurred while attempting to process another error.\nDetails:\n${e.message}\n\nOriginal Error Message:\n${message}`); return this.#processErrorCountsForGeneratedError(errOP); // Post-process the count and return the rollover hail mary error object } } } const ERR = new ErrorHandler(); // Export a singleton instance of the ErrorHandler export default ERR