@ideem/zsm-client-sdk 2.4.0 → 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/{RELEASE_NOTES.txt → CHANGELOG.md} +42 -6
- package/EventCoordinator.js +267 -0
- package/FIDO2Client.js +15 -0
- package/FIDO2ClientBase.js +117 -0
- package/GlobalScoping.js +8 -0
- package/IdentityIndexing.js +51 -0
- package/PluginManager.js +111 -0
- package/RelyingParty.js +15 -0
- package/RelyingPartyBase.js +197 -0
- package/UMFAClient.js +15 -0
- package/UMFAClientBase.js +125 -0
- package/Utils.js +44 -0
- package/WASMRustInterface.js +1458 -0
- package/WebAuthnClient.js +29 -0
- package/WebAuthnClientBase.js +429 -0
- package/ZSMClientSDK.js +28 -463
- package/package.json +19 -50
- package/shims/VanillaJSLoader.js +96 -0
- package/shims/es-module-shims.js +1067 -0
- package/worker.js +0 -2503
|
@@ -1,21 +1,57 @@
|
|
|
1
1
|
ZSM Client SDK for Browser Apps
|
|
2
|
-
Latest version: v2.
|
|
3
|
-
Latest release date:
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
47
|
+
- Minor bug fixes and improvements
|
|
48
|
+
- Updates to documentation
|
|
49
|
+
- Updates to sample apps
|
|
50
|
+
- Alters worker communication protocol to ensure that the worker is not blocked
|
|
51
|
+
by the main thread and vice versa, while still ensuring that the initialization
|
|
52
|
+
of the worker is completed before any other operations are performed.
|
|
53
|
+
|
|
54
|
+
## Release 2.4.0 - May 1, 2025
|
|
19
55
|
- Minor bug fixes and improvements
|
|
20
56
|
- Updates to documentation
|
|
21
57
|
- Adds event emitters to the UMFAClient and FIDO2Client for on-the-fly initializations
|
|
@@ -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;
|
package/GlobalScoping.js
ADDED
|
@@ -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 };
|
package/PluginManager.js
ADDED
|
@@ -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 };
|