diff --git a/.changeset/config.json b/.changeset/config.json index 14c1d2c02..93387a8c3 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -13,7 +13,6 @@ "baseBranch": "master", "updateInternalDependencies": "patch", "ignore": [ - "@forgerock/device-client", "autoscript-apps", "autoscript-suites", "davinci-app", diff --git a/.changeset/little-grapes-end.md b/.changeset/little-grapes-end.md new file mode 100644 index 000000000..b43c3587c --- /dev/null +++ b/.changeset/little-grapes-end.md @@ -0,0 +1,5 @@ +--- +'@forgerock/javascript-sdk': minor +--- + +add-device-client diff --git a/e2e/autoscript-apps/package.json b/e2e/autoscript-apps/package.json index 746907575..6e75a9b48 100644 --- a/e2e/autoscript-apps/package.json +++ b/e2e/autoscript-apps/package.json @@ -5,7 +5,6 @@ "type": "module", "dependencies": { "@forgerock/javascript-sdk": "workspace:*", - "@forgerock/device-client": "workspace:*", "@forgerock/ping-protect": "workspace:*", "rxjs": "^7.4.0" }, diff --git a/e2e/autoscript-apps/src/authn-basic-self-service/autoscript.ts b/e2e/autoscript-apps/src/authn-basic-self-service/autoscript.ts index 06d97773d..2f6d47824 100644 --- a/e2e/autoscript-apps/src/authn-basic-self-service/autoscript.ts +++ b/e2e/autoscript-apps/src/authn-basic-self-service/autoscript.ts @@ -7,9 +7,8 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -// @ts-nocheck import * as forgerock from '@forgerock/javascript-sdk'; -import { deviceClient } from '@forgerock/device-client'; +import { deviceClient } from '@forgerock/javascript-sdk/device-client'; import { delay as rxDelay, map, mergeMap } from 'rxjs/operators'; import { from } from 'rxjs'; @@ -19,9 +18,14 @@ function autoscript() { const url = new URL(window.location.href); const amUrl = url.searchParams.get('amUrl') || 'https://openam-sdks.forgeblocks.com/am'; const realmPath = url.searchParams.get('realmPath') || 'alpha'; - const un = url.searchParams.get('un') || 'demo'; + /** + * Make sure this `un` is a real user + * this is a manual test and requires a real tenant and a real user + * that has devices. + */ + const un = url.searchParams.get('un') || 'demouser'; const platformHeader = url.searchParams.get('platformHeader') === 'true' ? true : false; - const pw = url.searchParams.get('pw') || 'Demo1234!'; + const pw = url.searchParams.get('pw') || '1111'; const tree = url.searchParams.get('tree') || 'selfservice'; console.log('Configure the SDK'); @@ -100,65 +104,27 @@ function autoscript() { try { const user = await forgerock.UserManager.getCurrentUser(); - //const { result: deviceArr } = await client.oath.get({ - // userId: user.sub, - // realm: 'alpha', - //}); - // - //console.log('retrieveOathDevices', deviceArr); - // - //const [{ _id: id, _rev, deviceManagementStatus, ...device }] = deviceArr; + const query = { userId: user.sub, realm: 'alpha' }; - //const oathDeviceDeleted = await client.oath.delete({ userId: user.sub, id, ...device }); + const boundArr = await client.bound.get(query); + console.log('BOUND GET', boundArr); + if (Array.isArray(boundArr)) { + const [bound] = boundArr; + console.log('updated bound', bound); + const updatedBound = await client.bound.update({ + ...query, + device: { ...bound, deviceName: 'BoundDeviceRyan' }, + }); + console.log('updated', updatedBound); - //console.log(oathDeviceDeleted); + if ('error' in updatedBound) return; - //const { result: pushDevices } = await client.push.get({ - // userId: user.sub, - // realm: 'alpha', - //}); - // - const bindingDevices = await client.boundDevices.get({ - userId: user.sub, - realm: 'alpha', - }); - //console.log('bindingDevices', bindingDevices); - // - //const webauthnDevices = await client.webauthn.get({ - // userId: user.sub, - // realm: 'alpha', - //}); - //console.log('webauthn devices', webauthnDevices); - //const { - // _id: userId, - // _rev: ignoreThis, - // deviceManagementStatus: ignoreDeviceManagementStatus, - // ...rest - //} = webauthnDevices.result[0]; - // - //const updatedDevice = await client.webauthn.update({ - // userId: user.sub, - // realm: 'alpha', - // ...rest, - // deviceName: 'RyansDeviceUpdated!!', - //}); - //console.log('updatedDevice', updatedDevice); - // - const bindingDeviceNameUpdated = await client.boundDevices.update({ - userId: user.sub, - realm: 'alpha', - ...bindingDevices.result[0], - deviceName: 'RyanDevice', - }); - - console.log('bindingDeviceNameUpdated', bindingDeviceNameUpdated); - - const removedDevice = await client.boundDevices.delete({ - realm: 'alpha', - userId: user.sub, - ...bindingDeviceNameUpdated, - }); - //console.log('removeDevice', removedDevice); + const deletedBound = await client.bound.delete({ + ...query, + device: updatedBound, + }); + console.log(updatedBound); + } } catch (err) { console.log('failed', err); } diff --git a/e2e/autoscript-apps/tsconfig.json b/e2e/autoscript-apps/tsconfig.json index 2e6faed4e..722ac38f9 100644 --- a/e2e/autoscript-apps/tsconfig.json +++ b/e2e/autoscript-apps/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "../../tsconfig.base.json", "files": [], - "include": [], "compilerOptions": { "forceConsistentCasingInFileNames": true, "strict": false, diff --git a/packages/device-client/.eslintignore b/packages/device-client/.eslintignore deleted file mode 100644 index 288d66ab2..000000000 --- a/packages/device-client/.eslintignore +++ /dev/null @@ -1,11 +0,0 @@ -node_modules -*.md -LICENSE -.babelrc -.env* -.bin -dist -.eslintignore -docs -coverage -vite.config.*.timestamp* diff --git a/packages/device-client/.eslintrc.json b/packages/device-client/.eslintrc.json deleted file mode 100644 index 3fc5707da..000000000 --- a/packages/device-client/.eslintrc.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.json"], - "parser": "jsonc-eslint-parser", - "rules": { - "@nx/dependency-checks": [ - "error", - { - "ignoredFiles": [ - "{projectRoot}/eslint.config.{js,cjs,mjs}", - "{projectRoot}/vite.config.{js,ts,mjs,mts}" - ] - } - ] - } - } - ] -} diff --git a/packages/device-client/README.md b/packages/device-client/README.md deleted file mode 100644 index ff60e5c6c..000000000 --- a/packages/device-client/README.md +++ /dev/null @@ -1,247 +0,0 @@ -# Device Client API - -The `deviceClient` API provides a structured interface for managing various types of devices, including Oath devices, Push devices, WebAuthn devices, and bound devices. This API leverages Redux Toolkit Query (RTK Query) for efficient data fetching and state management. - -## Table of Contents - -1. [Overview](#overview) -2. [Installation](#installation) -3. [Configuration](#configuration) -4. [API Methods](#api-methods) - - [Oath Management](#oath-management) - - [Push Management](#push-management) - - [WebAuthn Management](#webauthn-management) - - [Bound Devices Management](#bound-devices-management) -5. [Example Usage](#example-usage) -6. [Error Handling](#error-handling) -7. [Authentication](#authentication) -8. [Best Practices](#best-practices) -9. [License](#license) - -## 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. - -## Installation - -To install the necessary dependencies for using the `deviceClient`, run: - -```bash -npm install @reduxjs/toolkit @forgerock/device-client --save -``` - -## 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 './path/to/deviceClient'; -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. - -const apiClient = deviceClient(config); -``` - -## API Methods - -### Oath Management - -#### Methods - -- **get(query: RetrieveOathQuery): Promise** -- Retrieves Oath devices based on the specified query. - -- **delete(query: DeleteOathQuery & OathDevice): Promise** -- Deletes an Oath device based on the provided query and device information. - -### Push Management - -#### Methods - -- **get(query: PushDeviceQuery): Promise** -- Retrieves Push devices based on the specified query. - -- **delete(query: DeleteDeviceQuery): Promise** -- Deletes a Push device based on the provided query. - -### WebAuthn Management - -#### Methods - -- **get(query: WebAuthnQuery): Promise** -- Retrieves WebAuthn devices based on the specified query. - -- **update(query: WebAuthnQueryWithUUID & WebAuthnBody): Promise** -- Updates the name of a WebAuthn device based on the provided query and body. - -- **delete(query: WebAuthnQueryWithUUID & WebAuthnBody): Promise** -- Deletes a WebAuthn device based on the provided query and body. - -### Bound Devices Management - -#### Methods - -- **get(query: BindingDeviceQuery): Promise** -- Retrieves bound devices based on the specified query. - -- **delete(query: BindingDeviceQuery): Promise** -- Deletes a bound device based on the provided query. - -- **update(query: BindingDeviceQuery): Promise** -- Updates the name of a bound device based on the provided query. - -## Example Usage - -### Oath Management Example - -```typescript -const oathQuery: RetrieveOathQuery = { - /* your query parameters */ -}; - -apiClient.oath - .get(oathQuery) - .then((response) => { - console.log('Oath Devices:', response); - }) - .catch((error) => { - console.error('Error fetching Oath devices:', error); - }); - -const deleteOathQuery: DeleteOathQuery & OathDevice = { - /* your delete query */ -}; - -apiClient.oath - .delete(deleteOathQuery) - .then((response) => { - console.log('Deleted Oath Device:', response); - }) - .catch((error) => { - console.error('Error deleting Oath device:', error); - }); -``` - -### Push Management Example - -```typescript -const pushQuery: PushDeviceQuery = { - /* your query parameters */ -}; - -apiClient.push - .get(pushQuery) - .then((response) => { - console.log('Push Devices:', response); - }) - .catch((error) => { - console.error('Error fetching Push devices:', error); - }); - -const deletePushQuery: DeleteDeviceQuery = { - /* your delete query */ -}; - -apiClient.push - .delete(deletePushQuery) - .then((response) => { - console.log('Deleted Push Device:', response); - }) - .catch((error) => { - console.error('Error deleting Push device:', error); - }); -``` - -### WebAuthn Management Example - -```typescript -const webAuthnQuery: WebAuthnQuery = { - /* your query parameters */ -}; - -apiClient.webauthn - .get(webAuthnQuery) - .then((response) => { - console.log('WebAuthn Devices:', response); - }) - .catch((error) => { - console.error('Error fetching WebAuthn devices:', error); - }); - -const updateWebAuthnQuery: WebAuthnQueryWithUUID & WebAuthnBody = { - /* your update query */ -}; - -apiClient.webauthn - .update(updateWebAuthnQuery) - .then((response) => { - console.log('Updated WebAuthn Device:', response); - }) - .catch((error) => { - console.error('Error updating WebAuthn device:', error); - }); - -const deleteWebAuthnQuery: WebAuthnQueryWithUUID & WebAuthnBody = { - /* your delete query */ -}; - -apiClient.webauthn - .delete(deleteWebAuthnQuery) - .then((response) => { - console.log('Deleted WebAuthn Device:', response); - }) - .catch((error) => { - console.error('Error deleting WebAuthn device:', error); - }); -``` - -### Bound Devices Management Example - -```typescript const bindingQuery: BindingDeviceQuery = { /* your query parameters */ }; -apiClient.boundDevices - .get(bindingQuery) - .then((response) => { - console.log('Bound Devices:', response); - }) - .catch((error) => { - console.error('Error fetching bound devices:', error); - }); - -const deleteBindingQuery: BindingDeviceQuery = { - /* your delete query */ -}; - -apiClient.boundDevices - .delete(deleteBindingQuery) - .then((response) => { - console.log('Deleted Bound Device:', response); - }) - .catch((error) => { - console.error('Error deleting bound device:', error); - }); - -const updateBindingQuery: BindingDeviceQuery = { - /* your update query */ -}; - -apiClient.boundDevices - .update(updateBindingQuery) - .then((response) => { - console.log('Updated Bound Device:', response); - }) - .catch((error) => { - console.error('Error updating bound device:', error); - }); -``` - -## License - -This project is licensed under the MIT License. See the LICENSE file for details. diff --git a/packages/device-client/package.json b/packages/device-client/package.json deleted file mode 100644 index 312567fc0..000000000 --- a/packages/device-client/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@forgerock/device-client", - "version": "0.0.1", - "private": true, - "main": "./dist/index.cjs", - "module": "./dist/index.js", - "typings": "./dist/index.d.ts", - "sideEffects": false, - "type": "module", - "exports": { - ".": "./dist/index.js", - "./package.json": "./package.json", - "./types": "./dist/lib/types/index.d.ts" - }, - "files": ["./dist"], - "dependencies": { - "@reduxjs/toolkit": "2.3.0", - "@forgerock/javascript-sdk": "4.6.0" - }, - "devDependencies": { - "msw": "^2.5.1", - "vitest": "^1.4.0" - } -} diff --git a/packages/device-client/project.json b/packages/device-client/project.json deleted file mode 100644 index f6337495b..000000000 --- a/packages/device-client/project.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "device-client", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/self-service/src", - "projectType": "library", - "tags": ["scope:package"], - "targets": { - "docs": { - "command": "pnpm typedoc --options {projectRoot}/typedoc.json" - }, - "build": { - "outputs": ["{projectRoot}/{options.outDir}", "{projectRoot}/dist"], - "options": { - "assets": ["packages/self-service/*.md"] - } - }, - "lint": { - "options": { - "fix": true, - "ignore-path": ".eslintignore", - "args": ["**/*.ts"] - } - }, - "test": { - "inputs": [ - "default", - "^default", - { - "externalDependencies": ["vitest"] - } - ], - "options": { - "testPathPattern": ["packages/javascript-sdk/src"] - }, - "configurations": { - "watch": { - "watch": true, - "mode": "ui" - } - } - } - } -} diff --git a/packages/device-client/src/index.ts b/packages/device-client/src/index.ts deleted file mode 100644 index 9b9a9508e..000000000 --- a/packages/device-client/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/device.store.js'; diff --git a/packages/device-client/src/lib/device.store.ts b/packages/device-client/src/lib/device.store.ts deleted file mode 100644 index 107074d96..000000000 --- a/packages/device-client/src/lib/device.store.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { type ConfigOptions } from '@forgerock/javascript-sdk'; -import { configureStore } from '@reduxjs/toolkit'; -import { deviceService } from './services/index.js'; -import { DeleteOathQuery, OathDevice, RetrieveOathQuery } from './types/oath.types.js'; -import { DeleteDeviceQuery, PushDeviceQuery } from './types/push-device.types.js'; -import { WebAuthnBody, WebAuthnQuery, WebAuthnQueryWithUUID } from './types/webauthn.types.js'; -import { BindingDeviceQuery } from './types/binding-device.types.js'; - -export const deviceClient = (config: ConfigOptions) => { - const { middleware, reducerPath, reducer, endpoints } = deviceService({ - baseUrl: config.serverConfig?.baseUrl ?? '', - realmPath: config?.realmPath ?? '', - }); - - const store = configureStore({ - reducer: { - [reducerPath]: reducer, - }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware), - }); - - /** - * Device management object containing methods for handling various device types. - * - * @type {DeviceManagement} - */ - return { - /** - * Oath device management methods. - * - * @type {OathManagement} - */ - oath: { - /** - * Retrieves Oath devices based on the specified query. - * - * @async - * @function get - * @param {RetrieveOathQuery} query - The query used to retrieve Oath devices. - * @returns {Promise} - A promise that resolves to the retrieved data or undefined if the response is not valid. - */ - get: async function (query: RetrieveOathQuery) { - const response = await store.dispatch(endpoints.getOAthDevices.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - - /** - * Deletes an Oath device based on the provided query and device information. - * - * @async - * @function delete - * @param {DeleteOathQuery & OathDevice} query - The query and device information used to delete the Oath device. - * @returns {Promise} - A promise that resolves to the response data or undefined if the response is not valid. - */ - delete: async function (query: DeleteOathQuery & OathDevice) { - const response = await store.dispatch(endpoints.deleteOathDevice.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - }, - - /** - * Push device management methods. - * - * @type {PushManagement} - */ - push: { - /** - * Retrieves Push devices based on the specified query. - * - * @async - * @function get - * @param {PushDeviceQuery} query - The query used to retrieve Push devices. - * @returns {Promise} - A promise that resolves to the retrieved data or undefined if the response is not valid. - */ - get: async function (query: PushDeviceQuery) { - const response = await store.dispatch(endpoints.getPushDevices.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - - /** - * Deletes a Push device based on the provided query. - * - * @async - * @function delete - * @param {DeleteDeviceQuery} query - The query used to delete the Push device. - * @returns {Promise} - A promise that resolves to the response data or undefined if the response is not valid. - */ - delete: async function (query: DeleteDeviceQuery) { - const response = await store.dispatch(endpoints.deletePushDevice.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - }, - - /** - * WebAuthn device management methods. - * - * @type {WebAuthnManagement} - */ - webauthn: { - /** - * Retrieves WebAuthn devices based on the specified query. - * - * @async - * @function get - * @param {WebAuthnQuery} query - The query used to retrieve WebAuthn devices. - * @returns {Promise} - A promise that resolves to the retrieved data or undefined if the response is not valid. - */ - get: async function (query: WebAuthnQuery) { - const response = await store.dispatch(endpoints.getWebAuthnDevices.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - - /** - * Updates the name of a WebAuthn device based on the provided query and body. - * - * @async - * @function update - * @param {WebAuthnQueryWithUUID & WebAuthnBody} query - The query and body used to update the WebAuthn device name. - * @returns {Promise} - A promise that resolves to the response data or undefined if the response is not valid. - */ - update: async function (query: WebAuthnQueryWithUUID & WebAuthnBody) { - const response = await store.dispatch(endpoints.updateWebAuthnDeviceName.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - - /** - * Deletes a WebAuthn device based on the provided query and body. - * - * @async - * @function delete - * @param {WebAuthnQueryWithUUID & WebAuthnBody} query - The query and body used to delete the WebAuthn device. - * @returns {Promise} - A promise that resolves to the response data or undefined if the response is not valid. - */ - delete: async function (query: WebAuthnQueryWithUUID & WebAuthnBody) { - const response = await store.dispatch(endpoints.deleteWebAuthnDeviceName.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - }, - - /** - * Bound devices management methods. - * - * @type {BoundDevicesManagement} - */ - boundDevices: { - /** - * Retrieves bound devices based on the specified query. - * - * @async - * @function get - * @param {BindingDeviceQuery} query - The query used to retrieve bound devices. - * @returns {Promise} - A promise that resolves to the retrieved data or undefined if the response is not valid. - */ - get: async function (query: BindingDeviceQuery) { - const response = await store.dispatch(endpoints.getBoundDevices.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - - /** - * Deletes a bound device based on the provided query. - * - * @async - * @function delete - * @param {BindingDeviceQuery} query - The query used to delete the bound device. - * @returns {Promise} - A promise that resolves to the response data or undefined if the response is not valid. - */ - delete: async function (query: BindingDeviceQuery) { - const response = await store.dispatch(endpoints.deleteBindingDevice.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - - /** - * Updates the name of a bound device based on the provided query. - * - * @async - * @function update - * @param {BindingDeviceQuery} query - The query used to update the bound device name. - * @returns {Promise} - A promise that resolves to the response data or undefined if the response is not valid. - */ - update: async function (query: BindingDeviceQuery) { - const response = await store.dispatch(endpoints.updateBindingDeviceName.initiate(query)); - - if (!response || !response.data) { - return undefined; - } - - return response.data; - }, - }, - }; -}; diff --git a/packages/device-client/src/lib/services/index.ts b/packages/device-client/src/lib/services/index.ts deleted file mode 100644 index 6a90906b0..000000000 --- a/packages/device-client/src/lib/services/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -import { - DeletedOAthDevice, - DeleteOathQuery, - OathDevice, - OAthResponse, - RetrieveOathQuery, -} from '../types/oath.types.js'; -import { - DeleteDeviceQuery, - PushDevice, - PushDeviceQuery, - PushDevicesResponse, -} from '../types/push-device.types.js'; -import { BindingDeviceQuery, Device, DeviceResponse } from '../types/binding-device.types.js'; - -import { - UpdatedWebAuthnDevice, - WebAuthnBody, - WebAuthnDevice, - WebAuthnDevicesResponse, - WebAuthnQuery, - WebAuthnQueryWithUUID, -} from '../types/webauthn.types.js'; - -export const deviceService = ({ baseUrl, realmPath }: { baseUrl: string; realmPath: string }) => - createApi({ - reducerPath: 'deviceClient', - baseQuery: fetchBaseQuery({ - credentials: 'include', - prepareHeaders: (headers) => { - headers.set('Content-Type', 'application/json'); - headers.set('Accept', 'application/json'); - headers.set('x-requested-with', 'forgerock-sdk'); - headers.set('x-requested-platform', 'javascript'); - return headers; - }, - baseUrl, - }), - endpoints: (builder) => ({ - // oath endpoints - getOAthDevices: builder.query({ - query: ({ realm = realmPath, userId }) => - `json/realms/${realm}/users/${userId}/devices/2fa/oath?_queryFilter=true`, - }), - - deleteOathDevice: builder.mutation({ - query: ({ realm = realmPath, userId, uuid, ...body }) => ({ - method: 'DELETE', - url: `json/realms/${realm}/users/${userId}/devices/2fa/oath/${uuid}`, - body: { uuid, ...body }, - }), - }), - - // push device - getPushDevices: builder.query({ - query: ({ realm = realmPath, userId }) => - `/json/realms/${realm}/users/${userId}/devices/2fa/push?_queryFilter=true`, - }), - - deletePushDevice: builder.mutation({ - query: ({ realm = realmPath, userId, uuid }) => ({ - url: `/json/realms/${realm}/users/${userId}/devices/2fa/push/${uuid}`, - method: 'DELETE', - body: {}, - }), - }), - - // webauthn devices - getWebAuthnDevices: builder.query({ - query: ({ realm = realmPath, userId }) => - `/json/realms/${realm}/users/${userId}/devices/2fa/webauthn?_queryFilter=true`, - }), - updateWebAuthnDeviceName: builder.mutation< - UpdatedWebAuthnDevice, - WebAuthnQueryWithUUID & WebAuthnBody - >({ - query: ({ realm = realmPath, userId, ...device }) => ({ - url: `/json/realms/${realm}/users/${userId}/devices/2fa/webauthn/${device.uuid}`, - method: 'PUT', - body: device satisfies WebAuthnBody, - }), - }), - deleteWebAuthnDeviceName: builder.mutation< - WebAuthnDevice, - WebAuthnQueryWithUUID & WebAuthnBody - >({ - query: ({ realm = realmPath, userId, ...device }) => ({ - url: `/json/realms/${realm}/users/${userId}/devices/2fa/webauthn/${device.uuid}`, - method: 'DELETE', - body: device satisfies WebAuthnBody, - }), - }), - getBoundDevices: builder.mutation({ - query: ({ realm = realmPath, userId }) => - `/json/realms/${realm}/users/${userId}/devices/2fa/binding?_queryFilter=true`, - }), - updateBindingDeviceName: builder.mutation({ - query: ({ realm = realmPath, userId, ...device }) => ({ - url: `/json/realms/root/realms/${realm}/users/${userId}/devices/2fa/binding/${device.uuid}`, - method: 'PUT', - body: device satisfies Device, - }), - }), - deleteBindingDevice: builder.mutation({ - query: ({ realm = realmPath, userId, ...device }) => ({ - url: `/json/realms/root/realms/${realm}/users/${userId}/devices/2fa/binding/${device.uuid}`, - method: 'DELETE', - body: device satisfies Device, - }), - }), - }), - }); diff --git a/packages/device-client/tsconfig.json b/packages/device-client/tsconfig.json deleted file mode 100644 index 0e799d2ad..000000000 --- a/packages/device-client/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "ES2020", - "moduleResolution": "Bundler", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/device-client/tsconfig.lib.json b/packages/device-client/tsconfig.lib.json deleted file mode 100644 index 410d0b374..000000000 --- a/packages/device-client/tsconfig.lib.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "declaration": true - }, - "include": ["src/**/*.ts"], - "exclude": [ - "vite.config.ts", - "src/**/*.spec.ts", - "src/**/*.test.ts", - "src/lib/mock-data/*" - ] -} diff --git a/packages/device-client/tsconfig.spec.json b/packages/device-client/tsconfig.spec.json deleted file mode 100644 index 55d312d19..000000000 --- a/packages/device-client/tsconfig.spec.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "types": [ - "vitest/vitest", - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node" - ] - }, - "include": [ - "vite.config.ts", - "vitest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ] -} diff --git a/packages/device-client/typedoc.json b/packages/device-client/typedoc.json deleted file mode 100644 index 1b03966f3..000000000 --- a/packages/device-client/typedoc.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "entryPointStrategy": "expand", - "entryPoints": ["./src/"], - "tsconfig": "tsconfig.lib.json", - "compilerOptions": {}, - "exclude": [ - "**/*.(spec|test|e2e).ts", - "**/*.mock.*", - "**/*.data.*", - "docs/**", - "tests/**", - "specs/**", - "spec/**", - "test/**" - ], - "externalPattern": ["**/node_modules/**"], - "excludeExternals": true, - "excludeInternal": false, - "excludePrivate": false, - "excludeProtected": false, - "excludeNotDocumented": false, - "externalSymbolLinkMappings": {}, - "out": "./docs", - "emit": "docs", - "theme": "typedoc-github-theme", - "name": "device-client", - "includeVersion": true, - "readme": "./README.md", - "disableSources": false, - "excludeTags": [], - "cname": "", - "sourceLinkTemplate": "", - "gitRevision": "master", - "gitRemote": "origin", - "lang": "en", - "githubPages": true, - "hideGenerator": true, - "searchInComments": false, - "cleanOutputDir": true, - "titleLink": "", - "navigationLinks": {}, - "sidebarLinks": {}, - "commentStyle": "all", - "categorizeByGroup": true, - "defaultCategory": "Other", - "categoryOrder": [], - "sort": ["visibility", "required-first", "source-order"], - "visibilityFilters": { - "protected": true, - "private": true, - "inherited": true, - "external": true - }, - "searchCategoryBoosts": {}, - "searchGroupBoosts": {}, - "preserveWatchOutput": false, - "skipErrorChecking": false, - "validation": { - "notExported": true, - "invalidLink": true, - "notDocumented": true - }, - "requiredToBeDocumented": [], - "treatWarningsAsErrors": false, - "intentionallyNotExported": [], - "logLevel": "Verbose", - "plugin": ["typedoc-plugin-rename-defaults", "typedoc-github-theme"] -} diff --git a/packages/device-client/vite.config.ts b/packages/device-client/vite.config.ts deleted file mode 100644 index 584d1a3ef..000000000 --- a/packages/device-client/vite.config.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { defineConfig } from 'vite'; -import dts from 'vite-plugin-dts'; - -export default defineConfig(() => ({ - cacheDir: '../../node_modules/.vite/ping-protect', - build: { - outDir: './dist', - lib: { - entry: 'src/index.ts', - name: 'self-service', - formats: ['es'], - fileName: (extension, filename) => `${filename}.js`, - }, - rollupOptions: { - external: [/node_modules/, '@forgerock/javascript-sdk'], - output: { - dir: './dist', - preserveModules: true, - preserveModulesRoot: 'src', - }, - }, - }, - plugins: [ - dts({ - declarationOnly: false, - rollupTypes: false, - entryRoot: 'src', - tsconfigPath: './tsconfig.lib.json', - }), - ], - test: { - reporters: ['default'], - globals: true, - setupFiles: ['./vitest.setup.ts'], - passWithNoTests: true, - watch: !process.env['CI'], - coverage: { - enabled: Boolean(process.env['CI']), - reporter: ['text', 'json', 'html'], - reportsDirectory: './coverage', - provider: 'v8', - }, - - deps: { - optimizer: { - web: { - include: ['vitest-canvas-mock'], - }, - }, - }, - environment: 'jsdom', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - }, -})); diff --git a/packages/javascript-sdk/package.json b/packages/javascript-sdk/package.json index ad8ece6c5..ca32b2aed 100644 --- a/packages/javascript-sdk/package.json +++ b/packages/javascript-sdk/package.json @@ -29,6 +29,14 @@ "default": "./dist/index.cjs" } }, + "./device-client": { + "types": "./dist/device-client/device.store.d.ts", + "default": "./dist/device-client/device.store.js" + }, + "./device-client/types": { + "types": "./dist/device-client/types/index.d.ts", + "default": "./dist/device-client/types/index.d.ts" + }, "./src/*": { "import": { "types": "./dist/*.d.ts", @@ -50,5 +58,14 @@ } } }, - "type": "module" + "type": "module", + "dependencies": { + "@reduxjs/toolkit": "catalog:", + "immer": "catalog:" + }, + "devDependencies": { + "@testing-library/react": "16.1.0", + "msw": "2.7.0", + "vitest": "^1.4.0" + } } diff --git a/packages/javascript-sdk/src/device-client/device.store.test.ts b/packages/javascript-sdk/src/device-client/device.store.test.ts new file mode 100644 index 000000000..20cb1dd40 --- /dev/null +++ b/packages/javascript-sdk/src/device-client/device.store.test.ts @@ -0,0 +1,429 @@ +import { afterEach, afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { deviceClient } from './device.store'; + +import { + MOCK_PUSH_DEVICES, + MOCK_BINDING_DEVICES, + MOCK_OATH_DEVICES, + MOCK_DELETED_OATH_DEVICE, + MOCK_WEBAUTHN_DEVICES, + MOCK_DEVICE_PROFILE_SUCCESS, +} from './mock-data/msw-mock-data'; + +// Create handlers +export const handlers = [ + // OATH Devices + http.get('*/json/realms/:realm/users/:userId/devices/2fa/oath', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json(MOCK_OATH_DEVICES); + }), + + http.delete('*/json/realms/:realm/users/:userId/devices/2fa/oath/:uuid', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json(MOCK_DELETED_OATH_DEVICE); + }), + + // Push Devices + http.get('*/json/realms/:realm/users/:userId/devices/2fa/push', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ result: MOCK_PUSH_DEVICES }); + }), + + http.delete('*/json/realms/:realm/users/:userId/devices/2fa/push/:uuid', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + if (params['uuid'] === 'bad-uuid') { + return HttpResponse.json({ error: 'bad uuid' }, { status: 401 }); + } + return HttpResponse.json(MOCK_PUSH_DEVICES[0]); + }), + + // WebAuthn Devices + http.get('*/json/realms/:realm/users/:userId/devices/2fa/webauthn', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ result: MOCK_WEBAUTHN_DEVICES }); + }), + + http.put('*/json/realms/:realm/users/:userId/devices/2fa/webauthn/:uuid', ({ params }) => { + if (params['userId'] === 'bad-uuid') { + return HttpResponse.json({ error: 'bad uuid' }, { status: 401 }); + } + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ + ...MOCK_WEBAUTHN_DEVICES.result[0], + deviceName: 'Updated WebAuthn Device', + }); + }), + + http.delete('*/json/realms/:realm/users/:userId/devices/2fa/webauthn/:uuid', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + if (params['uuid'] === 'bad-uuid') { + return HttpResponse.json({ error: 'bad-uuid' }, { status: 401 }); + } + return HttpResponse.json(MOCK_WEBAUTHN_DEVICES.result[0]); + }), + + // Binding Devices + http.get('*/json/realms/:realm/users/:userId/devices/2fa/binding', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ result: MOCK_BINDING_DEVICES }); + }), + + http.put( + '*/json/realms/root/realms/:realm/users/:userId/devices/2fa/binding/:uuid', + ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + if (params['userId'] === 'bad-uuid') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ + ...MOCK_BINDING_DEVICES.result[0], + deviceName: 'Updated Binding Device', + }); + }, + ), + + http.delete( + '*/json/realms/root/realms/:realm/users/:userId/devices/2fa/binding/:uuid', + ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + if (params['userId'] === 'bad-uuid') { + return HttpResponse.json({ error: 'bad uuid' }, { status: 401 }); + } + return HttpResponse.json({ result: MOCK_BINDING_DEVICES.result[0] }); + }, + ), + + // profile devices + http.get('*/json/realms/:realm/users/:userId/devices/profile', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ result: MOCK_DEVICE_PROFILE_SUCCESS }); + }), + http.put('*/json/realms/:realm/users/:userId/devices/profile/:uuid', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + return HttpResponse.json({ + ...MOCK_DEVICE_PROFILE_SUCCESS.result[0], + alias: 'new-name', + }); + }), + http.delete('*/json/realms/:realm/users/:userId/devices/profile/:uuid', ({ params }) => { + if (params['realm'] === 'fake-realm') { + return HttpResponse.json({ error: 'bad realm' }, { status: 401 }); + } + if (params['userId'] === 'bad-user') { + return HttpResponse.json({ error: 'bad user' }, { status: 401 }); + } + if (params['userId'] === 'bad-uuid') { + return HttpResponse.json({ error: 'bad uuid' }, { status: 401 }); + } + return HttpResponse.json(MOCK_DEVICE_PROFILE_SUCCESS.result[0]); + }), +]; + +export const server = setupServer(...handlers); + +// Establish API mocking before all tests. +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); + +// Reset any request handlers that we may add during the tests, +// so they don't affect other tests. +afterEach(() => server.resetHandlers()); + +// Clean up after the tests are finished. +afterAll(() => server.close()); + +describe('Device Client Store', () => { + const config = { + serverConfig: { + baseUrl: 'https://api.example.com', + }, + realmPath: 'test-realm', + }; + + describe('OATH Device Management', () => { + const client = deviceClient(config); + + it('should fetch OATH devices', async () => { + const result = await client.oath.get({ + userId: 'test-user', + }); + + expect(result).toEqual(MOCK_OATH_DEVICES.result); + }); + + it('should delete OATH device', async () => { + const result = await client.oath.delete({ + userId: 'test-user', + device: { + deviceManagementStatus: false, + _rev: '1221312', + uuid: 'oath-uuid-1', + deviceName: 'Test OATH Device', + _id: 'test-id', + createdDate: 1705555555555, + lastAccessDate: 1705555555555, + }, + }); + + expect(result).toEqual(MOCK_DELETED_OATH_DEVICE); + }); + it('should return error obj if a user does not exist', async () => { + const badClient = deviceClient(config); + const result = await badClient.oath.get({ + userId: 'bad-user', + }); + expect(result).toStrictEqual({ error: new Error('response did not contain data') }); + }); + it('should return undefined if a realm does not exist', async () => { + const badConfig = { ...config, realmPath: 'fake-realm' }; + const badClient = deviceClient(badConfig); + const result = await badClient.oath.get({ + userId: 'test-user', + }); + expect(result).toStrictEqual({ error: new Error('response did not contain data') }); + }); + }); + + describe('Push Device Management', () => { + const client = deviceClient(config); + + it('should fetch push devices', async () => { + const result = await client.push.get({ + userId: 'test-user', + }); + + expect(result).toEqual(MOCK_PUSH_DEVICES); + }); + + it('should delete push device', async () => { + const result = await client.push.delete({ + userId: 'test-user', + device: MOCK_PUSH_DEVICES[0], + }); + expect(result).toEqual(MOCK_PUSH_DEVICES[0]); + }); + it('should fail with a bad uuid', async () => { + const client = deviceClient(config); + const result1 = await client.push.delete({ + userId: 'test-user', + device: { ...MOCK_PUSH_DEVICES[0], uuid: 'bad-uuid' }, + }); + + expect(result1).toStrictEqual({ error: new Error('response did not contain data') }); + }); + it('should fail with a bad userId', async () => { + const badConfig = { ...config, realmPath: 'bad-realm' }; + const badClient = deviceClient(badConfig); + const result1 = await badClient.push.delete({ + userId: 'bad-user', + device: MOCK_PUSH_DEVICES[0], + }); + const result2 = await badClient.push.get({ userId: 'bad-user' }); + + expect(result1).toStrictEqual({ error: new Error('response did not contain data') }); + expect(result2).toStrictEqual({ error: new Error('response did not contain data') }); + }); + it('should return error if a uuid does not exist', async () => { + const badClient = deviceClient(config); + const result = await badClient.push.delete({ + userId: 'user', + device: { ...MOCK_PUSH_DEVICES[0], uuid: 'bad-uuid' }, + }); + expect(result).toStrictEqual({ error: new Error('response did not contain data') }); + }); + it('should return undefined if a user does not exist', async () => { + const badClient = deviceClient(config); + const result = await badClient.push.get({ + userId: 'bad-user', + }); + expect(result).toStrictEqual({ error: new Error('response did not contain data') }); + }); + it('should return undefined if a realm does not exist', async () => { + const badConfig = { ...config, realmPath: 'fake-realm' }; + const badClient = deviceClient(badConfig); + const result = await badClient.push.get({ + userId: 'test-user', + }); + expect(result).toStrictEqual({ error: new Error('response did not contain data') }); + }); + }); + // + describe('WebAuthn Device Management', () => { + const client = deviceClient(config); + + it('should fetch webauthn devices', async () => { + const result = await client.webAuthn.get({ + userId: 'test-user', + }); + + expect(result).toEqual(MOCK_WEBAUTHN_DEVICES); + }); + + it('should update webauthn device name', async () => { + const mockDevice = MOCK_WEBAUTHN_DEVICES.result[0]; + const result = await client.webAuthn.update({ + userId: 'test-user', + device: { + _id: mockDevice._id, + _rev: mockDevice._rev, + uuid: mockDevice.uuid, + deviceName: 'Updated WebAuthn Device', + credentialId: mockDevice.credentialId, + createdDate: mockDevice.createdDate, + lastAccessDate: mockDevice.lastAccessDate, + deviceManagementStatus: mockDevice.deviceManagementStatus, + }, + }); + + expect(result).toEqual({ + ...mockDevice, + deviceName: 'Updated WebAuthn Device', + }); + }); + it('should error when deleting webauthn device with invalid uuid', async () => { + const mockDevice = MOCK_WEBAUTHN_DEVICES.result[0]; + const result = await client.webAuthn.delete({ + userId: 'test-user', + device: { + ...mockDevice, + uuid: 'bad-uuid', + }, + }); + + expect(result).toStrictEqual({ error: new Error('response did not contain data') }); + }); + + it('should delete webauthn device', async () => { + const mockDevice = MOCK_WEBAUTHN_DEVICES.result[0]; + const result = await client.webAuthn.delete({ + userId: 'test-user', + device: mockDevice, + }); + + expect(result).toEqual(mockDevice); + }); + }); + // + describe('Bound Device Management', () => { + const client = deviceClient(config); + const mockDevice = MOCK_BINDING_DEVICES.result[0]; + + it('should fetch bound devices', async () => { + const result = await client.bound.get({ + userId: 'test-user', + ...mockDevice, + }); + + expect(result).toEqual(MOCK_BINDING_DEVICES); + }); + + it('should update bound device name', async () => { + const result = await client.bound.update({ + userId: 'test-user', + device: mockDevice, + }); + + expect(result).toEqual({ + ...mockDevice, + deviceName: 'Updated Binding Device', + }); + }); + + it('should delete bound device', async () => { + const result = await client.bound.delete({ + userId: 'test-user', + device: mockDevice, + }); + + expect(result).toEqual(mockDevice); + }); + }); + describe('Profile Device', () => { + const client = deviceClient(config); + + it('should fetch device profiles', async () => { + const result = await client.profile.get({ userId: 'test-user', realm: 'test-realm' }); + + expect(result).toEqual(MOCK_DEVICE_PROFILE_SUCCESS); + }); + it('should update device profiles', async () => { + const result = await client.profile.update({ + userId: 'test-user', + realm: 'test-realm', + device: MOCK_DEVICE_PROFILE_SUCCESS.result[0], + }); + + expect(result).toEqual({ ...MOCK_DEVICE_PROFILE_SUCCESS.result[0], alias: 'new-name' }); + }); + it('should delete device profiles', async () => { + const result = await client.profile.delete({ + userId: 'hello', + realm: 'alpha', + device: MOCK_DEVICE_PROFILE_SUCCESS.result[0], + }); + + expect(result).toEqual({ ...MOCK_DEVICE_PROFILE_SUCCESS.result[0] }); + }); + }); +}); diff --git a/packages/javascript-sdk/src/device-client/device.store.ts b/packages/javascript-sdk/src/device-client/device.store.ts new file mode 100644 index 000000000..4a045c737 --- /dev/null +++ b/packages/javascript-sdk/src/device-client/device.store.ts @@ -0,0 +1,370 @@ +import { type ConfigOptions } from '../config/interfaces'; + +import { configureStore } from '@reduxjs/toolkit'; +import { deviceService } from './services/index.js'; +import type { DeletedOathDevice, OathDevice, RetrieveOathQuery } from './types/oath.types.js'; +import type { + DeleteDeviceQuery, + DeletedPushDevice, + PushDevice, + PushDeviceQuery, +} from './types/push-device.types.js'; +import type { + UpdatedWebAuthnDevice, + WebAuthnDevice, + WebAuthnQuery, +} from './types/webauthn.types.js'; +import type { BoundDeviceQuery, Device, GetBoundDevicesQuery } from './types/bound-device.types.js'; +import type { + GetProfileDevices, + ProfileDevice, + ProfileDevicesQuery, +} from './types/profile-device.types.js'; + +export const deviceClient = (config: ConfigOptions) => { + const { middleware, reducerPath, reducer, endpoints } = deviceService({ + baseUrl: config.serverConfig?.baseUrl ?? '', + realmPath: config?.realmPath ?? '', + }); + + const store = configureStore({ + reducer: { + [reducerPath]: reducer, + }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware), + }); + + /** + * Device management object containing methods for handling various device types. + * + * @type {DeviceManagement} + */ + return { + /** + * Oath device management methods. + * + * @type {OathManagement} + */ + oath: { + /** + * Retrieves Oath devices based on the specified query. + * + * @async + * @function get + * @param {RetrieveOathQuery} query - The query used to retrieve Oath devices. + * @returns {Promise} - A promise that resolves to the retrieved data or an error object if the response is not valid. + */ + get: async function (query: RetrieveOathQuery): Promise { + try { + const response = await store.dispatch(endpoints.getOAthDevices.initiate(query)); + + if (!response || !response.data || !response.data.result) { + throw new Error('response did not contain data'); + } + + return response.data.result; + } catch (error) { + return { error }; + } + }, + + /** + * Deletes an Oath device based on the provided query and device information. + * + * @async + * @function delete + * @param {DeleteOathQuery & OathDevice} query - The query and device information used to delete the Oath device. + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + delete: async function ( + query: RetrieveOathQuery & { device: OathDevice }, + ): Promise { + try { + const response = await store.dispatch(endpoints.deleteOathDevice.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + }, + + /** + * Push device management methods. + * + * @type {PushManagement} + */ + push: { + /** + * Retrieves Push devices based on the specified query. + * + * @async + * @function get + * @param {PushDeviceQuery} query - The query used to retrieve Push devices. + * @returns {Promise} - A promise that resolves to the retrieved data or an error object if the response is not valid. + */ + get: async function (query: PushDeviceQuery): Promise { + try { + const response = await store.dispatch(endpoints.getPushDevices.initiate(query)); + + if (!response || !response.data || !response.data.result) { + throw new Error('response did not contain data'); + } + + return response.data.result; + } catch (error) { + return { error }; + } + }, + + /** + * Deletes a Push device based on the provided query. + * + * @async + * @function delete + * @param {DeleteDeviceQuery} query - The query used to delete the Push device. + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + delete: async function ( + query: DeleteDeviceQuery, + ): Promise { + try { + const response = await store.dispatch(endpoints.deletePushDevice.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + }, + + /** + * WebAuthn device management methods. + * + * @type {WebAuthnManagement} + */ + webAuthn: { + /** + * Retrieves WebAuthn devices based on the specified query. + * + * @async + * @function get + * @param {WebAuthnQuery} query - The query used to retrieve WebAuthn devices. + * @returns {Promise} - A promise that resolves to the retrieved data or an error object if the response is not valid. + */ + get: async function (query: WebAuthnQuery): Promise { + try { + const response = await store.dispatch(endpoints.getWebAuthnDevices.initiate(query)); + + if (!response || !response.data || !response.data.result) { + throw new Error('response did not contain data'); + } + + return response.data.result; + } catch (error) { + return { error }; + } + }, + + /** + * Updates the name of a WebAuthn device based on the provided query and body. + * + * @async + * @function update + * @param {WebAuthnQueryWithUUID & { device: WebAuthnBody } } query - The query and body used to update the WebAuthn device name. + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + update: async function ( + query: WebAuthnQuery & { device: WebAuthnDevice }, + ): Promise { + try { + const response = await store.dispatch(endpoints.updateWebAuthnDeviceName.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + + /** + * Deletes a WebAuthn device based on the provided query and body. + * + * @async + * @function delete + * @param {WebAuthnQueryWithUUID & { device: WebAuthnBody } } query - The query and body used to delete the WebAuthn device. + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + delete: async function ( + query: WebAuthnQuery & { device: WebAuthnDevice | UpdatedWebAuthnDevice }, + ): Promise { + try { + const response = await store.dispatch(endpoints.deleteWebAuthnDeviceName.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + }, + + /** + * Bound devices management methods. + * + * @type {BoundDevicesManagement} + */ + bound: { + /** + * Retrieves bound devices based on the specified query. + * + * @async + * @function get + * @param {BoundDeviceQuery} query - The query used to retrieve bound devices. + * @returns {Promise} - A promise that resolves to the retrieved data or an error object if the response is not valid. + */ + get: async function (query: GetBoundDevicesQuery): Promise { + try { + const response = await store.dispatch(endpoints.getBoundDevices.initiate(query)); + + if (!response || !response.data || !response.data.result) { + throw new Error('response did not contain data'); + } + + return response.data.result; + } catch (error) { + return { error }; + } + }, + + /** + * Deletes a bound device based on the provided query. + * + * @async + * @function delete + * @param {BoundDeviceQuery} query - The query used to delete the bound device. + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + delete: async function (query: BoundDeviceQuery): Promise { + try { + const response = await store.dispatch(endpoints.deleteBoundDevice.initiate(query)); + + if (!response || !response.data || !response.data.result) { + throw new Error('response did not contain data'); + } + + return response.data.result; + } catch (error) { + return { error }; + } + }, + + /** + * Updates the name of a bound device based on the provided query. + * + * @async + * @function update + * @param {BoundDeviceQuery} query - The query used to update the bound device name. + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + update: async function (query: BoundDeviceQuery): Promise { + try { + const response = await store.dispatch(endpoints.updateBoundDevice.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + }, + profile: { + /** + * Get profile devices + * + * @async + * @function update + * @param {GetProfileDevice} query - The query used to get profile devices + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + get: async function ( + query: GetProfileDevices, + ): Promise { + try { + const response = await store.dispatch(endpoints.getDeviceProfiles.initiate(query)); + + if (!response || !response.data || !response.data.result) { + throw new Error('response did not contain data'); + } + + return response.data.result; + } catch (error) { + return { error }; + } + }, + /** + * Get profile devices + * + * @async + * @function update + * @param {ProfileDevicesQuery} query - The query used to update a profile device + * @returns {Promise} - A promise that resolves to the response data or or an error object if the response is not valid. + */ + update: async function ( + query: ProfileDevicesQuery, + ): Promise { + try { + const response = await store.dispatch(endpoints.updateDeviceProfile.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + /** + * Get profile devices + * + * @async + * @function update + * @param {ProfileDevicesQuery} query - The query used to update a profile device + * @returns {Promise} - A promise that resolves to the response data or an error object if the response is not valid. + */ + delete: async function ( + query: ProfileDevicesQuery, + ): Promise { + try { + const response = await store.dispatch(endpoints.deleteDeviceProfile.initiate(query)); + + if (!response || !response.data) { + throw new Error('response did not contain data'); + } + + return response.data; + } catch (error) { + return { error }; + } + }, + }, + }; +}; diff --git a/packages/device-client/src/lib/mock-data/access-denied.json b/packages/javascript-sdk/src/device-client/mock-data/access-denied.json similarity index 100% rename from packages/device-client/src/lib/mock-data/access-denied.json rename to packages/javascript-sdk/src/device-client/mock-data/access-denied.json diff --git a/packages/device-client/src/lib/mock-data/forbidden.json b/packages/javascript-sdk/src/device-client/mock-data/forbidden.json similarity index 100% rename from packages/device-client/src/lib/mock-data/forbidden.json rename to packages/javascript-sdk/src/device-client/mock-data/forbidden.json diff --git a/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts b/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts new file mode 100644 index 000000000..c204c0fa8 --- /dev/null +++ b/packages/javascript-sdk/src/device-client/mock-data/msw-mock-data.ts @@ -0,0 +1,165 @@ +import { GeneralResponse } from '../services/index.js'; +import type { + OathResponse, + DeletedOAthDevice, + DeviceResponse, + PushDevice, + WebAuthnDevice, +} from '../types/index.js'; +import { ProfileDevice } from '../types/profile-device.types.js'; + +// Mock data +export const MOCK_OATH_DEVICES: OathResponse = { + pagedResultsCookie: 'cookie', + remainingPagedResults: -1, + resultCount: 2, + totalPagedResults: 2, + totalPagedResultsPolicy: 'string', + result: [ + { + _id: 'oath-1', + _rev: '1-oath', + createdDate: 1705555555555, + lastAccessDate: 1705555555555, + deviceName: 'Test OATH Device', + uuid: 'oath-uuid-1', + deviceManagementStatus: true, + }, + ], +}; + +export const MOCK_DELETED_OATH_DEVICE: DeletedOAthDevice = { + _id: 'oath-1', + _rev: '1-oath', + uuid: 'oath-uuid-1', + recoveryCodes: ['code1', 'code2'], + createdDate: 1705555555555, + lastAccessDate: 1705555555555, + sharedSecret: 'secret123', + deviceName: 'Test OATH Device', + lastLogin: 1705555555555, + counter: 0, + checksumDigit: true, + truncationOffset: 0, + clockDriftSeconds: 0, +}; + +export const MOCK_PUSH_DEVICES: PushDevice[] = [ + { + _id: 'push-1', + _rev: '1-push', + createdDate: 1705555555555, + lastAccessDate: 1705555555555, + deviceName: 'Test Push Device', + uuid: 'push-uuid-1', + deviceManagementStatus: true, + }, +]; + +export const MOCK_WEBAUTHN_DEVICES: GeneralResponse = { + result: [ + { + _id: 'webauthn-1', + _rev: '1-webauthn', + createdDate: 1705555555555, + lastAccessDate: 1705555555555, + credentialId: 'credential-1', + deviceName: 'Test WebAuthn Device', + uuid: 'webauthn-uuid-1', + deviceManagementStatus: true, + }, + ], + resultCount: 1, + pagedResultsCookie: null, + totalPagedResultsPolicy: 'NONE', + totalPagedResults: -1, + remainingPagedResults: -1, +}; + +export const MOCK_BINDING_DEVICES: DeviceResponse = { + result: [ + { + _id: 'binding-1', + _rev: '1-binding', + createdDate: 1705555555555, + lastAccessDate: 1705555555555, + deviceId: 'device-1', + deviceName: 'Test Binding Device', + uuid: 'binding-uuid-1', + key: { + kty: 'RSA', + kid: 'key-1', + use: 'sig', + alg: 'RS256', + n: 'mock-n', + e: 'mock-e', + }, + deviceManagementStatus: true, + }, + ], + resultCount: 1, + pagedResultsCookie: null, + totalPagedResultsPolicy: 'NONE', + totalPagedResults: -1, + remainingPagedResults: -1, +}; + +export const MOCK_DEVICE_PROFILE_SUCCESS: GeneralResponse = { + result: [ + { + _id: 'ce0677ca57da8b38-5bfaa23e9a8ddc7899638da7cccbfe6a8879b6cf', + _rev: '755317638', + identifier: 'ce0677ca57da8b38-5bfaa23e9a8ddc7899638da7cccbfe6a8879b6cf', + metadata: { + platform: { + platform: 'Android', + version: 34, + device: 'emu64a', + deviceName: 'sdk_gphone64_arm64', + model: 'sdk_gphone64_arm64', + brand: 'google', + locale: 'en_US', + timeZone: 'America/Vancouver', + jailBreakScore: 0, + }, + hardware: { + hardware: 'ranchu', + manufacturer: 'Google', + storage: 5939, + memory: 2981, + cpu: 4, + display: { + width: 1440, + height: 2678, + orientation: 1, + }, + camera: { + numberOfCameras: 2, + }, + }, + browser: { + userAgent: + 'Mozilla/5.0 (Linux; Android 14; sdk_gphone64_arm64 Build/UPB4.230623.005; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.127 Mobile Safari/537.36', + }, + bluetooth: { + supported: true, + }, + network: { + connected: true, + }, + telephony: { + networkCountryIso: 'us', + carrierName: 'T-Mobile', + }, + }, + lastSelectedDate: 1727110785783, + alias: 'test', + recoveryCodes: [], + }, + ], + resultCount: 1, + pagedResultsCookie: null, + totalPagedResultsPolicy: 'NONE', + totalPagedResults: -1, + remainingPagedResults: -1, +}; diff --git a/packages/device-client/src/lib/mock-data/sessionInfo.json b/packages/javascript-sdk/src/device-client/mock-data/sessionInfo.json similarity index 100% rename from packages/device-client/src/lib/mock-data/sessionInfo.json rename to packages/javascript-sdk/src/device-client/mock-data/sessionInfo.json diff --git a/packages/device-client/src/lib/mock-data/successDeviceBinding.json b/packages/javascript-sdk/src/device-client/mock-data/successDeviceBinding.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successDeviceBinding.json rename to packages/javascript-sdk/src/device-client/mock-data/successDeviceBinding.json diff --git a/packages/device-client/src/lib/mock-data/successDeviceProfile.json b/packages/javascript-sdk/src/device-client/mock-data/successDeviceProfile.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successDeviceProfile.json rename to packages/javascript-sdk/src/device-client/mock-data/successDeviceProfile.json diff --git a/packages/device-client/src/lib/mock-data/successOath.json b/packages/javascript-sdk/src/device-client/mock-data/successOath.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successOath.json rename to packages/javascript-sdk/src/device-client/mock-data/successOath.json diff --git a/packages/device-client/src/lib/mock-data/successPush.json b/packages/javascript-sdk/src/device-client/mock-data/successPush.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successPush.json rename to packages/javascript-sdk/src/device-client/mock-data/successPush.json diff --git a/packages/device-client/src/lib/mock-data/successUpdateDeviceProfile.json b/packages/javascript-sdk/src/device-client/mock-data/successUpdateDeviceProfile.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successUpdateDeviceProfile.json rename to packages/javascript-sdk/src/device-client/mock-data/successUpdateDeviceProfile.json diff --git a/packages/device-client/src/lib/mock-data/successUpdateWebAuthn.json b/packages/javascript-sdk/src/device-client/mock-data/successUpdateWebAuthn.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successUpdateWebAuthn.json rename to packages/javascript-sdk/src/device-client/mock-data/successUpdateWebAuthn.json diff --git a/packages/device-client/src/lib/mock-data/successWebAuthn.json b/packages/javascript-sdk/src/device-client/mock-data/successWebAuthn.json similarity index 100% rename from packages/device-client/src/lib/mock-data/successWebAuthn.json rename to packages/javascript-sdk/src/device-client/mock-data/successWebAuthn.json diff --git a/packages/javascript-sdk/src/device-client/services/index.ts b/packages/javascript-sdk/src/device-client/services/index.ts new file mode 100644 index 000000000..151130ed0 --- /dev/null +++ b/packages/javascript-sdk/src/device-client/services/index.ts @@ -0,0 +1,141 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; +import { + DeletedOathDevice, + OathDevice, + OathResponse, + RetrieveOathQuery, +} from '../types/oath.types.js'; +import { + DeleteDeviceQuery, + DeletedPushDevice, + PushDevice, + PushDeviceQuery, +} from '../types/push-device.types.js'; +import { BoundDeviceQuery, Device, GetBoundDevicesQuery } from '../types/bound-device.types.js'; + +import { UpdatedWebAuthnDevice, WebAuthnDevice, WebAuthnQuery } from '../types/webauthn.types.js'; +import { + GetProfileDevices, + ProfileDevicesQuery, + ProfileDevice, +} from '../types/profile-device.types.js'; + +export interface GeneralResponse { + pagedResultsCookie: string | null; + remainingPagedResults: number; + resultCount: number; + totalPagedResults: number; + totalPagedResultsPolicy: string; + result: T; +} + +export const deviceService = ({ baseUrl, realmPath }: { baseUrl: string; realmPath: string }) => + createApi({ + reducerPath: 'deviceClient', + baseQuery: fetchBaseQuery({ + credentials: 'include', + prepareHeaders: (headers) => { + headers.set('Content-Type', 'application/json'); + headers.set('Accept', 'application/json'); + headers.set('x-requested-with', 'forgerock-sdk'); + headers.set('x-requested-platform', 'javascript'); + + return headers; + }, + baseUrl, + }), + endpoints: (builder) => ({ + // oath endpoints + getOAthDevices: builder.query({ + query: ({ realm = realmPath, userId }) => + `json/realms/${realm}/users/${userId}/devices/2fa/oath?_queryFilter=true`, + }), + + deleteOathDevice: builder.mutation< + DeletedOathDevice, + RetrieveOathQuery & { device: OathDevice } + >({ + query: ({ realm = realmPath, userId, device }) => ({ + method: 'DELETE', + url: `json/realms/${realm}/users/${userId}/devices/2fa/oath/${device.uuid}`, + body: device, + }), + }), + + // push device + getPushDevices: builder.query, PushDeviceQuery>({ + query: ({ realm = realmPath, userId }) => + `/json/realms/${realm}/users/${userId}/devices/2fa/push?_queryFilter=true`, + }), + + deletePushDevice: builder.mutation({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `/json/realms/${realm}/users/${userId}/devices/2fa/push/${device.uuid}`, + method: 'DELETE', + body: {}, + }), + }), + + // webauthn devices + getWebAuthnDevices: builder.query, WebAuthnQuery>({ + query: ({ realm = realmPath, userId }) => + `/json/realms/${realm}/users/${userId}/devices/2fa/webauthn?_queryFilter=true`, + }), + updateWebAuthnDeviceName: builder.mutation< + UpdatedWebAuthnDevice, + WebAuthnQuery & { device: WebAuthnDevice } + >({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `/json/realms/${realm}/users/${userId}/devices/2fa/webauthn/${device.uuid}`, + method: 'PUT', + body: device, + }), + }), + deleteWebAuthnDeviceName: builder.mutation< + UpdatedWebAuthnDevice, + WebAuthnQuery & { device: UpdatedWebAuthnDevice | WebAuthnDevice } + >({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `/json/realms/${realm}/users/${userId}/devices/2fa/webauthn/${device.uuid}`, + method: 'DELETE', + body: device, + }), + }), + getBoundDevices: builder.mutation, GetBoundDevicesQuery>({ + query: ({ realm = realmPath, userId }) => + `/json/realms/${realm}/users/${userId}/devices/2fa/binding?_queryFilter=true`, + }), + updateBoundDevice: builder.mutation({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `/json/realms/root/realms/${realm}/users/${userId}/devices/2fa/binding/${device.uuid}`, + method: 'PUT', + body: device, + }), + }), + deleteBoundDevice: builder.mutation, BoundDeviceQuery>({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `/json/realms/root/realms/${realm}/users/${userId}/devices/2fa/binding/${device.uuid}`, + method: 'DELETE', + body: device satisfies Device, + }), + }), + getDeviceProfiles: builder.query, GetProfileDevices>({ + query: ({ realm = realmPath, userId }) => + `json/realms/${realm}/users/${userId}/devices/profile?_queryFilter=true`, + }), + updateDeviceProfile: builder.mutation>({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `json/realms/${realm}/users/${userId}/devices/profile/${device.identifier}`, + method: 'PUT', + body: device, + }), + }), + deleteDeviceProfile: builder.mutation({ + query: ({ realm = realmPath, userId, device }) => ({ + url: `json/realms/${realm}/users/${userId}/devices/profile/${device.identifier}`, + method: 'DELETE', + body: device, + }), + }), + }), + }); diff --git a/packages/device-client/src/lib/types/binding-device.types.ts b/packages/javascript-sdk/src/device-client/types/bound-device.types.ts similarity index 79% rename from packages/device-client/src/lib/types/binding-device.types.ts rename to packages/javascript-sdk/src/device-client/types/bound-device.types.ts index 69a8bb306..a4d93c918 100644 --- a/packages/device-client/src/lib/types/binding-device.types.ts +++ b/packages/javascript-sdk/src/device-client/types/bound-device.types.ts @@ -1,7 +1,8 @@ -export type BindingDeviceQuery = { +export type GetBoundDevicesQuery = { userId: string; realm?: string; -} & Device; +}; +export type BoundDeviceQuery = GetBoundDevicesQuery & { device: Device }; export type DeviceResponse = { result: Device[]; @@ -20,6 +21,7 @@ export type Device = { deviceId: string; deviceName: string; uuid: string; + recoveryCodes: string[]; key: { kty: string; kid: string; diff --git a/packages/device-client/src/lib/types/index.ts b/packages/javascript-sdk/src/device-client/types/index.ts similarity index 78% rename from packages/device-client/src/lib/types/index.ts rename to packages/javascript-sdk/src/device-client/types/index.ts index 5a3ae88c9..d1af79e63 100644 --- a/packages/device-client/src/lib/types/index.ts +++ b/packages/javascript-sdk/src/device-client/types/index.ts @@ -1,5 +1,5 @@ export * from './oath.types.js'; export * from './webauthn.types.js'; export * from './push-device.types.js'; -export * from './binding-device.types.js'; +export * from './bound-device.types.js'; export * from './updateDeviceProfile.types.js'; diff --git a/packages/device-client/src/lib/types/oath.types.ts b/packages/javascript-sdk/src/device-client/types/oath.types.ts similarity index 72% rename from packages/device-client/src/lib/types/oath.types.ts rename to packages/javascript-sdk/src/device-client/types/oath.types.ts index 6565d4b64..68c068057 100644 --- a/packages/device-client/src/lib/types/oath.types.ts +++ b/packages/javascript-sdk/src/device-client/types/oath.types.ts @@ -1,9 +1,11 @@ export type OathDevice = { - id: string; + _id: string; + deviceManagementStatus: boolean; deviceName: string; uuid: string; - createdDate: Date; - lastAccessDate: Date; + createdDate: number; + lastAccessDate: number; + _rev: string; }; export type DeleteOathQuery = { @@ -17,17 +19,16 @@ export type RetrieveOathQuery = { userId: string; }; -export type OAthResponse = { - _id: string; - _rev: string; - createdDate: number; - lastAccessDate: number; - deviceName: string; - uuid: string; - deviceManagementStatus: boolean; -}[]; +export type OathResponse = { + pagedResultsCookie: string | null; + remainingPagedResults: number; + resultCount: number; + totalPagedResults: number; + totalPagedResultsPolicy: string; + result: OathDevice[]; +}; -export type DeletedOAthDevice = { +export type DeletedOathDevice = { _id: string; _rev: string; uuid: string; diff --git a/packages/javascript-sdk/src/device-client/types/profile-device.types.ts b/packages/javascript-sdk/src/device-client/types/profile-device.types.ts new file mode 100644 index 000000000..448634fd5 --- /dev/null +++ b/packages/javascript-sdk/src/device-client/types/profile-device.types.ts @@ -0,0 +1,58 @@ +export interface GetProfileDevices { + realm: string; + userId: string; +} + +export interface ProfileDevicesQuery extends GetProfileDevices { + device: ProfileDevice; +} + +export type ProfileDevice = { + _id: string; + _rev: string; + identifier: string; + metadata: { + platform: { + platform: string; + version: number; + device: string; + deviceName: string; + model: string; + brand: string; + locale: string; + timeZone: string; + jailBreakScore: number; + }; + hardware: { + hardware: string; + manufacturer: string; + storage: number; + memory: number; + cpu: number; + display: { + width: number; + height: number; + orientation: number; + }; + camera: { + numberOfCameras: number; + }; + }; + browser: { + userAgent: string; + }; + bluetooth: { + supported: boolean; + }; + network: { + connected: boolean; + }; + telephony: { + networkCountryIso: string; + carrierName: string; + }; + }; + lastSelectedDate: number; + alias: string; + recoveryCodes: string[]; +}; diff --git a/packages/device-client/src/lib/types/push-device.types.ts b/packages/javascript-sdk/src/device-client/types/push-device.types.ts similarity index 81% rename from packages/device-client/src/lib/types/push-device.types.ts rename to packages/javascript-sdk/src/device-client/types/push-device.types.ts index fff0db149..908bbfac4 100644 --- a/packages/device-client/src/lib/types/push-device.types.ts +++ b/packages/javascript-sdk/src/device-client/types/push-device.types.ts @@ -14,7 +14,7 @@ export type PushDeviceBody = { export type DeleteDeviceQuery = { realm?: string; userId: string; - uuid: string; + device: PushDevice; }; export type DeviceInfoResponse = { @@ -75,9 +75,6 @@ export type DeviceInfo = { alias: string; recoveryCodes: string[]; }; - -export type PushDevicesResponse = PushDevice[]; - export type PushDevice = { _id: string; _rev: string; @@ -87,3 +84,20 @@ export type PushDevice = { uuid: string; deviceManagementStatus: boolean; }; + +export interface DeletedPushDevice { + communicationId: string; + communicationType: string; + createdDate: number; + deviceId: string; + deviceMechanismUID: string; + deviceName: string; + deviceType: string; + issuer: string; + lastAccessDate: number; + recoveryCodes: string[]; + sharedSecret: string; + uuid: string; + _id: string; + _rev: string; +} diff --git a/packages/device-client/src/lib/types/updateDeviceProfile.types.ts b/packages/javascript-sdk/src/device-client/types/updateDeviceProfile.types.ts similarity index 100% rename from packages/device-client/src/lib/types/updateDeviceProfile.types.ts rename to packages/javascript-sdk/src/device-client/types/updateDeviceProfile.types.ts diff --git a/packages/device-client/src/lib/types/webauthn.types.ts b/packages/javascript-sdk/src/device-client/types/webauthn.types.ts similarity index 79% rename from packages/device-client/src/lib/types/webauthn.types.ts rename to packages/javascript-sdk/src/device-client/types/webauthn.types.ts index 858338f25..5b722806b 100644 --- a/packages/device-client/src/lib/types/webauthn.types.ts +++ b/packages/javascript-sdk/src/device-client/types/webauthn.types.ts @@ -3,10 +3,6 @@ export type WebAuthnQuery = { userId: string; }; -export type WebAuthnQueryWithUUID = { - uuid: string; -} & WebAuthnQuery; - export type WebAuthnBody = { id: string; deviceName: string; @@ -16,15 +12,6 @@ export type WebAuthnBody = { lastAccessDate: number; }; -export type WebAuthnDevicesResponse = { - result: WebAuthnDevice[]; - resultCount: number; - pagedResultsCookie: null; - totalPagedResultsPolicy: 'NONE'; - totalPagedResults: -1; - remainingPagedResults: -1; -}; - export type WebAuthnDevice = { _id: string; _rev: string; diff --git a/packages/javascript-sdk/src/index.ts b/packages/javascript-sdk/src/index.ts index 6944a4ace..86188b5f6 100644 --- a/packages/javascript-sdk/src/index.ts +++ b/packages/javascript-sdk/src/index.ts @@ -19,6 +19,7 @@ import AttributeInputCallback from './fr-auth/callbacks/attribute-input-callback import ChoiceCallback from './fr-auth/callbacks/choice-callback'; import ConfirmationCallback from './fr-auth/callbacks/confirmation-callback'; import DeviceProfileCallback from './fr-auth/callbacks/device-profile-callback'; +import { deviceClient } from './device-client/device.store'; import type { FRCallbackFactory } from './fr-auth/callbacks/factory'; import HiddenValueCallback from './fr-auth/callbacks/hidden-value-callback'; import KbaCreateCallback from './fr-auth/callbacks/kba-create-callback'; @@ -114,6 +115,7 @@ export { Config, ConfirmationCallback, Deferred, + deviceClient, DeviceProfileCallback, ErrorCode, FRAuth, diff --git a/packages/javascript-sdk/tsconfig.lib.json b/packages/javascript-sdk/tsconfig.lib.json index e4307b479..6cf373044 100644 --- a/packages/javascript-sdk/tsconfig.lib.json +++ b/packages/javascript-sdk/tsconfig.lib.json @@ -11,6 +11,7 @@ "declaration": true, "declarationMap": true, "strict": true, + "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true, "moduleResolution": "Bundler" @@ -21,6 +22,8 @@ "**/*.test.ts", "**/*.mock.ts", "tests/*", - "**/*.mock.data.ts" + "**/*.mock.data.ts", + "**/mock-data/*.ts", + "**/*.mock-data.ts" ] } diff --git a/packages/javascript-sdk/tsconfig.spec.json b/packages/javascript-sdk/tsconfig.spec.json index dfd5c8d0a..fc4e3b456 100644 --- a/packages/javascript-sdk/tsconfig.spec.json +++ b/packages/javascript-sdk/tsconfig.spec.json @@ -4,11 +4,13 @@ "outDir": "../../dist/out-tsc", "target": "ES2020", "composite": true, + "resolveJsonModule": true, "esModuleInterop": true, - "moduleResolution": "Bundler", + "moduleResolution": "Node", "lib": ["DOM", "DOM.Iterable", "es2023"] }, "include": [ + "package.json", "vite.config.ts", "vitest.config.ts", "src/**/*.test.ts", @@ -21,8 +23,10 @@ "src/**/*.spec.jsx", "src/**/*.d.ts", "src/**/*.mock.*", + "src/**/mock-data/msw-mock-data*", "tests/**/*.test.ts", "tests/**/*.spec.ts", "tests/**/*.mock*.ts" - ] + ], + "exclude": ["node_modules", "dist"] } diff --git a/packages/javascript-sdk/vite.config.ts b/packages/javascript-sdk/vite.config.ts index e0aedebcb..90c08413f 100644 --- a/packages/javascript-sdk/vite.config.ts +++ b/packages/javascript-sdk/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; import { copyFileSync } from 'fs'; +import pkg from './package.json'; export default defineConfig({ root: __dirname, @@ -23,6 +24,10 @@ export default defineConfig({ preserveModulesRoot: './src', preserveModules: true, }, + external: Array.from(Object.keys(pkg.dependencies) || []).concat([ + './**/mock-data/*', + '@reduxjs/toolkit/query', + ]), }, }, plugins: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad07e9e8c..931d1fe94 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,15 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@reduxjs/toolkit': + specifier: ^2.2.5 + version: 2.3.0 + immer: + specifier: ^10.1.1 + version: 10.1.1 + overrides: jsonpath-plus@<=7.2.0: '>=10.2.0' nanoid@<=3.3.7: '>=3.3.8' @@ -58,7 +67,7 @@ importers: version: 0.68.23(effect@3.5.3) '@effect/vitest': specifier: ^0.6.7 - version: 0.6.7(effect@3.5.3)(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) + version: 0.6.7(effect@3.5.3)(vitest@1.5.0) '@nx/devkit': specifier: 20.2.2 version: 20.2.2(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))) @@ -82,10 +91,10 @@ importers: version: 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(eslint@8.57.0)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(ts-node@10.9.1(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(typescript@5.6.3))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0)) '@nx/playwright': specifier: 20.2.2 - version: 20.2.2(@babel/traverse@7.24.1)(@playwright/test@1.47.2)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.19.12)(eslint@8.57.0)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) + version: 20.2.2(@babel/traverse@7.24.1)(@playwright/test@1.47.2)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.19.12)(eslint@8.57.0)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0) '@nx/vite': specifier: 20.2.2 - version: 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) + version: 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0) '@nx/web': specifier: 20.2.2 version: 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0)) @@ -130,7 +139,7 @@ importers: version: 1.1.0(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) '@vitest/coverage-v8': specifier: ^1.5.0 - version: 1.5.0(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) + version: 1.5.0(vitest@1.5.0) '@vitest/ui': specifier: ^1.4.0 version: 1.5.0(vitest@1.5.0) @@ -268,13 +277,10 @@ importers: version: 1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) + version: 0.3.3(vitest@1.5.0) e2e/autoscript-apps: dependencies: - '@forgerock/device-client': - specifier: workspace:* - version: link:../../packages/device-client '@forgerock/javascript-sdk': specifier: workspace:* version: link:../../packages/javascript-sdk @@ -346,24 +352,25 @@ importers: specifier: ^1.4.0 version: 1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0) - packages/device-client: + packages/javascript-sdk: dependencies: - '@forgerock/javascript-sdk': - specifier: 4.6.0 - version: link:../javascript-sdk '@reduxjs/toolkit': - specifier: 2.3.0 + specifier: 'catalog:' version: 2.3.0(react@18.3.1) + immer: + specifier: 'catalog:' + version: 10.1.1 devDependencies: + '@testing-library/react': + specifier: 16.1.0 + version: 16.1.0(@testing-library/dom@10.4.0)(react-dom@19.0.0(react@18.3.1))(react@18.3.1) msw: - specifier: ^2.5.1 - version: 2.5.1(@types/node@22.10.2)(typescript@5.6.3) + specifier: 2.7.0 + version: 2.7.0(@types/node@22.10.2)(typescript@5.6.3) vitest: specifier: ^1.4.0 version: 1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0) - packages/javascript-sdk: {} - packages/ping-protect: dependencies: '@forgerock/javascript-sdk': @@ -1374,10 +1381,10 @@ packages: integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==, } - '@bundled-es-modules/cookie@2.0.0': + '@bundled-es-modules/cookie@2.0.1': resolution: { - integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==, + integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==, } '@bundled-es-modules/statuses@1.0.1': @@ -2549,10 +2556,10 @@ packages: integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==, } - '@mswjs/interceptors@0.36.5': + '@mswjs/interceptors@0.37.5': resolution: { - integrity: sha512-aQ8WF5zQwOdcxLsxSEk9Jd01GgGb80xxqCaiDDlewhtwqpSm8MOvUHslwPydVirasdW09++NxDNNftm1vLY8yA==, + integrity: sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==, } engines: { node: '>=18' } @@ -3510,6 +3517,31 @@ packages: } engines: { node: '>=14.16' } + '@testing-library/dom@10.4.0': + resolution: + { + integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==, + } + engines: { node: '>=18' } + + '@testing-library/react@16.1.0': + resolution: + { + integrity: sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==, + } + engines: { node: '>=18' } + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tootallnate/once@2.0.0': resolution: { @@ -3566,6 +3598,12 @@ packages: integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==, } + '@types/aria-query@5.0.4': + resolution: + { + integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==, + } + '@types/babel__core@7.20.5': resolution: { @@ -4766,6 +4804,12 @@ packages: integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, } + aria-query@5.3.0: + resolution: + { + integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==, + } + array-buffer-byte-length@1.0.1: resolution: { @@ -5849,17 +5893,17 @@ packages: } engines: { node: '>= 0.6' } - cookie@0.5.0: + cookie@0.6.0: resolution: { - integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==, + integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, } engines: { node: '>= 0.6' } - cookie@0.6.0: + cookie@0.7.2: resolution: { - integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==, + integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, } engines: { node: '>= 0.6' } @@ -6408,6 +6452,13 @@ packages: } engines: { node: '>= 0.8' } + dequal@2.0.3: + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, + } + engines: { node: '>=6' } + destroy@1.2.0: resolution: { @@ -6505,6 +6556,12 @@ packages: } engines: { node: '>=6.0.0' } + dom-accessibility-api@0.5.16: + resolution: + { + integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==, + } + dom-serializer@2.0.0: resolution: { @@ -9897,6 +9954,13 @@ packages: integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==, } + lz-string@1.5.0: + resolution: + { + integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==, + } + hasBin: true + magic-string@0.30.11: resolution: { @@ -10288,10 +10352,10 @@ packages: integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, } - msw@2.5.1: + msw@2.7.0: resolution: { - integrity: sha512-V0BmHvFkbWGXqbyrc+XiuQ8DU3qzcb6lb8gB9Vzltp3cgHLHLCDF/KmmFo0xw58StNaRMTebw3/xpWVvU9xq9g==, + integrity: sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==, } engines: { node: '>=18' } hasBin: true @@ -11072,6 +11136,12 @@ packages: integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==, } + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + picomatch@2.3.1: resolution: { @@ -11552,6 +11622,13 @@ packages: engines: { node: '>=14' } hasBin: true + pretty-format@27.5.1: + resolution: + { + integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==, + } + engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } + pretty-format@29.7.0: resolution: { @@ -11787,6 +11864,20 @@ packages: } hasBin: true + react-dom@19.0.0: + resolution: + { + integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==, + } + peerDependencies: + react: ^19.0.0 + + react-is@17.0.2: + resolution: + { + integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==, + } + react-is@18.2.0: resolution: { @@ -12254,6 +12345,12 @@ packages: } engines: { node: '>=v12.22.7' } + scheduler@0.25.0: + resolution: + { + integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==, + } + schema-utils@3.3.0: resolution: { @@ -15208,9 +15305,9 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@bundled-es-modules/cookie@2.0.0': + '@bundled-es-modules/cookie@2.0.1': dependencies: - cookie: 0.5.0 + cookie: 0.7.2 '@bundled-es-modules/statuses@1.0.1': dependencies: @@ -15567,7 +15664,7 @@ snapshots: effect: 3.5.3 fast-check: 3.19.0 - '@effect/vitest@0.6.7(effect@3.5.3)(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))': + '@effect/vitest@0.6.7(effect@3.5.3)(vitest@1.5.0)': dependencies: effect: 3.5.3 vitest: 1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0) @@ -16111,7 +16208,7 @@ snapshots: '@microsoft/tsdoc@0.15.0': {} - '@mswjs/interceptors@0.36.5': + '@mswjs/interceptors@0.37.5': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -16383,12 +16480,12 @@ snapshots: '@nx/nx-win32-x64-msvc@20.2.2': optional: true - '@nx/playwright@20.2.2(@babel/traverse@7.24.1)(@playwright/test@1.47.2)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.19.12)(eslint@8.57.0)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))': + '@nx/playwright@20.2.2(@babel/traverse@7.24.1)(@playwright/test@1.47.2)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.19.12)(eslint@8.57.0)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0)': dependencies: '@nx/devkit': 20.2.2(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))) '@nx/eslint': 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(eslint@8.57.0)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(verdaccio@5.30.3(typanion@3.14.0)) '@nx/js': 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0)) - '@nx/vite': 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)) + '@nx/vite': 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0) '@nx/webpack': 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(esbuild@0.19.12)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0)) '@phenomnomnominal/tsquery': 5.0.1(typescript@5.6.3) minimatch: 9.0.3 @@ -16427,7 +16524,7 @@ snapshots: - vue-template-compiler - webpack-cli - '@nx/vite@20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))': + '@nx/vite@20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0))(vite@5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))(vitest@1.5.0)': dependencies: '@nx/devkit': 20.2.2(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))) '@nx/js': 20.2.2(@babel/traverse@7.24.1)(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12))(@types/node@22.10.2)(nx@20.2.2(@swc-node/register@1.9.2(@swc/core@1.5.7(@swc/helpers@0.5.12))(@swc/types@0.1.7)(typescript@5.6.3))(@swc/core@1.5.7(@swc/helpers@0.5.12)))(typescript@5.6.3)(verdaccio@5.30.3(typanion@3.14.0)) @@ -16925,6 +17022,24 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/runtime': 7.24.4 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/react@16.1.0(@testing-library/dom@10.4.0)(react-dom@19.0.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.4 + '@testing-library/dom': 10.4.0 + react: 18.3.1 + react-dom: 19.0.0(react@18.3.1) + '@tootallnate/once@2.0.0': {} '@tootallnate/quickjs-emscripten@0.23.0': {} @@ -16945,6 +17060,8 @@ snapshots: '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.24.4 @@ -17479,7 +17596,7 @@ snapshots: dependencies: vite: 5.4.8(@types/node@22.10.2)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0) - '@vitest/coverage-v8@1.5.0(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0))': + '@vitest/coverage-v8@1.5.0(vitest@1.5.0)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -17836,6 +17953,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + array-buffer-byte-length@1.0.1: dependencies: call-bind: 1.0.7 @@ -18598,10 +18719,10 @@ snapshots: cookie@0.4.1: {} - cookie@0.5.0: {} - cookie@0.6.0: {} + cookie@0.7.2: {} + cookiejar@2.1.4: {} cookies@0.9.1: @@ -18930,6 +19051,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + destroy@1.2.0: {} detect-file@1.0.0: {} @@ -18974,6 +19097,8 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -21327,7 +21452,6 @@ snapshots: loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - optional: true loupe@2.3.7: dependencies: @@ -21359,6 +21483,8 @@ snapshots: lunr@2.3.9: {} + lz-string@1.5.0: {} + magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -21542,22 +21668,23 @@ snapshots: ms@2.1.3: {} - msw@2.5.1(@types/node@22.10.2)(typescript@5.6.3): + msw@2.7.0(@types/node@22.10.2)(typescript@5.6.3): dependencies: - '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 '@inquirer/confirm': 5.0.0(@types/node@22.10.2) - '@mswjs/interceptors': 0.36.5 + '@mswjs/interceptors': 0.37.5 + '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.5 - chalk: 4.1.2 graphql: 16.9.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 path-to-regexp: 6.3.0 + picocolors: 1.1.1 strict-event-emitter: 0.5.1 type-fest: 4.26.1 yargs: 17.7.2 @@ -22039,6 +22166,8 @@ snapshots: picocolors@1.1.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} picomatch@3.0.1: {} @@ -22317,6 +22446,12 @@ snapshots: prettier@3.2.5: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -22444,12 +22579,18 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 + react-dom@19.0.0(react@18.3.1): + dependencies: + react: 18.3.1 + scheduler: 0.25.0 + + react-is@17.0.2: {} + react-is@18.2.0: {} react@18.3.1: dependencies: loose-envify: 1.4.0 - optional: true read-cache@1.0.0: dependencies: @@ -22739,6 +22880,8 @@ snapshots: dependencies: xmlchars: 2.2.0 + scheduler@0.25.0: {} + schema-utils@3.3.0: dependencies: '@types/json-schema': 7.0.15 @@ -23852,7 +23995,7 @@ snapshots: stylus: 0.64.0 terser: 5.33.0 - vitest-canvas-mock@0.3.3(vitest@1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0)): + vitest-canvas-mock@0.3.3(vitest@1.5.0): dependencies: jest-canvas-mock: 2.5.2 vitest: 1.5.0(@types/node@22.10.2)(@vitest/ui@1.5.0)(jsdom@22.1.0)(less@4.1.3)(sass@1.75.0)(stylus@0.64.0)(terser@5.33.0) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee99969e..5d7e9e0d1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,4 @@ packages: catalog: '@reduxjs/toolkit': ^2.2.5 + immer: ^10.1.1