@ideem/zsm-client-sdk 2.4.1 → 2.5.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.
package/CHANGELOG.md CHANGED
@@ -1,21 +1,49 @@
1
1
  ZSM Client SDK for Browser Apps
2
- Latest version: v2.4.1
3
- Latest release date: May 5, 2025
2
+ Latest version: v2.5.0
3
+ Latest release date: Aug 29, 2025
4
4
 
5
5
  The ZSM Client SDK for Browser Apps provides the resources needed to integrate Ideem's FIDO2 Authenticator
6
6
  and Universal MFA into a browser application.
7
7
 
8
8
  ## Resources:
9
- - UMFA Documentation: https://docs.useideem.com/umfa_sdk/browser/index.html
10
- - FIDO2 Documentation: https://docs.useideem.com/fido2_sdk/browser/index.html
11
9
  - Support: support@useideem.com
12
10
 
13
11
  ## Known Issues
14
- - None
12
+ - A corner-case exists in which a developer, using both the core SDK packages as well as the Passkeys+ SDK, may encounter conflicts if a ZSM credential is re-enrolled after the Passkeys+ credential has been enrolled. Dev team is aware of this issue and a fix is forthcoming. A safeguard has been implemented to prevent this issue from occurring accidentally but, in the immediate term, developers are advised to avoid re-enrolling ZSM credentials after Passkeys+ credentials have been enrolled.
15
13
 
16
14
  ----
17
15
 
18
- ## Release 2.4.1 - May 5, 2025 [ NPM PACKAGE RELEASE ] **[ LATEST ]**
16
+ ## Release 2.5.0 - Aug 29, 2025 [ NPM PACKAGE RELEASE ] **[ LATEST ]**
17
+ - Significant re-architecture of the SDK to improve performance and maintainability (non-breaking, but may alter existing implementations, if plugins are used)
18
+ - Introduction of a new plugin system for easier integration of additional Ideem services
19
+ - Plugin system allows for dynamic loading and unloading of features
20
+ - Improved modularity and separation of concerns
21
+ - Enhanced error handling and recovery mechanisms
22
+ - Plugins can be easily added or removed without affecting the core SDK functionality
23
+ - Industry-leading implementation approach; no code changes or plugin registration required! Simply `import` the plugin and the functionality is available!
24
+ - Improved performance and reduced bundle size
25
+ - Advanced event coordination capabilities
26
+ - Fine-grained control over event propagation and handling
27
+ - Support for complex event workflows and dependencies
28
+ - Asynchronous loading of SDK and its constituent components and plugins
29
+ - Optional `async`/`await` support for class constructors
30
+ - Optional `finished()` method introduced for better async flow control
31
+ - Optional callback method support for SDK initialization
32
+ - Optional global-scoped events emitted during SDK initialization and lifecycle
33
+ - Relying Party improvements
34
+ - (RP) identification improvements
35
+ - (RP) configuration enhancements
36
+ - RP now single-instance when using Passkeys+; no need for secondary instance when using the same browser context
37
+ - Error handling, reporting, logging, and recovery improvements
38
+ - Centralized error handling and recovery mechanisms
39
+ - Extended support for custom error types and structured error information
40
+ - Normalized error responses across SDK components
41
+ - Significant improvements to documentation
42
+ - Significant updates to sample apps
43
+ - Automated shim polyfills introduced to simplify VanillaJS integrations using NPM packages (no more `importmap` configuration required!)
44
+ - Significant updates to build process and tooling
45
+
46
+ ## Release 2.4.1 - May 5, 2025
19
47
  - Minor bug fixes and improvements
20
48
  - Updates to documentation
21
49
  - Updates to sample apps
