@ideem/zsm-client-sdk 2.9.3 → 2.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -12,7 +12,75 @@ and Universal MFA into a browser application.
12
12
 
13
13
  ----
14
14
 
15
- ## Release 2.9.3 - August 13, 2026 [ NPM PACKAGE RELEASE ] **latest release**
15
+ ## Release 2.9.5 - October 30, 2026 **latest release**
16
+ - `checkAllEnrollments()` now consults the Ideem server on every call. Earlier releases skipped the server whenever the local record looked complete, so the device's own view of an enrollment could go unverified indefinitely. The `forceRemoteCheck` argument is still accepted but no longer has any effect, and `remoteCheckStatus` is never `"skipped"`. A call whose server check fails still resolves with the local data, as described below.
17
+ - `checkAllEnrollments()` now reports a server check that failed with an HTTP error separately from one that got no answer at all. Earlier releases reported both as `"unavailable"`, so a misconfiguration (HTTP 400) and a rejected API key (HTTP 401) looked identical, and both looked like an unreachable server.
18
+ - `remoteCheckStatus` is `"error::NNN"` when the server answered with an HTTP error, `NNN` being its status. It remains `"unavailable"` only when no answer was received (offline, DNS, CORS).
19
+ - When the check fails and the device holds no enrollment, `state` is `"ERROR::NNN"` (or `"ERROR"` when there is no HTTP status) instead of `"NONE"`. An unverified `"NONE"` cannot be told apart from a user who is genuinely not enrolled, and acting on it sends an enrolled user back through enrollment. A local `"BOTH"` or `"MPC_ONLY"` is never overwritten, because the user can still authenticate with it. Test for a failure with `/^ERROR/`: the `::NNN` suffix is advisory and may be absent.
20
+ - The result carries a new `error` property: `{ status, traceId, text }` describing the failed check, or `null` when the check succeeded. `status` is `null` when no answer was received.
21
+ - A failed request to the Ideem server now produces an `Error` that keeps the HTTP status, even when the error response is not JSON (for example an HTML page from a proxy). Earlier releases could lose the status entirely in that case.
22
+ - Headers passed for a single request to the Ideem server no longer carry over into later requests.
23
+ - Handles a credential that the Ideem server reports as `replaced`, meaning it has been superseded by an enrollment elsewhere. Before resolving, `checkAllEnrollments()` removes that user's credential from the device, then resolves exactly as it would for a user the device has never seen: any remote passkey is still reported, but the device no longer holds or reports the ZSM credential. If the credential cannot be removed after two attempts, ALL local Ideem data on the device is deleted, including other users' credentials and the installation identifier. If even that fails, `checkAllEnrollments()` resolves with an `Error` whose `code` is `"ENROLLMENT_DETAILS_UNAVAILABLE"`.
24
+ - The result carries `replaced: true` when the server reported the device's credential as replaced and it is no longer on the device, and `localDataWiped: true` when removing it took deleting ALL local Ideem data. Both are `false` otherwise. After a wipe, the SDK reconfigures itself, so `enroll()` works in the same tab without a reload.
25
+ - A wipe that is blocked by an open connection (for example another tab) is now reported: `checkAllEnrollments()` resolves with an `Error` (`code` `"ENROLLMENT_DETAILS_UNAVAILABLE"`, `cause.code` `"LOCAL_DATA_WIPE_BLOCKED"`). Earlier releases reported success while the deletion was still queued.
26
+ - The credential is removed only when the device can read it back. If the local record cannot be read, nothing more is deleted and `checkAllEnrollments()` resolves with an `Error` (`cause.code` `"LOCAL_CREDENTIAL_UNREADABLE"`); a failed read no longer counts as a removal, and no longer leads to the full wipe.
27
+ - The credential is removed only when it is still the one the server was asked about. If another credential was stored meanwhile (for example by an enrollment in another tab), before or during the removal, it is left intact and the check is repeated once for that credential instead.
28
+ - `UMFAClient.authenticate()` resolves with an `Error` whose `code` is `"ENROLLMENT_REPLACED"` when the user's enrollment was replaced on another device: in passkey mode after `checkAllEnrollments()` removed the replaced credential (earlier releases said the user "must be enrolled"), and in ZSM mode when the server refuses the credential with error 15040. The Crypto Module now passes the server's error on an MPC round through to the SDK, which the 15040 case needs. The credential is not removed by `authenticate()`; the next `checkAllEnrollments()` or `enroll()` removes it.
29
+ - `checkAllEnrollments()` reports a `pending` or `suspended` enrollment as not enrolled (`state` `"NONE"`, `enrollmentActive` `false`), even when the server's `zsm_active` is `true`. The server's state is exposed as `enrollmentState` (`"active"`, `"pending"`, `"suspended"`, or `null` when not reported), and whether the ZSM credential itself was validated as `credentialActive` (`true`, `false`, or `null` when unknown), independently of the enrollment's state. A credential is cleared from the device only when the server reports the credential itself as never validated, as before, whether its enrollment reads `pending` or (under AutoVerify) `active`. A suspended enrollment's credential is always kept, and so is a validated credential whose enrollment is pending activation.
30
+ - `unenroll()` now removes only the given user's local records. Earlier releases matched records by substring, so unenrolling `turley@x` also removed records of `aturley@x`.
31
+ - `UMFAClient.enroll()` now resolves with the actual `Error` when the enrollment lookup fails, instead of reporting that the user is already enrolled, and resolves (rather than rejects) with an `Error` when the identity check fails.
32
+ - `FIDO2Client.webauthnGet()` and `FIDO2Client.webauthnCreate()` follow the same rules as `UMFAClient.authenticate()` and `UMFAClient.enroll()`: `webauthnGet()` resolves with an `ENROLLMENT_REPLACED` `Error` for a replaced enrollment (in passkey mode after the purge, in ZSM mode on 15040), and both resolve with the actual `Error` when the enrollment lookup fails. `webauthnCreate()` now enrolls a user whose local credential `checkAllEnrollments()` has just removed as replaced, instead of reporting that the user is already enrolled, and resolves (rather than rejects) with an `Error` when the identity check fails.
33
+ - The Crypto Module closes its database connection on every path, including errors. An open connection could block the deletion of the local database (`LOCAL_DATA_WIPE_BLOCKED`).
34
+ - The device unenrollment used by `unenroll()` now closes its database connection when it finishes, whether it succeeds or fails. An open connection could block a later deletion of the local database.
35
+ - `UMFAClient.authenticate()` with `userVerification` set to `"preferred"` now falls back to the ZSM credential alone when the passkey ceremony fails. A user cancellation also causes this fallback. Earlier releases returned an error. The iOS and Android SDKs already behave this way.
36
+ - `UMFAClient.authenticate()` with `userVerification` set to `"preferred"` now completes the ZSM-only fallback where the browser does not support passkeys, or where the user holds no passkey on the device. Earlier releases failed with `Credential is missing` on each fallback.
37
+ - With `userVerification` set to `"required"`, a passkey failure still fails the authentication.
38
+ - Adds `isPasskeySupported()`, `passkeyNotSupportedReason()` and `getPasskeySupportStatus()` to the `UMFAClient` class. The names and the report shape match the Android SDK. Each method resolves asynchronously.
39
+ - `isPasskeySupported()` resolves with `false` only when a signal proves that a passkey ceremony cannot start:
40
+ - The page is not a secure context.
41
+ - The browser does not expose `PublicKeyCredential` and `navigator.credentials`.
42
+ - The permissions policy of the frame blocks both `publickey-credentials-create` and `publickey-credentials-get`. Only Chromium exposes this policy to a script.
43
+ - A missing platform authenticator does not turn passkeys off. A phone, a security key or a password manager extension can still hold the passkey.
44
+ - A frame that blocks only one ceremony keeps passkey support. The blocked ceremony now fails at once with an error that names the policy and the `allow` value for the iframe. Earlier releases started the ceremony, and the browser then rejected it with an unexplained `NotAllowedError`.
45
+ - `getPasskeySupportStatus()` resolves with a report. The report has `"platform": "browser"` and these fields:
46
+ - `supported`, `capability` and `reason`. The `capability` value is one of `"capable"`, `"no_platform_authenticator"`, `"unsupported"` or `"unknown"`.
47
+ - `secure_context`, `webauthn_api`, `create_allowed`, `get_allowed` and `cross_origin_frame`.
48
+ - `platform_authenticator`, from `isUserVerifyingPlatformAuthenticatorAvailable()`. The value is `null` where the browser answers a constant: Chromium on Linux, and Firefox on systems other than Windows. The value is also `null` where an extension replaced the check. In WebKit, a `false` value includes an app whose webview cannot use passkeys for the RP.
49
+ - `hybrid_transport`. Only desktop Chromium measures this value, so other browsers report `null`.
50
+ - `api_wrapped`. The value is `true` when a script, usually a password manager extension, replaced a WebAuthn function.
51
+ - `client_capabilities`, the unchanged result of `getClientCapabilities()`. The SDK takes no decision from it, because most engines return constants.
52
+ - A `null` field means that the browser gives no reliable value for that field. A caller must not treat `null` as `false`.
53
+ - The client logs the report with `console.debug` when it starts.
54
+ - Fixes the ZSM-only fallback of `enroll()` on `UMFAClient` and `webauthnCreate()` on `FIDO2Client`. Where the browser does not support passkeys and `userVerification` is `"preferred"`, earlier releases returned an "Unexpected PKP flow" `Error` and enrolled nothing. The methods now enroll the user with a ZSM credential only, as the warning in the console says. Under `"required"`, the methods still return an `Error`.
55
+
56
+ ## Release 2.9.4 - September 14, 2026
57
+ - Removes the optional `installation_id` override mechanism that release 2.9.0 added.
58
+ - `InstallationID::get()` now always derives the identifier from the IndexedDB keypair at `installation_id_key_pair`.
59
+ - Adds `getInstallationId()` to the `UMFAClient` class. It resolves with this installation's semi-persistent identifier as a string, and throws if the module returns no identifier. A page that must name its own installation to a server no longer has to reach through the client's internals for the value.
60
+ - Keeps the original WebAuthn error name on a failed passkey ceremony. When `enroll` or `authenticate` on `UMFAClient`, or `webauthnCreate` or `webauthnGet` on `FIDO2Client`, fails at the passkey step under a `userVerification` of `"required"`, the returned `Error` now carries the name of the underlying `DOMException`, for example `NotAllowedError`, `SecurityError` or `InvalidStateError`. The original `DOMException` is also attached as the `cause` property. Earlier releases discarded both and kept the message text only. A consumer can now detect a user cancellation with `error.name === 'NotAllowedError'`. The consumer no longer has to match the message text, which each browser phrases differently.
61
+ - Automatically binds a local credential's `state` value to the one corresponding to it on the Ideem remote server, ensuring that the client's view of the user's enrollment status is always up-to-date with the server. Accordingly, adjusts the `checkAllEnrollments()` return value to reflect the current state of the local enrollment(s) and the actions that the user can take based upon that state.
62
+ - Expands the `checkAllEnrollments()` return value to include two new properties: `state` and `actions`.
63
+ - The `state` property is a string that indicates the overall enrollment state of the user, and can be one of:
64
+ - `"NONE"` (indicating there are NO finalized credentials available)
65
+ - `"MPC_ONLY"` (indicating the presence of a finalized ZSM credential only)
66
+ - `"BOTH"` (indicating the presence of both finalized ZSM and passkey credentials).
67
+ - The `actions` property is an array of two strings that indicates the actions that the user can take based upon the current state of the local enrollment(s). This allows consumers to provide more detailed guidance to their users on how to complete their enrollment, and to tailor their UI/UX accordingly. These include:
68
+ - `"ENROLL-ZSM"` (the user can perform a ZSM-only enrollment)
69
+ - `"ENROLL-PKP"` (the user can perform a passkey-only enrollment)
70
+ - `"ENROLL-BOTH"` (the user can perform both ZSM and passkey enrollments)
71
+ - `"AUTHENTICATE-ZSM"` (the user can authenticate using a ZSM credential)
72
+ - `"AUTHENTICATE-BOTH"` (the user can authenticate using either a ZSM or passkey credential)
73
+ - Resolves a lockout that could follow an enrollment whose out-of-band validation never completed. A credential that is created but never validated remains inactive on the Ideem server, so every subsequent authentication is rejected, while the local record still reads as enrolled and blocks any attempt to enroll the user again. `enroll()` and `authenticate()` now recognize this state, clear the stranded local record, and return the user to the ordinary first-time enrollment path. `enroll()` clears the record and proceeds directly to a fresh enrollment; `authenticate()` reports the condition as an `Error` explaining that the enrollment was never validated and has been cleared from the device, and that the user must enroll again. A local record is only ever cleared when the server has explicitly confirmed the credential to be inactive: a check that could not reach the server, or that was not needed, leaves the local record untouched.
74
+ - `checkAllEnrollments()` now RESOLVES with an `Error` when enrollment details cannot be retrieved at all, rather than rejecting. This aligns the method with its documented contract, under which it resolves with either an enrollment details object or an `Error`, and with the `enroll()` and `authenticate()` methods, which already behave this way. A consumer relying on a `try`/`catch` around `checkAllEnrollments()` should test the resolved value with `instanceof Error` instead. The returned `Error` carries a `code` property of `"ENROLLMENT_DETAILS_UNAVAILABLE"`, the originating error as its `cause`, and the `userIdentifier` and `forceRemoteCheck` values the call was made with. Note that an unenrolled user is NOT an error, and still resolves with an ordinary details object whose `state` is `"NONE"`.
75
+ - Adds a `remoteCheckStatus` property to the data returned by `checkAllEnrollments()`, reporting whether the values in the returned object were confirmed against the Ideem server. It is one of:
76
+ - `"confirmed"` (the server was consulted, and returned the user's enrollment details)
77
+ - `"not-found"` (the server was consulted, and reported no enrollment for the user)
78
+ - `"unavailable"` (the server could not be reached; every other value in the object is local-only and unverified)
79
+ - `"skipped"` (local data was sufficient, so no server call was made)
80
+ This allows a consumer to distinguish an authoritative "this user is not enrolled" from a failure to reach the server, which earlier releases reported identically. It is particularly relevant when calling `checkAllEnrollments()` with its `forceRemoteCheck` argument set to `true`, as it reports whether that request was actually satisfied.
81
+ - `unenroll()` now throws an `Error` when it is unable to unbind a user's identity from the device, where earlier releases threw a bare string. A consumer's `catch` block can now read `error.message` and `error.cause`.
82
+
83
+ ## Release 2.9.3 - August 13, 2026 [ NPM PACKAGE RELEASE ]
16
84
  - Minor bug fixes and improvements
17
85
  - Resolves a strange conflict experienced by users running third party password managers (e.g. 1Password, Bitwarden, etc.) in which the `hints` KVP on Webauthn publicKey objects was preventing said password managers from properly handling the enrollment and authorization flows (flows still worked, but passkeys would not be saved to the password manager). The SDK now automatically removes the optional `hints` KVP from publicKey objects before passing them to the browser's Webauthn API, which resolves this issue and allows password managers to properly handle passkey enrollment and authorization flows.
18
86
 
@@ -1,3 +1,3 @@
1
- import e from"./EventCoordinator.js";import{zsmPluginManager as t}from"./PluginManager.js";import{encodeUserIdentifier as r}from"./encodeUserIdentifier.js";export default class{constructor(r){e.reset(),e.update("FIDO2ClientBase"),this.config=r;const s=t.classes("WEBAUTHNCLIENT");this.zsmAPI=new s(r),this.checkEnrollment=this.checkEnrollment.bind(this),this.checkIdentity=this.checkIdentity.bind(this),this.webauthnRetrieve=this.webauthnRetrieve.bind(this),this.webauthnDelete=this.webauthnDelete.bind(this),this.resetDevice=this.resetDevice.bind(this),this.webauthnCreate=this.webauthnCreate.bind(this),this.webauthnGet=this.webauthnGet.bind(this),this.checkAllEnrollments=this.checkAllEnrollments.bind(this),this.webauthnPasskeyPlusCreate=this.webauthnPasskeyPlusCreate.bind(this),this.webauthnPasskeyPlusGet=this.webauthnPasskeyPlusGet.bind(this),e.update("FIDO2ClientBase","READY")}get userIdentifier(){return this.zsmAPI.userIdentifier}get credentialID(){return this.zsmAPI.credentialID}typeChecks(e,t,s=null,n=""){if(!t)throw new TypeError(`[FIDO2Client] :: ${e} :: A userIdentifier String is required! Received: ${t} (type: ${typeof t}).`);let i;if("object"==typeof t&&(i={...t},t=t.userIdentifier),null!=s){if("boolean"==typeof s)s=!0===s?"preferred":"prevented";else{if("string"!=typeof s)throw new TypeError(`[FIDO2Client] :: ${e} :: userVerified must be a boolean or string. Received type: ${typeof s}.`);s=s.toLowerCase()}const t=["preferred","required","discouraged","prevented",...n.split(/, ?/g).filter(Boolean)];if(!new RegExp(`^(${t.join("|")})$`).test(s))throw new TypeError(`[FIDO2Client] :: ${e} :: userVerified must be one of the following Strings: "${t.join('", "')}"). Received: ${s}.`)}let a={userIdentifier:t=r(t),userVerified:s};return i&&(a={enrollmentDetails:i,...a}),a}async checkIdentity(e,t){return this.zsmAPI.checkIdentity(r(e),t)}async checkEnrollment(e){({userIdentifier:e}=this.typeChecks("checkEnrollment",e));return await this.zsmAPI.webauthnRetrieve(e)}async checkAllEnrollments(e,t=!1){return this.zsmAPI.webauthnRetrieveAll(r(e),t)}webauthnRetrieve(e){return this.zsmAPI.webauthnRetrieve(r(e))}webauthnDelete(e){return this.zsmAPI.unbindFromDevice(r(e))}async resetDevice(){this.zsmAPI.webauthnReset()}async webauthnCreate(e,t="prevented"){({userIdentifier:e,userVerified:t}=this.typeChecks("webauthnCreate",e,t,"zsm,re-enroll,reenroll"));const r=/^re-?enroll$/.test(t),s="preferred"===t;let n,i,a;if(r||/^(preferred|required)$/.test(t)){i=await this.checkAllEnrollments(e);try{if(i.passkeySupported)if(r){if(!i.zsmCredentialID&&!i.hasLocalPasskey)return new Error(`[FIDO2Client] :: webauthnCreate :: Cannot RE-enroll user's passkey; user ${e} is not currently enrolled with either ZSM or Passkeys+ credentials. To enroll the user, call webauthnCreate with a userVerified value of "preferred", "required", or "discouraged".`);n=await this.webauthnPasskeyPlusCreate(i,"required")}else{if(i.zsmCredentialID&&i.hasLocalPasskey)return new Error(`[FIDO2Client] :: webauthnCreate :: User ${e} is already enrolled with both ZSM and Passkeys+ credentials. To authenticate one or both credential, call the webauthnGet method.`);i instanceof Error||!1===i||!i.zsmCredentialID&&!i.hasLocalPasskey?n=await this.webauthnPasskeyPlusCreate(i,t):i.zsmCredentialID&&!i.hasLocalPasskey?n=await this.webauthnBindPasskeyToZSM(i,t):!i.zsmCredentialID&&i.pkpCredential&&(n=await this.webauthnBindZSMToPasskey(i,t))}else{if(!s)return new Error(`[FIDO2Client] :: webauthnCreate :: Passkeys+ is required for enrollment (userVerified set to "required"), but this device does not support Passkeys+. User ${e} cannot be enrolled. Please use a Passkeys+-compatible device to enroll.`);console.warn(`[FIDO2Client] :: webauthnCreate :: Passkeys+ enrollment has been requested, but this device does not support Passkeys+. User ${e} will be enrolled with ZSM-only credentials. Please use a Passkeys+-compatible device to enroll with Passkeys+ in the future.`)}return void 0===n?new Error(`[FIDO2Client] :: webauthnCreate :: Unexpected PKP flow: PKP enrollment attempted but no result was returned for user ${e}.`):n}catch(r){if(console.error("ENROLL FLOW :: PASSKEY OPERATION FAILED ::",r),!s)return new Error(`[FIDO2Client] :: webauthnCreate :: The passkey portion of the Passkeys+ enrollment failed for user ${e} with error: ${r.message||r}. Since userVerified is set to "${t}", the entire enrollment process has failed. You may: retry, downgrade the userVerified mandate to "preferred" or "discouraged" to allow for ZSM-only enrollment.`)}}if(a=i?.zsmCredentialID?i?.zsmCredentialID:await this.checkEnrollment(e),"string"==typeof a)return new Error(`[FIDO2Client] :: webauthnCreate :: User ${e} is already enrolled with a ZSM credential. To authenticate it, call the webauthnGet method.`);await this.zsmAPI.checkIdentity(e,!0);const l=await this.zsmAPI.webauthnCreate(e);return l instanceof Error?new Error(`[FIDO2Client] :: webauthnCreate :: Unable to enroll ${e}!\nDetails:\n${l?.message||l}`,{cause:l?.stack}):l}async webauthnGet(e,t="prevented"){({userIdentifier:e,userVerified:t}=this.typeChecks("webauthnGet",e,t,"zsm"));const r=/^(preferred|required)$/.test(t),s="preferred"===t;let n,i,a;if(r){if(i=await this.checkAllEnrollments(e),!i.zsmCredentialID)return new Error(`[FIDO2Client] :: webauthnGet :: User ${e} must be enrolled with a ZSM credential before authenticating. Please enroll the user first.`);if(!i.passkeySupported)return new Error(`[FIDO2Client] :: webauthnGet :: User ${e} is not eligible for Passkeys+ authentication; their device does not support passkey authentication. To authenticate, call the webauthnGet method with userVerified set to "preferred", "discouraged", or "prevented"/"zsm". To enroll with Passkeys+, call webauthnCreate with userVerified set to "preferred" or "required" on a Passkeys+-compatible device.`);if(!i.hasLocalPasskey)return new Error(`[FIDO2Client] :: webauthnGet :: User ${e} must be enrolled with both ZSM and Passkeys+ credentials to authenticate using Passkeys+ (userVerified set to a non-prevented value).`);if(r){try{n=await this.webauthnPasskeyPlusGet(i,t)}catch(r){if(!s)return new Error(`[FIDO2Client] :: webauthnGet :: The passkey portion of the Passkeys+ authentication failed for user ${e} with error: ${r.message||r}. Since userVerified is set to "${t}", the entire authentication process has failed. You may: retry or downgrade the userVerified mandate to "preferred" or "discouraged" to allow for ZSM-only authentication.`)}return void 0===n?new Error(`[FIDO2Client] :: webauthnGet :: Unexpected PKP flow: PKP authentication attempted but no result was returned for user ${e}.`):n}}a=i?.zsmCredentialID?i?.zsmCredentialID:await this.checkEnrollment(e);if(!1===("string"==typeof a))return new Error(`[FIDO2Client] :: webauthnGet :: ${e} is not enrolled. To authenticate the user, please enroll them first by calling the webauthnCreate method.`);const l=await this.zsmAPI.webauthnPartialGet(e);return l?.credential??l}
1
+ import e from"./EventCoordinator.js";import{zsmPluginManager as t}from"./PluginManager.js";import{encodeUserIdentifier as r}from"./encodeUserIdentifier.js";const s=e=>{const t=new Error(`[FIDO2Client] :: webauthnGet :: User ${e}'s enrollment was replaced by a newer one on another device, and has been removed from this device. Please enroll the user again.`);return t.code="ENROLLMENT_REPLACED",t};export default class{constructor(r){e.reset(),e.update("FIDO2ClientBase"),this.config=r;const s=t.classes("WEBAUTHNCLIENT");this.zsmAPI=new s(r),this.checkEnrollment=this.checkEnrollment.bind(this),this.checkIdentity=this.checkIdentity.bind(this),this.webauthnRetrieve=this.webauthnRetrieve.bind(this),this.webauthnDelete=this.webauthnDelete.bind(this),this.resetDevice=this.resetDevice.bind(this),this.webauthnCreate=this.webauthnCreate.bind(this),this.webauthnGet=this.webauthnGet.bind(this),this.checkAllEnrollments=this.checkAllEnrollments.bind(this),this.webauthnPasskeyPlusCreate=this.webauthnPasskeyPlusCreate.bind(this),this.webauthnPasskeyPlusGet=this.webauthnPasskeyPlusGet.bind(this),e.update("FIDO2ClientBase","READY")}get userIdentifier(){return this.zsmAPI.userIdentifier}get credentialID(){return this.zsmAPI.credentialID}typeChecks(e,t,s=null,n=""){if(!t)throw new TypeError(`[FIDO2Client] :: ${e} :: A userIdentifier String is required! Received: ${t} (type: ${typeof t}).`);let i;if("object"==typeof t&&(i={...t},t=t.userIdentifier),null!=s){if("boolean"==typeof s)s=!0===s?"preferred":"prevented";else{if("string"!=typeof s)throw new TypeError(`[FIDO2Client] :: ${e} :: userVerified must be a boolean or string. Received type: ${typeof s}.`);s=s.toLowerCase()}const t=["preferred","required","discouraged","prevented",...n.split(/, ?/g).filter(Boolean)];if(!new RegExp(`^(${t.join("|")})$`).test(s))throw new TypeError(`[FIDO2Client] :: ${e} :: userVerified must be one of the following Strings: "${t.join('", "')}"). Received: ${s}.`)}let a={userIdentifier:t=r(t),userVerified:s};return i&&(a={enrollmentDetails:i,...a}),a}async checkIdentity(e,t){return this.zsmAPI.checkIdentity(r(e),t)}async checkEnrollment(e){({userIdentifier:e}=this.typeChecks("checkEnrollment",e));return await this.zsmAPI.webauthnRetrieve(e)}async checkAllEnrollments(e,t=!1){return this.zsmAPI.webauthnRetrieveAll(r(e),t)}webauthnRetrieve(e){return this.zsmAPI.webauthnRetrieve(r(e))}webauthnDelete(e){return this.zsmAPI.unbindFromDevice(r(e))}async resetDevice(){this.zsmAPI.webauthnReset()}async webauthnCreate(e,t="prevented"){({userIdentifier:e,userVerified:t}=this.typeChecks("webauthnCreate",e,t,"zsm,re-enroll,reenroll"));const r=/^re-?enroll$/.test(t),s="preferred"===t;let n,i,a;if(r||/^(preferred|required)$/.test(t)){if(i=await this.checkAllEnrollments(e),i instanceof Error)return i;try{if(i.passkeySupported)if(r){if(!i.zsmCredentialID&&!i.hasLocalPasskey)return new Error(`[FIDO2Client] :: webauthnCreate :: Cannot RE-enroll user's passkey; user ${e} is not currently enrolled with either ZSM or Passkeys+ credentials. To enroll the user, call webauthnCreate with a userVerified value of "preferred", "required", or "discouraged".`);n=await this.webauthnPasskeyPlusCreate(i,"required")}else{if(i.zsmCredentialID&&i.hasLocalPasskey)return new Error(`[FIDO2Client] :: webauthnCreate :: User ${e} is already enrolled with both ZSM and Passkeys+ credentials. To authenticate one or both credential, call the webauthnGet method.`);i instanceof Error||!1===i||!i.zsmCredentialID&&!i.hasLocalPasskey?n=await this.webauthnPasskeyPlusCreate(i,t):i.zsmCredentialID&&!i.hasLocalPasskey?n=await this.webauthnBindPasskeyToZSM(i,t):!i.zsmCredentialID&&i.pkpCredential&&(n=await this.webauthnBindZSMToPasskey(i,t))}else{if(!s)return new Error(`[FIDO2Client] :: webauthnCreate :: Passkeys+ is required for enrollment (userVerified set to "required"), but this device does not support Passkeys+. User ${e} cannot be enrolled. Please use a Passkeys+-compatible device to enroll.`);console.warn(`[FIDO2Client] :: webauthnCreate :: Passkeys+ enrollment has been requested, but this device does not support Passkeys+. User ${e} will be enrolled with ZSM-only credentials. Please use a Passkeys+-compatible device to enroll with Passkeys+ in the future.`)}if(i?.passkeySupported)return void 0===n?new Error(`[FIDO2Client] :: webauthnCreate :: Unexpected PKP flow: PKP enrollment attempted but no result was returned for user ${e}.`):n}catch(r){if(console.error("ENROLL FLOW :: PASSKEY OPERATION FAILED ::",r),!s){const s=new Error(`[FIDO2Client] :: webauthnCreate :: The passkey portion of the Passkeys+ enrollment failed for user ${e} with error: ${r.message||r}. Since userVerified is set to "${t}", the entire enrollment process has failed. You may: retry, downgrade the userVerified mandate to "preferred" or "discouraged" to allow for ZSM-only enrollment.`,{cause:r});return s.name=r?.name||"Error",s}}}if(a=i?.zsmCredentialID?i?.zsmCredentialID:await this.checkEnrollment(e),"string"==typeof a){if(i||(i=await this.checkAllEnrollments(e)),i instanceof Error)return i;if(i.zsmCredentialID)return new Error(`[FIDO2Client] :: webauthnCreate :: User ${e} is already enrolled with a ZSM credential. To authenticate it, call the webauthnGet method.`);a=!1}try{await this.zsmAPI.checkIdentity(e,!0)}catch(t){return new Error(`[FIDO2Client] :: webauthnCreate :: Unable to establish an identity for ${e}!\nDetails:\n${t?.message||t}`,{cause:t})}const l=await this.zsmAPI.webauthnCreate(e);return l instanceof Error?new Error(`[FIDO2Client] :: webauthnCreate :: Unable to enroll ${e}!\nDetails:\n${l?.message||l}`,{cause:l?.stack}):l}async webauthnGet(e,t="prevented"){({userIdentifier:e,userVerified:t}=this.typeChecks("webauthnGet",e,t,"zsm"));const r=/^(preferred|required)$/.test(t),n="preferred"===t;let i,a,l;if(r){if(a=await this.checkAllEnrollments(e),a instanceof Error)return a;if(!a.zsmCredentialID)return a.replaced?s(e):new Error(`[FIDO2Client] :: webauthnGet :: User ${e} must be enrolled with a ZSM credential before authenticating. Please enroll the user first.`);if(!a.passkeySupported)return new Error(`[FIDO2Client] :: webauthnGet :: User ${e} is not eligible for Passkeys+ authentication; their device does not support passkey authentication. To authenticate, call the webauthnGet method with userVerified set to "preferred", "discouraged", or "prevented"/"zsm". To enroll with Passkeys+, call webauthnCreate with userVerified set to "preferred" or "required" on a Passkeys+-compatible device.`);if(!a.hasLocalPasskey)return new Error(`[FIDO2Client] :: webauthnGet :: User ${e} must be enrolled with both ZSM and Passkeys+ credentials to authenticate using Passkeys+ (userVerified set to a non-prevented value).`);if(r){try{i=await this.webauthnPasskeyPlusGet(a,t)}catch(r){if("ENROLLMENT_REPLACED"===r?.code)return r;if(!n){const s=new Error(`[FIDO2Client] :: webauthnGet :: The passkey portion of the Passkeys+ authentication failed for user ${e} with error: ${r.message||r}. Since userVerified is set to "${t}", the entire authentication process has failed. You may: retry or downgrade the userVerified mandate to "preferred" or "discouraged" to allow for ZSM-only authentication.`,{cause:r});return s.name=r?.name||"Error",s}}return void 0===i?new Error(`[FIDO2Client] :: webauthnGet :: Unexpected PKP flow: PKP authentication attempted but no result was returned for user ${e}.`):i}}l=a?.zsmCredentialID?a?.zsmCredentialID:await this.checkEnrollment(e);if(!1===("string"==typeof l))return new Error(`[FIDO2Client] :: webauthnGet :: ${e} is not enrolled. To authenticate the user, please enroll them first by calling the webauthnCreate method.`);let o;try{o=await this.zsmAPI.webauthnPartialGet(e)}catch(e){if("ENROLLMENT_REPLACED"===e?.code)return e;throw e}return o?.credential??o}
2
2
  //! PASSKEYS PLUS FIDO2 CLIENT METHODS ==============================================================================================================================
3
3
  async webauthnPasskeyPlusCreate(e,t="preferred"){({userIdentifier:e,userVerified:t}=this.typeChecks("webauthnPasskeyPlusCreate",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpCreate(e,t)}async webauthnPasskeyPlusGet(e,t="preferred"){let r=null;({userIdentifier:e,userVerified:t,enrollmentDetails:r}=this.typeChecks("webauthnPasskeyPlusGet",e,t,"re-enroll,reenroll")),r||(r=await this.checkAllEnrollments(e,!0));return await this.zsmAPI.pkpAuthenticate(r,t,!r.zsmCredentialID)}async webauthnBindZSMToPasskey(e,t){({userIdentifier:e}=this.typeChecks("webauthnBindZSMToPasskey",e));return await this.webauthnPasskeyPlusGet(e)}async webauthnBindPasskeyToZSM(e,t="preferred"){let r=null;({userIdentifier:e,userVerified:t,enrollmentDetails:r}=this.typeChecks("webauthnBindPasskeyToZSM",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpAddPasskeyCredential(r||e,t)}}
@@ -1 +1 @@
1
- function e(e,n){const r=indexedDB.open("ideem",1);r.onerror=(e=r.error)=>{throw new Error(`[IdentityIndexing] :: uid2iid :: Unable to bind UID to IID:\n${e}`)},r.onsuccess=(r,o=r.target.result)=>{const t=o.transaction("zsm","readwrite").objectStore("zsm").put(n,e);t.onsuccess=()=>o.close(),t.onerror=(e=t.error)=>(o.close(),console.error(`[IdentityIndexing] :: uid2iid :: Unable to bind UID to IID:\n${e}`))}}function n(e){return new Promise((n,r)=>{const o=indexedDB.open("ideem",1);o.onupgradeneeded=(e,n=e.target.result)=>{n.objectStoreNames.contains("zsm")||n.createObjectStore("zsm")},o.onerror=(e=o.error)=>r(new Error(`[IdentityIndexing] :: iid4uid :: Unable to connect to IDB:\n${e}`)),o.onsuccess=(o,t=o.target.result)=>{if(!t.objectStoreNames.contains("zsm"))return n(e);const i=t.transaction("zsm","readonly").objectStore("zsm").get(e);i.onsuccess=()=>(t.close(),i.result?n(i.result):n(e)),i.onerror=(e=i.error)=>(t.close(),console.error(`[IdentityIndexing] :: iid4uid :: Unable to find IID for UID:\n${e}`),r(new Error(`[IdentityIndexing] :: iid4uid :: Unable to find IID for UID:\n${e}`)))}})}export{e as uid2iid,n as iid4uid};
1
+ const e=(e,n=e.target.result)=>{n.objectStoreNames.contains("zsm")||n.createObjectStore("zsm")};function n(n,o){return new Promise(r=>{const t=(e,n)=>(e?.close(),console.error(`[IdentityIndexing] :: uid2iid :: Unable to bind UID to IID:\n${n}`),r(!1)),s=indexedDB.open("ideem",1);s.onupgradeneeded=e,s.onerror=()=>t(null,s.error),s.onsuccess=(e,s=e.target.result)=>{s.onversionchange=()=>s.close();try{const e=s.transaction("zsm","readwrite").objectStore("zsm").put(o,n);e.onsuccess=()=>(s.close(),r(!0)),e.onerror=()=>t(s,e.error)}catch(e){t(s,e)}}})}function o(n){return new Promise((o,r)=>{const t=(e,n)=>(e?.close(),console.error(`[IdentityIndexing] :: iid4uid :: Unable to find IID for UID:\n${n}`),r(new Error(`[IdentityIndexing] :: iid4uid :: Unable to find IID for UID:\n${n}`))),s=indexedDB.open("ideem",1);s.onupgradeneeded=e,s.onerror=(e=s.error)=>r(new Error(`[IdentityIndexing] :: iid4uid :: Unable to connect to IDB:\n${e}`)),s.onsuccess=(e,r=e.target.result)=>{if(r.onversionchange=()=>r.close(),!r.objectStoreNames.contains("zsm"))return r.close(),o(n);try{const e=r.transaction("zsm","readonly").objectStore("zsm").get(n);e.onsuccess=()=>(r.close(),e.result?o(e.result):o(n)),e.onerror=()=>t(r,e.error)}catch(e){t(r,e)}}})}export{n as uid2iid,o as iid4uid};
@@ -0,0 +1 @@
1
+ const e=1,t=Object.freeze({CAPABLE:"capable",NO_PLATFORM_AUTHENTICATOR:"no_platform_authenticator",UNSUPPORTED:"unsupported",UNKNOWN:"unknown"}),n="publickey-credentials-create",o="publickey-credentials-get",r=`${n}; ${o}`;function i(e=globalThis){const t=e?.navigator?.credentials;return{secureContext:!0===e?.isSecureContext,webauthnApi:"function"==typeof e?.PublicKeyCredential&&"function"==typeof t?.create&&"function"==typeof t?.get,createAllowed:s(e,n),getAllowed:s(e,o),crossOriginFrame:u(e)}}function a(e){return e.secureContext&&e.webauthnApi&&!(!1===e.createAllowed&&!1===e.getAllowed)}function l(e){return e.secureContext?e.webauthnApi?!1===e.createAllowed&&!1===e.getAllowed?`The permissions policy of this frame blocks ${n} and ${o}. Add allow="${r}" to the iframe element.`:!1===e.createAllowed?`The permissions policy of this frame blocks ${n}, so passkey enrollment cannot start. Add allow="${r}" to the iframe element.`:!1===e.getAllowed?`The permissions policy of this frame blocks ${o}, so passkey authentication cannot start. Add allow="${r}" to the iframe element.`:null:"This browser does not expose the WebAuthn API (PublicKeyCredential and navigator.credentials).":"This page is not a secure context. WebAuthn requires HTTPS or localhost."}async function c(e,n=globalThis){const o=a(e),r=e.webauthnApi?n.PublicKeyCredential:null,i=r?.isUserVerifyingPlatformAuthenticatorAvailable,c="function"==typeof i&&!p(i),s=e.webauthnApi&&(!p(n.navigator.credentials.create)||!p(n.navigator.credentials.get)||c),[u,d]=await Promise.all([f(r,"isUserVerifyingPlatformAuthenticatorAvailable"),f(r,"getClientCapabilities")]),b=function(e){if(!e||"object"!=typeof e)return null;const t="function"==typeof e.entries&&"function"==typeof e.get?Array.from(e.entries()):Object.entries(e),n={};for(const[e,o]of t)"boolean"==typeof o&&(n[e]=o);return n}(d),h=function(e){const t=e?.navigator?.userAgentData,n=String(e?.navigator?.userAgent||""),o=Array.isArray(t?.brands)&&t.brands.some(e=>"Chromium"===e?.brand),r=!o&&/\bFirefox\/\d/.test(n);return{chromium:o,chromiumDesktop:o&&!1===t.mobile&&"Android"!==t.platform,chromiumLinux:o&&"Linux"===t.platform,gecko:r,geckoWindows:r&&/\bWindows\b/.test(n)}}(n),m=function(e,t,n){return"boolean"!=typeof e||t||n.chromiumLinux||n.gecko&&!n.geckoWindows?null:e}(u,c,h),y=h.chromiumDesktop&&"boolean"==typeof b?.hybridTransport?b.hybridTransport:null;let A=t.UNKNOWN;return o?!0===m?A=t.CAPABLE:!1===m&&(A=t.NO_PLATFORM_AUTHENTICATOR):A=t.UNSUPPORTED,{schema:1,platform:"browser",supported:o,capability:A,secure_context:e.secureContext,webauthn_api:e.webauthnApi,create_allowed:e.createAllowed,get_allowed:e.getAllowed,cross_origin_frame:e.crossOriginFrame,platform_authenticator:m,hybrid_transport:y,api_wrapped:s,client_capabilities:b,reason:l(e)}}export{t as PASSKEY_CAPABILITY,e as PASSKEY_SUPPORT_REPORT_SCHEMA_VERSION,i as readPasskeyGates,a as isSupportedByGates,l as passkeyGateReason,c as buildPasskeySupportStatus};function s(e,t){try{const n=e?.document?.featurePolicy;return"function"!=typeof n?.allowsFeature||"function"!=typeof n?.features?null:n.features().includes(t)?!0===n.allowsFeature(t):null}catch{return null}}function u(e){try{if(!e?.top||!e?.self)return null;if(e.top===e.self)return!1;const t=e.location?.ancestorOrigins&&"number"==typeof e.location.ancestorOrigins.length?Array.from(e.location.ancestorOrigins):null;if(t&&!t.includes("null"))return t.some(t=>t!==e.location.origin);for(let t=e.self;t!==t.parent;t=t.parent)if(t.parent.location.origin!==e.location?.origin)return!0;return!1}catch{return!0}}async function f(e,t){try{return"function"!=typeof e?.[t]?null:await e[t]()}catch{return null}}function p(e){try{return"function"==typeof e&&/\{\s*\[native code\]\s*\}\s*$/.test(Function.prototype.toString.call(e))}catch{return!1}}
@@ -1 +1 @@
1
- import e from"./EventCoordinator.js";export default class{constructor(){e.update("PasskeysPlus"),this.passkeyAPI=(navigator?.credentials??null)||null,e.update("PasskeysPlus","READY")}pkpSupported=()=>!!this.passkeyAPI&&"function"==typeof this.passkeyAPI.create&&"function"==typeof this.passkeyAPI.get;blockUnsupportedInvocation=()=>{if(!this.pkpSupported())throw new Error("[PK+ PasskeysPlus] :: blockUnsupportedInvocation :: This browser and/or device does not support the PasskeysPlus API!");return!0};toArrayBuffer(e){if("string"!=typeof e||!/^[A-Za-z0-9\-_]+$/.test(e))throw new Error(`[PK+ PasskeysPlus] :: toArrayBuffer :: Invalid base64url string provided: ${e}`);try{const t=atob(e.replace(/-/g,"+").replace(/_/g,"/")+"====".slice(0,-e.length%4)),r=new Uint8Array(t.length);for(let e=0;e<t.length;e++)r[e]=t.charCodeAt(e);return r.buffer}catch(e){throw new Error(`[PK+ PasskeysPlus] :: toArrayBuffer :: Error converting base64url string to ArrayBuffer: ${e.message||e}`)}}pkpCreatePasskeyCredential=async(e,t="preferred")=>{if(!this.blockUnsupportedInvocation())return!1;if(e?.authenticatorSelection&&(e.authenticatorSelection.userVerification=t),e?.hints&&delete e.hints,e?.extensions?.credentialProtectionPolicy&&delete e.extensions.credentialProtectionPolicy,!e||"object"!=typeof e)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid publicKey parameter provided: ${e}! It must be an object. Received: ${e}`);if(!(e?.challenge&&e?.rp&&e?.rp?.id&&e?.rp?.name))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid publicKey structure! It must contain challenge and rp properties with id and name. Received: ${e}`);if(!(e?.user&&e?.user?.id&&e?.user?.id instanceof ArrayBuffer))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid user ID in publicKey! It must be an ArrayBuffer. Received: ${e?.user?.id}`);if(!e?.pubKeyCredParams||!Array.isArray(e?.pubKeyCredParams)||0===e?.pubKeyCredParams?.length)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: pubKeyCredParams must be a non-empty array of public key credential parameters. Received: ${e?.pubKeyCredParams}`);if(!e?.authenticatorSelection||"object"!=typeof e?.authenticatorSelection)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid authenticatorSelection in publicKey! It must be an object. Received: ${e?.authenticatorSelection} (${typeof e?.authenticatorSelection})`);if(e?.authenticatorSelection?.authenticatorAttachment&&!["platform","cross-platform"].includes(e?.authenticatorSelection?.authenticatorAttachment))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid authenticatorAttachment in authenticatorSelection! It must be either "platform" or "cross-platform". Received: ${e?.authenticatorSelection?.authenticatorAttachment}`);if(e?.authenticatorSelection?.requireResidentKey&&"boolean"!=typeof e?.authenticatorSelection?.requireResidentKey)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid requireResidentKey in authenticatorSelection! It must be a boolean. Received: ${e?.authenticatorSelection?.requireResidentKey} (${typeof e?.authenticatorSelection?.requireResidentKey})`);if(e?.authenticatorSelection?.userVerification&&!["required","preferred","discouraged"].includes(e?.authenticatorSelection?.userVerification))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid userVerification in authenticatorSelection! It must be one of "required", "preferred", or "discouraged". Received: ${e?.authenticatorSelection?.userVerification}`);if(!e?.timeout||"number"!=typeof e?.timeout||e?.timeout<=0)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid timeout in publicKey! It must be a positive number. Received: ${e?.timeout} (${typeof e?.timeout})`);if(e?.excludeCredentials&&(!Array.isArray(e?.excludeCredentials)||e?.excludeCredentials?.length>0&&!e?.excludeCredentials[0].id))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid credential ID in excludeCredentials! It must be an ArrayBuffer. Received: ${e?.excludeCredentials[0]?.id}`);return await this.passkeyAPI.create({publicKey:{...e,userVerification:t}})};pkpGetPasskeyCredential=async(e,t="preferred")=>{if(!this.blockUnsupportedInvocation())return!1;if(e?.authenticatorSelection&&(e.authenticatorSelection.userVerification=t),e?.hints&&delete e.hints,!e||"object"!=typeof e)throw new Error("[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid publicKey parameter provided!");if(!e?.challenge||!e?.allowCredentials||!Array.isArray(e?.allowCredentials))throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid publicKey structure provided! It must contain challenge and allowCredentials properties. Received: ${e}`);if(0===e?.allowCredentials?.length)throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: allowCredentials array must not be empty! It should contain at least one credential ID. Received: ${e?.allowCredentials}`);const r=e?.allowCredentials.reduce((e,t)=>e&&t?.id&&(t.id instanceof ArrayBuffer||ArrayBuffer.isView(t.id)),!0);if(!r)throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid credential ID in allowCredentials! It must be an ArrayBuffer. Received: ${e?.allowCredentials[0]?.id}`);return await this.passkeyAPI.get({publicKey:{...e,userVerification:t}})}}
1
+ import e from"./EventCoordinator.js";import{readPasskeyGates as t,isSupportedByGates as r,passkeyGateReason as a,buildPasskeySupportStatus as s}from"./PasskeySupport.js";export default class{constructor(){e.update("PasskeysPlus"),this.passkeyAPI=(navigator?.credentials??null)||null,this.passkeyGates=t(),this.deviceSupportsPasskeys=r(this.passkeyGates),e.update("PasskeysPlus","READY")}get passkeysSupported(){return this.deviceSupportsPasskeys}pkpSupported=()=>this.deviceSupportsPasskeys;passkeyNotSupportedReason=()=>a(this.passkeyGates);getPasskeySupportStatus=()=>s(this.passkeyGates);blockUnsupportedInvocation=e=>{const t="create"===e&&!1===this.passkeyGates.createAllowed||"get"===e&&!1===this.passkeyGates.getAllowed;if(!this.pkpSupported()||t)throw new Error(`[PK+ PasskeysPlus] :: blockUnsupportedInvocation :: This browser and/or device does not support the PasskeysPlus API! ${this.passkeyNotSupportedReason()}`);return!0};toArrayBuffer(e){if("string"!=typeof e||!/^[A-Za-z0-9\-_]+$/.test(e))throw new Error(`[PK+ PasskeysPlus] :: toArrayBuffer :: Invalid base64url string provided: ${e}`);try{const t=atob(e.replace(/-/g,"+").replace(/_/g,"/")+"====".slice(0,-e.length%4)),r=new Uint8Array(t.length);for(let e=0;e<t.length;e++)r[e]=t.charCodeAt(e);return r.buffer}catch(e){throw new Error(`[PK+ PasskeysPlus] :: toArrayBuffer :: Error converting base64url string to ArrayBuffer: ${e.message||e}`)}}pkpCreatePasskeyCredential=async(e,t="preferred")=>{if(!this.blockUnsupportedInvocation("create"))return!1;if(e?.authenticatorSelection&&(e.authenticatorSelection.userVerification=t),e?.hints&&delete e.hints,e?.extensions?.credentialProtectionPolicy&&delete e.extensions.credentialProtectionPolicy,!e||"object"!=typeof e)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid publicKey parameter provided: ${e}! It must be an object. Received: ${e}`);if(!(e?.challenge&&e?.rp&&e?.rp?.id&&e?.rp?.name))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid publicKey structure! It must contain challenge and rp properties with id and name. Received: ${e}`);if(!(e?.user&&e?.user?.id&&e?.user?.id instanceof ArrayBuffer))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid user ID in publicKey! It must be an ArrayBuffer. Received: ${e?.user?.id}`);if(!e?.pubKeyCredParams||!Array.isArray(e?.pubKeyCredParams)||0===e?.pubKeyCredParams?.length)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: pubKeyCredParams must be a non-empty array of public key credential parameters. Received: ${e?.pubKeyCredParams}`);if(!e?.authenticatorSelection||"object"!=typeof e?.authenticatorSelection)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid authenticatorSelection in publicKey! It must be an object. Received: ${e?.authenticatorSelection} (${typeof e?.authenticatorSelection})`);if(e?.authenticatorSelection?.authenticatorAttachment&&!["platform","cross-platform"].includes(e?.authenticatorSelection?.authenticatorAttachment))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid authenticatorAttachment in authenticatorSelection! It must be either "platform" or "cross-platform". Received: ${e?.authenticatorSelection?.authenticatorAttachment}`);if(e?.authenticatorSelection?.requireResidentKey&&"boolean"!=typeof e?.authenticatorSelection?.requireResidentKey)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid requireResidentKey in authenticatorSelection! It must be a boolean. Received: ${e?.authenticatorSelection?.requireResidentKey} (${typeof e?.authenticatorSelection?.requireResidentKey})`);if(e?.authenticatorSelection?.userVerification&&!["required","preferred","discouraged"].includes(e?.authenticatorSelection?.userVerification))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid userVerification in authenticatorSelection! It must be one of "required", "preferred", or "discouraged". Received: ${e?.authenticatorSelection?.userVerification}`);if(!e?.timeout||"number"!=typeof e?.timeout||e?.timeout<=0)throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid timeout in publicKey! It must be a positive number. Received: ${e?.timeout} (${typeof e?.timeout})`);if(e?.excludeCredentials&&(!Array.isArray(e?.excludeCredentials)||e?.excludeCredentials?.length>0&&!e?.excludeCredentials[0].id))throw new Error(`[PK+ PasskeysPlus] :: pkpCreatePasskeyCredential :: Invalid credential ID in excludeCredentials! It must be an ArrayBuffer. Received: ${e?.excludeCredentials[0]?.id}`);return await this.passkeyAPI.create({publicKey:{...e,userVerification:t}})};pkpGetPasskeyCredential=async(e,t="preferred")=>{if(!this.blockUnsupportedInvocation("get"))return!1;if(e?.authenticatorSelection&&(e.authenticatorSelection.userVerification=t),e?.hints&&delete e.hints,!e||"object"!=typeof e)throw new Error("[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid publicKey parameter provided!");if(!e?.challenge||!e?.allowCredentials||!Array.isArray(e?.allowCredentials))throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid publicKey structure provided! It must contain challenge and allowCredentials properties. Received: ${e}`);if(0===e?.allowCredentials?.length)throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: allowCredentials array must not be empty! It should contain at least one credential ID. Received: ${e?.allowCredentials}`);const r=e?.allowCredentials.reduce((e,t)=>e&&t?.id&&(t.id instanceof ArrayBuffer||ArrayBuffer.isView(t.id)),!0);if(!r)throw new Error(`[PK+ PasskeysPlus] :: pkpGetPasskeyCredential :: Invalid credential ID in allowCredentials! It must be an ArrayBuffer. Received: ${e?.allowCredentials[0]?.id}`);return await this.passkeyAPI.get({publicKey:{...e,userVerification:t}})}}
@@ -1,3 +1,3 @@
1
- import t from"./EventCoordinator.js";import e from"./GlobalScoping.js";import{uid2iid as i}from"./IdentityIndexing.js";import{b64urlToBuf as r,toObject as n}from"./Utils.js";t.update("RelyingPartyBase");export default class{constructor(e,i,r,n,a,s){this.host=e,this.apiKey=i,this.applicationId=r,this.appEnvironment=n||null,this.customerDefinedIdentifier=void 0,this.publicKey=void 0,this.identityID=void 0,this.credentialID=void 0,this.traceId=void 0,this.xhrHeaders={"Content-Type":"application/json"},this.addlRelyingPartyBodyProps=a?{use_origin:"true"}:{},this.metadata=s||null,t.update("RelyingPartyBase","READY")}get userIdentifier(){return this.customerDefinedIdentifier}set userIdentifier(t){return userIdentifier=this.customerDefinedIdentifier=t}makePostRequest(t,i={},r={},n="POST"){t=`${this.host.endsWith("/")?this.host.slice(0,-1):this.host}/${t.startsWith("/")?t.slice(1):t}`,i="object"==typeof i?i:{},this.metadata&&Object.keys(this.metadata).length&&(i=Object.assign({},i,{metadata:this.metadata})),this.appEnvironment&&(i=Object.assign({},i,{environment:this.appEnvironment})),i=Object.assign({customer_defined_identifier:this.customerDefinedIdentifier,application_id:this.applicationId},i,this.addlRelyingPartyBodyProps,function(t=!1){const e=[[(new Date).toISOString(),"TRACEPRIMER"]];return t?{trace:e}:e}(!0));let a={method:n,headers:Object.assign(this.xhrHeaders,r?.Authorization?{}:{Authorization:`Bearer ${this.apiKey}`},r),body:JSON.stringify(i)};return fetch(t,a).then(t=>t.ok?t.json():t.json().then(e=>{throw new Error(`[RelyingParty] :: makePostRequest :: Request failed: ${t.statusText}, ${JSON.stringify(e)}`)})).then(t=>(e.dispatchEvent(new CustomEvent("AuthResponse",{bubbles:!0,cancelable:!1,detail:t})),t))}clearEnrollmentCredentials(){return this.publicKey=this.credentialID=null}clearLoginCredentials(){}resetAll(){return this.clearLoginCredentials(),this.clearEnrollmentCredentials()}registrationStart(){return this.makePostRequest("api/webauthn/registration/start").then(t=>(t.identity_id&&(e.mpcConfig.consumer_id=t.identity_id,i(this.userIdentifier,t.identity_id)),this.publicKey=t.ccr.publicKey)).then(t=>t).catch(t=>Promise.reject(new Error(t)))}registrationFinish(t){return t}authenticationStart(t=this.credentialID,e="passkey",i,r,n){return null!=t?this.makePostRequest("api/webauthn/authentication/start",{credential_id:t}).then(t=>t):Promise.reject(new Error("[RelyingParty] :: authenticationStart :: No user ID or credential ID provided"))}authenticationFinish(t){return t}createIdentityThenRegistrationStart(t="zsm",r,n,a){const s={credential_type:t,chain_handoff:r,chain_credential_id:n,chain_challenge:a};return this.makePostRequest("api/webauthn/registration/create-identity-then-start",s).then(t=>(t.identity_id&&(e.mpcConfig.consumer_id=t.identity_id,i(this.userIdentifier,t.identity_id)),{publicKey:this.publicKey=t.ccr.publicKey,identity_id:t.identity_id})).then(t=>t).catch(t=>Promise.reject(new Error(t)))}registrationFinishAuthenticationStart(t){return t}checkServerSideIdentity(t){return t=t?{create_new_identity:"true"}:{},this.makePostRequest("api/umfa/check-identity",t).then(t=>t).catch(t=>Promise.reject(new Error(t)))}
1
+ import t from"./EventCoordinator.js";import e from"./GlobalScoping.js";import{uid2iid as i}from"./IdentityIndexing.js";import{b64urlToBuf as r,toObject as n}from"./Utils.js";t.update("RelyingPartyBase");export default class{constructor(i,r,n,a,s,o){this.host=i,this.apiKey=r,this.applicationId=n,this.appEnvironment=a||null,this.customerDefinedIdentifier=void 0,this.publicKey=void 0,this.identityID=void 0,this.credentialID=void 0,this.traceId=void 0,this.xhrHeaders={"Content-Type":"application/json",...e.ZSMVersions?{"X-SDK-Version":JSON.stringify(e.ZSMVersions)}:{}},this.addlRelyingPartyBodyProps=s?{use_origin:"true"}:{},this.metadata=o||null,t.update("RelyingPartyBase","READY")}get userIdentifier(){return this.customerDefinedIdentifier}set userIdentifier(t){return userIdentifier=this.customerDefinedIdentifier=t}makePostRequest(t,i={},r={},n="POST"){t=`${this.host.endsWith("/")?this.host.slice(0,-1):this.host}/${t.startsWith("/")?t.slice(1):t}`,i="object"==typeof i?i:{},this.metadata&&Object.keys(this.metadata).length&&(i=Object.assign({},i,{metadata:this.metadata})),this.appEnvironment&&(i=Object.assign({},i,{environment:this.appEnvironment})),i=Object.assign({customer_defined_identifier:this.customerDefinedIdentifier,application_id:this.applicationId},i,this.addlRelyingPartyBodyProps,function(t=!1){const e=[[(new Date).toISOString(),"TRACEPRIMER"]];return t?{trace:e}:e}(!0));let a={method:n,headers:Object.assign({},this.xhrHeaders,r?.Authorization?{}:{Authorization:`Bearer ${this.apiKey}`},r),body:JSON.stringify(i)};return fetch(t,a).then(t=>t.ok?t.json():t.text().then(t=>{let e=null;try{e=t?JSON.parse(t):null}catch(t){}return e}).catch(()=>null).then(e=>{throw function(t,e){const i=new Error(`[RelyingParty] :: makePostRequest :: Request failed: ${t.status}${e?.message?` -- ${e.message}`:""}`);return i.status=t.status,i.traceId=e?.trace_id??null,i.text=e?.message??e?.detail??null,i.body=e??null,i}(t,e)})).then(t=>(e.dispatchEvent(new CustomEvent("AuthResponse",{bubbles:!0,cancelable:!1,detail:t})),t))}clearEnrollmentCredentials(){return this.publicKey=this.credentialID=null}clearLoginCredentials(){}resetAll(){return this.clearLoginCredentials(),this.clearEnrollmentCredentials()}registrationStart(){return this.makePostRequest("api/webauthn/registration/start").then(t=>(t.identity_id&&(e.mpcConfig.consumer_id=t.identity_id,i(this.userIdentifier,t.identity_id)),this.publicKey=t.ccr.publicKey)).then(t=>t).catch(t=>Promise.reject(new Error(t)))}registrationFinish(t){return t}authenticationStart(t=this.credentialID,e="passkey",i,r,n){return null!=t?this.makePostRequest("api/webauthn/authentication/start",{credential_id:t}).then(t=>t):Promise.reject(new Error("[RelyingParty] :: authenticationStart :: No user ID or credential ID provided"))}authenticationFinish(t){return t}createIdentityThenRegistrationStart(t="zsm",r,n,a){const s={credential_type:t,chain_handoff:r,chain_credential_id:n,chain_challenge:a};return this.makePostRequest("api/webauthn/registration/create-identity-then-start",s).then(t=>(t.identity_id&&(e.mpcConfig.consumer_id=t.identity_id,i(this.userIdentifier,t.identity_id)),{publicKey:this.publicKey=t.ccr.publicKey,identity_id:t.identity_id})).then(t=>t).catch(t=>Promise.reject(new Error(t)))}registrationFinishAuthenticationStart(t){return t}checkServerSideIdentity(t){return t=t?{create_new_identity:"true"}:{},this.makePostRequest("api/umfa/check-identity",t).then(t=>t).catch(t=>Promise.reject(new Error(t)))}
2
2
  //! PASSKEYS PLUS RELYING PARTY METHODS =========================================================================================================================
3
- async pkpRegistrationStart(t,n,a=null,s=!1){const o=a?{chain_handoff:"internal",chain_credential_id:a}:{chain_handoff:"internal"},l=await this.makePostRequest("api/passkey/registration/create-identity-then-start",o).then(t=>(t.identity_id&&(e.mpcConfig.consumer_id=t.identity_id,i(this.userIdentifier,t.identity_id)),{publicKey:this.publicKey=t.ccr.publicKey,identity_id:t.identity_id})).then(t=>t).catch(t=>Promise.reject(new Error(t)));if(!l)throw new Error("[PK+ RelyingParty] :: pkpRegistrationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!");if(!l?.publicKey)throw new Error("[PK+ RelyingParty] :: pkpRegistrationStart :: Platform Authority Challenge's Public Key was not found!");if(l.publicKey.challenge=r(l.publicKey.challenge),!l?.publicKey?.user)throw new Error("[PK+ RelyingParty] :: pkpRegistrationStart :: User for the Platform Authority Challenge Public Key's user node was not found!");return l.publicKey.user.id=r(l.publicKey.user.id),s?delete l.publicKey.excludeCredentials:Array.isArray(l.publicKey.excludeCredentials)&&l.publicKey.excludeCredentials.forEach(t=>{t&&"string"==typeof t.id&&(t.id=r(t.id))}),l}async pkpRegistrationFinish(t){if(!t)throw new TypeError("[PK+ RelyingParty] :: pkpRegistrationFinish :: No Platform Authenticator Credential provided!");return this.makePostRequest("api/passkey/registration/finish",{credential:t}).then(e=>(this.credentialID=t.rawId,{credential:t,credentialID:this.credentialID,response:{credentialID:this.credentialID,...e}})).catch(t=>{throw this.clearEnrollmentCredentials(),new Error("[PK+ RelyingParty] :: pkpRegistrationFinish :: Unable to complete registration!",{cause:t})})}async pkpAuthenticationStart(t){if(null==t||"object"!=typeof t)throw new TypeError("[PK+ RelyingParty] :: pkpAuthenticationStart :: No Credential Data object provided!");if(!t.hasRemotePasskey&&!t.zsmCredentialID)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Credential Data object does not contain a valid Passkeys+ Credential ID or ZSM Credential ID!");const e={credential_id:t?.pkpCredentialID,chain_credential_id:t.zsmCredentialID,chain_handoff:t?.chainHandoff||"internal",chain_challenge:t?.pkpCredential?JSON.stringify(t.pkpCredential):null};let i=await this.makePostRequest("api/passkey/authentication/start",e);if(i instanceof Error||!i)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!",{cause:i});if(i=n(i),!i?.rcr||!i?.rcr?.publicKey)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority's RCR challenge and/or publicKey was not found!");const a=i.rcr.publicKey;a.challenge=r(a.challenge);const s=a.allowCredentials;if(!s||!s[0]?.id)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority Challenge's allowCredentials and/or id was not found!");return s[0].id=r(s[0].id),a}async pkpAuthenticationFinish(t){if(!t||"object"!=typeof t)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationFinish :: No Platform Authenticator Credential provided or is not an object!");return this.makePostRequest("api/passkey/authentication/finish",{credential:t}).then(e=>({credential:t,response:e})).catch(t=>Promise.reject(new Error("[PK+ RelyingParty] :: pkpAuthenticationFinish :: Unable to complete authentication!",{cause:t})))}async getRemoteEnrollmentDetails(t,e,i=!1){const r={customer_defined_identifier:t};return await this.makePostRequest("/api/v1/enrollment-details",r)}}
3
+ async pkpRegistrationStart(t,n,a=null,s=!1){const o=a?{chain_handoff:"internal",chain_credential_id:a}:{chain_handoff:"internal"},l=await this.makePostRequest("api/passkey/registration/create-identity-then-start",o).then(t=>(t.identity_id&&(e.mpcConfig.consumer_id=t.identity_id,i(this.userIdentifier,t.identity_id)),{publicKey:this.publicKey=t.ccr.publicKey,identity_id:t.identity_id})).then(t=>t).catch(t=>Promise.reject(new Error(t)));if(!l)throw new Error("[PK+ RelyingParty] :: pkpRegistrationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!");if(!l?.publicKey)throw new Error("[PK+ RelyingParty] :: pkpRegistrationStart :: Platform Authority Challenge's Public Key was not found!");if(l.publicKey.challenge=r(l.publicKey.challenge),!l?.publicKey?.user)throw new Error("[PK+ RelyingParty] :: pkpRegistrationStart :: User for the Platform Authority Challenge Public Key's user node was not found!");return l.publicKey.user.id=r(l.publicKey.user.id),s?delete l.publicKey.excludeCredentials:Array.isArray(l.publicKey.excludeCredentials)&&l.publicKey.excludeCredentials.forEach(t=>{t&&"string"==typeof t.id&&(t.id=r(t.id))}),l}async pkpRegistrationFinish(t,e=!1){if(!t)throw new TypeError("[PK+ RelyingParty] :: pkpRegistrationFinish :: No Platform Authenticator Credential provided!");return this.makePostRequest("api/passkey/registration/finish",{credential:t}).then(i=>(e||(this.credentialID=t.rawId),{credential:t,credentialID:t.rawId,response:{credentialID:t.rawId,...i}})).catch(t=>{throw this.clearEnrollmentCredentials(),new Error("[PK+ RelyingParty] :: pkpRegistrationFinish :: Unable to complete registration!",{cause:t})})}async pkpAuthenticationStart(t){if(null==t||"object"!=typeof t)throw new TypeError("[PK+ RelyingParty] :: pkpAuthenticationStart :: No Credential Data object provided!");if(!t.hasRemotePasskey&&!t.zsmCredentialID)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Credential Data object does not contain a valid Passkeys+ Credential ID or ZSM Credential ID!");const e={credential_id:t?.pkpCredentialID,chain_credential_id:t.zsmCredentialID,chain_handoff:t?.chainHandoff||"internal",chain_challenge:t?.pkpCredential?JSON.stringify(t.pkpCredential):null};let i=await this.makePostRequest("api/passkey/authentication/start",e);if(i instanceof Error||!i)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Unable to retrieve Platform Authority Challenge from Relying Party Server!",{cause:i});if(i=n(i),!i?.rcr||!i?.rcr?.publicKey)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority's RCR challenge and/or publicKey was not found!");const a=i.rcr.publicKey;a.challenge=r(a.challenge);const s=a.allowCredentials;if(!s||!s[0]?.id)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationStart :: Platform Authority Challenge's allowCredentials and/or id was not found!");return s[0].id=r(s[0].id),a}async pkpAuthenticationFinish(t){if(!t||"object"!=typeof t)throw new Error("[PK+ RelyingParty] :: pkpAuthenticationFinish :: No Platform Authenticator Credential provided or is not an object!");return this.makePostRequest("api/passkey/authentication/finish",{credential:t}).then(e=>({credential:t,response:e})).catch(t=>Promise.reject(new Error("[PK+ RelyingParty] :: pkpAuthenticationFinish :: Unable to complete authentication!",{cause:t})))}async getRemoteEnrollmentDetails(t,e,i=!1){const r={customer_defined_identifier:t};i||e.zsm_credential_id&&(r.zsm_credential_id=e.zsm_credential_id);const n=await this.makePostRequest("/api/v1/enrollment-details",r);return n?.trace_map&&delete n.trace_map,n}}
package/UMFAClientBase.js CHANGED
@@ -1 +1 @@
1
- import e from"./EventCoordinator.js";import{zsmPluginManager as t}from"./PluginManager.js";import{encodeUserIdentifier as r}from"./encodeUserIdentifier.js";export default class{constructor(r){e.reset(),e.update("UMFAClientBase"),this.config=r;const s=t.classes("WEBAUTHNCLIENT");this.zsmAPI=new s(r),this.checkEnrollment=this.checkEnrollment.bind(this),this.enroll=this.enroll.bind(this),this.authenticate=this.authenticate.bind(this),this.checkAllEnrollments=this.checkAllEnrollments.bind(this),this.passkeyPlusEnroll=this.passkeyPlusEnroll.bind(this),this.passkeyPlusAuthenticate=this.passkeyPlusAuthenticate.bind(this),this.unenroll=this.unenroll.bind(this),this.resetDevice=this.resetDevice.bind(this),e.update("UMFAClientBase","READY")}get userIdentifier(){return this.zsmAPI.userIdentifier}get credentialID(){return this.zsmAPI.credentialID}get metadata(){return this.zsmAPI.metadata}set metadata(e){return this.zsmAPI.metadata=e}typeChecks(e,t,s=null,i=""){if(!t)throw new TypeError(`[UMFAClient] :: ${e} :: A userIdentifier String or Object is required! Received: ${t} (type: ${typeof t}).`);let n;if("object"==typeof t&&(n={...t},t=t.userIdentifier),null!=s){if("boolean"==typeof s)s=!0===s?"preferred":"prevented";else{if("string"!=typeof s)throw new TypeError(`[UMFAClient] :: ${e} :: userVerification must be a boolean or string. Received type: ${typeof s}.`);s=s.toLowerCase()}const t=["preferred","required","discouraged","prevented",...i.split(/, ?/g).filter(Boolean)];if(!new RegExp(`^(${t.join("|")})$`).test(s))throw new TypeError(`[UMFAClient] :: ${e} :: userVerification must be one of the following Strings: "${t.join('", "')}"). Received: ${s}.`)}let a={userIdentifier:t=r(t),userVerification:s};return n&&(a={enrollmentDetails:n,...a}),a}async allowBrowserObserversToSettle(){"undefined"!=typeof window&&("function"==typeof requestAnimationFrame&&await new Promise(e=>requestAnimationFrame(e)),await new Promise(e=>setTimeout(e,0)))}async checkIdentity(e,t){return this.zsmAPI.checkIdentity(r(e),t)}async checkEnrollment(e){({userIdentifier:e}=this.typeChecks("checkEnrollment",e));return await this.zsmAPI.webauthnRetrieve(e)}async checkAllEnrollments(e,t=!1){return this.zsmAPI.webauthnRetrieveAll(r(e),t)}async enroll(e,t="prevented"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("enroll",e,t,"zsm,re-enroll,reenroll"));const r=/^re-?enroll$/.test(t),s="preferred"===t;let i,n,a;if(r||/^(preferred|required)$/.test(t)){n=await this.checkAllEnrollments(e);try{if(n.passkeySupported)if(r){if(!n.zsmCredentialID&&!n.hasLocalPasskey)return new Error(`[UMFAClient] :: enroll :: Cannot RE-enroll user's passkey; user ${e} is not currently enrolled with either ZSM or Passkeys+ credentials. To enroll the user, call enroll with a userVerification value of "preferred", "required", or "discouraged".`);i=await this.passkeyPlusEnroll(n,"required",!0)}else{if(n.zsmCredentialID&&n.hasLocalPasskey)return new Error(`[UMFAClient] :: enroll :: User ${e} is already enrolled with both ZSM and Passkeys+ credentials. To authenticate one or both credential, call the authenticate method.`);n instanceof Error||!1===n||!n.zsmCredentialID&&!n.hasLocalPasskey?i=await this.passkeyPlusEnroll(n,t):n.zsmCredentialID&&!n.hasLocalPasskey?i=await this.addPasskeyToZSMCredential(n,t):!n.zsmCredentialID&&n.pkpCredential&&(i=await this.addZSMToPasskeyCredential(n,t))}else{if(!s)return new Error(`[UMFAClient] :: enroll :: Passkeys+ is required for enrollment (userVerification set to "required"), but this device does not support Passkeys+. User ${e} cannot be enrolled. Please use a Passkeys+-compatible device to enroll.`);console.warn(`[UMFAClient] :: enroll :: Passkeys+ enrollment has been requested, but this device does not support Passkeys+. User ${e} will be enrolled with ZSM-only credentials. Please use a Passkeys+-compatible device to enroll with Passkeys+ in the future.`)}return void 0===i?new Error(`[UMFAClient] :: enroll :: Unexpected PKP flow: PKP enrollment attempted but no result was returned for user ${e}.`):i}catch(r){if(console.error("ENROLL FLOW :: PASSKEY OPERATION FAILED ::",r),!s)return new Error(`[UMFAClient] :: enroll :: The passkey portion of the Passkeys+ enrollment failed for user ${e} with error: ${r.message||r}. Since userVerification is set to "${t}", the entire enrollment process has failed. You may: retry, downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only enrollment.`)}}if(a=n?.zsmCredentialID?n?.zsmCredentialID:await this.checkEnrollment(e),"string"==typeof a)return new Error(`[UMFAClient] :: enroll :: User ${e} is already enrolled with a ZSM credential. To authenticate it, call the authenticate method.`);await this.zsmAPI.checkIdentity(e,!0);const l=await this.zsmAPI.webauthnCreate(e);return l instanceof Error?new Error(`[UMFAClient] :: enroll :: Unable to enroll ${e}!\nDetails:\n${l?.message||l}`,{cause:l?.stack}):l}async authenticate(e,t="prevented"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("authenticate",e,t,"zsm"));const r=/^(preferred|required)$/.test(t),s="preferred"===t;let i,n,a;if(r){if(n=await this.checkAllEnrollments(e),!n.zsmCredentialID)return new Error(`[UMFAClient] :: authenticate :: User ${e} must be enrolled with a ZSM credential before authenticating. Please enroll the user first.`);if(!n.passkeySupported)return new Error(`[UMFAClient] :: authenticate :: User ${e} is not eligible for Passkeys+ authentication; their device does not support passkey authentication. To authenticate, call the authenticate method with userVerification set to "preferred", "discouraged", or "prevented"/"zsm". To enroll with Passkeys+, call enroll with userVerification set to "preferred" or "required" on a Passkeys+-compatible device.`);if(!n.hasLocalPasskey)return new Error(`[UMFAClient] :: authenticate :: User ${e} must be enrolled with both ZSM and Passkeys+ credentials to authenticate using Passkeys+ (userVerification set to a non-prevented value).`);if(r){try{i=await this.passkeyPlusAuthenticate(n,t)}catch(r){if(!s)return new Error(`[UMFAClient] :: authenticate :: The passkey portion of the Passkeys+ authentication failed for user ${e} with error: ${r.message||r}. Since userVerification is set to "${t}", the entire authentication process has failed. You may: retry or downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only authentication.`)}return void 0===i?new Error(`[UMFAClient] :: authenticate :: Unexpected PKP flow: PKP authentication attempted but no result was returned for user ${e}.`):i}}a=n?.zsmCredentialID?n?.zsmCredentialID:await this.checkEnrollment(e);if(!1===("string"==typeof a))return new Error(`[UMFAClient] :: authenticate :: ${e} is not enrolled. To authenticate the user, please enroll them first by calling the enroll method.`);const l=await this.zsmAPI.webauthnPartialGet(e);return l?.credential??l}async addPasskey(e,t="required"){let r,s;return await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("addPasskey",e,t,"re-enroll,reenroll")),s=r?await this.zsmAPI.pkpCreate(r||e,t):await this.addPasskeyToZSMCredential(r||e,t),s}async passkeyPlusEnroll(e,t="preferred",r=!1){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("passkeyPlusEnroll",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpCreate(e,t,!1,r)}async passkeyPlusAuthenticate(e,t="preferred"){await this.allowBrowserObserversToSettle();let r=null;({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("passkeyPlusAuthenticate",e,t,"re-enroll,reenroll")),r||(r=await this.checkAllEnrollments(e,!0));return await this.zsmAPI.pkpAuthenticate(r,t,!r.zsmCredentialID)}async addZSMToPasskeyCredential(e,t){({userIdentifier:e}=this.typeChecks("addZSMToPasskeyCredential",e));return await this.passkeyPlusAuthenticate(e)}async addPasskeyToZSMCredential(e,t="preferred"){let r=null;({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("addPasskeyToZSMCredential",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpAddPasskeyCredential(r||e,t)}async unenroll(e){if(({userIdentifier:e}=this.typeChecks("unenroll",e)),!1===await this.checkEnrollment(e))return!1;let t=await this.zsmAPI.unbindFromDevice(e);if(t instanceof Error)throw`Unable to unenroll ${e}'s identity from this device.\nDetails:\n${t}`;return!0===t}async resetDevice(){this.zsmAPI.webauthnReset()}}
1
+ import e from"./EventCoordinator.js";import{zsmPluginManager as t}from"./PluginManager.js";import{encodeUserIdentifier as r}from"./encodeUserIdentifier.js";const s=e=>{const t=new Error(`[UMFAClient] :: authenticate :: User ${e}'s enrollment was replaced by a newer one on another device, and has been removed from this device. Please enroll the user again.`);return t.code="ENROLLMENT_REPLACED",t};export default class{constructor(r){e.reset(),e.update("UMFAClientBase"),this.config=r;const s=t.classes("WEBAUTHNCLIENT");this.zsmAPI=new s(r),this.checkEnrollment=this.checkEnrollment.bind(this),this.enroll=this.enroll.bind(this),this.authenticate=this.authenticate.bind(this),this.checkAllEnrollments=this.checkAllEnrollments.bind(this),this.passkeyPlusEnroll=this.passkeyPlusEnroll.bind(this),this.passkeyPlusAuthenticate=this.passkeyPlusAuthenticate.bind(this),this.unenroll=this.unenroll.bind(this),this.resetDevice=this.resetDevice.bind(this),this.isPasskeySupported=this.isPasskeySupported.bind(this),this.passkeyNotSupportedReason=this.passkeyNotSupportedReason.bind(this),this.getPasskeySupportStatus=this.getPasskeySupportStatus.bind(this),this.getPasskeySupportStatus().then(e=>console.debug("[UMFAClient] :: Passkey capability:",JSON.stringify(e,null,2))).catch(()=>{}),e.update("UMFAClientBase","READY")}get userIdentifier(){return this.zsmAPI.userIdentifier}get credentialID(){return this.zsmAPI.credentialID}get metadata(){return this.zsmAPI.metadata}set metadata(e){return this.zsmAPI.metadata=e}typeChecks(e,t,s=null,n=""){if(!t)throw new TypeError(`[UMFAClient] :: ${e} :: A userIdentifier String or Object is required! Received: ${t} (type: ${typeof t}).`);let i;if("object"==typeof t&&(i={...t},t=t.userIdentifier),null!=s){if("boolean"==typeof s)s=!0===s?"preferred":"prevented";else{if("string"!=typeof s)throw new TypeError(`[UMFAClient] :: ${e} :: userVerification must be a boolean or string. Received type: ${typeof s}.`);s=s.toLowerCase()}const t=["preferred","required","discouraged","prevented",...n.split(/, ?/g).filter(Boolean)];if(!new RegExp(`^(${t.join("|")})$`).test(s))throw new TypeError(`[UMFAClient] :: ${e} :: userVerification must be one of the following Strings: "${t.join('", "')}"). Received: ${s}.`)}let a={userIdentifier:t=r(t),userVerification:s};return i&&(a={enrollmentDetails:i,...a}),a}async allowBrowserObserversToSettle(){"undefined"!=typeof window&&("function"==typeof requestAnimationFrame&&await new Promise(e=>requestAnimationFrame(e)),await new Promise(e=>setTimeout(e,0)))}async checkIdentity(e,t){return this.zsmAPI.checkIdentity(r(e),t)}async checkEnrollment(e){({userIdentifier:e}=this.typeChecks("checkEnrollment",e));return await this.zsmAPI.webauthnRetrieve(e)}async checkAllEnrollments(e,t=!1){return this.zsmAPI.webauthnRetrieveAll(r(e),t)}async enroll(e,t="prevented"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("enroll",e,t,"zsm,re-enroll,reenroll"));const r=/^re-?enroll$/.test(t),s="preferred"===t;let n,i,a;if(r||/^(preferred|required)$/.test(t)){if(i=await this.checkAllEnrollments(e),i instanceof Error)return i;if(await this.clearUnvalidatedEnrollment(e,i)&&(i=await this.checkAllEnrollments(e),i instanceof Error))return i;try{if(i.passkeySupported)if(r){if(!i.zsmCredentialID&&!i.hasLocalPasskey)return new Error(`[UMFAClient] :: enroll :: Cannot RE-enroll user's passkey; user ${e} is not currently enrolled with either ZSM or Passkeys+ credentials. To enroll the user, call enroll with a userVerification value of "preferred", "required", or "discouraged".`);n=await this.passkeyPlusEnroll(i,"required",!0)}else{if(i.zsmCredentialID&&i.hasLocalPasskey)return new Error(`[UMFAClient] :: enroll :: User ${e} is already enrolled with both ZSM and Passkeys+ credentials. To authenticate one or both credential, call the authenticate method.`);i instanceof Error||!1===i||!i.zsmCredentialID&&!i.hasLocalPasskey?n=await this.passkeyPlusEnroll(i,t):i.zsmCredentialID&&!i.hasLocalPasskey?n=await this.addPasskeyToZSMCredential(i,t):!i.zsmCredentialID&&i.pkpCredential&&(n=await this.addZSMToPasskeyCredential(i,t))}else{if(!s)return new Error(`[UMFAClient] :: enroll :: Passkeys+ is required for enrollment (userVerification set to "required"), but this device does not support Passkeys+. User ${e} cannot be enrolled. Please use a Passkeys+-compatible device to enroll.`);console.warn(`[UMFAClient] :: enroll :: Passkeys+ enrollment has been requested, but this device does not support Passkeys+. User ${e} will be enrolled with ZSM-only credentials. Please use a Passkeys+-compatible device to enroll with Passkeys+ in the future.`)}if(i?.passkeySupported)return void 0===n?new Error(`[UMFAClient] :: enroll :: Unexpected PKP flow: PKP enrollment attempted but no result was returned for user ${e}.`):n}catch(r){if(console.error("ENROLL FLOW :: PASSKEY OPERATION FAILED ::",r),!s){const s=new Error(`[UMFAClient] :: enroll :: The passkey portion of the Passkeys+ enrollment failed for user ${e} with error: ${r.message||r}. Since userVerification is set to "${t}", the entire enrollment process has failed. You may: retry, downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only enrollment.`,{cause:r});return s.name=r?.name||"Error",s}}}if(a=i?.zsmCredentialID?i?.zsmCredentialID:await this.checkEnrollment(e),"string"==typeof a){if(i||(i=await this.checkAllEnrollments(e)),i instanceof Error)return i;if(i.zsmCredentialID&&!await this.clearUnvalidatedEnrollment(e,i))return new Error(`[UMFAClient] :: enroll :: User ${e} is already enrolled with a ZSM credential. To authenticate it, call the authenticate method.`);a=!1}try{await this.zsmAPI.checkIdentity(e,!0)}catch(t){return new Error(`[UMFAClient] :: enroll :: Unable to establish an identity for ${e}!\nDetails:\n${t?.message||t}`,{cause:t})}const l=await this.zsmAPI.webauthnCreate(e);return l instanceof Error?new Error(`[UMFAClient] :: enroll :: Unable to enroll ${e}!\nDetails:\n${l?.message||l}`,{cause:l?.stack}):l}async authenticate(e,t="prevented"){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("authenticate",e,t,"zsm"));const r=/^(preferred|required)$/.test(t),n="preferred"===t;let i,a,l;if(r){if(a=await this.checkAllEnrollments(e),a instanceof Error)return a;if(await this.clearUnvalidatedEnrollment(e,a))return new Error(`[UMFAClient] :: authenticate :: User ${e}'s enrollment was never validated, and has been cleared from this device. Please enroll the user again.`);if(!a.zsmCredentialID)return a.replaced?s(e):new Error(`[UMFAClient] :: authenticate :: User ${e} must be enrolled with a ZSM credential before authenticating. Please enroll the user first.`);if(a.passkeySupported)if(a.hasLocalPasskey){if(r){try{i=await this.passkeyPlusAuthenticate(a,t)}catch(r){if("ENROLLMENT_REPLACED"===r?.code)return r;if(!n){const s=new Error(`[UMFAClient] :: authenticate :: The passkey portion of the Passkeys+ authentication failed for user ${e} with error: ${r.message||r}. Since userVerification is set to "${t}", the entire authentication process has failed. You may: retry or downgrade the userVerification mandate to "preferred" or "discouraged" to allow for ZSM-only authentication.`,{cause:r});return s.name=r?.name||"Error",s}i=r instanceof Error?r:new Error(String(r),{cause:r})}if(void 0===i&&(i=new Error(`[UMFAClient] :: authenticate :: Unexpected PKP flow: PKP authentication attempted but no result was returned for user ${e}.`)),!(i instanceof Error&&n&&"ENROLLMENT_REPLACED"!==i.code))return i;console.warn(`[UMFAClient] :: authenticate :: The passkey portion of the Passkeys+ authentication failed for user ${e} with error: ${i.message||i}. They are authenticated with their ZSM credential alone.`)}}else{if(!n)return new Error(`[UMFAClient] :: authenticate :: User ${e} holds no passkey on this device, and userVerification is "required". Call enroll to add one. Do not retry authenticate: no passkey exists, so this call gives the same error until enroll creates one.`);console.warn(`[UMFAClient] :: authenticate :: User ${e} holds no passkey on this device. They are authenticated with their ZSM credential alone.`)}else{if(!n)return new Error(`[UMFAClient] :: authenticate :: User ${e} is not eligible for Passkeys+ authentication; their device does not support passkey authentication. To authenticate, call the authenticate method with userVerification set to "preferred", "discouraged", or "prevented"/"zsm". To enroll with Passkeys+, call enroll with userVerification set to "preferred" or "required" on a Passkeys+-compatible device.`);console.warn(`[UMFAClient] :: authenticate :: This device does not support passkeys. User ${e} is authenticated with their ZSM credential alone.`)}}l=a?.zsmCredentialID?a?.zsmCredentialID:await this.checkEnrollment(e);if(!1===("string"==typeof l))return new Error(`[UMFAClient] :: authenticate :: ${e} is not enrolled. To authenticate the user, please enroll them first by calling the enroll method.`);let o;try{o=await this.zsmAPI.webauthnPartialGet(e)}catch(e){if("ENROLLMENT_REPLACED"===e?.code)return e;throw e}return o?.credential??o}async addPasskey(e,t="required"){let r,s;return await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("addPasskey",e,t,"re-enroll,reenroll")),s=r?await this.zsmAPI.pkpCreate(r||e,t):await this.addPasskeyToZSMCredential(r||e,t),s}async passkeyPlusEnroll(e,t="preferred",r=!1){await this.allowBrowserObserversToSettle(),({userIdentifier:e,userVerification:t}=this.typeChecks("passkeyPlusEnroll",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpCreate(e,t,!1,r)}async passkeyPlusAuthenticate(e,t="preferred"){await this.allowBrowserObserversToSettle();let r=null;if(({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("passkeyPlusAuthenticate",e,t,"re-enroll,reenroll")),r||(r=await this.checkAllEnrollments(e,!0)),r instanceof Error)return r;if(await this.clearUnvalidatedEnrollment(e,r))return new Error(`[UMFAClient] :: passkeyPlusAuthenticate :: User ${e}'s enrollment was never validated, and has been cleared from this device. Please enroll the user again.`);return await this.zsmAPI.pkpAuthenticate(r,t,!r.zsmCredentialID)}async addZSMToPasskeyCredential(e,t){({userIdentifier:e}=this.typeChecks("addZSMToPasskeyCredential",e));return await this.passkeyPlusAuthenticate(e)}async addPasskeyToZSMCredential(e,t="preferred"){let r=null;({userIdentifier:e,userVerification:t,enrollmentDetails:r}=this.typeChecks("addPasskeyToZSMCredential",e,t,"re-enroll,reenroll"));return await this.zsmAPI.pkpAddPasskeyCredential(r||e,t)}async unenroll(e){if(({userIdentifier:e}=this.typeChecks("unenroll",e)),!1===await this.checkEnrollment(e))return!1;let t=await this.zsmAPI.unbindFromDevice(e);if(t instanceof Error)throw new Error(`Unable to unenroll ${e}'s identity from this device.\nDetails:\n${t.message||t}`,{cause:t});return!0===t}async clearUnvalidatedEnrollment(e,t){if(!("confirmed"===t?.remoteCheckStatus&&!1===t?.credentialActive&&!!t?.zsmCredentialID&&"suspended"!==t?.enrollmentState))return!1;try{return await this.unenroll(e),console.warn(`[UMFAClient] :: clearUnvalidatedEnrollment :: ${e}'s credential was never validated and has been cleared from this device. The user must enroll again.`),!0}catch(e){return console.error("[UMFAClient] :: clearUnvalidatedEnrollment :: Unable to clear the unvalidated credential:",e),!1}}async isPasskeySupported(){return this.zsmAPI.passkeysPlus.pkpSupported()}async passkeyNotSupportedReason(){return this.zsmAPI.passkeysPlus.passkeyNotSupportedReason()}async getPasskeySupportStatus(){return this.zsmAPI.passkeysPlus.getPasskeySupportStatus()}async resetDevice(){this.zsmAPI.webauthnReset()}async getInstallationId(){const e=await this.zsmAPI.primeZsmApi(),t=await e.installation_id();if(!t||"string"!=typeof t)throw new TypeError(`[UMFAClient] :: getInstallationId :: No installation identifier is available! Received: ${t} (type: ${typeof t}).`);return t}}