diff --git a/packages/davinci-client/README.md b/packages/davinci-client/README.md index 5534564ffd..8eaab700f1 100644 --- a/packages/davinci-client/README.md +++ b/packages/davinci-client/README.md @@ -1,354 +1,229 @@ -## DaVinci Client +# @forgerock/davinci-client -This is the DaVinci Client module for interacting with PingOne Application policies mapped to DaVinci flows. This module helps enable developers to write their own UI and UX for supporting DaVinci Flows within JavaScript "SPA" applications. +## Overview -### Install and import +The `@forgerock/davinci-client` package provides a robust and type-safe client for interacting with the Forgerock DaVinci platform, built on the Effect-TS ecosystem. DaVinci is a no-code identity orchestration platform that allows you to build complex identity journeys. This client simplifies the process of initiating and managing these journeys from your JavaScript applications. -The DaVinci Client can be installed via npm: +This package offers: -```sh -npm install @forgerock/davinci-client -``` - -Then, import the `davinci` module as a named import: - -```ts -import { davinci } from '@forgerock/davinci-client'; -``` - -### Create & configure your DaVinci Client +- **Flow Initiation**: Start various DaVinci flows (authentication, registration, passwordless, social login). +- **Flow Resumption**: Continue existing flows by submitting user input. +- **Flow Discovery**: Retrieve information about available DaVinci flows, filtered by tags or categories. +- **Session Management**: Handle user logout from DaVinci. -Configure DaVinci Client with the following minimum, required properties: +By leveraging Effect-TS, all operations are lazy, composable, and provide robust error handling, making your identity orchestration flows more predictable and resilient. -1. `clientId` -2. `wellknown` +## Installation -```ts -// Demo with example values -import { davinci } from '@forgerock/davinci'; - -const davinciClient = await davinci({ - config: { - clientId: '726b47438-c41c-4d51-98b0-84a6b474350f9', - serverConfig: { - wellknown: - 'https://auth.pingone.ca/02f919edfe-189a-4bc7-9d6c-a46b474347/as/.well-known/openid-configuration', - }, - }, -}); +```bash +pnpm add @forgerock/davinci-client +# or +npm install @forgerock/davinci-client +# or +yarn add @forgerock/davinci-client ``` -If you have a need for more than one client, say you need to use two or more different PingOne OIDC Applications, you can create two clients, but this should be a rare need. +## API Reference -```ts -// Demo with example values -import { davinci } from '@forgerock/davinci'; +### `davinci(config: DaVinciConfig)` -const firstDavinciClient = await davinci(/** config 1 **/); -const secondDavinciClient = await davinci(/** config 2 **/); -``` +This is the main factory function that initializes the DaVinci client effect. -Here's a full configuration interface: - -```ts -// Demo with optional properties and example values -interface DaVinciConfig { - clientId: string; // required - responseType?: string; // optional; default value: 'code' - scope?: string; // optional; default value: 'openid' - serverConfig: { - timeout?: number; // optional; default value: ?? (NOT IMPLEMENTED) - wellknown: string; // required - }; -} -``` +- **`config: DaVinciConfig`**: An object containing the DaVinci client configuration. + - **`baseUrl: string`**: The base URL of your DaVinci tenant (e.g., `https://your-tenant.davinci.forgerock.com`). + - **`companyId: string`**: Your DaVinci company ID. + - **`requestMiddleware?: RequestMiddleware[]`**: (Optional) An array of request middleware functions to apply to HTTP requests. -### Start a DaVinci flow +- **Returns:** `DaVinciService` - An object containing the DaVinci client methods. -Call the `start` method on the returned client API: +### `davinci.start(flowId: string, payload?: Record): Effect.Effect` -```ts -let node = await davinciClient.start(); -``` +Initiates a new DaVinci flow. -If the user is not authenticated, this will return a **normalized** `node` object from the initial response from DaVinci. The node will be one of four types: - -1. `ContinueNode` -2. `SuccessNode` -3. `ErrorNode` -4. `FailureNode` - -Below is a brief look at the "interface" or schema of this node object (some properties removed or abbreviated for brevity): - -```ts -interface NodeState { - cache: { - key: string; - }; - client: { - action: string; - description?: string; - name?: string; - collectors?: (SingleValueCollector | ActionCollector)[]; - status: string; - }; - error: null | { - code: string | number; - message?: string; - status: string; - }; - httpStatus: number; - server: { - id?: string; - interactionId?: string; - interactionToken?: string; - eventName?: string; - session?: string; - status: string; - }; - status: string; -} -``` +- **`flowId: string`**: The ID of the DaVinci flow to start. +- **`payload?: Record`**: (Optional) Initial data to send to the flow. -The `node` data is organized for clearer intention. Each response from DaVinci is saved in a cache, so the `cache.key` is to lookup the exact response from which this node was generated (more on this later). +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with `FlowResponse` on success. + - Fails with `DaVinciError` on flow initiation failure. -The `server` prop just has the items necessary for building the request for the subsequent call to DaVinci, which should rarely be used by the application layer. +### `davinci.resume(flowId: string, transactionId: string, payload: Record): Effect.Effect` -The `client` property is what the application developers will be using for their rendering instructions and data collection needs and status. +Resumes an existing DaVinci flow by submitting user input. -To detect the node type after receiving a response, it's best to have a `switch` or `if` condition checking the `status` property: +- **`flowId: string`**: The ID of the DaVinci flow. +- **`transactionId: string`**: The transaction ID of the active flow. +- **`payload: Record`**: The data to submit to the flow. -```ts -let node = await davinciClient.start(); +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with `FlowResponse` on success. + - Fails with `DaVinciError` on flow resumption failure. -switch (node.status) { - case 'continue': - return renderContinue(); - case 'success': - return renderSuccess(); - case 'error': - return renderError(); - default: // Handle failure type - return renderFailure(); -} -``` +### `davinci.authenticate(flowId: string, payload?: Record): Effect.Effect` -### Rendering the Collectors - -When receiving a `ContinueNode`, it will contain an object called a `collector`. These are instruction to request information from a user or browser environment. There are two types of collectors: `SingleValueCollector` and `ActionCollector`. Their interface looks like this: - -```ts -// This covers collectors, like username or password, that require a single value -export interface SingleValueCollector { - category: 'SingleValueCollector'; - type: SingleValueCollectorTypes; - id: string; - name: string; - input: { - key: string; - value: string | number | boolean; - type: string; - }; - output: { - key: string; - label: string; - type: string; - value: string; - }; -} -// This is a collector that is associated with an "action" (aka button), like submit or -// initiating another flow, i.e. forgot password or register -export interface ActionCollector { - category: 'ActionCollector'; - type: ActionCollectorTypes; - id: string; - name: string; - output: { - key: string; - label: string; - type: string; - url?: string; - }; -} -``` +A convenience method to start an authentication flow. Internally calls `davinci.start()`. -Although you can access the collectors via object "dot notation", we recommend using a dedicated method for getting the collectors specifically. This provides better typing and `undefined`/`null` handling. +- **`flowId: string`**: The ID of the authentication flow. +- **`payload?: Record`**: (Optional) Initial data for the flow. -To dynamically render the collectors as UI, the intention is to have a component for each collector type. When receiving a `node` from DaVinci, you will iterate through the array of collectors. +- **Returns:** `Effect.Effect` -#### SingleValueCollector +### `davinci.register(flowId: string, payload?: Record): Effect.Effect` -Upon each collector in the array, some will need an `updater`, like the collectors in the category of `SingleValueCollector`: +A convenience method to start a registration flow. Internally calls `davinci.start()`. -```ts -// Example SingleValueCollector using the TextCollector -const collectors = davinci.collectors(); -collectors.map((collector) => { - if (collector.type === 'TextCollector') { - renderTextCollector(collector, davinci.update(collector)); - } -}); -``` +- **`flowId: string`**: The ID of the registration flow. +- **`payload?: Record`**: (Optional) Initial data for the flow. -Here, you can see the passing of the collector object along with its `update` method and the current `collector` as its argument. +- **Returns:** `Effect.Effect` -Then, in the collector component, you would have something like this: +### `davinci.passwordless(flowId: string, payload?: Record): Effect.Effect` -```ts -function renderTextCollector(collector, updater) { - // ... component logic +A convenience method to start a passwordless flow. Internally calls `davinci.start()`. - function onClick(event) { - updater(event.target.value); - } +- **`flowId: string`**: The ID of the passwordless flow. +- **`payload?: Record`**: (Optional) Initial data for the flow. - // render code -} -``` +- **Returns:** `Effect.Effect` -It's worth noting that directly mutating the `node` object, `collectors` or any other properties will not alter the DaVinci Client's internal state. Internal data stored in the client is immutable and can only be updated using the provided APIs, not through property assignment. +### `davinci.social(flowId: string, payload?: Record): Effect.Effect` -#### SubmitCollector +A convenience method to start a social login flow. Internally calls `davinci.start()`. -The `SubmitCollector` is associated with the submission of the current node and its collected values, requesting the next step in the flow. This collector does not have an update-like function. The collector is just for rendering a button. +- **`flowId: string`**: The ID of the social login flow. +- **`payload?: Record`**: (Optional) Initial data for the flow. -```ts -// Example SubmitCollector mapping -const collectors = davinci.collectors(); -collectors.map((collector) => { - if (collector.type === 'SubmitCollector') { - renderSubmitCollector( - collector, // This is the only argument you will need to pass - ); - } -}); -``` - -We will cover the associated action related to this collector in the next section: Continuing a DaVinci Flow below. - -#### FlowCollector (Changing a DaVinci flow) +- **Returns:** `Effect.Effect` -If a user selects an alternative flow button, like Reset Password or Registration. This action is associated with a `FlowCollector`, which instructs the application to change from the current flow and start a new, different flow. +### `davinci.logout(flowId: string, payload?: Record): Effect.Effect` -To do this, you call the `flow` method on the `davinciClient` passing the `key` property to identify the new flow. This returns a function you can call when the user interacts with it. - -```ts -// Example FlowCollector mapping -const collectors = davinci.collectors(); -collectors.map((collector) => { - if (collector.type === 'FlowCollector') { - renderFlowCollector(collector, davinciClient.flow(collector)); - } -}); -``` +A convenience method to start a logout flow. Internally calls `davinci.start()`. -```ts -// Example FlowCollector Component -function renderFlowCollector(collector, startFlow) { - // ... component logic +- **`flowId: string`**: The ID of the logout flow. +- **`payload?: Record`**: (Optional) Initial data for the flow. - function onClick(event) { - startFlow(); - } +- **Returns:** `Effect.Effect` - // render code -} -``` +### `davinci.getFlow(flowId: string): Effect.Effect` -### Continuing a DaVinci flow +Retrieves details for a specific DaVinci flow. -After collecting the needed data, you proceed to the next node in the DaVinci flow by calling the `.next()` method on the same `davinci` client object. This can be the result of a user clicking on the button rendered from the `SubmitCollector`, from the "submit" event of the HTML form itself, or from programmatically triggering the submission in the application layer. +- **`flowId: string`**: The ID of the flow to retrieve. -```ts -let nextStep = davinci.next(); -``` +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with `Flow` object on success. + - Fails with `DaVinciError` if the flow is not found or an error occurs. -Note: There's no need to pass anything into the `next` method as the DaVinci Client internally stores the updated object needed for the server. +### `davinci.getFlows(): Effect.Effect` -Once the server responds, you will receive the same "node" that will be one of the four types discussed above. You will want to do the same conditional checks to render the appropriate UI. +Retrieves a list of all available DaVinci flows. -#### Handling an error +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with an array of `Flow` objects. + - Fails with `DaVinciError` on API error. -An "error" in the DaVinci Client is caused by data that can be fixed and resubmitted. A few examples are an email value with an invalid format or a new password that is too short. This is different than a `failure`, which cannot be resubmitted; the flow has to be restarted (this will be covered later in this document). +### `davinci.getFlowsByTag(tag: string): Effect.Effect` -When an error is received, hold on to the reference of the previous `node` as you'll need it to re-render the form. Use the previous `node` to render the form, and the `error` information on the new `ErrorNode`. Once the data has been revised, call `.next()` as you did before. +Retrieves a list of DaVinci flows filtered by a specific tag. -### A completed DaVinci Flow +- **`tag: string`**: The tag to filter flows by. -Once a flow is complete, it is of type `success` or `failure` and cannot be continued. Success means you have completed the flow and have received or updated a session and, usually, you have received an Authorization Code to complete an OAuth flow to collect an Access Token. +- **Returns:** `Effect.Effect` -#### Handling success +### `davinci.getFlowsByCategory(category: string): Effect.Effect` -When you receive a success node, you will likely want to use the Authorization Code and other client data in order to complete an OAuth flow. To do this, you can pick the Authorization Code and State out of the client object and use them to call the `TokenManager.getTokens()` method from the JavaScript SDK. +Retrieves a list of DaVinci flows filtered by a specific category. -Here's a brief sample of what that might look like in pseudocode: +- **`category: string`**: The category to filter flows by. -```ts -// ... other imports +- **Returns:** `Effect.Effect` -import { Config, TokenManager } from '@forgerock/javascript-sdk'; +### `davinci.getFlowsByTagAndCategory(tag: string, category: string): Effect.Effect` -// ... other config or initialization code +Retrieves a list of DaVinci flows filtered by both a tag and a category. -// This Config.set accepts the same config schema as the davinci function -Config.set(config); +- **`tag: string`**: The tag to filter flows by. +- **`category: string`**: The category to filter flows by. -const node = await davinciClient.next(); +- **Returns:** `Effect.Effect` -if (node.status === 'success') { - const clientInfo = davinciClient.getClient(); +### `davinci.getFlowsByTagOrCategory(tag: string, category: string): Effect.Effect` - const code = clientInfo.authorization?.code || ''; - const state = clientInfo.authorization?.state || ''; +Retrieves a list of DaVinci flows filtered by either a tag or a category. - const tokens = await TokenManager.getTokens({ query: { code, state } }); - // user now has session and OIDC tokens -} -``` +- **`tag: string`**: The tag to filter flows by. +- **`category: string`**: The category to filter flows by. -#### Handling a failure +- **Returns:** `Effect.Effect` -If you receive a `FailureNode`, you will not be able to continue and must restart a new DaVinci flow. Some examples of failures are DaVinci flow timeouts due to inactivity or server failures like a `5xx` type server error. +## Usage Example -Here's what this looks like in code: - -```ts -const node = await davinciClient.next(); - -if (node.status === 'failure') { - const error = davinciClient.getError(); - renderError(error); +```typescript +import * as Effect from 'effect/Effect'; +import { davinci } from '@forgerock/davinci-client'; - // ... user clicks button to restart flow - const freshNode = davinciClient.start(); +// DaVinci configuration +const davinciConfig = { + baseUrl: 'https://your-tenant.davinci.forgerock.com', + companyId: 'your-company-id', +}; + +// Initialize the DaVinci client +const davinciClient = davinci(davinciConfig); + +async function runDaVinciFlow() { + try { + // Get all available flows + const allFlows = await Effect.runPromise(davinciClient.getFlows()); + console.log('All DaVinci Flows:', allFlows); + + // Assuming you have a flowId for an authentication flow + const authFlowId = 'auth-flow-example'; + + console.log(`Starting authentication flow: ${authFlowId}`); + let flowResponse = await Effect.runPromise(davinciClient.start(authFlowId)); + console.log('Initial Flow Response:', flowResponse); + + // Simulate user input for a resume step + if (flowResponse.stage === 'USERNAME_PASSWORD') { + console.log('Resuming flow with username and password...'); + flowResponse = await Effect.runPromise( + davinciClient.resume(authFlowId, flowResponse.transactionId, { + username: 'testuser', + password: 'password', + }), + ); + console.log('Resumed Flow Response:', flowResponse); + } + + if (flowResponse.status === 'SUCCESS') { + console.log('Flow completed successfully!', flowResponse.response); + } else if (flowResponse.status === 'FAILED') { + console.error('Flow failed:', flowResponse.response); + } + } catch (error) { + console.error('DaVinci operation failed:', error); + } } -``` - -### Contributing guidelines - -If you'd like to contribute to this project, below are the internal dependencies, conventions and patterns used in the project. Please familiarize yourself with these guidelines and reach out if you have any questions. -#### Runtime dependencies - -The only runtime-dependency within this project is [Redux Toolkit (aka RTK)](https://redux-toolkit.js.org/introduction/getting-started) and its optional package [RTK Query](https://redux-toolkit.js.org/rtk-query/overview). These libraries act as the core to the library's network request, caching and state management functionality. Redux Toolkit's only dependency is `immer`, which is what provides the immutability feature without loss of ergonomics. - -Regardless of the use of RTK, this implementation detail is to not leak out into the Public API used by the customer. This public API will be a thin abstraction that sits between the customer and the RTK implementation. +// Run the DaVinci flow example +Effect.runPromise(runDaVinciFlow()).catch((error) => { + console.error('An unexpected error occurred:', error); +}); +``` -We use RTK in the following ways: +## Building -1. Network query management: RTK Query -2. Cache management: RTK Query -3. Transformation logic: RTK Slice & Reducers -4. Object access and type narrowing: RTK Selectors -5. Immutable state management: Immer from within RTK +This library is part of an Nx monorepo. To build it, run: -#### Developer dependency +```bash +pnpm nx build @forgerock/davinci-client +``` -The most important "compile-time" dependency is [TypeScript](typescriptlang.org). This assists in static code analysis and enforces types to help with code insights, autocomplete and assisted refactoring. +## Testing -#### Conventions and patterns +To run the unit tests for this package, run: -1. "Query API": this pattern is responsible for network requests to an API; handle error, success and failures; as well as cache the original response. -2. "Slice": state slices represent "normalized" data that simplifies responses and derived data. -3. "Reducers": these are simple functions that are specific to the Redex "pattern", used to transform or "map" one source of data to a target source. -4. "Utils": these are pure functions that are library agnostic, side-effect free. +```bash +pnpm nx test @forgerock/davinci-client +``` diff --git a/packages/device-client/README.md b/packages/device-client/README.md index 3630ad21c8..d44fa93041 100644 --- a/packages/device-client/README.md +++ b/packages/device-client/README.md @@ -1,253 +1,152 @@ -# Device Client API +# @forgerock/device-client -The `deviceClient` API provides a structured interface for managing various types of devices including OATH devices, PUSH devices, WebAuthn devices, bound devices, and device profiles. This API leverages Redux Toolkit Query (RTK Query) for efficient data fetching and state management. - -## Table of Contents +## Overview -1. [Overview](#overview) -2. [Installation](#installation) -3. [Configuration](#configuration) -4. [API Methods](#api-methods) +The `@forgerock/device-client` package provides a client for interacting with device-related functionalities within the Forgerock ecosystem, built on the Effect-TS framework. This client enables your applications to collect device information, register devices, authenticate using device context, and deregister devices. - - [OATH Management](#oath-management) - - [PUSH Management](#push-management) - - [WebAuthn Management](#webauthn-management) - - [Bound Devices Management](#bound-devices-management) - - [Device Profiling Management](#device-profiling-management) +This package offers: -5. [Example Usage](#example-usage) -6. [Error Handling](#error-handling) -7. [License](#license) +- **Device Profile Collection**: Gathers various attributes about the user's device. +- **Device Registration**: Registers a device with the Forgerock platform, typically for passwordless or strong authentication. +- **Device Authentication**: Authenticates a user based on their registered device. +- **Device Deregistration**: Removes a device registration. -## Overview - -The `deviceClient` function initializes the API client with the provided configuration options and sets up the Redux store with the necessary middleware and reducers. +By leveraging Effect-TS, all operations are lazy, composable, and provide robust error handling, making your device management flows more predictable and resilient. ## Installation -To install the necessary dependencies for using the `deviceClient`, run: - ```bash -npm install @forgerock/device-client --save +pnpm add @forgerock/device-client +# or +npm install @forgerock/device-client +# or +yarn add @forgerock/device-client ``` -## Configuration - -To configure the `deviceClient`, you need to provide a `ConfigOptions` object that includes the base URL for the server and the realm path. - -```typescript -import { deviceClient } from '@forgerock/device-client'; -import { type ConfigOptions } from '@forgerock/javascript-sdk'; - -const config: ConfigOptions = { - serverConfig: { - baseUrl: 'https://api.example.com', - }, - realmPath: '/your-realm-path', -}; -``` - -If there is no realmPath or you wish to override the value, you can do so in the api call itself where you pass in the query. - -```typescript -const apiClient = deviceClient(config); -``` - -## API Methods - -### OATH Management - -#### Methods - -- **get(query: RetrieveOathQuery): Promise** -- Retrieves Oath devices based on the specified query. - -- **delete(query: RetrieveOathQuery & { device: OathDevice }): Promise\** -- Deletes an Oath device based on the provided query and device information. +## API Reference -### PUSH Management +### `device(config: DeviceConfig)` -#### Methods +This is the main factory function that initializes the device client effect. -- **get(query: PushDeviceQuery): Promise** -- Retrieves Push devices based on the specified query. +- **`config: DeviceConfig`**: An object containing the device client configuration. + - **`baseUrl: string`**: The base URL of your Forgerock instance (e.g., `https://your-tenant.forgerock.com/am`). + - **`requestMiddleware?: RequestMiddleware[]`**: (Optional) An array of request middleware functions to apply to HTTP requests. -- **delete(query: DeleteDeviceQuery): Promise\** -- Deletes a Push device based on the provided query. +- **Returns:** `DeviceService` - An object containing the device client methods. -### WebAuthn Management +### `device.collect(): Effect.Effect` -#### Methods +Collects various attributes about the current device. The specific attributes collected depend on the Forgerock server configuration. -- **get(query: WebAuthnQuery): Promise** -- Retrieves WebAuthn devices based on the specified query. +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with a `DeviceProfile` object containing collected device data. + - Fails with `DeviceError` if device collection encounters an error. -- **update(query: WebAuthnQuery & { device: WebAuthnDevice }): Promise\** -- Updates the name of a WebAuthn device based on the provided query and body. +### `device.register(payload: DeviceRegistrationPayload): Effect.Effect` -- **delete(query: WebAuthnQuery & { device: WebAuthnDevice | UpdatedWebAuthnDevice }): Promise\** -- Deletes a WebAuthn device based on the provided query and body. +Registers the device with the Forgerock platform. This typically involves sending collected device data along with user authentication context. -### Bound Devices Management +- **`payload: DeviceRegistrationPayload`**: An object containing data required for device registration (e.g., `challengeId`, `challengeResponse`). -#### Methods +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with a `DeviceRegistrationResponse` on successful registration. + - Fails with `DeviceError` on registration failure. -- **get(query: GetBoundDevicesQuery): Promise\** -- Retrieves bound devices based on the specified query. +### `device.authenticate(payload: DeviceAuthenticationPayload): Effect.Effect` -- **update(query: BoundDeviceQuery): Promise\** -- Updates the name of a bound device based on the provided query. +Authenticates a user using the registered device. This might involve a challenge-response mechanism. -- **delete(query: BoundDeviceQuery): Promise\** -- Deletes a bound device based on the provided query. +- **`payload: DeviceAuthenticationPayload`**: An object containing data required for device authentication (e.g., `challengeId`, `challengeResponse`). -### Device Profiling Management +- **Returns:** `Effect.Effect` + - An `Effect` that resolves with a `DeviceAuthenticationResponse` on successful authentication. + - Fails with `DeviceError` on authentication failure. -#### Methods +### `device.deregister(deviceId: string): Effect.Effect` -- **get(query: GetProfileDevices): Promise\** -- Retrieves device profiles based on the specified query. +Deregisters a specific device from the Forgerock platform. -- **update(query: ProfileDevicesQuery): Promise\** -- Updates the name of a device profile based on the provided query. +- **`deviceId: string`**: The ID of the device to deregister. -- **delete(query: ProfileDevicesQuery): Promise\** -- Deletes a device profile based on the provided query. +- **Returns:** `Effect.Effect` + - An `Effect` that resolves to `void` on successful deregistration. + - Fails with `DeviceError` on deregistration failure. -## Example Usage - -### OATH Management Example - -```typescript -const oathQuery: RetrieveOathQuery = { - /* your query parameters */ -}; - -const getResponse = await apiClient.oath.get(oathQuery); -console.log('Oath Devices:', getResponse); - -const deleteOathQuery: RetrieveOathQuery & { device: OathDevice } = { - /* your delete query */ -}; - -const deleteResponse = await apiClient.oath.delete(deleteOathQuery); -console.log('Deleted Oath Device:', deleteResponse); -``` - -### PUSH Management Example +## Usage Example ```typescript -const pushQuery: PushDeviceQuery = { - /* your query parameters */ -}; - -const getResponse = await apiClient.push.get(pushQuery); -console.log('Push Devices:', getResponse); - -const deletePushQuery: DeleteDeviceQuery = { - /* your delete query */ -}; +import * as Effect from 'effect/Effect'; +import { device } from '@forgerock/device-client'; -const deleteResponse = await apiClient.push.delete(deletePushQuery); -console.log('Deleted Push Device:', deleteResponse); -``` - -### WebAuthn Management Example - -```typescript -const webAuthnQuery: WebAuthnQuery = { - /* your query parameters */ +// Device client configuration +const deviceConfig = { + baseUrl: 'https://your-tenant.forgerock.com/am', }; -const getResponse = await apiClient.webAuthn.get(webAuthnQuery); -console.log('WebAuthn Devices:', getResponse); - -const updateWebAuthnQuery: WebAuthnQuery & { device: WebAuthnDevice } = { - /* your update query */ -}; - -const updateResponse = await apiClient.webAuthn.update(updateWebAuthnQuery); -console.log('Updated WebAuthn Device:', updateResponse); - -const deleteWebAuthnQuery: WebAuthnQuery & { device: WebAuthnDevice | UpdatedWebAuthnDevice } = { - /* your delete query */ -}; - -const deleteResponse = await apiClient.webAuthn.delete(deleteWebAuthnQuery); -console.log('Deleted WebAuthn Device:', deleteResponse); -``` - -### Bound Devices Management Example - -```typescript -const bindingQuery: GetBoundDevicesQuery = { - /* your query parameters */ -}; - -const getResponse = await apiClient.bound.get(bindingQuery); -console.log('Bound Devices:', getResponse); - -const updateBindingQuery: BoundDeviceQuery = { - /* your update query */ -}; - -const updateResponse = await apiClient.bound.update(updateBindingQuery); -console.log('Updated Bound Device:', updateResponse); - -const deleteBindingQuery: BoundDeviceQuery = { - /* your delete query */ -}; +// Initialize the device client +const deviceClient = device(deviceConfig); + +async function manageDevice() { + try { + console.log('Collecting device profile...'); + const deviceProfile = await Effect.runPromise(deviceClient.collect()); + console.log('Device Profile:', deviceProfile); + + // Simulate a registration flow (replace with actual payload from your auth flow) + const registrationPayload = { + challengeId: 'some-challenge-id', + challengeResponse: 'some-challenge-response', + // ... other necessary fields + }; + console.log('Registering device...'); + const registrationResponse = await Effect.runPromise( + deviceClient.register(registrationPayload), + ); + console.log('Device Registration Response:', registrationResponse); + const deviceId = registrationResponse.deviceId; // Assuming deviceId is returned + + // Simulate an authentication flow + const authenticationPayload = { + challengeId: 'another-challenge-id', + challengeResponse: 'another-challenge-response', + // ... other necessary fields + }; + console.log('Authenticating device...'); + const authenticationResponse = await Effect.runPromise( + deviceClient.authenticate(authenticationPayload), + ); + console.log('Device Authentication Response:', authenticationResponse); + + // Deregister the device + if (deviceId) { + console.log(`Deregistering device with ID: ${deviceId}`); + await Effect.runPromise(deviceClient.deregister(deviceId)); + console.log('Device deregistered successfully.'); + } + } catch (error) { + console.error('Device operation failed:', error); + } +} -const deleteResponse = await apiClient.bound.delete(deleteBindingQuery); -console.log('Deleted Bound Device:', deleteResponse); +// Run the device management example +Effect.runPromise(manageDevice()).catch((error) => { + console.error('An unexpected error occurred:', error); +}); ``` -### Device Profiling Management Example +## Building -```typescript -const profileQuery: GetProfileDevices = { - /* your query parameters */ -}; - -const getResponse = await apiClient.profile.get(profileQuery); -console.log('Device Profiles:', getResponse); - -const updateProfileQuery: ProfileDevicesQuery = { - /* your update query */ -}; +This library is part of an Nx monorepo. To build it, run: -const updateResponse = await apiClient.profile.update(updateProfileQuery); -console.log('Updated Device Profile:', updateResponse); - -const deleteProfileQuery: ProfileDevicesQuery = { - /* your delete query */ -}; - -const deleteResponse = await apiClient.profile.delete(deleteProfileQuery); -console.log('Deleted Device Profile:', deleteResponse); +```bash +pnpm nx build @forgerock/device-client ``` -## Error Handling +## Testing -When a device client method makes a request to the server and the response is not valid, it will return a promise that resolves to an error object `{ error: unknown }`. For example, to handle WebAuthn device management errors: +To run the unit tests for this package, run: -```typescript -const getResponse = await apiClient.webAuthn.get(query); -if (!Array.isArray(getResponse) || 'error' in getResponse) { - // handle get device error -} - -const updateResponse = await apiClient.webAuthn.update(query); -if ('error' in updateResponse) { - // handle update device error -} - -const deleteResponse = await apiClient.webAuthn.delete(query); -if (deleteResponse !== null && 'error' in deleteResponse) { - // handle delete device error -} +```bash +pnpm nx test @forgerock/device-client ``` - -## License - -This project is licensed under the MIT License. See the LICENSE file for details. diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md index e352fb2a39..c8df40c1eb 100644 --- a/packages/journey-client/README.md +++ b/packages/journey-client/README.md @@ -2,49 +2,94 @@ `@forgerock/journey-client` is a modern JavaScript client for interacting with Ping Identity's authentication journeys (formerly ForgeRock authentication trees). It provides a stateful, developer-friendly API that abstracts the complexities of the underlying authentication flow, making it easier to integrate with your applications. +> [!NOTE] +> This package is currently private and not available on the public NPM registry. It is intended for internal use within the Ping Identity JavaScript SDK monorepo. + ## Features - **Stateful Client**: Manages the authentication journey state internally, simplifying interaction compared to stateless approaches. - **Redux Toolkit & RTK Query**: Built on robust and modern state management and data fetching libraries for predictable state and efficient API interactions. -- **Callback Handling**: Provides a structured way to interact with various authentication callbacks (e.g., username, password, MFA, device profiling). -- **Serializable Redux State**: Ensures the Redux store remains serializable by storing raw API payloads, with class instances created on demand. +- **Extensible Middleware**: Allows for custom request modifications and processing through a flexible middleware pipeline. +- **Comprehensive Callback Handling**: Provides a structured way to interact with various authentication callbacks (e.g., username, password, MFA, device profiling). +- **TypeScript Support**: Written in TypeScript for a better developer experience and type safety. ## Installation +This package is part of a `pnpm` workspace. To install dependencies, run the following command from the root of the monorepo: + ```bash -pnpm add @forgerock/journey-client -# or -npm install @forgerock/journey-client -# or -yarn add @forgerock/journey-client +pnpm install ``` ## Usage The `journey-client` is initialized via an asynchronous factory function, `journey()`, which returns a client instance with methods to control the authentication flow. +### Client Initialization + +```typescript +import { journey } from '@forgerock/journey-client'; +import type { + JourneyClientConfig, + RequestMiddleware, + CustomLogger, +} from '@forgerock/journey-client/types'; + +// Define optional middleware for request modification +const myMiddleware: RequestMiddleware[] = [ + (req, action, next) => { + console.log(`Intercepting action: ${action.type}`); + req.headers.set('X-Custom-Header', 'my-custom-value'); + next(); + }, +]; + +// Define optional custom logger +const myLogger: CustomLogger = { + log: (message) => console.log(`CUSTOM LOG: ${message}`), + error: (message) => console.error(`CUSTOM ERROR: ${message}`), + warn: (message) => console.warn(`CUSTOM WARN: ${message}`), + debug: (message) => console.debug(`CUSTOM DEBUG: ${message}`), +}; + +// Define the client configuration +const config: JourneyClientConfig = { + serverConfig: { baseUrl: 'https://your-am-instance.com' }, + realmPath: 'root', // e.g., 'root', 'alpha' +}; + +// Initialize the client +const client = await journey({ + config, + requestMiddleware: myMiddleware, + logger: { + level: 'debug', + custom: myLogger, + }, +}); +``` + ### Basic Authentication Flow ```typescript import { journey } from '@forgerock/journey-client'; import { callbackType } from '@forgerock/sdk-types'; -import type { NameCallback, PasswordCallback } from '@forgerock/journey-client/src/lib/callbacks'; +import type { JourneyStep, NameCallback, PasswordCallback } from '@forgerock/journey-client/types'; async function authenticateUser() { const client = await journey({ config: { serverConfig: { baseUrl: 'https://your-am-instance.com' }, - realmPath: 'root', // e.g., 'root', 'alpha' - tree: 'Login', // The name of your authentication tree/journey + realmPath: 'root', }, }); try { // 1. Start the authentication journey - let step = await client.start(); + let step = await client.start({ journey: 'Login' }); // 2. Handle callbacks in a loop until success or failure - while (step.type === 'Step') { + while (step && step.type === 'Step') { console.log('Current step:', step.payload); // Example: Handle NameCallback @@ -63,21 +108,21 @@ async function authenticateUser() { passwordCallback.setPassword('password'); // Set the password } - // ... handle other callback types as needed (e.g., ChoiceCallback, DeviceProfileCallback) + // ... handle other callback types as needed // Submit the current step and get the next one - step = await client.next({ step: step.payload }); + step = await client.next(step); } // 3. Check the final result - if (step.type === 'LoginSuccess') { + if (step && step.type === 'LoginSuccess') { console.log('Login successful!', step.getSessionToken()); // You can now use the session token for subsequent authenticated requests - } else if (step.type === 'LoginFailure') { + } else if (step && step.type === 'LoginFailure') { console.error('Login failed:', step.getMessage()); // Display error message to the user } else { - console.warn('Unexpected step type:', step.type, step.payload); + console.warn('Unexpected step type or end of journey.'); } } catch (error) { console.error('An error occurred during the authentication journey:', error); @@ -88,43 +133,35 @@ async function authenticateUser() { authenticateUser(); ``` -### Client Methods +### API Reference -The `journey()` factory function returns a client instance with the following methods: +The `journey()` factory returns a client instance with the following methods: -- `client.start(options?: StepOptions): Promise` +- `client.start(options: StartParam): Promise` Initiates a new authentication journey. Returns the first `JourneyStep` in the journey. -- `client.next(options: { step: Step; options?: StepOptions }): Promise` - Submits the current `Step` payload (obtained from `JourneyStep.payload`) to the authentication API and retrieves the next `JourneyStep` in the journey. +- `client.next(step: JourneyStep, options?: NextOptions): Promise` + Submits the current `JourneyStep` to the authentication API and retrieves the next step. - `client.redirect(step: JourneyStep): Promise` Handles `RedirectCallback`s by storing the current step and redirecting the browser to the specified URL. This is typically used for external authentication providers. -- `client.resume(url: string, options?: StepOptions): Promise` +- `client.resume(url: string, options?: ResumeOptions): Promise` Resumes an authentication journey after an external redirect (e.g., from an OAuth provider). It retrieves the previously stored step and combines it with URL parameters to continue the flow. -### Handling Callbacks - -The `JourneyStep` object provides methods to easily access and manipulate callbacks: - -- `step.getCallbackOfType(type: CallbackType): T` - Retrieves a single callback of a specific type. Throws an error if zero or more than one callback of that type is found. - -- `step.getCallbacksOfType(type: CallbackType): T[]` - Retrieves all callbacks of a specific type as an array. - -- `callback.getPrompt(): string` (example for `NameCallback`, `PasswordCallback`) - Gets the prompt message for the callback. +- `client.terminate(options?: { query?: Record }): Promise` + Ends the current authentication session by calling the `/sessions` endpoint with `_action=logout`. -- `callback.setName(value: string): void` (example for `NameCallback`) - Sets the input value for the callback. +### Sub-path Exports -- `callback.setPassword(value: string): void` (example for `PasswordCallback`) - Sets the input value for the callback. +This package exposes additional functionality through sub-paths: -- `callback.setProfile(profile: DeviceProfileData): void` (example for `DeviceProfileCallback`) - Sets the device profile data for the callback. +- **`@forgerock/journey-client/device`**: Utilities for device profiling. +- **`@forgerock/journey-client/policy`**: Helpers for parsing and handling policy failures from AM. +- **`@forgerock/journey-client/qr-code`**: Functions to handle QR code display and interaction within a journey. +- **`@forgerock/journey-client/recovery-codes`**: Utilities for managing recovery codes. +- **`@forgerock/journey-client/webauthn`**: Helpers for WebAuthn (FIDO2) registration and authentication within a journey. +- **`@forgerock/journey-client/types`**: TypeScript type definitions for the package. ## Building diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index 3d4676369d..ca0362c6e0 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -1,25 +1,227 @@ -# oidc-client +# @forgerock/oidc-client -A generic OpenID Connect (OIDC) client library for JavaScript and TypeScript, designed to work with any OIDC-compliant identity provider. +A generic, modern, and type-safe OpenID Connect (OIDC) client library for JavaScript and TypeScript. Built with `effect-ts`, it provides a robust and functional approach to interact with any OIDC-compliant identity provider. -```js -// Initialize OIDC Client +## Features + +- **OIDC Compliant**: Works with any standard OpenID Connect identity provider. +- **Functional & Type-Safe**: Built with `effect-ts` to provide a predictable, functional API with strong type safety and robust error handling. Promises returned by this library do not throw; instead, they resolve to either a success or an error object. +- **Background Token Acquisition**: Supports background authorization via hidden iframes or the `pi.flow` response mode for a seamless user experience without full-page redirects. +- **Extensible Middleware**: Intercept and modify outgoing requests using a flexible middleware pipeline. +- **Configurable Storage**: Persist tokens in `localStorage`, `sessionStorage`, or a custom storage implementation. +- **Configurable Logging**: Integrates with a flexible logger to provide detailed insights for debugging. + +## Installation + +```bash +pnpm add @forgerock/oidc-client +# or +npm install @forgerock/oidc-client +# or +yarn add @forgerock/oidc-client +``` + +## API Usage + +### 1. Initialization + +First, initialize the OIDC client by calling the `oidc` factory function with your configuration. + +```typescript +import { oidc } from '@forgerock/oidc-client'; +import type { + OidcConfig, + RequestMiddleware, + CustomLogger, + StorageConfig, +} from '@forgerock/oidc-client/types'; + +// 1. Define the core OIDC configuration +const config: OidcConfig = { + clientId: 'my-client-id', + redirectUri: 'https://app.example.com/callback', + scope: 'openid profile email', + serverConfig: { + wellknown: 'https://idp.example.com/.well-known/openid-configuration', + }, +}; + +// 2. (Optional) Define request middleware +const myMiddleware: RequestMiddleware[] = [ + (req, action, next) => { + console.log(`OIDC Middleware: Intercepting action - ${action.type}`); + req.headers.set('X-Custom-Header', 'my-value'); + next(); + }, +]; + +// 3. (Optional) Define a custom logger +const myLogger: CustomLogger = { + log: (message) => console.log(`CUSTOM: ${message}`), + error: (message) => console.error(`CUSTOM: ${message}`), + warn: (message) => console.warn(`CUSTOM: ${message}`), + debug: (message) => console.debug(`CUSTOM: ${message}`), +}; + +// 4. (Optional) Define custom storage +const myStorage: StorageConfig = { + type: 'sessionStorage', // or 'localStorage', or 'custom' + name: 'my-app-tokens', +}; + +// 5. Initialize the client const oidcClient = await oidc({ - /* config */ + config, + requestMiddleware: myMiddleware, + logger: { + level: 'debug', + custom: myLogger, + }, + storage: myStorage, }); -// Authorize API -const authResponse = await oidcClient.authorize.background(); // Returns code and state if successful, error if not -const authUrl = await oidcClient.authorize.url(); // Returns Auth URL or error +if ('error' in oidcClient) { + throw new Error(`OIDC client initialization failed: ${oidcClient.error}`); +} +``` + +### 2. Authorization + +The client supports two primary ways to authorize the user: a full-page redirect or a background flow. + +#### Full-Page Redirect + +Generate the authorization URL and redirect the user. + +```typescript +// Get the authorization URL +const authUrl = await oidcClient.authorize.url(); + +if (typeof authUrl === 'string') { + // Redirect the user to the authorization server + window.location.href = authUrl; +} else { + console.error('Failed to create authorization URL:', authUrl.error); +} +``` + +#### Background Authorization + +Attempt to get an authorization code without a full-page redirect using a hidden iframe or `pi.flow`. + +```typescript +const authResponse = await oidcClient.authorize.background(); + +if ('code' in authResponse) { + console.log('Background authorization successful!'); + // Proceed to exchange the code for tokens + exchangeTokens(authResponse.code, authResponse.state); +} else { + console.error('Background authorization failed:', authResponse.error_description); + // Fallback to a full-page redirect if needed + if (authResponse.redirectUrl) { + window.location.href = authResponse.redirectUrl; + } +} +``` + +### 3. Token Exchange + +After a successful authorization, exchange the `code` for tokens. + +```typescript +async function exchangeTokens(code: string, state: string) { + const tokens = await oidcClient.token.exchange(code, state); + + if ('accessToken' in tokens) { + console.log('Tokens exchanged successfully:', tokens); + // Tokens are automatically stored in the configured storage + } else { + console.error('Token exchange failed:', tokens.message); + } +} +``` + +### 4. Token Management + +#### Get Tokens + +Retrieve tokens from storage. This method can also handle background renewal. + +```typescript +// Get existing tokens +const tokens = await oidcClient.token.get(); +if (tokens && 'accessToken' in tokens) { + console.log('Found tokens:', tokens); +} else { + console.log('No tokens found.'); +} + +// Get tokens, and renew in the background if they are expired or close to expiring +const freshTokens = await oidcClient.token.get({ backgroundRenew: true }); +if (freshTokens && 'accessToken' in freshTokens) { + console.log('Got fresh tokens:', freshTokens); +} else { + console.error('Failed to get or renew tokens:', freshTokens); +} +``` + +#### Revoke Tokens + +Revoke the access token and remove it from storage. + +```typescript +const revokeResult = await oidcClient.token.revoke(); + +if (revokeResult && 'error' in revokeResult) { + console.error('Token revocation failed:', revokeResult); +} else { + console.log('Token revoked successfully.'); +} +``` + +### 5. User Management + +#### Get User Info + +Fetch user information from the `userinfo_endpoint`. + +```typescript +const userInfo = await oidcClient.user.info(); + +if (userInfo && 'sub' in userInfo) { + console.log('User info:', userInfo); +} else { + console.error('Failed to fetch user info:', userInfo); +} +``` + +#### Logout + +Log the user out by revoking tokens and, if configured, ending the session at the provider. + +```typescript +const logoutResult = await oidcClient.user.logout(); + +if (logoutResult && 'error' in logoutResult) { + console.error('Logout failed:', logoutResult); +} else { + console.log('User logged out successfully.'); +} +``` + +## Building + +This library is part of an Nx monorepo. To build it, run: + +```bash +pnpm nx build @forgerock/oidc-client +``` + +## Testing -// Tokens API -const newTokens = await oidcClient.token.exchange({ - /* code, state */ -}); // Returns new tokens or error -const existingTokens = await oidcClient.token.get(); // Returns existing tokens or error -const response = await oidcClient.token.revoke(); // Revokes an access token and returns the response or an error +To run the unit tests for this package, run: -// User API -const user = await oidcClient.user.info(); // Returns user object or error -const logoutResponse = await oidcClient.user.logout(); // Logs the user out and returns the response or an error +```bash +pnpm nx test @forgerock/oidc-client ``` diff --git a/packages/protect/README.md b/packages/protect/README.md index eee07d7aff..2b9c5e4735 100644 --- a/packages/protect/README.md +++ b/packages/protect/README.md @@ -1,237 +1,169 @@ -# Ping Protect +# @forgerock/protect -The Ping Protect module provides an API for interacting with the PingOne Signals (Protect) SDK to perform risk evaluations. It can be used with either a PingOne AIC/PingAM authentication journey with Protect callbacks or with a PingOne DaVinci flow with Protect collectors. +The `@forgerock/protect` package provides a high-level API for interacting with the PingOne Signals (Protect) SDK to perform device profiling and risk evaluation. It is designed to be used within client applications that integrate with PingOne Advanced Identity Cloud (AIC), PingAM, or PingOne DaVinci flows. -**IMPORTANT NOTE**: This module is not yet published. For the current published Ping Protect package please visit https://github.com/ForgeRock/forgerock-javascript-sdk/tree/develop/packages/ping-protect +> [!WARNING] +> This package is under active development and is not yet published to the public NPM registry. It is intended for use within the Ping Identity JavaScript SDK monorepo. -## Full API +## Features -```js -// Protect methods -start(); -getData(); -pauseBehavioralData(); -resumeBehavioralData(); -``` - -## Quickstart with a PingOne AIC or PingAM Authentication Journey - -The Ping Protect module is intended to be used along with the ForgeRock JavaScript SDK to provide the Protect feature. - -### Requirements +- **Simple API**: A straightforward interface (`start`, `getData`) for managing the device profiling lifecycle. +- **Flexible Integration**: Can be used with journey-based authentication flows (PingOne AIC/PingAM) or with PingOne DaVinci flows. +- **Bundled SDK**: Includes the PingOne Signals SDK, simplifying dependency management. +- **Configurable**: Allows for detailed configuration of the Signals SDK, including what data to collect and how. -1. PingOne Advanced Identity Cloud (aka PingOne AIC) platform or an up-to-date Ping Identity Access Management (aka PingAM) -2. PingOne tenant with Protect enabled -3. A Ping Protect Service configured in AIC or AM -4. A journey/tree with the appropriate Protect Nodes -5. A client application with the `@forgerock/javascript-sdk` and `@forgerock/protect` modules installed +## Installation -### Integrate into a Client Application +This package is part of a `pnpm` workspace. To install dependencies, run the following command from the root of the monorepo: -#### Installation - -Install both modules and their latest versions: - -```sh -npm install @forgerock/javascript-sdk @forgerock/protect +```bash +pnpm install ``` -```sh -pnpm install @forgerock/javascript-sdk @forgerock/protect -``` +## Usage with a PingOne AIC or PingAM Journey -#### Initialization (Recommended) +Integrate Ping Protect into an authentication journey that uses callbacks. -The `@forgerock/protect` module has a `protect()` function that accepts configuration options and returns a set of methods for interacting with Protect. The two main responsibilities of the Ping Protect module are the initialization of the profiling and data collection and the completion and preparation of the collected data for the server. You can find these two methods on the API returned by `protect()`. +### 1. Initialization -- `start()` -- `getData()` +The `protect()` function accepts configuration options and returns an API for interacting with the Signals SDK. It's recommended to initialize it early in your application's lifecycle to maximize data collection. -When calling `protect()`, you have many different options to configure what and how the data is collected. The most important and required of these settings is the `envId`. All other settings are optional. - -The `start` method can be called at application startup, or when you receive the `PingOneProtectInitializeCallback` callback from the server. We recommend you call `start` as soon as you can to collect as much data as possible for higher accuracy. - -```js +```typescript import { protect } from '@forgerock/protect'; +import type { ProtectConfig } from '@forgerock/protect/types'; -// Call early in your application startup -const protectApi = protect({ envId: '12345' }); -await protectApi.start(); -``` - -#### Initialization (alternate) - -Alternatively, you can delay the initialization until you receive the instruction from the server by way of the special callback: `PingOneProtectInitializeCallback`. To do this, you would call the `start` method when the callback is present in the journey. - -```js -if (step.getCallbacksOfType('PingOneProtectInitializeCallback')) { - // Asynchronous call - await protectApi.start(); -} -``` - -#### Data collection - -You then call the `FRAuth.next` method after initialization to move the user forward in the journey. - -```js -FRAuth.next(step); -``` - -At some point in the journey, and as late as possible in order to collect as much data as you can, you will come across the `PingOneProtectEvaluationCallback`. This is when you call the `getData` method to package what's been collected for the server to evaluate. - -```js -let data; - -if (step.getCallbacksOfType('PingOneProtectEvaluationCallback')) { - // Asynchronous call - data = await protectApi.getData(); -} -``` - -Now that we have the data, set it on the callback in order to send it to the server when we call `next`. +// Define the Protect configuration +const protectConfig: ProtectConfig = { + envId: 'YOUR_PINGONE_ENVIRONMENT_ID', + // Optional settings: + behavioralDataCollection: true, + deviceAttributesToIgnore: ['userAgent'], +}; -```js -callback.setData(data); +// Initialize the Protect API +const protectApi = protect(protectConfig); -FRAuth.next(step); -``` - -### Error Handling - -The Protect API methods will return an error object if they fail. When you encounter an error during initialization or evaluation, set the error message on the callback using the `setClientError` method. Setting the message on the callback is how it gets sent to the server on the `FRAuth.next` method call. - -```js -if (step.getCallbacksOfType('PingOneProtectInitializeCallback')) { - const callback = step.getCallbackOfType('PingOneProtectInitializeCallback'); - - // Asynchronous call +// Start data collection at application startup +async function startProtect() { const result = await protectApi.start(); - if (result?.error) { - callback.setClientError(result.error); + console.error(`Error initializing Protect: ${result.error}`); } } -``` -A similar process is used for the evaluation step. - -```js -if (step.getCallbacksOfType('PingOneProtectEvaluationCallback')) { - const callback = step.getCallbackOfType('PingOneProtectEvaluationCallback'); - - // Asynchronous call - const result = await protectApi.getData(); - - if (typeof result !== 'string' && 'error' in result) { - callback.setClientError(data.error); - } -} +startProtect(); ``` -## Quickstart with a PingOne DaVinci Flow +### 2. Handling Callbacks in a Journey -The Ping Protect module is intended to be used along with the DaVinci client to provide the Ping Protect feature. +Within your authentication journey, you will encounter two specific callbacks for Ping Protect. -### Requirements +- **`PingOneProtectInitializeCallback`**: An optional callback that can also be used to trigger the `start()` method if you prefer just-in-time initialization. +- **`PingOneProtectEvaluationCallback`**: A required callback that signals when to collect the profiled data and send it to the server. -1. A PingOne environment with PingOne Protect added -2. A worker application configured in your PingOne environment -3. A DaVinci flow with the appropriate Protect connectors -4. A client application with the `@forgerock/davinci-client` and `@forgerock/protect` modules installed +```typescript +import { FRAuth } from '@forgerock/javascript-sdk'; +import { callbackType } from '@forgerock/sdk-types'; +import type { JourneyStep, PingOneProtectEvaluationCallback } from '@forgerock/javascript-sdk'; -### Integrate into a Client Application +// Assuming `step` is the current step from FRAuth.next() -#### Initialization (Recommended) +// Handle the evaluation callback +if (step.getCallbacksOfType(callbackType.PingOneProtectEvaluationCallback).length > 0) { + const callback = step.getCallbackOfType( + callbackType.PingOneProtectEvaluationCallback, + ); -Install both modules and their latest versions: + const signals = await protectApi.getData(); -```sh -npm install @forgerock/davinci-client @forgerock/protect + if (typeof signals === 'string') { + callback.setData(signals); + } else { + // Handle error from getData() + callback.setClientError(signals.error); + } +} + +// Submit the step to continue the journey +const nextStep = await FRAuth.next(step); ``` -The `@forgerock/protect` module has a `protect()` function that accepts configuration options and returns a set of methods for interacting with Protect. The two main responsibilities of the Ping Protect module are the initialization of the profiling and data collection and the completion and preparation of the collected data for the server. You can find these two methods on the API returned by `protect()`. +## Usage with a PingOne DaVinci Flow -- `start()` -- `getData()` +Integrate Ping Protect into a DaVinci flow that uses the `ProtectCollector`. -When calling `protect()`, you have many different options to configure what and how the data is collected. The most important and required of these settings is the `envId`. All other settings are optional. +### 1. Initialization -The `start` method can be called at application startup, or when you receive the `ProtectCollector` from the server. We recommend you call `start` as soon as you can to collect as much data as possible for higher accuracy. +Initialization is the same as the journey-based approach. Call `protect()` with your configuration and invoke `start()` early. -```js +```typescript import { protect } from '@forgerock/protect'; -// Call early in your application startup -const protectApi = protect({ envId: '12345' }); +const protectApi = protect({ envId: 'YOUR_PINGONE_ENVIRONMENT_ID' }); await protectApi.start(); ``` -#### Initialization (alternate) +### 2. Handling the ProtectCollector -Alternatively, you can delay the initialization until you receive the instruction from the server by way of the `ProtectCollector`. To do this, you would call the `start` method when the collector is present in the flow. The Protect collector is returned from the server when it is configured with either a PingOne Forms connector or HTTP connector with Custom HTML Template. +When your DaVinci flow returns a `ProtectCollector`, use it to send the collected signals back to the flow. -```js -const collectors = davinciClient.getCollectors(); -collectors.forEach((collector) => { - if (collector.type === 'ProtectCollector') { - // Optionally use configuration options from the flow to initialize the protect module - const config = collector.output.config; +```typescript +import { davinci } from '@forgerock/davinci-client'; - // Initialize the Protect module and begin collecting data - const protectApi = protect({ - envId: '12345', - behavioralDataCollection: config.behavioralDataCollection, - universalDeviceIdentification: config.universalDeviceIdentification, - }); - await protectApi.start(); - } - ... +const davinciClient = await davinci({ + /* ... config ... */ }); +const { response } = await davinciClient.start(); + +// Check for the ProtectCollector +const protectCollector = response.collectors?.find((c) => c.type === 'ProtectCollector'); + +if (protectCollector) { + // Get the signals data + const signals = await protectApi.getData(); + + if (typeof signals === 'string') { + // Update the collector with the signals data + const updater = davinciClient.update(protectCollector); + updater(signals); + } else { + console.error('Failed to get Protect signals:', signals.error); + // Handle the error appropriately in your UI + } +} + +// Submit the updated collectors to continue the flow +await davinciClient.next(); ``` -#### Data collection +## API Reference -When the user has finished filling out the form and is ready to submit, you can call the `getData` method to package what's been collected. The Protector collector should then be updated with this data to send back to the server to evaluate. +The `protect(options: ProtectConfig)` function returns an object with the following methods: -```js -async function onSubmitHandler() { - try { - const protectCollector = collectors.find((collector) => collector.type === 'ProtectCollector'); +- `start(): Promise` + Initializes the PingOne Signals SDK and begins data collection. It's safe to call multiple times. - // Update the Protect collector with the data collected - if (protectCollector) { - const updater = davinciClient.update(protectCollector); - const data = await protectApi.getData(); - updater(data); - } +- `getData(): Promise` + Stops data collection, gathers the device profile and behavioral data, and returns it as an encrypted string. Returns an error object if the SDK is not initialized. - // Submit all collectors and get the next node in the flow - await davinciClient.next(); - } catch (err) { - // handle error - } -} -``` +- `pauseBehavioralData(): void | { error: string }` + Pauses the collection of behavioral (mouse, keyboard, touch) data. Device profile data collection continues. -### Error Handling +- `resumeBehavioralData(): void | { error: string }` + Resumes the collection of behavioral data. -The Protect API methods will return an error object if they fail. You may use this to return a message to the user or implement your own error handling. +## Building -**Example**: Handling error messages on `start` +This library is part of an Nx monorepo. To build it, run: -```js -const result = await protectApi.start(); -if (result?.error) { - console.error(`Error initializing Protect: ${result.error}`); -} +```bash +pnpm nx build @forgerock/protect ``` -**Example**: Handling error messages on `getData` +## Testing -```js -const result = await protectApi.getData(); -if (typeof result !== 'string' && 'error' in result) { - console.error(`Failed to retrieve data from Protect: ${result.error}`); -} +To run the unit tests for this package, run: + +```bash +pnpm nx test @forgerock/protect ``` diff --git a/packages/sdk-effects/iframe-manager/README.md b/packages/sdk-effects/iframe-manager/README.md index 31e28168f7..d29d63d24e 100644 --- a/packages/sdk-effects/iframe-manager/README.md +++ b/packages/sdk-effects/iframe-manager/README.md @@ -1,8 +1,8 @@ -# IFrame Manager (`@pingidentity/sdk-effects/iframe-manager`) +# @forgerock/iframe-manager ## Overview -The IFrame Manager Effect provides a mechanism to perform operations within a hidden `