@ideem/zsm-client-sdk 2.8.1 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,7 +12,18 @@ and Universal MFA into a browser application.
12
12
 
13
13
  ----
14
14
 
15
- ## Release 2.8.1 - Mat 8, 2026 [ NPM PACKAGE RELEASE ] **latest release**
15
+ ## Release 2.9.1 - June 18, 2026 [ NPM PACKAGE RELEASE ] **latest release**
16
+ - Minor bug fixes and improvements
17
+ - Provides a workaround for an issue in which certain browser extensions (e.g. credential managers) interfere with the webauthn flows by overriding the `navigator.credentials.get` method.
18
+ - Adds a new property to the data returned by `checkAllEnrollments()` called `isUsingPasswordManager`, which is a boolean that indicates whether or not the `navigator.credentials.get` method has been overridden by an external source (e.g. a browser extension like a password manager). This allows consumers to detect when their users are using password managers that may interfere with the webauthn flows, and adjust their UI/UX accordingly (e.g. by displaying a warning message about potential issues with password managers, or by providing additional guidance on how to use the SDK effectively in conjunction with password managers).
19
+
20
+ ## Release 2.9.0 - May 17, 2026
21
+ - Adds an optional `installation_id` override mechanism for bridging contexts:
22
+ - Rust extern `__ideem_get_install_id_override_v1` declared on `globalThis` via `#[wasm_bindgen(catch)]`, embedded in `WASMRustInterface.js` (which runs in the page context).
23
+ - Consumers install the JS-side getter at page scope: `globalThis.__ideem_get_install_id_override_v1 = () => value`.
24
+ - Non-bridging contexts unchanged — `InstallationID::get()` falls through to the existing IndexedDB-derived value when no override getter is installed (the `catch` arm in the extern handles the missing-getter case).
25
+
26
+ ## Release 2.8.1 - Mat 8, 2026 [ NPM PACKAGE RELEASE ]
16
27
  - Minor bug fixes and improvements
17
28
  - Alters Webauthn object to provide Passkeys+ support Android WebViews
18
29
 
