@ideem/zsm-client-sdk 2.7.1 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,135 @@ 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 = !!navigator?.credentials;
214
+ // } catch { /* unsupported environment */ }
215
+
216
+ this.credentialID = localData['zsm_credential_id']??null;
217
+
218
+ // Server consultation: only needed when local passkey knowledge is insufficient // [J] Replaced hasFailingStatus shotgun check with targeted remote passkey logic
219
+ const shouldCheckServer = forceRemoteCheck
220
+ || (passkeySupported && !localData?.['has_remote_passkey'] && !localData?.['passkey_credential_id'])
221
+ || !localData?.['zsm_active']
222
+ || !localData?.['zsm_active']
223
+
224
+ let completeData = localData;
225
+ if(shouldCheckServer) {
226
+ let remoteData = await this.checkRemoteEnrollmentDetails(userIdentifier, completeData); // Grab the remote enrollment details...
227
+
228
+ if(remoteData && !(remoteData instanceof Error)) { // ... and if we got something back (and it wasn't an error)...
229
+ completeData = Object.assign({}, localData, remoteData); // ...merge it with completeData (giving remote precedence)
230
+ // if(remoteData?.['rcr']) // Persist remote passkey discovery locally to prevent future RTTs
231
+ // this.zsmApi.updateEnrollmentStatus(userIdentifier, "passkey"); // [J] Fire-and-forget; next webauthn_retrieve will return hasRemotePasskey locally
232
+ }
233
+ }
234
+
235
+
236
+
237
+ const cleanFinalData = { // Finally, clean up the net enrollment details results...
238
+ userIdentifier,
239
+ enrollmentActive : completeData?.['zsm_active'] ?? false,
240
+ passkeySupported, // [J] Device/browser supports platform authenticator (best-effort)
241
+ passkeyEligible : passkeySupported && !!(completeData?.['passkey_credential_id'] || completeData?.['has_remote_passkey']), // [J] Passkeys supported AND a passkey credential exists to use
242
+ hasLocalPasskey : passkeySupported && !!(completeData?.['passkey_credential_id']??false), // [J] Now sourced from WASM response directly
243
+ hasRemotePasskey : !!completeData?.['rcr'] ?? null, // [J] null = unknown; false = confirmed absent; true = confirmed present
244
+ 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
245
+ 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
246
+ hasZSMCred : !!completeData?.['zsm_credential'] || false,
247
+ zsmCredential : completeData?.['zsm_credential'] ?? null,
248
+ zsmCredentialID : completeData?.['zsm_credential_id'] ?? null,
249
+ zsmCredID : completeData?.['zsm_credential_id'] ?? null
250
+ }
251
+ if(cleanFinalData.hasLocalPasskey === true) cleanFinalData.hasRemotePasskey = true; // [J] Sanity check: local passkey presence implies remote passkey presence
252
+ if(completeData?.['rcr']) {
253
+ 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+)
254
+ cleanFinalData.hasLocalPasskey = true;
255
+ if (!cleanFinalData.pkpCredentialID) {
256
+ cleanFinalData.pkpCredentialID = completeData['rcr']?.publicKey?.allowCredentials?.flatMap(c=>c.id)??null;
257
+ 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
258
+ }
259
+ }
260
+
261
+ // console.log('[CORE] :: [WebAuthnClientBase] :: webauthnRetrieve :: cleanFinalData :', cleanFinalData);
262
+ return cleanFinalData;
263
+ };
264
+
265
+ /**
266
+ * @name webauthnRetrieveAll
267
+ * @description Syntactic sugar for webauthnRetrieve with returnExpandedDetails=true to retrieve all enrollment information.
268
+ * @param {string} userIdentifier The user identifier.
269
+ * @param {boolean} forceRemoteCheck If true, forces server consultation even when hasRemotePasskey is false (use before enrollment workflows).
270
+ * @returns {Promise<Object>} Returns the full enrollment details for the user.
271
+ * @memberof WebAuthnClientBase
272
+ */
273
+ async webauthnRetrieveAll (userIdentifier, forceRemoteCheck=false) {
274
+ const allEnrollmentDetails = await this.webauthnRetrieve(userIdentifier, true, forceRemoteCheck);
275
+ return allEnrollmentDetails;
276
+ };
277
+
278
+ /**
279
+ * @name checkRemoteEnrollmentDetails
280
+ * @description Checks the server for remote enrollment details when local data has failing status flags.
281
+ * @param {string} userIdentifier The user identifier.
282
+ * @param {Object} localPayload The local enrollment data to send to the server for comparison.
283
+ * @returns {Promise<Object>} Returns remote enrollment details, or false if none found.
284
+ * @throws {Error} Throws if the server returns an error.
285
+ * @memberof WebAuthnClientBase
286
+ */
287
+ async checkRemoteEnrollmentDetails (userIdentifier, localPayload) {
288
+ try {
289
+ const allEnrollmentDetails = await this.relyingParty.getRemoteEnrollmentDetails(userIdentifier, localPayload);
290
+ if(allEnrollmentDetails instanceof Error || !allEnrollmentDetails || allEnrollmentDetails['identity_id'] == null) return false;
291
+ return allEnrollmentDetails; // [J] Fixed: was missing return
292
+ } catch(err) {
293
+ console.error('Error checking remote enrollment details:', err);
294
+ return false;
295
+ }
296
+ }
297
+
165
298
  /**
166
299
  * @name webauthnCreate
167
300
  * @description Creates a new WebAuthn credential for the specified userIdentifier.
@@ -174,8 +307,7 @@ class WebAuthnClientBase {
174
307
  * @throws {Error} Throws a COUNTED error if unable to verify the credential with the Relying Party Server.
175
308
  */