@@ -0,0 +1,267 @@
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
+ setCoreSDKClass(classDesc, classRef) {
56
+ const core = (this.coreSDKClass = classRef); // Set a reference to the core class on the singleton instance, then...
57
+ core.ready = false; // ... set its default ready state to false
58
+ core.containsErrors = false; // ... set its default containsErrors state to false
59
+ Object.defineProperty(core, "finished", { // ... and define a "finished" function on the core object...
60
+ value: this.finished.bind(this), // ... binding it to the method on this singleton instance
61
+ enumerable: false, writable: false, configurable: false // ... and making it non-enumerable, non-writable, and non-configurable
62
+ });
63
+ this.specifyClient(classDesc); // ... finally, specify the client class that is being initialized.
64
+ }
65
+
66
+ specifyClient(selectedClient) {
67
+ if (!selectedClient || typeof selectedClient !== 'string' || !selectedClient.length)
68
+ throw new TypeError(`[ZSM EventCoordinator] :: specifyClient :: Invalid 'selectedClient' value: "${selectedClient}". 'selectedClient' is required and expects a String.`);
69
+
70
+ if (this.client == null) {
71
+ this._eventStack.delete('SELECTEDCLIENT');
72
+ this.cleanState.delete('SELECTEDCLIENT');
73
+ this.client = selectedClient.toUpperCase();
74
+ }
75
+ }
76
+
77
+ summarizeEventStack (...triggerData) {
78
+ let summaryObject = this.object,
79
+ { client, containsErrors, done } = this;
80
+
81
+ summaryObject[`counts (${this.length})`] = {
82
+ total : this.length,
83
+ unregistered : this.unregistered,
84
+ pending : this.pending,
85
+ ready : this.ready,
86
+ error : this.error
87
+ };
88
+
89
+ let opFn = (typeof console?.l === 'function') ? console.l : console.log;
90
+ opFn(`[ZSM EventCoordinator] :: Event Update!\n${(!!triggerData.length) ? `(ACTIVE EVENT: '${triggerData.join(': ')}')\n` : ''} Status summary:\n`, Object.assign({client, containsErrors, done}, summaryObject));
91
+ }
92
+
93
+ /**
94
+ * @name dispatchEvent
95
+ * @description Dispatches a custom event with the given name and detail.
96
+ * @param {string} classDesc The name of the event to dispatch.
97
+ * @param {Object} eventDetail The detail object to attach to the event.
98
+ * @returns {void}
99
+ */
100
+ dispatchEvent(eventName, eventDetail={}) {
101
+ if (!this.scopeSupportsEventDispatch) return;
102
+ GLOBAL.dispatchEvent(new CustomEvent(eventName, { detail: eventDetail }));
103
+ }
104
+
105
+ update (classDesc, eventStatus="PENDING") {
106
+ if (!classDesc || typeof classDesc !== 'string' || !classDesc.length)
107
+ throw new TypeError(`[ZSM EventCoordinator] :: update :: Invalid 'classDesc' value: "${classDesc}". 'classDesc' is required and expects a String.`);
108
+
109
+ if(classDesc === 'SELECTEDCLIENT'){
110
+ if(!this.client) throw new TypeError(`[ZSM EventCoordinator] :: update :: Status for 'SELECTEDCLIENT' cannot be updated until a client is specified!`);
111
+ classDesc = this.client;
112
+ }
113
+ classDesc = classDesc.toUpperCase();
114
+
115
+ eventStatus = (!eventStatus || typeof eventStatus !== 'string' || !eventStatus.length) ? "PENDING" : eventStatus.toUpperCase();
116
+
117
+ if (!this.statusOptions.includes(eventStatus))
118
+ throw new TypeError(`[ZSM EventCoordinator] :: update :: Invalid 'eventStatus' value: "${eventStatus}". Accepts: ${this.statusOptions.join(', ')}.`);
119
+
120
+ this._eventStack.set(classDesc, eventStatus);
121
+
122
+ if( this.containsErrors ) {
123
+ 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 -')}.`;
124
+ this.coreErrs = true; // Set the core SDK class error state to true
125
+ if(window && window?.queueMicrotask) {
126
+ queueMicrotask(() => this.dispatchEvent('ZSMClientError', {
127
+ message: errorMsg,
128
+ modules: this.byStatus('ERROR')
129
+ }));
130
+ }else this.dispatchEvent('ZSMClientError', { message: errorMsg, modules: this.object });
131
+ console.error(errorMsg);
132
+ return this.object; // don't throw here
133
+ }
134
+
135
+ if (this.done) {
136
+ this.coreReady = true; // Set the core SDK class ready state to true
137
+ if(window && window?.queueMicrotask) {
138
+ queueMicrotask(() => this.dispatchEvent('ZSMClientReady', {
139
+ ready: true
140
+ }));
141
+ }else this.dispatchEvent('ZSMClientReady', { ready: true });
142
+ }
143
+
144
+ if(GLOBAL.showEvents) this.summarizeEventStack(classDesc, eventStatus);
145
+ return this.object;
146
+ }
147
+
148
+ /**
149
+ * @name byStatus
150
+ * @description Retrieves all event names that match a specific status.
151
+ * @param {string} eventStatus The status to filter events by (accepts: UNREGISTERED, PENDING, READY, ERROR).
152
+ * @throws {TypeError} If the eventStatus is invalid or not one of the allowed values.
153
+ * @returns {Array} Returns an array of event names that match the specified status.
154
+ */
155
+ byStatus(eventStatus) {
156
+ if (!eventStatus || typeof eventStatus !== 'string' || !eventStatus.length || !["UNREGISTERED", "PENDING", "READY", "ERROR"].includes(eventStatus))
157
+ throw new Error(`[ZSM EventCoordinator] :: byStatus :: Invalid 'eventStatus' value: "${eventStatus}". Valid statuses are: UNREGISTERED, PENDING, READY, ERROR.`);
158
+ return [...this._eventStack.entries().flatMap(([k, v]) => (v === eventStatus) ? [k] : [])];
159
+ }
160
+
161
+ /**
162
+ * @name lengthByStatus
163
+ * @description Returns the count of events that match a specific status.
164
+ * @param {string} eventStatus The status to filter events by (accepts: UNREGISTERED, PENDING, READY, ERROR).
165
+ * @throws {TypeError} If the eventStatus is invalid or not one of the allowed values.
166
+ * @returns {number} Returns the count of events with the specified status.
167
+ */
168
+ lengthByStatus(eventStatus) {
169
+ return this.byStatus(eventStatus).length || 0;
170
+ }
171
+
172
+ /**
173
+ * @name reset
174
+ * @description Resets the event stack to its initial state (cleanState).
175
+ * @returns {Object} Returns the reset event stack object.
176
+ */
177
+ reset() {
178
+ this._eventStack = new Map(this.cleanState);
179
+ if(window && window?.queueMicrotask) {
180
+ queueMicrotask(() => this.dispatchEvent('ZSMClientEventStackReset', { stack: this.object }));
181
+ } else this.dispatchEvent('ZSMClientEventStackReset', { stack: this.object });
182
+ return this.object;
183
+ }
184
+
185
+ /**
186
+ * @name finished
187
+ * @description Waits for the ZSMClient initialization to complete, either by checking the done state or by listening for the ZSMClientReady event.
188
+ * @param {function|number} callbackFnOrTimeout A callback function to call when the initialization is done, or a timeout in milliseconds to wait before rejecting.
189
+ * @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.
190
+ * @param {array<any>} args Additional arguments to pass to the callback function.
191
+ * @returns {Promise<boolean>} Resolves to true if the initialization is done, or rejects with an error if it takes too long or contains errors.
192
+ * @throws {Error} If the initialization takes too long or contains errors.
193
+ * @throws {Error} If some portion of the initialization fails.
194
+ */
195
+ async finished(callbackFnOrTimeout, callbackFnOrFirstArg, ...args) {
196
+ if (this.done) return Promise.resolve(true);
197
+
198
+ let timeoutID,
199
+ callbackFn,
200
+ onSDKReady,
201
+ onSDKError,
202
+ terminatePoll = false,
203
+ timeoutDuration = 20000;
204
+
205
+ if(!isNaN(callbackFnOrTimeout)) {
206
+ timeoutDuration = callbackFnOrTimeout;
207
+ if(typeof callbackFnOrFirstArg === 'function') {
208
+ callbackFn = callbackFnOrFirstArg;
209
+ }
210
+ }else if(typeof callbackFnOrTimeout === 'function') {
211
+ callbackFn = callbackFnOrTimeout;
212
+ args.unshift(callbackFnOrFirstArg);
213
+ }
214
+
215
+ return new Promise((resolve, reject) => {
216
+ onSDKReady = () => {
217
+ terminatePoll = true;
218
+ if(callbackFn) callbackFn(...args); // Call the callback function if provided
219
+ resolve(true); // Resolve the promise with a value of true
220
+ };
221
+ onSDKError = (e) => {
222
+ terminatePoll = true;
223
+ reject(new Error(`[ZSM EventCoordinator] :: finished :: Initialization Error! :: ${e.message || e}\nModules containing errors:\n - ${this.byStatus('ERROR').join('\n -')}.`));
224
+ };
225
+ if (this.done) onSDKReady();
226
+ if (this.containsErrors) onSDKError({ detail: { modules: this.byStatus('ERROR') } });
227
+
228
+ const onTimeout = () => {
229
+ terminatePoll = true;
230
+ if (this.done) return;
231
+ reject(new Error("[ZSM EventCoordinator] :: Timeout Error: ZSMClient initialization took too long to complete. Please check your network connection or server status."));
232
+ };
233
+
234
+ timeoutID = setTimeout(onTimeout, timeoutDuration);
235
+
236
+ if (this.scopeSupportsEventDispatch) {
237
+ GLOBAL.addEventListener('ZSMClientReady', onSDKReady, { once: true });
238
+ GLOBAL.addEventListener('ZSMClientError', onSDKError, { once: true });
239
+ } else {
240
+ const poll = () => {
241
+ if (this.done) {
242
+ onSDKReady();
243
+ } else if (this.containsErrors) {
244
+ onSDKError("[ZSM EventCoordinator] :: finished :: Error: ZSMClient initialization contains errors and is unable to continue.");
245
+ }
246
+ else if (!terminatePoll) setTimeout(poll, 200);
247
+ };
248
+ poll();
249
+ }
250
+ })
251
+ .catch(err => {
252
+ throw new Error(`[ZSM EventCoordinator] :: finished :: Initialization Error! :: ${err.message || err}\nModules containing errors:\n - ${this.byStatus('ERROR').join('\n -')}.`);
253
+ })
254
+ .finally(() => {
255
+ clearTimeout(timeoutID);
256
+ if (this.scopeSupportsEventDispatch) {
257
+ GLOBAL.removeEventListener('ZSMClientReady', onSDKReady);
258
+ GLOBAL.removeEventListener('ZSMClientError', onSDKError);
259
+ }
260
+ });
261
+ }
262
+ }
263
+
264
+ const eventCoordinator = new EventCoordinator(); // Create a singleton instance of EventCoordinator
265
+
266
+ export default eventCoordinator;
267
+ export { eventCoordinator };
package/FIDO2Client.js ADDED
@@ -0,0 +1,15 @@
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};
@@ -0,0 +1,117 @@
1
+ import eventCoordinator from './EventCoordinator.js';
2
+ import {zsmPluginManager} from './PluginManager.js';
3
+
4
+
5
+ class FIDO2ClientBase {
6
+ /**
7
+ * @constructor
8
+ * @param {Object} config - The configuration for the FIDO2Client
9
+ */
10
+ constructor(config) {
11
+ if(eventCoordinator.done) eventCoordinator.reset();
12
+ eventCoordinator.update('FIDO2ClientBase');
13
+ this.config = config;
14
+ const WebAuthnClient = zsmPluginManager.classes('WEBAUTHNCLIENT');
15
+ this.zsmAPI = new WebAuthnClient(config);
16
+ this.checkEnrollment = this.checkEnrollment.bind(this);
17
+ this.checkIdentity = this.checkIdentity.bind(this);
18
+ this.webauthnRetrieve = this.webauthnRetrieve.bind(this);
19
+ this.webauthnDelete = this.webauthnDelete.bind(this);
20
+ this.resetDevice = this.resetDevice.bind(this);
21
+ this.webauthnCreate = this.webauthnCreate.bind(this);
22
+ this.webauthnGet = this.webauthnGet.bind(this);
23
+ eventCoordinator.update('FIDO2ClientBase', 'READY');
24
+ }
25
+
26
+ get userIdentifier() { return this.zsmAPI.userIdentifier; }
27
+ get credentialID() { return this.zsmAPI.credentialID; }
28
+
29
+ /**
30
+ * @name checkEnrollment
31
+ * @description Alias for webauthnRetrieve
32
+ * @param {string} userIdentifier The identifier for the user
33
+ * @returns {Promise<Object>} Returns the result of webauthnRetrieve
34
+ * @memberOf FIDO2ClientBase, WebAuthnClient
35
+ */
36
+ checkEnrollment (userIdentifier) { return this.webauthnRetrieve(userIdentifier); }
37
+
38
+ /**
39
+ * @name checkIdentity
40
+ * @description Pass-through method to call WebAuthnClient's checkIdentity
41
+ * @param {string} userIdentifier The identifier for the user
42
+ * @param {boolean} primeEnroll Whether to create a new identity if it doesn't exist
43
+ * @returns {Promise<Object>} Returns the results of zsmAPI.checkIdentity
44
+ * @memberOf FIDO2ClientBase, WebAuthnClient
45
+ */
46
+ checkIdentity (userIdentifier, primeEnroll) { return this.zsmAPI.checkIdentity(userIdentifier, primeEnroll); }
47
+
48
+ /**
49
+ * @name webauthnRetrieve
50
+ * @description Pass-through method to call WebAuthnClient's webauthnRetrieve
51
+ * @param {string} userIdentifier The identifier for the user
52
+ * @returns {Promise<Object>} Returns the results of zsmAPI.webauthnRetrieve
53
+ * @memberOf FIDO2ClientBase, WebAuthnClient
54
+ */
55
+ webauthnRetrieve (userIdentifier) { return this.zsmAPI.webauthnRetrieve(userIdentifier); }
56
+
57
+ /**
58
+ * @name webauthnDelete
59
+ * @description Pass-through method to call WebAuthnClient's unbindFromDevice
60
+ * @param {string} userIdentifier The identifier for the user
61
+ * @returns {Promise<Object>} Returns the results of zsmAPI.unbindFromDevice
62
+ * @memberOf FIDO2ClientBase, WebAuthnClient
63
+ */
64
+ webauthnDelete (userIdentifier) { return this.zsmAPI.unbindFromDevice(userIdentifier); }
65
+
66
+ /**
67
+ * @name resetDevice
68
+ * @description Pass-through method to call WebAuthnClient's webauthnReset
69
+ * @returns {Promise<void>} No results are returned
70
+ * @memberOf FIDO2ClientBase, WebAuthnClient
71
+ */
72
+ resetDevice () { this.zsmAPI.webauthnReset(); }
73
+
74
+ /**
75
+ * @name webauthnCreate
76
+ * @description Enrolls a user by creating a ZSM credential on the device
77
+ * @param {string} userIdentifier The identifier for the user
78
+ * @returns {Promise<Object>|Error} Resolves with the created credential, false if user is already enrolled, or an error if enrollment fails
79
+ * @throws {Error} If the userIdentifier is not provided or is empty
80
+ * @throws {Error} If an error occurs during enrollment
81
+ * @memberOf FIDO2ClientBase
82
+ */
83
+ async webauthnCreate (userIdentifier) {
84
+ try {
85
+ if(userIdentifier == null
86
+ || userIdentifier === '') throw new Error(`[FIDO2Client] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
87
+
88
+ const createResult = this.zsmAPI.webauthnCreate(userIdentifier);
89
+ if(createResult instanceof Error) throw(`[FIDO2Client] :: webauthnCreate :: Unable to associate ${userIdentifier}'s identity with device profile.\nDetails:\n${createResult}`);
90
+ return createResult;
91
+ }catch(e) {
92
+ return new Error(e.message || e || '[FIDO2Client] :: webauthnCreate :: An error occurred during credential creation.');
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @name webauthnGet
98
+ * @description Retrieves the ZSM credential for the specified user
99
+ * @param {string} userIdentifier The identifier for the user
100
+ * @returns {Promise<Object>} The ZSM credential for the user
101
+ * @throws {Error} If the userIdentifier is not provided or is empty
102
+ * @throws {Error} If an error occurs during credential retrieval
103
+ * @memberOf FIDO2ClientBase
104
+ */
105
+ async webauthnGet (userIdentifier) {
106
+ try {
107
+ if(userIdentifier == null
108
+ || userIdentifier === '') throw new Error(`[FIDO2Client] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
109
+ let authCredential = await this.zsmAPI.webauthnGet(userIdentifier);
110
+ return authCredential.credential;
111
+ }catch(e) {
112
+ return new Error(e.message || e || '[FIDO2Client] :: webauthnGet :: An error occurred during credential retrieval.');
113
+ }
114
+ }
115
+ }
116
+
117
+ export default FIDO2ClientBase;
@@ -0,0 +1,8 @@
1
+ // Alias environment-specific global object
2
+ const GLOBAL = typeof globalThis !== "undefined" ? globalThis
3
+ : typeof self !== "undefined" ? self
4
+ : typeof window !== "undefined" ? window
5
+ : typeof global !== "undefined" ? global
6
+ : (() => { throw new Error("[ZSM GlobalScoping] :: Unable to determine global scope!"); })();
7
+
8
+ export default GLOBAL;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @file IdentityIndexing.js
3
+ * @description This module provides functions to bind a user identifier (UID) to an identity ID (IID) in IndexedDB and to retrieve the IID associated with a given UID.
4
+ * @author Jason Jurusz for Ideem
5
+ * @version 1.0.0
6
+ * @exports uid2iid, iid4uid
7
+ * @todo Migrate this code into the WebAssembly module.
8
+ */
9
+
10
+ /**
11
+ * @name uid2iid
12
+ * @description Binds a user identifier (UID) to an identity ID (IID) in IndexedDB.
13
+ * @param {string} uid The user identifier (UID) to bind.
14
+ * @param {string} iid The identity ID (IID) to bind to the UID.
15
+ * @throws {Error} If unable to bind UID to IID or if a DB Connection Error occurs.
16
+ * @returns {void}
17
+ */
18
+ function uid2iid(uid, iid) {
19
+ const idbInstance = indexedDB.open("ideem", 1);
20
+ idbInstance.onerror = (err=idbInstance.error) => { throw new Error(`[IdentityIndexing] :: uid2iid :: Unable to bind UID to IID:\n${err}`); }
21
+ idbInstance.onsuccess = (e, db=e.target.result) => {
22
+ const zsmTbl = db.transaction("zsm", "readwrite");
23
+ const write = zsmTbl.objectStore("zsm").put(iid, uid);
24
+ write.onsuccess = () => (db.close());
25
+ write.onerror = (err=write.error) => (db.close(), console.error(`[IdentityIndexing] :: uid2iid :: Unable to bind UID to IID:\n${err}`));
26
+ };
27
+ }
28
+
29
+ /**
30
+ * @name iid4uid
31
+ * @description Finds the identity ID for a given user identifier (UID) in IndexedDB.
32
+ * @param {string} uid The user identifier (UID) to search for.
33
+ * @returns {Promise<string>} A promise that resolves to the identity ID (IID) associated with the UID, or the UID itself if not found.
34
+ * @throws {Error} If unable to search IndexedDB or if a DB Connection Error occurs.
35
+ */
36
+ function iid4uid(uid) {
37
+ return new Promise((resolve, reject) => {
38
+ const idbInstance = indexedDB.open("ideem", 1);
39
+ idbInstance.onupgradeneeded = (e, db=e.target.result) => { if (!db.objectStoreNames.contains("zsm")) db.createObjectStore("zsm");};
40
+ idbInstance.onerror = (err=idbInstance.error) => { return reject(new Error(`[IdentityIndexing] :: iid4uid :: Unable to connect to IDB:\n${err}`)); }
41
+ idbInstance.onsuccess = (e, db=e.target.result) => {
42
+ if(!db.objectStoreNames.contains("zsm")) return resolve(uid);
43
+ const zsmTbl = db.transaction("zsm", "readonly");
44
+ const lookup = zsmTbl.objectStore("zsm").get(uid);
45
+ lookup.onsuccess = () => (db.close(), (lookup.result) ? resolve(lookup.result) : resolve(uid));
46
+ lookup.onerror = (err=lookup.error) => (db.close(), console.error(`[IdentityIndexing] :: iid4uid :: Unable to find IID for UID:\n${err}`), reject(new Error(`[IdentityIndexing] :: iid4uid :: Unable to find IID for UID:\n${err}`)));
47
+ };
48
+ });
49
+ }
50
+
51
+ export { uid2iid, iid4uid };
@@ -0,0 +1,111 @@
1
+ class PluginManager {
2
+ #sdkClassMap = new Map(); // Map of SDK class names to their implementations
3
+ #classCorpus = new Map(); // Map of plugin identities to their class maps
4
+ #status = 'INITIALIZED'; // Indicates the current status of the plugin manager (INITIALIZED, SEEDED, FROZEN)
5
+
6
+ /**
7
+ * Returns whether the plugin manager is frozen.
8
+ * @returns {boolean} True if the plugin manager is frozen, false otherwise.
9
+ */
10
+ get isFrozen() {
11
+ return this.#status === 'FROZEN';
12
+ }
13
+
14
+ /**
15
+ * @name classes
16
+ * @description Retrieves the SDK class associated with the given class description.
17
+ * @param {string} classDesc The name of the class to retrieve.
18
+ * @returns {Function} The SDK class associated with the given name.
19
+ * @throws {Error} If the classDesc is not provided
20
+ * @throws {Error} If no class is registered for the given name.
21
+ */
22
+ classes (classDesc) {
23
+ if(this.#status === 'INITIALIZED') throw new Error('[zsmPluginManager] :: classes :: collection accessor cannot be referenced before SOME classes are registered!');
24
+ if(!classDesc || typeof classDesc !== 'string') throw new Error(`[zsmPluginManager] :: classes :: collection accessor requires a valid class description (received ${typeof classDesc}: ${classDesc})!`);
25
+ classDesc = classDesc.toUpperCase();
26
+ if(!this.isFrozen){
27
+ console.warn('[zsmPluginManager] :: Freezing the plugin manager as the first class has been preemptively requested. No further plugins can be registered after this point.\nIf you feel you received this message unexpectedly, please ensure all plugins are imported before initializing (`new UMFAClient()` or `new FIDO2Client()`) a client.');
28
+ this.freezePlugins();
29
+ }
30
+ if(!this.#sdkClassMap.has(classDesc)) throw new Error(`[zsmPluginManager] :: classes :: No SDK class registered for: ${classDesc}`);
31
+ return this.#sdkClassMap.get(classDesc);
32
+ }
33
+
34
+ /**
35
+ * @name seedClassMap
36
+ * @description Seeds the class map with initial values.
37
+ * @param {Object<string, Object>} registeredObj The key-value pairs to seed the class map with.
38
+ * @returns {Map<string, Function>} A Map containing the seeded class definitions
39
+ */
40
+ seedClassMap(registeredObj) {
41
+ const constructedMap = new Map();
42
+ for (const [classDesc, ctor] of Object.entries(registeredObj.classDefs || {})) {
43
+ if(ctor) constructedMap.set(classDesc.toUpperCase(), ctor);
44
+ }
45
+ return constructedMap;
46
+ }
47
+
48
+ /**
49
+ * @name registerPlugin
50
+ * @description Registers a plugin with the plugin manager.
51
+ * @param {string} pluginIdentity The unique identity of the plugin.
52
+ * @param {Object<string, Function>} classObj The class map for the plugin.
53
+ * @throws {Error} Throws an error if the plugin manager is frozen.
54
+ */
55
+ registerPlugin(pluginIdentity, classObj){
56
+ if (this.isFrozen) throw new Error('[zsmPluginManager] :: registerPlugin :: Cannot register plugin: PlugInManager is frozen!\nThis typically occurs when a plugin is imported after a client - UMFAClient or FIDO2Client - has already been initialized.\nMake sure to import all plugins before initializing (`new UMFAClient()` or `new FIDO2Client()`) a client.');
57
+ if (!pluginIdentity || typeof classObj !== 'object' || classObj == null) throw new Error('[zsmPluginManager] :: registerPlugin :: pluginIdentity and class map object required');
58
+
59
+ this.#status = 'SEEDED';
60
+
61
+ this.#classCorpus.set(pluginIdentity.toUpperCase(), classObj);
62
+ console.info(`[zsmPluginManager] :: PluginManager :: Registered plugin "${pluginIdentity}".`);
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * @name freezePlugins
68
+ * @description Freezes the plugin manager, preventing further modifications.
69
+ */
70
+ freezePlugins(){
71
+ if (this.isFrozen) return; // If the plugin manager is already frozen, bail.
72
+
73
+ // Below is the plug-in precedence resolution logic. As new plugins are registered, they may override core classes.
74
+ // As new plugins are DEVELOPED, they may introduce new classes or modify existing ones, and their extension/override logic must be declared below.
75
+
76
+ // [CORE PACKAGE] Default all of the active classes to the "core" classes (set below this declaration)
77
+ const core = this.#classCorpus.get('CORE');
78
+ if (!core) throw new Error('[zsmPluginManager] :: freezePlugins :: Core not registered before freeze');
79
+
80
+ const constructedMap = this.seedClassMap(core); // returns Map
81
+
82
+ // [PASSKEYS+ PLUGIN PACKAGE] If the "PASSKEYS+" plugin is registered, supercede the core classes with the passkeys classes
83
+ if(this.#classCorpus.has('PASSKEYS+')) {
84
+ const passkeysMap = this.#classCorpus.get('PASSKEYS+');
85
+ if (passkeysMap?.classDefs) for (const [k,v] of Object.entries(passkeysMap.classDefs)) constructedMap.set(k.toUpperCase(), v);
86
+ }
87
+
88
+ // Apply class decorators (wrappers) AFTER overlays, if present in any of the registrations
89
+ const postConstructDecorators = [...this.#classCorpus.values()].filter(entry => entry.mode === 'DECORATOR');
90
+ if (postConstructDecorators.length) {
91
+ for (const modifier of postConstructDecorators) {
92
+ let { classDesc, postModFn } = modifier;
93
+ if (typeof postModFn !== 'function') throw new Error('[zsmPluginManager] :: freezePlugins :: Decorator must contain a valid function (received ' + typeof postModFn + ')');
94
+
95
+ const baseCtor = constructedMap.get(classDesc.toUpperCase());
96
+ if (typeof baseCtor !== 'function') throw new Error(`[zsmPluginManager] :: freezePlugins :: Decorator targets unknown class "${classDesc}"`);
97
+
98
+ const decorated = postModFn(baseCtor);
99
+ if (typeof decorated !== 'function') throw new Error('[zsmPluginManager] :: freezePlugins :: Decorator must return a constructor (received ' + typeof decorated + ')');
100
+ constructedMap.set(classDesc.toUpperCase(), decorated);
101
+ }
102
+ }
103
+
104
+ this.#sdkClassMap = constructedMap;
105
+ this.#status = 'FROZEN';
106
+ }
107
+ }
108
+
109
+ const pluginManager = new PluginManager()
110
+
111
+ export { pluginManager as zsmPluginManager };
@@ -0,0 +1,15 @@
1
+ import eventCoordinator from './EventCoordinator.js';
2
+ import RelyingPartyBase from './RelyingPartyBase.js';
3
+
4
+ eventCoordinator.update('RelyingParty');
5
+
6
+ //## BEGIN RelyingParty ===================================================================================================================================
7
+ class RelyingParty extends RelyingPartyBase {
8
+ constructor(host, apiKey, applicationID, appEnvironment, usePasskeys) {
9
+ super(host, apiKey, applicationID, appEnvironment, (usePasskeys != 'undefined' && usePasskeys != undefined && usePasskeys))
10
+ eventCoordinator.update('RelyingParty', 'READY');
11
+ }
12
+ }
13
+ //## END RelyingParty =====================================================================================================================================
14
+
15
+ export default RelyingParty;