package/ErrorHandler.js CHANGED
@@ -1,154 +1 @@
1
- /**
2
- * @file zsm-client-sdk/ErrorHandler.js
3
- * @name ErrorHandler
4
- * @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.
5
- * @param {ZSMAPI} zsmAPIInstance An optional instance of a ZSM API (e.g., WebAuthnClient) to facilitate local credential purging.
6
- * @setter {ZSMAPI} ERR.zsmAPI Sets the ZSM API instance (WebAuthnClient, etc.) to facilitate local credential purging.
7
- * @setter {string} ERR.userIdentifier Sets the active userIdentifier (used for local credential purging).
8
- * @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)
9
- * @method ERR.reset() Resets the sequential error count to zero.
10
- * @exports ERR An instance of the ErrorHandler singleton.
11
- *
12
- * @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.
13
- *
14
- * @notes IMPORTANT: the `reset()` method MUST be called to reset the sequential error count upon successful transaction with the RP (see: makePostRequest in RelyingPartyBase.js).
15
- *
16
- * @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.
17
- * @example ERR.zsmAPI = webAuthnClientInstance; // Set the ZSM API instance (e.g., WebAuthnClient)
18
- * @example ERR.userIdentifier = 'user123'; // Set the active userIdentifier
19
- * @example throw new Error("An example error."); // Generate and throw an error via the ErrorHandler
20
- *
21
- * @notes The sequential error count is persisted in sessionStorage to survive page reloads within the same browser session.
22
- *
23
- */
24
- class ErrorHandler {
25
- #sequentialErrorCount; // Number of sequential errors encountered
26
- #zsmAPIInstance = null; // Instance of ZSM API (e.g., WebAuthnClient)
27
- #activeUserIdentifier = null; // Currently active userIdentifier
28
-
29
- constructor(zsmAPIInstance=null) {
30
- try { this.#sequentialErrorCount = Number(sessionStorage.getItem('zsmSequentialErrorCt') || 0); } // Try to retrieve sequential error count from sessionStorage...
31
- catch { this.#sequentialErrorCount = 0; } // ...and roll over to 0 if unable to do so (or it's absent)
32
- this.#zsmAPIInstance = zsmAPIInstance || null; // Set ZSM API instance if provided, else null
33
- }
34
-
35
- get #count() { return this.#sequentialErrorCount; } // Get sequential error count
36
- set #count(v) { try { sessionStorage.setItem('zsmSequentialErrorCt', (this.#sequentialErrorCount = v)); }catch(e){} } // Set sequential error count and store in sessionStorage
37
- get #userIdentifier() { return this.#activeUserIdentifier; } // Get active userIdentifier
38
- set userIdentifier(id) { return (this.#activeUserIdentifier = id); } // Set active userIdentifier
39
- get #zsmAPI() { return this.#zsmAPIInstance; } // Get ZSM API instance
40
- set zsmAPI(instance) { return (this.#zsmAPIInstance = instance); } // Set ZSM API instance
41
-
42
- /**
43
- * @name purgeLocalCredentialForActiveUser
44
- * @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.
45
- * @throws {Promise<void>} Rollover functionality (full reset) is triggered if userIdentifier or zsmAPIInstance is unset, or unable to unbind active user
46
- * @returns {Promise<void>}
47
- * @memberOf ErrorHandler
48
- * @private
49
- * @async
50
- */
51
- async #purgeLocalCredentialForActiveUser() {
52
- try {
53
-
54
- const uid = this.#userIdentifier; // Use the active userIdentifier...
55
- if(!uid || uid === '') throw new Error(`No active userIdentifier is set on this ErrorHandler.`); // ...or throw an error if not set
56
- 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
57
- await this.#zsmAPI.unbindFromDevice(uid); // Attempt to unenroll the active user from the device via the WebAuthnClient
58
- console.info(`[ErrorHandler] :: Local ZSM credential data purged for ${uid}`);
59
- } catch (err) {
60
- console.warn(new Error(`[ErrorHandler] :: purgeLocalCredentialForActiveUser :: ${err.message}.`, err));
61
- console.warn(`\n\nBecause local credential purge failed, attempting to purge entire local IndexedDB instead.`);
62
- this.#purgeLocalIDB(); // If unable to unbind the active user, purge the entire IndexedDB instead
63
- }
64
- }
65
-
66
- /**
67
- * @name purgeLocalIDB
68
- * @description Attempts to purge the entire local IndexedDB database named 'ideem', removing all credential and ZSM data.
69
- * @returns {void}
70
- * @memberOf ErrorHandler
71
- * @private
72
- */
73
- #purgeLocalIDB() {
74
- try {
75
- indexedDB.deleteDatabase('ideem'); // Delete the entire 'ideem' IndexedDB database
76
- console.info(`[ErrorHandler] :: Local IndexedDB purged of all credential and ZSM data.`);
77
- } catch (err) {
78
- console.error(`[ErrorHandler] :: purgeLocalIDB :: Unable to purge local IndexedDB (usually this is due to the local store being absent).\nDetails:\n`, err);
79
- }
80
- }
81
-
82
- /**
83
- * @name processErrorCountsForGeneratedError
84
- * @description Processes the sequential error count after an error has been generated, triggering local credential or IndexedDB purging as necessary.
85
- * @param {Error} errOP The newly-generated or amended error object.
86
- * @returns {Error} The same error object passed in, for chaining.
87
- * @memberOf ErrorHandler
88
- * @private
89
- */
90
- #processErrorCountsForGeneratedError(errOP) {
91
- if(this.#count === 3) { // IF sequential error count is exactly 3...
92
- this.#purgeLocalCredentialForActiveUser(); // ...purge local credential for active user
93
- } else if(this.#count >= 5) { // ELSE IF sequential error count is 5 or more...
94
- this.#purgeLocalIDB(); // ...purge entire local IndexedDB
95
- this.reset(); // ...reset the sequential error count
96
- }
97
- return errOP; // Return the [new|amended] error object
98
- }
99
-
100
- /**
101
- * @name reset
102
- * @description Resets the sequential error count to zero and persists the change in sessionStorage.
103
- * @param {...any} args Optional arguments to pass through transparently.
104
- * @returns {void|any|Array<any>} For 0 args: undefined; 1 arg: that arg; multiple args: array of those args.
105
- * @memberOf ErrorHandler
106
- */
107
- reset(...args) {
108
- this.#count = 0; // Reset the sequential error count and store it in sessionStorage
109
- console.info(`[ErrorHandler] :: reset :: Sequential Error Count reset to 0.`); // Log the reset action
110
- return (typeof args === 'array' && args.length === 0) ? void(0) // Pass through received arguments transparently, if any
111
- : (typeof args === 'array' && args.length === 1) ? args[0]
112
- : args;
113
- }
114
-
115
- /**
116
- * @name error
117
- * @description Generates & returns an Error object, increments sequential error count, and triggers individual or global IDB record purging (by count)
118
- * @param {function|Error} errType Either an Error constructor function (e.g., Error, TypeError; counted) or existing Error object to amend (not counted).
119
- * @param {string} message The error message to associate with the generated or amended Error object.
120
- * @param {Object} options Optional additional options to pass to the Error constructor.
121
- * @returns {Error} The newly-generated or amended Error object.
122
- * @memberOf ErrorHandler
123
- * @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).
124
- */
125
- error(errType=Error, message="An error occurred.", options={}) {
126
- let errOP;
127
- try {
128
- if(typeof message !== 'string') message = "An error occurred."; // Ensure message is a string (or supplant it with a default)
129
- if(options == null || typeof options !== 'object') options = {}; // Ensure options is an object (or supplant it with an empty one)
130
- if(typeof errType === 'string') { // [NEW ERROR] Only the error message was provided as a string...
131
- message = errType; // ...so shift it into the message...
132
- errType = Error; // ...and set errType to the generic Error constructor
133
- } else if(typeof errType === 'function') { // [NEW ERROR] A constructor function was provided
134
- errOP = new errType(message, options); // Attempt to create the error object
135
- if(!(errOP instanceof Error)) { // IF the newly-created object is NOT an Error...
136
- errOP = new Error(message, options); // ...replace it with a generic Error object (still with message & options)
137
- }
138
- this.#count = this.#count + 1; // Increment the sequential error count
139
- } else if (errType instanceof Error) { // [EXISTING ERROR] An existing error object was provided
140
- errOP = new errType.constructor(message, options); // Create a new Error object of the previous's prototype...
141
- errOP.cause = errType; // ...and leverage its cause to preserve the causal stack trace
142
- } else { // [INVALID INPUT] Neither a constructor function nor an existing error object was provided
143
- throw new Error(`Invalid errType provided to ErrorHandler.error(): must be either an Error constructor function or an existing Error object.`);
144
- }
145
- return this.#processErrorCountsForGeneratedError(errOP); // Post-process the count and return the [new|amended] error object
146
- }catch(e) {
147
- this.#count = this.#count + 1; // Increment the sequential error count
148
- errOP = new Error(`[ErrorHandler] :: error :: An error occurred while attempting to process another error.\nDetails:\n${e.message}\n\nOriginal Error Message:\n${message}`);
149
- return this.#processErrorCountsForGeneratedError(errOP); // Post-process the count and return the rollover hail mary error object
150
- }
151
- }
152
- }
153
- const ERR = new ErrorHandler(); // Export a singleton instance of the ErrorHandler
154
- export default ERR
1
+ const r=new class{#r;#e=null;#t=null;constructor(r=null){try{this.#r=Number(sessionStorage.getItem("zsmSequentialErrorCt")||0)}catch{this.#r=0}this.#e=r||null}get#n(){return this.#r}set#n(r){try{sessionStorage.setItem("zsmSequentialErrorCt",this.#r=r)}catch(r){}}get#o(){return this.#t}set userIdentifier(r){return this.#t=r}get#s(){return this.#e}set zsmAPI(r){return this.#e=r}async#i(){try{const r=this.#o;if(!r||""===r)throw new Error("No active userIdentifier is set on this ErrorHandler.");if(null==this.#s)throw new Error("No ZSM API instance is associated with this ErrorHandler.");await this.#s.unbindFromDevice(r),console.info(`[ErrorHandler] :: Local ZSM credential data purged for ${r}`)}catch(r){console.warn(new Error(`[ErrorHandler] :: purgeLocalCredentialForActiveUser :: ${r.message}.`,r)),console.warn("\n\nBecause local credential purge failed, attempting to purge entire local IndexedDB instead."),this.#a()}}#a(){try{indexedDB.deleteDatabase("ideem"),console.info("[ErrorHandler] :: Local IndexedDB purged of all credential and ZSM data.")}catch(r){console.error("[ErrorHandler] :: purgeLocalIDB :: Unable to purge local IndexedDB (usually this is due to the local store being absent).\nDetails:\n",r)}}#c(r){return 3===this.#n?this.#i():this.#n>=5&&(this.#a(),this.reset()),r}reset(...r){return this.#n=0,console.info("[ErrorHandler] :: reset :: Sequential Error Count reset to 0."),"array"==typeof r&&0===r.length?void 0:"array"==typeof r&&1===r.length?r[0]:r}error(r=Error,e="An error occurred.",t={}){let n;try{if("string"!=typeof e&&(e="An error occurred."),null!=t&&"object"==typeof t||(t={}),"string"==typeof r)e=r,r=Error;else if("function"==typeof r)n=new r(e,t),n instanceof Error||(n=new Error(e,t)),this.#n=this.#n+1;else{if(!(r instanceof Error))throw new Error("Invalid errType provided to ErrorHandler.error(): must be either an Error constructor function or an existing Error object.");n=new r.constructor(e,t),n.cause=r}return this.#c(n)}catch(r){return this.#n=this.#n+1,n=new Error(`[ErrorHandler] :: error :: An error occurred while attempting to process another error.\nDetails:\n${r.message}\n\nOriginal Error Message:\n${e}`),this.#c(n)}}};export default r;
@@ -1,295 +1 @@
1
- import GLOBAL from './GlobalScoping.js';
2
-
3
- // baseStack is a Map that holds the initial state of the event stack. It is defaulted to those classes that are ALWAYS present in the ZSMClient package, and
4
- // constitutes the minimum set of classes that are both expected in ALL permutations of the SDK, and which are REQUIRED for the ZSMClient package to function.
5
- // Additional classes are added contextually (their status STARTING at "PENDING") when the client app imports them, and CAN remove a baseStack entry entirely.
6
- // Each event inside represents a class used by the NPM Package's "main" export - ZSMClientSDK - and tracks its status throughout its initialization lifecycle
7
- // (status include: UNREGISTERED, PENDING, READY, or ERROR). Each time a class's status changes, the event stack is updated, and, once all classes reach their
8
- // respective READY statuses, the ZSMClient package is considered fully initialized, and a custom event is dispatched to notify the client app that the SDK is
9
- // finished loading and is ready for use. Additional possible event stack entries (at time of this writing) include: WORKER, PERFORMANCELOGGER, PASSKEYCLIENT.
10
-
11
- let baseStack = new Map([
12
- ["SELECTEDCLIENT", "UNREGISTERED"], // Active client class - UMFAClient or FIDO2Client (dynamically set by client's import choice) - that's being imported
13
- ["RELYINGPARTY", "UNREGISTERED"], // The RelyingParty class (extended from RelyingPartyBase) used for registration/authentication calls to the RP Server
14
- ["WEBAUTHNCLIENT", "UNREGISTERED"] // The WebAuthnClient class (extended from WebAuthnClientBase) used to coordinate the MPCRound calls to the ZSM Server
15
- ]);
16
-
17
- // EventCoordinator is a class instance that both represents and manages the event stack, allows for dynamically updating class initialization statuses (along
18
- // with dispatching the relevant custom events associated with those changes), and provides a means for additional, plug-in NPM Packages' classes to be added,
19
- // monitored, and included in the overall ZSMClient package's readiness state. Note: the act of importing EventCoordinator (or any other package, really) will
20
- // initialize it as a singleton, so it manages its own state, and there is no concern if it is imported multiple times; it always refers to the same instance.
21
- class EventCoordinator {
22
- constructor(initialStack = baseStack) {
23
- this.coreSDKClass = null; // The core SDK class that is user-facing (if any; usually ZSMClientSDK)
24
- this._selectedClient = null; // The active customer-facing SDK client (UMFAClient or FIDO2Client)
25
- this._eventStack = new Map(initialStack); // Clone the initialStack provided to ensure it isn't modified by reference
26
- this.cleanState = new Map(initialStack); // Store the initial state of the event stack as cleanState, so it can be reset later
27
-
28
- // Even if the GLOBAL scope does not support event dispatches, while we will not be able to dispatch events, we can still manage the app's event stack
29
- this.scopeSupportsEventDispatch = (typeof GLOBAL.dispatchEvent === 'function' && typeof CustomEvent === 'function');
30
- }
31
- get supported() { return this.scopeSupportsEventDispatch; } // Returns true/false if GLOBAL scope supports events
32
- get statusOptions() { return ['UNREGISTERED', 'PENDING', 'READY', 'ERROR']; } // Returns the possible status options for events
33
- get stack() { return this._eventStack; } // Returns the current event stack as a Map
34
- get length() { return [...this._eventStack.keys()].length; } // Returns count of all events in the stack
35
- get keys() { return [...this._eventStack.keys()]; } // Returns array of event names in the stack
36
- get values() { return [...this._eventStack.values()]; } // Returns array of event statuses in the stack
37
- get entries() { return [...this._eventStack.entries()]; } // Returns array of [name, status] pairs in the stack
38
- get object() { return Object.fromEntries(this.entries); } // Returns the event stack as a plain object
39
- get unregistered() { return this.lengthByStatus('UNREGISTERED'); } // Returns count of events with status "UNREGISTERED"
40
- get pending() { return this.lengthByStatus('PENDING'); } // Returns count of events with status "PENDING"
41
- get ready() { return this.lengthByStatus('READY'); } // Returns count of events with status "READY"
42
- get error() { return this.lengthByStatus('ERROR'); } // Returns count of events with status "ERROR"
43
- get incomplete() { return this.length - this.ready; } // Returns count of events that are NOT "READY"
44
- get containsErrors() { return this.error > 0; } // Returns true/false if any events' status is "ERROR"
45
- get done() { return (this.ready === this.length); } // Returns true/false if all events are "READY"
46
- get client() { return this._selectedClient; } // Returns the currently selected client (UMFA/FIDO2)
47
- get status() { return this.containsErrors ? 'ERROR'
48
- : this.done ? 'READY'
49
- : !!this.unregistered ? 'INCOMPLETE REGISTRATIONS'
50
- : 'PENDING'; }
51
- set client(value) { this._selectedClient = value; } // Returns the currently selected client (UMFA/FIDO2)
52
- set coreReady(value) { this.coreSDKClass.ready = !!value; }
53
- set coreErrs(value) { this.coreSDKClass.containsErrors = !!value; }
54
-
55
- /**
56
- * @name setCoreSDKClass
57
- * @description Sets the core SDK class reference on the EventCoordinator instance, and defines its "finished" method.
58
- * @param {string} classDesc The name of the client class being initialized (UMFAClient or FIDO2Client).
59
- * @param {Object} classRef The reference to the core SDK class being initialized.
60
- * @returns {void}
61
- */
62
- setCoreSDKClass(classDesc, classRef) {
63
- const core = (this.coreSDKClass = classRef); // Set a reference to the core class on the singleton instance, then...
64
- core.ready = false; // ... set its default ready state to false
65
- core.containsErrors = false; // ... set its default containsErrors state to false
66
- Object.defineProperty(core, "finished", { // ... and define a "finished" function on the core object...
67
- value: this.finished.bind(this), // ... binding it to the method on this singleton instance
68
- enumerable: false, writable: false, configurable: false // ... and making it non-enumerable, non-writable, and non-configurable
69
- });
70
- this.specifyClient(classDesc); // ... finally, specify the client class that is being initialized.
71
- }
72
-
73
- /**
74
- * @name specifyClient
75
- * @description Specifies the active client class (UMFAClient or FIDO2Client) being initialized.
76
- * @param {string} selectedClient The name of the client class being initialized (UMFAClient or FIDO2Client).
77
- * @returns {void}
78
- */
79
- specifyClient(selectedClient) {
80
- if (!selectedClient || typeof selectedClient !== 'string' || !selectedClient.length)
81
- throw new TypeError(`[ZSM EventCoordinator] :: specifyClient :: Invalid 'selectedClient' value: "${selectedClient}". 'selectedClient' is required and expects a String.`);
82
-
83
- if (this.client == null) {
84
- this._eventStack.delete('SELECTEDCLIENT');
85
- this.cleanState.delete('SELECTEDCLIENT');
86
- this.client = selectedClient.toUpperCase();
87
- }
88
- }
89
-
90
- /**
91
- * @name summarizeEventStack
92
- * @description Logs a summary of the current event stack to the console.
93
- * @param {...any} triggerData Additional data to include in the log (e.g., the event that triggered the update).
94
- * @returns {void}
95
- */
96
- summarizeEventStack (...triggerData) {
97
- let summaryObject = this.object,
98
- { client, containsErrors, done } = this;
99
-
100
- summaryObject[`counts (${this.length})`] = {
101
- total : this.length,
102
- unregistered : this.unregistered,
103
- pending : this.pending,
104
- ready : this.ready,
105
- error : this.error
106
- };
107
-
108
- let opFn = (typeof console?.l === 'function') ? console.l : console.log;
109
- opFn(`[ZSM EventCoordinator] :: Event Update!\n${(!!triggerData.length) ? `(ACTIVE EVENT: '${triggerData.join(': ')}')\n` : ''} Status summary:\n`, Object.assign({client, containsErrors, done}, summaryObject));
110
- }
111
-
112
- /**
113
- * @name dispatchEvent
114
- * @description Dispatches a custom event with the given name and detail.
115
- * @param {string} classDesc The name of the event to dispatch.
116
- * @param {Object} eventDetail The detail object to attach to the event.
117
- * @returns {void}
118
- */
119
- dispatchEvent(eventName, eventDetail={}) {
120
- if (!this.scopeSupportsEventDispatch) return;
121
- GLOBAL.dispatchEvent(new CustomEvent(eventName, { detail: eventDetail }));
122
- }
123
-
124
- /**
125
- * @name update
126
- * @description Updates the status of a specific event in the event stack.
127
- * @param {string} classDesc The name of the event to update.
128
- * @param {string} eventStatus The new status for the event (accepts: UNREGISTERED, PENDING, READY, ERROR).
129
- * @throws {TypeError} If the classDesc is invalid or eventStatus is not one of the allowed values.
130
- * @returns {Object} Returns the updated event stack object.
131
- */
132
- update (classDesc, eventStatus="PENDING") {
133
- if (!classDesc || typeof classDesc !== 'string' || !classDesc.length)
134
- throw new TypeError(`[ZSM EventCoordinator] :: update :: Invalid 'classDesc' value: "${classDesc}". 'classDesc' is required and expects a String.`);
135
-
136
- if(classDesc === 'SELECTEDCLIENT'){
137
- if(!this.client) throw new TypeError(`[ZSM EventCoordinator] :: update :: Status for 'SELECTEDCLIENT' cannot be updated until a client is specified!`);
138
- classDesc = this.client;
139
- }
140
- classDesc = classDesc.toUpperCase();
141
-
142
- eventStatus = (!eventStatus || typeof eventStatus !== 'string' || !eventStatus.length) ? "PENDING" : eventStatus.toUpperCase();
143
-
144
- if (!this.statusOptions.includes(eventStatus))
145
- throw new TypeError(`[ZSM EventCoordinator] :: update :: Invalid 'eventStatus' value: "${eventStatus}". Accepts: ${this.statusOptions.join(', ')}.`);
146
-
147
- this._eventStack.set(classDesc, eventStatus);
148
-
149
- if( this.containsErrors ) {
150
- const errorMsg = `[ZSM EventCoordinator] :: Initialization Error! :: The ZSMClient initialization contains errors and is unable to continue!]\nModules containing errors:\n - ${this.byStatus('ERROR').join('\n -')}.`;
151
- this.coreErrs = true; // Set the core SDK class error state to true
152
- if(window && window?.queueMicrotask) {
153
- queueMicrotask(() => this.dispatchEvent('ZSMClientError', {
154
- message: errorMsg,
155
- modules: this.byStatus('ERROR')
156
- }));
157
- }else this.dispatchEvent('ZSMClientError', { message: errorMsg, modules: this.object });
158
- console.error(errorMsg);
159
- return this.object; // don't throw here
160
- }
161
-
162
- if (this.done) {
163
- this.coreReady = true; // Set the core SDK class ready state to true
164
- if(window && window?.queueMicrotask) {
165
- queueMicrotask(() => this.dispatchEvent('ZSMClientReady', {
166
- ready: true
167
- }));
168
- }else this.dispatchEvent('ZSMClientReady', { ready: true });
169
- }
170
-
171
- if(GLOBAL.showEvents) this.summarizeEventStack(classDesc, eventStatus);
172
- return this.object;
173
- }
174
-
175
- /**
176
- * @name byStatus
177
- * @description Retrieves all event names that match a specific status.
178
- * @param {string} eventStatus The status to filter events by (accepts: UNREGISTERED, PENDING, READY, ERROR).
179
- * @throws {TypeError} If the eventStatus is invalid or not one of the allowed values.
180
- * @returns {Array} Returns an array of event names that match the specified status.
181
- */
182
- byStatus(eventStatus) {
183
- if (!eventStatus || typeof eventStatus !== 'string' || !eventStatus.length || !["UNREGISTERED", "PENDING", "READY", "ERROR"].includes(eventStatus))
184
- throw new Error(`[ZSM EventCoordinator] :: byStatus :: Invalid 'eventStatus' value: "${eventStatus}". Valid statuses are: UNREGISTERED, PENDING, READY, ERROR.`);
185
- return this.entries.flatMap(([k, v]) => v === eventStatus ? [k] : []);
186
- }
187
-
188
- /**
189
- * @name lengthByStatus
190
- * @description Returns the count of events that match a specific status.
191
- * @param {string} eventStatus The status to filter events by (accepts: UNREGISTERED, PENDING, READY, ERROR).
192
- * @throws {TypeError} If the eventStatus is invalid or not one of the allowed values.
193
- * @returns {number} Returns the count of events with the specified status.
194
- */
195
- lengthByStatus(eventStatus) {
196
- return this.byStatus(eventStatus).length || 0;
197
- }
198
-
199
- /**
200
- * @name reset
201
- * @description Resets the event stack to its initial state (cleanState).
202
- * @returns {Object} Returns the reset event stack object.
203
- */
204
- reset() {
205
- this.keys.forEach(key => this._eventStack.set(key, 'PENDING'));
206
-
207
- if(window && window?.queueMicrotask) {
208
- queueMicrotask(() => this.dispatchEvent('ZSMClientEventStackReset', { stack: this.object }));
209
- } else this.dispatchEvent('ZSMClientEventStackReset', { stack: this.object });
210
- return this.object;
211
- }
212
-
213
- /**
214
- * @name finished
215
- * @description Waits for the ZSMClient initialization to complete, either by checking the done state or by listening for the ZSMClientReady event.
216
- * @param {function|number} callbackFnOrTimeout A callback function to call when the initialization is done, or a timeout in milliseconds to wait before rejecting.
217
- * @param {function|any} callbackFnOrFirstArg An optional callback function to call when the initialization is done (if timeout is specified), or the first argument to pass to the callback function.
218
- * @param {array<any>} args Additional arguments to pass to the callback function.
219
- * @returns {Promise<boolean>} Resolves to true if the initialization is done, or rejects with an error if it takes too long or contains errors.
220
- * @throws {Error} If the initialization takes too long or contains errors.
221
- * @throws {Error} If some portion of the initialization fails.
222
- */
223
- async finished(callbackFnOrTimeout, callbackFnOrFirstArg, ...args) {
224
- if (this.done) return Promise.resolve(true);
225
-
226
- let timeoutID,
227
- callbackFn,
228
- onSDKReady,
229
- onSDKError,
230
- terminatePoll = false,
231
- timeoutDuration = 20000;
232
-
233
- if(!isNaN(callbackFnOrTimeout)) {
234
- timeoutDuration = callbackFnOrTimeout;
235
- if(typeof callbackFnOrFirstArg === 'function') {
236
- callbackFn = callbackFnOrFirstArg;
237
- }
238
- }else if(typeof callbackFnOrTimeout === 'function') {
239
- callbackFn = callbackFnOrTimeout;
240
- args.unshift(callbackFnOrFirstArg);
241
- }
242
-
243
- return new Promise((resolve, reject) => {
244
- onSDKReady = () => {
245
- terminatePoll = true;
246
- if(callbackFn) callbackFn(...args); // Call the callback function if provided
247
- resolve(true); // Resolve the promise with a value of true
248
- };
249
- onSDKError = (e) => {
250
- terminatePoll = true;
251
- reject(new Error(`[ZSM EventCoordinator] :: finished :: Initialization Error! :: ${e.message || e}\nModules containing errors:\n - ${this.byStatus('ERROR').join('\n -')}.`));
252
- };
253
- if (this.done) onSDKReady();
254
- if (this.containsErrors) onSDKError({ detail: { modules: this.byStatus('ERROR') } });
255
-
256
- const onTimeout = () => {
257
- terminatePoll = true;
258
- if (this.done) return;
259
- reject(new Error("[ZSM EventCoordinator] :: Timeout Error: ZSMClient initialization took too long to complete. Please check your network connection or server status."));
260
- };
261
-
262
- timeoutID = setTimeout(onTimeout, timeoutDuration);
263
-
264
- if (this.scopeSupportsEventDispatch) {
265
- GLOBAL.addEventListener('ZSMClientReady', onSDKReady, { once: true });
266
- GLOBAL.addEventListener('ZSMClientError', onSDKError, { once: true });
267
- } else {
268
- const poll = () => {
269
- if (this.done) {
270
- onSDKReady();
271
- } else if (this.containsErrors) {
272
- onSDKError("[ZSM EventCoordinator] :: finished :: Error: ZSMClient initialization contains errors and is unable to continue.");
273
- }
274
- else if (!terminatePoll) setTimeout(poll, 200);
275
- };
276
- poll();
277
- }
278
- })
279
- .catch(err => {
280
- throw new Error(`[ZSM EventCoordinator] :: finished :: Initialization Error! :: ${err.message || err}\nModules containing errors:\n - ${this.byStatus('ERROR').join('\n -')}.`);
281
- })
282
- .finally(() => {
283
- clearTimeout(timeoutID);
284
- if (this.scopeSupportsEventDispatch) {
285
- GLOBAL.removeEventListener('ZSMClientReady', onSDKReady);
286
- GLOBAL.removeEventListener('ZSMClientError', onSDKError);
287
- }
288
- });
289
- }
290
- }
291
-
292
- const eventCoordinator = new EventCoordinator(); // Create a singleton instance of EventCoordinator
293
-
294
- export default eventCoordinator;
295
- export { eventCoordinator };
1
+ import t from"./GlobalScoping.js";let e=new Map([["SELECTEDCLIENT","UNREGISTERED"],["RELYINGPARTY","UNREGISTERED"],["WEBAUTHNCLIENT","UNREGISTERED"]]);const n=new class{constructor(n=e){this.coreSDKClass=null,this._selectedClient=null,this._eventStack=new Map(n),this.cleanState=new Map(n),this.scopeSupportsEventDispatch="function"==typeof t.dispatchEvent&&"function"==typeof CustomEvent}get supported(){return this.scopeSupportsEventDispatch}get statusOptions(){return["UNREGISTERED","PENDING","READY","ERROR"]}get stack(){return this._eventStack}get length(){return[...this._eventStack.keys()].length}get keys(){return[...this._eventStack.keys()]}get values(){return[...this._eventStack.values()]}get entries(){return[...this._eventStack.entries()]}get object(){return Object.fromEntries(this.entries)}get unregistered(){return this.lengthByStatus("UNREGISTERED")}get pending(){return this.lengthByStatus("PENDING")}get ready(){return this.lengthByStatus("READY")}get error(){return this.lengthByStatus("ERROR")}get incomplete(){return this.length-this.ready}get containsErrors(){return this.error>0}get done(){return this.ready===this.length}get client(){return this._selectedClient}get status(){return this.containsErrors?"ERROR":this.done?"READY":this.unregistered?"INCOMPLETE REGISTRATIONS":"PENDING"}set client(t){this._selectedClient=t}set coreReady(t){this.coreSDKClass.ready=!!t}set coreErrs(t){this.coreSDKClass.containsErrors=!!t}setCoreSDKClass(t,e){const n=this.coreSDKClass=e;n.ready=!1,n.containsErrors=!1,Object.defineProperty(n,"finished",{value:this.finished.bind(this),enumerable:!1,writable:!1,configurable:!1}),this.specifyClient(t)}specifyClient(t){if(!t||"string"!=typeof t||!t.length)throw new TypeError(`[ZSM EventCoordinator] :: specifyClient :: Invalid 'selectedClient' value: "${t}". 'selectedClient' is required and expects a String.`);null==this.client&&(this._eventStack.delete("SELECTEDCLIENT"),this.cleanState.delete("SELECTEDCLIENT"),this.client=t.toUpperCase())}summarizeEventStack(...t){let e=this.object,{client:n,containsErrors:s,done:i}=this;e[`counts (${this.length})`]={total:this.length,unregistered:this.unregistered,pending:this.pending,ready:this.ready,error:this.error},("function"==typeof console?.l?console.l:console.log)(`[ZSM EventCoordinator] :: Event Update!\n${t.length?`(ACTIVE EVENT: '${t.join(": ")}')\n`:""} Status summary:\n`,Object.assign({client:n,containsErrors:s,done:i},e))}dispatchEvent(e,n={}){this.scopeSupportsEventDispatch&&t.dispatchEvent(new CustomEvent(e,{detail:n}))}update(e,n="PENDING"){if(!e||"string"!=typeof e||!e.length)throw new TypeError(`[ZSM EventCoordinator] :: update :: Invalid 'classDesc' value: "${e}". 'classDesc' is required and expects a String.`);if("SELECTEDCLIENT"===e){if(!this.client)throw new TypeError("[ZSM EventCoordinator] :: update :: Status for 'SELECTEDCLIENT' cannot be updated until a client is specified!");e=this.client}if(e=e.toUpperCase(),n=n&&"string"==typeof n&&n.length?n.toUpperCase():"PENDING",!this.statusOptions.includes(n))throw new TypeError(`[ZSM EventCoordinator] :: update :: Invalid 'eventStatus' value: "${n}". Accepts: ${this.statusOptions.join(", ")}.`);if(this._eventStack.set(e,n),this.containsErrors){const t=`[ZSM EventCoordinator] :: Initialization Error! :: The ZSMClient initialization contains errors and is unable to continue!]\nModules containing errors:\n - ${this.byStatus("ERROR").join("\n -")}.`;return this.coreErrs=!0,window&&window?.queueMicrotask?queueMicrotask(()=>this.dispatchEvent("ZSMClientError",{message:t,modules:this.byStatus("ERROR")})):this.dispatchEvent("ZSMClientError",{message:t,modules:this.object}),console.error(t),this.object}return this.done&&(this.coreReady=!0,window&&window?.queueMicrotask?queueMicrotask(()=>this.dispatchEvent("ZSMClientReady",{ready:!0})):this.dispatchEvent("ZSMClientReady",{ready:!0})),t.showEvents&&this.summarizeEventStack(e,n),this.object}byStatus(t){if(!t||"string"!=typeof t||!t.length||!["UNREGISTERED","PENDING","READY","ERROR"].includes(t))throw new Error(`[ZSM EventCoordinator] :: byStatus :: Invalid 'eventStatus' value: "${t}". Valid statuses are: UNREGISTERED, PENDING, READY, ERROR.`);return this.entries.flatMap(([e,n])=>n===t?[e]:[])}lengthByStatus(t){return this.byStatus(t).length||0}reset(){return this.keys.forEach(t=>this._eventStack.set(t,"PENDING")),window&&window?.queueMicrotask?queueMicrotask(()=>this.dispatchEvent("ZSMClientEventStackReset",{stack:this.object})):this.dispatchEvent("ZSMClientEventStackReset",{stack:this.object}),this.object}async finished(e,n,...s){if(this.done)return Promise.resolve(!0);let i,r,o,a,E=!1,c=2e4;return isNaN(e)?"function"==typeof e&&(r=e,s.unshift(n)):(c=e,"function"==typeof n&&(r=n)),new Promise((e,n)=>{o=()=>{E=!0,r&&r(...s),e(!0)},a=t=>{E=!0,n(new Error(`[ZSM EventCoordinator] :: finished :: Initialization Error! :: ${t.message||t}\nModules containing errors:\n - ${this.byStatus("ERROR").join("\n -")}.`))},this.done&&o(),this.containsErrors&&a({detail:{modules:this.byStatus("ERROR")}});if(i=setTimeout(()=>{E=!0,this.done||n(new Error("[ZSM EventCoordinator] :: Timeout Error: ZSMClient initialization took too long to complete. Please check your network connection or server status."))},c),this.scopeSupportsEventDispatch)t.addEventListener("ZSMClientReady",o,{once:!0}),t.addEventListener("ZSMClientError",a,{once:!0});else{const t=()=>{this.done?o():this.containsErrors?a("[ZSM EventCoordinator] :: finished :: Error: ZSMClient initialization contains errors and is unable to continue."):E||setTimeout(t,200)};t()}}).catch(t=>{throw new Error(`[ZSM EventCoordinator] :: finished :: Initialization Error! :: ${t.message||t}\nModules containing errors:\n - ${this.byStatus("ERROR").join("\n -")}.`)}).finally(()=>{clearTimeout(i),this.scopeSupportsEventDispatch&&(t.removeEventListener("ZSMClientReady",o),t.removeEventListener("ZSMClientError",a))})}};export default n;export{n as eventCoordinator};
package/FIDO2Client.js CHANGED
@@ -1,15 +1 @@
1
- import eventCoordinator from './EventCoordinator.js';
2
- import FIDO2ClientBase from './FIDO2ClientBase.js';
3
-
4
- class FIDO2Client extends FIDO2ClientBase {
5
- /**
6
- * Constructs a FIDO2Client object by extending the FIDO2ClientBase class.
7
- * @param {Object} config - The configuration for ZSM initialization (ref: ./zsm_app_config.json)
8
- */
9
- constructor(config) {
10
- super(config);
11
- eventCoordinator.update('FIDO2Client', 'READY');
12
- }
13
- }
14
-
15
- export {FIDO2Client};
1
+ import t from"./EventCoordinator.js";import o from"./FIDO2ClientBase.js";class r extends o{constructor(o){super(o),t.update("FIDO2Client","READY")}}export{r as FIDO2Client};