176
309
  async webauthnCreate (userIdentifier) {
177
- if(userIdentifier == null
178
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
310
+ if(!userIdentifier) throw new Error(`[WebAuthnClient] :: webauthnCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
179
311
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
180
312
 
181
313
  await this.initializeZsm('webauthnCreate');
@@ -186,62 +318,26 @@ class WebAuthnClientBase {
186
318
  else if (!publicKey) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to retrieve public key from Relying Party Server!');
187
319
 
188
320
  publicKey.user_id = publicKey.user.id;
189
-
321
+
190
322
  let createResponse = await this.zsmApi.webauthn_create(publicKey, undefined, userIdentifier, tracePrimer());
191
323
  if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain response from Crypto Server!');
192
324
 
193
-
325
+
194
326
  createResponse = toObject(createResponse);
195
327
  GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
196
-
328
+
197
329
  if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to obtain credential from Crypto Server!');
198
330
  if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreate :: Unable to acquire attestation object!');
199
-
331
+
200
332
  const credential = createResponse.result;
201
333
 
202
-
334
+
203
335
  const registrationResult = await this.relyingParty.registrationFinish(credential);
204
336
  if(registrationResult instanceof Error) throw new Error(registrationResult);
205
337
  else if(registrationResult == false) throw new Error('[WebAuthnClient] :: webauthnCreate :: Unable to verify credential Relying Party Server!');
206
338
 
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
339
 
244
- return registrationResult.credential;
340
+ return registrationResult;
245
341
  };
246
342
 
247
343
  /**
@@ -257,12 +353,11 @@ class WebAuthnClientBase {
257
353
  * @throws {Error} Throws an error if the user ID or token is missing from the response body.
258
354
  */
259
355
  async webauthnGet (userIdentifier) {
260
- if(userIdentifier == null
261
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
356
+ if(userIdentifier == null|| userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
262
357
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
263
358
 
264
359
  await this.initializeZsm('webauthnGet');
265
- const credentialId = await this.webauthnRetrieve(userIdentifier, true);
360
+ const credentialId = await this.webauthnRetrieve(userIdentifier, true); // [J] Now passes expanded details to authenticationStart
266
361
 
267
362
  if (!this.isEnrolled) throw new Error(`[WebAuthnClient] :: webauthnGet :: ${userIdentifier} is not enrolled, lacks a credential, or credential is corrupted!`);
268
363
 
@@ -270,14 +365,13 @@ class WebAuthnClientBase {
270
365
 
271
366
  if (authStartResponse instanceof Error) throw new Error(authStartResponse);
272
367
  else if (!authStartResponse) throw new Error('[WebAuthnClient] :: webauthnGet :: Unable to start authentication with Relying Party Server!');
273
-
368
+
274
369
  authStartResponse = toObject(authStartResponse);
275
370
 
276
371
  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
372
  const publicKey = authStartResponse.rcr.publicKey;
278
373
 
279
374
  let mpcResponse = await this.zsmApi.webauthn_get(publicKey, undefined, userIdentifier, tracePrimer());
280
-
281
375
 
282
376
  if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGet :: Crypto server did not respond!');
283
377
 
@@ -285,62 +379,7 @@ class WebAuthnClientBase {
285
379
  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
380
 
287
381
  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;
382
+ return mpcResponse.result;
344
383
  };
345
384
 
346
385
  /**
@@ -355,8 +394,8 @@ class WebAuthnClientBase {
355
394
  * @emits {CustomEvent} Emits a "MPCResponse" event with the mpcResponse data.
356
395
  */
357
396
  async webauthnPartialGet (userIdentifier) {
358
- if(userIdentifier == null
359
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: webauthnPartialGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
397
+ if(userIdentifier == null
398
+ || userIdentifier === '') throw new Error(`[WebAuthnClient] :: webauthnPartialGet :: A userIdentifier String is required! Received: ${userIdentifier}.`);
360
399
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
361
400
  this.status = 'AUTHENTICATION_STARTING';
362
401
  await this.initializeZsm('webauthnPartialGet');
@@ -379,16 +418,7 @@ class WebAuthnClientBase {
379
418
  if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnPartialGet :: Unable to obtain credential from Crypto Server!');
380
419
 
381
420
  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
- };
421
+ return mpcResponse.result;
392
422
  };
393
423
 
394
424
  /**
@@ -401,8 +431,8 @@ class WebAuthnClientBase {
401
431
  * @throws {Error<UnenrollError>} Throws an UnenrollError if a credential matching userIdentifier IS stored on the device, but was unable to be removed.
402
432
  */
403
433
  async unbindFromDevice (userIdentifier) {
404
- if(userIdentifier == null
405
- || userIdentifier === '') return new Error(`[WebAuthnClient] :: unbindFromDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
434
+ if(userIdentifier == null
435
+ || userIdentifier === '') throw new Error(`[WebAuthnClient] :: unbindFromDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
406
436
  if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
407
437
  this.status = 'UNENROLL_STARTING';
408
438
  if(userIdentifier !== this.userIdentifier) return false;
@@ -420,9 +450,9 @@ class WebAuthnClientBase {
420
450
  if (cursor) {
421
451
  const { key, value } = cursor;
422
452
  if(
423
- key.includes(userIdentifier) ||
453
+ key.includes(userIdentifier) ||
424
454
  (typeof value === 'string' && value.includes(userIdentifier)) ||
425
- typeof value === 'object' && value?.zsm_credential?.get('rawId') === rawID
455
+ typeof value === 'object' && value?.zsmCredential?.get('rawId') === rawID
426
456
  ) (store.delete(key), true);
427
457
  cursor.continue();
428
458
  }
@@ -435,6 +465,292 @@ class WebAuthnClientBase {
435
465
  return true;
436
466
  }
437
467
 
468
+ // PKP Methods ============================================================================================================
469
+
470
+ /**
471
+ * @name webauthnCreateFromPKP
472
+ * @description Constructs a new ZSM WebAuthn credential from a Passkeys Plus Platform Authenticator registration response.
473
+ * @param {string} userIdentifier The user identifier.
474
+ * @param {string} publicKey The public key for the enrollment process.
475
+ * @param {string} pairing_token The pairing token for the enrollment process.
476
+ * @returns {Promise<Object>} Resolves with the created credential.
477
+ * @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
478
+ * @throws {Error} (MPC Round) Crypto Server failed to respond.
479
+ * @throws {Error} (MPC Round) Response did not contain a valid credential/attestation object.
480
+ * @throws {Error} Could not verify credential with Relying Party Server.
481
+ * @memberof WebAuthnClientBase
482
+ * @async
483
+ */
484
+ async webauthnCreateFromPKP (userIdentifier, publicKey, pairing_token) {
485
+ if(userIdentifier == null
486
+ || userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: webauthnCreateFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
487
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
488
+ if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing publicKey parameter!');
489
+ if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnCreateFromPKP :: Missing pairing_token parameter!');
490
+ await this.initializeZsm('webauthnCreate');
491
+
492
+ let createResponse = await this.zsmApi.webauthn_create(publicKey, pairing_token, userIdentifier);
493
+ if(!createResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain response from Crypto Server!');
494
+
495
+ createResponse = toObject(createResponse);
496
+ GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: createResponse }));
497
+
498
+ if(!createResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to obtain credential from Crypto Server!');
499
+ if(!createResponse?.result?.response) throw new Error('[WebAuthnClient - mpc_round] :: webauthnCreateFromPKP :: Unable to acquire attestation object!');
500
+
501
+ const credential = createResponse.result;
502
+ return credential;
503
+ };
504
+
505
+ /**
506
+ * @name webauthnGetFromPKP
507
+ * @description Retrieves a WebAuthn credential for the specified userIdentifier.
508
+ * @param {string} userIdentifier The user identifier.
509
+ * @param {string} publicKey The public key for the authentication process.
510
+ * @param {string} pairing_token The pairing token for the authentication process.
511
+ * @returns {Promise<ZSMAPI>} Resolves with the retrieved credential.
512
+ * @throws {TypeError} userIdentifier, publicKey, or pairing_token parameters are missing.
513
+ * @throws {Error} The class-level credential is missing for the specified userIdentifier.
514
+ * @throws {Error} (MPC Round) Crypto Server did not respond.
515
+ * @throws {Error} (MPC Round) Response from Crypto Server did not contain a credential.
516
+ * @throws {Error} Could not verify credential with Relying Party Server.
517
+ * @throws {Error} Token mismatched or missing from Relying Party Server verification attempt.
518
+ * @memberof WebAuthnClientBase
519
+ * @async
520
+ */
521
+ async webauthnGetFromPKP (userIdentifier, publicKey, pairing_token) {
522
+ if(userIdentifier == null
523
+ || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: webauthnGetFromPKP :: A userIdentifier String is required! Received: ${userIdentifier}.`);
524
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
525
+ if(!publicKey) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing publicKey parameter!');
526
+ if(!pairing_token) throw new TypeError('[WebAuthnClient] :: webauthnGetFromPKP :: Missing pairing_token parameter!');
527
+
528
+
529
+ let mpcResponse = await this.zsmApi.webauthn_get(publicKey, pairing_token, userIdentifier);
530
+ if(!mpcResponse) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Crypto server did not respond!');
531
+
532
+ mpcResponse = toObject(mpcResponse);
533
+ if(!mpcResponse?.result) throw new Error('[WebAuthnClient - mpc_round] :: webauthnGetFromPKP :: Response from Crypto Server did not contain a credential!\nResponse Body: ' + JSON.stringify(mpcResponse));
534
+
535
+ GLOBAL.dispatchEvent(new CustomEvent("MPCResponse", {bubbles: true, cancelable: false, detail: mpcResponse }));
536
+
537
+ const credential = mpcResponse.result;
538
+ return credential;
539
+ };
540
+
541
+ /**
542
+ * @name pkpCreate
543
+ * @description Creates a Passkeys+ credential for the user, if the user is not already enrolled, then creates a ZSM credential.
544
+ * @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
545
+ * @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
546
+ * @param {boolean} bootstrapping If true, indicates that this is part of a bootstrapping flow from another device.
547
+ * @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnCreate method
548
+ * @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
549
+ * @throws {Error} No response is received from the Relying Party Server.
550
+ * @throws {Error} Relying Party Challenge does not contain a publicKey.
551
+ * @throws {Error} If the Platform Authenticator Credential is not created successfully.
552
+ * @throws {Error} If the Platform Authenticator response is invalid.
553
+ * @memberof WebAuthnClientBase
554
+ * @async
555
+ */
556
+ async pkpCreate (userIdentifier, userVerified="preferred", bootstrapping=false) {
557
+
558
+ let zsmCredentialChainID = null;
559
+ if(bootstrapping && typeof userIdentifier === 'object') {
560
+ const credentialDataObject = Object.assign({}, userIdentifier);
561
+ zsmCredentialChainID = userIdentifier.zsmCredentialID; // [J] Was .zsmCredID; now uses cleanFinalData field name
562
+ this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
563
+
564
+
565
+ if(userIdentifier == null || typeof userIdentifier !== 'string'
566
+ || userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
567
+ this.userIdentifier = userIdentifier;
568
+
569
+ const relyingPartyChallenge = await this.relyingParty.pkpAuthenticationStart(credentialDataObject);
570
+ if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
571
+
572
+
573
+ const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(relyingPartyChallenge, userVerified);
574
+ if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
575
+
576
+ const rpRegistrationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
577
+ if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
578
+
579
+ const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
580
+
581
+ let zsm_create = await this.webauthnGetFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
582
+ this.zsmCredentialId = zsm_create.rawId;
583
+
584
+ return Promise.resolve(zsm_create);
585
+ }else{
586
+
587
+ if(userIdentifier == null || typeof userIdentifier !== 'string'
588
+ || userIdentifier === '') throw new TypeError(`[PK+ WebAuthnClient] :: pkpCreate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
589
+ this.userIdentifier = userIdentifier;
590
+
591
+
592
+ const relyingPartyChallenge = await this.relyingParty.pkpRegistrationStart(userIdentifier, userVerified, zsmCredentialChainID);
593
+ if(!relyingPartyChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No response from ZSM API's attempt to interface with Relying Party Server!`);
594
+
595
+ const rpChallengePublicKey = relyingPartyChallenge?.publicKey;
596
+ if(!rpChallengePublicKey) throw new Error(`[PK+ WebAuthnClient] :: pkpCreate :: No publicKey found in Relying Party Challenge!`);
597
+
598
+ const paCredential = await this.passkeysPlus.pkpCreatePasskeyCredential(rpChallengePublicKey, userVerified);
599
+ if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Unable to call Platform Authenticator to create Passkey!');
600
+
601
+ const rpRegistrationResult = await this.relyingParty.pkpRegistrationFinish(paCredential);
602
+ if (!rpRegistrationResult) throw new Error('[PK+ WebAuthnClient] :: pkpCreate :: Platform Authenticator response was invalid!');
603
+
604
+ uid2iid('pk:' + userIdentifier, paCredential.id);
605
+
606
+ const publicKey = rpRegistrationResult?.response?.ccr?.publicKey??rpRegistrationResult?.response?.rcr?.publicKey;
607
+
608
+ let zsm_create = await this.webauthnCreateFromPKP(userIdentifier, publicKey, rpRegistrationResult.response.token);
609
+ this.zsmCredentialId = zsm_create.rawId;
610
+
611
+ return Promise.resolve(zsm_create);
612
+ }
613
+ }
614
+
615
+ /**
616
+ * @name pkpAuthenticate
617
+ * @description Authenticates a user using a Passkeys+ credential.
618
+ * @param {string|object} userIdentifier The identifier for the user (or bootstrapping challenge object).
619
+ * @param {string} userVerified Accepts "required", "preferred", "discouraged", to be passed into the Passkey ceremony.
620
+ * @returns {Promise<Object>} Call to WebAuthnClientBase's webauthnGet method
621
+ * @throws {TypeError} The userIdentifier is not provided, not a string, or is empty.
622
+ * @throws {Error} If no Platform Authenticator Credential ID is found for the user.
623
+ * @throws {Error} If the Platform Authority Challenge is not retrieved successfully.
624
+ * @throws {Error} If the Platform Authenticator Credential is not created successfully.
625
+ * @throws {Error} If the Platform Authenticator response is invalid.
626
+ * @memberof WebAuthnClientBase
627
+ * @async
628
+ */
629
+ async pkpAuthenticate (userIdentifier, userVerified="preferred", bootstrapping=false) {
630
+ let enrollmentDetails = null;
631
+ if(userIdentifier && typeof userIdentifier === 'object') {
632
+ enrollmentDetails = {...userIdentifier};
633
+ this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
634
+ }
635
+ if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: A userIdentifier String is required! Received: ${userIdentifier}.`);
636
+
637
+ let platformAuthorityChallenge = null;
638
+
639
+ // Normal authentication flow
640
+ this.userIdentifier = userIdentifier;
641
+ // const completeUserIdentity = await this.webauthnRetrieveAll(userIdentifier);
642
+ 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)
643
+ if(!platformAuthorityChallenge) throw new Error(`[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to retrieve Platform Authority Challenge from Relying Party Server!`);
644
+
645
+ const paCredential = await this.passkeysPlus.pkpGetPasskeyCredential(platformAuthorityChallenge, userVerified);
646
+ if (!paCredential) throw new Error('[PK+ WebAuthnClient] :: pkpAuthenticate :: Unable to call Platform Authenticator to create Passkey!');
647
+
648
+ const paAuthenticationResult = await this.relyingParty.pkpAuthenticationFinish(paCredential);
649
+ if (!paAuthenticationResult) throw new Error('[PK+ WebAuthnClient] :: pkpAuthenticate :: Platform Authenticator response was invalid!');
650
+
651
+ const publicKey = paAuthenticationResult?.response?.ccr?.publicKey??paAuthenticationResult?.response?.rcr?.publicKey;
652
+
653
+ const getResult = bootstrapping ? await this.webauthnCreateFromPKP(userIdentifier, publicKey, paAuthenticationResult.response.token)
654
+ : await this.webauthnGetFromPKP(userIdentifier, publicKey, paAuthenticationResult.response.token);
655
+
656
+ return getResult;
657
+ }
658
+
659
+ /**
660
+ * @name pkpAddZSMCredential
661
+ * @description Adds a ZSM credential for a user who already has Passkeys+ credentials registered on another device.
662
+ * @param {string} userIdentifier The identifier for the user.
663
+ * @returns {Promise<Object>} Completed ZSM + Passkeys+ authentication result.
664
+ * @throws {Error} If no Passkeys+ credentials are found for the user on another device.
665
+ * @memberof WebAuthnClientBase
666
+ * @async
667
+ */
668
+ async pkpAddZSMCredential (userIdentifier) {
669
+ if(userIdentifier == null || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: pkpAddZSMCredential :: A userIdentifier String is required! Received: ${userIdentifier}.`);
670
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
671
+ const bootstrapChallenge = await this.webauthnRetrieveAll(userIdentifier);
672
+ if(bootstrapChallenge === false) throw new Error('[PK+ WebAuthnClient] :: pkpAddZSMCredential :: Cannot add ZSM; no passkey credentials registered for this user!');
673
+
674
+ bootstrapChallenge.userIdentifier = userIdentifier;
675
+
676
+
677
+ // userVerified set to 'required' to ensure strong authentication when adding ZSM credential
678
+ return this.pkpAuthenticate(bootstrapChallenge, 'required', true);
679
+ }
680
+
681
+ /**
682
+ * @name pkpAddPasskeyCredential
683
+ * @description Adds a Passkeys+ credential for a user who already has a ZSM credential registered.
684
+ * @param {string} userIdentifier The user identifier.
685
+ * @returns {Promise<Object>} Call to pkpCreate to add Passkeys+ credential.
686
+ * @throws {Error} If the user is not eligible to add a Passkeys+ credential.
687
+ * @memberof WebAuthnClientBase
688
+ * @async
689
+ */
690
+ async pkpAddPasskeyCredential (userIdentifier, userVerified="preferred") {
691
+ let enrollmentDetails = null;
692
+ if(userIdentifier && typeof userIdentifier === 'object') {
693
+ enrollmentDetails = {...userIdentifier};
694
+ this.userIdentifier = userIdentifier = userIdentifier.userIdentifier;
695
+ }else{
696
+ this.userIdentifier = userIdentifier;
697
+ enrollmentDetails = await this.webauthnRetrieveAll(userIdentifier, true);
698
+ };
699
+
700
+ if (enrollmentDetails instanceof Error) throw new Error(`[WebAuthnClient] :: pkpAddPasskeyCredential :: Unable to retrieve existing credentials for userIdentifier: ${userIdentifier}. Details: ${enrollmentDetails.message || enrollmentDetails}`);
701
+ if (!enrollmentDetails?.zsmCredentialID) throw new Error(`[WebAuthnClient] :: pkpAddPasskeyCredential :: userIdentifier: ${userIdentifier} does not have an existing ZSM credential!`); // [J] Was .hasZSMCred
702
+
703
+ return this.pkpCreate ( // [J] Updated to use cleanFinalData field names
704
+ !enrollmentDetails.hasLocalPasskey ? userIdentifier : Object.assign({}, enrollmentDetails, { userIdentifier }), // [J] Was enrollmentDetails.userIdentifier; now attaches userIdentifier to the object for pkpCreate's bootstrapping path
705
+ userVerified,
706
+ enrollmentDetails.hasLocalPasskey // [J] Was .hasLocalPasskey
707
+ );
708
+ }
709
+
710
+ /**
711
+ * @name checkForPasskeysOnOtherDevice
712
+ * @description Checks if Passkeys+ credentials are registered for the specified userIdentifier on another device.
713
+ * @ (If found, converts challenge data to ArrayBuffers for WebAuthn API consumption.)
714
+ * @param {string} userIdentifier The user identifier.
715
+ * @returns {object} Returns the bootstrap challenge object if passkey(s) are found, or...
716
+ * @returns {boolean} ...returns false if no passkey credentials are found.
717
+ * @memberof WebAuthnClientBase
718
+ * @async
719
+ */
720
+ async checkForPasskeysOnOtherDevice (userIdentifier) {
721
+ if(userIdentifier == null
722
+ || userIdentifier === '') throw new Error(`[PK+ WebAuthnClient] :: checkForPasskeysOnOtherDevice :: A userIdentifier String is required! Received: ${userIdentifier}.`);
723
+ if(userIdentifier && typeof userIdentifier === 'string') this.userIdentifier = userIdentifier;
724
+ try {
725
+
726
+ this.userIdentifier = userIdentifier;
727
+ const bootstrapChallenge = await this.relyingParty.makePostRequest( `api/passkey/bootstrap/start`)
728
+ .catch( error => {
729
+ return false;
730
+ });
731
+
732
+ if(!bootstrapChallenge || bootstrapChallenge instanceof Error) return false;
733
+
734
+ if((bootstrapChallenge?.rcr?.publicKey?.allowCredentials?.length??0) === 0) return false;
735
+ if((bootstrapChallenge?.rcr?.publicKey?.allowCredentials?.length??0) >= 1) {
736
+ uid2iid('pk:' + userIdentifier, bootstrapChallenge.rcr.publicKey.allowCredentials[0].id);
737
+ return bootstrapChallenge.rcr.publicKey.allowCredentials[0].id;
738
+ }
739
+
740
+ if(bootstrapChallenge?.rcr?.publicKey?.challenge) bootstrapChallenge.rcr.publicKey.challenge = this.passkeysPlus.toArrayBuffer(bootstrapChallenge.rcr.publicKey.challenge);
741
+ if(bootstrapChallenge?.rcr?.publicKey?.allowCredentials) {
742
+ bootstrapChallenge.rcr.publicKey.allowCredentials = bootstrapChallenge.rcr.publicKey.allowCredentials.map(cred => {
743
+ return {
744
+ ...cred,
745
+ id: this.passkeysPlus.toArrayBuffer(cred.id)
746
+ };
747
+ });
748
+ }
749
+ return bootstrapChallenge;
750
+ } catch (error) {
751
+ return false;
752
+ }
753
+ }
438
754
  /**
439
755
  * Resets the WebAuthn credential for the user.
440
756
  * @param {string} userIdentifier - The userIdentifier.