@ideem/zsm-client-sdk 2.5.0 → 2.6.0

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.
@@ -0,0 +1,154 @@
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 trannsaction 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
@@ -103,6 +103,7 @@ class FIDO2ClientBase {
103
103
  * @memberOf FIDO2ClientBase
104
104
  */
105
105
  async webauthnGet (userIdentifier) {
106
+
106
107
  try {
107
108
  if(userIdentifier == null
108
109
  || userIdentifier === '') throw new Error(`[FIDO2Client] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
@@ -2,6 +2,7 @@ import eventCoordinator from './EventCoordinator.js';
2
2
  import GLOBAL from './GlobalScoping.js';
3
3
  import {uid2iid} from './IdentityIndexing.js';
4
4
 
5
+
5
6
  eventCoordinator.update('RelyingPartyBase');
6
7
 
7
8
  function tracePrimer (obj=false) {
@@ -33,7 +34,7 @@ class RelyingPartyBase {
33
34
  }
34
35
 
35
36
  get userIdentifier() { return this.customerDefinedIdentifier; }
36
- set userIdentifier(v) { return this.customerDefinedIdentifier = v; }
37
+ set userIdentifier(v) { return userIdentifier = this.customerDefinedIdentifier = v; }
37
38
 
38
39
  /**
39
40
  * @name makePostRequest
@@ -48,7 +49,7 @@ class RelyingPartyBase {
48
49
  url = `${this.host.endsWith('/') ? this.host.slice(0,-1) : this.host}/${url.startsWith('/') ? url.slice(1) : url}`;
49
50
  body = ((typeof body === 'object') ? body : {});
50
51
  if(this.appEnvironment) body = Object.assign(body, {environment: this.appEnvironment});
51
- body = Object.assign({ customer_defined_identifier : this.customerDefinedIdentifier, application_id : this.applicationId }, body, this.addlRelyingPartyBodyProps, tracePrimer(true));
52
+ body = Object.assign({ customer_defined_identifier : this.customerDefinedIdentifier, application_id : this.applicationId}, body, this.addlRelyingPartyBodyProps, tracePrimer(true));
52
53
  let fetchObj = {
53
54
  method,
54
55
  headers : Object.assign(this.xhrHeaders, ((!headers?.Authorization) ? {'Authorization': `Bearer ${this.apiKey}`} : {}), headers),
@@ -92,8 +93,10 @@ class RelyingPartyBase {
92
93
  * @returns {Promise<Object>} Resolves with publicKey object used in the subsequent Crypto Server API call.
93
94
  */
94
95
  registrationStart () {
95
- return this.makePostRequest("api/webauthn/registration/start")
96
+
97
+ return this.makePostRequest(`api/webauthn/registration/start`)
96
98
  .then(data => {
99
+
97
100
  if(data.identity_id){
98
101
  GLOBAL.mpcConfig.consumer_id = data.identity_id;
99
102
  uid2iid(this.userIdentifier, data.identity_id);
@@ -101,7 +104,7 @@ class RelyingPartyBase {
101
104
  return (this.publicKey = data.ccr.publicKey)
102
105
  })
103
106
  .then(result => result)
104
- .catch(error => Promise.reject((error instanceof Error) ? error : new Error(error)));
107
+ .catch(error => Promise.reject(new Error(error)));
105
108
  }
106
109
  /**
107
110
  * @name registrationFinish
@@ -111,10 +114,12 @@ class RelyingPartyBase {
111
114
  * @throws {Error} Throws an error if the request fails.
112
115
  */
113
116
  registrationFinish (credential) {
114
- return this.makePostRequest("api/webauthn/registration/finish", { credential })
115
- .then(() => (this.credentialID = credential.rawId, credential))
116
- .then(credential => credential)
117
- .catch(error => (this.clearEnrollmentCredentials(), Promise.reject((error instanceof Error) ? error : new Error(error))))
117
+ return this.makePostRequest(`api/webauthn/registration/finish`, { credential })
118
+ .then(body => {
119
+ this.credentialID = credential.rawId;
120
+ return {credential, response: body, ...body};
121
+ })
122
+ .catch(error => (this.clearEnrollmentCredentials(), Promise.reject(new Error(error))));
118
123
 
119
124
  }
120
125
 
@@ -125,13 +130,21 @@ class RelyingPartyBase {
125
130
  * @returns {Promise<Object>} Resolves with the authentication challenge.
126
131
  * @throws {Error} Throws an error if the request fails.
127
132
  */
128
- authenticationStart (credential_id=this.credentialID) {
133
+ authenticationStart (credential_id=this.credentialID, credential_type = 'passkey', chain_handoff, chain_credential_id, chain_challenge) {
134
+ const body = {
135
+ credential_type,
136
+ credential_id,
137
+ chain_handoff,
138
+ chain_credential_id,
139
+ chain_challenge,
140
+ }
129
141
  return (credential_id != null)
130
- ? this.makePostRequest( "api/webauthn/authentication/start", { credential_id })
142
+ ? this.makePostRequest( `api/webauthn/authentication/start`, { credential_id })
131
143
  .then(result => result)
132
- : (Promise.reject(new Error("[RelyingParty] :: authenticationStart :: No user ID or credential ID provided")))
144
+ : (Promise.reject(new Error("[RelyingParty] :: authenticationStart :: No user ID or credential ID provided")));
133
145
  }
134
146
 
147
+
135
148
  /**
136
149
  * @name authenticationFinish
137
150
  * @description Completes the authentication process by sending the credential to the server.
@@ -142,17 +155,24 @@ class RelyingPartyBase {
142
155
  authenticationFinish (credential) {
143
156
  return this.makePostRequest("api/webauthn/authentication/finish", { credential })
144
157
  .then(result => result)
145
- .catch(error => Promise.reject((error instanceof Error) ? error : new Error(error)));
158
+ .catch(error => Promise.reject(new Error(error)));
146
159
  }
147
160
 
161
+
148
162
  /**
149
163
  * @name createIdentityThenRegistrationStart
150
164
  * @description UMFA-OPTIMIZED FLOW: Creates a server-side Identity and then starts the WebAuthn Registration challenge automatically.
151
165
  * @returns {Promise<Object>} Resolves with an object containing the publicKey and identity_id.
152
166
  * @throws {Error} Throws an error if the request fails.
153
167
  */
154
- createIdentityThenRegistrationStart() {
155
- return this.makePostRequest("api/webauthn/registration/create-identity-then-start")
168
+ createIdentityThenRegistrationStart(credential_type = 'zsm', chain_handoff, chain_credential_id, chain_challenge) {
169
+ const body = {
170
+ credential_type,
171
+ chain_handoff,
172
+ chain_credential_id,
173
+ chain_challenge,
174
+ };
175
+ return this.makePostRequest("api/webauthn/registration/create-identity-then-start", body)
156
176
  .then(data => {
157
177
  if(data.identity_id){
158
178
  GLOBAL.mpcConfig.consumer_id = data.identity_id;
@@ -161,7 +181,7 @@ class RelyingPartyBase {
161
181
  return {publicKey:(this.publicKey = data.ccr.publicKey), identity_id:data.identity_id}
162
182
  })
163
183
  .then(result => result)
164
- .catch(error => Promise.reject((error instanceof Error) ? error : new Error(error)));
184
+ .catch(error => Promise.reject(new Error(error)));
165
185
  }
166
186
 
167
187
  /**
@@ -172,10 +192,10 @@ class RelyingPartyBase {
172
192
  * @throws {Error} Throws an error if the request fails.
173
193
  */
174
194
  registrationFinishAuthenticationStart (credential) {
175
- return this.makePostRequest("api/webauthn/registration/finish-then-authentication-start", { credential })
195
+ return this.makePostRequest(`api/webauthn/registration/finish-then-authentication-start`, { credential })
176
196
  .then(result => (this.credentialID = credential.rawId, result))
177
197
  .then(credential => credential)
178
- .catch(error => (this.clearEnrollmentCredentials(), Promise.reject((error instanceof Error) ? error : new Error(error))));
198
+ .catch(error => (this.clearEnrollmentCredentials(), Promise.reject(new Error(error))));
179
199
  }
180
200
 
181
201
  /**
@@ -189,7 +209,7 @@ class RelyingPartyBase {
189
209
  createNewIdentity = createNewIdentity ? { create_new_identity: "true" } : {};
190
210
  return this.makePostRequest("api/umfa/check-identity", createNewIdentity)
191
211
  .then(result => result)
192
- .catch(error => Promise.reject(error));
212
+ .catch(error => Promise.reject(new Error(error)));
193
213
  }
194
214
  }
195
215
  //## END RelyingPartyBase =====================================================================================================================================
package/UMFAClientBase.js CHANGED
@@ -8,7 +8,6 @@ class UMFAClientBase {
8
8
  * @param {Object} config - The configuration for ZSM initialization (ref: ./zsm_app_config.json)
9
9
  */
10
10
  constructor(config) {
11
- if(eventCoordinator.done) eventCoordinator.reset();
12
11
  eventCoordinator.update('UMFAClientBase');
13
12
  this.config = config;
14
13
  const WebAuthnClient = zsmPluginManager.classes('WEBAUTHNCLIENT');
@@ -24,20 +23,33 @@ class UMFAClientBase {
24
23
  get userIdentifier() { return this.zsmAPI.userIdentifier; }
25
24
  get credentialID() { return this.zsmAPI.credentialID; }
26
25
 
26
+ /**
27
+ * @name checkIdentity
28
+ * @description Pass-through method to call WebAuthnClient's checkIdentity
29
+ * @param {string} userIdentifier The identifier for the user
30
+ * @param {boolean} primeEnroll Whether to create a new identity if it doesn't exist
31
+ * @returns {Promise<Object>} Returns the results of zsmAPI.checkIdentity
32
+ * @memberOf FIDO2ClientBase, WebAuthnClient
33
+ */
34
+ async checkIdentity (userIdentifier, primeEnroll) { return this.zsmAPI.checkIdentity(userIdentifier, primeEnroll); }
35
+
27
36
  /**
28
37
  * @name checkEnrollment
29
38
  * @description Checks if a ZSM credential is enrolled on the device for the specified user
30
39
  * @param {string} userIdentifier The identifier for the user
40
+ * @param {string} targetData The type of data to retrieve ('id' for credential ID, 'all' for full details)
31
41
  * @returns {Promise<Object>|boolean} Resolves with the enrollment details or false if not enrolled, or an error if checking fails
32
42
  * @throws {Error} If the userIdentifier is not provided or is empty
33
43
  * @throws {Error} If an error occurs while checking enrollment
34
44
  * @memberOf UMFAClientBase
35
45
  */
36
- async checkEnrollment(userIdentifier) {
46
+ async checkEnrollment(userIdentifier, targetData='id') {
37
47
  try {
38
48
  if(userIdentifier == null
39
49
  || userIdentifier === '') throw new Error(`[UMFAClient] :: checkEnrollment :: A userIdentifier String is required! Received: ${userIdentifier}.`);
40
- return await this.zsmAPI.webauthnRetrieve(userIdentifier);
50
+ const enrollmentStatus = await this.zsmAPI.webauthnRetrieve(userIdentifier, targetData);
51
+
52
+ return enrollmentStatus;
41
53
  } catch(e) {
42
54
  return new Error(e.message || e || '[UMFAClient] :: checkEnrollment :: An error occurred while attempting to check enrollment.');
43
55
  }
@@ -54,20 +66,20 @@ class UMFAClientBase {
54
66
  * @memberOf UMFAClientBase
55
67
  */
56
68
  async enroll(userIdentifier) {
57
- try {
69
+
70
+
71
+ // try {
58
72
  if(userIdentifier == null
59
73
  || userIdentifier === '') throw new Error(`[UMFAClient] :: enroll :: A userIdentifier String is required! Received: ${userIdentifier}.`);
60
-
61
- const userIsEnrolled = await this.checkEnrollment(userIdentifier);
62
- if(userIsEnrolled !== false) return false;
74
+ const resultOfCheck = await this.zsmAPI.checkIdentity(userIdentifier, true);
75
+
76
+ const createResult = await this.zsmAPI.webauthnCreate(userIdentifier);
63
77
 
64
- const createResult = await this.zsmAPI.webauthnCreateThenPartialGet(userIdentifier);
65
78
  if(createResult instanceof Error) throw(`[UMFAClient] :: enroll :: Unable to associate ${userIdentifier}'s identity with device profile.\nDetails:\n${createResult}`);
66
- if(!createResult?.credential) throw(`[UMFAClient] :: enroll :: Unable to acquire authentication token for ${userIdentifier}!`);
67
- return createResult.credential;
68
- }catch(e) {
69
- return new Error(e.message || e || '[UMFAClient] :: enroll :: An error occurred during enrollment.');
70
- }
79
+ return createResult;
80
+ // }catch(e) {
81
+ // return new Error(e.message || e || '[UMFAClient] :: enroll :: An error occurred during enrollment.');
82
+ // }
71
83
  }
72
84
 
73
85
  /**