@ideem/zsm-client-sdk 2.7.1 → 2.8.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.
@@ -3,6 +3,7 @@ import GLOBAL from './GlobalScoping.js';
3
3
  import { uid2iid, iid4uid } from './IdentityIndexing.js';
4
4
  import { toObject } from './Utils.js';
5
5
  import {zsmPluginManager} from './PluginManager.js';
6
+ import PasskeysPlusClient from './PasskeysPlusClient.js';
6
7
 
7
8
 
8
9
  eventCoordinator.update('WebAuthnClientBase');
@@ -27,6 +28,8 @@ class WebAuthnClientBase {
27
28
  // Helper function to standardize URL format (ensuring it's parsable and ends with a '/')
28
29
  const cleanURL = (url) => canParseURL(url) ? new URL(url).origin + '/' : false;
29
30
 
31
+ this.passkeysPlus = new PasskeysPlusClient();
32
+
30
33
  if(config?.region){
31
34
  config.authenticator_host_url ||= config.authenticator_host = `https://zsm-authenticator-${config.region}.useideem.com/`;
32
35
  config.zsm_host_url ||= config.zsm_host = `https://zsm-${config.region}.useideem.com/`;
@@ -49,10 +52,10 @@ class WebAuthnClientBase {
49
52
  delete config.use_origin;
50
53
  }
51
54
 
52
- this.zsmApi = undefined; // The ZSM API instance (read: WASM-driven Crypto Module)
53
- this.relyingParty = undefined; // The Relying Party instance (read: ZSM Authenticator API)
54
- this.status = undefined; // The status of the WebAuthnClientBase
55
- this.config = undefined; // The configuration object passed into the constructor
55
+ this.zsmApi = undefined; // The ZSM API instance (read: WASM-driven Crypto Module)
56
+ this.relyingParty = undefined; // The Relying Party instance (read: ZSM Authenticator API)
57
+ this.status = undefined; // The status of the WebAuthnClientBase
58
+ this.config = undefined; // The configuration object passed into the constructor
56
59
 
57
60
  // If the inputted host_url is not a valid URL, null it for future checks' sake.
58
61
  config.host_url = cleanURL(config.host_url) || null;
@@ -72,8 +75,10 @@ class WebAuthnClientBase {
72
75
  config.host_url = config.zsm_host_url;
73
76
 
74
77
  this.config = GLOBAL.zsmAppConfig = config;
78
+
75
79
  iid4uid(this.config.consumer_id)
76
80
  .then(v=>{
81
+ // console.log('(v :', v);
77
82
  GLOBAL.mpcConfig = Object.assign({}, config, {consumer_id: v});
78
83
  if((GLOBAL.mpcConfig?.use_passkeys??undefined)) {
79
84
  GLOBAL.mpcConfig.use_origin = true;
@@ -98,7 +103,6 @@ class WebAuthnClientBase {
98
103
  get credentialID() { return this.relyingParty.credentialID; }
99
104
  set credentialID(v) { return this.relyingParty.credentialID = v; }
100
105
  get isEnrolled() { return !!this.credentialID }
101
- // get version() { return "ZSM WebAuthn client"; }
102
106
 
103
107
  /**
104
108
  * @name getZsmApi
@@ -142,10 +146,10 @@ class WebAuthnClientBase {
142
146
  */
143
147
 
144
148
  async checkIdentity (userIdentifier, primeEnroll=false) {
145
- if(userIdentifier == null
146
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: checkIdentity :: A userIdentifier String is required! Received: ${userIdentifier}.`);
149
+ if(userIdentifier == null
150
+ || userIdentifier === '') throw new Error(`[WebAuthnClient] :: checkIdentity :: A userIdentifier String is required! Received: ${userIdentifier}.`);
147
151
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
148
-
152
+
149
153
  try {
150
154
  const resultOfCheck = await this.relyingParty.checkServerSideIdentity(primeEnroll);
151
155
 
@@ -162,6 +166,137 @@ class WebAuthnClientBase {
162
166
  }
163
167
  }
164
168
 
169
+ /**
170
+ * @name webauthnRetrieve
171
+ * @description Looks up the Identity ID for the specified userIdentifier and retrieves its credential ID (if it exists).
172
+ * @ This is used to make a client-only check to see if a given userIdentifier already has an enrollment on the device.
173
+ * @param {string} userIdentifier The user identifier.
174
+ * @param {boolean} returnExpandedDetails Whether to return the full enrollment details object or just the credential ID.
175
+ * @ - false (default): retrieves the ZSM credential ID, or false if not enrolled
176
+ * @ - true: retrieves a cleanFinalData object containing all relevant enrollment data
177
+ * @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before passkey enrollment workflows).
178
+ * @returns {Promise<Boolean:false>} Returns `false` if retrieval fails (no enrollment found).
179
+ * @returns {Promise<String>} Returns the credential ID if found.
180
+ * @returns {Promise<Object>} Resolves with an object containing all relevant credential data.
181
+ * @throws {Error} Throws an error if an uncaught/unhandled error occurs while retrieving the credential.
182
+ * @memberof WebAuthnClientBase
183
+ */
184
+ async webauthnRetrieve (userIdentifier, returnExpandedDetails=false, forceRemoteCheck=false) { // [J] Merged with __webauthnRetrieve; added forceRemoteCheck for enrollment flows
185
+ // console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: userIdentifier, returnExpandedDetails', userIdentifier, returnExpandedDetails);
186
+
187
+ if(userIdentifier == null || typeof userIdentifier !== 'string'
188
+ || userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnRetrieve :: A userIdentifier String is required! Received: ${userIdentifier} (of type ${typeof userIdentifier}).`);
189
+
190
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
191
+ this.credentialID = null;
192
+
193
+ this.initializeZsm('webauthnRetrieve');
194
+
195
+ let credentialIdentifier = await iid4uid(userIdentifier); // Obtain identity_id for userIdentifier
196
+ GLOBAL.mpcConfig.consumer_id = credentialIdentifier; // Update mpcConfig with identity_id
197
+
198
+ const response = await this.zsmApi.webauthn_retrieve(userIdentifier); // Perform local retrieval of credential details
199
+ let localData = toObject(response);
200
+
201
+ if (!returnExpandedDetails){ // If we're only looking for the credential ID, we can short-circuit so...
202
+ return (localData?.error || !localData?.result) // ... if there's an error (or no result/result's missing)...
203
+ ? false // ... return false (indicating no enrollment)...
204
+ : (this.credentialID = localData.result['zsm_credential_id']??null); // ... otherwise, set the credential ID (if it exists) and return it.
205
+ }else{ // Otherwise (we're looking for expanded details)...
206
+ localData = (localData?.error || !localData?.result) // ... if there's an error (or no result/result's missing)... [J] Fixed: was reassigning const `response`
207
+ ? {} // ... we're starting with an empty object (indicating no local knowledge)...
208
+ : localData.result; // ... otherwise, we start with any local knowledge we DO have
209
+ }
210
+
211
+ let passkeySupported = false; // [J] Best-effort detection of platform authenticator support
212
+ try {
213
+ passkeySupported = !!(typeof window !== 'undefined' && window.PublicKeyCredential
214
+ && navigator?.credentials
215
+ && await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable());
216
+ } catch { /* unsupported environment */ }
217
+
218
+ this.credentialID = localData['zsm_credential_id']??null;
219
+
220
+ // Server consultation: only needed when local passkey knowledge is insufficient // [J] Replaced hasFailingStatus shotgun check with targeted remote passkey logic
221
+ const shouldCheckServer = forceRemoteCheck
222
+ || (passkeySupported && !localData?.['has_remote_passkey'] && !localData?.['passkey_credential_id'])
223
+ || !localData?.['zsm_active']
224
+ || !localData?.['zsm_active']
225
+
226
+ let completeData = localData;
227
+ if(shouldCheckServer) {
228
+ let remoteData = await this.checkRemoteEnrollmentDetails(userIdentifier, completeData); // Grab the remote enrollment details...
229
+
230
+ if(remoteData && !(remoteData instanceof Error)) { // ... and if we got something back (and it wasn't an error)...
231
+ completeData = Object.assign({}, localData, remoteData); // ...merge it with completeData (giving remote precedence)
232
+ // if(remoteData?.['rcr']) // Persist remote passkey discovery locally to prevent future RTTs
233
+ // this.zsmApi.updateEnrollmentStatus(userIdentifier, "passkey"); // [J] Fire-and-forget; next webauthn_retrieve will return hasRemotePasskey locally
234
+ }
235
+ }
236
+
237
+
238
+
239
+ const cleanFinalData = { // Finally, clean up the net enrollment details results...
240
+ userIdentifier,
241
+ enrollmentActive : completeData?.['zsm_active'] ?? false,
242
+ passkeySupported, // [J] Device/browser supports platform authenticator (best-effort)
243
+ passkeyEligible : passkeySupported && !!(completeData?.['passkey_credential_id'] || completeData?.['has_remote_passkey']), // [J] Passkeys supported AND a passkey credential exists to use
244
+ hasLocalPasskey : passkeySupported && !!(completeData?.['passkey_credential_id']??false), // [J] Now sourced from WASM response directly
245
+ hasRemotePasskey : !!completeData?.['rcr'] ?? null, // [J] null = unknown; false = confirmed absent; true = confirmed present
246
+ pkpCredentialID : completeData?.['passkey_credential_id'] ?? null, // [J] If we have a local passkey credential ID, use it (this is the most reliable source for passkey presence, as it indicates we've actually used the passkey on this device before); if not, null it (instead of false) to indicate uncertainty
247
+ pkpCredID : completeData?.['passkey_credential_id'] ?? null, // [J] If we have a local passkey credential ID, use it (this is the most reliable source for passkey presence, as it indicates we've actually used the passkey on this device before); if not, null it (instead of false) to indicate uncertainty
248
+ hasZSMCred : !!completeData?.['zsm_credential'] || false,
249
+ zsmCredential : completeData?.['zsm_credential'] ?? null,
250
+ zsmCredentialID : completeData?.['zsm_credential_id'] ?? null,
251
+ zsmCredID : completeData?.['zsm_credential_id'] ?? null
252
+ }
253
+ if(cleanFinalData.hasLocalPasskey === true) cleanFinalData.hasRemotePasskey = true; // [J] Sanity check: local passkey presence implies remote passkey presence
254
+ if(completeData?.['rcr']) {
255
+ cleanFinalData.pkpCredential = completeData['rcr']; // [J] If the server provided a remote credential ID, include it in the cleanFinalData for use in authentication flows (e.g., Passkeys+)
256
+ cleanFinalData.hasLocalPasskey = true;
257
+ if (!cleanFinalData.pkpCredentialID) {
258
+ cleanFinalData.pkpCredentialID = completeData['rcr']?.publicKey?.allowCredentials?.flatMap(c=>c.id)??null;
259
+ if(cleanFinalData.pkpCredentialID.length) cleanFinalData.pkpCredentialID = cleanFinalData.pkpCredentialID[0]; // [J] If the server provided multiple remote credential IDs (which shouldn't happen, but just in case), we'll take the first one as the primary indicator of passkey presence/eligibility
260
+ }
261
+ }
262
+
263
+ // console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: cleanFinalData :', cleanFinalData);
264
+ return cleanFinalData;
265
+ };
266
+
267
+ /**
268
+ * @name webauthnRetrieveAll
269
+ * @description Syntactic sugar for webauthnRetrieve with returnExpandedDetails=true to retrieve all enrollment information.
270
+ * @param {string} userIdentifier The user identifier.
271
+ * @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before enrollment workflows).
272
+ * @returns {Promise<Object>} Returns the full enrollment details for the user.
273
+ * @memberof WebAuthnClientBase
274
+ */
275
+ async webauthnRetrieveAll (userIdentifier, forceRemoteCheck=false) {
276
+ const allEnrollmentDetails = await this.webauthnRetrieve(userIdentifier, true, forceRemoteCheck);
277
+ return allEnrollmentDetails;
278
+ };
279
+
280
+ /**
281
+ * @name checkRemoteEnrollmentDetails
282
+ * @description Checks the server for remote enrollment details when local data has failing status flags.
283
+ * @param {string} userIdentifier The user identifier.
284
+ * @param {Object} localPayload The local enrollment data to send to the server for comparison.
285
+ * @returns {Promise<Object>} Returns remote enrollment details, or false if none found.
286
+ * @throws {Error} Throws if the server returns an error.
287
+ * @memberof WebAuthnClientBase
288
+ */
289
+ async checkRemoteEnrollmentDetails (userIdentifier, localPayload) {
290
+ try {
291
+ const allEnrollmentDetails = await this.relyingParty.getRemoteEnrollmentDetails(userIdentifier, localPayload);
292
+ if(allEnrollmentDetails instanceof Error || !allEnrollmentDetails || allEnrollmentDetails['identity_id'] == null) return false;
293
+ return allEnrollmentDetails; // [J] Fixed: was missing return
294
+ } catch(err) {
295
+ console.error('Error checking remote enrollment details:', err);
296
+ return false;
297
+ }
298
+ }
299
+
165
300
  /**
166
301
  * @name webauthnCreate
167
302
  * @description Creates a new WebAuthn credential for the specified userIdentifier.
@@ -174,8 +309,7 @@ class WebAuthnClientBase {
174
309
  * @throws {Error} Throws a COUNTED error if unable to verify the credential with the Relying Party Server.
175
310
  */
176
311
  async webauthnCreate (userIdentifier) {
177
- if(userIdentifier == null
178
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
312
+ if(!userIdentifier) throw new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
179
313
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
180
314
 
181
315
  await this.initializeZsm('webauthnCreate');
@@ -186,62 +320,26 @@ class WebAuthnClientBase {
186
320
  else if (!publicKey) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to retrieve public key from Relying Party Server!');
187
321
 
188
322
  publicKey.user_id = publicKey.user.id;
189
-
323
+
190
324
  let createResponse = await this.zsmApi.webauthn_create(publicKey, undefined, userIdentifier, tracePrimer());
191
325
  if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain response from Crypto Server!');
192
326
 
193
-
327
+
194
328
  createResponse = toObject(createResponse);
195
329
  GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
196
-
330
+
197
331
  if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain credential from Crypto Server!');
198
332
  if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to acquire attestation object!');
199
-
333
+
200
334
  const credential = createResponse.result;
201
335
 
202
-
336
+
203
337
  const registrationResult = await this.relyingParty.registrationFinish(credential);
204
338
  if(registrationResult instanceof Error) throw new Error(registrationResult);
205
339
  else if(registrationResult == false) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to verify credential Relying Party Server!');
206
340
 
207
-
208
- return registrationResult;
209
- };
210
-
211
- /**
212
- * @name webauthnCreate
213
- * @description Creates a new WebAuthn credential for the specified userIdentifier.
214
- * @param {string} userIdentifier The user identifier.
215
- * @returns {Promise<Object>} Resolves with the created credential.
216
- * @throws {Error} Throws an error if unable to start enrollment with the Relying Party Server.
217
- * @throws {Error} Throws an error if unable to retrieve the public key/IdentityID from the Relying Party Server.
218
- * @throws {Error} Throws an error if unable to obtain the createResponse/credential from the Crypto Server.
219
- * @throws {Error} Throws an error if unable to verify the credential with the Relying Party Server.
220
- * @throws {Error} Throws an error if unable to acquire the identity ID from the Relying Party Server.
221
- * @throws {Error} Throws an error if the user ID or token is missing from the response body.
222
- */
223
- async webauthnCreateMpcThenFinish (userIdentifier, publicKey, pairing_token) {
224
- if(userIdentifier == null
225
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnCreateMpcThenFinish :: A userIdentifier String is required! Received: ${userIdentifier}.`);
226
- if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
227
- this.status = 'ENROLLMENT_STARTING';
228
- await this.initializeZsm('webauthnCreateMpcThenFinish');
229
-
230
- let createResponse = await this.zsmApi.webauthn_create(publicKey, pairing_token, userIdentifier, tracePrimer());
231
- if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateMpcThenFinish :: Unable to obtain response from Crypto Server!');
232
-
233
- createResponse = toObject(createResponse);
234
- GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
235
-
236
- if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateMpcThenFinish :: Unable to obtain credential from Crypto Server!');
237
- if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateMpcThenFinish :: Unable to acquire attestation object!');
238
-
239
- const credential = createResponse.result;
240
-
241
- const registrationResult = await this.relyingParty.registrationFinish(credential);
242
- if (!registrationResult) throw new Error('[WebAuthnClient] :: webauthnCreateMpcThenFinish :: Unable to verify credential Relying Party Server!');
243
341
 
244
- return registrationResult.credential;
342
+ return registrationResult;
245
343
  };
246
344
 
247
345
  /**
@@ -257,12 +355,11 @@ class WebAuthnClientBase {
257
355
  * @throws {Error} Throws an error if the user ID or token is missing from the response body.
258
356
  */
259
357
  async webauthnGet (userIdentifier) {
260
- if(userIdentifier == null
261
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
358
+ if(userIdentifier == null|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
262
359
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
263
360
 
264
361
  await this.initializeZsm('webauthnGet');
265
- const credentialId = await this.webauthnRetrieve(userIdentifier, true);
362
+ const credentialId = await this.webauthnRetrieve(userIdentifier, true); // [J] Now passes expanded details to authenticationStart
266
363
 
267
364
  if (!this.isEnrolled) throw new Error(`[WebAuthnClient] :: webauthnGet :: ${userIdentifier} is not enrolled, lacks a credential, or credential is corrupted!`);
268
365
 
@@ -270,14 +367,13 @@ class WebAuthnClientBase {
270
367
 
271
368
  if (authStartResponse instanceof Error) throw new Error(authStartResponse);
272
369
  else if (!authStartResponse) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to start authentication with Relying Party Server!');
273
-
370
+
274
371
  authStartResponse = toObject(authStartResponse);
275
372
 
276
373
  if(!authStartResponse?.rcr?.publicKey?.challenge) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to locate public key/challenge inside Relying Party Server response!\nResponse Body: ' + JSON.stringify(authStartResponse));
277
374
  const publicKey = authStartResponse.rcr.publicKey;
278
375
 
279
376
  let mpcResponse = await this.zsmApi.webauthn_get(publicKey, undefined, userIdentifier, tracePrimer());
280
-
281
377
 
282
378
  if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Crypto server did not respond!');
283
379
 
@@ -285,62 +381,7 @@ class WebAuthnClientBase {
285
381
  if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
286
382
 
287
383
  GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
288
-
289
- const credential = mpcResponse.result;
290
- return credential;
291
- let authFinishResponse = await this.relyingParty.authenticationFinish(credential);
292
-
293
- if (authFinishResponse instanceof Error) throw new Error(authFinishResponse);
294
- else if (!authFinishResponse) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to verify credential from Relying Party Server!');
295
-
296
- authFinishResponse = toObject(authFinishResponse)
297
- if (!authFinishResponse.token) throw new Error('[WebAuthnClient] :: webauthnGet :: Token mismatched or missing from Relying Party Server verification attempt!\nResponse Body: ' + JSON.stringify(authFinishResponse));
298
-
299
- return {
300
- signature : credential.response.signature,
301
- signedChallenge : credential.response.clientDataJSON,
302
- user_id : authFinishResponse.user_id,
303
- token : authFinishResponse.token,
304
- challenge : publicKey.challenge,
305
- credential
306
- };
307
- };
308
-
309
-
310
-
311
- /**
312
- * @name webauthnRetrieve
313
- * @description Looks up the Identity ID for the specified userIdentifier and retrieves its credential ID (if it exists).
314
- * @ This is used to make a client-only check to see if a given userIdentifier already has an enrollment on the device.
315
- * @param {string} userIdentifier The user identifier.
316
- * @param {string} targetData The specific data to retrieve. Options are:
317
- * @ - 'id' or omitted: retrieves the ZSM credential ID (default)
318
- * @ - 'all': retrieves an object containing all relevant credential data
319
- * @ - any other string: retrieves the specified field from the response
320
- * @returns {Promise<Boolean:false>} Returns `false` if retrieval fails (no enrollment found).
321
- * @returns {Promise<String>} Returns the requested data if found (e.g., ").
322
- * @returns {Promise<Object>} Resolves with an object containing all relevant credential data.
323
- * @returns {Promise<Null>} Resolves with null if the requested data is not found.
324
- * @throws {Error} Throws an error if an uncaught/unhandled error occurs while retrieving the credential.
325
- */
326
- async webauthnRetrieve (userIdentifier) {
327
- if(userIdentifier == null
328
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnRetrieve :: A userIdentifier String is required! Received: ${userIdentifier}.`);
329
- if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
330
- this.credentialID = null;
331
-
332
- this.initializeZsm('webauthnRetrieve');
333
- let credentialIdentifier = await iid4uid(userIdentifier),
334
- requestedData = {};
335
- GLOBAL.mpcConfig.consumer_id = credentialIdentifier;
336
-
337
- let response = await this.zsmApi.webauthn_retrieve(userIdentifier);
338
- response = toObject(response);
339
-
340
- if (response?.error || !response?.result) return false;
341
-
342
- this.credentialID = response?.result['zsm_credential_id']??null;
343
- return this.credentialID || false;
384
+ return mpcResponse.result;
344
385
  };
345
386
 
346
387
  /**
@@ -355,8 +396,8 @@ class WebAuthnClientBase {
355
396
  * @emits {CustomEvent} Emits a "MPCResponse" event with the mpcResponse data.
356
397
  */
357
398
  async webauthnPartialGet (userIdentifier) {
358
- if(userIdentifier == null
359
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnPartialGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
399
+ if(userIdentifier == null
400
+ || userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnPartialGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
360
401
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
361
402
  this.status = 'AUTHENTICATION_STARTING';
362
403
  await this.initializeZsm('webauthnPartialGet');
@@ -379,16 +420,7 @@ class WebAuthnClientBase {
379
420
  if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnPartialGet :: Unable to obtain credential from Crypto Server!');
380
421
 
381
422
  GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
382
-
383
- const getCredential = mpcResponse.result;
384
- return getCredential;
385
- return {
386
- signature: getCredential.response.signature,
387
- signedChallenge: getCredential.response.clientDataJSON,
388
- user_id: userIdentifier,
389
- challenge: getChallenge.challenge,
390
- credential: getCredential,
391
- };
423
+ return mpcResponse.result;
392
424
  };
393
425
 
394
426
  /**
@@ -401,8 +433,8 @@ class WebAuthnClientBase {
401
433
  * @throws {Error<UnenrollError>} Throws an UnenrollError if a credential matching userIdentifier IS stored on the device, but was unable to be removed.
402
434
  */
403
435
  async unbindFromDevice (userIdentifier) {
404
- if(userIdentifier == null
405
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: unbindFromDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
436
+ if(userIdentifier == null
437
+ || userIdentifier === '') throw new Error(`[WebAuthnClient] :: unbindFromDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
406
438
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
407
439
  this.status = 'UNENROLL_STARTING';
408
440
  if(userIdentifier !== this.userIdentifier) return false;
@@ -420,9 +452,9 @@ class WebAuthnClientBase {
420
452
  if (cursor) {
421
453
  const { key, value } = cursor;
422
454
  if(
423
- key.includes(userIdentifier) ||
455
+ key.includes(userIdentifier) ||
424
456
  (typeof value === 'string' && value.includes(userIdentifier)) ||
425
- typeof value === 'object' && value?.zsm_credential?.get('rawId') === rawID
457
+ typeof value === 'object' && value?.zsmCredential?.get('rawId') === rawID
426
458
  ) (store.delete(key), true);
427
459
  cursor.continue();
428
460
  }
@@ -435,6 +467,292 @@ class WebAuthnClientBase {
435
467
  return true;
436
468
  }
437
469
 
470
+ // PKP Methods ============================================================================================================
471
+
472
+ /**
473
+ * @name webauthnCreateFromPKP
474
+ * @description Constructs a new ZSM WebAuthn credential from a Passkeys Plus Platform Authenticator registration response.
475
+ * @param {string} userIdentifier The user identifier.
476
+ * @param {string} publicKey The public key for the enrollment process.
477
+ * @param {string} pairing_token The pairing token for the enrollment process.
478
+ * @returns {Promise<Object>} Resolves with the created credential.
479
+ * @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
480
+ * @throws {Error} (MPC Round) Crypto Server failed to respond.
481
+ * @throws {Error} (MPC Round) Response did not contain a valid credential/attestation object.
482
+ * @throws {Error} Could not verify credential with Relying Party Server.
483
+ * @memberof WebAuthnClientBase
484
+ * @async
485
+ */
486
+ async webauthnCreateFromPKP (userIdentifier, publicKey, pairing_token) {
487
+ if(userIdentifier == null
488
+ || userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: webauthnCreateFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
489
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
490
+ if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing publicKey parameter!');
491
+ if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing pairing_token parameter!');
492
+ await this.initializeZsm('webauthnCreate');
493
+
494
+ let createResponse = await this.zsmApi.webauthn_create(publicKey, pairing_token, userIdentifier);
495
+ if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain response from Crypto Server!');
496
+
497
+ createResponse = toObject(createResponse);
498
+ GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
499
+
500
+ if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain credential from Crypto Server!');
501
+ if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to acquire attestation object!');
502
+
503
+ const credential = createResponse.result;
504
+ return credential;
505
+ };
506
+
507
+ /**
508
+ * @name webauthnGetFromPKP
509
+ * @description Retrieves a WebAuthn credential for the specified userIdentifier.
510
+ * @param {string} userIdentifier The user identifier.
511
+ * @param {string} publicKey The public key for the authentication process.
512
+ * @param {string} pairing_token The pairing token for the authentication process.
513
+ * @returns {Promise<ZSMAPI>} Resolves with the retrieved credential.
514
+ * @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
515
+ * @throws {Error} The class-level credential is missing for the specified userIdentifier.
516
+ * @throws {Error} (MPC Round) Crypto Server did not respond.
517
+ * @throws {Error} (MPC Round) Response from Crypto Server did not contain a credential.
518
+ * @throws {Error} Could not verify credential with Relying Party Server.
519
+ * @throws {Error} Token mismatched or missing from Relying Party Server verification attempt.
520
+ * @memberof WebAuthnClientBase
521
+ * @async
522
+ */
523
+ async webauthnGetFromPKP (userIdentifier, publicKey, pairing_token) {
524
+ if(userIdentifier == null
525
+ || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: webauthnGetFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
526
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
527
+ if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing publicKey parameter!');
528
+ if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing pairing_token parameter!');
529
+
530
+
531
+ let mpcResponse = await this.zsmApi.webauthn_get(publicKey, pairing_token, userIdentifier);
532
+ if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Crypto server did not respond!');
533
+
534
+ mpcResponse = toObject(mpcResponse);
535
+ if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
536
+
537
+ GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
538
+
539
+ const credential = mpcResponse.result;
540
+ return credential;
541
+ };
542
+
543
+ /**
544
+ * @name pkpCreate
545
+ * @description Creates a Passkeys+ credential for the user, if the user is not already enrolled, then creates a ZSM credential.
546
+ * @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
547
+ * @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
548
+ * @param {boolean} bootstrapping If true, indicates that this is part of a bootstrapping flow from another device.
549
+ * @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnCreate method
550
+ * @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
551
+ * @throws {Error} No response is received from the Relying Party Server.
552
+ * @throws {Error} Relying Party Challenge does not contain a publicKey.
553
+ * @throws {Error} If the Platform Authenticator Credential is not created successfully.
554
+ * @throws {Error} If the Platform Authenticator response is invalid.
555
+ * @memberof WebAuthnClientBase
556
+ * @async
557
+ */
558
+ async pkpCreate (userIdentifier, userVerified="preferred", bootstrapping=false) {
559
+
560
+ let zsmCredentialChainID = null;
561
+ if(bootstrapping && typeof userIdentifier === 'object') {
562
+ const credentialDataObject = Object.assign({}, userIdentifier);
563
+ zsmCredentialChainID = userIdentifier.zsmCredentialID; // [J] Was .zsmCredID; now uses cleanFinalData field name
564
+ this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
565
+
566
+
567
+ if(userIdentifier == null || typeof userIdentifier !== 'string'
568
+ || userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
569
+ this.userIdentifier = userIdentifier;
570
+
571
+ const relyingPartyChallenge = await this.relyingParty.pkpAuthenticationStart(credentialDataObject);
572
+ if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
573
+
574
+
575
+ const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(relyingPartyChallenge, userVerified);
576
+ if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
577
+
578
+ const rpRegistrationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
579
+ if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
580
+
581
+ const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
582
+
583
+ let zsm_create = await this.webauthnGetFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
584
+ this.zsmCredentialId = zsm_create.rawId;
585
+
586
+ return Promise.resolve(zsm_create);
587
+ }else{
588
+
589
+ if(userIdentifier == null || typeof userIdentifier !== 'string'
590
+ || userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
591
+ this.userIdentifier = userIdentifier;
592
+
593
+
594
+ const relyingPartyChallenge = await this.relyingParty.pkpRegistrationStart(userIdentifier, userVerified, zsmCredentialChainID);
595
+ if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
596
+
597
+ const rpChallengePublicKey = relyingPartyChallenge?.publicKey;
598
+ if(!rpChallengePublicKey) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No publicKey found in Relying Party Challenge!`);
599
+
600
+ const paCredential = await this.passkeysPlus.pkpCreatePasskeyCredential(rpChallengePublicKey, userVerified);
601
+ if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
602
+
603
+ const rpRegistrationResult = await this.relyingParty.pkpRegistrationFinish(paCredential);
604
+ if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
605
+
606
+ uid2iid('pk:' + userIdentifier, paCredential.id);
607
+
608
+ const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
609
+
610
+ let zsm_create = await this.webauthnCreateFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
611
+ this.zsmCredentialId = zsm_create.rawId;
612
+
613
+ return Promise.resolve(zsm_create);
614
+ }
615
+ }
616
+
617
+ /**
618
+ * @name pkpAuthenticate
619
+ * @description Authenticates a user using a Passkeys+ credential.
620
+ * @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
621
+ * @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
622
+ * @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnGet method
623
+ * @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
624
+ * @throws {Error} If no Platform Authenticator Credential ID is found for the user.
625
+ * @throws {Error} If the Platform Authority Challenge is not retrieved successfully.
626
+ * @throws {Error} If the Platform Authenticator Credential is not created successfully.
627
+ * @throws {Error} If the Platform Authenticator response is invalid.
628
+ * @memberof WebAuthnClientBase
629
+ * @async
630
+ */
631
+ async pkpAuthenticate (userIdentifier, userVerified="preferred", bootstrapping=false) {
632
+ let enrollmentDetails = null;
633
+ if(userIdentifier && typeof userIdentifier === 'object') {
634
+ enrollmentDetails = {...userIdentifier};
635
+ this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
636
+ }
637
+ if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
638
+
639
+ let platformAuthorityChallenge = null;
640
+
641
+ // Normal authentication flow
642
+ this.userIdentifier = userIdentifier;
643
+ // const completeUserIdentity = await this.webauthnRetrieveAll(userIdentifier);
644
+ platformAuthorityChallenge = await this.relyingParty.pkpAuthenticationStart(enrollmentDetails); // [J] Updated to use cleanFinalData field names; will pass null if we have no info on passkey presence, allowing the server to determine eligibility based on userIdentifier alone (and potentially return a remote credential ID if the user is eligible for Passkeys+ but doesn't have a local passkey)
645
+ if(!platformAuthorityChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to retrieve Platform Authority Challenge from Relying Party Server!`);
646
+
647
+ const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(platformAuthorityChallenge, userVerified);
648
+ if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to call Platform Authenticator to create Passkey!');
649
+
650
+ const paAuthenticationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
651
+ if (!paAuthenticationResult) throw new Error('[PK+ WebAuthnClient] :: pkpAuthenticate :: Platform Authenticator response was invalid!');
652
+
653
+ const publicKey = paAuthenticationResult?.response?.ccr?.publicKey??paAuthenticationResult?.response?.rcr?.publicKey;
654
+
655
+ const getResult = bootstrapping ? await this.webauthnCreateFromPKP(userIdentifier, publicKey, paAuthenticationResult.response.token)
656
+ : await this.webauthnGetFromPKP(userIdentifier, publicKey, paAuthenticationResult.response.token);
657
+
658
+ return getResult;
659
+ }
660
+
661
+ /**
662
+ * @name pkpAddZSMCredential
663
+ * @description Adds a ZSM credential for a user who already has Passkeys+ credentials registered on another device.
664
+ * @param {string} userIdentifier The identifier for the user.
665
+ * @returns {Promise<Object>} Completed ZSM + Passkeys+ authentication result.
666
+ * @throws {Error} If no Passkeys+ credentials are found for the user on another device.
667
+ * @memberof WebAuthnClientBase
668
+ * @async
669
+ */
670
+ async pkpAddZSMCredential (userIdentifier) {
671
+ if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAddZSMCredential :: A userIdentifier String is required! Received: ${userIdentifier}.`);
672
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
673
+ const bootstrapChallenge = await this.webauthnRetrieveAll(userIdentifier);
674
+ if(bootstrapChallenge === false) throw new Error('[PK+ WebAuthnClient] :: pkpAddZSMCredential :: Cannot add ZSM; no passkey credentials registered for this user!');
675
+
676
+ bootstrapChallenge.userIdentifier = userIdentifier;
677
+
678
+
679
+ // userVerified set to 'required' to ensure strong authentication when adding ZSM credential
680
+ return this.pkpAuthenticate(bootstrapChallenge, 'required', true);
681
+ }
682
+
683
+ /**
684
+ * @name pkpAddPasskeyCredential
685
+ * @description Adds a Passkeys+ credential for a user who already has a ZSM credential registered.
686
+ * @param {string} userIdentifier The user identifier.
687
+ * @returns {Promise<Object>} Call to pkpCreate to add Passkeys+ credential.
688
+ * @throws {Error} If the user is not eligible to add a Passkeys+ credential.
689
+ * @memberof WebAuthnClientBase
690
+ * @async
691
+ */
692
+ async pkpAddPasskeyCredential (userIdentifier, userVerified="preferred") {
693
+ let enrollmentDetails = null;
694
+ if(userIdentifier && typeof userIdentifier === 'object') {
695
+ enrollmentDetails = {...userIdentifier};
696
+ this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
697
+ }else{
698
+ this.userIdentifier = userIdentifier;
699
+ enrollmentDetails = await this.webauthnRetrieveAll(userIdentifier, true);
700
+ };
701
+
702
+ if (enrollmentDetails instanceof Error) throw new Error(`[WebAuthnClient] :: pkpAddPasskeyCredential :: Unable to retrieve existing credentials for userIdentifier: ${userIdentifier}. Details: ${enrollmentDetails.message || enrollmentDetails}`);
703
+ if (!enrollmentDetails?.zsmCredentialID) throw new Error(`[WebAuthnClient] :: pkpAddPasskeyCredential :: userIdentifier: ${userIdentifier} does not have an existing ZSM credential!`); // [J] Was .hasZSMCred
704
+
705
+ return this.pkpCreate ( // [J] Updated to use cleanFinalData field names
706
+ !enrollmentDetails.hasLocalPasskey ? userIdentifier : Object.assign({}, enrollmentDetails, { userIdentifier }), // [J] Was enrollmentDetails.userIdentifier; now attaches userIdentifier to the object for pkpCreate's bootstrapping path
707
+ userVerified,
708
+ enrollmentDetails.hasLocalPasskey // [J] Was .hasLocalPasskey
709
+ );
710
+ }
711
+
712
+ /**
713
+ * @name checkForPasskeysOnOtherDevice
714
+ * @description Checks if Passkeys+ credentials are registered for the specified userIdentifier on another device.
715
+ * @ (If found, converts challenge data to ArrayBuffers for WebAuthn API consumption.)
716
+ * @param {string} userIdentifier The user identifier.
717
+ * @returns {object} Returns the bootstrap challenge object if passkey(s) are found, or...
718
+ * @returns {boolean} ...returns false if no passkey credentials are found.
719
+ * @memberof WebAuthnClientBase
720
+ * @async
721
+ */
722
+ async checkForPasskeysOnOtherDevice (userIdentifier) {
723
+ if(userIdentifier == null
724
+ || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: checkForPasskeysOnOtherDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
725
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
726
+ try {
727
+
728
+ this.userIdentifier = userIdentifier;
729
+ const bootstrapChallenge = await this.relyingParty.makePostRequest( `api/passkey/bootstrap/start`)
730
+ .catch( error => {
731
+ return false;
732
+ });
733
+
734
+ if(!bootstrapChallenge || bootstrapChallenge instanceof Error) return false;
735
+
736
+ if((bootstrapChallenge?.rcr?.publicKey?.allowCredentials?.length??0) === 0) return false;
737
+ if((bootstrapChallenge?.rcr?.publicKey?.allowCredentials?.length??0) >= 1) {
738
+ uid2iid('pk:' + userIdentifier, bootstrapChallenge.rcr.publicKey.allowCredentials[0].id);
739
+ return bootstrapChallenge.rcr.publicKey.allowCredentials[0].id;
740
+ }
741
+
742
+ if(bootstrapChallenge?.rcr?.publicKey?.challenge) bootstrapChallenge.rcr.publicKey.challenge = this.passkeysPlus.toArrayBuffer(bootstrapChallenge.rcr.publicKey.challenge);
743
+ if(bootstrapChallenge?.rcr?.publicKey?.allowCredentials) {
744
+ bootstrapChallenge.rcr.publicKey.allowCredentials = bootstrapChallenge.rcr.publicKey.allowCredentials.map(cred => {
745
+ return {
746
+ ...cred,
747
+ id: this.passkeysPlus.toArrayBuffer(cred.id)
748
+ };
749
+ });
750
+ }
751
+ return bootstrapChallenge;
752
+ } catch (error) {
753
+ return false;
754
+ }
755
+ }
438
756
  /**
439
757
  * Resets the WebAuthn credential for the user.
440
758
  * @param {string} userIdentifier - The userIdentifier.