From b60a5a3080a5ba1927fb0aada8e86f7a9cf086f7 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 15 Sep 2025 09:13:49 -0600 Subject: [PATCH 01/11] feat(journey-client): Initial migration and type fixes --- nx.json | 27 +- packages/journey-client/README.md | 7 + packages/journey-client/eslint.config.mjs | 19 ++ packages/journey-client/package.json | 30 ++ packages/journey-client/project.json | 41 +++ packages/journey-client/src/index.ts | 1 + packages/journey-client/src/lib/auth/index.ts | 109 +++++++ .../attribute-input-callback.test.ts | 83 ++++++ .../lib/callbacks/attribute-input-callback.ts | 91 ++++++ .../src/lib/callbacks/choice-callback.ts | 69 +++++ .../lib/callbacks/confirmation-callback.ts | 82 ++++++ .../lib/callbacks/device-profile-callback.ts | 55 ++++ .../src/lib/callbacks/factory.ts | 98 +++++++ .../lib/callbacks/fr-auth-callback.test.ts | 42 +++ .../lib/callbacks/hidden-value-callback.ts | 26 ++ .../journey-client/src/lib/callbacks/index.ts | 107 +++++++ .../src/lib/callbacks/kba-create-callback.ts | 66 +++++ .../src/lib/callbacks/metadata-callback.ts | 33 +++ .../src/lib/callbacks/name-callback.ts | 40 +++ .../src/lib/callbacks/password-callback.ts | 54 ++++ .../ping-protect-evaluation-callback.test.ts | 68 +++++ .../ping-protect-evaluation-callback.ts | 75 +++++ .../ping-protect-initialize-callback.test.ts | 152 ++++++++++ .../ping-protect-initialize-callback.ts | 49 ++++ .../lib/callbacks/polling-wait-callback.ts | 40 +++ .../src/lib/callbacks/recaptcha-callback.ts | 40 +++ .../recaptcha-enterprise-callback.test.ts | 86 ++++++ .../recaptcha-enterprise-callback.ts | 90 ++++++ .../src/lib/callbacks/redirect-callback.ts | 33 +++ .../src/lib/callbacks/select-idp-callback.ts | 53 ++++ .../suspended-text-output-callback.ts | 26 ++ .../terms-and-conditions-callback.ts | 55 ++++ .../lib/callbacks/text-input-callback.test.ts | 41 +++ .../src/lib/callbacks/text-input-callback.ts | 40 +++ .../src/lib/callbacks/text-output-callback.ts | 40 +++ ...validated-create-password-callback.test.ts | 82 ++++++ .../validated-create-password-callback.ts | 81 ++++++ ...validated-create-username-callback.test.ts | 82 ++++++ .../validated-create-username-callback.ts | 81 ++++++ packages/journey-client/src/lib/enums.ts | 20 ++ .../src/lib/fr-device/collector.ts | 53 ++++ .../src/lib/fr-device/defaults.ts | 90 ++++++ .../lib/fr-device/device-profile.mock.data.ts | 122 ++++++++ .../src/lib/fr-device/device-profile.test.ts | 86 ++++++ .../journey-client/src/lib/fr-device/index.ts | 270 ++++++++++++++++++ .../src/lib/fr-device/interfaces.ts | 67 +++++ .../src/lib/fr-device/sample-profile.json | 45 +++ .../src/lib/fr-login-failure.ts | 64 +++++ .../src/lib/fr-login-success.ts | 48 ++++ .../journey-client/src/lib/fr-policy/enums.ts | 35 +++ .../src/lib/fr-policy/fr-policy.test.ts | 199 +++++++++++++ .../src/lib/fr-policy/helpers.ts | 18 ++ .../journey-client/src/lib/fr-policy/index.ts | 124 ++++++++ .../src/lib/fr-policy/interfaces.ts | 22 ++ .../src/lib/fr-policy/message-creator.ts | 68 +++++ packages/journey-client/src/lib/fr-step.ts | 121 ++++++++ packages/journey-client/src/lib/interfaces.ts | 157 ++++++++++ .../journey-client/src/lib/journey-client.ts | 196 +++++++++++++ .../src/lib/shared/constants.ts | 4 + .../journey-client/src/lib/utils/strings.ts | 21 ++ .../src/lib/utils/timeout.test.ts | 27 ++ .../journey-client/src/lib/utils/timeout.ts | 27 ++ packages/journey-client/src/lib/utils/url.ts | 84 ++++++ packages/journey-client/tsconfig.json | 25 ++ packages/journey-client/tsconfig.lib.json | 35 +++ packages/journey-client/tsconfig.spec.json | 41 +++ packages/journey-client/vite.config.ts | 19 ++ pnpm-lock.yaml | 18 ++ tsconfig.json | 3 + 69 files changed, 4391 insertions(+), 12 deletions(-) create mode 100644 packages/journey-client/README.md create mode 100644 packages/journey-client/eslint.config.mjs create mode 100644 packages/journey-client/package.json create mode 100644 packages/journey-client/project.json create mode 100644 packages/journey-client/src/index.ts create mode 100644 packages/journey-client/src/lib/auth/index.ts create mode 100644 packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/attribute-input-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/choice-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/confirmation-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/device-profile-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/factory.ts create mode 100644 packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/hidden-value-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/index.ts create mode 100644 packages/journey-client/src/lib/callbacks/kba-create-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/metadata-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/name-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/password-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/polling-wait-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/recaptcha-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/redirect-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/select-idp-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/text-input-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/text-input-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/text-output-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts create mode 100644 packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts create mode 100644 packages/journey-client/src/lib/enums.ts create mode 100644 packages/journey-client/src/lib/fr-device/collector.ts create mode 100644 packages/journey-client/src/lib/fr-device/defaults.ts create mode 100644 packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts create mode 100644 packages/journey-client/src/lib/fr-device/device-profile.test.ts create mode 100644 packages/journey-client/src/lib/fr-device/index.ts create mode 100644 packages/journey-client/src/lib/fr-device/interfaces.ts create mode 100644 packages/journey-client/src/lib/fr-device/sample-profile.json create mode 100644 packages/journey-client/src/lib/fr-login-failure.ts create mode 100644 packages/journey-client/src/lib/fr-login-success.ts create mode 100644 packages/journey-client/src/lib/fr-policy/enums.ts create mode 100644 packages/journey-client/src/lib/fr-policy/fr-policy.test.ts create mode 100644 packages/journey-client/src/lib/fr-policy/helpers.ts create mode 100644 packages/journey-client/src/lib/fr-policy/index.ts create mode 100644 packages/journey-client/src/lib/fr-policy/interfaces.ts create mode 100644 packages/journey-client/src/lib/fr-policy/message-creator.ts create mode 100644 packages/journey-client/src/lib/fr-step.ts create mode 100644 packages/journey-client/src/lib/interfaces.ts create mode 100644 packages/journey-client/src/lib/journey-client.ts create mode 100644 packages/journey-client/src/lib/shared/constants.ts create mode 100644 packages/journey-client/src/lib/utils/strings.ts create mode 100644 packages/journey-client/src/lib/utils/timeout.test.ts create mode 100644 packages/journey-client/src/lib/utils/timeout.ts create mode 100644 packages/journey-client/src/lib/utils/url.ts create mode 100644 packages/journey-client/tsconfig.json create mode 100644 packages/journey-client/tsconfig.lib.json create mode 100644 packages/journey-client/tsconfig.spec.json create mode 100644 packages/journey-client/vite.config.ts diff --git a/nx.json b/nx.json index 75a097048d..10c64004be 100644 --- a/nx.json +++ b/nx.json @@ -103,7 +103,8 @@ "configName": "tsconfig.lib.json" } }, - "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"] + "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"], + "exclude": ["packages/journey-client/*"] }, { "plugin": "@nx/playwright/plugin", @@ -133,6 +134,19 @@ "watchDepsTargetName": "vite:watch-deps" }, "include": ["packages/**/**/*", "e2e/**/**/*", "tools/**/**/*"] + }, + { + "plugin": "@nx/js/typescript", + "include": ["packages/journey-client/*"], + "options": { + "typecheck": { + "targetName": "typecheck" + }, + "build": { + "targetName": "build", + "configName": "tsconfig.lib.json" + } + } } ], "parallel": 1, @@ -140,17 +154,6 @@ "appsDir": "", "libsDir": "" }, - "generators": { - "@nx/js:library": { - "outDir": "{projectRoot}/dist", - "unitTestRunner": "vitest" - }, - "@nx/web:application": { - "style": "css", - "unitTestRunner": "none", - "e2eTestRunner": "playwright" - } - }, "useDaemonProcess": true, "useInferencePlugins": true, "defaultBase": "main", diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md new file mode 100644 index 0000000000..1efbddd040 --- /dev/null +++ b/packages/journey-client/README.md @@ -0,0 +1,7 @@ +# journey-client + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build journey-client` to build the library. diff --git a/packages/journey-client/eslint.config.mjs b/packages/journey-client/eslint.config.mjs new file mode 100644 index 0000000000..c334bc0bc0 --- /dev/null +++ b/packages/journey-client/eslint.config.mjs @@ -0,0 +1,19 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'error', + { + ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], + }, + ], + }, + languageOptions: { + parser: await import('jsonc-eslint-parser'), + }, + }, +]; diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json new file mode 100644 index 0000000000..d4949a6bdc --- /dev/null +++ b/packages/journey-client/package.json @@ -0,0 +1,30 @@ +{ + "name": "@forgerock/journey-client", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "pnpm nx nxBuild", + "lint": "pnpm nx nxLint", + "test": "pnpm nx nxTest", + "test:watch": "pnpm nx nxTest --watch" + }, + "dependencies": { + "@forgerock/sdk-logger": "workspace:*", + "@forgerock/sdk-types": "workspace:*", + "@forgerock/sdk-utilities": "workspace:*", + "@forgerock/sdk-request-middleware": "workspace:*", + "tslib": "^2.3.0" + } +} diff --git a/packages/journey-client/project.json b/packages/journey-client/project.json new file mode 100644 index 0000000000..300ac05b3c --- /dev/null +++ b/packages/journey-client/project.json @@ -0,0 +1,41 @@ +{ + "name": "@forgerock/journey-client", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/journey-client/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/journey-client", + "tsConfig": "packages/journey-client/tsconfig.lib.json", + "packageJson": "packages/journey-client/package.json", + "main": "packages/journey-client/src/index.ts", + "assets": ["packages/journey-client/*.md"] + }, + "configurations": { + "production": { + "tsConfig": "packages/journey-client/tsconfig.lib.prod.json" + } + } + }, + "nxBuild": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/journey-client", + "tsConfig": "packages/journey-client/tsconfig.lib.json", + "packageJson": "packages/journey-client/package.json", + "main": "packages/journey-client/src/index.ts", + "assets": ["packages/journey-client/*.md"] + }, + "configurations": { + "production": { + "tsConfig": "packages/journey-client/tsconfig.lib.prod.json" + } + } + } + }, + "tags": [] +} \ No newline at end of file diff --git a/packages/journey-client/src/index.ts b/packages/journey-client/src/index.ts new file mode 100644 index 0000000000..d3827f7d60 --- /dev/null +++ b/packages/journey-client/src/index.ts @@ -0,0 +1 @@ +export * from './lib/journey-client.js'; diff --git a/packages/journey-client/src/lib/auth/index.ts b/packages/journey-client/src/lib/auth/index.ts new file mode 100644 index 0000000000..6a9a389335 --- /dev/null +++ b/packages/journey-client/src/lib/auth/index.ts @@ -0,0 +1,109 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { ServerConfig, StepOptions } from '../config.js'; +import Config from '../config.js'; +import { ActionTypes } from '../config/enums.js'; +import { REQUESTED_WITH, X_REQUESTED_PLATFORM } from '../shared/constants.js'; +import { middlewareWrapper } from '@forgerock/sdk-request-middleware'; +import type { Step } from '../interfaces.js'; +import { stringify, resolve } from '../utils/url.js'; +import { withTimeout } from '../utils/timeout.js'; +import { getEndpointPath } from '@forgerock/sdk-utilities'; + +/** + * Provides direct access to the OpenAM authentication tree API. + */ +abstract class Auth { + /** + * Gets the next step in the authentication tree. + * + * @param {Step} previousStep The previous step, including any required input. + * @param {StepOptions} options Configuration default overrides. + * @return {Step} The next step in the authentication tree. + */ + public static async next(previousStep?: Step, options?: StepOptions): Promise { + const { middleware, platformHeader, realmPath, serverConfig, tree, type } = Config.get(options); + const query = options ? options.query : {}; + const url = this.constructUrl(serverConfig, realmPath, tree, query); + const requestOptions = this.configureRequest(previousStep); + + const runMiddleware = middlewareWrapper( + { + ...requestOptions, + url: new URL(url), + headers: new Headers(requestOptions.headers), + }, + { + type: previousStep ? ActionTypes.Authenticate : ActionTypes.StartAuthenticate, + payload: { + tree, + type: type ? type : 'service', + }, + }, + ); + const req = runMiddleware(middleware); + + if (platformHeader) { + if (req.headers) { + req.headers.set('X-Requested-Platform', X_REQUESTED_PLATFORM); + } + } + + const res = await withTimeout(fetch(req.url.toString(), req), serverConfig.timeout); + const json = await this.getResponseJson(res); + return json; + } + + private static constructUrl( + serverConfig: ServerConfig, + realmPath?: string, + tree?: string, + query?: Record, + ): string { + const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined; + const params: Record = { ...query, ...treeParams }; + const queryString = Object.keys(params).length > 0 ? `?${stringify(params)}` : ''; + const path = getEndpointPath({ + endpoint: 'authenticate', + realmPath, + customPaths: serverConfig.paths, + }); + const url = resolve(serverConfig.baseUrl, `${path}${queryString}`); + return url; + } + + private static configureRequest(step?: Step): RequestInit { + const init: RequestInit = { + body: step ? JSON.stringify(step) : undefined, + credentials: 'include', + headers: new Headers({ + Accept: 'application/json', + 'Accept-API-Version': 'protocol=1.0,resource=2.1', + 'Content-Type': 'application/json', + 'X-Requested-With': REQUESTED_WITH, + }), + method: 'POST', + }; + + return init; + } + + private static async getResponseJson(res: Response): Promise { + const contentType = res.headers.get('content-type'); + const isJson = contentType && contentType.indexOf('application/json') > -1; + const json = isJson ? await res.json() : {}; + json.status = res.status; + json.ok = res.ok; + return json; + } +} + +export default Auth; diff --git a/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts b/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts new file mode 100644 index 0000000000..08d6c1828b --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts @@ -0,0 +1,83 @@ +/* + * @forgerock/javascript-sdk + * + * attribute-input-callback.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { CallbackType } from '../../auth/enums.js'; +import type { Callback } from '../interfaces.js'; +import AttributeInputCallback from './attribute-input-callback.js'; + +describe('AttributeInputCallback', () => { + const payload: Callback = { + _id: 0, + input: [ + { + name: 'IDToken0', + value: '', + }, + { + name: 'IDToken0validateOnly', + value: false, + }, + ], + output: [ + { + name: 'name', + value: 'givenName', + }, + { + name: 'prompt', + value: 'First Name:', + }, + { + name: 'required', + value: true, + }, + { + name: 'policies', + value: { + policyRequirements: ['a', 'b'], + name: 'givenName', + policies: [], + }, + }, + { + name: 'failedPolicies', + value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })], + }, + { + name: 'validateOnly', + value: false, + }, + ], + type: CallbackType.StringAttributeInputCallback, + }; + + it('reads/writes basic properties with "validate only"', () => { + const cb = new AttributeInputCallback(payload); + cb.setValue('Clark'); + cb.setValidateOnly(true); + + expect(cb.getType()).toBe('StringAttributeInputCallback'); + expect(cb.getName()).toBe('givenName'); + expect(cb.getPrompt()).toBe('First Name:'); + expect(cb.isRequired()).toBe(true); + expect(cb.getPolicies().policyRequirements).toStrictEqual(['a', 'b']); + expect(cb.getFailedPolicies()).toStrictEqual([{ failedPolicies: { c: 'c', d: 'd' } }]); + expect(cb.getInputValue()).toBe('Clark'); + expect(cb.payload.input[1].value).toBe(true); + }); + + it('writes validate only to `false` for submission', () => { + const cb = new AttributeInputCallback(payload); + cb.setValidateOnly(false); + expect(cb.payload.input[1].value).toBe(false); + }); +}); + +}); diff --git a/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts b/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts new file mode 100644 index 0000000000..da66ed08c1 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts @@ -0,0 +1,91 @@ +/* + * @forgerock/javascript-sdk + * + * attribute-input-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback, PolicyRequirement } from '../interfaces.js'; + + +/** + * Represents a callback used to collect attributes. + * + * @typeparam T Maps to StringAttributeInputCallback, NumberAttributeInputCallback and + * BooleanAttributeInputCallback, respectively + */ +class AttributeInputCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the attribute name. + */ + public getName(): string { + return this.getOutputByName('name', ''); + } + + /** + * Gets the attribute prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Gets whether the attribute is required. + */ + public isRequired(): boolean { + return this.getOutputByName('required', false); + } + + /** + * Gets the callback's failed policies. + */ + public getFailedPolicies(): PolicyRequirement[] { + const failedPolicies = this.getOutputByName( + 'failedPolicies', + [], + ) as unknown as string[]; + try { + return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[]; + } catch (err) { + throw new Error( + 'Unable to parse "failed policies" from the ForgeRock server. The JSON within `AttributeInputCallback` was either malformed or missing.', + ); + } + } + + /** + * Gets the callback's applicable policies. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public getPolicies(): Record { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return this.getOutputByName>('policies', {}); + } + + /** + * Set if validating value only. + */ + public setValidateOnly(value: boolean): void { + this.setInputValue(value, /validateOnly/); + } + + /** + * Sets the attribute's value. + */ + public setValue(value: T): void { + this.setInputValue(value); + } +} + +export default AttributeInputCallback; diff --git a/packages/journey-client/src/lib/callbacks/choice-callback.ts b/packages/journey-client/src/lib/callbacks/choice-callback.ts new file mode 100644 index 0000000000..9e5d15892a --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/choice-callback.ts @@ -0,0 +1,69 @@ +/* + * @forgerock/javascript-sdk + * + * choice-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect an answer to a choice. + */ +class ChoiceCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the choice's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Gets the choice's default answer. + */ + public getDefaultChoice(): number { + return this.getOutputByName('defaultChoice', 0); + } + + /** + * Gets the choice's possible answers. + */ + public getChoices(): string[] { + return this.getOutputByName('choices', []); + } + + /** + * Sets the choice's answer by index position. + */ + public setChoiceIndex(index: number): void { + const length = this.getChoices().length; + if (index < 0 || index > length - 1) { + throw new Error(`${index} is out of bounds`); + } + this.setInputValue(index); + } + + /** + * Sets the choice's answer by value. + */ + public setChoiceValue(value: string): void { + const index = this.getChoices().indexOf(value); + if (index === -1) { + throw new Error(`"${value}" is not a valid choice`); + } + this.setInputValue(index); + } +} + +export default ChoiceCallback; diff --git a/packages/journey-client/src/lib/callbacks/confirmation-callback.ts b/packages/journey-client/src/lib/callbacks/confirmation-callback.ts new file mode 100644 index 0000000000..ddd54e3d01 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/confirmation-callback.ts @@ -0,0 +1,82 @@ +/* + * @forgerock/javascript-sdk + * + * confirmation-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect a confirmation to a message. + */ +class ConfirmationCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the index position of the confirmation's default answer. + */ + public getDefaultOption(): number { + return Number(this.getOutputByName('defaultOption', 0)); + } + + /** + * Gets the confirmation's message type. + */ + public getMessageType(): number { + return Number(this.getOutputByName('messageType', 0)); + } + + /** + * Gets the confirmation's possible answers. + */ + public getOptions(): string[] { + return this.getOutputByName('options', []); + } + + /** + * Gets the confirmation's option type. + */ + public getOptionType(): number { + return Number(this.getOutputByName('optionType', 0)); + } + + /** + * Gets the confirmation's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Set option index. + */ + public setOptionIndex(index: number): void { + if (index !== 0 && index !== 1) { + throw new Error(`"${index}" is not a valid choice`); + } + this.setInputValue(index); + } + + /** + * Set option value. + */ + public setOptionValue(value: string): void { + const index = this.getOptions().indexOf(value); + if (index === -1) { + throw new Error(`"${value}" is not a valid choice`); + } + this.setInputValue(index); + } +} + +export default ConfirmationCallback; diff --git a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts new file mode 100644 index 0000000000..0b73e06e30 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts @@ -0,0 +1,55 @@ +/* + * @forgerock/javascript-sdk + * + * device-profile-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; +import type { DeviceProfileData } from '../fr-device/interfaces.js'; + +/** + * Represents a callback used to collect device profile data. + */ +class DeviceProfileCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's data. + */ + public getMessage(): string { + return this.getOutputByName('message', ''); + } + + /** + * Does callback require metadata? + */ + public isMetadataRequired(): boolean { + return this.getOutputByName('metadata', false); + } + + /** + * Does callback require location data? + */ + public isLocationRequired(): boolean { + return this.getOutputByName('location', false); + } + + /** + * Sets the profile. + */ + public setProfile(profile: DeviceProfileData): void { + this.setInputValue(JSON.stringify(profile)); + } +} + +export default DeviceProfileCallback; diff --git a/packages/journey-client/src/lib/callbacks/factory.ts b/packages/journey-client/src/lib/callbacks/factory.ts new file mode 100644 index 0000000000..383eff8769 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/factory.ts @@ -0,0 +1,98 @@ +/* + * @forgerock/javascript-sdk + * + * factory.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import { CallbackType } from '../interfaces.js'; +import type { Callback } from '../interfaces.js'; +import AttributeInputCallback from './attribute-input-callback.js'; +import ChoiceCallback from './choice-callback.js'; +import ConfirmationCallback from './confirmation-callback.js'; +import DeviceProfileCallback from './device-profile-callback.js'; +import HiddenValueCallback from './hidden-value-callback.js'; +import KbaCreateCallback from './kba-create-callback.js'; +import MetadataCallback from './metadata-callback.js'; +import NameCallback from './name-callback.js'; +import PasswordCallback from './password-callback.js'; +import PingOneProtectEvaluationCallback from './ping-protect-evaluation-callback.js'; +import PingOneProtectInitializeCallback from './ping-protect-initialize-callback.js'; +import PollingWaitCallback from './polling-wait-callback.js'; +import ReCaptchaCallback from './recaptcha-callback.js'; +import ReCaptchaEnterpriseCallback from './recaptcha-enterprise-callback.js'; +import RedirectCallback from './redirect-callback.js'; +import SelectIdPCallback from './select-idp-callback.js'; +import SuspendedTextOutputCallback from './suspended-text-output-callback.js'; +import TermsAndConditionsCallback from './terms-and-conditions-callback.js'; +import TextInputCallback from './text-input-callback.js'; +import TextOutputCallback from './text-output-callback.js'; +import ValidatedCreatePasswordCallback from './validated-create-password-callback.js'; +import ValidatedCreateUsernameCallback from './validated-create-username-callback.js'; + +type FRCallbackFactory = (callback: Callback) => FRCallback; + +/** + * @hidden + */ +function createCallback(callback: Callback): FRCallback { + switch (callback.type) { + case CallbackType.BooleanAttributeInputCallback: + return new AttributeInputCallback(callback); + case CallbackType.ChoiceCallback: + return new ChoiceCallback(callback); + case CallbackType.ConfirmationCallback: + return new ConfirmationCallback(callback); + case CallbackType.DeviceProfileCallback: + return new DeviceProfileCallback(callback); + case CallbackType.HiddenValueCallback: + return new HiddenValueCallback(callback); + case CallbackType.KbaCreateCallback: + return new KbaCreateCallback(callback); + case CallbackType.MetadataCallback: + return new MetadataCallback(callback); + case CallbackType.NameCallback: + return new NameCallback(callback); + case CallbackType.NumberAttributeInputCallback: + return new AttributeInputCallback(callback); + case CallbackType.PasswordCallback: + return new PasswordCallback(callback); + case CallbackType.PingOneProtectEvaluationCallback: + return new PingOneProtectEvaluationCallback(callback); + case CallbackType.PingOneProtectInitializeCallback: + return new PingOneProtectInitializeCallback(callback); + case CallbackType.PollingWaitCallback: + return new PollingWaitCallback(callback); + case CallbackType.ReCaptchaCallback: + return new ReCaptchaCallback(callback); + case CallbackType.ReCaptchaEnterpriseCallback: + return new ReCaptchaEnterpriseCallback(callback); + case CallbackType.RedirectCallback: + return new RedirectCallback(callback); + case CallbackType.SelectIdPCallback: + return new SelectIdPCallback(callback); + case CallbackType.StringAttributeInputCallback: + return new AttributeInputCallback(callback); + case CallbackType.SuspendedTextOutputCallback: + return new SuspendedTextOutputCallback(callback); + case CallbackType.TermsAndConditionsCallback: + return new TermsAndConditionsCallback(callback); + case CallbackType.TextInputCallback: + return new TextInputCallback(callback); + case CallbackType.TextOutputCallback: + return new TextOutputCallback(callback); + case CallbackType.ValidatedCreatePasswordCallback: + return new ValidatedCreatePasswordCallback(callback); + case CallbackType.ValidatedCreateUsernameCallback: + return new ValidatedCreateUsernameCallback(callback); + default: + return new FRCallback(callback); + } +} + +export default createCallback; +export type { FRCallbackFactory }; diff --git a/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts b/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts new file mode 100644 index 0000000000..f306a712a8 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts @@ -0,0 +1,42 @@ +/* + * @forgerock/javascript-sdk + * + * fr-auth-callback.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from '.'; +import { CallbackType } from '../../auth/enums.js'; +import type { Callback } from '../interfaces.js'; + +describe('FRCallback', () => { + it('reads/writes basic properties', () => { + const payload: Callback = { + _id: 0, + input: [ + { + name: 'userName', + value: '', + }, + ], + output: [ + { + name: 'prompt', + value: 'Username:', + }, + ], + type: CallbackType.NameCallback, + }; + const cb = new FRCallback(payload); + cb.setInputValue('superman'); + + expect(cb.getType()).toBe('NameCallback'); + expect(cb.getOutputValue('prompt')).toBe('Username:'); + expect(cb.getInputValue()).toBe('superman'); + }); +}); + +}); diff --git a/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts b/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts new file mode 100644 index 0000000000..b4dc361136 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts @@ -0,0 +1,26 @@ +/* + * @forgerock/javascript-sdk + * + * hidden-value-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect information indirectly from the user. + */ +class HiddenValueCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } +} + +export default HiddenValueCallback; diff --git a/packages/journey-client/src/lib/callbacks/index.ts b/packages/journey-client/src/lib/callbacks/index.ts new file mode 100644 index 0000000000..5eadaa8673 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/index.ts @@ -0,0 +1,107 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { CallbackType } from '../interfaces.js'; +import type { Callback, NameValue } from '../interfaces.js'; + +/** + * Base class for authentication tree callback wrappers. + */ +class FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public payload: Callback) {} + + /** + * Gets the name of this callback type. + */ + public getType(): CallbackType { + return this.payload.type; + } + + /** + * Gets the value of the specified input element, or the first element if `selector` is not + * provided. + * + * @param selector The index position or name of the desired element + */ + public getInputValue(selector: number | string = 0): unknown { + return this.getArrayElement(this.payload.input, selector).value; + } + + /** + * Sets the value of the specified input element, or the first element if `selector` is not + * provided. + * + * @param selector The index position or name of the desired element + */ + public setInputValue(value: unknown, selector: number | string | RegExp = 0): void { + this.getArrayElement(this.payload.input, selector).value = value; + } + + /** + * Gets the value of the specified output element, or the first element if `selector` + * is not provided. + * + * @param selector The index position or name of the desired element + */ + public getOutputValue(selector: number | string = 0): unknown { + return this.getArrayElement(this.payload.output, selector).value; + } + + /** + * Gets the value of the first output element with the specified name or the + * specified default value. + * + * @param name The name of the desired element + */ + public getOutputByName(name: string, defaultValue: T): T { + const output = this.payload.output.find((x: NameValue) => x.name === name); + return output ? (output.value as T) : defaultValue; + } + + private getArrayElement( + array: NameValue[] | undefined, + selector: number | string | RegExp = 0, + ): NameValue { + if (array === undefined) { + throw new Error(`No NameValue array was provided to search (selector ${selector})`); + } + + if (typeof selector === 'number') { + if (selector < 0 || selector > array.length - 1) { + throw new Error(`Selector index ${selector} is out of range`); + } + return array[selector]; + } + + if (typeof selector === 'string') { + const input = array.find((x) => x.name === selector); + if (!input) { + throw new Error(`Missing callback input entry "${selector}"`); + } + return input; + } + + // Duck typing for RegEx + if (typeof selector === 'object' && selector.test && Boolean(selector.exec)) { + const input = array.find((x) => selector.test(x.name)); + if (!input) { + throw new Error(`Missing callback input entry "${selector}"`); + } + return input; + } + + throw new Error('Invalid selector value type'); + } +} + +export default FRCallback; diff --git a/packages/journey-client/src/lib/callbacks/kba-create-callback.ts b/packages/journey-client/src/lib/callbacks/kba-create-callback.ts new file mode 100644 index 0000000000..ee4cc3dd67 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/kba-create-callback.ts @@ -0,0 +1,66 @@ +/* + * @forgerock/javascript-sdk + * + * kba-create-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect KBA-style security questions and answers. + */ +class KbaCreateCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Gets the callback's list of pre-defined security questions. + */ + public getPredefinedQuestions(): string[] { + return this.getOutputByName('predefinedQuestions', []); + } + + /** + * Sets the callback's security question. + */ + public setQuestion(question: string): void { + this.setValue('question', question); + } + + /** + * Sets the callback's security question answer. + */ + public setAnswer(answer: string): void { + this.setValue('answer', answer); + } + + private setValue(type: 'question' | 'answer', value: string): void { + if (!this.payload.input) { + throw new Error('KBA payload is missing input'); + } + + const input = this.payload.input.find((x) => x.name.endsWith(type)); + if (!input) { + throw new Error(`No input has name ending in "${type}"`); + } + input.value = value; + } +} + +export default KbaCreateCallback; diff --git a/packages/journey-client/src/lib/callbacks/metadata-callback.ts b/packages/journey-client/src/lib/callbacks/metadata-callback.ts new file mode 100644 index 0000000000..4c75b7bf10 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/metadata-callback.ts @@ -0,0 +1,33 @@ +/* + * @forgerock/javascript-sdk + * + * metadata-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to deliver and collect miscellaneous data. + */ +class MetadataCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's data. + */ + public getData(): T { + return this.getOutputByName('data', {} as T); + } +} + +export default MetadataCallback; diff --git a/packages/journey-client/src/lib/callbacks/name-callback.ts b/packages/journey-client/src/lib/callbacks/name-callback.ts new file mode 100644 index 0000000000..26330feb35 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/name-callback.ts @@ -0,0 +1,40 @@ +/* + * @forgerock/javascript-sdk + * + * name-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect a username. + */ +class NameCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Sets the username. + */ + public setName(name: string): void { + this.setInputValue(name); + } +} + +export default NameCallback; diff --git a/packages/journey-client/src/lib/callbacks/password-callback.ts b/packages/journey-client/src/lib/callbacks/password-callback.ts new file mode 100644 index 0000000000..ead58aa0c0 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/password-callback.ts @@ -0,0 +1,54 @@ +/* + * @forgerock/javascript-sdk + * + * password-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback, PolicyRequirement } from '../interfaces.js'; + +/** + * Represents a callback used to collect a password. + */ +class PasswordCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's failed policies. + */ + public getFailedPolicies(): PolicyRequirement[] { + return this.getOutputByName('failedPolicies', []); + } + + /** + * Gets the callback's applicable policies. + */ + public getPolicies(): string[] { + return this.getOutputByName('policies', []); + } + + /** + * Gets the callback's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Sets the password. + */ + public setPassword(password: string): void { + this.setInputValue(password); + } +} + +export default PasswordCallback; diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts new file mode 100644 index 0000000000..e42963dd6e --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts @@ -0,0 +1,68 @@ +/* + * @forgerock/javascript-sdk + * + * ping-protect-evaluation-callback.test.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { vi, describe, it, expect } from 'vitest'; +import { CallbackType } from '../interfaces.js'; +import PingOneProtectEvaluationCallback from './ping-protect-evaluation-callback.js'; + +describe('PingOneProtectEvaluationCallback', () => { + it('should be defined', () => { + expect(PingOneProtectEvaluationCallback).toBeDefined(); + }); + it('should test that the pauseBehavior method can be called', () => { + const callback = new PingOneProtectEvaluationCallback({ + type: 'PingOneProtectEvaluationCallback' as CallbackType.PingOneProtectEvaluationCallback, + output: [{ name: 'pauseBehavioralData', value: true }], + }); + const mock = vi.spyOn(callback, 'getPauseBehavioralData'); + callback.getPauseBehavioralData(); + expect(mock).toHaveBeenCalled(); + }); + it('should test setData method', () => { + const callback = new PingOneProtectEvaluationCallback({ + type: 'PingOneProtectEvaluationCallback' as CallbackType.PingOneProtectEvaluationCallback, + output: [{ name: 'signals', value: '' }], + input: [ + { + name: 'IDToken1signals', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + ], + }); + const mock = vi.spyOn(callback, 'setData'); + callback.setData('data'); + expect(mock).toHaveBeenCalledWith('data'); + expect(callback.getInputValue('IDToken1signals')).toBe('data'); + }); + it('should test setClientError method', () => { + const callback = new PingOneProtectEvaluationCallback({ + type: 'PingOneProtectEvaluationCallback' as CallbackType.PingOneProtectEvaluationCallback, + output: [{ name: 'signals', value: '' }], + input: [ + { + name: 'IDToken1signals', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + ], + }); + const mock = vi.spyOn(callback, 'setClientError'); + callback.setClientError('error i just set'); + expect(mock).toHaveBeenCalledWith('error i just set'); + expect(callback.getInputValue('IDToken1clientError')).toBe('error i just set'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts new file mode 100644 index 0000000000..fceef54232 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts @@ -0,0 +1,75 @@ +/* + * @forgerock/javascript-sdk + * + * ping-protect-evaluation-callback.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * @class - Represents a callback used to complete and package up device and behavioral data. + */ +class PingOneProtectEvaluationCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's pauseBehavioralData value. + * @returns {boolean} + */ + public getPauseBehavioralData(): boolean { + return this.getOutputByName('pauseBehavioralData', false); + } + + /** + * @method setData - Set the result of data collection + * @param {string} data - Data from calling pingProtect.get() + * @returns {void} + */ + public setData(data: string): void { + this.setInputValue(data, /signals/); + } + + /** + * @method setClientError - Set the client error message + * @param {string} errorMessage - Error message + * @returns {void} + */ + public setClientError(errorMessage: string): void { + this.setInputValue(errorMessage, /clientError/); + } +} + +export default PingOneProtectEvaluationCallback; + +/** + * Example of callback: +{ + "type": "PingOneProtectEvaluationCallback", + "output": [ + { + "name": "pauseBehavioralData", + "value": true + } + ], + "input": [ + { + "name": "IDToken1signals", + "value": "" + }, + { + "name": "IDToken1clientError", + "value": "" + } + ] +} +*/ diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts new file mode 100644 index 0000000000..5a373e5072 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts @@ -0,0 +1,152 @@ +/* + * @forgerock/javascript-sdk + * + * ping-protect-intitialize-callback.test.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { vi, describe, expect, it } from 'vitest'; +import { CallbackType } from '../interfaces.js'; +import PingOneProtectInitializeCallback from './ping-protect-initialize-callback.js'; + +describe('PingOneProtectInitializeCallback', () => { + it('should exist', () => { + expect(PingOneProtectInitializeCallback).toBeDefined(); + }); + it('should test the getConfig method', () => { + const callback = new PingOneProtectInitializeCallback({ + type: 'PingOneProtectInitializeCallback' as CallbackType, + input: [ + { + name: 'IDToken1signals', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + ], + output: [ + { + name: 'envId', + value: '02fb4743-189a-4bc7-9d6c-a919edfe6447', + }, + { + name: 'consoleLogEnabled', + value: false, + }, + { + name: 'deviceAttributesToIgnore', + value: [], + }, + { + name: 'customHost', + value: '', + }, + { + name: 'lazyMetadata', + value: false, + }, + { + name: 'behavioralDataCollection', + value: true, + }, + { + name: 'deviceKeyRsyncIntervals', + value: 14, + }, + { + name: 'enableTrust', + value: false, + }, + { + name: 'disableTags', + value: false, + }, + { + name: 'disableHub', + value: false, + }, + ], + }); + const mock = vi.spyOn(callback, 'getConfig'); + const config = callback.getConfig(); + expect(mock).toHaveBeenCalled(); + expect(config).toMatchObject({ + envId: '02fb4743-189a-4bc7-9d6c-a919edfe6447', + consoleLogEnabled: false, + deviceAttributesToIgnore: [], + customHost: '', + lazyMetadata: false, + behavioralDataCollection: true, + deviceKeyRsyncIntervals: 14, + enableTrust: false, + disableTags: false, + disableHub: false, + }); + }); + it('should test the setClientError method', () => { + const callback = new PingOneProtectInitializeCallback({ + type: 'PingOneProtectInitializeCallback' as CallbackType, + input: [ + { + name: 'IDToken1signals', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + ], + output: [ + { + name: 'envId', + value: '02fb4743-189a-4bc7-9d6c-a919edfe6447', + }, + { + name: 'consoleLogEnabled', + value: false, + }, + { + name: 'deviceAttributesToIgnore', + value: [], + }, + { + name: 'customHost', + value: '', + }, + { + name: 'lazyMetadata', + value: false, + }, + { + name: 'behavioralDataCollection', + value: true, + }, + { + name: 'deviceKeyRsyncIntervals', + value: 14, + }, + { + name: 'enableTrust', + value: false, + }, + { + name: 'disableTags', + value: false, + }, + { + name: 'disableHub', + value: false, + }, + ], + }); + const mock = vi.spyOn(callback, 'setClientError'); + callback.setClientError('error i just set'); + expect(mock).toHaveBeenCalled(); + expect(callback.getInputValue('IDToken1clientError')).toBe('error i just set'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts new file mode 100644 index 0000000000..9d08b40878 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts @@ -0,0 +1,49 @@ +/* + * @forgerock/javascript-sdk + * + * ping-protect-initialize-callback.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * @class - Represents a callback used to initialize and start device and behavioral data collection. + */ +class PingOneProtectInitializeCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Get callback's initialization config settings + */ + public getConfig() { + const config = { + envId: this.getOutputByName('envId', ''), + consoleLogEnabled: this.getOutputByName('consoleLogEnabled', false), + deviceAttributesToIgnore: this.getOutputByName('deviceAttributesToIgnore', []), + customHost: this.getOutputByName('customHost', ''), + lazyMetadata: this.getOutputByName('lazyMetadata', false), + behavioralDataCollection: this.getOutputByName('behavioralDataCollection', true), + deviceKeyRsyncIntervals: this.getOutputByName('deviceKeyRsyncIntervals', 14), + enableTrust: this.getOutputByName('enableTrust', false), + disableTags: this.getOutputByName('disableTags', false), + disableHub: this.getOutputByName('disableHub', false), + }; + return config; + } + + public setClientError(errorMessage: string): void { + this.setInputValue(errorMessage, /clientError/); + } +} + +export default PingOneProtectInitializeCallback; diff --git a/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts b/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts new file mode 100644 index 0000000000..2e59a356e2 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts @@ -0,0 +1,40 @@ +/* + * @forgerock/javascript-sdk + * + * polling-wait-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to instruct the system to poll while a backend process completes. + */ +class PollingWaitCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the message to display while polling. + */ + public getMessage(): string { + return this.getOutputByName('message', ''); + } + + /** + * Gets the polling interval in milliseconds. + */ + public getWaitTime(): number { + return Number(this.getOutputByName('waitTime', 0)); + } +} + +export default PollingWaitCallback; diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts b/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts new file mode 100644 index 0000000000..7ebb8f98f4 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts @@ -0,0 +1,40 @@ +/* + * @forgerock/javascript-sdk + * + * recaptcha-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to integrate reCAPTCHA. + */ +class ReCaptchaCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the reCAPTCHA site key. + */ + public getSiteKey(): string { + return this.getOutputByName('recaptchaSiteKey', ''); + } + + /** + * Sets the reCAPTCHA result. + */ + public setResult(result: string): void { + this.setInputValue(result); + } +} + +export default ReCaptchaCallback; diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts new file mode 100644 index 0000000000..93780f3a16 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts @@ -0,0 +1,86 @@ +/* + * @forgerock/javascript-sdk + * + * recaptcha-enterprise-callback.test.ts + * + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, expect, it, beforeAll } from 'vitest'; +import ReCaptchaEnterpriseCallback from './recaptcha-enterprise-callback.js'; +import { CallbackType } from '../../auth/enums.js'; +import { Callback } from '../interfaces.js'; + +const recaptchaCallback: Callback = { + type: 'ReCaptchaEnterpriseCallback' as CallbackType.ReCaptchaEnterpriseCallback, + output: [ + { + name: 'recaptchaSiteKey', + value: '6LdSu_spAAAAAKz3UhIy4JYQld2lm_WRt7dEhf9T', + }, + { + name: 'captchaApiUri', + value: 'https://www.google.com/recaptcha/enterprise.js', + }, + { + name: 'captchaDivClass', + value: 'g-recaptcha', + }, + ], + input: [ + { + name: 'IDToken1token', + value: '', + }, + { + name: 'IDToken1action', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + { + name: 'IDToken1payload', + value: '', + }, + ], +}; +describe('enterprise recaptcha', () => { + let callback: ReCaptchaEnterpriseCallback; + beforeAll(() => { + callback = new ReCaptchaEnterpriseCallback(recaptchaCallback); + }); + it('should get the site key', () => { + const siteKey = callback.getSiteKey(); + expect(siteKey).toEqual('6LdSu_spAAAAAKz3UhIy4JYQld2lm_WRt7dEhf9T'); + }); + it('should get captchaApiUri', () => { + const url = callback.getApiUrl(); + expect(url).toEqual('https://www.google.com/recaptcha/enterprise.js'); + }); + it('should set the action', () => { + callback.setAction('my_action'); + expect(callback.getInputValue('IDToken1action')).toEqual('my_action'); + }); + it('should set the client error', () => { + callback.setClientError('error here'); + expect(callback.getInputValue('IDToken1clientError')).toEqual('error here'); + }); + it('should set the payload', () => { + callback.setPayload({ test: 'here' }); + expect(callback.getInputValue('IDToken1payload')).toEqual({ test: 'here' }); + }); + it('should set the token', () => { + callback.setResult('12345'); + expect(callback.getInputValue('IDToken1token')).toEqual('12345'); + }); + it('should get the class', () => { + const className = callback.getElementClass(); + expect(className).toBe('g-recaptcha'); + }); +}); + +}); diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts new file mode 100644 index 0000000000..a1757df479 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts @@ -0,0 +1,90 @@ +/* + * @forgerock/javascript-sdk + * + * recaptcha-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; +//"input": [ +// { +// "name": "IDToken1token", +// "value": "" +// }, +// { +// "name": "IDToken1action", +// "value": "" +// }, +// { +// "name": "IDToken1clientError", +// "value": "" +// }, +// { +// "name": "IDToken1payload", +// "value": "" +// } + +/** + * Represents a callback used to integrate reCAPTCHA. + */ +class ReCaptchaEnterpriseCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the reCAPTCHA site key. + */ + public getSiteKey(): string { + return this.getOutputByName('recaptchaSiteKey', ''); + } + + /** + * Get the api url + */ + public getApiUrl(): string { + return this.getOutputByName('captchaApiUri', ''); + } + /** + * Get the class name + */ + public getElementClass(): string { + return this.getOutputByName('captchaDivClass', ''); + } + /** + * Sets the reCAPTCHA result. + */ + public setResult(result: string): void { + this.setInputValue(result); + } + + /** + * Set client client error + */ + public setClientError(error: string) { + this.setInputValue(error, 'IDToken1clientError'); + } + + /** + * Set the recaptcha payload + */ + public setPayload(payload: unknown) { + this.setInputValue(payload, 'IDToken1payload'); + } + + /** + * Set the recaptcha action + */ + public setAction(action: string) { + this.setInputValue(action, 'IDToken1action'); + } +} + +export default ReCaptchaEnterpriseCallback; diff --git a/packages/journey-client/src/lib/callbacks/redirect-callback.ts b/packages/journey-client/src/lib/callbacks/redirect-callback.ts new file mode 100644 index 0000000000..6418915f38 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/redirect-callback.ts @@ -0,0 +1,33 @@ +/* + * @forgerock/javascript-sdk + * + * redirect-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect an answer to a choice. + */ +class RedirectCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the redirect URL. + */ + public getRedirectUrl(): string { + return this.getOutputByName('redirectUrl', ''); + } +} + +export default RedirectCallback; diff --git a/packages/journey-client/src/lib/callbacks/select-idp-callback.ts b/packages/journey-client/src/lib/callbacks/select-idp-callback.ts new file mode 100644 index 0000000000..d733ec06d6 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/select-idp-callback.ts @@ -0,0 +1,53 @@ +/* + * @forgerock/javascript-sdk + * + * select-idp-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +interface IdPValue { + provider: string; + uiConfig: { + [key: string]: string; + }; +} + +/** + * Represents a callback used to collect an answer to a choice. + */ +class SelectIdPCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the available providers. + */ + public getProviders(): IdPValue[] { + return this.getOutputByName('providers', []); + } + + /** + * Sets the provider by name. + */ + public setProvider(value: string): void { + const item = this.getProviders().find((item) => item.provider === value); + if (!item) { + throw new Error(`"${value}" is not a valid choice`); + } + this.setInputValue(item.provider); + } +} + +export default SelectIdPCallback; + +export type { IdPValue }; diff --git a/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts b/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts new file mode 100644 index 0000000000..4a4fd636a6 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts @@ -0,0 +1,26 @@ +/* + * @forgerock/javascript-sdk + * + * suspended-text-output-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import TextOutputCallback from './text-output-callback.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to display a message. + */ +class SuspendedTextOutputCallback extends TextOutputCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } +} + +export default SuspendedTextOutputCallback; diff --git a/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts new file mode 100644 index 0000000000..fcf6ec1afa --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts @@ -0,0 +1,55 @@ +/* + * @forgerock/javascript-sdk + * + * terms-and-conditions-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to collect acceptance of terms and conditions. + */ +class TermsAndConditionsCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the terms and conditions content. + */ + public getTerms(): string { + return this.getOutputByName('terms', ''); + } + + /** + * Gets the version of the terms and conditions. + */ + public getVersion(): string { + return this.getOutputByName('version', ''); + } + + /** + * Gets the date of the terms and conditions. + */ + public getCreateDate(): Date { + const date = this.getOutputByName('createDate', ''); + return new Date(date); + } + + /** + * Sets the callback's acceptance. + */ + public setAccepted(accepted = true): void { + this.setInputValue(accepted); + } +} + +export default TermsAndConditionsCallback; diff --git a/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts b/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts new file mode 100644 index 0000000000..1ffba1f627 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts @@ -0,0 +1,41 @@ +/* + * @forgerock/javascript-sdk + * + * attribute-input-callback.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { CallbackType } from '../interfaces.js'; +import type { Callback } from '../../auth/interfaces.js'; +import TextInputCallback from './text-input-callback.js'; + +describe('TextInputCallback', () => { + const payload: Callback = { + type: CallbackType.TextInputCallback, + output: [ + { + name: 'prompt', + value: 'Provide a nickname for this account', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }; + + it('reads/writes basic properties', () => { + const cb = new TextInputCallback(payload); + + expect(cb.getPrompt()).toBe('Provide a nickname for this account'); + + cb.setInput('Test setting input'); + + expect(cb.getInputValue()).toBe('Test setting input'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/text-input-callback.ts b/packages/journey-client/src/lib/callbacks/text-input-callback.ts new file mode 100644 index 0000000000..73d8bc637a --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/text-input-callback.ts @@ -0,0 +1,40 @@ +/* + * @forgerock/javascript-sdk + * + * text-input-callback.ts + * + * Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to retrieve input from the user. + */ +class TextInputCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Sets the callback's input value. + */ + public setInput(input: string): void { + this.setInputValue(input); + } +} + +export default TextInputCallback; diff --git a/packages/journey-client/src/lib/callbacks/text-output-callback.ts b/packages/journey-client/src/lib/callbacks/text-output-callback.ts new file mode 100644 index 0000000000..48915c939a --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/text-output-callback.ts @@ -0,0 +1,40 @@ +/* + * @forgerock/javascript-sdk + * + * text-output-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback } from '../interfaces.js'; + +/** + * Represents a callback used to display a message. + */ +class TextOutputCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the message content. + */ + public getMessage(): string { + return this.getOutputByName('message', ''); + } + + /** + * Gets the message type. + */ + public getMessageType(): string { + return this.getOutputByName('messageType', ''); + } +} + +export default TextOutputCallback; diff --git a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts new file mode 100644 index 0000000000..6c3d021c82 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts @@ -0,0 +1,82 @@ +/* + * @forgerock/javascript-sdk + * + * validated-create-password-callback.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { CallbackType } from '../../auth/enums.js'; +import type { Callback } from '../interfaces.js'; +import ValidatedCreatePasswordCallback from './validated-create-password-callback.js'; + +describe('ValidatedCreatePasswordCallback', () => { + const payload: Callback = { + type: CallbackType.ValidatedCreatePasswordCallback, + output: [ + { + name: 'echoOn', + value: false, + }, + { + name: 'required', + value: true, + }, + { + name: 'policies', + value: { + policyRequirements: ['a', 'b'], + name: 'password', + policies: [], + }, + }, + { + name: 'failedPolicies', + value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })], + }, + { + name: 'validateOnly', + value: false, + }, + { + name: 'prompt', + value: 'Password', + }, + ], + input: [ + { + name: 'IDToken2', + value: '', + }, + { + name: 'IDToken2validateOnly', + value: false, + }, + ], + _id: 1, + }; + + it('reads/writes basic properties with "validate only"', () => { + const cb = new ValidatedCreatePasswordCallback(payload); + cb.setPassword('abcd123'); + cb.setValidateOnly(true); + + expect(cb.getType()).toBe('ValidatedCreatePasswordCallback'); + expect(cb.getPrompt()).toBe('Password'); + expect(cb.isRequired()).toBe(true); + expect(cb.getPolicies().policyRequirements).toStrictEqual(['a', 'b']); + expect(cb.getFailedPolicies()).toStrictEqual([{ failedPolicies: { c: 'c', d: 'd' } }]); + expect(cb.payload.input[0].value).toBe('abcd123'); + expect(cb.payload.input[1].value).toBe(true); + }); + + it('writes validate only to `false` for submission', () => { + const cb = new ValidatedCreatePasswordCallback(payload); + cb.setValidateOnly(false); + expect(cb.payload.input[1].value).toBe(false); + }); +}); + +}); diff --git a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts new file mode 100644 index 0000000000..a4cee78ba7 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts @@ -0,0 +1,81 @@ +/* + * @forgerock/javascript-sdk + * + * validated-create-password-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback, PolicyRequirement } from '../interfaces.js'; + + +/** + * Represents a callback used to collect a valid platform password. + */ +class ValidatedCreatePasswordCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's failed policies. + */ + public getFailedPolicies(): PolicyRequirement[] { + const failedPolicies = this.getOutputByName( + 'failedPolicies', + [], + ) as unknown as string[]; + try { + return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[]; + } catch (err) { + throw new Error( + 'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.', + ); + } + } + + /** + * Gets the callback's applicable policies. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public getPolicies(): Record { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return this.getOutputByName>('policies', {}); + } + + /** + * Gets the callback's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Gets whether the password is required. + */ + public isRequired(): boolean { + return this.getOutputByName('required', false); + } + + /** + * Sets the callback's password. + */ + public setPassword(password: string): void { + this.setInputValue(password); + } + + /** + * Set if validating value only. + */ + public setValidateOnly(value: boolean): void { + this.setInputValue(value, /validateOnly/); + } +} + +export default ValidatedCreatePasswordCallback; diff --git a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts new file mode 100644 index 0000000000..d01f921b67 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts @@ -0,0 +1,82 @@ +/* + * @forgerock/javascript-sdk + * + * validated-create-username-callback.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { CallbackType } from '../../auth/enums.js'; +import type { Callback } from '../interfaces.js'; +import ValidatedCreateUsernameCallback from './validated-create-username-callback.js'; + +describe('ValidatedCreateUsernameCallback', () => { + const payload: Callback = { + type: CallbackType.ValidatedCreateUsernameCallback, + output: [ + { + name: 'echoOn', + value: false, + }, + { + name: 'required', + value: true, + }, + { + name: 'policies', + value: { + policyRequirements: ['a', 'b'], + name: 'username', + policies: [], + }, + }, + { + name: 'failedPolicies', + value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })], + }, + { + name: 'validateOnly', + value: false, + }, + { + name: 'prompt', + value: 'Username', + }, + ], + input: [ + { + name: 'IDToken2', + value: '', + }, + { + name: 'IDToken2validateOnly', + value: false, + }, + ], + _id: 1, + }; + + it('reads/writes basic properties with "validate only"', () => { + const cb = new ValidatedCreateUsernameCallback(payload); + cb.setName('abcd123'); + cb.setValidateOnly(true); + + expect(cb.getType()).toBe('ValidatedCreateUsernameCallback'); + expect(cb.getPrompt()).toBe('Username'); + expect(cb.isRequired()).toBe(true); + expect(cb.getPolicies().policyRequirements).toStrictEqual(['a', 'b']); + expect(cb.getFailedPolicies()).toStrictEqual([{ failedPolicies: { c: 'c', d: 'd' } }]); + expect(cb.payload.input[0].value).toBe('abcd123'); + expect(cb.payload.input[1].value).toBe(true); + }); + + it('writes validate only to `false` for submission', () => { + const cb = new ValidatedCreateUsernameCallback(payload); + cb.setValidateOnly(false); + expect(cb.payload.input[1].value).toBe(false); + }); +}); + +}); diff --git a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts new file mode 100644 index 0000000000..6dcd146861 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts @@ -0,0 +1,81 @@ +/* + * @forgerock/javascript-sdk + * + * validated-create-username-callback.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRCallback from './index.js'; +import type { Callback, PolicyRequirement } from '../interfaces.js'; + + +/** + * Represents a callback used to collect a valid platform username. + */ +class ValidatedCreateUsernameCallback extends FRCallback { + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public override payload: Callback) { + super(payload); + } + + /** + * Gets the callback's prompt. + */ + public getPrompt(): string { + return this.getOutputByName('prompt', ''); + } + + /** + * Gets the callback's failed policies. + */ + public getFailedPolicies(): PolicyRequirement[] { + const failedPolicies = this.getOutputByName( + 'failedPolicies', + [], + ) as unknown as string[]; + try { + return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[]; + } catch (err) { + throw new Error( + 'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreateUsernameCallback` was either malformed or missing.', + ); + } + } + + /** + * Gets the callback's applicable policies. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public getPolicies(): Record { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return this.getOutputByName>('policies', {}); + } + + /** + * Gets whether the username is required. + */ + public isRequired(): boolean { + return this.getOutputByName('required', false); + } + + /** + * Sets the callback's username. + */ + public setName(name: string): void { + this.setInputValue(name); + } + + /** + * Set if validating value only. + */ + public setValidateOnly(value: boolean): void { + this.setInputValue(value, /validateOnly/); + } +} + +export default ValidatedCreateUsernameCallback; diff --git a/packages/journey-client/src/lib/enums.ts b/packages/journey-client/src/lib/enums.ts new file mode 100644 index 0000000000..35b5b2f64a --- /dev/null +++ b/packages/journey-client/src/lib/enums.ts @@ -0,0 +1,20 @@ +/* + * @forgerock/javascript-sdk + * + * enums.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Types of steps returned by the authentication tree. + */ +enum StepType { + LoginFailure = 'LoginFailure', + LoginSuccess = 'LoginSuccess', + Step = 'Step', +} + +export { StepType }; diff --git a/packages/journey-client/src/lib/fr-device/collector.ts b/packages/journey-client/src/lib/fr-device/collector.ts new file mode 100644 index 0000000000..542da93775 --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/collector.ts @@ -0,0 +1,53 @@ +/* + * @forgerock/javascript-sdk + * + * collector.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + + + +/** + * @class Collector - base class for FRDevice + * Generic collector functions for collecting a device profile attributes + */ +class Collector { + /** + * @method reduceToObject - goes one to two levels into source to collect attribute + * @param props - array of strings; can use dot notation for two level lookup + * @param src - source of attributes to check + */ + // eslint-disable-next-line + reduceToObject(props: string[], src: Record): Record { + return props.reduce((prev, curr) => { + if (curr.includes('.')) { + const propArr = curr.split('.'); + const prop1 = propArr[0]; + const prop2 = propArr[1]; + const prop = src[prop1] && src[prop1][prop2]; + prev[prop2] = prop != undefined ? prop : ''; + } else { + prev[curr] = src[curr] != undefined ? src[curr] : null; + } + return prev; + }, {} as Record); + } + + /** + * @method reduceToString - goes one level into source to collect attribute + * @param props - array of strings + * @param src - source of attributes to check + */ + // eslint-disable-next-line + reduceToString(props: string[], src: any): string { + return props.reduce((prev, curr) => { + prev = `${prev}${src[curr].filename};`; + return prev; + }, ''); + } +} + +export default Collector; diff --git a/packages/journey-client/src/lib/fr-device/defaults.ts b/packages/journey-client/src/lib/fr-device/defaults.ts new file mode 100644 index 0000000000..c239c0d32a --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/defaults.ts @@ -0,0 +1,90 @@ +/* + * @forgerock/javascript-sdk + * + * defaults.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +const browserProps = [ + 'userAgent', + 'appName', + 'appCodeName', + 'appVersion', + 'appMinorVersion', + 'buildID', + 'product', + 'productSub', + 'vendor', + 'vendorSub', + 'browserLanguage', +]; +const configurableCategories = [ + 'fontNames', + 'displayProps', + 'browserProps', + 'hardwareProps', + 'platformProps', +]; +const delay = 30 * 1000; +const devicePlatforms = { + mac: ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], + windows: ['Win32', 'Win64', 'Windows', 'WinCE'], + ios: ['iPhone', 'iPad', 'iPod'], +}; +const displayProps = ['width', 'height', 'pixelDepth', 'orientation.angle']; +const fontNames = [ + 'cursive', + 'monospace', + 'serif', + 'sans-serif', + 'fantasy', + 'Arial', + 'Arial Black', + 'Arial Narrow', + 'Arial Rounded MT Bold', + 'Bookman Old Style', + 'Bradley Hand ITC', + 'Century', + 'Century Gothic', + 'Comic Sans MS', + 'Courier', + 'Courier New', + 'Georgia', + 'Gentium', + 'Impact', + 'King', + 'Lucida Console', + 'Lalit', + 'Modena', + 'Monotype Corsiva', + 'Papyrus', + 'Tahoma', + 'TeX', + 'Times', + 'Times New Roman', + 'Trebuchet MS', + 'Verdana', + 'Verona', +]; +const hardwareProps = [ + 'cpuClass', + 'deviceMemory', + 'hardwareConcurrency', + 'maxTouchPoints', + 'oscpu', +]; +const platformProps = ['language', 'platform', 'userLanguage', 'systemLanguage']; + +export { + browserProps, + configurableCategories, + delay, + devicePlatforms, + displayProps, + fontNames, + hardwareProps, + platformProps, +}; diff --git a/packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts b/packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts new file mode 100644 index 0000000000..680d277f39 --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts @@ -0,0 +1,122 @@ +/* + * @forgerock/javascript-sdk + * + * device-profile.mock.data.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +const expectedJsdom = { + identifier: '', + metadata: { + hardware: { + display: { + width: 0, + height: 0, + pixelDepth: 24, + angle: '', + }, + cpuClass: null, + deviceMemory: null, + hardwareConcurrency: 16, + maxTouchPoints: null, + oscpu: null, + }, + browser: { + appName: 'Netscape', + userAgent: 'Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/16.2.2', + appCodeName: 'Mozilla', + appVersion: '4.0', + appMinorVersion: null, + buildID: null, + product: 'Gecko', + productSub: '20030107', + vendor: 'Apple Computer, Inc.', + vendorSub: '', + browserLanguage: null, + plugins: '', + }, + platform: { + deviceName: 'Unknown (Browser)', + fonts: '', + language: 'en-US', + platform: '', + userLanguage: null, + systemLanguage: null, + timezone: 300, + }, + }, +}; + +const expectedJsdomWithoutDisplay = { + identifier: '', + metadata: { + hardware: { + display: {}, + cpuClass: null, + deviceMemory: null, + hardwareConcurrency: 16, + maxTouchPoints: null, + oscpu: null, + }, + browser: { + appName: 'Netscape', + userAgent: 'Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/16.2.2', + appCodeName: 'Mozilla', + appVersion: '4.0', + appMinorVersion: null, + buildID: null, + product: 'Gecko', + productSub: '20030107', + vendor: 'Apple Computer, Inc.', + vendorSub: '', + browserLanguage: null, + plugins: '', + }, + platform: { + deviceName: 'Unknown (Browser)', + fonts: '', + language: 'en-US', + platform: '', + userLanguage: null, + systemLanguage: null, + timezone: 300, + }, + }, +}; + +const expectedJsdomWithNarrowedBrowserProps = { + identifier: '', + metadata: { + hardware: { + display: { + width: 0, + height: 0, + pixelDepth: 24, + angle: '', + }, + cpuClass: null, + deviceMemory: null, + hardwareConcurrency: 16, + maxTouchPoints: null, + oscpu: null, + }, + browser: { + userAgent: 'Mozilla/5.0 (darwin) AppleWebKit/537.36 (KHTML, like Gecko) jsdom/16.2.2', + plugins: '', + }, + platform: { + deviceName: 'Unknown (Browser)', + fonts: '', + language: 'en-US', + platform: '', + userLanguage: null, + systemLanguage: null, + timezone: 300, + }, + }, +}; + +export { expectedJsdom, expectedJsdomWithoutDisplay, expectedJsdomWithNarrowedBrowserProps }; diff --git a/packages/journey-client/src/lib/fr-device/device-profile.test.ts b/packages/journey-client/src/lib/fr-device/device-profile.test.ts new file mode 100644 index 0000000000..251d1788be --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/device-profile.test.ts @@ -0,0 +1,86 @@ +/* + * @forgerock/javascript-sdk + * + * device-profile.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import { vi, expect, describe, it, beforeAll } from 'vitest'; +import Config from '../config'; +import FRDevice from './index.js'; + +Object.defineProperty(window, 'crypto', { + writable: true, + value: { + getRandomValues: vi.fn().mockImplementation(() => ['714524572', '2799534390', '3707617532']), + }, +}); + +beforeAll(() => { + Config.set({ + serverConfig: { + baseUrl: 'http://am.example.com:8443', + timeout: 3000, + }, + }); +}); +describe('Test DeviceProfile', () => { + it('should return basic metadata', async () => { + const device = new FRDevice(); + const profile = await device.getProfile({ + location: false, + metadata: true, + }); + const userAgent = profile.metadata.browser.userAgent as string; + const appName = profile.metadata.browser.appName as string; + const appVersion = profile.metadata.browser.appVersion as string; + const vendor = profile.metadata.browser.vendor as string; + const display = profile.metadata.hardware.display; + const deviceName = profile.metadata.platform.deviceName as string; + expect(userAgent.includes('jsdom')).toBeTruthy(); + expect(appName).toBe('Netscape'); + expect(appVersion).toBe('4.0'); + expect(vendor).toBe('Apple Computer, Inc.'); + expect(display).toHaveProperty('width'); + expect(display).toHaveProperty('height'); + expect(deviceName.length).toBeGreaterThan(1); + }); + + it('should return metadata without any display props', async () => { + const device = new FRDevice({ displayProps: [] }); + const profile = await device.getProfile({ + location: false, + metadata: true, + }); + const userAgent = profile.metadata.browser.userAgent as string; + const display = profile.metadata.hardware.display; + const deviceName = profile.metadata.platform.deviceName as string; + expect(userAgent.length).toBeGreaterThan(1); + expect(display.width).toBeFalsy(); + expect(display.height).toBeFalsy(); + expect(deviceName.length).toBeGreaterThan(1); + }); + + it('should return metadata according to narrowed browser props', async () => { + const device = new FRDevice({ browserProps: ['userAgent'] }); + const profile = await device.getProfile({ + location: false, + metadata: true, + }); + const userAgent = profile.metadata.browser.userAgent as string; + const appName = profile.metadata.browser.appName as string; + const appVersion = profile.metadata.browser.appVersion as string; + const vendor = profile.metadata.browser.vendor as string; + const display = profile.metadata.hardware.display; + const deviceName = profile.metadata.platform.deviceName as string; + expect(userAgent.includes('jsdom')).toBeTruthy(); + expect(appName).toBeFalsy(); + expect(appVersion).toBeFalsy(); + expect(vendor).toBeFalsy(); + expect(display).toHaveProperty('width'); + expect(display).toHaveProperty('height'); + expect(deviceName.length).toBeGreaterThan(1); + }); +}); diff --git a/packages/journey-client/src/lib/fr-device/index.ts b/packages/journey-client/src/lib/fr-device/index.ts new file mode 100644 index 0000000000..cbc2373497 --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/index.ts @@ -0,0 +1,270 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { + browserProps, + configurableCategories, + delay, + devicePlatforms, + displayProps, + fontNames, + hardwareProps, + platformProps, +} from './defaults.js'; +import type { + BaseProfileConfig, + Category, + CollectParameters, + DeviceProfileData, + Geolocation, + ProfileConfigOptions, +} from './interfaces.js'; +import Collector from './collector.js'; +import { logger } from '@forgerock/sdk-logger'; + +const FRLogger = logger({ level: 'info' }); +import Config from '../config.js'; + +/** + * @class FRDevice - Collects user device metadata + * + * Example: + * + * ```js + * // Instantiate new device object (w/optional config, if needed) + * const device = new forgerock.FRDevice( + * // optional configuration + * ); + * // override any instance methods, if needed + * // e.g.: device.getDisplayMeta = () => {}; + * + * // Call getProfile with required argument obj of boolean properties + * // of location and metadata + * const profile = await device.getProfile({ + * location: isLocationRequired, + * metadata: isMetadataRequired, + * }); + * ``` + */ +class FRDevice extends Collector { + config: BaseProfileConfig = { + fontNames, + devicePlatforms, + displayProps, + browserProps, + hardwareProps, + platformProps, + }; + + constructor(config?: ProfileConfigOptions) { + super(); + if (config) { + Object.keys(config).forEach((key: string) => { + if (!configurableCategories.includes(key)) { + throw new Error('Device profile configuration category does not exist.'); + } + this.config[key as Category] = config[key as Category]; + }); + } + } + + getBrowserMeta(): { [key: string]: string } { + if (typeof navigator === 'undefined') { + FRLogger.warn('Cannot collect browser metadata. navigator is not defined.'); + return {}; + } + return this.reduceToObject(this.config.browserProps, navigator); + } + + getBrowserPluginsNames(): string { + if (!(typeof navigator !== 'undefined' && navigator.plugins)) { + FRLogger.warn('Cannot collect browser plugin information. navigator.plugins is not defined.'); + return ''; + } + return this.reduceToString(Object.keys(navigator.plugins), navigator.plugins); + } + + getDeviceName(): string { + if (typeof navigator === 'undefined') { + FRLogger.warn('Cannot collect device name. navigator is not defined.'); + return ''; + } + const userAgent = navigator.userAgent; + const platform = navigator.platform; + + switch (true) { + case this.config.devicePlatforms.mac.includes(platform): + return 'Mac (Browser)'; + case this.config.devicePlatforms.ios.includes(platform): + return `${platform} (Browser)`; + case this.config.devicePlatforms.windows.includes(platform): + return 'Windows (Browser)'; + case /Android/.test(platform) || /Android/.test(userAgent): + return 'Android (Browser)'; + case /CrOS/.test(userAgent) || /Chromebook/.test(userAgent): + return 'Chrome OS (Browser)'; + case /Linux/.test(platform): + return 'Linux (Browser)'; + default: + return `${platform || 'Unknown'} (Browser)`; + } + } + + getDisplayMeta(): { [key: string]: string | number | null } { + if (typeof screen === 'undefined') { + FRLogger.warn('Cannot collect screen information. screen is not defined.'); + return {}; + } + return this.reduceToObject(this.config.displayProps, screen); + } + + getHardwareMeta(): { [key: string]: string } { + if (typeof navigator === 'undefined') { + FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.'); + return {}; + } + return this.reduceToObject(this.config.hardwareProps, navigator); + } + + getIdentifier(): string { + const storageKey = `${Config.get().prefix}-DeviceID`; + + if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { + FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.'); + return ''; + } + if (!localStorage) { + FRLogger.warn('Cannot store profile ID. localStorage is not supported.'); + return ''; + } + let id = localStorage.getItem(storageKey); + if (!id) { + // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" + id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); + localStorage.setItem(storageKey, id); + } + return id; + } + + getInstalledFonts(): string { + if (typeof document === 'undefined') { + FRLogger.warn('Cannot collect font data. Global document object is undefined.'); + return ''; + } + const canvas = document.createElement('canvas'); + if (!canvas) { + FRLogger.warn('Cannot collect font data. Browser does not support canvas element'); + return ''; + } + const context = canvas.getContext && canvas.getContext('2d'); + + if (!context) { + FRLogger.warn('Cannot collect font data. Browser does not support 2d canvas context'); + return ''; + } + const text = 'abcdefghi0123456789'; + context.font = '72px Comic Sans'; + const baseWidth = context.measureText(text).width; + + const installedFonts = this.config.fontNames.reduce((prev: string, curr: string) => { + context.font = `72px ${curr}, Comic Sans`; + const newWidth = context.measureText(text).width; + + if (newWidth !== baseWidth) { + prev = `${prev}${curr};`; + } + return prev; + }, ''); + + return installedFonts; + } + + async getLocationCoordinates(): Promise> { + if (!(typeof navigator !== 'undefined' && navigator.geolocation)) { + FRLogger.warn( + 'Cannot collect geolocation information. navigator.geolocation is not defined.', + ); + return Promise.resolve({}); + } + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + navigator.geolocation.getCurrentPosition( + (position) => + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }), + (error) => { + FRLogger.warn( + 'Cannot collect geolocation information. ' + error.code + ': ' + error.message, + ); + resolve({}); + }, + { + enableHighAccuracy: true, + timeout: delay, + maximumAge: 0, + }, + ); + }); + } + + getOSMeta(): { [key: string]: string } { + if (typeof navigator === 'undefined') { + FRLogger.warn('Cannot collect OS metadata. navigator is not defined.'); + return {}; + } + return this.reduceToObject(this.config.platformProps, navigator); + } + + async getProfile({ location, metadata }: CollectParameters): Promise { + const profile: DeviceProfileData = { + identifier: this.getIdentifier(), + }; + + if (metadata) { + profile.metadata = { + hardware: { + ...this.getHardwareMeta(), + display: this.getDisplayMeta(), + }, + browser: { + ...this.getBrowserMeta(), + plugins: this.getBrowserPluginsNames(), + }, + platform: { + ...this.getOSMeta(), + deviceName: this.getDeviceName(), + fonts: this.getInstalledFonts(), + timezone: this.getTimezoneOffset(), + }, + }; + } + if (location) { + profile.location = await this.getLocationCoordinates(); + } + return profile; + } + + getTimezoneOffset(): number | null { + try { + return new Date().getTimezoneOffset(); + } catch (err) { + FRLogger.warn('Cannot collect timezone information. getTimezoneOffset is not defined.'); + return null; + } + } +} + +export default FRDevice; + + + FRDevice; + diff --git a/packages/journey-client/src/lib/fr-device/interfaces.ts b/packages/journey-client/src/lib/fr-device/interfaces.ts new file mode 100644 index 0000000000..edbdc9c3a7 --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/interfaces.ts @@ -0,0 +1,67 @@ +/* + * @forgerock/javascript-sdk + * + * interfaces.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +type Category = 'fontNames' | 'displayProps' | 'browserProps' | 'hardwareProps' | 'platformProps'; + +interface CollectParameters { + location: boolean; + metadata: boolean; +} + +interface DeviceProfileData { + identifier: string; + metadata?: { + hardware: { + display: { + [key: string]: string | number | null; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + }; + browser: { + [key: string]: string | number | null; + }; + platform: { + [key: string]: string | number | null; + }; + }; + location?: Geolocation | Record; +} + +interface Geolocation { + latitude: number; + longitude: number; +} + +interface BaseProfileConfig { + fontNames: string[]; + devicePlatforms: { + mac: string[]; + windows: string[]; + ios: string[]; + }; + displayProps: string[]; + browserProps: string[]; + hardwareProps: string[]; + platformProps: string[]; +} + +interface ProfileConfigOptions { + [key: string]: string[]; +} + +export type { + BaseProfileConfig, + Category, + CollectParameters, + DeviceProfileData, + Geolocation, + ProfileConfigOptions, +}; diff --git a/packages/journey-client/src/lib/fr-device/sample-profile.json b/packages/journey-client/src/lib/fr-device/sample-profile.json new file mode 100644 index 0000000000..1b0375352c --- /dev/null +++ b/packages/journey-client/src/lib/fr-device/sample-profile.json @@ -0,0 +1,45 @@ +{ + "indentifier": "714524572-2799534390-3707617532", + "metadata": { + "hardware": { + "cpuClass": null, + "deviceMemory": 8, + "hardwareConcurrency": 16, + "maxTouchPoints": 0, + "oscpu": null, + "display": { + "width": 1080, + "height": 1920, + "pixelDepth": 24, + "angle": 270 + } + }, + "browser": { + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36 Edg/80.0.361.111", + "appName": "Netscape", + "appCodeName": "Mozilla", + "appVersion": "5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36 Edg/80.0.361.111", + "appMinorVersion": null, + "buildID": null, + "product": "Gecko", + "productSub": "20030107", + "vendor": "Google Inc.", + "vendorSub": "", + "browserLanguage": null, + "plugins": "internal-pdf-viewer;mhjfbmdgcfjbbpaeojofohoefgiehjai;internal-nacl-plugin;" + }, + "platform": { + "deviceName": "Mac (Browser)", + "language": "en-US", + "platform": "MacIntel", + "userLanguage": null, + "systemLanguage": null, + "fonts": "cursive;monospace;sans-serif;fantasy;Arial;Arial Black;Arial Narrow;Arial Rounded MT Bold;Comic Sans MS;Courier;Courier New;Georgia;Impact;Papyrus;Tahoma;Trebuchet MS;Verdana;", + "timezone": 300 + } + }, + "location": { + "latitude": 27.766237, + "longitude": -94.889905 + } +} diff --git a/packages/journey-client/src/lib/fr-login-failure.ts b/packages/journey-client/src/lib/fr-login-failure.ts new file mode 100644 index 0000000000..91a1ce84c4 --- /dev/null +++ b/packages/journey-client/src/lib/fr-login-failure.ts @@ -0,0 +1,64 @@ +/* + * @forgerock/javascript-sdk + * + * fr-login-failure.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRPolicy from './fr-policy/index.js'; +import type { MessageCreator, ProcessedPropertyError } from './fr-policy/interfaces.js'; +import type { Step } from './interfaces.js'; +import { StepType } from './enums.js'; +import type { AuthResponse, FailureDetail } from './interfaces.js'; + +class FRLoginFailure implements AuthResponse { + /** + * The type of step. + */ + public readonly type = StepType.LoginFailure; + + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public payload: Step) {} + + /** + * Gets the error code. + */ + public getCode(): number { + return Number(this.payload.code); + } + + /** + * Gets the failure details. + */ + public getDetail(): FailureDetail | undefined { + return this.payload.detail; + } + + /** + * Gets the failure message. + */ + public getMessage(): string | undefined { + return this.payload.message; + } + + /** + * Gets processed failure message. + */ + public getProcessedMessage(messageCreator?: MessageCreator): ProcessedPropertyError[] { + return FRPolicy.parseErrors(this.payload, messageCreator); + } + + /** + * Gets the failure reason. + */ + public getReason(): string | undefined { + return this.payload.reason; + } +} + +export default FRLoginFailure; diff --git a/packages/journey-client/src/lib/fr-login-success.ts b/packages/journey-client/src/lib/fr-login-success.ts new file mode 100644 index 0000000000..693455ecb5 --- /dev/null +++ b/packages/journey-client/src/lib/fr-login-success.ts @@ -0,0 +1,48 @@ +/* + * @forgerock/javascript-sdk + * + * fr-login-success.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { Step } from './interfaces.js'; +import { StepType } from './enums.js'; +import type { AuthResponse } from './interfaces.js'; + +class FRLoginSuccess implements AuthResponse { + /** + * The type of step. + */ + public readonly type = StepType.LoginSuccess; + + /** + * @param payload The raw payload returned by OpenAM + */ + constructor(public payload: Step) {} + + /** + * Gets the step's realm. + */ + public getRealm(): string | undefined { + return this.payload.realm; + } + + /** + * Gets the step's session token. + */ + public getSessionToken(): string | undefined { + return this.payload.tokenId; + } + + /** + * Gets the step's success URL. + */ + public getSuccessUrl(): string | undefined { + return this.payload.successUrl; + } +} + +export default FRLoginSuccess; diff --git a/packages/journey-client/src/lib/fr-policy/enums.ts b/packages/journey-client/src/lib/fr-policy/enums.ts new file mode 100644 index 0000000000..ce1acaaf13 --- /dev/null +++ b/packages/journey-client/src/lib/fr-policy/enums.ts @@ -0,0 +1,35 @@ +/* + * @forgerock/javascript-sdk + * + * enums.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +enum PolicyKey { + CannotContainCharacters = 'CANNOT_CONTAIN_CHARACTERS', + CannotContainDuplicates = 'CANNOT_CONTAIN_DUPLICATES', + CannotContainOthers = 'CANNOT_CONTAIN_OTHERS', + LeastCapitalLetters = 'AT_LEAST_X_CAPITAL_LETTERS', + LeastNumbers = 'AT_LEAST_X_NUMBERS', + MatchRegexp = 'MATCH_REGEXP', + MaximumLength = 'MAX_LENGTH', + MaximumNumber = 'MAXIMUM_NUMBER_VALUE', + MinimumLength = 'MIN_LENGTH', + MinimumNumber = 'MINIMUM_NUMBER_VALUE', + Required = 'REQUIRED', + Unique = 'UNIQUE', + UnknownPolicy = 'UNKNOWN_POLICY', + ValidArrayItems = 'VALID_ARRAY_ITEMS', + ValidDate = 'VALID_DATE', + ValidEmailAddress = 'VALID_EMAIL_ADDRESS_FORMAT', + ValidNameFormat = 'VALID_NAME_FORMAT', + ValidNumber = 'VALID_NUMBER', + ValidPhoneFormat = 'VALID_PHONE_FORMAT', + ValidQueryFilter = 'VALID_QUERY_FILTER', + ValidType = 'VALID_TYPE', +} + +export { PolicyKey }; diff --git a/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts b/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts new file mode 100644 index 0000000000..64401d2913 --- /dev/null +++ b/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts @@ -0,0 +1,199 @@ +/* + * @forgerock/javascript-sdk + * + * fr-policy.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRPolicy from './index.js'; +import { PolicyKey } from './enums.js'; + +describe('The IDM error handling', () => { + const property = 'userName'; + + it('returns a human readable error message', () => { + const test = { + expectedString: `${property} must be unique`, + policy: { + policyRequirement: 'UNIQUE', + }, + }; + const message = FRPolicy.parsePolicyRequirement(property, test.policy); + expect(message).toBe(test.expectedString); + }); + + it('returns a human readable error message with param data', () => { + const test = { + expectedString: `${property} must be at least 6 characters`, + policy: { + params: { + minLength: 6, + }, + policyRequirement: 'MIN_LENGTH', + }, + }; + const message = FRPolicy.parsePolicyRequirement(property, test.policy); + expect(message).toBe(test.expectedString); + }); + + it('returns a human readable generic message for unknown error', () => { + const test = { + expectedString: `${property}: Unknown policy requirement "SOME_UNKNOWN_POLICY"`, + policy: { + params: { + unknownParam: 6, + }, + policyRequirement: 'SOME_UNKNOWN_POLICY', + }, + }; + const message = FRPolicy.parsePolicyRequirement(property, test.policy); + expect(message).toBe(test.expectedString); + }); + + it('error handling is extensible by customer', () => { + const test = { + customMessage: { + CUSTOM_POLICY: (property: string, params: { policyRequirement: string }): string => + `this is a custom message for "${params.policyRequirement}" policy of ${property}`, + }, + expectedString: `this is a custom message for "CUSTOM_POLICY" policy of ${property}`, + policy: { + policyRequirement: 'CUSTOM_POLICY', + }, + }; + const message = FRPolicy.parsePolicyRequirement(property, test.policy, test.customMessage); + expect(message).toBe(test.expectedString); + }); + + it('error handling is overwritable by customer', () => { + const test = { + customMessage: { + [PolicyKey.Unique]: (property: string): string => + `this is a custom message for "UNIQUE" policy of ${property}`, + }, + expectedString: `this is a custom message for "UNIQUE" policy of ${property}`, + policy: { + policyRequirement: 'UNIQUE', + }, + }; + const message = FRPolicy.parsePolicyRequirement(property, test.policy, test.customMessage); + expect(message).toBe(test.expectedString); + }); + + it('groups failed policies for one property', () => { + const policy = { + policyRequirements: [ + { + policyRequirement: 'UNIQUE', + }, + { + params: { + minLength: 6, + }, + policyRequirement: 'MIN_LENGTH', + }, + ], + property: 'userName', + }; + + const messageArray = FRPolicy.parseFailedPolicyRequirement(policy); + expect(messageArray).toEqual([ + 'userName must be unique', + 'userName must be at least 6 characters', + ]); + }); + + it('returns an object array with a human readable error and the server error', () => { + const errorResponse = { + code: 403, + reason: 'Forbidden', + message: 'Policy validation failed', + detail: { + failedPolicyRequirements: [ + { + policyRequirements: [ + { + policyRequirement: 'UNIQUE', + }, + { + params: { + minLength: 6, + }, + policyRequirement: 'MIN_LENGTH', + }, + { + policyRequirement: 'CUSTOM_POLICY', + }, + ], + property: 'userName', + }, + { + policyRequirements: [ + { + params: { + numCaps: 1, + }, + policyRequirement: 'AT_LEAST_X_CAPITAL_LETTERS', + }, + { + params: { + minLength: 6, + }, + policyRequirement: 'MIN_LENGTH', + }, + ], + property: 'password', + }, + ], + result: false, + }, + }; + const customMessage = { + [PolicyKey.Unique]: (property: string): string => + `this is a custom message for "UNIQUE" policy of ${property}`, + CUSTOM_POLICY: (property: string, params: { policyRequirement: string }): string => + `this is a custom message for "${params.policyRequirement}" policy of ${property}`, + }; + const expected = [ + { + messages: [ + 'this is a custom message for "UNIQUE" policy of userName', + 'userName must be at least 6 characters', + 'this is a custom message for "CUSTOM_POLICY" policy of userName', + ], + detail: { + policyRequirements: [ + { policyRequirement: 'UNIQUE' }, + { params: { minLength: 6 }, policyRequirement: 'MIN_LENGTH' }, + { policyRequirement: 'CUSTOM_POLICY' }, + ], + property: 'userName', + }, + }, + { + messages: [ + 'password must contain at least 1 capital letter', + 'password must be at least 6 characters', + ], + detail: { + policyRequirements: [ + { + params: { numCaps: 1 }, + policyRequirement: 'AT_LEAST_X_CAPITAL_LETTERS', + }, + { params: { minLength: 6 }, policyRequirement: 'MIN_LENGTH' }, + ], + property: 'password', + }, + }, + ]; + + const errorObjArr = FRPolicy.parseErrors(errorResponse, customMessage); + expect(errorObjArr).toEqual(expected); + }); +}); +; +}); diff --git a/packages/journey-client/src/lib/fr-policy/helpers.ts b/packages/journey-client/src/lib/fr-policy/helpers.ts new file mode 100644 index 0000000000..3c6eb6f9f7 --- /dev/null +++ b/packages/journey-client/src/lib/fr-policy/helpers.ts @@ -0,0 +1,18 @@ +/* + * @forgerock/javascript-sdk + * + * helpers.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +function getProp(obj: { [key: string]: unknown } | undefined, prop: string, defaultValue: T): T { + if (!obj || obj[prop] === undefined) { + return defaultValue; + } + return obj[prop] as T; +} + +export { getProp }; diff --git a/packages/journey-client/src/lib/fr-policy/index.ts b/packages/journey-client/src/lib/fr-policy/index.ts new file mode 100644 index 0000000000..101f8ea074 --- /dev/null +++ b/packages/journey-client/src/lib/fr-policy/index.ts @@ -0,0 +1,124 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { FailedPolicyRequirement, PolicyRequirement, Step } from '../interfaces.js'; +import { PolicyKey } from './enums.js'; +import type { MessageCreator, ProcessedPropertyError } from './interfaces.js'; +import defaultMessageCreator from './message-creator.js'; + +/** + * Utility for processing policy failures into human readable messages. + * + * Example: + * + * ```js + * // Create message overrides and extensions as needed + * const messageCreator = { + * [PolicyKey.unique]: (property: string) => ( + * `this is a custom message for "UNIQUE" policy of ${property}` + * ), + * CUSTOM_POLICY: (property: string, params: any) => ( + * `this is a custom message for "${params.policyRequirement}" policy of ${property}` + * ), + * }; + * + * const thisStep = await FRAuth.next(previousStep); + * + * if (thisStep.type === StepType.LoginFailure) { + * const messagesStepMethod = thisStep.getProcessedMessage(messageCreator); + * const messagesClassMethod = FRPolicy.parseErrors(thisStep, messageCreator) + * } + */ +abstract class FRPolicy { + /** + * Parses policy errors and generates human readable error messages. + * + * @param {Step} err The step containing the error. + * @param {MessageCreator} messageCreator + * Extensible and overridable custom error messages for policy failures. + * @return {ProcessedPropertyError[]} Array of objects containing all processed policy errors. + */ + public static parseErrors( + err: Partial, + messageCreator?: MessageCreator, + ): ProcessedPropertyError[] { + const errors: ProcessedPropertyError[] = []; + if (err.detail && err.detail.failedPolicyRequirements) { + err.detail.failedPolicyRequirements.map((x: FailedPolicyRequirement) => { + errors.push.apply(errors, [ + { + detail: x, + messages: this.parseFailedPolicyRequirement(x, messageCreator), + }, + ]); + }); + } + return errors; + } + + /** + * Parses a failed policy and returns a string array of error messages. + * + * @param {FailedPolicyRequirement} failedPolicy The detail data of the failed policy. + * @param {MessageCreator} customMessage + * Extensible and overridable custom error messages for policy failures. + * @return {string[]} Array of strings with all processed policy errors. + */ + public static parseFailedPolicyRequirement( + failedPolicy: FailedPolicyRequirement, + messageCreator?: MessageCreator, + ): string[] { + const errors: string[] = []; + failedPolicy.policyRequirements.map((policyRequirement: PolicyRequirement) => { + errors.push( + this.parsePolicyRequirement(failedPolicy.property, policyRequirement, messageCreator), + ); + }); + return errors; + } + + /** + * Parses a policy error into a human readable error message. + * + * @param {string} property The property with the policy failure. + * @param {PolicyRequirement} policy The policy failure data. + * @param {MessageCreator} customMessage + * Extensible and overridable custom error messages for policy failures. + * @return {string} Human readable error message. + */ + public static parsePolicyRequirement( + property: string, + policy: PolicyRequirement, + messageCreator: MessageCreator = {}, + ): string { + // AM is returning policy requirement failures as JSON strings + const policyObject = typeof policy === 'string' ? JSON.parse(policy) : { ...policy }; + + const policyRequirement = policyObject.policyRequirement; + + // Determine which message creator function to use + const effectiveMessageCreator = + messageCreator[policyRequirement] || + defaultMessageCreator[policyRequirement] || + defaultMessageCreator[PolicyKey.UnknownPolicy]; + + // Flatten the parameters and create the message + const params = policyObject.params + ? { ...policyObject.params, policyRequirement } + : { policyRequirement }; + const message = effectiveMessageCreator(property, params); + + return message; + } +} + +export default FRPolicy; +export type { MessageCreator, ProcessedPropertyError }; +export { PolicyKey }; diff --git a/packages/journey-client/src/lib/fr-policy/interfaces.ts b/packages/journey-client/src/lib/fr-policy/interfaces.ts new file mode 100644 index 0000000000..7c1dfa905d --- /dev/null +++ b/packages/journey-client/src/lib/fr-policy/interfaces.ts @@ -0,0 +1,22 @@ +/* + * @forgerock/javascript-sdk + * + * interfaces.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { FailedPolicyRequirement } from '../interfaces.js'; + +interface MessageCreator { + [key: string]: (propertyName: string, params?: { [key: string]: unknown }) => string; +} + +interface ProcessedPropertyError { + detail: FailedPolicyRequirement; + messages: string[]; +} + +export type { MessageCreator, ProcessedPropertyError }; diff --git a/packages/journey-client/src/lib/fr-policy/message-creator.ts b/packages/journey-client/src/lib/fr-policy/message-creator.ts new file mode 100644 index 0000000000..bbbbfb080c --- /dev/null +++ b/packages/journey-client/src/lib/fr-policy/message-creator.ts @@ -0,0 +1,68 @@ +/* + * @forgerock/javascript-sdk + * + * message-creator.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { plural } from '../utils/strings.js'; +import { PolicyKey } from './enums.js'; +import { getProp } from './helpers.js'; +import type { MessageCreator } from './interfaces.js'; + +const defaultMessageCreator: MessageCreator = { + [PolicyKey.CannotContainCharacters]: (property: string, params?: { forbiddenChars?: string }) => { + const forbiddenChars = getProp(params, 'forbiddenChars', ''); + return `${property} must not contain following characters: "${forbiddenChars}"`; + }, + [PolicyKey.CannotContainDuplicates]: (property: string, params?: { duplicateValue?: string }) => { + const duplicateValue = getProp(params, 'duplicateValue', ''); + return `${property} must not contain duplicates: "${duplicateValue}"`; + }, + [PolicyKey.CannotContainOthers]: (property: string, params?: { disallowedFields?: string }) => { + const disallowedFields = getProp(params, 'disallowedFields', ''); + return `${property} must not contain: "${disallowedFields}"`; + }, + [PolicyKey.LeastCapitalLetters]: (property: string, params?: { numCaps?: number }) => { + const numCaps = getProp(params, 'numCaps', 0); + return `${property} must contain at least ${numCaps} capital ${plural(numCaps, 'letter')}`; + }, + [PolicyKey.LeastNumbers]: (property: string, params?: { numNums?: number }) => { + const numNums = getProp(params, 'numNums', 0); + return `${property} must contain at least ${numNums} numeric ${plural(numNums, 'value')}`; + }, + [PolicyKey.MatchRegexp]: (property: string) => `${property} has failed the "MATCH_REGEXP" policy`, + [PolicyKey.MaximumLength]: (property: string, params?: { maxLength?: number }) => { + const maxLength = getProp(params, 'maxLength', 0); + return `${property} must be at most ${maxLength} ${plural(maxLength, 'character')}`; + }, + [PolicyKey.MaximumNumber]: (property: string) => + `${property} has failed the "MAXIMUM_NUMBER_VALUE" policy`, + [PolicyKey.MinimumLength]: (property: string, params?: { minLength?: number }) => { + const minLength = getProp(params, 'minLength', 0); + return `${property} must be at least ${minLength} ${plural(minLength, 'character')}`; + }, + [PolicyKey.MinimumNumber]: (property: string) => + `${property} has failed the "MINIMUM_NUMBER_VALUE" policy`, + [PolicyKey.Required]: (property: string) => `${property} is required`, + [PolicyKey.Unique]: (property: string) => `${property} must be unique`, + [PolicyKey.UnknownPolicy]: (property: string, params?: { policyRequirement?: string }) => { + const policyRequirement = getProp(params, 'policyRequirement', 'Unknown'); + return `${property}: Unknown policy requirement "${policyRequirement}"`; + }, + [PolicyKey.ValidArrayItems]: (property: string) => + `${property} has failed the "VALID_ARRAY_ITEMS" policy`, + [PolicyKey.ValidDate]: (property: string) => `${property} has an invalid date`, + [PolicyKey.ValidEmailAddress]: (property: string) => `${property} has an invalid email address`, + [PolicyKey.ValidNameFormat]: (property: string) => `${property} has an invalid name format`, + [PolicyKey.ValidNumber]: (property: string) => `${property} has an invalid number`, + [PolicyKey.ValidPhoneFormat]: (property: string) => `${property} has an invalid phone number`, + [PolicyKey.ValidQueryFilter]: (property: string) => + `${property} has failed the "VALID_QUERY_FILTER" policy`, + [PolicyKey.ValidType]: (property: string) => `${property} has failed the "VALID_TYPE" policy`, +}; + +export default defaultMessageCreator; diff --git a/packages/journey-client/src/lib/fr-step.ts b/packages/journey-client/src/lib/fr-step.ts new file mode 100644 index 0000000000..f9bf6bbeeb --- /dev/null +++ b/packages/journey-client/src/lib/fr-step.ts @@ -0,0 +1,121 @@ +/* + * @forgerock/javascript-sdk + * + * fr-step.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { CallbackType } from './interfaces.js'; +import type { Callback, Step } from './interfaces.js'; +import type FRCallback from './callbacks/index.js'; +import type { FRCallbackFactory } from './callbacks/factory.js'; +import createCallback from './callbacks/factory.js'; +import { StepType } from './enums.js'; +import type { AuthResponse } from './interfaces.js'; + +/** + * Represents a single step of an authentication tree. + */ +class FRStep implements AuthResponse { + /** + * The type of step. + */ + public readonly type = StepType.Step; + + /** + * The callbacks contained in this step. + */ + public callbacks: FRCallback[] = []; + + /** + * @param payload The raw payload returned by OpenAM + * @param callbackFactory A function that returns am implementation of FRCallback + */ + constructor( + public payload: Step, + callbackFactory?: FRCallbackFactory, + ) { + if (payload.callbacks) { + this.callbacks = this.convertCallbacks(payload.callbacks, callbackFactory); + } + } + + /** + * Gets the first callback of the specified type in this step. + * + * @param type The type of callback to find. + */ + public getCallbackOfType(type: CallbackType): T { + const callbacks = this.getCallbacksOfType(type); + if (callbacks.length !== 1) { + throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`); + } + return callbacks[0]; + } + + /** + * Gets all callbacks of the specified type in this step. + * + * @param type The type of callback to find. + */ + public getCallbacksOfType(type: CallbackType): T[] { + return this.callbacks.filter((x) => x.getType() === type) as T[]; + } + + /** + * Sets the value of the first callback of the specified type in this step. + * + * @param type The type of callback to find. + * @param value The value to set for the callback. + */ + public setCallbackValue(type: CallbackType, value: unknown): void { + const callbacks = this.getCallbacksOfType(type); + if (callbacks.length !== 1) { + throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`); + } + callbacks[0].setInputValue(value); + } + + /** + * Gets the step's description. + */ + public getDescription(): string | undefined { + return this.payload.description; + } + + /** + * Gets the step's header. + */ + public getHeader(): string | undefined { + return this.payload.header; + } + + /** + * Gets the step's stage. + */ + public getStage(): string | undefined { + return this.payload.stage; + } + + private convertCallbacks( + callbacks: Callback[], + callbackFactory?: FRCallbackFactory, + ): FRCallback[] { + const converted = callbacks.map((x: Callback) => { + // This gives preference to the provided factory and falls back to our default implementation + return (callbackFactory || createCallback)(x) || createCallback(x); + }); + return converted; + } +} + +/** + * A function that can populate the provided authentication tree step. + */ +type FRStepHandler = (step: FRStep) => void; + +export default FRStep; +export type { FRStepHandler }; diff --git a/packages/journey-client/src/lib/interfaces.ts b/packages/journey-client/src/lib/interfaces.ts new file mode 100644 index 0000000000..cc60086f09 --- /dev/null +++ b/packages/journey-client/src/lib/interfaces.ts @@ -0,0 +1,157 @@ +/* + * @forgerock/javascript-sdk + * + * interfaces.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { StepType } from './enums.js'; + +/** + * Types of callbacks directly supported by the SDK. + * TODO: We should avoid enums. + */ +export enum CallbackType { + BooleanAttributeInputCallback = 'BooleanAttributeInputCallback', + ChoiceCallback = 'ChoiceCallback', + ConfirmationCallback = 'ConfirmationCallback', + DeviceProfileCallback = 'DeviceProfileCallback', + HiddenValueCallback = 'HiddenValueCallback', + KbaCreateCallback = 'KbaCreateCallback', + MetadataCallback = 'MetadataCallback', + NameCallback = 'NameCallback', + NumberAttributeInputCallback = 'NumberAttributeInputCallback', + PasswordCallback = 'PasswordCallback', + PingOneProtectEvaluationCallback = 'PingOneProtectEvaluationCallback', + PingOneProtectInitializeCallback = 'PingOneProtectInitializeCallback', + PollingWaitCallback = 'PollingWaitCallback', + ReCaptchaCallback = 'ReCaptchaCallback', + ReCaptchaEnterpriseCallback = 'ReCaptchaEnterpriseCallback', + RedirectCallback = 'RedirectCallback', + SelectIdPCallback = 'SelectIdPCallback', + StringAttributeInputCallback = 'StringAttributeInputCallback', + SuspendedTextOutputCallback = 'SuspendedTextOutputCallback', + TermsAndConditionsCallback = 'TermsAndConditionsCallback', + TextInputCallback = 'TextInputCallback', + TextOutputCallback = 'TextOutputCallback', + ValidatedCreatePasswordCallback = 'ValidatedCreatePasswordCallback', + ValidatedCreateUsernameCallback = 'ValidatedCreateUsernameCallback', +} +/** + * Base interface for all types of authentication step responses. + */ +interface AuthResponse { + type: StepType; +} + +/** + * Represents details of a failure in an authentication step. + */ +interface FailureDetail { + failureUrl?: string; +} + +/** + * Represents the authentication tree API payload schema. + */ +interface Step { + authId?: string; + callbacks?: Callback[]; + code?: number; + description?: string; + detail?: StepDetail; + header?: string; + message?: string; + ok?: string; + realm?: string; + reason?: string; + stage?: string; + status?: number; + successUrl?: string; + tokenId?: string; +} + +/** + * Represents details of a failure in an authentication step. + */ +interface StepDetail { + failedPolicyRequirements?: FailedPolicyRequirement[]; + failureUrl?: string; + result?: boolean; +} + +/** + * Represents failed policies for a matching property. + */ +interface FailedPolicyRequirement { + policyRequirements: PolicyRequirement[]; + property: string; +} + +/** + * Represents a failed policy policy and failed policy params. + */ +interface PolicyRequirement { + params?: Partial; + policyRequirement: string; +} + +interface PolicyParams { + [key: string]: unknown; + disallowedFields: string; + duplicateValue: string; + forbiddenChars: string; + maxLength: number; + minLength: number; + numCaps: number; + numNums: number; +} + +/** + * Represents the authentication tree API callback schema. + */ +interface Callback { + _id?: number; + input?: NameValue[]; + output: NameValue[]; + type: CallbackType; +} + +/** + * Represents a name/value pair found in an authentication tree callback. + */ +interface NameValue { + name: string; + value: unknown; +} + +type ConfigurablePaths = keyof CustomPathConfig; +/** + * Optional configuration for custom paths for actions + */ +interface CustomPathConfig { + authenticate?: string; + authorize?: string; + accessToken?: string; + endSession?: string; + userInfo?: string; + revoke?: string; + sessions?: string; +} + +export type { + CustomPathConfig, + ConfigurablePaths, + Callback, + FailedPolicyRequirement, + NameValue, + PolicyParams, + PolicyRequirement, + Step, + StepDetail, + AuthResponse, + FailureDetail, +}; diff --git a/packages/journey-client/src/lib/journey-client.ts b/packages/journey-client/src/lib/journey-client.ts new file mode 100644 index 0000000000..73a6b2c14d --- /dev/null +++ b/packages/journey-client/src/lib/journey-client.ts @@ -0,0 +1,196 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import Config, { StepOptions } from '../config.js'; +import Auth from '../auth/index.js'; +import { CallbackType } from './interfaces.js'; +import type RedirectCallback from './callbacks/redirect-callback.js'; +import FRLoginFailure from './fr-login-failure.js'; +import FRLoginSuccess from './fr-login-success.js'; +import FRStep from './fr-step.js'; + +/** + * Provides access to the OpenAM authentication tree API. + */ +abstract class FRAuth { + public static get previousStepKey() { + return `${Config.get().prefix}-PreviousStep`; + } + + /** + * Requests the next step in the authentication tree. + * + * Call `FRAuth.next()` recursively. At each step, check for session token or error, otherwise + * populate the step's callbacks and call `next()` again. + * + * Example: + * + * ```js + * async function nextStep(previousStep) { + * const thisStep = await FRAuth.next(previousStep); + * + * switch (thisStep.type) { + * case StepType.LoginSuccess: + * const token = thisStep.getSessionToken(); + * break; + * case StepType.LoginFailure: + * const detail = thisStep.getDetail(); + * break; + * case StepType.Step: + * // Populate `thisStep` callbacks here, and then continue + * thisStep.setInputValue('foo'); + * nextStep(thisStep); + * break; + * } + * } + * ``` + * + * @param previousStep The previous step with its callback values populated + * @param options Configuration overrides + * @return The next step in the authentication tree + */ + public static async next( + previousStep?: FRStep, + options?: StepOptions, + ): Promise { + const nextPayload = await Auth.next(previousStep ? previousStep.payload : undefined, options); + + if (nextPayload.authId) { + // If there's an authId, tree has not been completed + const callbackFactory = options ? options.callbackFactory : undefined; + return new FRStep(nextPayload, callbackFactory); + } + + if (!nextPayload.authId && nextPayload.ok) { + // If there's no authId, and the response is OK, tree is complete + return new FRLoginSuccess(nextPayload); + } + + // If there's no authId, and the response is not OK, tree has failure + return new FRLoginFailure(nextPayload); + } + + /** + * Redirects to the URL identified in the RedirectCallback and saves the full + * step information to localStorage for retrieval when user returns from login. + * + * Example: + * ```js + * forgerock.FRAuth.redirect(step); + * ``` + */ + public static redirect(step: FRStep): void { + const cb = step.getCallbackOfType(CallbackType.RedirectCallback) as RedirectCallback; + const redirectUrl = cb.getRedirectUrl(); + + localStorage.setItem(this.previousStepKey, JSON.stringify(step)); + location.assign(redirectUrl); + } + + /** + * Resumes a tree after returning from an external client or provider. + * Requires the full URL of the current window. It will parse URL for + * key-value pairs as well as, if required, retrieves previous step. + * + * Example; + * ```js + * forgerock.FRAuth.resume(window.location.href) + * ``` + */ + public static async resume( + url: string, + options?: StepOptions, + ): Promise { + const parsedUrl = new URL(url); + const code = parsedUrl.searchParams.get('code'); + const error = parsedUrl.searchParams.get('error'); + const errorCode = parsedUrl.searchParams.get('errorCode'); + const errorMessage = parsedUrl.searchParams.get('errorMessage'); + const form_post_entry = parsedUrl.searchParams.get('form_post_entry'); + const nonce = parsedUrl.searchParams.get('nonce'); + const RelayState = parsedUrl.searchParams.get('RelayState'); + const responsekey = parsedUrl.searchParams.get('responsekey'); + const scope = parsedUrl.searchParams.get('scope'); + const state = parsedUrl.searchParams.get('state'); + const suspendedId = parsedUrl.searchParams.get('suspendedId'); + const authIndexValue = parsedUrl.searchParams.get('authIndexValue') ?? undefined; + + let previousStep; + + function requiresPreviousStep() { + return (code && state) || form_post_entry || responsekey; + } + + /** + * If we are returning back from a provider, the previous redirect step data is required. + * Retrieve the previous step from localStorage, and then delete it to remove stale data. + * If suspendedId is present, no previous step data is needed, so skip below conditional. + */ + if (requiresPreviousStep()) { + const redirectStepString = localStorage.getItem(this.previousStepKey); + + if (!redirectStepString) { + throw new Error('Error: could not retrieve original redirect information.'); + } + + try { + previousStep = JSON.parse(redirectStepString); + } catch (err) { + throw new Error('Error: could not parse redirect params or step information'); + } + + localStorage.removeItem(this.previousStepKey); + } + + /** + * Construct options object from the options parameter and key-value pairs from URL. + * Ensure query parameters from current URL are the last properties spread in the object. + */ + const nextOptions = { + ...options, + query: { + // Conditionally spread properties into object. Don't spread props with undefined/null. + ...(code && { code }), + ...(error && { error }), + ...(errorCode && { errorCode }), + ...(errorMessage && { errorMessage }), + ...(form_post_entry && { form_post_entry }), + ...(nonce && { nonce }), + ...(RelayState && { RelayState }), + ...(responsekey && { responsekey }), + ...(scope && { scope }), + ...(state && { state }), + ...(suspendedId && { suspendedId }), + // Allow developer to add or override params with their own. + ...(options && options.query), + }, + ...((options?.tree ?? authIndexValue) && { + tree: options?.tree ?? authIndexValue, + }), + }; + + return await this.next(previousStep, nextOptions); + } + + /** + * Requests the first step in the authentication tree. + * This is essentially an alias to calling FRAuth.next without a previous step. + * + * @param options Configuration overrides + * @return The next step in the authentication tree + */ + public static async start( + options?: StepOptions, + ): Promise { + return await FRAuth.next(undefined, options); + } +} + +export default FRAuth; diff --git a/packages/journey-client/src/lib/shared/constants.ts b/packages/journey-client/src/lib/shared/constants.ts new file mode 100644 index 0000000000..7a59f819e4 --- /dev/null +++ b/packages/journey-client/src/lib/shared/constants.ts @@ -0,0 +1,4 @@ +const REQUESTED_WITH = 'forgerock-sdk'; +const X_REQUESTED_PLATFORM = 'javascript'; + +export { REQUESTED_WITH, X_REQUESTED_PLATFORM }; diff --git a/packages/journey-client/src/lib/utils/strings.ts b/packages/journey-client/src/lib/utils/strings.ts new file mode 100644 index 0000000000..92c0fed974 --- /dev/null +++ b/packages/journey-client/src/lib/utils/strings.ts @@ -0,0 +1,21 @@ +/* + * @forgerock/javascript-sdk + * + * strings.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * @module + * @ignore + * These are private utility functions + */ +export function plural(n: number, singularText: string, pluralText?: string): string { + if (n === 1) { + return singularText; + } + return pluralText !== undefined ? pluralText : singularText + 's'; +} diff --git a/packages/journey-client/src/lib/utils/timeout.test.ts b/packages/journey-client/src/lib/utils/timeout.test.ts new file mode 100644 index 0000000000..ea623bca5c --- /dev/null +++ b/packages/journey-client/src/lib/utils/timeout.test.ts @@ -0,0 +1,27 @@ +/* + * @forgerock/javascript-sdk + * + * timeout.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { withTimeout } from './timeout.js'; + +describe('withTimeout function', () => { + it('should return the promise passed', async () => { + const promise = new Promise((res) => res('ok')); + const result = await withTimeout(promise, 500); + expect(result).toBe('ok'); + }); + it('should return the promise passed if it rejects', async () => { + const promise = new Promise((_, rej) => rej('rejected')); + expect(withTimeout(promise, 500)).rejects.toBe('rejected'); + }); + it('should return the window timeout', async () => { + const promise = new Promise(() => 'ok'); + await withTimeout(promise, 1).catch((res) => expect(res).toEqual(new Error('Timeout'))); + }); +}); diff --git a/packages/journey-client/src/lib/utils/timeout.ts b/packages/journey-client/src/lib/utils/timeout.ts new file mode 100644 index 0000000000..9132693897 --- /dev/null +++ b/packages/journey-client/src/lib/utils/timeout.ts @@ -0,0 +1,27 @@ +/* + * @forgerock/javascript-sdk + * + * timeout.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +const DEFAULT_TIMEOUT = 5 * 1000; + +/** + * @module + * @ignore + * These are private utility functions + */ +function withTimeout(promise: Promise, timeout: number = DEFAULT_TIMEOUT): Promise { + const effectiveTimeout = timeout || DEFAULT_TIMEOUT; + const timeoutP = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), effectiveTimeout), + ); + + return Promise.race([promise, timeoutP]); +} + +export { withTimeout }; diff --git a/packages/journey-client/src/lib/utils/url.ts b/packages/journey-client/src/lib/utils/url.ts new file mode 100644 index 0000000000..e5e2a2d70d --- /dev/null +++ b/packages/journey-client/src/lib/utils/url.ts @@ -0,0 +1,84 @@ +/* + * @forgerock/javascript-sdk + * + * url.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { getRealmUrlPath } from '@forgerock/sdk-utilities'; +import type { ConfigurablePaths, CustomPathConfig } from '../interfaces.js'; + +/** + * Returns the base URL including protocol, hostname and any non-standard port. + * The returned URL does not include a trailing slash. + */ +function getBaseUrl(url: URL): string { + const isNonStandardPort = + (url.protocol === 'http:' && ['', '80'].indexOf(url.port) === -1) || + (url.protocol === 'https:' && ['', '443'].indexOf(url.port) === -1); + const port = isNonStandardPort ? `:${url.port}` : ''; + const baseUrl = `${url.protocol}//${url.hostname}${port}`; + return baseUrl; +} + +function getEndpointPath( + endpoint: ConfigurablePaths, + realmPath?: string, + customPaths?: CustomPathConfig, +): string { + const realmUrlPath = getRealmUrlPath(realmPath); + const defaultPaths = { + authenticate: `json/${realmUrlPath}/authenticate`, + authorize: `oauth2/${realmUrlPath}/authorize`, + accessToken: `oauth2/${realmUrlPath}/access_token`, + endSession: `oauth2/${realmUrlPath}/connect/endSession`, + userInfo: `oauth2/${realmUrlPath}/userinfo`, + revoke: `oauth2/${realmUrlPath}/token/revoke`, + sessions: `json/${realmUrlPath}/sessions/`, + }; + if (customPaths && customPaths[endpoint]) { + // TypeScript is not correctly reading the condition above + // It's thinking that customPaths[endpoint] may result in undefined + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return customPaths[endpoint]; + } else { + return defaultPaths[endpoint]; + } +} + +function resolve(baseUrl: string, path: string): string { + const url = new URL(baseUrl); + + if (path.startsWith('/')) { + return `${getBaseUrl(url)}${path}`; + } + + const basePath = url.pathname.split('/'); + const destPath = path.split('/').filter((x) => !!x); + const newPath = [...basePath.slice(0, -1), ...destPath].join('/'); + + return `${getBaseUrl(url)}${newPath}`; +} + +function parseQuery(fullUrl: string): Record { + const url = new URL(fullUrl); + const query: Record = {}; + url.searchParams.forEach((v, k) => (query[k] = v)); + return query; +} + +function stringify(data: Record): string { + const pairs: string[] = []; + for (const k in data) { + if (data[k]) { + pairs.push(k + '=' + encodeURIComponent(data[k] as string)); + } + } + return pairs.join('&'); +} + +export { getBaseUrl, getEndpointPath, parseQuery, resolve, stringify }; diff --git a/packages/journey-client/tsconfig.json b/packages/journey-client/tsconfig.json new file mode 100644 index 0000000000..b66329384c --- /dev/null +++ b/packages/journey-client/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "../sdk-effects/sdk-request-middleware" + }, + { + "path": "../sdk-utilities" + }, + { + "path": "../sdk-types" + }, + { + "path": "../sdk-effects/logger" + }, + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/journey-client/tsconfig.lib.json b/packages/journey-client/tsconfig.lib.json new file mode 100644 index 0000000000..3b50b0ca7b --- /dev/null +++ b/packages/journey-client/tsconfig.lib.json @@ -0,0 +1,35 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"], + "references": [ + { + "path": "../sdk-effects/sdk-request-middleware/tsconfig.lib.json" + }, + { + "path": "../sdk-utilities/tsconfig.lib.json" + }, + { + "path": "../sdk-types/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/logger/tsconfig.lib.json" + } + ] +} diff --git a/packages/journey-client/tsconfig.spec.json b/packages/journey-client/tsconfig.spec.json new file mode 100644 index 0000000000..c8fc6b21fa --- /dev/null +++ b/packages/journey-client/tsconfig.spec.json @@ -0,0 +1,41 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "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" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/journey-client/vite.config.ts b/packages/journey-client/vite.config.ts new file mode 100644 index 0000000000..a6b026895e --- /dev/null +++ b/packages/journey-client/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/journey-client', + plugins: [], + test: { + watch: false, + globals: true, + passWithNoTests: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + }, + }, +})); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4dcee8beca..0da90a76f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -382,6 +382,24 @@ importers: specifier: 'catalog:' version: 2.10.4(@types/node@22.14.1)(typescript@5.8.3) + packages/journey-client: + dependencies: + '@forgerock/sdk-logger': + specifier: workspace:* + version: link:../sdk-effects/logger + '@forgerock/sdk-request-middleware': + specifier: workspace:* + version: link:../sdk-effects/sdk-request-middleware + '@forgerock/sdk-types': + specifier: workspace:* + version: link:../sdk-types + '@forgerock/sdk-utilities': + specifier: workspace:* + version: link:../sdk-utilities + tslib: + specifier: ^2.3.0 + version: 2.8.1 + packages/oidc-client: dependencies: '@forgerock/iframe-manager': diff --git a/tsconfig.json b/tsconfig.json index 7dbd2aeb74..b5d57bda62 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -66,6 +66,9 @@ }, { "path": "./tools/user-scripts" + }, + { + "path": "./packages/journey-client" } ] } From b0f4368637a788c5472587f5232678312a7eabfe Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 15 Sep 2025 14:21:01 -0600 Subject: [PATCH 02/11] feat: refactor-config feat(journey-client): introduce robust tests and fix type errors Overhauls the testing strategy for the new journey client by replacing the heavily mocked original test file with a new suite that performs more of an integration-style test against the Redux store. - Rewrites tests to validate the full data flow, mocking only external boundaries (fetch, storage). - Systematically debugs and resolves multiple layers of TypeScript errors by correcting mock data shapes to align with actual types. - Introduces a GEMINI.md file to document architecture, development workflows, and troubleshooting steps for the package. - Moves the getProp utility function from @forgerock/journey-client/src/lib/fr-policy/helpers.ts to @forgerock/sdk-utilities/src/lib/object.utils.ts. This centralizes a generic utility function, improving reusability and reducing code duplication across the SDK. - Updates imports in journey-client to reflect the new location. - Removed redundant cross-package references from `packages/journey-client/tsconfig.json`, keeping only the references to `tsconfig.lib.json` and `tsconfig.spec.json`. These references are already handled by the respective `tsconfig.json` files, simplifying the configuration and avoiding potential conflicts. - Inlined the export of `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` constants in `packages/sdk-utilities/src/lib/constants/index.ts`. This change leverages TypeScript's literal types for improved type narrowing and enables better tree-shaking during bundling, optimizing the SDK's footprint. - Storing the plain `Step` payload instead of the `FRStep` instance in session storage. - Updating type guards (`isStoredStep`) to correctly handle plain `Step` objects. - Making the `redirect` method asynchronous and awaiting storage operations. - Adding null/undefined checks for `RedirectCallback` in the `redirect` method. - Ensuring `stepStorage.remove()` is awaited to prevent race conditions. - Updating the `resume` method to correctly use the retrieved `Step` payload. - Adding a new test case to verify `resume` functionality with plain `Step` objects from storage. - Introduces new capabilities to the `@forgerock/journey-client` for handling advanced authentication methods: - **WebAuthn:** Adds support for Web Authentication (WebAuthn) flows, including parsing WebAuthn registration and authentication options. This includes a fix for a TypeScript type incompatibility where `ParsedCredential.id` was narrowed to `ArrayBuffer` to align with the WebAuthn API's `BufferSource` expectation. - **QR Code:** Implements functionality for QR code-based authentication flows. - **Recovery Codes:** Adds support for managing and utilizing recovery codes. - Increases project and patch coverage targets. - Sets higher, more stringent targets for all packages. - Requires successful CI builds for Codecov processing. - Improves PR comment layout by including coverage flags. - Expands the ignore list to exclude non-source directories. --- .changeset/breezy-actors-sell.md | 8 + .changeset/bright-lights-yawn.md | 7 + .changeset/good-games-accept.md | 8 + .changeset/kind-guests-sneeze.md | 11 + .changeset/orange-peaches-warn.md | 11 + .changeset/tender-schools-scream.md | 8 + .changeset/wild-items-stop.md | 8 + .coderabbit.yaml | 7 + .gitignore | 4 + .whitesource | 2 +- CLAUDE.md | 91 --- codecov.yml | 65 +- e2e/mock-api-v2/GEMINI.md | 0 nx.json | 5 +- package.json | 2 +- packages/journey-client/README.md | 142 +++- packages/journey-client/eslint.config.mjs | 2 +- packages/journey-client/package.json | 39 +- packages/journey-client/project.json | 41 - packages/journey-client/src/lib/auth/index.ts | 109 --- .../attribute-input-callback.test.ts | 14 +- .../lib/callbacks/attribute-input-callback.ts | 23 +- .../src/lib/callbacks/choice-callback.test.ts | 74 ++ .../src/lib/callbacks/choice-callback.ts | 6 +- .../callbacks/confirmation-callback.test.ts | 91 +++ .../lib/callbacks/confirmation-callback.ts | 6 +- .../callbacks/device-profile-callback.test.ts | 65 ++ .../lib/callbacks/device-profile-callback.ts | 6 +- .../src/lib/callbacks/factory.test.ts | 86 +++ .../src/lib/callbacks/factory.ts | 57 +- .../lib/callbacks/fr-auth-callback.test.ts | 10 +- .../callbacks/hidden-value-callback.test.ts | 23 + .../lib/callbacks/hidden-value-callback.ts | 6 +- .../journey-client/src/lib/callbacks/index.ts | 8 +- .../lib/callbacks/kba-create-callback.test.ts | 50 ++ .../src/lib/callbacks/kba-create-callback.ts | 6 +- .../lib/callbacks/metadata-callback.test.ts | 28 + .../src/lib/callbacks/metadata-callback.ts | 6 +- .../src/lib/callbacks/name-callback.test.ts | 39 + .../src/lib/callbacks/name-callback.ts | 6 +- .../lib/callbacks/password-callback.test.ts | 57 ++ .../src/lib/callbacks/password-callback.ts | 6 +- .../ping-protect-evaluation-callback.test.ts | 8 +- .../ping-protect-evaluation-callback.ts | 6 +- .../ping-protect-initialize-callback.test.ts | 6 +- .../ping-protect-initialize-callback.ts | 6 +- .../callbacks/polling-wait-callback.test.ts | 33 + .../lib/callbacks/polling-wait-callback.ts | 6 +- .../lib/callbacks/recaptcha-callback.test.ts | 39 + .../src/lib/callbacks/recaptcha-callback.ts | 6 +- .../recaptcha-enterprise-callback.test.ts | 8 +- .../recaptcha-enterprise-callback.ts | 19 +- .../lib/callbacks/redirect-callback.test.ts | 28 + .../src/lib/callbacks/redirect-callback.ts | 6 +- .../lib/callbacks/select-idp-callback.test.ts | 48 ++ .../src/lib/callbacks/select-idp-callback.ts | 6 +- .../suspended-text-output-callback.test.ts | 32 + .../suspended-text-output-callback.ts | 6 +- .../terms-and-conditions-callback.test.ts | 52 ++ .../terms-and-conditions-callback.ts | 6 +- .../lib/callbacks/text-input-callback.test.ts | 6 +- .../src/lib/callbacks/text-input-callback.ts | 6 +- .../callbacks/text-output-callback.test.ts | 33 + .../src/lib/callbacks/text-output-callback.ts | 6 +- ...validated-create-password-callback.test.ts | 10 +- .../validated-create-password-callback.ts | 9 +- ...validated-create-username-callback.test.ts | 10 +- .../validated-create-username-callback.ts | 9 +- .../journey-client/src/lib/config.types.ts | 15 + .../src/lib/fr-device/collector.ts | 53 -- .../src/lib/fr-device/device-profile.test.ts | 14 +- .../journey-client/src/lib/fr-device/index.ts | 56 +- .../src/lib/fr-device/sample-profile.json | 2 +- .../src/lib/fr-login-failure.ts | 9 +- .../src/lib/fr-login-success.ts | 9 +- .../src/lib/fr-policy/fr-policy.test.ts | 22 +- .../src/lib/fr-policy/helpers.ts | 18 - .../journey-client/src/lib/fr-policy/index.ts | 9 +- .../src/lib/fr-policy/interfaces.ts | 6 +- .../src/lib/fr-policy/message-creator.ts | 5 +- .../src/lib/fr-qrcode/fr-qr-code.mock.data.ts | 184 +++++ .../src/lib/fr-qrcode/fr-qrcode.test.ts | 78 ++ .../src/lib/fr-qrcode/fr-qrcode.ts | 96 +++ .../src/lib/fr-recovery-codes/index.ts | 72 ++ .../fr-recovery-codes/recovery-codes.test.ts | 43 ++ .../fr-recovery-codes/script-parser.test.ts | 34 + .../lib/fr-recovery-codes/script-parser.ts | 51 ++ .../script-text.mock.data.ts | 120 +++ .../journey-client/src/lib/fr-step.test.ts | 101 +++ packages/journey-client/src/lib/fr-step.ts | 16 +- .../src/lib/fr-webauthn/enums.ts | 36 + .../lib/fr-webauthn/fr-webauthn.mock.data.ts | 491 ++++++++++++ .../src/lib/fr-webauthn/fr-webauthn.test.ts | 107 +++ .../src/lib/fr-webauthn/helpers.mock.data.ts | 25 + .../src/lib/fr-webauthn/helpers.test.ts | 54 ++ .../src/lib/fr-webauthn/helpers.ts | 124 +++ .../src/lib/fr-webauthn/index.ts | 517 +++++++++++++ .../src/lib/fr-webauthn/interfaces.ts | 125 +++ .../src/lib/fr-webauthn/script-parser.test.ts | 123 +++ .../src/lib/fr-webauthn/script-parser.ts | 192 +++++ .../lib/fr-webauthn/script-text.mock.data.ts | 464 +++++++++++ packages/journey-client/src/lib/interfaces.ts | 154 +--- .../src/lib/journey-client.test.ts | 235 ++++++ .../journey-client/src/lib/journey-client.ts | 302 +++----- .../journey-client/src/lib/journey.api.ts | 131 ++++ .../journey-client/src/lib/journey.slice.ts | 31 + .../journey-client/src/lib/journey.store.ts | 46 ++ .../src/lib/shared/constants.ts | 4 - .../src/lib/utils/timeout.test.ts | 27 - .../journey-client/src/lib/utils/timeout.ts | 27 - packages/journey-client/tsconfig.json | 5 +- packages/journey-client/tsconfig.lib.json | 6 +- packages/journey-client/tsconfig.spec.json | 5 +- packages/journey-client/vite.config.ts | 10 +- packages/journey-client/vitest.setup.ts | 1 + .../iframe-manager/eslint.config.mjs | 2 +- packages/sdk-effects/logger/eslint.config.mjs | 2 +- .../sdk-effects/storage/eslint.config.mjs | 2 +- packages/sdk-types/eslint.config.mjs | 2 +- packages/sdk-types/src/index.ts | 2 + .../sdk-types/src/lib/am-callback.types.ts | 89 ++- .../src/lib/enums.ts | 0 .../sdk-types/src/lib/legacy-config.types.ts | 16 +- .../src/lib/policy.types.ts} | 2 +- packages/sdk-utilities/eslint.config.mjs | 2 +- packages/sdk-utilities/package.json | 11 +- packages/sdk-utilities/src/index.ts | 3 + .../sdk-utilities/src/lib/constants/index.ts | 2 + .../sdk-utilities/src/lib/object.utils.ts | 56 ++ .../sdk-utilities/src/lib/strings/index.ts | 8 + .../src/lib/strings/strings.utils.ts} | 0 packages/sdk-utilities/src/lib/url/index.ts | 1 + .../src/lib/url/url.utils.ts} | 38 +- packages/sdk-utilities/tsconfig.lib.json | 15 +- pnpm-lock.yaml | 730 +++++++++++++++++- scratchpad/package.json | 2 +- scratchpad/src/main.ts | 52 +- scratchpad/tsconfig.json | 5 +- 138 files changed, 5818 insertions(+), 1132 deletions(-) create mode 100644 .changeset/breezy-actors-sell.md create mode 100644 .changeset/bright-lights-yawn.md create mode 100644 .changeset/good-games-accept.md create mode 100644 .changeset/kind-guests-sneeze.md create mode 100644 .changeset/orange-peaches-warn.md create mode 100644 .changeset/tender-schools-scream.md create mode 100644 .changeset/wild-items-stop.md create mode 100644 .coderabbit.yaml delete mode 100644 CLAUDE.md delete mode 100644 e2e/mock-api-v2/GEMINI.md delete mode 100644 packages/journey-client/project.json delete mode 100644 packages/journey-client/src/lib/auth/index.ts create mode 100644 packages/journey-client/src/lib/callbacks/choice-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/factory.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/metadata-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/name-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/password-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/redirect-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts create mode 100644 packages/journey-client/src/lib/callbacks/text-output-callback.test.ts create mode 100644 packages/journey-client/src/lib/config.types.ts delete mode 100644 packages/journey-client/src/lib/fr-device/collector.ts delete mode 100644 packages/journey-client/src/lib/fr-policy/helpers.ts create mode 100644 packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts create mode 100644 packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts create mode 100644 packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts create mode 100644 packages/journey-client/src/lib/fr-recovery-codes/index.ts create mode 100644 packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts create mode 100644 packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts create mode 100644 packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts create mode 100644 packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts create mode 100644 packages/journey-client/src/lib/fr-step.test.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/enums.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/helpers.test.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/helpers.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/index.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/interfaces.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/script-parser.ts create mode 100644 packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts create mode 100644 packages/journey-client/src/lib/journey-client.test.ts create mode 100644 packages/journey-client/src/lib/journey.api.ts create mode 100644 packages/journey-client/src/lib/journey.slice.ts create mode 100644 packages/journey-client/src/lib/journey.store.ts delete mode 100644 packages/journey-client/src/lib/shared/constants.ts delete mode 100644 packages/journey-client/src/lib/utils/timeout.test.ts delete mode 100644 packages/journey-client/src/lib/utils/timeout.ts create mode 100644 packages/journey-client/vitest.setup.ts rename packages/{journey-client => sdk-types}/src/lib/enums.ts (100%) rename packages/{journey-client/src/lib/fr-policy/enums.ts => sdk-types/src/lib/policy.types.ts} (98%) create mode 100644 packages/sdk-utilities/src/lib/constants/index.ts create mode 100644 packages/sdk-utilities/src/lib/object.utils.ts create mode 100644 packages/sdk-utilities/src/lib/strings/index.ts rename packages/{journey-client/src/lib/utils/strings.ts => sdk-utilities/src/lib/strings/strings.utils.ts} (100%) rename packages/{journey-client/src/lib/utils/url.ts => sdk-utilities/src/lib/url/url.utils.ts} (56%) diff --git a/.changeset/breezy-actors-sell.md b/.changeset/breezy-actors-sell.md new file mode 100644 index 0000000000..a90b0953bd --- /dev/null +++ b/.changeset/breezy-actors-sell.md @@ -0,0 +1,8 @@ +--- +'@forgerock/storage': minor +--- + +feat: Update storage package + +- Updated ESLint configurations for consistent code style and linting rules. +- Ensured compatibility with `verbatimModuleSyntax` by correcting type-only imports and module exports. diff --git a/.changeset/bright-lights-yawn.md b/.changeset/bright-lights-yawn.md new file mode 100644 index 0000000000..eb1948b05d --- /dev/null +++ b/.changeset/bright-lights-yawn.md @@ -0,0 +1,7 @@ +--- +'@forgerock/sdk-utilities': minor +--- + +feat: Update SDK utilities + +- Inlined `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` constants with literal types for better tree-shaking and type narrowing. diff --git a/.changeset/good-games-accept.md b/.changeset/good-games-accept.md new file mode 100644 index 0000000000..c4d31c9a65 --- /dev/null +++ b/.changeset/good-games-accept.md @@ -0,0 +1,8 @@ +--- +'@forgerock/iframe-manager': minor +--- + +feat: Update iframe-manager + +- Updated ESLint configurations for consistent code style and linting rules. +- Ensured compatibility with `verbatimModuleSyntax` by correcting type-only imports and module exports. diff --git a/.changeset/kind-guests-sneeze.md b/.changeset/kind-guests-sneeze.md new file mode 100644 index 0000000000..127bbb8bbc --- /dev/null +++ b/.changeset/kind-guests-sneeze.md @@ -0,0 +1,11 @@ +--- +'@forgerock/journey-client': minor +--- + +feat: Implement new journey client + +- Implemented a new `journey()` factory function for creating stateful client instances. +- Integrated Redux Toolkit and RTK Query for robust state management and API interactions. +- Refactored `resume` logic to correctly persist and retrieve plain `Step` payloads, resolving prototype loss issues during serialization. +- Improved error handling and type safety within the client. +- Updated internal callback handling and device profiling integration. diff --git a/.changeset/orange-peaches-warn.md b/.changeset/orange-peaches-warn.md new file mode 100644 index 0000000000..cd1cd2aa25 --- /dev/null +++ b/.changeset/orange-peaches-warn.md @@ -0,0 +1,11 @@ +--- +'@forgerock/journey-client': minor +--- + +feat(journey-client): Add WebAuthn, QR Code, and Recovery Code support + +- Introduces new utility modules (`FRWebAuthn`, `FRQRCode`, `FRRecoveryCodes`) to handle advanced authentication methods within authentication journeys. +- Adds comprehensive parsing and handling for WebAuthn registration and authentication steps, including a fix for a type error where `TextOutputCallback` was being incorrectly inferred as `TextInputCallback`. +- Implements support for displaying QR codes (for both OTP and Push) and for displaying and using recovery codes. +- Includes extensive unit tests for the new callback types and utility modules to ensure correctness. +- Updates documentation to reflect the new capabilities and architectural changes. diff --git a/.changeset/tender-schools-scream.md b/.changeset/tender-schools-scream.md new file mode 100644 index 0000000000..6857437f0c --- /dev/null +++ b/.changeset/tender-schools-scream.md @@ -0,0 +1,8 @@ +--- +'@forgerock/sdk-types': minor +--- + +feat: Update SDK types + +- Updated ESLint configurations for consistent code style and linting rules. +- Ensured compatibility with `verbatimModuleSyntax` by correcting type-only imports and module exports. diff --git a/.changeset/wild-items-stop.md b/.changeset/wild-items-stop.md new file mode 100644 index 0000000000..43e8e413a3 --- /dev/null +++ b/.changeset/wild-items-stop.md @@ -0,0 +1,8 @@ +--- +'@forgerock/sdk-logger': minor +--- + +feat: Update SDK logger + +- Updated ESLint configurations for consistent code style and linting rules. +- Ensured compatibility with `verbatimModuleSyntax` by correcting type-only imports and module exports. diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000000..624d2e247d --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,7 @@ +reviews: + profile: chill + poem: false + # You can specify coding guideline documents here. + # CodeRabbit will scan these files to understand your team’s standards. + # guidelines: + # - "**/GEMINI.md" diff --git a/.gitignore b/.gitignore index 09f2485ffe..982be8b22b 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,7 @@ packages/davinci-client/src/lib/mock-data/*.d.ts.map test-output .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md + +# Gemini local knowledge base files +GEMINI.md +**/GEMINI.md \ No newline at end of file diff --git a/.whitesource b/.whitesource index 95a879f280..78465e3590 100644 --- a/.whitesource +++ b/.whitesource @@ -15,7 +15,7 @@ "enableScan": true, "scanPullRequests": true, "incrementalScan": false, - "baseBranches": ["integration"] + "baseBranches": ["main"] }, "checkRunSettingsSAST": { "checkRunConclusionLevel": "failure", diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index ac4236760e..0000000000 --- a/CLAUDE.md +++ /dev/null @@ -1,91 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is the Ping JavaScript SDK - a monorepo containing multiple packages for web applications integrating with the Ping platform. The SDK provides APIs for user authentication, device management, and accessing Ping-secured resources. - -## Development Commands - -### Core Commands - -- `pnpm build` - Build all affected packages -- `pnpm test` - Run tests for all affected packages -- `pnpm lint` - Lint all affected packages -- `pnpm format` - Format code using Prettier -- `pnpm nx typecheck` - Run TypeScript type checking - -### Package Management - -- `pnpm create-package` - Generate a new library package using Nx -- `pnpm nx serve ` - Serve a specific package in development -- `pnpm nx test --watch` - Run tests for a specific package in watch mode - -### E2E Testing - -- `pnpm test:e2e` - Run end-to-end tests for affected packages -- Individual e2e apps are in `e2e/` directory with their own test suites - -## Architecture - -### Monorepo Structure - -The repository uses **Nx** as the monorepo tool with the following structure: - -``` -packages/ -├── davinci-client/ # DaVinci flow orchestration client -├── device-client/ # Device management (binding, profiles, WebAuthn) -├── oidc-client/ # OpenID Connect authentication client -├── protect/ # Ping Protect fraud detection -├── sdk-effects/ # Effect-based utilities (logger, storage, etc.) -│ ├── iframe-manager/ -│ ├── logger/ -│ ├── oidc/ -│ ├── sdk-request-middleware/ -│ └── storage/ -├── sdk-types/ # Shared TypeScript types -└── sdk-utilities/ # Common utilities (PKCE, URL handling) -``` - -### Key Packages - -- **davinci-client**: State management for DaVinci authentication flows using Redux Toolkit -- **oidc-client**: OIDC authentication with token management and storage -- **device-client**: Device binding, profiles, OATH, Push, and WebAuthn capabilities -- **protect**: Fraud detection and risk assessment integration -- **sdk-effects**: Effect-based architecture packages for common functionalities - -### Technology Stack - -- **Package Manager**: pnpm with workspace configuration -- **Build Tool**: Vite for building and bundling -- **Testing**: Vitest for unit tests, Playwright for e2e tests -- **State Management**: Redux Toolkit (in davinci-client) -- **TypeScript**: Strict typing with composite project configuration -- **Linting**: ESLint with Prettier integration - -### Nx Configuration - -- Uses target defaults for build, test, lint, and typecheck -- Caching enabled for build and test targets -- Workspace layout configured for packages and e2e apps -- Parallel execution limited to 1 for consistency - -### Package Dependencies - -Packages use workspace references (`workspace:*`) for internal dependencies and catalog references (`catalog:`) for shared external dependencies like `@reduxjs/toolkit`. - -### Testing Strategy - -- Unit tests: Vitest with coverage reporting -- E2E tests: Playwright with dedicated test apps in `e2e/` directory -- Mock API server available in `e2e/mock-api-v2/` for testing - -## Development Notes - -- All packages are built as ES modules with TypeScript declarations -- Use `pnpm nx affected` commands to run tasks only on changed packages -- The repository uses conventional commits and automated releases via changesets -- Individual packages can be tested independently using their specific build/test scripts diff --git a/codecov.yml b/codecov.yml index e0e409adb9..04660c41a9 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,23 +1,36 @@ +# A more comprehensive Codecov configuration codecov: - require_ci_to_pass: false + # Require CI to pass before Codecov processes reports. This ensures that + # coverage is only measured on successful builds. + require_ci_to_pass: true notify: - wait_for_ci: false + # Wait for CI to finish before sending notifications. + wait_for_ci: true + coverage: + # Set a higher default target for project and patch coverage. + # This encourages a better testing culture. status: project: default: - target: 40% - threshold: 1% + target: 70% + threshold: 2% # Allow for a small drop in coverage patch: default: - target: 40% - threshold: 1% + target: 80% # New code should be well-tested + threshold: 0% # Do not allow new code to decrease coverage + parsers: v1: include_full_missed_files: true + + # Configure how coverage is displayed. range: '70...100' round: down precision: 2 + +# Ignore files that are not relevant for code coverage. +# This keeps the coverage report clean and focused on the source code. ignore: - '**/dist/**/*' - '**/*.mock*' @@ -35,33 +48,39 @@ ignore: - '**/*.md' - '**/LICENSE' - '**/*.json' + - 'e2e/**/*' # E2E tests are not unit tests + - 'tools/**/*' # Tooling scripts + - 'scripts/**/*' # Other scripts +# Configure the comment that Codecov posts on pull requests. comment: - layout: 'header, diff, files' + layout: 'header, diff, flags, files' behavior: default - require_changes: false - require_base: false - require_head: true - hide_project_coverage: false - -bundle_analysis: - status: 'informational' + require_changes: false # Post a comment even if there are no coverage changes + require_head: true # Only post a comment if there is a coverage report for the head commit +# Configure how flags are managed. This allows for different coverage +# targets for different parts of the codebase. flag_management: default_rules: + # Carry forward coverage from previous commits for all flags. carryforward: true - statuses: - - type: project - target: 40% - - type: patch - target: 40% individual_flags: + # This rule applies to all flags that start with "package-". + # It is expected that the CI script uploads coverage reports with flags + # like "package-davinci-client", "package-oidc-client", etc. + # This makes the configuration future-proof for new packages. - name: package-* paths: - - packages/*/ - carryforward: true + - packages/ # Only consider files in the packages directory for these flags statuses: - type: project - target: 40% + target: 80% # Higher target for packages + threshold: 2% - type: patch - target: 40% + target: 90% # New code in packages should be very well-tested + threshold: 0% + +# Enable bundle analysis to track the size of the published packages. +bundle_analysis: + status: 'informational' diff --git a/e2e/mock-api-v2/GEMINI.md b/e2e/mock-api-v2/GEMINI.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/nx.json b/nx.json index 10c64004be..540f8f89fc 100644 --- a/nx.json +++ b/nx.json @@ -103,8 +103,7 @@ "configName": "tsconfig.lib.json" } }, - "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"], - "exclude": ["packages/journey-client/*"] + "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"] }, { "plugin": "@nx/playwright/plugin", @@ -137,7 +136,7 @@ }, { "plugin": "@nx/js/typescript", - "include": ["packages/journey-client/*"], + "include": ["packages/journey-client/**"], "options": { "typecheck": { "targetName": "typecheck" diff --git a/package.json b/package.json index e308c8bc4a..0c9f69883c 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "ci:release": "pnpm publish -r --no-git-checks && changeset tag", "ci:version": "changeset version && pnpm install --no-frozen-lockfile && pnpm nx format:write --uncommitted", "circular-dep-check": "madge --circular .", - "clean": "shx rm -rf ./{coverage,dist,docs,node_modules,tmp}/ ./{packages,e2e}/*/{dist,node_modules}/ && git clean -fX -e \"!.env*,nx-cloud.env\"", + "clean": "shx rm -rf ./{coverage,dist,docs,node_modules,tmp}/ ./{packages,e2e}/*/{dist,node_modules}/ && git clean -fX -e \"!.env*,nx-cloud.env\" -e \"!**/GEMINI.md\"", "commit": "git cz", "commitlint": "commitlint --edit", "create-package": "nx g @nx/js:library", diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md index 1efbddd040..d7e61d1188 100644 --- a/packages/journey-client/README.md +++ b/packages/journey-client/README.md @@ -1,7 +1,143 @@ -# journey-client +# @forgerock/journey-client -This library was generated with [Nx](https://nx.dev). +`@forgerock/journey-client` is a modern JavaScript client for interacting with Ping Identity's authentication journeys (formerly ForgeRock authentication trees). It provides a stateful, developer-friendly API that abstracts the complexities of the underlying authentication flow, making it easier to integrate with your applications. + +## Features + +- **Stateful Client**: Manages the authentication journey state internally, simplifying interaction compared to stateless approaches. +- **Redux Toolkit & RTK Query**: Built on robust and modern state management and data fetching libraries for predictable state and efficient API interactions. +- **Callback Handling**: Provides a structured way to interact with various authentication callbacks (e.g., username, password, MFA, device profiling). +- **Serializable Redux State**: Ensures the Redux store remains serializable by storing raw API payloads, with class instances created on demand. + +## Installation + +```bash +pnpm add @forgerock/journey-client +# or +npm install @forgerock/journey-client +# or +yarn add @forgerock/journey-client +``` + +## Usage + +The `journey-client` is initialized via an asynchronous factory function, `journey()`, which returns a client instance with methods to control the authentication flow. + +### Basic Authentication Flow + +```typescript +import { journey } from '@forgerock/journey-client'; +import { callbackType } from '@forgerock/sdk-types'; +import type { NameCallback, PasswordCallback } from '@forgerock/journey-client/src/lib/callbacks'; + +async function authenticateUser() { + const client = await journey({ + config: { + serverConfig: { baseUrl: 'https://your-am-instance.com' }, + realmPath: 'root', // e.g., 'root', 'alpha' + tree: 'Login', // The name of your authentication tree/journey + }, + }); + + try { + // 1. Start the authentication journey + let step = await client.start(); + + // 2. Handle callbacks in a loop until success or failure + while (step.type === 'Step') { + console.log('Current step:', step.payload); + + // Example: Handle NameCallback + if (step.getCallbacksOfType(callbackType.NameCallback).length > 0) { + const nameCallback = step.getCallbackOfType(callbackType.NameCallback); + console.log('Prompt for username:', nameCallback.getPrompt()); + nameCallback.setName('demo'); // Set the username + } + + // Example: Handle PasswordCallback + if (step.getCallbacksOfType(callbackType.PasswordCallback).length > 0) { + const passwordCallback = step.getCallbackOfType( + callbackType.PasswordCallback, + ); + console.log('Prompt for password:', passwordCallback.getPrompt()); + passwordCallback.setPassword('password'); // Set the password + } + + // ... handle other callback types as needed (e.g., ChoiceCallback, DeviceProfileCallback) + + // Submit the current step and get the next one + step = await client.next({ step: step.payload }); + } + + // 3. Check the final result + if (step.type === 'LoginSuccess') { + console.log('Login successful!', step.getSessionToken()); + // You can now use the session token for subsequent authenticated requests + } else if (step.type === 'LoginFailure') { + console.error('Login failed:', step.getMessage()); + // Display error message to the user + } else { + console.warn('Unexpected step type:', step.type, step.payload); + } + } catch (error) { + console.error('An error occurred during the authentication journey:', error); + // Handle network errors or other unexpected issues + } +} + +authenticateUser(); +``` + +### Client Methods + +The `journey()` factory function returns a client instance with the following methods: + +- `client.start(options?: StepOptions): Promise` + Initiates a new authentication journey. Returns the first `FRStep` in the journey. + +- `client.next(options: { step: Step; options?: StepOptions }): Promise` + Submits the current `Step` payload (obtained from `FRStep.payload`) to the authentication API and retrieves the next `FRStep` in the journey. + +- `client.redirect(step: FRStep): Promise` + Handles `RedirectCallback`s by storing the current step and redirecting the browser to the specified URL. This is typically used for external authentication providers. + +- `client.resume(url: string, options?: StepOptions): Promise` + Resumes an authentication journey after an external redirect (e.g., from an OAuth provider). It retrieves the previously stored step and combines it with URL parameters to continue the flow. + +### Handling Callbacks + +The `FRStep` object provides methods to easily access and manipulate callbacks: + +- `step.getCallbackOfType(type: CallbackType): T` + Retrieves a single callback of a specific type. Throws an error if zero or more than one callback of that type is found. + +- `step.getCallbacksOfType(type: CallbackType): T[]` + Retrieves all callbacks of a specific type as an array. + +- `callback.getPrompt(): string` (example for `NameCallback`, `PasswordCallback`) + Gets the prompt message for the callback. + +- `callback.setName(value: string): void` (example for `NameCallback`) + Sets the input value for the callback. + +- `callback.setPassword(value: string): void` (example for `PasswordCallback`) + Sets the input value for the callback. + +- `callback.setProfile(profile: DeviceProfileData): void` (example for `DeviceProfileCallback`) + Sets the device profile data for the callback. ## Building -Run `nx build journey-client` to build the library. +This library is part of an Nx monorepo. To build it, run: + +```bash +pnpm nx build @forgerock/journey-client +``` + +## Testing + +To run the unit tests for this package, run: + +```bash +pnpm nx test @forgerock/journey-client +``` diff --git a/packages/journey-client/eslint.config.mjs b/packages/journey-client/eslint.config.mjs index c334bc0bc0..a881271cb4 100644 --- a/packages/journey-client/eslint.config.mjs +++ b/packages/journey-client/eslint.config.mjs @@ -13,7 +13,7 @@ export default [ ], }, languageOptions: { - parser: await import('jsonc-eslint-parser'), + parser: (await import('jsonc-eslint-parser')).default, }, }, ]; diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json index d4949a6bdc..074af17955 100644 --- a/packages/journey-client/package.json +++ b/packages/journey-client/package.json @@ -3,17 +3,17 @@ "version": "0.0.1", "private": true, "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", "exports": { - "./package.json": "./package.json", ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" - } + }, + "./package.json": "./package.json" }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", "scripts": { "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint", @@ -22,9 +22,34 @@ }, "dependencies": { "@forgerock/sdk-logger": "workspace:*", + "@forgerock/sdk-request-middleware": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", - "@forgerock/sdk-request-middleware": "workspace:*", - "tslib": "^2.3.0" + "@forgerock/storage": "workspace:*", + "@reduxjs/toolkit": "catalog:", + "tslib": "^2.3.0", + "vite": "6.3.4", + "vitest-canvas-mock": "^0.3.3" + }, + "devDependencies": { + "@vitest/coverage-v8": "^1.2.0", + "vite": "6.3.4", + "vitest": "^1.2.0" + }, + "nx": { + "tags": ["scope:package"], + "implicitDependencies": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "packages/journey-client/dist", + "main": "packages/journey-client/src/index.ts", + "tsConfig": "packages/journey-client/tsconfig.lib.json", + "format": ["esm"] + } + } + } } } diff --git a/packages/journey-client/project.json b/packages/journey-client/project.json deleted file mode 100644 index 300ac05b3c..0000000000 --- a/packages/journey-client/project.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@forgerock/journey-client", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/journey-client/src", - "projectType": "library", - "targets": { - "build": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/packages/journey-client", - "tsConfig": "packages/journey-client/tsconfig.lib.json", - "packageJson": "packages/journey-client/package.json", - "main": "packages/journey-client/src/index.ts", - "assets": ["packages/journey-client/*.md"] - }, - "configurations": { - "production": { - "tsConfig": "packages/journey-client/tsconfig.lib.prod.json" - } - } - }, - "nxBuild": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/packages/journey-client", - "tsConfig": "packages/journey-client/tsconfig.lib.json", - "packageJson": "packages/journey-client/package.json", - "main": "packages/journey-client/src/index.ts", - "assets": ["packages/journey-client/*.md"] - }, - "configurations": { - "production": { - "tsConfig": "packages/journey-client/tsconfig.lib.prod.json" - } - } - } - }, - "tags": [] -} \ No newline at end of file diff --git a/packages/journey-client/src/lib/auth/index.ts b/packages/journey-client/src/lib/auth/index.ts deleted file mode 100644 index 6a9a389335..0000000000 --- a/packages/journey-client/src/lib/auth/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * @forgerock/javascript-sdk - * - * index.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import type { ServerConfig, StepOptions } from '../config.js'; -import Config from '../config.js'; -import { ActionTypes } from '../config/enums.js'; -import { REQUESTED_WITH, X_REQUESTED_PLATFORM } from '../shared/constants.js'; -import { middlewareWrapper } from '@forgerock/sdk-request-middleware'; -import type { Step } from '../interfaces.js'; -import { stringify, resolve } from '../utils/url.js'; -import { withTimeout } from '../utils/timeout.js'; -import { getEndpointPath } from '@forgerock/sdk-utilities'; - -/** - * Provides direct access to the OpenAM authentication tree API. - */ -abstract class Auth { - /** - * Gets the next step in the authentication tree. - * - * @param {Step} previousStep The previous step, including any required input. - * @param {StepOptions} options Configuration default overrides. - * @return {Step} The next step in the authentication tree. - */ - public static async next(previousStep?: Step, options?: StepOptions): Promise { - const { middleware, platformHeader, realmPath, serverConfig, tree, type } = Config.get(options); - const query = options ? options.query : {}; - const url = this.constructUrl(serverConfig, realmPath, tree, query); - const requestOptions = this.configureRequest(previousStep); - - const runMiddleware = middlewareWrapper( - { - ...requestOptions, - url: new URL(url), - headers: new Headers(requestOptions.headers), - }, - { - type: previousStep ? ActionTypes.Authenticate : ActionTypes.StartAuthenticate, - payload: { - tree, - type: type ? type : 'service', - }, - }, - ); - const req = runMiddleware(middleware); - - if (platformHeader) { - if (req.headers) { - req.headers.set('X-Requested-Platform', X_REQUESTED_PLATFORM); - } - } - - const res = await withTimeout(fetch(req.url.toString(), req), serverConfig.timeout); - const json = await this.getResponseJson(res); - return json; - } - - private static constructUrl( - serverConfig: ServerConfig, - realmPath?: string, - tree?: string, - query?: Record, - ): string { - const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined; - const params: Record = { ...query, ...treeParams }; - const queryString = Object.keys(params).length > 0 ? `?${stringify(params)}` : ''; - const path = getEndpointPath({ - endpoint: 'authenticate', - realmPath, - customPaths: serverConfig.paths, - }); - const url = resolve(serverConfig.baseUrl, `${path}${queryString}`); - return url; - } - - private static configureRequest(step?: Step): RequestInit { - const init: RequestInit = { - body: step ? JSON.stringify(step) : undefined, - credentials: 'include', - headers: new Headers({ - Accept: 'application/json', - 'Accept-API-Version': 'protocol=1.0,resource=2.1', - 'Content-Type': 'application/json', - 'X-Requested-With': REQUESTED_WITH, - }), - method: 'POST', - }; - - return init; - } - - private static async getResponseJson(res: Response): Promise { - const contentType = res.headers.get('content-type'); - const isJson = contentType && contentType.indexOf('application/json') > -1; - const json = isJson ? await res.json() : {}; - json.status = res.status; - json.ok = res.ok; - return json; - } -} - -export default Auth; diff --git a/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts b/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts index 08d6c1828b..174407b194 100644 --- a/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts @@ -8,8 +8,8 @@ * of the MIT license. See the LICENSE file for details. */ -import { CallbackType } from '../../auth/enums.js'; -import type { Callback } from '../interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; +import type { Callback } from '@forgerock/sdk-types'; import AttributeInputCallback from './attribute-input-callback.js'; describe('AttributeInputCallback', () => { @@ -55,7 +55,7 @@ describe('AttributeInputCallback', () => { value: false, }, ], - type: CallbackType.StringAttributeInputCallback, + type: callbackType.StringAttributeInputCallback, }; it('reads/writes basic properties with "validate only"', () => { @@ -70,14 +70,18 @@ describe('AttributeInputCallback', () => { expect(cb.getPolicies().policyRequirements).toStrictEqual(['a', 'b']); expect(cb.getFailedPolicies()).toStrictEqual([{ failedPolicies: { c: 'c', d: 'd' } }]); expect(cb.getInputValue()).toBe('Clark'); + if (!cb.payload.input) { + throw new Error('Input is not defined'); + } expect(cb.payload.input[1].value).toBe(true); }); it('writes validate only to `false` for submission', () => { const cb = new AttributeInputCallback(payload); cb.setValidateOnly(false); + if (!cb.payload.input) { + throw new Error('Input is not defined'); + } expect(cb.payload.input[1].value).toBe(false); }); }); - -}); diff --git a/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts b/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts index da66ed08c1..c74b687830 100644 --- a/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts +++ b/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts @@ -1,16 +1,12 @@ /* - * @forgerock/javascript-sdk + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. * - * attribute-input-callback.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ +import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; import FRCallback from './index.js'; -import type { Callback, PolicyRequirement } from '../interfaces.js'; - /** * Represents a callback used to collect attributes. @@ -51,13 +47,10 @@ class AttributeInputCallback extends FRCall * Gets the callback's failed policies. */ public getFailedPolicies(): PolicyRequirement[] { - const failedPolicies = this.getOutputByName( - 'failedPolicies', - [], - ) as unknown as string[]; + const failedPoliciesJsonStrings = this.getOutputByName('failedPolicies', []); try { - return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[]; - } catch (err) { + return failedPoliciesJsonStrings.map((v) => JSON.parse(v)) as PolicyRequirement[]; + } catch { throw new Error( 'Unable to parse "failed policies" from the ForgeRock server. The JSON within `AttributeInputCallback` was either malformed or missing.', ); @@ -67,10 +60,8 @@ class AttributeInputCallback extends FRCall /** * Gets the callback's applicable policies. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public getPolicies(): Record { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return this.getOutputByName>('policies', {}); + public getPolicies(): Record { + return this.getOutputByName>('policies', {}); } /** diff --git a/packages/journey-client/src/lib/callbacks/choice-callback.test.ts b/packages/journey-client/src/lib/callbacks/choice-callback.test.ts new file mode 100644 index 0000000000..7f60cbb512 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/choice-callback.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import ChoiceCallback from './choice-callback.js'; + +describe('ChoiceCallback', () => { + const payload: Callback = { + type: callbackType.ChoiceCallback, + output: [ + { + name: 'prompt', + value: 'Select an option', + }, + { + name: 'choices', + value: ['one', 'two', 'three'], + }, + { + name: 'defaultChoice', + value: 1, + }, + ], + input: [ + { + name: 'IDToken1', + value: 0, + }, + ], + }; + + it('should allow getting the prompt', () => { + const cb = new ChoiceCallback(payload); + expect(cb.getPrompt()).toBe('Select an option'); + }); + + it('should allow getting the choices', () => { + const cb = new ChoiceCallback(payload); + expect(cb.getChoices()).toEqual(['one', 'two', 'three']); + }); + + it('should allow getting the default choice', () => { + const cb = new ChoiceCallback(payload); + expect(cb.getDefaultChoice()).toBe(1); + }); + + it('should allow setting the choice by index', () => { + const cb = new ChoiceCallback(payload); + cb.setChoiceIndex(2); + expect(cb.getInputValue()).toBe(2); + }); + + it('should throw an error for an out-of-bounds index', () => { + const cb = new ChoiceCallback(payload); + expect(() => cb.setChoiceIndex(3)).toThrow('3 is out of bounds'); + expect(() => cb.setChoiceIndex(-1)).toThrow('-1 is out of bounds'); + }); + + it('should allow setting the choice by value', () => { + const cb = new ChoiceCallback(payload); + cb.setChoiceValue('two'); + expect(cb.getInputValue()).toBe(1); + }); + + it('should throw an error for an invalid choice value', () => { + const cb = new ChoiceCallback(payload); + expect(() => cb.setChoiceValue('four')).toThrow('"four" is not a valid choice'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/choice-callback.ts b/packages/journey-client/src/lib/callbacks/choice-callback.ts index 9e5d15892a..cd6fa5984e 100644 --- a/packages/journey-client/src/lib/callbacks/choice-callback.ts +++ b/packages/journey-client/src/lib/callbacks/choice-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * choice-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect an answer to a choice. diff --git a/packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts b/packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts new file mode 100644 index 0000000000..05140c5655 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import ConfirmationCallback from './confirmation-callback.js'; + +describe('ConfirmationCallback', () => { + const payload: Callback = { + type: callbackType.ConfirmationCallback, + output: [ + { + name: 'prompt', + value: 'Are you sure?', + }, + { + name: 'messageType', + value: 0, + }, + { + name: 'options', + value: ['Yes', 'No'], + }, + { + name: 'optionType', + value: -1, + }, + { + name: 'defaultOption', + value: 1, + }, + ], + input: [ + { + name: 'IDToken1', + value: 0, + }, + ], + }; + + it('should allow getting the prompt', () => { + const cb = new ConfirmationCallback(payload); + expect(cb.getPrompt()).toBe('Are you sure?'); + }); + + it('should allow getting the message type', () => { + const cb = new ConfirmationCallback(payload); + expect(cb.getMessageType()).toBe(0); + }); + + it('should allow getting the options', () => { + const cb = new ConfirmationCallback(payload); + expect(cb.getOptions()).toEqual(['Yes', 'No']); + }); + + it('should allow getting the option type', () => { + const cb = new ConfirmationCallback(payload); + expect(cb.getOptionType()).toBe(-1); + }); + + it('should allow getting the default option', () => { + const cb = new ConfirmationCallback(payload); + expect(cb.getDefaultOption()).toBe(1); + }); + + it('should allow setting the option by index', () => { + const cb = new ConfirmationCallback(payload); + cb.setOptionIndex(1); + expect(cb.getInputValue()).toBe(1); + }); + + it('should throw an error for an invalid index', () => { + const cb = new ConfirmationCallback(payload); + expect(() => cb.setOptionIndex(2)).toThrow('"2" is not a valid choice'); + }); + + it('should allow setting the option by value', () => { + const cb = new ConfirmationCallback(payload); + cb.setOptionValue('No'); + expect(cb.getInputValue()).toBe(1); + }); + + it('should throw an error for an invalid value', () => { + const cb = new ConfirmationCallback(payload); + expect(() => cb.setOptionValue('Maybe')).toThrow('"Maybe" is not a valid choice'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/confirmation-callback.ts b/packages/journey-client/src/lib/callbacks/confirmation-callback.ts index ddd54e3d01..e06ce22037 100644 --- a/packages/journey-client/src/lib/callbacks/confirmation-callback.ts +++ b/packages/journey-client/src/lib/callbacks/confirmation-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * confirmation-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a confirmation to a message. diff --git a/packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts b/packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts new file mode 100644 index 0000000000..687b98464b --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import DeviceProfileCallback from './device-profile-callback.js'; + +describe('DeviceProfileCallback', () => { + const payload: Callback = { + type: callbackType.DeviceProfileCallback, + output: [ + { + name: 'message', + value: 'Collecting device profile...', + }, + { + name: 'metadata', + value: true, + }, + { + name: 'location', + value: false, + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }; + + it('should allow getting the message', () => { + const cb = new DeviceProfileCallback(payload); + expect(cb.getMessage()).toBe('Collecting device profile...'); + }); + + it('should allow getting the metadata requirement', () => { + const cb = new DeviceProfileCallback(payload); + expect(cb.isMetadataRequired()).toBe(true); + }); + + it('should allow getting the location requirement', () => { + const cb = new DeviceProfileCallback(payload); + expect(cb.isLocationRequired()).toBe(false); + }); + + it('should allow setting the device profile', () => { + const cb = new DeviceProfileCallback(payload); + const profile = { + identifier: 'test-id', + metadata: { + hardware: { display: {} }, + browser: {}, + platform: {}, + }, + }; + cb.setProfile(profile); + expect(cb.getInputValue()).toBe(JSON.stringify(profile)); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts index 0b73e06e30..48d0795e2e 100644 --- a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts +++ b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * device-profile-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; import type { DeviceProfileData } from '../fr-device/interfaces.js'; /** diff --git a/packages/journey-client/src/lib/callbacks/factory.test.ts b/packages/journey-client/src/lib/callbacks/factory.test.ts new file mode 100644 index 0000000000..bfc1542fe7 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/factory.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import createCallback from './factory.js'; + +// Import all callback classes to check against +import AttributeInputCallback from './attribute-input-callback.js'; +import ChoiceCallback from './choice-callback.js'; +import ConfirmationCallback from './confirmation-callback.js'; +import DeviceProfileCallback from './device-profile-callback.js'; +import HiddenValueCallback from './hidden-value-callback.js'; +import KbaCreateCallback from './kba-create-callback.js'; +import MetadataCallback from './metadata-callback.js'; +import NameCallback from './name-callback.js'; +import PasswordCallback from './password-callback.js'; +import PingOneProtectEvaluationCallback from './ping-protect-evaluation-callback.js'; +import PingOneProtectInitializeCallback from './ping-protect-initialize-callback.js'; +import PollingWaitCallback from './polling-wait-callback.js'; +import ReCaptchaCallback from './recaptcha-callback.js'; +import ReCaptchaEnterpriseCallback from './recaptcha-enterprise-callback.js'; +import RedirectCallback from './redirect-callback.js'; +import SelectIdPCallback from './select-idp-callback.js'; +import SuspendedTextOutputCallback from './suspended-text-output-callback.js'; +import TermsAndConditionsCallback from './terms-and-conditions-callback.js'; +import TextInputCallback from './text-input-callback.js'; +import TextOutputCallback from './text-output-callback.js'; +import ValidatedCreatePasswordCallback from './validated-create-password-callback.js'; +import ValidatedCreateUsernameCallback from './validated-create-username-callback.js'; +import FRCallback from './index.js'; + +describe('Callback Factory', () => { + const testCases = [ + { type: callbackType.BooleanAttributeInputCallback, class: AttributeInputCallback }, + { type: callbackType.ChoiceCallback, class: ChoiceCallback }, + { type: callbackType.ConfirmationCallback, class: ConfirmationCallback }, + { type: callbackType.DeviceProfileCallback, class: DeviceProfileCallback }, + { type: callbackType.HiddenValueCallback, class: HiddenValueCallback }, + { type: callbackType.KbaCreateCallback, class: KbaCreateCallback }, + { type: callbackType.MetadataCallback, class: MetadataCallback }, + { type: callbackType.NameCallback, class: NameCallback }, + { type: callbackType.NumberAttributeInputCallback, class: AttributeInputCallback }, + { type: callbackType.PasswordCallback, class: PasswordCallback }, + { + type: callbackType.PingOneProtectEvaluationCallback, + class: PingOneProtectEvaluationCallback, + }, + { + type: callbackType.PingOneProtectInitializeCallback, + class: PingOneProtectInitializeCallback, + }, + { type: callbackType.PollingWaitCallback, class: PollingWaitCallback }, + { type: callbackType.ReCaptchaCallback, class: ReCaptchaCallback }, + { type: callbackType.ReCaptchaEnterpriseCallback, class: ReCaptchaEnterpriseCallback }, + { type: callbackType.RedirectCallback, class: RedirectCallback }, + { type: callbackType.SelectIdPCallback, class: SelectIdPCallback }, + { type: callbackType.StringAttributeInputCallback, class: AttributeInputCallback }, + { type: callbackType.SuspendedTextOutputCallback, class: SuspendedTextOutputCallback }, + { type: callbackType.TermsAndConditionsCallback, class: TermsAndConditionsCallback }, + { type: callbackType.TextInputCallback, class: TextInputCallback }, + { type: callbackType.TextOutputCallback, class: TextOutputCallback }, + { type: callbackType.ValidatedCreatePasswordCallback, class: ValidatedCreatePasswordCallback }, + { type: callbackType.ValidatedCreateUsernameCallback, class: ValidatedCreateUsernameCallback }, + ]; + + testCases.forEach((testCase) => { + it(`should create an instance of ${testCase.class.name} for type ${testCase.type}`, () => { + const payload: Callback = { type: testCase.type, input: [], output: [] }; + const callback = createCallback(payload); + expect(callback).toBeInstanceOf(testCase.class); + }); + }); + + it('should create a base FRCallback for an unknown type', () => { + const payload: Callback = { type: 'UnknownCallback' as any, input: [], output: [] }; + const callback = createCallback(payload); + expect(callback).toBeInstanceOf(FRCallback); + // Ensure it's not an instance of a more specific class + expect(callback).not.toBeInstanceOf(NameCallback); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/factory.ts b/packages/journey-client/src/lib/callbacks/factory.ts index 383eff8769..82f723823a 100644 --- a/packages/journey-client/src/lib/callbacks/factory.ts +++ b/packages/journey-client/src/lib/callbacks/factory.ts @@ -1,16 +1,13 @@ /* - * @forgerock/javascript-sdk + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. * - * factory.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ +import { callbackType } from '@forgerock/sdk-types'; import FRCallback from './index.js'; -import { CallbackType } from '../interfaces.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; import AttributeInputCallback from './attribute-input-callback.js'; import ChoiceCallback from './choice-callback.js'; import ConfirmationCallback from './confirmation-callback.js'; @@ -41,53 +38,53 @@ type FRCallbackFactory = (callback: Callback) => FRCallback; */ function createCallback(callback: Callback): FRCallback { switch (callback.type) { - case CallbackType.BooleanAttributeInputCallback: + case callbackType.BooleanAttributeInputCallback: return new AttributeInputCallback(callback); - case CallbackType.ChoiceCallback: + case callbackType.ChoiceCallback: return new ChoiceCallback(callback); - case CallbackType.ConfirmationCallback: + case callbackType.ConfirmationCallback: return new ConfirmationCallback(callback); - case CallbackType.DeviceProfileCallback: + case callbackType.DeviceProfileCallback: return new DeviceProfileCallback(callback); - case CallbackType.HiddenValueCallback: + case callbackType.HiddenValueCallback: return new HiddenValueCallback(callback); - case CallbackType.KbaCreateCallback: + case callbackType.KbaCreateCallback: return new KbaCreateCallback(callback); - case CallbackType.MetadataCallback: + case callbackType.MetadataCallback: return new MetadataCallback(callback); - case CallbackType.NameCallback: + case callbackType.NameCallback: return new NameCallback(callback); - case CallbackType.NumberAttributeInputCallback: + case callbackType.NumberAttributeInputCallback: return new AttributeInputCallback(callback); - case CallbackType.PasswordCallback: + case callbackType.PasswordCallback: return new PasswordCallback(callback); - case CallbackType.PingOneProtectEvaluationCallback: + case callbackType.PingOneProtectEvaluationCallback: return new PingOneProtectEvaluationCallback(callback); - case CallbackType.PingOneProtectInitializeCallback: + case callbackType.PingOneProtectInitializeCallback: return new PingOneProtectInitializeCallback(callback); - case CallbackType.PollingWaitCallback: + case callbackType.PollingWaitCallback: return new PollingWaitCallback(callback); - case CallbackType.ReCaptchaCallback: + case callbackType.ReCaptchaCallback: return new ReCaptchaCallback(callback); - case CallbackType.ReCaptchaEnterpriseCallback: + case callbackType.ReCaptchaEnterpriseCallback: return new ReCaptchaEnterpriseCallback(callback); - case CallbackType.RedirectCallback: + case callbackType.RedirectCallback: return new RedirectCallback(callback); - case CallbackType.SelectIdPCallback: + case callbackType.SelectIdPCallback: return new SelectIdPCallback(callback); - case CallbackType.StringAttributeInputCallback: + case callbackType.StringAttributeInputCallback: return new AttributeInputCallback(callback); - case CallbackType.SuspendedTextOutputCallback: + case callbackType.SuspendedTextOutputCallback: return new SuspendedTextOutputCallback(callback); - case CallbackType.TermsAndConditionsCallback: + case callbackType.TermsAndConditionsCallback: return new TermsAndConditionsCallback(callback); - case CallbackType.TextInputCallback: + case callbackType.TextInputCallback: return new TextInputCallback(callback); - case CallbackType.TextOutputCallback: + case callbackType.TextOutputCallback: return new TextOutputCallback(callback); - case CallbackType.ValidatedCreatePasswordCallback: + case callbackType.ValidatedCreatePasswordCallback: return new ValidatedCreatePasswordCallback(callback); - case CallbackType.ValidatedCreateUsernameCallback: + case callbackType.ValidatedCreateUsernameCallback: return new ValidatedCreateUsernameCallback(callback); default: return new FRCallback(callback); diff --git a/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts b/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts index f306a712a8..c9744d44d1 100644 --- a/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts @@ -8,9 +8,9 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from '.'; -import { CallbackType } from '../../auth/enums.js'; -import type { Callback } from '../interfaces.js'; +import FRCallback from './index.js'; +import { callbackType } from '@forgerock/sdk-types'; +import type { Callback } from '@forgerock/sdk-types'; describe('FRCallback', () => { it('reads/writes basic properties', () => { @@ -28,7 +28,7 @@ describe('FRCallback', () => { value: 'Username:', }, ], - type: CallbackType.NameCallback, + type: callbackType.NameCallback, }; const cb = new FRCallback(payload); cb.setInputValue('superman'); @@ -38,5 +38,3 @@ describe('FRCallback', () => { expect(cb.getInputValue()).toBe('superman'); }); }); - -}); diff --git a/packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts b/packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts new file mode 100644 index 0000000000..9ef7f72873 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import HiddenValueCallback from './hidden-value-callback.js'; + +describe('HiddenValueCallback', () => { + it('should instantiate correctly', () => { + const payload: Callback = { + type: callbackType.HiddenValueCallback, + output: [{ name: 'value', value: 'some-hidden-value' }], + input: [{ name: 'IDToken1', value: '' }], + }; + const cb = new HiddenValueCallback(payload); + expect(cb).toBeInstanceOf(HiddenValueCallback); + expect(cb.getOutputValue('value')).toBe('some-hidden-value'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts b/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts index b4dc361136..e2e0ccbb25 100644 --- a/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts +++ b/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * hidden-value-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect information indirectly from the user. diff --git a/packages/journey-client/src/lib/callbacks/index.ts b/packages/journey-client/src/lib/callbacks/index.ts index 5eadaa8673..86ddd41687 100644 --- a/packages/journey-client/src/lib/callbacks/index.ts +++ b/packages/journey-client/src/lib/callbacks/index.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. * - * index.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { CallbackType } from '../interfaces.js'; -import type { Callback, NameValue } from '../interfaces.js'; +import { type CallbackType, type Callback, type NameValue } from '@forgerock/sdk-types'; /** * Base class for authentication tree callback wrappers. diff --git a/packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts b/packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts new file mode 100644 index 0000000000..41eecbf99e --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import KbaCreateCallback from './kba-create-callback.js'; + +describe('KbaCreateCallback', () => { + const payload: Callback = { + type: callbackType.KbaCreateCallback, + output: [ + { + name: 'prompt', + value: 'What is your favorite color?', + }, + { + name: 'predefinedQuestions', + value: ['Question 1', 'Question 2'], + }, + ], + input: [ + { + name: 'IDToken1question', + value: '', + }, + { + name: 'IDToken1answer', + value: '', + }, + ], + }; + + it('should allow getting the prompt and questions', () => { + const cb = new KbaCreateCallback(payload); + expect(cb.getPrompt()).toBe('What is your favorite color?'); + expect(cb.getPredefinedQuestions()).toEqual(['Question 1', 'Question 2']); + }); + + it('should allow setting the question and answer', () => { + const cb = new KbaCreateCallback(payload); + cb.setQuestion('My custom question'); + cb.setAnswer('Blue'); + expect(cb.getInputValue('IDToken1question')).toBe('My custom question'); + expect(cb.getInputValue('IDToken1answer')).toBe('Blue'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/kba-create-callback.ts b/packages/journey-client/src/lib/callbacks/kba-create-callback.ts index ee4cc3dd67..017dcfe0f9 100644 --- a/packages/journey-client/src/lib/callbacks/kba-create-callback.ts +++ b/packages/journey-client/src/lib/callbacks/kba-create-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * kba-create-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect KBA-style security questions and answers. diff --git a/packages/journey-client/src/lib/callbacks/metadata-callback.test.ts b/packages/journey-client/src/lib/callbacks/metadata-callback.test.ts new file mode 100644 index 0000000000..868383dd2d --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/metadata-callback.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import MetadataCallback from './metadata-callback.js'; + +describe('MetadataCallback', () => { + it('should allow getting the data', () => { + const mockData = { foo: 'bar', baz: 123 }; + const payload: Callback = { + type: callbackType.MetadataCallback, + output: [ + { + name: 'data', + value: mockData, + }, + ], + input: [], + }; + const cb = new MetadataCallback(payload); + expect(cb.getData()).toEqual(mockData); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/metadata-callback.ts b/packages/journey-client/src/lib/callbacks/metadata-callback.ts index 4c75b7bf10..cdd278f1fc 100644 --- a/packages/journey-client/src/lib/callbacks/metadata-callback.ts +++ b/packages/journey-client/src/lib/callbacks/metadata-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * metadata-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to deliver and collect miscellaneous data. diff --git a/packages/journey-client/src/lib/callbacks/name-callback.test.ts b/packages/journey-client/src/lib/callbacks/name-callback.test.ts new file mode 100644 index 0000000000..3243af0aa6 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/name-callback.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import NameCallback from './name-callback.js'; + +describe('NameCallback', () => { + const payload: Callback = { + type: callbackType.NameCallback, + output: [ + { + name: 'prompt', + value: 'Username', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }; + + it('should allow getting the prompt', () => { + const cb = new NameCallback(payload); + expect(cb.getPrompt()).toBe('Username'); + }); + + it('should allow setting the name', () => { + const cb = new NameCallback(payload); + cb.setName('test-user'); + expect(cb.getInputValue()).toBe('test-user'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/name-callback.ts b/packages/journey-client/src/lib/callbacks/name-callback.ts index 26330feb35..b905675781 100644 --- a/packages/journey-client/src/lib/callbacks/name-callback.ts +++ b/packages/journey-client/src/lib/callbacks/name-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * name-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a username. diff --git a/packages/journey-client/src/lib/callbacks/password-callback.test.ts b/packages/journey-client/src/lib/callbacks/password-callback.test.ts new file mode 100644 index 0000000000..3b35a5ff85 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/password-callback.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import PasswordCallback from './password-callback.js'; + +describe('PasswordCallback', () => { + const payload: Callback = { + type: callbackType.PasswordCallback, + output: [ + { + name: 'prompt', + value: 'Password', + }, + { + name: 'policies', + value: ['policy1', 'policy2'], + }, + { + name: 'failedPolicies', + value: ['failedPolicy1'], + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }; + + it('should allow getting the prompt', () => { + const cb = new PasswordCallback(payload); + expect(cb.getPrompt()).toBe('Password'); + }); + + it('should allow getting policies', () => { + const cb = new PasswordCallback(payload); + expect(cb.getPolicies()).toEqual(['policy1', 'policy2']); + }); + + it('should allow getting failed policies', () => { + const cb = new PasswordCallback(payload); + expect(cb.getFailedPolicies()).toEqual(['failedPolicy1']); + }); + + it('should allow setting the password', () => { + const cb = new PasswordCallback(payload); + cb.setPassword('new-password'); + expect(cb.getInputValue()).toBe('new-password'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/password-callback.ts b/packages/journey-client/src/lib/callbacks/password-callback.ts index ead58aa0c0..b43b47cd40 100644 --- a/packages/journey-client/src/lib/callbacks/password-callback.ts +++ b/packages/journey-client/src/lib/callbacks/password-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * password-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback, PolicyRequirement } from '../interfaces.js'; +import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a password. diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts index e42963dd6e..4c2843ab62 100644 --- a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts @@ -9,7 +9,7 @@ */ import { vi, describe, it, expect } from 'vitest'; -import { CallbackType } from '../interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; import PingOneProtectEvaluationCallback from './ping-protect-evaluation-callback.js'; describe('PingOneProtectEvaluationCallback', () => { @@ -18,7 +18,7 @@ describe('PingOneProtectEvaluationCallback', () => { }); it('should test that the pauseBehavior method can be called', () => { const callback = new PingOneProtectEvaluationCallback({ - type: 'PingOneProtectEvaluationCallback' as CallbackType.PingOneProtectEvaluationCallback, + type: callbackType.PingOneProtectEvaluationCallback, output: [{ name: 'pauseBehavioralData', value: true }], }); const mock = vi.spyOn(callback, 'getPauseBehavioralData'); @@ -27,7 +27,7 @@ describe('PingOneProtectEvaluationCallback', () => { }); it('should test setData method', () => { const callback = new PingOneProtectEvaluationCallback({ - type: 'PingOneProtectEvaluationCallback' as CallbackType.PingOneProtectEvaluationCallback, + type: callbackType.PingOneProtectEvaluationCallback, output: [{ name: 'signals', value: '' }], input: [ { @@ -47,7 +47,7 @@ describe('PingOneProtectEvaluationCallback', () => { }); it('should test setClientError method', () => { const callback = new PingOneProtectEvaluationCallback({ - type: 'PingOneProtectEvaluationCallback' as CallbackType.PingOneProtectEvaluationCallback, + type: callbackType.PingOneProtectEvaluationCallback, output: [{ name: 'signals', value: '' }], input: [ { diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts index fceef54232..1fae82eb25 100644 --- a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * ping-protect-evaluation-callback.ts - * * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * @class - Represents a callback used to complete and package up device and behavioral data. diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts index 5a373e5072..c54a30ce4f 100644 --- a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts @@ -9,7 +9,7 @@ */ import { vi, describe, expect, it } from 'vitest'; -import { CallbackType } from '../interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; import PingOneProtectInitializeCallback from './ping-protect-initialize-callback.js'; describe('PingOneProtectInitializeCallback', () => { @@ -18,7 +18,7 @@ describe('PingOneProtectInitializeCallback', () => { }); it('should test the getConfig method', () => { const callback = new PingOneProtectInitializeCallback({ - type: 'PingOneProtectInitializeCallback' as CallbackType, + type: callbackType.PingOneProtectInitializeCallback, input: [ { name: 'IDToken1signals', @@ -90,7 +90,7 @@ describe('PingOneProtectInitializeCallback', () => { }); it('should test the setClientError method', () => { const callback = new PingOneProtectInitializeCallback({ - type: 'PingOneProtectInitializeCallback' as CallbackType, + type: callbackType.PingOneProtectInitializeCallback, input: [ { name: 'IDToken1signals', diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts index 9d08b40878..998dbfbbce 100644 --- a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * ping-protect-initialize-callback.ts - * * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * @class - Represents a callback used to initialize and start device and behavioral data collection. diff --git a/packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts b/packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts new file mode 100644 index 0000000000..78203621c8 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import PollingWaitCallback from './polling-wait-callback.js'; + +describe('PollingWaitCallback', () => { + const payload: Callback = { + type: callbackType.PollingWaitCallback, + output: [ + { + name: 'message', + value: 'Please wait...', + }, + { + name: 'waitTime', + value: '5000', + }, + ], + input: [], + }; + + it('should allow getting the message and wait time', () => { + const cb = new PollingWaitCallback(payload); + expect(cb.getMessage()).toBe('Please wait...'); + expect(cb.getWaitTime()).toBe(5000); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts b/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts index 2e59a356e2..aca5968a46 100644 --- a/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts +++ b/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * polling-wait-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to instruct the system to poll while a backend process completes. diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts b/packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts new file mode 100644 index 0000000000..106c5514ed --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import ReCaptchaCallback from './recaptcha-callback.js'; + +describe('ReCaptchaCallback', () => { + const payload: Callback = { + type: callbackType.ReCaptchaCallback, + output: [ + { + name: 'recaptchaSiteKey', + value: 'test-site-key', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }; + + it('should allow getting the site key', () => { + const cb = new ReCaptchaCallback(payload); + expect(cb.getSiteKey()).toBe('test-site-key'); + }); + + it('should allow setting the result', () => { + const cb = new ReCaptchaCallback(payload); + cb.setResult('recaptcha-response-token'); + expect(cb.getInputValue()).toBe('recaptcha-response-token'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts b/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts index 7ebb8f98f4..e75c89bb3e 100644 --- a/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts +++ b/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * recaptcha-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to integrate reCAPTCHA. diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts index 93780f3a16..417f6fea76 100644 --- a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts @@ -10,11 +10,11 @@ import { describe, expect, it, beforeAll } from 'vitest'; import ReCaptchaEnterpriseCallback from './recaptcha-enterprise-callback.js'; -import { CallbackType } from '../../auth/enums.js'; -import { Callback } from '../interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; +import { Callback } from '@forgerock/sdk-types'; const recaptchaCallback: Callback = { - type: 'ReCaptchaEnterpriseCallback' as CallbackType.ReCaptchaEnterpriseCallback, + type: callbackType.ReCaptchaEnterpriseCallback, output: [ { name: 'recaptchaSiteKey', @@ -82,5 +82,3 @@ describe('enterprise recaptcha', () => { expect(className).toBe('g-recaptcha'); }); }); - -}); diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts index a1757df479..f31cbd98ef 100644 --- a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts +++ b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts @@ -9,24 +9,7 @@ */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; -//"input": [ -// { -// "name": "IDToken1token", -// "value": "" -// }, -// { -// "name": "IDToken1action", -// "value": "" -// }, -// { -// "name": "IDToken1clientError", -// "value": "" -// }, -// { -// "name": "IDToken1payload", -// "value": "" -// } +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to integrate reCAPTCHA. diff --git a/packages/journey-client/src/lib/callbacks/redirect-callback.test.ts b/packages/journey-client/src/lib/callbacks/redirect-callback.test.ts new file mode 100644 index 0000000000..1b5d576130 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/redirect-callback.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import RedirectCallback from './redirect-callback.js'; + +describe('RedirectCallback', () => { + const payload: Callback = { + type: callbackType.RedirectCallback, + output: [ + { + name: 'redirectUrl', + value: 'https://am.example.com/callback?param=value', + }, + ], + input: [], + }; + + it('should allow getting the redirect URL', () => { + const cb = new RedirectCallback(payload); + expect(cb.getRedirectUrl()).toBe('https://am.example.com/callback?param=value'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/redirect-callback.ts b/packages/journey-client/src/lib/callbacks/redirect-callback.ts index 6418915f38..4992fd4f58 100644 --- a/packages/journey-client/src/lib/callbacks/redirect-callback.ts +++ b/packages/journey-client/src/lib/callbacks/redirect-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * redirect-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect an answer to a choice. diff --git a/packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts b/packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts new file mode 100644 index 0000000000..04e95d1943 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import SelectIdPCallback from './select-idp-callback.js'; + +describe('SelectIdPCallback', () => { + const payload: Callback = { + type: callbackType.SelectIdPCallback, + output: [ + { + name: 'providers', + value: [ + { provider: 'google', uiConfig: {} }, + { provider: 'facebook', uiConfig: {} }, + ], + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }; + + it('should allow getting the providers', () => { + const cb = new SelectIdPCallback(payload); + expect(cb.getProviders()).toHaveLength(2); + expect(cb.getProviders()[0].provider).toBe('google'); + }); + + it('should allow setting the provider', () => { + const cb = new SelectIdPCallback(payload); + cb.setProvider('facebook'); + expect(cb.getInputValue()).toBe('facebook'); + }); + + it('should throw an error for an invalid provider', () => { + const cb = new SelectIdPCallback(payload); + expect(() => cb.setProvider('twitter')).toThrow('"twitter" is not a valid choice'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/select-idp-callback.ts b/packages/journey-client/src/lib/callbacks/select-idp-callback.ts index d733ec06d6..db6e701984 100644 --- a/packages/journey-client/src/lib/callbacks/select-idp-callback.ts +++ b/packages/journey-client/src/lib/callbacks/select-idp-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * select-idp-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; interface IdPValue { provider: string; diff --git a/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts b/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts new file mode 100644 index 0000000000..5fde2f7392 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import SuspendedTextOutputCallback from './suspended-text-output-callback.js'; + +describe('SuspendedTextOutputCallback', () => { + it('should instantiate correctly and inherit from TextOutputCallback', () => { + const payload: Callback = { + type: callbackType.SuspendedTextOutputCallback, + output: [ + { + name: 'message', + value: 'Suspended message', + }, + { + name: 'messageType', + value: '0', + }, + ], + input: [], + }; + const cb = new SuspendedTextOutputCallback(payload); + expect(cb.getMessage()).toBe('Suspended message'); + expect(cb.getMessageType()).toBe('0'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts b/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts index 4a4fd636a6..3057b739f2 100644 --- a/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts +++ b/packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * suspended-text-output-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import TextOutputCallback from './text-output-callback.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to display a message. diff --git a/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts new file mode 100644 index 0000000000..223b279793 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import TermsAndConditionsCallback from './terms-and-conditions-callback.js'; + +describe('TermsAndConditionsCallback', () => { + const date = new Date().toString(); + const payload: Callback = { + type: callbackType.TermsAndConditionsCallback, + output: [ + { + name: 'terms', + value: 'Lorem ipsum...', + }, + { + name: 'version', + value: '1.0', + }, + { + name: 'createDate', + value: date, + }, + ], + input: [ + { + name: 'IDToken1', + value: false, + }, + ], + }; + + it('should allow getting terms, version, and date', () => { + const cb = new TermsAndConditionsCallback(payload); + expect(cb.getTerms()).toBe('Lorem ipsum...'); + expect(cb.getVersion()).toBe('1.0'); + expect(cb.getCreateDate()).toEqual(new Date(date)); + }); + + it('should allow setting acceptance', () => { + const cb = new TermsAndConditionsCallback(payload); + cb.setAccepted(true); + expect(cb.getInputValue()).toBe(true); + cb.setAccepted(false); + expect(cb.getInputValue()).toBe(false); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts index fcf6ec1afa..9bc1cbe85d 100644 --- a/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts +++ b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * terms-and-conditions-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect acceptance of terms and conditions. diff --git a/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts b/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts index 1ffba1f627..55e3e6759b 100644 --- a/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/text-input-callback.test.ts @@ -8,13 +8,13 @@ * of the MIT license. See the LICENSE file for details. */ -import { CallbackType } from '../interfaces.js'; -import type { Callback } from '../../auth/interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; +import type { Callback } from '@forgerock/sdk-types'; import TextInputCallback from './text-input-callback.js'; describe('TextInputCallback', () => { const payload: Callback = { - type: CallbackType.TextInputCallback, + type: callbackType.TextInputCallback, output: [ { name: 'prompt', diff --git a/packages/journey-client/src/lib/callbacks/text-input-callback.ts b/packages/journey-client/src/lib/callbacks/text-input-callback.ts index 73d8bc637a..fa34ee7fc5 100644 --- a/packages/journey-client/src/lib/callbacks/text-input-callback.ts +++ b/packages/journey-client/src/lib/callbacks/text-input-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * text-input-callback.ts - * * Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to retrieve input from the user. diff --git a/packages/journey-client/src/lib/callbacks/text-output-callback.test.ts b/packages/journey-client/src/lib/callbacks/text-output-callback.test.ts new file mode 100644 index 0000000000..e7e395eb28 --- /dev/null +++ b/packages/journey-client/src/lib/callbacks/text-output-callback.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Callback } from '@forgerock/sdk-types'; +import TextOutputCallback from './text-output-callback.js'; + +describe('TextOutputCallback', () => { + const payload: Callback = { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: 'This is a message', + }, + { + name: 'messageType', + value: '0', + }, + ], + input: [], + }; + + it('should allow getting the message and message type', () => { + const cb = new TextOutputCallback(payload); + expect(cb.getMessage()).toBe('This is a message'); + expect(cb.getMessageType()).toBe('0'); + }); +}); diff --git a/packages/journey-client/src/lib/callbacks/text-output-callback.ts b/packages/journey-client/src/lib/callbacks/text-output-callback.ts index 48915c939a..1a677e0486 100644 --- a/packages/journey-client/src/lib/callbacks/text-output-callback.ts +++ b/packages/journey-client/src/lib/callbacks/text-output-callback.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * text-output-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback } from '../interfaces.js'; +import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to display a message. diff --git a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts index 6c3d021c82..b717e3f8b0 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts @@ -8,13 +8,13 @@ * of the MIT license. See the LICENSE file for details. */ -import { CallbackType } from '../../auth/enums.js'; -import type { Callback } from '../interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; +import type { Callback } from '@forgerock/sdk-types'; import ValidatedCreatePasswordCallback from './validated-create-password-callback.js'; describe('ValidatedCreatePasswordCallback', () => { const payload: Callback = { - type: CallbackType.ValidatedCreatePasswordCallback, + type: callbackType.ValidatedCreatePasswordCallback, output: [ { name: 'echoOn', @@ -68,6 +68,7 @@ describe('ValidatedCreatePasswordCallback', () => { expect(cb.isRequired()).toBe(true); expect(cb.getPolicies().policyRequirements).toStrictEqual(['a', 'b']); expect(cb.getFailedPolicies()).toStrictEqual([{ failedPolicies: { c: 'c', d: 'd' } }]); + if (!cb.payload.input) throw new Error('Input is not defined'); expect(cb.payload.input[0].value).toBe('abcd123'); expect(cb.payload.input[1].value).toBe(true); }); @@ -75,8 +76,7 @@ describe('ValidatedCreatePasswordCallback', () => { it('writes validate only to `false` for submission', () => { const cb = new ValidatedCreatePasswordCallback(payload); cb.setValidateOnly(false); + if (!cb.payload.input) throw new Error('Input is not defined'); expect(cb.payload.input[1].value).toBe(false); }); }); - -}); diff --git a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts index a4cee78ba7..db99507945 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts @@ -1,16 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * validated-create-password-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback, PolicyRequirement } from '../interfaces.js'; - +import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a valid platform password. @@ -33,7 +28,7 @@ class ValidatedCreatePasswordCallback extends FRCallback { ) as unknown as string[]; try { return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[]; - } catch (err) { + } catch { throw new Error( 'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.', ); diff --git a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts index d01f921b67..2a4036d01d 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts @@ -8,13 +8,13 @@ * of the MIT license. See the LICENSE file for details. */ -import { CallbackType } from '../../auth/enums.js'; -import type { Callback } from '../interfaces.js'; +import { callbackType } from '@forgerock/sdk-types'; +import type { Callback } from '@forgerock/sdk-types'; import ValidatedCreateUsernameCallback from './validated-create-username-callback.js'; describe('ValidatedCreateUsernameCallback', () => { const payload: Callback = { - type: CallbackType.ValidatedCreateUsernameCallback, + type: callbackType.ValidatedCreateUsernameCallback, output: [ { name: 'echoOn', @@ -68,6 +68,7 @@ describe('ValidatedCreateUsernameCallback', () => { expect(cb.isRequired()).toBe(true); expect(cb.getPolicies().policyRequirements).toStrictEqual(['a', 'b']); expect(cb.getFailedPolicies()).toStrictEqual([{ failedPolicies: { c: 'c', d: 'd' } }]); + if (!cb.payload.input) throw new Error('Input is not defined'); expect(cb.payload.input[0].value).toBe('abcd123'); expect(cb.payload.input[1].value).toBe(true); }); @@ -75,8 +76,7 @@ describe('ValidatedCreateUsernameCallback', () => { it('writes validate only to `false` for submission', () => { const cb = new ValidatedCreateUsernameCallback(payload); cb.setValidateOnly(false); + if (!cb.payload.input) throw new Error('Input is not defined'); expect(cb.payload.input[1].value).toBe(false); }); }); - -}); diff --git a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts index 6dcd146861..d982332657 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts @@ -1,16 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * validated-create-username-callback.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ import FRCallback from './index.js'; -import type { Callback, PolicyRequirement } from '../interfaces.js'; - +import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a valid platform username. @@ -40,7 +35,7 @@ class ValidatedCreateUsernameCallback extends FRCallback { ) as unknown as string[]; try { return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[]; - } catch (err) { + } catch { throw new Error( 'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreateUsernameCallback` was either malformed or missing.', ); diff --git a/packages/journey-client/src/lib/config.types.ts b/packages/journey-client/src/lib/config.types.ts new file mode 100644 index 0000000000..3fc367b8c6 --- /dev/null +++ b/packages/journey-client/src/lib/config.types.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type { LegacyConfigOptions, StepOptions } from '@forgerock/sdk-types'; + +export interface JourneyClientConfig extends LegacyConfigOptions { + query?: Record; + // Add any journey-specific config options here +} + +export type { StepOptions }; diff --git a/packages/journey-client/src/lib/fr-device/collector.ts b/packages/journey-client/src/lib/fr-device/collector.ts deleted file mode 100644 index 542da93775..0000000000 --- a/packages/journey-client/src/lib/fr-device/collector.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * @forgerock/javascript-sdk - * - * collector.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - - - -/** - * @class Collector - base class for FRDevice - * Generic collector functions for collecting a device profile attributes - */ -class Collector { - /** - * @method reduceToObject - goes one to two levels into source to collect attribute - * @param props - array of strings; can use dot notation for two level lookup - * @param src - source of attributes to check - */ - // eslint-disable-next-line - reduceToObject(props: string[], src: Record): Record { - return props.reduce((prev, curr) => { - if (curr.includes('.')) { - const propArr = curr.split('.'); - const prop1 = propArr[0]; - const prop2 = propArr[1]; - const prop = src[prop1] && src[prop1][prop2]; - prev[prop2] = prop != undefined ? prop : ''; - } else { - prev[curr] = src[curr] != undefined ? src[curr] : null; - } - return prev; - }, {} as Record); - } - - /** - * @method reduceToString - goes one level into source to collect attribute - * @param props - array of strings - * @param src - source of attributes to check - */ - // eslint-disable-next-line - reduceToString(props: string[], src: any): string { - return props.reduce((prev, curr) => { - prev = `${prev}${src[curr].filename};`; - return prev; - }, ''); - } -} - -export default Collector; diff --git a/packages/journey-client/src/lib/fr-device/device-profile.test.ts b/packages/journey-client/src/lib/fr-device/device-profile.test.ts index 251d1788be..17c08fffab 100644 --- a/packages/journey-client/src/lib/fr-device/device-profile.test.ts +++ b/packages/journey-client/src/lib/fr-device/device-profile.test.ts @@ -7,8 +7,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { vi, expect, describe, it, beforeAll } from 'vitest'; -import Config from '../config'; +import { vi, expect, describe, it } from 'vitest'; import FRDevice from './index.js'; Object.defineProperty(window, 'crypto', { @@ -18,14 +17,6 @@ Object.defineProperty(window, 'crypto', { }, }); -beforeAll(() => { - Config.set({ - serverConfig: { - baseUrl: 'http://am.example.com:8443', - timeout: 3000, - }, - }); -}); describe('Test DeviceProfile', () => { it('should return basic metadata', async () => { const device = new FRDevice(); @@ -33,6 +24,7 @@ describe('Test DeviceProfile', () => { location: false, metadata: true, }); + if (!profile.metadata) throw new Error('Metadata is not defined'); const userAgent = profile.metadata.browser.userAgent as string; const appName = profile.metadata.browser.appName as string; const appVersion = profile.metadata.browser.appVersion as string; @@ -54,6 +46,7 @@ describe('Test DeviceProfile', () => { location: false, metadata: true, }); + if (!profile.metadata) throw new Error('Metadata is not defined'); const userAgent = profile.metadata.browser.userAgent as string; const display = profile.metadata.hardware.display; const deviceName = profile.metadata.platform.deviceName as string; @@ -69,6 +62,7 @@ describe('Test DeviceProfile', () => { location: false, metadata: true, }); + if (!profile.metadata) throw new Error('Metadata is not defined'); const userAgent = profile.metadata.browser.userAgent as string; const appName = profile.metadata.browser.appName as string; const appVersion = profile.metadata.browser.appVersion as string; diff --git a/packages/journey-client/src/lib/fr-device/index.ts b/packages/journey-client/src/lib/fr-device/index.ts index cbc2373497..98471e9913 100644 --- a/packages/journey-client/src/lib/fr-device/index.ts +++ b/packages/journey-client/src/lib/fr-device/index.ts @@ -26,11 +26,10 @@ import type { Geolocation, ProfileConfigOptions, } from './interfaces.js'; -import Collector from './collector.js'; +import { reduceToObject, reduceToString } from '@forgerock/sdk-utilities'; import { logger } from '@forgerock/sdk-logger'; const FRLogger = logger({ level: 'info' }); -import Config from '../config.js'; /** * @class FRDevice - Collects user device metadata @@ -53,7 +52,7 @@ import Config from '../config.js'; * }); * ``` */ -class FRDevice extends Collector { +class FRDevice { config: BaseProfileConfig = { fontNames, devicePlatforms, @@ -63,8 +62,10 @@ class FRDevice extends Collector { platformProps, }; - constructor(config?: ProfileConfigOptions) { - super(); + private prefix: string; + + constructor(config?: ProfileConfigOptions, prefix = 'forgerock') { + this.prefix = prefix; if (config) { Object.keys(config).forEach((key: string) => { if (!configurableCategories.includes(key)) { @@ -75,12 +76,15 @@ class FRDevice extends Collector { } } - getBrowserMeta(): { [key: string]: string } { + getBrowserMeta(): Record { if (typeof navigator === 'undefined') { FRLogger.warn('Cannot collect browser metadata. navigator is not defined.'); return {}; } - return this.reduceToObject(this.config.browserProps, navigator); + return reduceToObject( + this.config.browserProps, + navigator as unknown as Record, + ); } getBrowserPluginsNames(): string { @@ -88,7 +92,10 @@ class FRDevice extends Collector { FRLogger.warn('Cannot collect browser plugin information. navigator.plugins is not defined.'); return ''; } - return this.reduceToString(Object.keys(navigator.plugins), navigator.plugins); + return reduceToString( + Object.keys(navigator.plugins), + navigator.plugins as unknown as Record, + ); } getDeviceName(): string { @@ -122,19 +129,22 @@ class FRDevice extends Collector { FRLogger.warn('Cannot collect screen information. screen is not defined.'); return {}; } - return this.reduceToObject(this.config.displayProps, screen); + return reduceToObject(this.config.displayProps, screen as unknown as Record); } - getHardwareMeta(): { [key: string]: string } { + getHardwareMeta(): Record { if (typeof navigator === 'undefined') { FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.'); return {}; } - return this.reduceToObject(this.config.hardwareProps, navigator); + return reduceToObject( + this.config.hardwareProps, + navigator as unknown as Record, + ); } getIdentifier(): string { - const storageKey = `${Config.get().prefix}-DeviceID`; + const storageKey = `${this.prefix}-DeviceID`; if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.'); @@ -193,18 +203,15 @@ class FRDevice extends Collector { ); return Promise.resolve({}); } - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve) => { + return new Promise((resolve) => { navigator.geolocation.getCurrentPosition( (position) => resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, }), - (error) => { - FRLogger.warn( - 'Cannot collect geolocation information. ' + error.code + ': ' + error.message, - ); + () => { + FRLogger.warn('Cannot collect geolocation information. Geolocation API error.'); resolve({}); }, { @@ -216,12 +223,15 @@ class FRDevice extends Collector { }); } - getOSMeta(): { [key: string]: string } { + getOSMeta(): Record { if (typeof navigator === 'undefined') { FRLogger.warn('Cannot collect OS metadata. navigator is not defined.'); return {}; } - return this.reduceToObject(this.config.platformProps, navigator); + return reduceToObject( + this.config.platformProps, + navigator as unknown as Record, + ); } async getProfile({ location, metadata }: CollectParameters): Promise { @@ -256,7 +266,7 @@ class FRDevice extends Collector { getTimezoneOffset(): number | null { try { return new Date().getTimezoneOffset(); - } catch (err) { + } catch { FRLogger.warn('Cannot collect timezone information. getTimezoneOffset is not defined.'); return null; } @@ -264,7 +274,3 @@ class FRDevice extends Collector { } export default FRDevice; - - - FRDevice; - diff --git a/packages/journey-client/src/lib/fr-device/sample-profile.json b/packages/journey-client/src/lib/fr-device/sample-profile.json index 1b0375352c..5cd568f245 100644 --- a/packages/journey-client/src/lib/fr-device/sample-profile.json +++ b/packages/journey-client/src/lib/fr-device/sample-profile.json @@ -1,5 +1,5 @@ { - "indentifier": "714524572-2799534390-3707617532", + "identifier": "714524572-2799534390-3707617532", "metadata": { "hardware": { "cpuClass": null, diff --git a/packages/journey-client/src/lib/fr-login-failure.ts b/packages/journey-client/src/lib/fr-login-failure.ts index 91a1ce84c4..c1761eb600 100644 --- a/packages/journey-client/src/lib/fr-login-failure.ts +++ b/packages/journey-client/src/lib/fr-login-failure.ts @@ -1,18 +1,13 @@ /* - * @forgerock/javascript-sdk - * - * fr-login-failure.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ +import type { Step, AuthResponse, FailureDetail } from '@forgerock/sdk-types'; +import { StepType } from '@forgerock/sdk-types'; import FRPolicy from './fr-policy/index.js'; import type { MessageCreator, ProcessedPropertyError } from './fr-policy/interfaces.js'; -import type { Step } from './interfaces.js'; -import { StepType } from './enums.js'; -import type { AuthResponse, FailureDetail } from './interfaces.js'; class FRLoginFailure implements AuthResponse { /** diff --git a/packages/journey-client/src/lib/fr-login-success.ts b/packages/journey-client/src/lib/fr-login-success.ts index 693455ecb5..ff087596ba 100644 --- a/packages/journey-client/src/lib/fr-login-success.ts +++ b/packages/journey-client/src/lib/fr-login-success.ts @@ -1,16 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * fr-login-success.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import type { Step } from './interfaces.js'; -import { StepType } from './enums.js'; -import type { AuthResponse } from './interfaces.js'; +import type { Step, AuthResponse } from '@forgerock/sdk-types'; +import { StepType } from '@forgerock/sdk-types'; class FRLoginSuccess implements AuthResponse { /** diff --git a/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts b/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts index 64401d2913..3b0f82b3f0 100644 --- a/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts +++ b/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts @@ -8,8 +8,8 @@ * of the MIT license. See the LICENSE file for details. */ -import FRPolicy from './index.js'; -import { PolicyKey } from './enums.js'; +import FRPolicy, { MessageCreator } from './index.js'; +import { PolicyKey } from '@forgerock/sdk-types'; describe('The IDM error handling', () => { const property = 'userName'; @@ -56,15 +56,19 @@ describe('The IDM error handling', () => { it('error handling is extensible by customer', () => { const test = { customMessage: { - CUSTOM_POLICY: (property: string, params: { policyRequirement: string }): string => - `this is a custom message for "${params.policyRequirement}" policy of ${property}`, + CUSTOM_POLICY: (property: string, params?: { [key: string]: unknown }): string => + `this is a custom message for "${params?.policyRequirement}" policy of ${property}`, }, expectedString: `this is a custom message for "CUSTOM_POLICY" policy of ${property}`, policy: { policyRequirement: 'CUSTOM_POLICY', }, }; - const message = FRPolicy.parsePolicyRequirement(property, test.policy, test.customMessage); + const message = FRPolicy.parsePolicyRequirement( + property, + test.policy, + test.customMessage as MessageCreator, + ); expect(message).toBe(test.expectedString); }); @@ -151,11 +155,11 @@ describe('The IDM error handling', () => { result: false, }, }; - const customMessage = { + const customMessage: MessageCreator = { [PolicyKey.Unique]: (property: string): string => `this is a custom message for "UNIQUE" policy of ${property}`, - CUSTOM_POLICY: (property: string, params: { policyRequirement: string }): string => - `this is a custom message for "${params.policyRequirement}" policy of ${property}`, + CUSTOM_POLICY: (property: string, params?: { [key: string]: unknown }): string => + `this is a custom message for "${params?.policyRequirement}" policy of ${property}`, }; const expected = [ { @@ -195,5 +199,3 @@ describe('The IDM error handling', () => { expect(errorObjArr).toEqual(expected); }); }); -; -}); diff --git a/packages/journey-client/src/lib/fr-policy/helpers.ts b/packages/journey-client/src/lib/fr-policy/helpers.ts deleted file mode 100644 index 3c6eb6f9f7..0000000000 --- a/packages/journey-client/src/lib/fr-policy/helpers.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * @forgerock/javascript-sdk - * - * helpers.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -function getProp(obj: { [key: string]: unknown } | undefined, prop: string, defaultValue: T): T { - if (!obj || obj[prop] === undefined) { - return defaultValue; - } - return obj[prop] as T; -} - -export { getProp }; diff --git a/packages/journey-client/src/lib/fr-policy/index.ts b/packages/journey-client/src/lib/fr-policy/index.ts index 101f8ea074..b84fa614ae 100644 --- a/packages/journey-client/src/lib/fr-policy/index.ts +++ b/packages/journey-client/src/lib/fr-policy/index.ts @@ -1,15 +1,11 @@ /* - * @forgerock/javascript-sdk - * - * index.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import type { FailedPolicyRequirement, PolicyRequirement, Step } from '../interfaces.js'; -import { PolicyKey } from './enums.js'; +import type { FailedPolicyRequirement, PolicyRequirement, Step } from '@forgerock/sdk-types'; +import { PolicyKey } from '@forgerock/sdk-types'; import type { MessageCreator, ProcessedPropertyError } from './interfaces.js'; import defaultMessageCreator from './message-creator.js'; @@ -121,4 +117,3 @@ abstract class FRPolicy { export default FRPolicy; export type { MessageCreator, ProcessedPropertyError }; -export { PolicyKey }; diff --git a/packages/journey-client/src/lib/fr-policy/interfaces.ts b/packages/journey-client/src/lib/fr-policy/interfaces.ts index 7c1dfa905d..0d9553f4e4 100644 --- a/packages/journey-client/src/lib/fr-policy/interfaces.ts +++ b/packages/journey-client/src/lib/fr-policy/interfaces.ts @@ -1,14 +1,10 @@ /* - * @forgerock/javascript-sdk - * - * interfaces.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import type { FailedPolicyRequirement } from '../interfaces.js'; +import type { FailedPolicyRequirement } from '@forgerock/sdk-types'; interface MessageCreator { [key: string]: (propertyName: string, params?: { [key: string]: unknown }) => string; diff --git a/packages/journey-client/src/lib/fr-policy/message-creator.ts b/packages/journey-client/src/lib/fr-policy/message-creator.ts index bbbbfb080c..c0b30f205f 100644 --- a/packages/journey-client/src/lib/fr-policy/message-creator.ts +++ b/packages/journey-client/src/lib/fr-policy/message-creator.ts @@ -8,9 +8,8 @@ * of the MIT license. See the LICENSE file for details. */ -import { plural } from '../utils/strings.js'; -import { PolicyKey } from './enums.js'; -import { getProp } from './helpers.js'; +import { getProp, plural } from '@forgerock/sdk-utilities'; +import { PolicyKey } from '@forgerock/sdk-types'; import type { MessageCreator } from './interfaces.js'; const defaultMessageCreator: MessageCreator = { diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts b/packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts new file mode 100644 index 0000000000..9a72c7e5f9 --- /dev/null +++ b/packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts @@ -0,0 +1,184 @@ +/* + * @forgerock/javascript-sdk + * + * fr-qr-code.mock.data.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; + +export const otpQRCodeStep = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + 'Scan the QR code image below with the ForgeRock Authenticator app to register your ' + + 'device with your login.', + }, + { + name: 'messageType', + value: '0', + }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + `window.QRCodeReader.createCode({\n id: 'callback_0',\n text: 'otpauth\\x3A\\x` + + `2F\\x2Ftotp\\x2FForgeRock\\x3Ajlowery\\x3Fperiod\\x3D30\\x26b\\x3D032b75\\x26` + + `digits\\x3D6\\x26secret\\QITSTC234FRIU8DD987DW3VPICFY\\x3D\\x3D\\x3D\\x3D\\x3` + + `D\\x',\n 3D\\x26issuer\\x3DForgeRock version: '20',\n code: 'L'\n});`, + }, + { + name: 'messageType', + value: '4', + }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { + name: 'value', + value: + 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' + + 'r=ForgeRock&period=30&digits=6&b=032b75', + }, + { + name: 'id', + value: 'mfaDeviceRegistration', + }, + ], + input: [ + { + name: 'IDToken3', + value: 'mfaDeviceRegistration', + }, + ], + }, + { + type: callbackType.ConfirmationCallback, + output: [ + { + name: 'prompt', + value: '', + }, + { + name: 'messageType', + value: 0, + }, + { + name: 'options', + value: ['Next'], + }, + { + name: 'optionType', + value: -1, + }, + { + name: 'defaultOption', + value: 0, + }, + ], + input: [ + { + name: 'IDToken4', + value: 0, + }, + ], + }, + ], +}; + +export const pushQRCodeStep = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + 'Scan the QR code image below with the ForgeRock Authenticator app to register ' + + 'your device with your login.', + }, + { + name: 'messageType', + value: '0', + }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + `window.QRCodeReader.createCode({\n id: 'callback_0',\n text: 'pushauth\\x` + + `3A\\x2F\\x2Fpush\\x2Fforgerock\\x3AJustin\\x2520Lowery\\x3Fa\\x3DaHR0cHM6Ly9vc` + + `GVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXN` + + `oL3Nucy9tZXNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl\\x26r\\x3DaHR0cHM6Ly9vcGVuYW0tZm` + + `9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9t` + + `ZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI\\x26b\\x3D032b75\\x26s\\x3DFoxEr5uAzrys1yBmuyg` + + `PbxrVjysElmzsmqifi6eO_AI\\x26c\\x3DXD\\x2DMxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5` + + `kjgw\\x26l\\x3DYW1sYmNvb2tpZT0wMQ\\x26m\\x3DREGISTER\\x3A53b85112\\x2D8ba9\\x2` + + `D4b7e\\x2D9107\\x2Decbca2d65f7b1695151603616\\x26issuer\\x3DRm9yZ2VSb2Nr',\n ` + + ` version: '20',\n code: 'L'\n});`, + }, + { + name: 'messageType', + value: '4', + }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { + name: 'value', + value: + 'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' + + '2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' + + '1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' + + '=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' + + '9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' + + 'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' + + 'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75', + }, + { + name: 'id', + value: 'mfaDeviceRegistration', + }, + ], + input: [ + { + name: 'IDToken3', + value: 'mfaDeviceRegistration', + }, + ], + }, + { + type: callbackType.PollingWaitCallback, + output: [ + { + name: 'waitTime', + value: '5000', + }, + { + name: 'message', + value: 'Waiting for response...', + }, + ], + }, + ], +}; diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts new file mode 100644 index 0000000000..101350ae48 --- /dev/null +++ b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts @@ -0,0 +1,78 @@ +/* + * @forgerock/javascript-sdk + * + * index.test.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRStep from '../fr-step.js'; +import FRQRCode from './fr-qrcode.js'; +import { otpQRCodeStep, pushQRCodeStep } from './fr-qr-code.mock.data.js'; +// import WebAuthn step as it's similar in structure for testing non-QR Code steps +import { webAuthnRegJSCallback70 } from '../fr-webauthn/fr-webauthn.mock.data.js'; + +describe('Class for managing QR Codes', () => { + it('should return true for step containing OTP QR Code callbacks', () => { + const expected = true; + const step = new FRStep(otpQRCodeStep); + const result = FRQRCode.isQRCodeStep(step); + + expect(result).toBe(expected); + }); + + it('should return true for step containing Push QR Code callbacks', () => { + const expected = true; + const step = new FRStep(pushQRCodeStep); + const result = FRQRCode.isQRCodeStep(step); + + expect(result).toBe(expected); + }); + + it('should return false for step containing WebAuthn step', () => { + const expected = false; + const step = new FRStep(webAuthnRegJSCallback70); + const result = FRQRCode.isQRCodeStep(step); + + expect(result).toBe(expected); + }); + + it('should return an object with OTP QR Code data', () => { + const expected = { + message: + 'Scan the QR code image below with the ForgeRock Authenticator app to register your ' + + 'device with your login.', + use: 'otp', + uri: + 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' + + 'r=ForgeRock&period=30&digits=6&b=032b75', + }; + const step = new FRStep(otpQRCodeStep); + const result = FRQRCode.getQRCodeData(step); + + expect(result).toStrictEqual(expected); + }); + + it('should return an object with Push QR Code data', () => { + const expected = { + message: + 'Scan the QR code image below with the ForgeRock Authenticator app to register ' + + 'your device with your login.', + use: 'push', + uri: + 'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' + + '2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' + + '1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' + + '=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' + + '9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' + + 'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' + + 'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75', + }; + const step = new FRStep(pushQRCodeStep); + const result = FRQRCode.getQRCodeData(step); + + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts new file mode 100644 index 0000000000..db24ad9fab --- /dev/null +++ b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts @@ -0,0 +1,96 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; +import FRStep from '../fr-step.js'; +import TextOutputCallback from '../callbacks/text-output-callback.js'; +import HiddenValueCallback from '../callbacks/hidden-value-callback.js'; + +export type QRCodeData = { + message: string; + use: string; + uri: string; +}; + +/** + * @class FRQRCode - A utility class for handling QR Code steps + * + * Example: + * + * ```js + * const isQRCodeStep = FRQRCode.isQRCodeStep(step); + * let qrCodeData; + * if (isQRCodeStep) { + * qrCodeData = FRQRCode.getQRCodeData(step); + * } + * ``` + */ +abstract class FRQRCode { + /** + * @method isQRCodeStep - determines if step contains QR Code callbacks + * @param {FRStep} step - step object from AM response + * @returns {boolean} + */ + public static isQRCodeStep(step: FRStep): boolean { + const hiddenValueCb = step.getCallbacksOfType(callbackType.HiddenValueCallback); + + // QR Codes step should have at least one HiddenValueCallback + if (hiddenValueCb.length === 0) { + return false; + } + return !!this.getQRCodeURICb(hiddenValueCb); + } + + /** + * @method getQRCodeData - gets the necessary information from the QR Code callbacks + * @param {FRStep} step - step object from AM response + * @returns {QRCodeData} + */ + public static getQRCodeData(step: FRStep): QRCodeData { + const hiddenValueCb = step.getCallbacksOfType(callbackType.HiddenValueCallback); + + // QR Codes step should have at least one HiddenValueCallback + if (hiddenValueCb.length === 0) { + throw new Error( + 'QR Code step must contain a HiddenValueCallback. Use `FRQRCode.isQRCodeStep` to guard.', + ); + } + const qrCodeURICb = this.getQRCodeURICb(hiddenValueCb) as HiddenValueCallback; + const outputValue = qrCodeURICb ? qrCodeURICb.getOutputValue('value') : ''; + const qrCodeUse = + typeof outputValue === 'string' && outputValue.includes('otpauth://') ? 'otp' : 'push'; + + const messageCbs = step.getCallbacksOfType(callbackType.TextOutputCallback); + const displayMessageCb = messageCbs.find((cb) => { + const textOutputCallback = cb as TextOutputCallback; + return textOutputCallback.getMessageType() !== '4'; + }) as TextOutputCallback | null; + + return { + message: displayMessageCb ? displayMessageCb.getMessage() : '', + use: qrCodeUse, + uri: typeof outputValue === 'string' ? outputValue : '', + }; + } + + private static getQRCodeURICb(hiddenValueCbs: HiddenValueCallback[]) { + // Look for a HiddenValueCallback with an OTP URI + return hiddenValueCbs.find((cb) => { + const outputValue = cb.getOutputValue('value'); + + if (typeof outputValue === 'string') { + return outputValue?.includes('otpauth://') || outputValue?.includes('pushauth://'); + } + return false; + }); + } +} + +export default FRQRCode; diff --git a/packages/journey-client/src/lib/fr-recovery-codes/index.ts b/packages/journey-client/src/lib/fr-recovery-codes/index.ts new file mode 100644 index 0000000000..f43e8b20a9 --- /dev/null +++ b/packages/journey-client/src/lib/fr-recovery-codes/index.ts @@ -0,0 +1,72 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; +import type TextOutputCallback from '../callbacks/text-output-callback.js'; +import type FRStep from '../fr-step.js'; +import { parseDeviceNameText, parseDisplayRecoveryCodesText } from './script-parser.js'; + +/** + * Utility for handling recovery code nodes. + * + * Example: + * + * ```js + * // Determine if step is Display Recovery Codes step + * const isDisplayRecoveryCodesStep = FRRecoveryCodes.isDisplayStep(step); + * if (isDisplayRecoveryCodesStep) { + * const recoveryCodes = FRRecoveryCodes.getCodes(step); + * // Do the UI needful + * } + * ``` + */ +abstract class FRRecoveryCodes { + public static getDeviceName(step: FRStep): string { + const text = this.getDisplayCallback(step)?.getOutputByName('message', '') ?? ''; + return parseDeviceNameText(text); + } + /** + * Retrieves the recovery codes by parsing the JavaScript message text in callback. + * + * @param step The step to evaluate + * @return Recovery Code values in array + */ + public static getCodes(step: FRStep): string[] { + const text = this.getDisplayCallback(step)?.getOutputByName('message', ''); + return parseDisplayRecoveryCodesText(text || ''); + } + + /** + * Determines if the given step is a Display Recovery Codes step. + * + * @param step The step to evaluate + * @return Is this step a Display Recovery Codes step + */ + public static isDisplayStep(step: FRStep): boolean { + return !!this.getDisplayCallback(step); + } + + /** + * Gets the recovery codes step. + * + * @param step The step to evaluate + * @return gets the Display Recovery Codes' callback + */ + private static getDisplayCallback(step: FRStep): TextOutputCallback | undefined { + return step + .getCallbacksOfType(callbackType.TextOutputCallback) + .find((x) => { + const cb = x.getOutputByName('message', undefined); + return cb && (cb.includes('Recovery Codes') || cb.includes('recovery codes')); + }); + } +} + +export default FRRecoveryCodes; diff --git a/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts b/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts new file mode 100644 index 0000000000..f1ae9cc61a --- /dev/null +++ b/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts @@ -0,0 +1,43 @@ +/* + * @forgerock/javascript-sdk + * + * recovery-codes.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import FRStep from '../fr-step.js'; +import FRRecoveryCodes from './index.js'; +import { + displayRecoveryCodesResponse, + expectedDeviceName, + expectedRecoveryCodes, + otherResponse, +} from './script-text.mock.data.js'; + +describe('Class for managing the Display Recovery Codes node', () => { + it('should return true if Display Recovery Codes step', () => { + const step = new FRStep(displayRecoveryCodesResponse); + const isDisplayStep = FRRecoveryCodes.isDisplayStep(step); + expect(isDisplayStep).toBe(true); + }); + + it('should return false if not Display Recovery Codes step', () => { + const step = new FRStep(otherResponse); + const isDisplayStep = FRRecoveryCodes.isDisplayStep(step); + expect(isDisplayStep).toBe(false); + }); + + it('should return the Recovery Codes as array of strings', () => { + const step = new FRStep(displayRecoveryCodesResponse); + const recoveryCodes = FRRecoveryCodes.getCodes(step); + expect(recoveryCodes).toStrictEqual(expectedRecoveryCodes); + }); + it('should return a display name from the getDisplayName method', () => { + const step = new FRStep(displayRecoveryCodesResponse); + const displayName = FRRecoveryCodes.getDeviceName(step); + expect(displayName).toStrictEqual(expectedDeviceName); + }); +}); diff --git a/packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts b/packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts new file mode 100644 index 0000000000..95acf799a3 --- /dev/null +++ b/packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts @@ -0,0 +1,34 @@ +/* + * @forgerock/javascript-sdk + * + * script-parser.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { parseDeviceNameText, parseDisplayRecoveryCodesText } from './script-parser.js'; +import { + displayRecoveryCodes, + expectedRecoveryCodes, + securityKeyCustomNameResponse, + securityKeyResponse, +} from './script-text.mock.data.js'; + +describe('Parsing of the Display Recovery Codes script text', () => { + it('should parse the Display Recovery Codes Text', () => { + const result = parseDisplayRecoveryCodesText(displayRecoveryCodes); + expect(result).toStrictEqual(expectedRecoveryCodes); + }); + it('should parse the display name from recovery codes script', () => { + const text = securityKeyResponse; + const result = parseDeviceNameText(text); + expect(result).toStrictEqual('New Security Key'); + }); + it('should parse a custom name out of the recovery text', () => { + const text = securityKeyCustomNameResponse; + const result = parseDeviceNameText(text); + expect(result).toStrictEqual('My Custom Device Name'); + }); +}); diff --git a/packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts b/packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts new file mode 100644 index 0000000000..bce108da9c --- /dev/null +++ b/packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts @@ -0,0 +1,51 @@ +/* + * @forgerock/javascript-sdk + * + * script-parser.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +function parseDisplayRecoveryCodesText(text: string): string[] { + /** + * e.g. ` ... + * "
\n" + + * "iZmEtxvQ00\n" + + * "
\n" + + * ... ` + */ + + const recoveryCodesMatches = text.match(/\s[\w\W]"([\w]*)\\/g); + const recoveryCodes = + Array.isArray(recoveryCodesMatches) && + recoveryCodesMatches.map((substr: string) => { + // e.g. `"iZmEtxvQ00\` + const arr = substr.match(/"([\w]*)\\/); + return Array.isArray(arr) ? arr[1] : ''; + }); + return recoveryCodes || []; +} + +/** + * + * @param text + * @returns string + */ +function parseDeviceNameText(text: string): string { + /** + * We default the device name to 'New Security Key' + * If the user has a device name, it will be wrapped in tags + * e.g. ` ... My Security Key ... ` + * We want to remove the tags and just return the device name + * e.g. ` ... My Security Key ... ` + */ + const displayName = + text + ?.match(/\s*.*<\/em>/g)?.[0] + ?.replace('', '') + ?.replace('', '') ?? 'New Security Key'; + return displayName; +} +export { parseDeviceNameText, parseDisplayRecoveryCodesText }; diff --git a/packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts b/packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts new file mode 100644 index 0000000000..fea2ea410a --- /dev/null +++ b/packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts @@ -0,0 +1,120 @@ +/* eslint-disable no-useless-escape */ +/* + * @forgerock/javascript-sdk + * + * script-text.mock.data.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; + +const displayRecoveryCodes = `/* +* Copyright 2018 ForgeRock AS. All Rights Reserved +* +* Use of this code requires a commercial software license with ForgeRock AS. +* or with one of its affiliates. All use shall be exclusively subject +* to such license between the licensee and ForgeRock AS. +*/\n\nvar newLocation = document.getElementById(\"wrapper\");\nvar oldHtml = newLocation.getElementsByTagName(\"fieldset\")[0].innerHTML;\nnewLocation.getElementsByTagName(\"fieldset\")[0].innerHTML = \"
\\n\" + + \"
\\n\" + + \"

Your Recovery Codes

\\n\" + + \"

You must make a copy of these WebAuthn authenticator recovery codes. They cannot be displayed again.

\\n\" + + \"
\\n\" + + \"
\\n\" + + \"iZmEtxvQ00\\n\" + + \"
\\n\" + + \"
\\n\" + + \"Eqw3GFVamY\\n\" + + \"
\\n\" + + \"
\\n\" + + \"nNPqIEtIpS\\n\" + + \"
\\n\" + + \"
\\n\" + + \"vGhNQpDjP8\\n\" + + \"
\\n\" + + \"
\\n\" + + \"ItA4W3iBaA\\n\" + + \"
\\n\" + + \"
\\n\" + + \"JmLQP6XyIo\\n\" + + \"
\\n\" + + \"
\\n\" + + \"G2e6foNKke\\n\" + + \"
\\n\" + + \"
\\n\" + + \"h2SqAqvT21\\n\" + + \"
\\n\" + + \"
\\n\" + + \"q6VX1ojNbI\\n\" + + \"
\\n\" + + \"
\\n\" + + \"IZKIQXAfY2\\n\" + + \"
\\n\" + + \"
\\n\" + + \"

Use one of these codes to authenticate if you lose your device, which has been named: New Security Key

\\n\" + + \"
\\n\" + + \"
\" + oldHtml;\ndocument.body.appendChild(newLocation);\n\n\n +`; + +const displayRecoveryCodesResponse = { + authId: 'foo', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nvar newLocation = document.getElementById("wrapper");\nvar oldHtml = newLocation.getElementsByTagName("fieldset")[0].innerHTML;\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML = "
\\n" +\n "
\\n" +\n "

Your Recovery Codes

\\n" +\n "

You must make a copy of these WebAuthn authenticator recovery codes. They cannot be displayed again.

\\n" +\n "
\\n" +\n "
\\n" +\n "iZmEtxvQ00\\n" +\n "
\\n" +\n "
\\n" +\n "Eqw3GFVamY\\n" +\n "
\\n" +\n "
\\n" +\n "nNPqIEtIpS\\n" +\n "
\\n" +\n "
\\n" +\n "vGhNQpDjP8\\n" +\n "
\\n" +\n "
\\n" +\n "ItA4W3iBaA\\n" +\n "
\\n" +\n "
\\n" +\n "JmLQP6XyIo\\n" +\n "
\\n" +\n "
\\n" +\n "G2e6foNKke\\n" +\n "
\\n" +\n "
\\n" +\n "h2SqAqvT21\\n" +\n "
\\n" +\n "
\\n" +\n "q6VX1ojNbI\\n" +\n "
\\n" +\n "
\\n" +\n "IZKIQXAfY2\\n" +\n "
\\n" +\n "
\\n" +\n "

Use one of these codes to authenticate if you lose your device, which has been named: New Security Key

\\n" +\n "
\\n" +\n "
" + oldHtml;\ndocument.body.appendChild(newLocation);\n\n\n', + }, + { name: 'messageType', value: '4' }, + ], + }, + ], +}; +const expectedDeviceName = 'New Security Key'; + +const expectedRecoveryCodes = [ + 'iZmEtxvQ00', + 'Eqw3GFVamY', + 'nNPqIEtIpS', + 'vGhNQpDjP8', + 'ItA4W3iBaA', + 'JmLQP6XyIo', + 'G2e6foNKke', + 'h2SqAqvT21', + 'q6VX1ojNbI', + 'IZKIQXAfY2', +]; + +const otherResponse = { + authId: 'foo', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nvar new Location = foo;', + }, + { name: 'messageType', value: '4' }, + ], + }, + ], +}; + +const securityKeyResponse = `/* * Copyright 2018 ForgeRock AS. All Rights Reserved * * Use of this code requires a commercial software license with ForgeRock AS. * or with one of its affiliates. All use shall be exclusively subject * to such license between the licensee and ForgeRock AS. */ var newLocation = document.getElementById("wrapper"); var oldHtml = newLocation.getElementsByTagName("fieldset")[0].innerHTML; newLocation.getElementsByTagName("fieldset")[0].innerHTML = "
\n" + "
\n" + "

Your Recovery Codes

\n" + "

You must make a copy of these recovery codes. They cannot be displayed again.

\n" + "
\n" + "
\n" + "kw50qtmm32\n" + "
\n" + "
\n" + "Rt2Td8AK2s\n" + "
\n" + "
\n" + "6vZrIRCBJB\n" + "
\n" + "
\n" + "HCmL01Yiyv\n" + "
\n" + "
\n" + "BvDsSpazA2\n" + "
\n" + "
\n" + "T6pbKLyW2l\n" + "
\n" + "
\n" + "gZR0u6XubS\n" + "
\n" + "
\n" + "VZ1bH94IfO\n" + "
\n" + "
\n" + "O9BZ4bLefQ\n" + "
\n" + "
\n" + "NKNrBxlHCt\n" + "
\n" + "
\n" + "

Use one of these codes to authenticate if you lose your device, which has been named: New Security Key

\n" + "
\n" + "
" + oldHtml; document.body.appendChild(newLocation);`; +const securityKeyCustomNameResponse = `/* * Copyright 2018 ForgeRock AS. All Rights Reserved * * Use of this code requires a commercial software license with ForgeRock AS. * or with one of its affiliates. All use shall be exclusively subject * to such license between the licensee and ForgeRock AS. */ var newLocation = document.getElementById("wrapper"); var oldHtml = newLocation.getElementsByTagName("fieldset")[0].innerHTML; newLocation.getElementsByTagName("fieldset")[0].innerHTML = "
\n" + "
\n" + "

Your Recovery Codes

\n" + "

You must make a copy of these recovery codes. They cannot be displayed again.

\n" + "
\n" + "
\n" + "kw50qtmm32\n" + "
\n" + "
\n" + "Rt2Td8AK2s\n" + "
\n" + "
\n" + "6vZrIRCBJB\n" + "
\n" + "
\n" + "HCmL01Yiyv\n" + "
\n" + "
\n" + "BvDsSpazA2\n" + "
\n" + "
\n" + "T6pbKLyW2l\n" + "
\n" + "
\n" + "gZR0u6XubS\n" + "
\n" + "
\n" + "VZ1bH94IfO\n" + "
\n" + "
\n" + "O9BZ4bLefQ\n" + "
\n" + "
\n" + "NKNrBxlHCt\n" + "
\n" + "
\n" + "

Use one of these codes to authenticate if you lose your device, which has been named: My Custom Device Name

\n" + "
\n" + "
" + oldHtml; document.body.appendChild(newLocation);`; + +export { + displayRecoveryCodes, + displayRecoveryCodesResponse, + expectedDeviceName, + expectedRecoveryCodes, + otherResponse, + securityKeyCustomNameResponse, + securityKeyResponse, +}; diff --git a/packages/journey-client/src/lib/fr-step.test.ts b/packages/journey-client/src/lib/fr-step.test.ts new file mode 100644 index 0000000000..ad06d3c17a --- /dev/null +++ b/packages/journey-client/src/lib/fr-step.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, it, expect } from 'vitest'; +import { callbackType, type Step } from '@forgerock/sdk-types'; +import FRStep from './fr-step.js'; +import NameCallback from './callbacks/name-callback.js'; + +describe('fr-step.ts', () => { + const stepPayload: Step = { + authId: '123', + callbacks: [ + { + type: callbackType.NameCallback, + input: [{ name: 'IDToken1', value: '' }], + output: [{ name: 'prompt', value: 'Username' }], + }, + { + type: callbackType.PasswordCallback, + input: [{ name: 'IDToken2', value: '' }], + output: [{ name: 'prompt', value: 'Password' }], + }, + { + type: callbackType.NameCallback, // Duplicate for testing + input: [{ name: 'IDToken3', value: '' }], + output: [{ name: 'prompt', value: 'Username 2' }], + }, + ], + description: 'Step description', + header: 'Step header', + stage: 'Step stage', + }; + + it('should correctly initialize with a payload', () => { + const step = new FRStep(stepPayload); + expect(step.payload).toEqual(stepPayload); + expect(step.callbacks).toHaveLength(3); + expect(step.callbacks[0]).toBeInstanceOf(NameCallback); + }); + + it('should get a single callback of a specific type', () => { + const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] }; + const step = new FRStep(singleCallbackPayload); + const cb = step.getCallbackOfType(callbackType.PasswordCallback); + expect(cb).toBeDefined(); + expect(cb.getType()).toBe(callbackType.PasswordCallback); + }); + + it('should throw an error if getCallbackOfType finds no matching callbacks', () => { + const step = new FRStep(stepPayload); + const err = `Expected 1 callback of type "TermsAndConditionsCallback", but found 0`; + expect(() => step.getCallbackOfType(callbackType.TermsAndConditionsCallback)).toThrow(err); + }); + + it('should throw an error if getCallbackOfType finds multiple matching callbacks', () => { + const step = new FRStep(stepPayload); + const err = `Expected 1 callback of type "NameCallback", but found 2`; + expect(() => step.getCallbackOfType(callbackType.NameCallback)).toThrow(err); + }); + + it('should get all callbacks of a specific type', () => { + const step = new FRStep(stepPayload); + const callbacks = step.getCallbacksOfType(callbackType.NameCallback); + expect(callbacks).toHaveLength(2); + expect(callbacks[0].getType()).toBe(callbackType.NameCallback); + expect(callbacks[1].getType()).toBe(callbackType.NameCallback); + }); + + it('should return an empty array if getCallbacksOfType finds no matches', () => { + const step = new FRStep(stepPayload); + const callbacks = step.getCallbacksOfType(callbackType.TermsAndConditionsCallback); + expect(callbacks).toHaveLength(0); + }); + + it('should set the value of a specific callback', () => { + const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] }; + const step = new FRStep(singleCallbackPayload); + step.setCallbackValue(callbackType.PasswordCallback, 'password123'); + const cb = step.getCallbackOfType(callbackType.PasswordCallback); + expect(cb.getInputValue()).toBe('password123'); + }); + + it('should return the description', () => { + const step = new FRStep(stepPayload); + expect(step.getDescription()).toBe('Step description'); + }); + + it('should return the header', () => { + const step = new FRStep(stepPayload); + expect(step.getHeader()).toBe('Step header'); + }); + + it('should return the stage', () => { + const step = new FRStep(stepPayload); + expect(step.getStage()).toBe('Step stage'); + }); +}); diff --git a/packages/journey-client/src/lib/fr-step.ts b/packages/journey-client/src/lib/fr-step.ts index f9bf6bbeeb..0d9341a37f 100644 --- a/packages/journey-client/src/lib/fr-step.ts +++ b/packages/journey-client/src/lib/fr-step.ts @@ -1,20 +1,20 @@ /* - * @forgerock/javascript-sdk - * - * fr-step.ts - * * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { CallbackType } from './interfaces.js'; -import type { Callback, Step } from './interfaces.js'; +import { + type CallbackType, + type Callback, + type Step, + type AuthResponse, +} from '@forgerock/sdk-types'; import type FRCallback from './callbacks/index.js'; import type { FRCallbackFactory } from './callbacks/factory.js'; import createCallback from './callbacks/factory.js'; -import { StepType } from './enums.js'; -import type { AuthResponse } from './interfaces.js'; +import { StepType } from '@forgerock/sdk-types'; /** * Represents a single step of an authentication tree. diff --git a/packages/journey-client/src/lib/fr-webauthn/enums.ts b/packages/journey-client/src/lib/fr-webauthn/enums.ts new file mode 100644 index 0000000000..13f4c7c092 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/enums.ts @@ -0,0 +1,36 @@ +/* + * @forgerock/javascript-sdk + * + * enums.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +enum WebAuthnOutcome { + Error = 'ERROR', + Unsupported = 'unsupported', +} + +enum WebAuthnOutcomeType { + AbortError = 'AbortError', + DataError = 'DataError', + ConstraintError = 'ConstraintError', + EncodingError = 'EncodingError', + InvalidError = 'InvalidError', + NetworkError = 'NetworkError', + NotAllowedError = 'NotAllowedError', + NotSupportedError = 'NotSupportedError', + SecurityError = 'SecurityError', + TimeoutError = 'TimeoutError', + UnknownError = 'UnknownError', +} + +enum WebAuthnStepType { + None = 0, + Authentication = 1, + Registration = 2, +} + +export { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType }; diff --git a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts b/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts new file mode 100644 index 0000000000..996028d76e --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts @@ -0,0 +1,491 @@ +/* + * @forgerock/javascript-sdk + * + * fr-webauthn.mock.data.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; + +export const webAuthnRegJSCallback653 = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar publicKey = {\n challenge: new Int8Array([63, -71, 8, -32, 51, 11, 35, -85, -19, -93, -17, 9, -10, 104, 96, -5, -43, -94, 126, 123, 18, 44, -53, 27, 69, -59, -45, -113, 4, -120, -12, -17]).buffer,\n // Relying Party:\n rp: {\n \n name: "ForgeRock"\n },\n // User:\n user: {\n id: Uint8Array.from("sgsP5DNBy2TvEhwnWHu1BFRw2_LQepAdjkOfC1z6nxU", function (c) { return c.charCodeAt(0) }),\n name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629",\n displayName: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629"\n },\n pubKeyCredParams: [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257 } ],\n attestation: "none",\n timeout: 60000,\n excludeCredentials: [],\n authenticatorSelection: {"userVerification":"discouraged"}\n};\n\nnavigator.credentials.create({publicKey: publicKey})\n .then(function (newCredentialInfo) {\n var rawId = newCredentialInfo.id;\n var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON));\n var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString();\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + keyData + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken3', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnAuthJSCallback653 = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar acceptableCredentials = [\n { "type": "public-key", "id": new Int8Array([1, 97, 2, 123, -105, -19, -106, 10, -86, 82, -23, 5, 52, 63, 103, 110, -71, 53, 107, 104, 76, -42, -49, 96, 67, -114, -97, 19, -59, 89, -102, -115, -110, -101, -6, -98, 39, -75, 2, 74, 23, -105, 67, 6, -112, 21, -3, 36, -114, 52, 35, 75, 74, 82, -8, 115, -128, -34, -105, 110, 124, 41, -79, -53, -90, 81, -11, -7, 91, -45, -67, -82, 106, 74, 30, 112, 100, -47, 54, -12, 95, 81, 97, 36, 123, -15, -91, 87, -82, 87, -45, -103, -80, 109, -111, 82, 109, 58, 50, 19, -21, -102, 54, -108, -68, 12, -101, -53, -65, 11, -94, -36, 112, -101, -95, -90, -118, 68, 13, 8, -49, -77, -28, -82, -97, 126, -71, 33, -58, 19, 58, -118, 36, -28, 22, -55, 64, -72, -80, -9, -48, -50, 58, -52, 64, -64, -27, -5, -12, 110, -95, -17]).buffer }\n];\n\nvar options = {\n \n challenge: new Int8Array([109, 14, 35, -101, 97, -69, -105, -89, -58, 14, 108, 59, 45, 87, 109, -78, -51, 64, 90, 124, -97, 43, -84, -108, -69, -117, 101, -4, -36, -69, -106, 103]).buffer,\n timeout: 60000,\n userVerification: "discouraged",\n allowCredentials: acceptableCredentials\n};\n\nnavigator.credentials.get({ "publicKey" : options })\n .then(function (assertion) {\n var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON));\n var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString();\n var signature = new Int8Array(assertion.response.signature).toString();\n var rawId = assertion.id;\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken3', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnRegJSCallback70 = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar publicKey = {\n challenge: new Int8Array([87, -95, 18, -17, -59, -3, -72, -9, -109, 77, -66, 67, 101, -59, -29, -92, -31, -58, 117, -14, 3, -123, 1, -54, -69, -122, 44, 111, 30, 49, 12, 81]).buffer,\n // Relying Party:\n rp: {\n id: "https://user.example.com:3002",\n name: "ForgeRock"\n },\n // User:\n user: {\n id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }),\n name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629",\n displayName: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629"\n },\n pubKeyCredParams: [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ],\n attestation: "none",\n timeout: 60000,\n excludeCredentials: [{ "type": "public-key", "id": new Int8Array([49, -96, -107, 113, 106, 5, 115, 22, 68, 121, -85, -27, 8, -58, -113, 127, -105, -37, -10, -12, -58, -25, 29, -82, -18, 69, -99, 125, 33, 82, 38, -66, -27, -128, -91, -86, 87, 68, 94, 0, -78, 70, -11, -70, -14, -53, 38, -60, 46, 27, 66, 46, 21, -125, -70, 123, -46, -124, 86, -2, 102, 70, -52, 54]).buffer },{ "type": "public-key", "id": new Int8Array([64, 17, -15, -123, -21, 127, 76, -120, 90, -112, -5, 54, 105, 93, 82, -104, -79, 107, -69, -3, -113, -94, -59, -4, 126, -33, 117, 32, -44, 122, -97, 8, -112, 105, -96, 96, 90, 44, -128, -121, 107, 79, -98, -68, -93, 11, -105, -47, 102, 13, 110, 84, 59, -91, -30, 37, -3, -22, 39, 111, -10, 87, -50, -35]).buffer }],\n authenticatorSelection: {"userVerification":"preferred"}\n};\n\nnavigator.credentials.create({publicKey: publicKey})\n .then(function (newCredentialInfo) {\n var rawId = newCredentialInfo.id;\n var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON));\n var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString();\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + keyData + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken3', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnAuthJSCallback70 = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar options = {\n \n challenge: new Int8Array([-2, 85, 78, -80, -124, -6, -118, 15, 77, -30, -76, -27, -43, -19, -51, -68, 60, -80, -64, -102, 73, -103, 76, -77, -96, -28, 5, -23, -59, -36, 1, -1]).buffer,\n timeout: 60000,\n allowCredentials: [{ "type": "public-key", "id": new Int8Array([-107, 93, 68, -67, -5, 107, 18, 16, -25, -30, 80, 103, -75, -53, -2, -95, 102, 42, 47, 126, -1, 85, 93, 45, -85, 8, -108, 107, 47, -25, 66, 12, -96, 81, 104, -127, 26, -59, -69, -23, 75, 89, 58, 124, -93, 4, 28, -128, 121, 35, 39, 103, -86, -86, 123, -67, -7, -4, 79, -49, 127, -19, 7, 4]).buffer }]\n};\n\nnavigator.credentials.get({ "publicKey" : options })\n .then(function (assertion) {\n var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON));\n var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString();\n var signature = new Int8Array(assertion.response.signature).toString();\n var rawId = assertion.id;\n var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle));\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken3', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnRegJSCallback70StoredUsername = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar publicKey = {\n challenge: new Int8Array([-90, -30, 14, -111, 43, -115, -125, 43, -96, 124, -109, -1, -100, -64, -52, -56, -15, -9, 41, 22, -111, -116, -65, -88, 108, -60, -58, 53, 62, -66, -34, 104]).buffer,\n // Relying Party:\n rp: {\n \n name: "ForgeRock"\n },\n // User:\n user: {\n id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }),\n name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629",\n displayName: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629"\n },\n pubKeyCredParams: [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ],\n attestation: "none",\n timeout: 60000,\n excludeCredentials: [],\n authenticatorSelection: {"userVerification":"preferred","requireResidentKey":true}\n};\n\nnavigator.credentials.create({publicKey: publicKey})\n .then(function (newCredentialInfo) {\n var rawId = newCredentialInfo.id;\n var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON));\n var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString();\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + keyData + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken3', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnRegMetaCallback70 = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.MetadataCallback, + output: [ + { + name: 'data', + value: { + relyingPartyName: 'ForgeRock', + attestationPreference: 'none', + displayName: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + _type: 'WebAuthn', + relyingPartyId: '', + userName: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + authenticatorSelection: '{"userVerification":"preferred"}', + userId: 'NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5', + timeout: '60000', + excludeCredentials: '', + pubKeyCredParams: + '[ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ]', + challenge: 'PiIwSUMSo5qN7ahxaBVzWCHnpIxiWZPBix3PDI4/O8k=', + }, + }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken2', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnAuthMetaCallback70 = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.MetadataCallback, + output: [ + { + name: 'data', + value: { + userVerification: 'preferred', + _type: 'WebAuthn', + challenge: 'J7kVW1EpFY3thLYVMAAJuR9dswysFhqgrBT6vvSuidE=', + relyingPartyId: '', + allowCredentials: + // eslint-disable-next-line + 'allowCredentials: [{ "type": "public-key", "id": new Int8Array([1, 122, 110, -32, -105, -95, -90, 81, 20, -122, -96, -115, -115, 38, -7, 15, -127, 48, 48, 97, 94, -23, -54, 74, 3, -41, -118, -124, 112, 5, -77, 87, -11, 102, -86, 93, 27, 112, -128, 103, -58, -75, 68, -62, -62, 72, -27, 108, -59, 0, -124, -117, -121, -52, -97, -88, -112, 22, 122, 109, 104, -89, -10, 46, -95, 62, 64, 43, -42, 127, -53, -98, 88, -126, -68, -94, -5, 81, -71, -52, -54, -12, -55, 127, -125, 125, 53, -61, 61, 47, -66, -12, 25, 115, -24, -56, 95, 8, -20, -6, 4, 72, -45, -103, -52, 39, 123, 13, 9, -79, 99, -62, 84, -2, -41, 55, 125, 17, 126, -38, -80, -83, -28, 99, -26, -30, -18, 122, 92, -91, -128, -27, 4, 27, -39, 36, 117, 4, 120, 9, -24, -72, 84, 124, 25, 100, -40, 32, 63, -97, 119, 10, 73, 8, -46, 61, 56]).buffer },{ "type": "public-key", "id": new Int8Array([1, 15, 3, 3, 70, 54, 31, -27, -121, 121, 41, 83, -28, -49, 9, -113, -58, 117, -97, 18, 1, 100, -29, 6, -116, -93, 90, -91, 75, -120, -127, 50, 99, -37, -56, -41, 105, 42, 8, -87, -21, 37, -7, 96, -121, -125, -33, 79, 2, -10, 127, -117, 23, 46, 42, 29, 125, 91, 47, -101, 126, 44, 70, -84, -124, -94, -119, -87, 63, -116, 11, -28, 127, 76, -67, 36, -62, 62, -125, -82, 99, 71, 24, 32, -87, 93, 53, 97, -44, 18, -14, 77, 80, 77, 110, -80, -52, 18, 69, 127, 82, -27, -116, 42, -66, -53, -26, -29, 74, 75, 34, -88, -119, 118, -50, -70, -110, -68, -91, -15, 100, 113, 24, 13, -127, 39, -1, -85, 114, -125, 89, 89, -101, 94, -37, 82, -61, 15, -2, 3, -4, 9, 28, -75, -84, 96, 60, 85, -44, -98, -27, -29, 107, -115, -111, -3, -102]).buffer }]', + timeout: '60000', + }, + }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken2', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnRegMetaCallback70StoredUsername = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.MetadataCallback, + output: [ + { + name: 'data', + value: { + relyingPartyName: 'ForgeRock', + attestationPreference: 'none', + displayName: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + _type: 'WebAuthn', + relyingPartyId: '', + userName: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + authenticatorSelection: '{"userVerification":"preferred","requireResidentKey":true}', + userId: 'NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5', + timeout: '60000', + excludeCredentials: '', + pubKeyCredParams: + '[ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ]', + challenge: 'DfZ7CvBgBaApXZgcqSb+7/yA5ih/yRHhpDzrrWLMtZc=', + }, + }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken2', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnAuthMetaCallback70StoredUsername = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.MetadataCallback, + output: [ + { + name: 'data', + value: { + userVerification: 'preferred', + _type: 'WebAuthn', + challenge: 'OHmmFKfBhrUZKkuZJ84lf9N8TaRmQSjRdZyueeSIXAo=', + relyingPartyId: '', + allowCredentials: '', + timeout: '60000', + }, + }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken2', value: 'webAuthnOutcome' }], + }, + ], +}; + +export const webAuthnAuthMetaCallbackJsonResponse = { + authId: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoSW5kZXhWYWx1ZSI6IndlYmF1dGhuIiwib3RrIjoicXN1dTA0anNxZ2hmcGpubjFiM2IxdDh0NTQiLCJhdXRoSW5kZXhUeXBlIjoic2VydmljZSIsInJlYWxtIjoiLyIsInNlc3Npb25JZCI6IipBQUpUU1FBQ01ERUFCSFI1Y0dVQUNFcFhWRjlCVlZSSUFBSlRNUUFBKmV5SjBlWEFpT2lKS1YxUWlMQ0pqZEhraU9pSktWMVFpTENKaGJHY2lPaUpJVXpJMU5pSjkuWlhsS01HVllRV2xQYVVwTFZqRlJhVXhEU214aWJVMXBUMmxLUWsxVVNUUlJNRXBFVEZWb1ZFMXFWVEpKYVhkcFdWZDRia2xxYjJsYVIyeDVTVzR3TGk0M01qbDZSRXhsU0hwM1pXbFVPRll6VDB4cFgycEJMbGxpV1Y5WVl6WTJPVWxJU0MxT01VTk5Va05MZVROdFZFaHliVlJSVDJoV2VVSTBUVUUzYUVoRmFISjFTRU5SVWpWeWVsQnRZVzFpYlRNeFFqQlhSMlZxVEU5allUWllWMEUwTVROSlozRnVXV2gwV0hadlRHaE9VMkpDYUdkUlJFaGFSV1pLVmxaeWNsOTBTRUpJU1Y5elIyMVhNMEZYWm5wTk9WZElXblJMTm10ZmFITm9hemRNUTBkSVkwbzVhVGQ2Wnpob2FUbFROVm8wTVVkbk1rZFpXSFJJTnpoWlRGOVVaVTl5VWtOc1ozTlBkbDlWTWpGRFJ6STBReTFMWVRJNVMxRm9ibFYwTTNCVlozWldiamRCVUhWdWRFdEdaR2h6VERselJ6QlBTVnAwYzNoV1NqTXRPVmc1TmpSM2VrVldSV2w2Vnprek9XNTRlVm94VjNWVVgzWmpTRzFFV2t4eE0yWTJXamswVlVWTU5VNDJjMjVMTWs5U01XeDBOR3BrVld3eWVVMVRaVXR6YzJkb2VIRmlNMnhoZW1WQlVtMDBWM3BUVld4c1JFUTRVVVJuWDNoSFozSktlVmhQZWxCa1RWUnZYeTB0U1c0MFkxVjBTeloxTVdWSVkxOWhZbkZLTlhsRVpWcEpaM04yYkU5eE5qVTJkVU5KV0dzNU9GbElWMHBEWkhSR1MzcGtWV1k1ZG0xNlJIWk9iMmxXTlZnd2RXd3hiRzlTV0dOaFZtTkhVMDlaTlVGNFdITkJkSGd5UVhkVlVXUnViR0pmYTJodWN5MUhXblowZUVOM1lYRldlR2h1T1RsdVVWY3ljWFphUjNCTk1raFVPRzFMYUU5SVIyOVJOQzFWVkZrNVVWbDBNbGcwZGpaZlQzSk5kemxUZEdwSVl6RnRjMTkxVTI1VWVtVmpUR2RYUkVZdFVFNXNVM0J1Ymkxc1EyRlljRXREVmxsS1FVeDVUbWhoWDBJeGQwNTRSRzV0WW5vNVdYVjFXakZMYzFWTkxVZHJjVlJZYkY5c2JqUlBMVEpXVUhoTVFYSjFVblZOZW0xaVgxQndRMjlqY1d4T1Z6Sm1jWHBPV25seVlteE9RVEZXUkdaM04yYzJNMnhmTkhvd05UWkhlRXhOVjBOck5rOTZVQzFMY1RJMVlXTmxSa0ZQWWpGd1JtMXBkVGgzWW5kUGVITmtZa0ZLVW0xSWMxVlJWVzlQWm5aQlpURldORmcyUW5veGNFeG9SV3d0UzNGblkwMDBjMjluYTFab1YyRkhZWFpyVFUxSVFTNVlWRVpOV1d4R2F6bHFSV1V5VG5CamFIZDVZVzVuLmYyS2t1RlhnM05MUU1NbGNnMU1HU2Y2YTZQVmdJalhtUC1wcmJhQTNtTnciLCJleHAiOjE2MTM0OTc0OTksImlhdCI6MTYxMzQ5NzE5OX0.EuDmsY3C6I6vc_x7KlkW4rSQJY1FWevbGGmxkSu4HVU', + callbacks: [ + { + type: 'MetadataCallback', + output: [ + { + name: 'data', + value: { + _action: 'webauthn_authentication', + challenge: 'qnMsxgya8h6mUc6OyRu8jJ6Oq16tHV3cgE7juXGMDbg=', + allowCredentials: '', + _allowCredentials: [], + timeout: '60000', + userVerification: 'preferred', + relyingPartyId: 'rpId: "humorous-cuddly-carrot.glitch.me",', + _relyingPartyId: 'humorous-cuddly-carrot.glitch.me', + _type: 'WebAuthn', + supportsJsonResponse: true, + }, + }, + ], + }, + { + type: 'HiddenValueCallback', + output: [ + { + name: 'value', + value: 'false', + }, + { + name: 'id', + value: 'webAuthnOutcome', + }, + ], + input: [ + { + name: 'IDToken2', + value: 'webAuthnOutcome', + }, + ], + }, + ], +}; + +export const webAuthnRegMetaCallbackJsonResponse = { + authId: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ3aGl0ZWxpc3Qtc3RhdGUiOiJjMjMzNDRkMC04ZTlhLTRhM2QtODZkMS1mNTIwNTExZmM3NjciLCJhdXRoSW5kZXhWYWx1ZSI6IkFuZHlXZWJBdXRobiIsIm90ayI6ImFzdTNjMmo4YThta2w0aWQyN3FndGFuaTVqIiwiYXV0aEluZGV4VHlwZSI6InNlcnZpY2UiLCJyZWFsbSI6Ii9hbHBoYSIsInNlc3Npb25JZCI6IipBQUpUU1FBQ01ESUFCSFI1Y0dVQUNFcFhWRjlCVlZSSUFBSlRNUUFDTURFLipleUowZVhBaU9pSktWMVFpTENKamRIa2lPaUpLVjFRaUxDSmhiR2NpT2lKSVV6STFOaUo5LlpYbEtNR1ZZUVdsUGFVcExWakZSYVV4RFNteGliVTFwVDJsS1FrMVVTVFJSTUVwRVRGVm9WRTFxVlRKSmFYZHBXVmQ0YmtscWIybGFSMng1U1c0d0xpNVJkVXBMWTBnNGIwUjViemxZV0ZKcE0wNDJjQzFCTG0xRU0xOWFOVlZrZURGeVYxUk9lR2MwVUVWNFNHMW1OMGN5TkdoSU1YRXpPV0ZhVDNoaWREbFlUa000VEZscU1VRm1la1pXUzBkME1ubEJVMmxvT1hCNmJIRjNOVTQ0Y0ZsblZXRnplV2g1VEdWUFdtWldaVkpqVldNMmNGRjJOVmxhZEROdGNHWnZOMWRuV1hsWVNrRnZRWGQxZEhka2NYVlhaVXBYWkRScVl6TlBkRTFOTlVOV1NWaGtTRFZqVURGTVlrRjVTblJHWVd4NlIyWkplVGRtWW0xQll6SkZTbGxLUVdGNFZqTTRSa0l0TjFWVFFWZEVaMWxsUW14elYzQXRjWEZSYlRSdFIyUnBkRlpwYzBFdGNWRTFUbEpaU0cxQlJXSnpVVXBtWVdoRVQzSllUMFo1ZVZNM2VUUnVaV3BHUjNaWExVaFFRV054YjJ0TGNFMWZhVFkyWVRrdFZrbE9UMWxaY0RoWFQxaFFiR2hVTWt4YVIwVk1SVEpuVUhsWFkzcDJNV2xCVjBwcU4zVlVjVWx2UjFCTllVNUxSSEJKZGpaTk4wdFphVnA2YzJoUE9IRmxTMWw1VjJ3emNWWk9Tek4wWjJOamNGbERZVkJmT0ZGcGNuZG9iMkV3UjNOSWRIRk9SR04yYlhCUldIbE1ZM1Z3VGpkdmJGbDNhREV5WlVsTU9HaEZla0pLUldkMlVFaGpTM0JDYmxKVVYwOWlkemhJUlZobFJFZzRNbGxDWmxJdGVXMXpYMjlZT0c1dFpURldhbFJvUVZnMlRHZGtkbkUwVTJ4MmJYVjFYMTlzZVZack9GQnVZV3RDTldvMWVHOUtkVkUwTjFwRlZERnBUbGsxVkhCQlVHNUNOMjVuVVhaeWExRnlWR1ZHZFZKdU1EUlFPV1E0TTJ0MFVXcEhaMlpyUVdkbVNrNWpiek54YUZOR1dGb3dWM2swWldkUlpHdGhUVFU0ZGxSVVJtRlhlRjl1VERkdk9XdGFha3hXUjA4eFh6Sk9Oa0ZGVW5SVWFpMU9MVll6UmxwMlJYTnpRbnBTV1V0V2VGUm5UMTlRUTNWVmMxaDJjVzFVU1ZkcFNUQnhYekE0ZFhSVlVXcGpRMHhWUjNCVlZHRnVhR0pCVldKV1R6SnZVWHBVVTJ4bkxUTTJhakJWZVMxdlYxQTVNblo0ZVhvMWRUSkZkRVpwWkRaRGMxWmFMVVl3TFdkbWNFTnBkUzFGVlVzNFJITklhelZCUXpFNVp6VnRNMkY0Tmw5TFZuTlBkMmxIWDIxMU5tcHhOVzlsTlhkbFFtVm9iRE5RV0ZCTmFERTRTRWRrTTFOVVRHUmphVjl0VXpKTFJuYzJTM2xvWVd0a1dIcElXREZSTFdwRVIzWkRlRzF4YzJGcFZuUllhVlJSTXpSM1pVdGFhRk15VEVSVVpITklWM0JZTFdReFREa3lSMlJrVFc1UGJWRkhlV3A0WkRScU16TlFPVW96TVdGNGMyVXhZbDlFY1MxeWFXZHZZM2huTG1wR2VXWmxRVTlRVjBwMldUaHNaV3B1Wm1RemIyYy43YmdYcE5RNGRLSEpTeGpmUEVZZm44MGxZc3owaXBwNngyaURtRXlqd1JjIiwiZXhwIjoxNzQyODQzMDQ4LCJpYXQiOjE3NDI4NDI3NDh9.3zuPwPZVeFSwezhmzSZe-HW-22zo1HXwEPJO5jGl0Cg', + callbacks: [ + { + type: 'MetadataCallback', + output: [ + { + name: 'data', + value: { + _action: 'webauthn_registration', + challenge: 'QMmVc2lSU6G+jx6IYNOd6EaPz6X8jBzkxI9TMdVyWTw=', + attestationPreference: 'none', + userName: 'demo', + userId: 'NzcyNTI2NmMtYmZiZi00ZGFiLWFhYzEtNjY3NjUyMGIzNmZl', + relyingPartyName: 'ForgeRock', + authenticatorSelection: + '{"userVerification":"preferred","residentKey":"required","requireResidentKey":true}', + _authenticatorSelection: { + userVerification: 'preferred', + residentKey: 'required', + requireResidentKey: true, + }, + pubKeyCredParams: + '[ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ]', + _pubKeyCredParams: [ + { + type: 'public-key', + alg: -257, + }, + { + type: 'public-key', + alg: -7, + }, + ], + timeout: '60000', + excludeCredentials: '', + _excludeCredentials: [], + displayName: 'demo', + relyingPartyId: 'id: "idc.petrov.ca",', + _relyingPartyId: 'idc.petrov.ca', + extensions: {}, + _type: 'WebAuthn', + supportsJsonResponse: true, + }, + }, + ], + }, + { + type: 'HiddenValueCallback', + output: [ + { + name: 'value', + value: 'false', + }, + { + name: 'id', + value: 'webAuthnOutcome', + }, + ], + input: [ + { + name: 'IDToken2', + value: 'webAuthnOutcome', + }, + ], + }, + ], +}; + +export const webAuthnAuthJSCallback70StoredUsername = { + authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ', + callbacks: [ + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar options = {\n \n challenge: new Int8Array([50, -11, 63, -112, 37, -61, 57, 126, 83, -127, 122, -42, -102, -82, 26, -95, -75, -37, 16, 52, 27, 54, -101, 124, -16, 99, 33, 92, 63, 10, -110, 102]).buffer,\n timeout: 60000,\n userVerification: "preferred",\n \n};\n\nnavigator.credentials.get({ "publicKey" : options })\n .then(function (assertion) {\n var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON));\n var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString();\n var signature = new Int8Array(assertion.response.signature).toString();\n var rawId = assertion.id;\n var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle));\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.TextOutputCallback, + output: [ + { + name: 'message', + value: + // eslint-disable-next-line + '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', + }, + { name: 'messageType', value: '4' }, + ], + }, + { + type: callbackType.HiddenValueCallback, + output: [ + { name: 'value', value: 'false' }, + { name: 'id', value: 'webAuthnOutcome' }, + ], + input: [{ name: 'IDToken3', value: 'webAuthnOutcome' }], + }, + ], +}; diff --git a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts b/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts new file mode 100644 index 0000000000..b7796d2a15 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts @@ -0,0 +1,107 @@ +/* + * @forgerock/javascript-sdk + * + * fr-webauthn.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { WebAuthnStepType } from './enums.js'; +import FRWebAuthn from './index.js'; +import { + webAuthnRegJSCallback653, + webAuthnAuthJSCallback653, + webAuthnRegJSCallback70, + webAuthnAuthJSCallback70, + webAuthnRegMetaCallback70, + webAuthnAuthMetaCallback70, + webAuthnRegJSCallback70StoredUsername, + webAuthnAuthJSCallback70StoredUsername, + webAuthnRegMetaCallback70StoredUsername, + webAuthnAuthMetaCallback70StoredUsername, +} from './fr-webauthn.mock.data.js'; +import FRStep from '../fr-step.js'; + +describe('Test FRWebAuthn class with 6.5.3 "Passwordless"', () => { + it('should return Registration type with register text-output callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnRegJSCallback653 as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Registration); + }); + it('should return Authentication type with authenticate text-output callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnAuthJSCallback653 as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Authentication); + }); + // it('should return Registration type with register metadata callbacks', () => { + // // eslint-disable-next-line + // const step = new FRStep(webAuthnRegMetaCallback653 as any); + // const stepType = FRWebAuthn.getWebAuthnStepType(step); + // expect(stepType).toBe(WebAuthnStepType.Registration); + // }); + // it('should return Authentication type with authenticate metadata callbacks', () => { + // // eslint-disable-next-line + // const step = new FRStep(webAuthnAuthMetaCallback653 as any); + // const stepType = FRWebAuthn.getWebAuthnStepType(step); + // expect(stepType).toBe(WebAuthnStepType.Authentication); + // }); +}); + +describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => { + it('should return Registration type with register text-output callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnRegJSCallback70 as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Registration); + }); + it('should return Authentication type with authenticate text-output callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnAuthJSCallback70 as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + console.log('the step type', stepType, WebAuthnStepType.Authentication); + expect(stepType).toBe(WebAuthnStepType.Authentication); + }); + it('should return Registration type with register metadata callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnRegMetaCallback70 as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Registration); + }); + it('should return Authentication type with authenticate metadata callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnAuthMetaCallback70 as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Authentication); + }); +}); + +describe('Test FRWebAuthn class with 7.0 "Usernameless"', () => { + it('should return Registration type with register text-output callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnRegJSCallback70StoredUsername as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Registration); + }); + it('should return Authentication type with authenticate text-output callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnAuthJSCallback70StoredUsername as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Authentication); + }); + it('should return Registration type with register metadata callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnRegMetaCallback70StoredUsername as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Registration); + }); + it('should return Authentication type with authenticate metadata callbacks', () => { + // eslint-disable-next-line + const step = new FRStep(webAuthnAuthMetaCallback70StoredUsername as any); + const stepType = FRWebAuthn.getWebAuthnStepType(step); + expect(stepType).toBe(WebAuthnStepType.Authentication); + }); +}); diff --git a/packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts b/packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts new file mode 100644 index 0000000000..8e0dd24d74 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts @@ -0,0 +1,25 @@ +/* + * @forgerock/javascript-sdk + * + * helpers.mock.data.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +// eslint-disable-next-line +export const allowCredentials70 = + 'allowCredentials: [{ "type": "public-key", "id": new Int8Array([1, -16, 9, 79, 6, -2, -82, 51, 124, -94, 95, 23, -86, 70, -43, 89, 91, -9, 45, -22, 91, -51, 84, 93, 24, -64, 38, 101, 126, -53, 87, 70, -49, -88, -105, 116, 33, 75, -39, -92, -5, 115, 12, 52, 124, -100, 85, 104, -15, 5, -13, 25, -74, 101, 71, -115, -102, 16, 10, -9, -19, -110, 65, 118, -28, 89, -15, -115, -81, 22, -104, 123, 17, -92, 49, 109, -38, -51, 100, 96, -65, 25, -48, 28, 106, -45, 17, -45, -37, 46, -5, -6, -26, -23, -108, 13, -66, -55, -117, -107, 119, 7, -32, 34, 46, 0, -29, -111, -32, 45, -15, -113, 110, 123, -44, 6, 10, 65, 99, 25, 105, 69, -127, 76, 127, -33, -89, -56, 74, 25, 43, -43, -56, 9, 87, 80, 124, -32, -39, 115, 17, 18, 78, 121, 69, -36, -44, -28, -109, -126, 58, 64, 80, -4, 21, 63, -19]).buffer }]'; +// eslint-disable-next-line +export const allowMultipleCredentials70 = + 'allowCredentials: [{ "type": "public-key", "id": new Int8Array([-33, 120, 18, 124, 16, 5, -127, -51, 68, 85, 76, -92, -3, 25, 116, 91, 71, 53, 106, -114, -86, 9, -81, -96, -32, -110, 66, -23, -5, 104, 96, 46, 43, -66, -110, 8, -70, 11, 51, -93, 19, 124, 81, 78, 58, -97, 89, -87, -26, -112, 95, -83, 118, -25, 118, 3, 35, -96, 120, -76, -87, 83, -101, 82]).buffer },{ "type": "public-key", "id": new Int8Array([1, 73, 82, 23, 76, -5, -53, -48, -104, -17, -42, 45, 7, 35, 35, -72, -7, 37, 9, 37, 117, 42, 66, 116, 58, 25, -68, 53, 113, -10, 102, 3, -60, -81, -74, 96, -5, 111, -56, 110, -101, -54, -31, -123, -100, 3, 37, -69, -114, -19, -25, -62, 18, 122, 39, 11, 83, 60, -58, 3, 116, 10, -80, -35, 6, -128, -51, -92, 100, -115, -22, -122, 21, -65, 97, 67, -49, 26, 42, -11, 90, 121, -63, 112, -16, 118, -99, -73, -89, -67, 72, -80, 18, -72, 109, 4, -14, 1, -93, 17, -17, -70, -2, -5, -116, 111, -128, 7, 62, -34, -43, 110, 89, 113, -65, -95, 113, -5, -104, -100, -73, 42, 2, 112, -21, 41, 41, 91, 108, -102, 47, -77, -52, 70, 107, -4, 25, -120, 114, 30, 23, 103, 120, 17, 55, 91, -110, -58, -110, 13, -56, 57, -126, 36, 40, 89, -9]).buffer }]'; +// eslint-disable-next-line +export const acceptableCredentials653 = + '{ "type": "public-key", "id": new Int8Array([53, -21, 26, -99, 5, 4, -112, -76, -126, 90, -35, -7, -31, -92, 19, -71, -39, 73, 52, -10, -14, -7, -59, -7, -36, -111, 64, 101, 29, 89, 90, -56, 108, 42, 32, -19, -113, 118, -114, 49, 109, -70, 68, -89, 36, -36, -103, -128, 34, -24, -40, -71, -125, -120, -80, 63, 25, -33, 2, 26, 111, -52, -15, -52]).buffer }'; +// eslint-disable-next-line +export const acceptableMultipleCredentials653 = + '{ "type": "public-key", "id": new Int8Array([17, 88, -12, 26, -50, -48, -38, 36, -69, -105, -68, -38, 66, -53, -37, -50, -109, -126, 122, 26, 25, -45, 96, 37, -124, 102, 124, 94, -98, -59, 113, 94, 115, -111, -69, 45, 37, -83, 118, -115, -4, -49, 34, 115, -24, -49, -37, -17, -127, -15, 62, 18, 93, 122, -109, 53, -52, 44, -63, -74, 109, 2, -110, 45]).buffer },{ "type": "public-key", "id": new Int8Array([1, 83, -98, 32, 110, -62, 78, 53, -63, -118, 12, 122, -72, -15, 85, 48, -39, -97, -73, 108, -122, -60, 56, -112, -89, -118, 111, 0, -4, 13, -50, -43, -53, 28, 114, 82, 22, -76, 15, 51, -95, 26, -90, -93, -51, 115, -28, 85, -105, -27, 111, 70, 106, -28, 45, 126, 44, 63, -16, 97, -55, 31, -24, 57, -92, 48, 26, 127, -39, 75, 12, 13, 100, -77, 13, -48, 49, 52, 31, 85, 9, 63, -122, -90, -54, -87, -110, -1, 115, -122, -69, -15, 83, 57, 95, -31, -92, -116, 89, -109, -98, 21, 24, -80, -28, 103, -28, -82, -39, 114, -29, -46, -76, 123, -69, -44, 124, 10, 53, -103, 19, -43, -12, 62, -83, -86, 95, -78, 70, -105, 116, -25, 106, 54, -72, -119, 91, 91, -71, -49, 22, 25, -108, 112, -14, 55, 9, 75, 89, -91, -59, 45]).buffer }'; +// eslint-disable-next-line +export const pubKeyCredParamsStr = + '[ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ]'; diff --git a/packages/journey-client/src/lib/fr-webauthn/helpers.test.ts b/packages/journey-client/src/lib/fr-webauthn/helpers.test.ts new file mode 100644 index 0000000000..bd2ad9ce68 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/helpers.test.ts @@ -0,0 +1,54 @@ +/* + * @forgerock/javascript-sdk + * + * helpers.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { parseCredentials, parsePubKeyArray } from './helpers.js'; +import { + allowCredentials70, + allowMultipleCredentials70, + acceptableCredentials653, + acceptableMultipleCredentials653, + pubKeyCredParamsStr, +} from './helpers.mock.data.js'; + +describe('Test WebAuthn helper functions', () => { + it('should parse the one credential in the MetadataCallback 7.0', () => { + const credentials = parseCredentials(allowCredentials70); + expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]'); + expect(credentials[0].type).toBe('public-key'); + }); + + it('should parse the two credentials in the MetadataCallback 7.0', () => { + const credentials = parseCredentials(allowMultipleCredentials70); + expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]'); + expect(credentials[0].type).toBe('public-key'); + expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]'); + expect(credentials[1].type).toBe('public-key'); + }); + + it('should parse the one credential in the MetadataCallback 6.5', () => { + const credentials = parseCredentials(acceptableCredentials653); + expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]'); + expect(credentials[0].type).toBe('public-key'); + }); + + it('should parse the two credentials in the MetadataCallback 6.5', () => { + const credentials = parseCredentials(acceptableMultipleCredentials653); + expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]'); + expect(credentials[0].type).toBe('public-key'); + expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]'); + expect(credentials[1].type).toBe('public-key'); + }); + + it('should parse the pubKeyCredParams in the MetadataCallback 7.0 & 6.5.3', () => { + const pubKeyCredParams = parsePubKeyArray(pubKeyCredParamsStr); + expect(pubKeyCredParams).toContainEqual({ type: 'public-key', alg: -7 }); + expect(pubKeyCredParams).toContainEqual({ type: 'public-key', alg: -257 }); + }); +}); diff --git a/packages/journey-client/src/lib/fr-webauthn/helpers.ts b/packages/journey-client/src/lib/fr-webauthn/helpers.ts new file mode 100644 index 0000000000..079ecb9599 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/helpers.ts @@ -0,0 +1,124 @@ +/* + * @forgerock/javascript-sdk + * + * helpers.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * @module + * @ignore + * These are private utility functions for HttpClient + */ +import { WebAuthnOutcomeType } from './enums.js'; +import type { ParsedCredential } from './interfaces.js'; + +function ensureArray(arr: RegExpMatchArray | null): string[] { + return arr || []; +} + +function arrayBufferToString(arrayBuffer: ArrayBuffer): string { + // https://goo.gl/yabPex - To future-proof, we'll pass along whatever the browser + // gives us and let AM disregard randomly-injected properties + const uint8Array = new Uint8Array(arrayBuffer); + const txtDecoder = new TextDecoder(); + + const json = txtDecoder.decode(uint8Array); + return json; +} + +function getIndexOne(arr: RegExpMatchArray | null): string { + return arr ? arr[1] : ''; +} + +// TODO: Remove this once AM is providing fully-serialized JSON +function parseCredentials(value: string): ParsedCredential[] { + try { + const creds = value + .split('}') + .filter((x) => !!x && x !== ']') + .map((x) => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const idArray = parseNumberArray(x); + return { + id: new Int8Array(idArray).buffer, + type: 'public-key' as PublicKeyCredentialType, + }; + }); + return creds; + } catch { + const e = new Error('Transforming credential object to string failed'); + e.name = WebAuthnOutcomeType.EncodingError; + throw e; + } +} + +function parseNumberArray(value: string): number[] { + const matches = /new Int8Array\((.+)\)/.exec(value); + if (matches === null || matches.length < 2) { + return []; + } + return JSON.parse(matches[1]); +} + +function parsePubKeyArray(value: string | unknown[]): PublicKeyCredentialParameters[] | undefined { + if (Array.isArray(value)) { + return value as PublicKeyCredentialParameters[]; + } + if (typeof value !== 'string') { + return undefined; + } + if (value && value[0] === '[') { + return JSON.parse(value); + } + value = value.replace(/(\w+):/g, '"$1":'); + return JSON.parse(`[${value}]`); +} + +function parseAllowCredentialsArray( + value: string | unknown[], +): PublicKeyCredentialDescriptor[] | undefined { + if (!value) { + return undefined; + } + if (Array.isArray(value)) { + return value as PublicKeyCredentialDescriptor[]; + } + if (typeof value !== 'string') { + return undefined; + } + if (value && value[0] === '[') { + return JSON.parse(value); + } + value = value.replace(/(\w+):/g, '"$1":'); + return JSON.parse(`[${value}]`); +} + +/** + * AM is currently serializing RP as one of the following formats, depending on + * whether RP ID has been configured: + * "relyingPartyId":"" + * "relyingPartyId":"rpId: \"foo\"," + * This regex handles both formats, but should be removed once AM is fixed. + */ +function parseRelyingPartyId(relyingPartyId: string): string { + if (relyingPartyId.includes('rpId')) { + return relyingPartyId.replace(/rpId: "(.+)",/, '$1'); + } else { + return relyingPartyId.replace(/id: "(.+)",/, '$1'); + } +} + +export { + ensureArray, + arrayBufferToString, + getIndexOne, + parseCredentials, + parseNumberArray, + parseAllowCredentialsArray, + parsePubKeyArray, + parseRelyingPartyId, +}; diff --git a/packages/journey-client/src/lib/fr-webauthn/index.ts b/packages/journey-client/src/lib/fr-webauthn/index.ts new file mode 100644 index 0000000000..373cea0752 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/index.ts @@ -0,0 +1,517 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { callbackType } from '@forgerock/sdk-types'; +import type HiddenValueCallback from '../callbacks/hidden-value-callback.js'; +import type MetadataCallback from '../callbacks/metadata-callback.js'; +import type FRStep from '../fr-step.js'; +import { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType } from './enums.js'; +import { + arrayBufferToString, + parseCredentials, + parsePubKeyArray, + parseRelyingPartyId, +} from './helpers.js'; +import type { + AttestationType, + RelyingParty, + WebAuthnAuthenticationMetadata, + WebAuthnCallbacks, + WebAuthnRegistrationMetadata, + WebAuthnTextOutputRegistration, +} from './interfaces.js'; +import type TextOutputCallback from '../callbacks/text-output-callback.js'; +import { parseWebAuthnAuthenticateText, parseWebAuthnRegisterText } from './script-parser.js'; + +// :::::: +type OutcomeWithName< + ClientId extends string, + Attestation extends AttestationType, + PubKeyCred extends PublicKeyCredential, + Name = '', +> = Name extends infer P extends string + ? `${ClientId}::${Attestation}::${PubKeyCred['id']}${P extends '' ? '' : `::${P}`}` + : never; +// JSON-based WebAuthn +type WebAuthnMetadata = WebAuthnAuthenticationMetadata | WebAuthnRegistrationMetadata; + +/** + * Utility for integrating a web browser's WebAuthn API. + * + * Example: + * + * ```js + * // Determine if a step is a WebAuthn step + * const stepType = FRWebAuthn.getWebAuthnStepType(step); + * if (stepType === WebAuthnStepType.Registration) { + * // Register a new device + * await FRWebAuthn.register(step); + * } else if (stepType === WebAuthnStepType.Authentication) { + * // Authenticate with a registered device + * await FRWebAuthn.authenticate(step); + * } + * ``` + */ +abstract class FRWebAuthn { + /** + * Determines if the given step is a WebAuthn step. + * + * @param step The step to evaluate + * @return A WebAuthnStepType value + */ + public static getWebAuthnStepType(step: FRStep): WebAuthnStepType { + const outcomeCallback = this.getOutcomeCallback(step); + const metadataCallback = this.getMetadataCallback(step); + const textOutputCallback = this.getTextOutputCallback(step); + + if (outcomeCallback && metadataCallback) { + const metadata = metadataCallback.getOutputValue('data') as { + pubKeyCredParams?: []; + }; + if (metadata?.pubKeyCredParams) { + return WebAuthnStepType.Registration; + } + + return WebAuthnStepType.Authentication; + } + + if (outcomeCallback && textOutputCallback) { + const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration; + + if (message.includes('pubKeyCredParams')) { + return WebAuthnStepType.Registration; + } + + return WebAuthnStepType.Authentication; + } else { + return WebAuthnStepType.None; + } + } + + /** + * Populates the step with the necessary authentication outcome. + * + * @param step The step that contains WebAuthn authentication data + * @return The populated step + */ + public static async authenticate(step: FRStep): Promise { + const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step); + if (hiddenCallback && (metadataCallback || textOutputCallback)) { + let outcome: ReturnType; + let credential: PublicKeyCredential | null = null; + + try { + let publicKey: PublicKeyCredentialRequestOptions; + if (metadataCallback) { + const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata; + publicKey = this.createAuthenticationPublicKey(meta); + + credential = await this.getAuthenticationCredential( + publicKey as PublicKeyCredentialRequestOptions, + ); + outcome = this.getAuthenticationOutcome(credential); + } else if (textOutputCallback) { + publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage()); + + credential = await this.getAuthenticationCredential( + publicKey as PublicKeyCredentialRequestOptions, + ); + outcome = this.getAuthenticationOutcome(credential); + } else { + throw new Error('No Credential found from Public Key'); + } + } catch (error) { + if (!(error instanceof Error)) throw error; + // NotSupportedError is a special case + if (error.name === WebAuthnOutcomeType.NotSupportedError) { + hiddenCallback.setInputValue(WebAuthnOutcome.Unsupported); + throw error; + } + hiddenCallback.setInputValue(`${WebAuthnOutcome.Error}::${error.name}:${error.message}`); + throw error; + } + + if (metadataCallback) { + const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata; + if (meta?.supportsJsonResponse && credential && 'authenticatorAttachment' in credential) { + hiddenCallback.setInputValue( + JSON.stringify({ + authenticatorAttachment: credential.authenticatorAttachment, + legacyData: outcome, + }), + ); + return step; + } + } + hiddenCallback.setInputValue(outcome); + return step; + } else { + const e = new Error('Incorrect callbacks for WebAuthn authentication'); + e.name = WebAuthnOutcomeType.DataError; + hiddenCallback?.setInputValue(`${WebAuthnOutcome.Error}::${e.name}:${e.message}`); + throw e; + } + } + /** + * Populates the step with the necessary registration outcome. + * + * @param step The step that contains WebAuthn registration data + * @return The populated step + */ + // Can make this generic const in Typescript 5.0 > and the name itself will + // be inferred from the type so `typeof deviceName` will not just return string + // but the actual name of the deviceName passed in as a generic. + public static async register( + step: FRStep, + deviceName?: T, + ): Promise { + const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step); + if (hiddenCallback && (metadataCallback || textOutputCallback)) { + let outcome: OutcomeWithName; + let credential: PublicKeyCredential | null = null; + + try { + let publicKey: PublicKeyCredentialRequestOptions; + if (metadataCallback) { + const meta = metadataCallback.getOutputValue('data') as WebAuthnRegistrationMetadata; + publicKey = this.createRegistrationPublicKey(meta); + credential = await this.getRegistrationCredential( + publicKey as PublicKeyCredentialCreationOptions, + ); + outcome = this.getRegistrationOutcome(credential); + } else if (textOutputCallback) { + publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage()); + credential = await this.getRegistrationCredential( + publicKey as PublicKeyCredentialCreationOptions, + ); + outcome = this.getRegistrationOutcome(credential); + } else { + throw new Error('No Credential found from Public Key'); + } + } catch (error) { + if (!(error instanceof Error)) throw error; + // NotSupportedError is a special case + if (error.name === WebAuthnOutcomeType.NotSupportedError) { + hiddenCallback.setInputValue(WebAuthnOutcome.Unsupported); + throw error; + } + hiddenCallback.setInputValue(`${WebAuthnOutcome.Error}::${error.name}:${error.message}`); + throw error; + } + + if (metadataCallback) { + const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata; + if (meta?.supportsJsonResponse && credential && 'authenticatorAttachment' in credential) { + hiddenCallback.setInputValue( + JSON.stringify({ + authenticatorAttachment: credential.authenticatorAttachment, + legacyData: + deviceName && deviceName.length > 0 ? `${outcome}::${deviceName}` : outcome, + }), + ); + return step; + } + } + + hiddenCallback.setInputValue( + deviceName && deviceName.length > 0 ? `${outcome}::${deviceName}` : outcome, + ); + return step; + } else { + const e = new Error('Incorrect callbacks for WebAuthn registration'); + e.name = WebAuthnOutcomeType.DataError; + hiddenCallback?.setInputValue(`${WebAuthnOutcome.Error}::${e.name}:${e.message}`); + throw e; + } + } + + /** + * Returns an object containing the two WebAuthn callbacks. + * + * @param step The step that contains WebAuthn callbacks + * @return The WebAuthn callbacks + */ + public static getCallbacks(step: FRStep): WebAuthnCallbacks { + const hiddenCallback = this.getOutcomeCallback(step); + const metadataCallback = this.getMetadataCallback(step); + const textOutputCallback = this.getTextOutputCallback(step); + + const returnObj: WebAuthnCallbacks = { + hiddenCallback, + }; + + if (metadataCallback) { + returnObj.metadataCallback = metadataCallback; + } else if (textOutputCallback) { + returnObj.textOutputCallback = textOutputCallback; + } + return returnObj; + } + + /** + * Returns the WebAuthn metadata callback containing data to pass to the browser + * Web Authentication API. + * + * @param step The step that contains WebAuthn callbacks + * @return The metadata callback + */ + public static getMetadataCallback(step: FRStep): MetadataCallback | undefined { + return step.getCallbacksOfType(callbackType.MetadataCallback).find((x) => { + const cb = x.getOutputByName('data', undefined); + // eslint-disable-next-line no-prototype-builtins + return cb && cb.hasOwnProperty('relyingPartyId'); + }); + } + + /** + * Returns the WebAuthn hidden value callback where the outcome should be populated. + * + * @param step The step that contains WebAuthn callbacks + * @return The hidden value callback + */ + public static getOutcomeCallback(step: FRStep): HiddenValueCallback | undefined { + return step + .getCallbacksOfType(callbackType.HiddenValueCallback) + .find((x) => x.getOutputByName('id', '') === 'webAuthnOutcome'); + } + + /** + * Returns the WebAuthn metadata callback containing data to pass to the browser + * Web Authentication API. + * + * @param step The step that contains WebAuthn callbacks + * @return The metadata callback + */ + public static getTextOutputCallback(step: FRStep): TextOutputCallback | undefined { + return step + .getCallbacksOfType(callbackType.TextOutputCallback) + .find((x) => { + const cb = x.getOutputByName('message', undefined); + return cb && cb.includes('webAuthnOutcome'); + }); + } + + /** + * Retrieves the credential from the browser Web Authentication API. + * + * @param options The public key options associated with the request + * @return The credential + */ + public static async getAuthenticationCredential( + options: PublicKeyCredentialRequestOptions, + ): Promise { + // Feature check before we attempt registering a device + if (!window.PublicKeyCredential) { + const e = new Error('PublicKeyCredential not supported by this browser'); + e.name = WebAuthnOutcomeType.NotSupportedError; + throw e; + } + const credential = await navigator.credentials.get({ publicKey: options }); + return credential as PublicKeyCredential; + } + + /** + * Converts an authentication credential into the outcome expected by OpenAM. + * + * @param credential The credential to convert + * @return The outcome string + */ + public static getAuthenticationOutcome( + credential: PublicKeyCredential | null, + ): + | OutcomeWithName + | OutcomeWithName { + if (credential === null) { + const e = new Error('No credential generated from authentication'); + e.name = WebAuthnOutcomeType.UnknownError; + throw e; + } + + try { + const clientDataJSON = arrayBufferToString(credential.response.clientDataJSON); + const assertionResponse = credential.response as AuthenticatorAssertionResponse; + const authenticatorData = new Int8Array( + assertionResponse.authenticatorData, + ).toString() as AttestationType; + const signature = new Int8Array(assertionResponse.signature).toString(); + + // Current native typing for PublicKeyCredential does not include `userHandle` + // eslint-disable-next-line + // @ts-ignore + const userHandle = arrayBufferToString(credential.response.userHandle); + + let stringOutput = + `${clientDataJSON}::${authenticatorData}::${signature}::${credential.id}` as OutcomeWithName< + string, + AttestationType, + PublicKeyCredential + >; + // Check if Username is stored on device + if (userHandle) { + stringOutput = `${stringOutput}::${userHandle}`; + return stringOutput as OutcomeWithName< + string, + AttestationType, + PublicKeyCredential, + string + >; + } + + return stringOutput; + } catch { + const e = new Error('Transforming credential object to string failed'); + e.name = WebAuthnOutcomeType.EncodingError; + throw e; + } + } + + /** + * Retrieves the credential from the browser Web Authentication API. + * + * @param options The public key options associated with the request + * @return The credential + */ + public static async getRegistrationCredential( + options: PublicKeyCredentialCreationOptions, + ): Promise { + // Feature check before we attempt registering a device + if (!window.PublicKeyCredential) { + const e = new Error('PublicKeyCredential not supported by this browser'); + e.name = WebAuthnOutcomeType.NotSupportedError; + throw e; + } + const credential = await navigator.credentials.create({ + publicKey: options, + }); + return credential as PublicKeyCredential; + } + + /** + * Converts a registration credential into the outcome expected by OpenAM. + * + * @param credential The credential to convert + * @return The outcome string + */ + public static getRegistrationOutcome( + credential: PublicKeyCredential | null, + ): OutcomeWithName { + if (credential === null) { + const e = new Error('No credential generated from registration'); + e.name = WebAuthnOutcomeType.UnknownError; + throw e; + } + + try { + const clientDataJSON = arrayBufferToString(credential.response.clientDataJSON); + const attestationResponse = credential.response as AuthenticatorAttestationResponse; + const attestationObject = new Int8Array( + attestationResponse.attestationObject, + ).toString() as AttestationType.Direct; + return `${clientDataJSON}::${attestationObject}::${credential.id}`; + } catch { + const e = new Error('Transforming credential object to string failed'); + e.name = WebAuthnOutcomeType.EncodingError; + throw e; + } + } + + /** + * Converts authentication tree metadata into options required by the browser + * Web Authentication API. + * + * @param metadata The metadata provided in the authentication tree MetadataCallback + * @return The Web Authentication API request options + */ + public static createAuthenticationPublicKey( + metadata: WebAuthnAuthenticationMetadata, + ): PublicKeyCredentialRequestOptions { + const { + acceptableCredentials, + allowCredentials, + challenge, + relyingPartyId, + timeout, + userVerification, + } = metadata; + const rpId = parseRelyingPartyId(relyingPartyId); + const allowCredentialsValue = parseCredentials(allowCredentials || acceptableCredentials || ''); + + return { + challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer, + timeout, + // only add key-value pair if proper value is provided + ...(allowCredentialsValue && { allowCredentials: allowCredentialsValue }), + ...(userVerification && { userVerification }), + ...(rpId && { rpId }), + }; + } + + /** + * Converts authentication tree metadata into options required by the browser + * Web Authentication API. + * + * @param metadata The metadata provided in the authentication tree MetadataCallback + * @return The Web Authentication API request options + */ + public static createRegistrationPublicKey( + metadata: WebAuthnRegistrationMetadata, + ): PublicKeyCredentialCreationOptions { + const { pubKeyCredParams: pubKeyCredParamsString } = metadata; + const pubKeyCredParams = parsePubKeyArray(pubKeyCredParamsString); + if (!pubKeyCredParams) { + const e = new Error('Missing pubKeyCredParams property from registration options'); + e.name = WebAuthnOutcomeType.DataError; + throw e; + } + const excludeCredentials = parseCredentials(metadata.excludeCredentials); + + const { + attestationPreference, + authenticatorSelection, + challenge, + relyingPartyId, + relyingPartyName, + timeout, + userId, + userName, + displayName, + } = metadata; + const rpId = parseRelyingPartyId(relyingPartyId); + const rp: RelyingParty = { + name: relyingPartyName, + ...(rpId && { id: rpId }), + }; + + return { + attestation: attestationPreference, + authenticatorSelection: JSON.parse(authenticatorSelection), + challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer, + ...(excludeCredentials.length && { excludeCredentials }), + pubKeyCredParams, + rp, + timeout, + user: { + displayName: displayName || userName, + id: Int8Array.from(userId.split('').map((c: string) => c.charCodeAt(0))), + name: displayName || userName, + }, + }; + } +} + +export default FRWebAuthn; +export type { + RelyingParty, + WebAuthnAuthenticationMetadata, + WebAuthnCallbacks, + WebAuthnRegistrationMetadata, +}; +export { WebAuthnOutcome, WebAuthnStepType }; diff --git a/packages/journey-client/src/lib/fr-webauthn/interfaces.ts b/packages/journey-client/src/lib/fr-webauthn/interfaces.ts new file mode 100644 index 0000000000..3521cb621a --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/interfaces.ts @@ -0,0 +1,125 @@ +/* + * @forgerock/javascript-sdk + * + * interfaces.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import type HiddenValueCallback from '../callbacks/hidden-value-callback.js'; +import type MetadataCallback from '../callbacks/metadata-callback.js'; +import type TextOutputCallback from '../callbacks/text-output-callback.js'; + +enum AttestationType { + Direct = 'direct', + Indirect = 'indirect', + None = 'none', +} + +interface DeviceStepState extends StepState { + value1: number; + value2: number; +} + +enum UserVerificationType { + Discouraged = 'discouraged', + Preferred = 'preferred', + Required = 'required', +} + +interface RelyingParty { + name: string; + id?: string; +} + +interface ResponseCredential { + response: { clientDataJSON: ArrayBuffer }; +} + +interface Step { + data?: TData; + state: TState; + type: StepType; +} + +interface StepState { + authId: string; +} + +enum StepType { + DeviceAuthentication = 'DeviceAuthentication', + DeviceRegistration = 'DeviceRegistration', + DeviceRegistrationChoice = 'DeviceRegistrationChoice', + LoginFailure = 'LoginFailure', + LoginSuccess = 'LoginSuccess', + OneTimePassword = 'OneTimePassword', + SecondFactorChoice = 'SecondFactorChoice', + Username = 'Username', + UsernamePassword = 'UsernamePassword', + UserPassword = 'UserPassword', +} + +interface WebAuthnRegistrationMetadata { + attestationPreference: 'none' | 'indirect' | 'direct'; + authenticatorSelection: string; + challenge: string; + excludeCredentials: string; + pubKeyCredParams: string; + relyingPartyId: string; + relyingPartyName: string; + timeout: number; + userId: string; + userName: string; + displayName?: string; + supportsJsonResponse?: boolean; +} + +interface WebAuthnAuthenticationMetadata { + acceptableCredentials?: string; + allowCredentials?: string; + challenge: string; + relyingPartyId: string; + timeout: number; + userVerification: UserVerificationType; + supportsJsonResponse?: boolean; +} + +interface WebAuthnCallbacks { + hiddenCallback?: HiddenValueCallback; + metadataCallback?: MetadataCallback; + textOutputCallback?: TextOutputCallback; +} + +type WebAuthnTextOutputRegistration = string; + +interface ParsedCredential { + /** + * The WebAuthn API (specifically `PublicKeyCredentialDescriptor['id']`) expects a `BufferSource` type. + * In current TypeScript environments, `SharedArrayBuffer` is not directly assignable to `BufferSource` + * due to missing properties like `resizable`, `resize`, etc. + * Although `SharedArrayBuffer` might have been implicitly compatible in older environments, + * explicitly using `ArrayBuffer` ensures strict type compatibility with the WebAuthn API. + * The `script-parser.ts` already converts the ID to an `ArrayBuffer` before use. + * + * See: + * - W3C WebAuthn Level 3: https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialdescriptor + * - MDN BufferSource: https://developer.mozilla.org/en-US/docs/Web/API/BufferSource + */ + id: ArrayBuffer; + type: 'public-key'; +} + +export type { + DeviceStepState, + ParsedCredential, + RelyingParty, + ResponseCredential, + Step, + WebAuthnCallbacks, + WebAuthnAuthenticationMetadata, + WebAuthnRegistrationMetadata, + WebAuthnTextOutputRegistration, +}; +export { AttestationType, StepType, UserVerificationType }; diff --git a/packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts b/packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts new file mode 100644 index 0000000000..9988255548 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts @@ -0,0 +1,123 @@ +/* + * @forgerock/javascript-sdk + * + * script-parser.test.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { parseWebAuthnAuthenticateText, parseWebAuthnRegisterText } from './script-parser.js'; +import { + authenticateInputWithRpidAndAllowCredentials, + authenticateInputWithRpidAndAllowCredentialsAndAllowRecoveryCode, + authenticateInputWithRpidAllowCredentialsAndQuotes, + authenticateInputWithoutRpidAndAllowCredentials, + authenticateInputWithAcceptableCredentialsWithoutRpid, + registerInputWithRpid, + registerInputWithRpidAndQuotes, + registerOutputWithRpid, + registerInputWithoutRpid, + registerOutputWithoutRpid, + registerInputWithExcludeCreds, +} from './script-text.mock.data.js'; + +describe('Parsing of the WebAuthn script type text', () => { + it('should parse the WebAuthn authenticate block of text with rpid and allow credentials', () => { + const obj = parseWebAuthnAuthenticateText(authenticateInputWithRpidAndAllowCredentials); + expect(obj.allowCredentials).toBeDefined(); + if (obj.allowCredentials) { + expect(obj.allowCredentials[0].type).toBe('public-key'); + expect(obj.allowCredentials[0].id.byteLength > 0).toBe(true); + } + expect(obj.challenge.byteLength > 0).toBe(true); + expect(obj.timeout).toBe(60000); + expect(obj.rpId).toBe('example.com'); + }); + it('should parse the WebAuthn authenticate block of text with rpid and allow credentials & Allow recovery code', () => { + const obj = parseWebAuthnAuthenticateText( + authenticateInputWithRpidAndAllowCredentialsAndAllowRecoveryCode, + ); + expect(obj.allowCredentials).toBeDefined(); + if (obj.allowCredentials) { + expect(obj.allowCredentials[0].type).toBe('public-key'); + expect(obj.allowCredentials[0].id.byteLength > 0).toBe(true); + } + expect(obj.challenge.byteLength > 0).toBe(true); + expect(obj.timeout).toBe(60000); + expect(obj.userVerification).toBe('preferred'); + }); + it('should parse the WebAuthn authenticate block of text with quoted keys', () => { + const obj = parseWebAuthnAuthenticateText(authenticateInputWithRpidAllowCredentialsAndQuotes); + expect(obj.allowCredentials).toBeDefined(); + if (obj.allowCredentials) { + expect(obj.allowCredentials[0].type).toBe('public-key'); + expect(obj.allowCredentials[0].id.byteLength > 0).toBe(true); + } + expect(obj.challenge.byteLength > 0).toBe(true); + expect(obj.timeout).toBe(60000); + expect(obj.rpId).toBe('example.com'); + }); + + it('should parse the WebAuthn authenticate block from 6.5.3 text', () => { + const obj = parseWebAuthnAuthenticateText( + authenticateInputWithAcceptableCredentialsWithoutRpid, + ); + expect(obj.allowCredentials).toBeDefined(); + if (obj.allowCredentials) { + expect(obj.allowCredentials[0].type).toBe('public-key'); + expect(obj.allowCredentials[0].id.byteLength > 0).toBe(true); + } + expect(obj.challenge.byteLength > 0).toBe(true); + expect(obj.timeout).toBe(60000); + }); + + it('should parse the WebAuthn authenticate block of text', () => { + const obj = parseWebAuthnAuthenticateText(authenticateInputWithoutRpidAndAllowCredentials); + expect(obj.allowCredentials).toBe(undefined); + expect(obj.rpId).toBe(undefined); + }); + + it('should parse the WebAuthn register block of text with rpid', () => { + const obj = parseWebAuthnRegisterText(registerInputWithRpid); + expect(obj.attestation).toBe(registerOutputWithRpid.attestation); + expect(obj.authenticatorSelection).toStrictEqual(registerOutputWithRpid.authenticatorSelection); + expect(obj.challenge.byteLength > 0).toBe(true); + expect(obj.pubKeyCredParams).toContainEqual(registerOutputWithRpid.pubKeyCredParams[0]); + expect(obj.pubKeyCredParams).toContainEqual(registerOutputWithRpid.pubKeyCredParams[1]); + expect(obj.rp).toStrictEqual(registerOutputWithRpid.rp); + expect(obj.timeout).toBe(registerOutputWithRpid.timeout); + expect(obj.user.displayName).toStrictEqual(registerOutputWithRpid.user.displayName); + expect(obj.user.name).toBe(registerOutputWithRpid.user.name); + expect(obj.user.id.byteLength > 0).toBe(true); + }); + + it('should parse the WebAuthn register block of text with rpid and quoted keys', () => { + const obj = parseWebAuthnRegisterText(registerInputWithRpidAndQuotes); + expect(obj.attestation).toBe(registerOutputWithRpid.attestation); + expect(obj.authenticatorSelection).toStrictEqual(registerOutputWithRpid.authenticatorSelection); + expect(obj.challenge.byteLength > 0).toBe(true); + expect(obj.pubKeyCredParams).toContainEqual(registerOutputWithRpid.pubKeyCredParams[0]); + expect(obj.pubKeyCredParams).toContainEqual(registerOutputWithRpid.pubKeyCredParams[1]); + expect(obj.rp).toStrictEqual(registerOutputWithRpid.rp); + expect(obj.timeout).toBe(registerOutputWithRpid.timeout); + expect(obj.user.displayName).toStrictEqual(registerOutputWithRpid.user.displayName); + expect(obj.user.name).toBe(registerOutputWithRpid.user.name); + expect(obj.user.id.byteLength > 0).toBe(true); + }); + + it('should parse the WebAuthn register block of text withOUT rpid', () => { + const obj = parseWebAuthnRegisterText(registerInputWithoutRpid); + expect(obj.rp).toStrictEqual(registerOutputWithoutRpid.rp); + }); + + it('should parse the WebAuthn register block of text with exclude creds', () => { + const obj = parseWebAuthnRegisterText(registerInputWithExcludeCreds); + expect(obj.excludeCredentials).toBeDefined(); + if (obj.excludeCredentials) { + expect(obj.excludeCredentials.length).toBe(2); + expect(obj.excludeCredentials[0].type).toBe('public-key'); + } + }); +}); diff --git a/packages/journey-client/src/lib/fr-webauthn/script-parser.ts b/packages/journey-client/src/lib/fr-webauthn/script-parser.ts new file mode 100644 index 0000000000..3e3b54d9a6 --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/script-parser.ts @@ -0,0 +1,192 @@ +/* eslint-disable no-useless-escape */ +/* + * @forgerock/javascript-sdk + * + * script-parser.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { WebAuthnOutcomeType } from './enums.js'; +import { ensureArray, getIndexOne, parsePubKeyArray, parseCredentials } from './helpers.js'; +import type { AttestationType, UserVerificationType } from './interfaces.js'; + +function parseWebAuthnRegisterText(text: string): PublicKeyCredentialCreationOptions { + const txtEncoder = new TextEncoder(); + + // TODO: Incrementally move to `*` instead of `{0,}` + // e.g. `attestation: "none"` + const attestation = getIndexOne(text.match(/attestation"{0,}:\s{0,}"(\w+)"/)) as AttestationType; + // e.g. `timeout: 60000` + const timeout = Number(getIndexOne(text.match(/timeout"{0,}:\s{0,}(\d+)/))); + // e.g. from 7.0: `"userVerification":"preferred"` + // e.g. from 6.5: `userVerification: "preferred"` + const userVerification = getIndexOne( + text.match(/userVerification"{0,}:\s{0,}"(\w+)"/), + ) as UserVerificationType; + // e.g. `"requireResidentKey":true` + const requireResidentKey = getIndexOne( + text.match(/requireResidentKey"{0,}:\s{0,}(\w+)/), + ) as string; + // e.g. `"authenticatorAttachment":"cross-platform"` + const authenticatorAttachment = getIndexOne( + text.match(/authenticatorAttachment"{0,}:\s{0,}"([\w-]+)/), + ) as AuthenticatorAttachment; + + // e.g. `rp: {\n id: \"https://user.example.com:3002\",\n name: \"ForgeRock\"\n }` + const rp = getIndexOne(text.match(/rp"{0,}:\s{0,}{([^}]+)}/)).trim(); + // e.g. `id: \"example.com\" + const rpId = getIndexOne(rp.match(/id"{0,}:\s{0,}"([^"]*)"/)); + // e.g. `name: \"ForgeRock\"` + const rpName = getIndexOne(rp.match(/name"{0,}:\s{0,}"([^"]*)"/)); + + // e.g. `user: {\n id: Uint8Array.from(\"NTdhN...RiNjI5\", + // function (c) { return c.charCodeAt(0) }),\n + // name: \"57a5b4e4-...-a4f2e5d4b629\",\n + // displayName: \"57a5b4e4-...-a4f2e5d4b629\"\n }` + const user = getIndexOne(text.match(/user"{0,}:\s{0,}{([^]{0,})},/)).trim(); + // e.g `id: Uint8Array.from(\"NTdhN...RiNjI5\",` + const userId = getIndexOne(user.match(/id"{0,}:\s{0,}Uint8Array.from\("([^"]+)"/)); + // e.g. `name: \"57a5b4e4-...-a4f2e5d4b629\",` + const userName = getIndexOne(user.match(/name"{0,}:\s{0,}"([\d\w._-]+)"/)); + // e.g. `displayName: \"57a5b4e4-...-a4f2e5d4b629\"` + const userDisplayName = getIndexOne(user.match(/displayName"{0,}:\s{0,}"([\d\w\s.@_-]+)"/)); + + // e.g. `pubKeyCredParams: [ + // { \"type\": \"public-key\", \"alg\": -257 }, { \"type\": \"public-key\", \"alg\": -7 } + // ]` + const pubKeyCredParamsString = getIndexOne( + // Capture the `pubKeyCredParams` without also matching `excludeCredentials` as well. + // `excludeCredentials` values are very similar to this property, so we need to make sure + // our last value doesn't end with "buffer", so we are only capturing objects that + // end in a digit and possibly a space. + text.match(/pubKeyCredParams"*:\s*\[([^]+\d\s*})\s*]/), + ).trim(); + // e.g. `{ \"type\": \"public-key\", \"alg\": -257 }, { \"type\": \"public-key\", \"alg\": -7 }` + const pubKeyCredParams = parsePubKeyArray(pubKeyCredParamsString); + if (!pubKeyCredParams) { + const e = new Error('Missing pubKeyCredParams property from registration options'); + e.name = WebAuthnOutcomeType.DataError; + throw e; + } + + // e.g. `excludeCredentials: [{ + // \"type\": \"public-key\", \"id\": new Int8Array([-18, 69, -99, 82, 38, -66]).buffer }, + // { \"type\": \"public-key\", \"id\": new Int8Array([64, 17, -15, 56, -32, 91]).buffer }],\n` + const excludeCredentialsString = getIndexOne( + text.match(/excludeCredentials"{0,}:\s{0,}\[([^]+)\s{0,}]/), + ).trim(); + // e.g. `{ \"type\": \"public-key\", \"id\": new Int8Array([-18, 69, -99, 82, 38, -66]).buffer }, + // { \"type\": \"public-key\", \"id\": new Int8Array([64, 17, -15, 56, -32, 91]).buffer }` + const excludeCredentials = parseCredentials(excludeCredentialsString); + + // e.g. `challenge: new Int8Array([87, -95, 18, ... -3, 49, 12, 81]).buffer,` + const challengeArr: string[] = ensureArray( + text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/), + ); + // e.g. `[87, -95, 18, ... -3, 49, 12, 81]` + const challengeJSON = JSON.parse(challengeArr[2]); + // e.g. [87, -95, 18, ... -3, 49, 12, 81] + const challenge = new Int8Array(challengeJSON).buffer; + + return { + attestation, + authenticatorSelection: { + userVerification, + // Only include authenticatorAttachment prop if the value is truthy + ...(authenticatorAttachment && { authenticatorAttachment }), + // Only include requireResidentKey prop if the value is of string "true" + ...(requireResidentKey === 'true' && { + requireResidentKey: !!requireResidentKey, + }), + }, + challenge, + ...(excludeCredentials.length && { excludeCredentials }), + pubKeyCredParams, + rp: { + name: rpName, + // only add key-value pair if truthy value is provided + ...(rpId && { id: rpId }), + }, + timeout, + user: { + displayName: userDisplayName, + id: txtEncoder.encode(userId), + name: userName, + }, + }; +} + +function parseWebAuthnAuthenticateText(text: string): PublicKeyCredentialRequestOptions { + let allowCredentials; + let allowCredentialsText; + + if (text.includes('acceptableCredentials')) { + // e.g. `var acceptableCredentials = [ + // { "type": "public-key", "id": new Int8Array([1, 97, 2, 123, ... -17]).buffer } + // ];` + allowCredentialsText = getIndexOne( + text.match(/acceptableCredentials"*\s*=\s*\[([^]+)\s*]/), + ).trim(); + } else { + // e.g. `allowCredentials: [ + // { \"type\": \"public-key\", + // \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer } + // ]` + allowCredentialsText = getIndexOne( + text.match(/allowCredentials"{0,}:\s{0,}\[([^]+)\s{0,}]/), + ).trim(); + } + // e.g. `"userVerification":"preferred"` + const userVerification = getIndexOne( + text.match(/userVerification"{0,}:\s{0,}"(\w+)"/), + ) as UserVerificationType; + + if (allowCredentialsText) { + // Splitting objects in array in case the user has multiple keys + const allowCredentialArr = allowCredentialsText.split('},') || [allowCredentialsText]; + // Iterating over array of substrings + allowCredentials = allowCredentialArr.map((str) => { + // e.g. `{ \"type\": \"public-key\", + const type = getIndexOne(str.match(/type"{0,}:\s{0,}"([\w-]+)"/)) as 'public-key'; + // e.g. \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer + const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/)); + // e.g. `[-107, 93, 68, -67, ... -19, 7, 4]` + const idJSON = JSON.parse(idArr[2]); + // e.g. [-107, 93, 68, -67, ... -19, 7, 4] + const id = new Int8Array(idJSON).buffer; + + return { + type, + id, + }; + }); + } + + // e.g. `timeout: 60000` + const timeout = Number(getIndexOne(text.match(/timeout"{0,}:\s{0,}(\d+)/))); + + // e.g. `challenge: new Int8Array([87, -95, 18, ... -3, 49, 12, 81]).buffer,` + const challengeArr: string[] = ensureArray( + text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/), + ); + // e.g. `[87, -95, 18, ... -3, 49, 12, 81]` + const challengeJSON = JSON.parse(challengeArr[2]); + // e.g. [87, -95, 18, ... -3, 49, 12, 81] + const challenge = new Int8Array(challengeJSON).buffer; + // e.g. `rpId: \"example.com\"` + const rpId = getIndexOne(text.match(/rpId"{0,}:\s{0,}\\{0,}"([^"\\]*)/)); + + return { + challenge, + timeout, + // only add key-value pairs if the truthy values are provided + ...(allowCredentials && { allowCredentials }), + ...(userVerification && { userVerification }), + ...(rpId && { rpId }), + }; +} + +export { parseWebAuthnAuthenticateText, parseWebAuthnRegisterText }; diff --git a/packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts b/packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts new file mode 100644 index 0000000000..9a22c381fe --- /dev/null +++ b/packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts @@ -0,0 +1,464 @@ +/* + * @forgerock/javascript-sdk + * + * script-text.mock.data.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/* eslint-disable max-len */ +export const authenticateInputWithRpidAndAllowCredentials = `/* +* Copyright 2018-2020 ForgeRock AS. All Rights Reserved +* +* Use of this code requires a commercial software license with ForgeRock AS. +* or with one of its affiliates. All use shall be exclusively subject +* to such license between the licensee and ForgeRock AS. +*/ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var options = { + rpId: "example.com", + challenge: new Int8Array([14, 126, -110, -74, 64, -66, 20, -56, -40, -28, 116, -61, -128, -20, 72, 24, 42, 79, -105, 94, -84, -12, -17, -97, 105, -31, -30, 92, 55, 67, -83, 65]).buffer, + timeout: 60000, + allowCredentials: [{ type: "public-key", id: new Int8Array([-107, 93, 68, -67, -5, 107, 18, 16, -25, -30, 80, 103, -75, -53, -2, -95, 102, 42, 47, 126, -1, 85, 93, 45, -85, 8, -108, 107, 47, -25, 66, 12, -96, 81, 104, -127, 26, -59, -69, -23, 75, 89, 58, 124, -93, 4, 28, -128, 121, 35, 39, 103, -86, -86, 123, -67, -7, -4, 79, -49, 127, -19, 7, 4]).buffer }] +}; + +navigator.credentials.get({ "publicKey" : options }) + .then(function (assertion) { + var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON)); + var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString(); + var signature = new Int8Array(assertion.response.signature).toString(); + var rawId = assertion.id; + var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle)); + document.getElementById('webAuthnOutcome').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const authenticateInputWithRpidAllowCredentialsAndQuotes = `/* +* Copyright 2018-2020 ForgeRock AS. All Rights Reserved +* +* Use of this code requires a commercial software license with ForgeRock AS. +* or with one of its affiliates. All use shall be exclusively subject +* to such license between the licensee and ForgeRock AS. +*/ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var options = { + "rpId": "example.com", + "challenge": new Int8Array([14, 126, -110, -74, 64, -66, 20, -56, -40, -28, 116, -61, -128, -20, 72, 24, 42, 79, -105, 94, -84, -12, -17, -97, 105, -31, -30, 92, 55, 67, -83, 65]).buffer, + "timeout": 60000, + "allowCredentials": [{ "type": "public-key", "id": new Int8Array([-107, 93, 68, -67, -5, 107, 18, 16, -25, -30, 80, 103, -75, -53, -2, -95, 102, 42, 47, 126, -1, 85, 93, 45, -85, 8, -108, 107, 47, -25, 66, 12, -96, 81, 104, -127, 26, -59, -69, -23, 75, 89, 58, 124, -93, 4, 28, -128, 121, 35, 39, 103, -86, -86, 123, -67, -7, -4, 79, -49, 127, -19, 7, 4]).buffer }] +}; + +navigator.credentials.get({ "publicKey" : options }) + .then(function (assertion) { + var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON)); + var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString(); + var signature = new Int8Array(assertion.response.signature).toString(); + var rawId = assertion.id; + var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle)); + document.getElementById('webAuthnOutcome').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const authenticateInputWithoutRpidAndAllowCredentials = `/* +* Copyright 2018-2020 ForgeRock AS. All Rights Reserved +* +* Use of this code requires a commercial software license with ForgeRock AS. +* or with one of its affiliates. All use shall be exclusively subject +* to such license between the licensee and ForgeRock AS. +*/ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var options = { + challenge: new Int8Array([14, 126, -110, -74, 64, -66, 20, -56, -40, -28, 116, -61, -128, -20, 72, 24, 42, 79, -105, 94, -84, -12, -17, -97, 105, -31, -30, 92, 55, 67, -83, 65]).buffer, + timeout: 60000, +}; + +navigator.credentials.get({ "publicKey" : options }) + .then(function (assertion) { + var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON)); + var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString(); + var signature = new Int8Array(assertion.response.signature).toString(); + var rawId = assertion.id; + var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle)); + document.getElementById('webAuthnOutcome').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +// AM 6.5.3 variant of JS text string +export const authenticateInputWithAcceptableCredentialsWithoutRpid = `/* +* Copyright 2018-2020 ForgeRock AS. All Rights Reserved +* +* Use of this code requires a commercial software license with ForgeRock AS. +* or with one of its affiliates. All use shall be exclusively subject +* to such license between the licensee and ForgeRock AS. +*/ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var acceptableCredentials = [ + { "type": "public-key", "id": new Int8Array([1, 97, 2, 123, -105, -19, -106, 10, -86, 82, -23, 5, 52, 63, 103, 110, -71, 53, 107, 104, 76, -42, -49, 96, 67, -114, -97, 19, -59, 89, -102, -115, -110, -101, -6, -98, 39, -75, 2, 74, 23, -105, 67, 6, -112, 21, -3, 36, -114, 52, 35, 75, 74, 82, -8, 115, -128, -34, -105, 110, 124, 41, -79, -53, -90, 81, -11, -7, 91, -45, -67, -82, 106, 74, 30, 112, 100, -47, 54, -12, 95, 81, 97, 36, 123, -15, -91, 87, -82, 87, -45, -103, -80, 109, -111, 82, 109, 58, 50, 19, -21, -102, 54, -108, -68, 12, -101, -53, -65, 11, -94, -36, 112, -101, -95, -90, -118, 68, 13, 8, -49, -77, -28, -82, -97, 126, -71, 33, -58, 19, 58, -118, 36, -28, 22, -55, 64, -72, -80, -9, -48, -50, 58, -52, 64, -64, -27, -5, -12, 110, -95, -17]).buffer } +]; + +var options = { + + challenge: new Int8Array([-42, -21, -101, -22, -35, 94, 14, 33, -75, -12, -113, 86, 109, -51, 62, 89, 29, 119, 48, -92, 33, -64, 102, -18, 18, -122, 73, 13, -17, -50, -22, -74]).buffer, + timeout: 60000, + userVerification: "preferred", + allowCredentials: acceptableCredentials +}; + +navigator.credentials.get({ "publicKey" : options }) + .then(function (assertion) { + var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON)); + var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString(); + var signature = new Int8Array(assertion.response.signature).toString(); + var rawId = assertion.id; + document.getElementById('webAuthnOutcome').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const registerInputWithRpid = `/* + * Copyright 2018-2020 ForgeRock AS. All Rights Reserved + * + * Use of this code requires a commercial software license with ForgeRock AS. + * or with one of its affiliates. All use shall be exclusively subject + * to such license between the licensee and ForgeRock AS. + */ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var publicKey = { + challenge: new Int8Array([102, -15, -36, -101, -95, 10, -20, 39, 29, 70, 122, 25, 53, 83, 72, -38, 83, -92, 31, -30, 26, -94, 92, -94, -83, 7, 82, -66, -125, -95, -4, -75]).buffer, + // Relying Party: + rp: { + id: "example.com", + name: "ForgeRock" + }, + // User: + user: { + id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }), + name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629", + displayName: "bob_lee-tester@me.co.uk" + }, + // Below pubKeyCredParams format represents AM 6.5 + pubKeyCredParams: [ + { + type: "public-key", + alg: -7 + } + ,{ + type: "public-key", + alg: -257 + } + ], + attestation: "none", + timeout: 60000, + excludeCredentials: [], + authenticatorSelection: { + userVerification: "preferred" + authenticatorAttachment:"cross-platform" + } +}; + +navigator.credentials.create({publicKey: publicKey}) + .then(function (newCredentialInfo) { + var rawId = newCredentialInfo.id; + var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON)); + var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString(); + document.getElementById('webAuthnOutcome').value = clientData + "::" + keyData + "::" + rawId; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const registerInputWithRpidAndQuotes = `/* + * Copyright 2018-2020 ForgeRock AS. All Rights Reserved + * + * Use of this code requires a commercial software license with ForgeRock AS. + * or with one of its affiliates. All use shall be exclusively subject + * to such license between the licensee and ForgeRock AS. + */ + + if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); + } + + var publicKey = { + "challenge": new Int8Array([102, -15, -36, -101, -95, 10, -20, 39, 29, 70, 122, 25, 53, 83, 72, -38, 83, -92, 31, -30, 26, -94, 92, -94, -83, 7, 82, -66, -125, -95, -4, -75]).buffer, + // Relying Party: + "rp": { + "id": "example.com", + "name": "ForgeRock" + }, + // User: + "user": { + "id": Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }), + "name": "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629", + "displayName": "bob_lee-tester@me.co.uk" + }, + "pubKeyCredParams": [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ], + "attestation": "none", + "timeout": 60000, + "excludeCredentials": [], + "authenticatorSelection": {"userVerification":"preferred","authenticatorAttachment":"cross-platform"} + }; + + navigator.credentials.create({publicKey: publicKey}) + .then(function (newCredentialInfo) { + var rawId = newCredentialInfo.id; + var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON)); + var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString(); + document.getElementById('webAuthnOutcome').value = clientData + "::" + keyData + "::" + rawId; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const registerOutputWithRpid = { + attestation: 'none', + authenticatorSelection: { + userVerification: 'preferred', + authenticatorAttachment: 'cross-platform', + }, + challenge: [ + /* don't directly test */ + ], + pubKeyCredParams: [ + { type: 'public-key', alg: -257 }, + { type: 'public-key', alg: -7 }, + ], + rp: { id: 'example.com', name: 'ForgeRock' }, + timeout: 60000, + user: { + displayName: 'bob_lee-tester@me.co.uk', + id: [ + /* don't directly test */ + ], + name: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + }, +}; + +export const registerInputWithoutRpid = `/* + * Copyright 2018-2020 ForgeRock AS. All Rights Reserved + * + * Use of this code requires a commercial software license with ForgeRock AS. + * or with one of its affiliates. All use shall be exclusively subject + * to such license between the licensee and ForgeRock AS. + */ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var publicKey = { + challenge: new Int8Array([102, -15, -36, -101, -95, 10, -20, 39, 29, 70, 122, 25, 53, 83, 72, -38, 83, -92, 31, -30, 26, -94, 92, -94, -83, 7, 82, -66, -125, -95, -4, -75]).buffer, + // Relying Party: + rp: { + name: "ForgeRock" + }, + // User: + user: { + id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }), + name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629", + displayName: "Bob Tester" + }, + pubKeyCredParams: [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ], + attestation: "none", + timeout: 60000, + excludeCredentials: [], + authenticatorSelection: {"userVerification":"preferred"} +}; + +navigator.credentials.create({publicKey: publicKey}) + .then(function (newCredentialInfo) { + var rawId = newCredentialInfo.id; + var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON)); + var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString(); + document.getElementById('webAuthnOutcome').value = clientData + "::" + keyData + "::" + rawId; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const registerOutputWithoutRpid = { + attestation: 'none', + authenticatorSelection: { userVerification: 'preferred' }, + challenge: [ + /* don't directly test */ + ], + pubKeyCredParams: [ + { type: 'public-key', alg: -257 }, + { type: 'public-key', alg: -7 }, + ], + rp: { name: 'ForgeRock' }, + timeout: 60000, + user: { + displayName: 'Bob Tester', + id: [ + /* don't directly test */ + ], + name: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + }, +}; + +export const registerInputWithExcludeCreds = `/* + * Copyright 2018-2020 ForgeRock AS. All Rights Reserved + * + * Use of this code requires a commercial software license with ForgeRock AS. + * or with one of its affiliates. All use shall be exclusively subject + * to such license between the licensee and ForgeRock AS. + */ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var publicKey = { + challenge: new Int8Array([102, -15, -36, -101, -95, 10, -20, 39, 29, 70, 122, 25, 53, 83, 72, -38, 83, -92, 31, -30, 26, -94, 92, -94, -83, 7, 82, -66, -125, -95, -4, -75]).buffer, + // Relying Party: + rp: { + name: "ForgeRock" + }, + // User: + user: { + id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }), + name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629", + displayName: "Bob Tester" + }, + pubKeyCredParams: [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ], + attestation: "none", + timeout: 60000, + excludeCredentials: [{ "type": "public-key", "id": new Int8Array([49, -96, -107, 113, 106, 5, 115, 22, 68, 121, -85, -27, 8, -58, -113, 127, -105, -37, -10, -12, -58, -25, 29, -82, -18, 69, -99, 125, 33, 82, 38, -66, -27, -128, -91, -86, 87, 68, 94, 0, -78, 70, -11, -70, -14, -53, 38, -60, 46, 27, 66, 46, 21, -125, -70, 123, -46, -124, 86, -2, 102, 70, -52, 54]).buffer },{ "type": "public-key", "id": new Int8Array([64, 17, -15, -123, -21, 127, 76, -120, 90, -112, -5, 54, 105, 93, 82, -104, -79, 107, -69, -3, -113, -94, -59, -4, 126, -33, 117, 32, -44, 122, -97, 8, -112, 105, -96, 96, 90, 44, -128, -121, 107, 79, -98, -68, -93, 11, -105, -47, 102, 13, 110, 84, 59, -91, -30, 37, -3, -22, 39, 111, -10, 87, -50, -35]).buffer }], + authenticatorSelection: {"userVerification":"preferred"} +}; + +navigator.credentials.create({publicKey: publicKey}) + .then(function (newCredentialInfo) { + var rawId = newCredentialInfo.id; + var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON)); + var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString(); + document.getElementById('webAuthnOutcome').value = clientData + "::" + keyData + "::" + rawId; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + document.getElementById("loginButton_0").click(); + });`; + +export const registerOutputWithExcludeCreds = { + attestation: 'none', + authenticatorSelection: { userVerification: 'preferred' }, + challenge: [ + /* don't directly test */ + ], + excludeCredentials: [ + { + type: 'public-key', + id: 'Do not directly test', + }, + { + type: 'public-key', + id: 'Do not directly test', + }, + ], + pubKeyCredParams: [ + { type: 'public-key', alg: -257 }, + { type: 'public-key', alg: -7 }, + ], + rp: { name: 'ForgeRock' }, + timeout: 60000, + user: { + displayName: 'Bob Tester', + id: [ + /* don't directly test */ + ], + name: '57a5b4e4-6999-4b45-bf86-a4f2e5d4b629', + }, +}; + +export const authenticateInputWithRpidAndAllowCredentialsAndAllowRecoveryCode = `/* + * Copyright 2018-2022 ForgeRock AS. All Rights Reserved + * + * Use of this code requires a commercial software license with ForgeRock AS. + * or with one of its affiliates. All use shall be exclusively subject + * to such license between the licensee and ForgeRock AS. + */ + +if (!window.PublicKeyCredential) { + document.getElementById('webAuthnOutcome').value = "unsupported"; + document.getElementById("loginButton_0").click(); +} + +var options = { + + challenge: new Int8Array([-17, -117, -10, 120, -90, 127, 70, -73, 114, -37, -94, 126, -96, -111, -65, 78, -84, 53, 74, -18, 93, 102, 24, -77, -97, -6, -106, -10, -101, -29, 36, -33]).buffer, + timeout: 60000, + userVerification: "preferred", + allowCredentials: [{ "type": "public-key", "id": new Int8Array([-33, 59, -68, 121, 57, -27, -33, -40, 55, 8, -65, -15, -40, -103, 73, 61, 49, 56, 65, -84, -27, -86, -103, -115, 15, 43, -64, -60, -105, 81, -111, 115, 105, 111, -105, 64, 73, 55, -35, 35, 38, 59, -91, 95, 64, 30, -10, -6, -91, -59, 26, 19, -3, 2, -39, 71, 112, 124, -66, -89, -10, -35, 122, 103]).buffer }] +}; + +navigator.credentials.get({ "publicKey" : options }) + .then(function (assertion) { + var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON)); + var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString(); + var signature = new Int8Array(assertion.response.signature).toString(); + var rawId = assertion.id; + var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle)); + document.getElementById('webAuthnOutcome').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle; + document.getElementById("loginButton_0").click(); + }).catch(function (err) { + document.getElementById('webAuthnOutcome').value = "ERROR" + "::" + err; + var allowRecoveryCode = 'true' === "true"; + if (allowRecoveryCode) { + var loginButton = document.getElementById("loginButton_0"); + if (loginButton) { + var prev = loginButton.previousElementSibling; + if (prev && prev.nodeName == "DIV") { + prev.getElementsByTagName("div")[0].innerHTML = " " + + err + ""; + } + } + } else { + document.getElementById("loginButton_0").click(); + } + }); +`; diff --git a/packages/journey-client/src/lib/interfaces.ts b/packages/journey-client/src/lib/interfaces.ts index cc60086f09..042e0cb194 100644 --- a/packages/journey-client/src/lib/interfaces.ts +++ b/packages/journey-client/src/lib/interfaces.ts @@ -1,157 +1,13 @@ /* - * @forgerock/javascript-sdk + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. * - * interfaces.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import type { StepType } from './enums.js'; - -/** - * Types of callbacks directly supported by the SDK. - * TODO: We should avoid enums. - */ -export enum CallbackType { - BooleanAttributeInputCallback = 'BooleanAttributeInputCallback', - ChoiceCallback = 'ChoiceCallback', - ConfirmationCallback = 'ConfirmationCallback', - DeviceProfileCallback = 'DeviceProfileCallback', - HiddenValueCallback = 'HiddenValueCallback', - KbaCreateCallback = 'KbaCreateCallback', - MetadataCallback = 'MetadataCallback', - NameCallback = 'NameCallback', - NumberAttributeInputCallback = 'NumberAttributeInputCallback', - PasswordCallback = 'PasswordCallback', - PingOneProtectEvaluationCallback = 'PingOneProtectEvaluationCallback', - PingOneProtectInitializeCallback = 'PingOneProtectInitializeCallback', - PollingWaitCallback = 'PollingWaitCallback', - ReCaptchaCallback = 'ReCaptchaCallback', - ReCaptchaEnterpriseCallback = 'ReCaptchaEnterpriseCallback', - RedirectCallback = 'RedirectCallback', - SelectIdPCallback = 'SelectIdPCallback', - StringAttributeInputCallback = 'StringAttributeInputCallback', - SuspendedTextOutputCallback = 'SuspendedTextOutputCallback', - TermsAndConditionsCallback = 'TermsAndConditionsCallback', - TextInputCallback = 'TextInputCallback', - TextOutputCallback = 'TextOutputCallback', - ValidatedCreatePasswordCallback = 'ValidatedCreatePasswordCallback', - ValidatedCreateUsernameCallback = 'ValidatedCreateUsernameCallback', -} -/** - * Base interface for all types of authentication step responses. - */ -interface AuthResponse { - type: StepType; -} - -/** - * Represents details of a failure in an authentication step. - */ -interface FailureDetail { - failureUrl?: string; -} - -/** - * Represents the authentication tree API payload schema. - */ -interface Step { - authId?: string; - callbacks?: Callback[]; - code?: number; - description?: string; - detail?: StepDetail; - header?: string; - message?: string; - ok?: string; - realm?: string; - reason?: string; - stage?: string; - status?: number; - successUrl?: string; - tokenId?: string; -} - -/** - * Represents details of a failure in an authentication step. - */ -interface StepDetail { - failedPolicyRequirements?: FailedPolicyRequirement[]; - failureUrl?: string; - result?: boolean; -} +import type { StepOptions, Step } from '@forgerock/sdk-types'; -/** - * Represents failed policies for a matching property. - */ -interface FailedPolicyRequirement { - policyRequirements: PolicyRequirement[]; - property: string; +export interface NextOptions { + step: Step; + options?: StepOptions; } - -/** - * Represents a failed policy policy and failed policy params. - */ -interface PolicyRequirement { - params?: Partial; - policyRequirement: string; -} - -interface PolicyParams { - [key: string]: unknown; - disallowedFields: string; - duplicateValue: string; - forbiddenChars: string; - maxLength: number; - minLength: number; - numCaps: number; - numNums: number; -} - -/** - * Represents the authentication tree API callback schema. - */ -interface Callback { - _id?: number; - input?: NameValue[]; - output: NameValue[]; - type: CallbackType; -} - -/** - * Represents a name/value pair found in an authentication tree callback. - */ -interface NameValue { - name: string; - value: unknown; -} - -type ConfigurablePaths = keyof CustomPathConfig; -/** - * Optional configuration for custom paths for actions - */ -interface CustomPathConfig { - authenticate?: string; - authorize?: string; - accessToken?: string; - endSession?: string; - userInfo?: string; - revoke?: string; - sessions?: string; -} - -export type { - CustomPathConfig, - ConfigurablePaths, - Callback, - FailedPolicyRequirement, - NameValue, - PolicyParams, - PolicyRequirement, - Step, - StepDetail, - AuthResponse, - FailureDetail, -}; diff --git a/packages/journey-client/src/lib/journey-client.test.ts b/packages/journey-client/src/lib/journey-client.test.ts new file mode 100644 index 0000000000..60cc5a98f4 --- /dev/null +++ b/packages/journey-client/src/lib/journey-client.test.ts @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { afterEach, describe, expect, test, vi } from 'vitest'; +import { journey } from './journey-client.js'; +import FRStep from './fr-step.js'; +import { JourneyClientConfig } from './config.types.js'; +import { callbackType, Step } from '@forgerock/sdk-types'; + +// Create a singleton mock instance for storage +const mockStorageInstance = { + get: vi.fn(), + set: vi.fn(), + remove: vi.fn(), +}; + +// Mock dependencies with side effects +vi.mock('@forgerock/storage', () => ({ + createStorage: vi.fn(() => mockStorageInstance), +})); + +vi.mock('./fr-device/index.js', () => ({ + default: vi.fn(() => ({ + getProfile: vi.fn().mockResolvedValue({}), + })), +})); + +// Mock the fetch API to control responses +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +const mockConfig: JourneyClientConfig = { + serverConfig: { + baseUrl: 'https://test.com', + }, + realmPath: 'root', +}; + +describe('journey-client', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + test('should initialize and return a client object with all methods', async () => { + const client = await journey({ config: mockConfig }); + expect(client).toBeDefined(); + expect(client.start).toBeInstanceOf(Function); + expect(client.next).toBeInstanceOf(Function); + expect(client.redirect).toBeInstanceOf(Function); + expect(client.resume).toBeInstanceOf(Function); + }); + + test('start() should fetch and return the first step', async () => { + const mockStepResponse: Step = { callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); + + const client = await journey({ config: mockConfig }); + const step = await client.start(); + expect(step).toBeDefined(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate + expect(request.url).toBe('https://test.com/json/realms/root/authenticate'); + expect(step).toBeInstanceOf(FRStep); + expect(step && step.payload).toEqual(mockStepResponse); + }); + + test('next() should send the current step and return the next step', async () => { + const initialStepPayload: Step = { + callbacks: [ + { + type: callbackType.NameCallback, + input: [{ name: 'IDToken1', value: 'test-user' }], + output: [], + }, + ], + }; + const nextStepPayload: Step = { + callbacks: [ + { + type: callbackType.PasswordCallback, + input: [{ name: 'IDToken2', value: 'test-password' }], + output: [], + }, + ], + }; + const initialStep = new FRStep(initialStepPayload); + + mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); + + const client = await journey({ config: mockConfig }); + const nextStep = await client.next(initialStep.payload, {}); + expect(nextStep).toBeDefined(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + // TODO: This should be /journeys?_action=next, but the current implementation calls /authenticate + expect(request.url).toBe('https://test.com/json/realms/root/authenticate'); + expect(request.method).toBe('POST'); + expect(await request.json()).toEqual(initialStep.payload); + expect(nextStep).toBeInstanceOf(FRStep); + expect(nextStep && nextStep.payload).toEqual(nextStepPayload); + }); + + test('redirect() should store the step and call location.assign', async () => { + const mockStepPayload: Step = { + callbacks: [ + { + type: callbackType.RedirectCallback, + input: [], + output: [{ name: 'redirectUrl', value: 'https://sso.com/redirect' }], + }, + ], + }; + const step = new FRStep(mockStepPayload); + + const assignMock = vi.fn(); + const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({ + ...window.location, + assign: assignMock, + }); + + const client = await journey({ config: mockConfig }); + await client.redirect(step); + + expect(mockStorageInstance.set).toHaveBeenCalledWith({ step: step.payload }); + expect(assignMock).toHaveBeenCalledWith('https://sso.com/redirect'); + + locationSpy.mockRestore(); + }); + + describe('resume()', () => { + test('should call next() with URL params when a previous step is in storage', async () => { + const previousStepPayload: Step = { + callbacks: [{ type: callbackType.RedirectCallback, input: [], output: [] }], + }; + mockStorageInstance.get.mockResolvedValue({ step: previousStepPayload }); + + const nextStepPayload: Step = { callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); + + const client = await journey({ config: mockConfig }); + const resumeUrl = 'https://app.com/callback?code=123&state=abc'; + + const step = await client.resume(resumeUrl, {}); + expect(step).toBeDefined(); + + expect(mockStorageInstance.get).toHaveBeenCalledTimes(1); + expect(mockStorageInstance.remove).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + + // TODO: This should be /journeys?_action=next, but the current implementation calls /authenticate + const url = new URL(request.url); + expect(url.origin + url.pathname).toBe('https://test.com/json/realms/root/authenticate'); + expect(url.searchParams.get('code')).toBe('123'); + expect(url.searchParams.get('state')).toBe('abc'); + + expect(request.method).toBe('POST'); + expect(await request.json()).toEqual(previousStepPayload); + expect(step).toBeInstanceOf(FRStep); + expect(step && step.payload).toEqual(nextStepPayload); + }); + + test('should correctly resume with a plain Step object from storage', async () => { + const plainStepPayload: Step = { + callbacks: [ + { type: callbackType.TextOutputCallback, output: [{ name: 'message', value: 'Hello' }] }, + ], + stage: 'testStage', + }; + mockStorageInstance.get.mockResolvedValue({ step: plainStepPayload }); + + const nextStepPayload: Step = { callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); + + const client = await journey({ config: mockConfig }); + const resumeUrl = 'https://app.com/callback?code=123&state=abc'; + + const step = await client.resume(resumeUrl, {}); + expect(step).toBeDefined(); + + expect(mockStorageInstance.get).toHaveBeenCalledTimes(1); + expect(mockStorageInstance.remove).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledTimes(1); + + const request = mockFetch.mock.calls[0][0] as Request; + expect(request.method).toBe('POST'); + expect(await request.json()).toEqual(plainStepPayload); // Expect the plain payload to be sent + expect(step).toBeInstanceOf(FRStep); // The returned step should still be an FRStep instance + expect(step && step.payload).toEqual(nextStepPayload); + }); + + test('should throw an error if a previous step is required but not found', async () => { + mockStorageInstance.get.mockResolvedValue(undefined); + + const client = await journey({ config: mockConfig }); + const resumeUrl = 'https://app.com/callback?code=123&state=abc'; + + await expect(client.resume(resumeUrl)).rejects.toThrow( + 'Error: previous step information not found in storage for resume operation.', + ); + expect(mockStorageInstance.get).toHaveBeenCalledTimes(1); + expect(mockStorageInstance.remove).toHaveBeenCalledTimes(1); + }); + + test('should call start() with URL params when no previous step is required', async () => { + mockStorageInstance.get.mockResolvedValue(undefined); + + const mockStepResponse: Step = { callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); + + const client = await journey({ config: mockConfig }); + const resumeUrl = 'https://app.com/callback?foo=bar'; + + const step = await client.resume(resumeUrl, {}); + expect(step).toBeDefined(); + + expect(mockStorageInstance.get).not.toHaveBeenCalled(); + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + + // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate + const url = new URL(request.url); + expect(url.origin + url.pathname).toBe('https://test.com/json/realms/root/authenticate'); + expect(step).toBeInstanceOf(FRStep); + expect(step && step.payload).toEqual(mockStepResponse); + }); + }); +}); diff --git a/packages/journey-client/src/lib/journey-client.ts b/packages/journey-client/src/lib/journey-client.ts index 73a6b2c14d..06cd29efdc 100644 --- a/packages/journey-client/src/lib/journey-client.ts +++ b/packages/journey-client/src/lib/journey-client.ts @@ -1,196 +1,140 @@ /* - * @forgerock/javascript-sdk + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. * - * index.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import Config, { StepOptions } from '../config.js'; -import Auth from '../auth/index.js'; -import { CallbackType } from './interfaces.js'; -import type RedirectCallback from './callbacks/redirect-callback.js'; -import FRLoginFailure from './fr-login-failure.js'; -import FRLoginSuccess from './fr-login-success.js'; +import { createJourneyStore } from './journey.store.js'; +import { JourneyClientConfig, StepOptions } from './config.types.js'; +import { journeyApi } from './journey.api.js'; +import { setConfig } from './journey.slice.js'; +import { createStorage } from '@forgerock/storage'; +import { GenericError, callbackType, type Step } from '@forgerock/sdk-types'; import FRStep from './fr-step.js'; - -/** - * Provides access to the OpenAM authentication tree API. - */ -abstract class FRAuth { - public static get previousStepKey() { - return `${Config.get().prefix}-PreviousStep`; - } - - /** - * Requests the next step in the authentication tree. - * - * Call `FRAuth.next()` recursively. At each step, check for session token or error, otherwise - * populate the step's callbacks and call `next()` again. - * - * Example: - * - * ```js - * async function nextStep(previousStep) { - * const thisStep = await FRAuth.next(previousStep); - * - * switch (thisStep.type) { - * case StepType.LoginSuccess: - * const token = thisStep.getSessionToken(); - * break; - * case StepType.LoginFailure: - * const detail = thisStep.getDetail(); - * break; - * case StepType.Step: - * // Populate `thisStep` callbacks here, and then continue - * thisStep.setInputValue('foo'); - * nextStep(thisStep); - * break; - * } - * } - * ``` - * - * @param previousStep The previous step with its callback values populated - * @param options Configuration overrides - * @return The next step in the authentication tree - */ - public static async next( - previousStep?: FRStep, - options?: StepOptions, - ): Promise { - const nextPayload = await Auth.next(previousStep ? previousStep.payload : undefined, options); - - if (nextPayload.authId) { - // If there's an authId, tree has not been completed - const callbackFactory = options ? options.callbackFactory : undefined; - return new FRStep(nextPayload, callbackFactory); - } - - if (!nextPayload.authId && nextPayload.ok) { - // If there's no authId, and the response is OK, tree is complete - return new FRLoginSuccess(nextPayload); - } - - // If there's no authId, and the response is not OK, tree has failure - return new FRLoginFailure(nextPayload); - } - - /** - * Redirects to the URL identified in the RedirectCallback and saves the full - * step information to localStorage for retrieval when user returns from login. - * - * Example: - * ```js - * forgerock.FRAuth.redirect(step); - * ``` - */ - public static redirect(step: FRStep): void { - const cb = step.getCallbackOfType(CallbackType.RedirectCallback) as RedirectCallback; - const redirectUrl = cb.getRedirectUrl(); - - localStorage.setItem(this.previousStepKey, JSON.stringify(step)); - location.assign(redirectUrl); - } - - /** - * Resumes a tree after returning from an external client or provider. - * Requires the full URL of the current window. It will parse URL for - * key-value pairs as well as, if required, retrieves previous step. - * - * Example; - * ```js - * forgerock.FRAuth.resume(window.location.href) - * ``` - */ - public static async resume( - url: string, - options?: StepOptions, - ): Promise { - const parsedUrl = new URL(url); - const code = parsedUrl.searchParams.get('code'); - const error = parsedUrl.searchParams.get('error'); - const errorCode = parsedUrl.searchParams.get('errorCode'); - const errorMessage = parsedUrl.searchParams.get('errorMessage'); - const form_post_entry = parsedUrl.searchParams.get('form_post_entry'); - const nonce = parsedUrl.searchParams.get('nonce'); - const RelayState = parsedUrl.searchParams.get('RelayState'); - const responsekey = parsedUrl.searchParams.get('responsekey'); - const scope = parsedUrl.searchParams.get('scope'); - const state = parsedUrl.searchParams.get('state'); - const suspendedId = parsedUrl.searchParams.get('suspendedId'); - const authIndexValue = parsedUrl.searchParams.get('authIndexValue') ?? undefined; - - let previousStep; - - function requiresPreviousStep() { - return (code && state) || form_post_entry || responsekey; - } +import RedirectCallback from './callbacks/redirect-callback.js'; +import { logger as loggerFn, LogLevel, CustomLogger } from '@forgerock/sdk-logger'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; + +export async function journey({ + config, + requestMiddleware, + logger, +}: { + config: JourneyClientConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; +}) { + const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); + + const store = createJourneyStore({ requestMiddleware, logger: log, config }); + store.dispatch(setConfig(config)); + + const stepStorage = createStorage<{ step: Step }>({ + type: 'sessionStorage', + name: 'journey-step', + }); + + const self = { + start: async (options?: StepOptions) => { + const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options)); + return data ? new FRStep(data) : undefined; + }, /** - * If we are returning back from a provider, the previous redirect step data is required. - * Retrieve the previous step from localStorage, and then delete it to remove stale data. - * If suspendedId is present, no previous step data is needed, so skip below conditional. + * Submits the current Step payload to the authentication API and retrieves the next FRStep in the journey. + * The `step` to be submitted is provided within the `options` object. + * + * @param options An object containing the current Step payload and optional StepOptions. + * @returns A Promise that resolves to the next FRStep in the journey, or undefined if the journey ends. */ - if (requiresPreviousStep()) { - const redirectStepString = localStorage.getItem(this.previousStepKey); + next: async (step: Step, options?: StepOptions) => { + const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options })); + return data ? new FRStep(data) : undefined; + }, + + redirect: async (step: FRStep) => { + const cb = step.getCallbackOfType(callbackType.RedirectCallback) as RedirectCallback; + if (!cb) { + throw new Error('RedirectCallback not found on step'); + } + const redirectUrl = cb.getRedirectUrl(); + const err = await stepStorage.set({ step: step.payload }); + if (err && (err as GenericError).type) { + log.warn('Failed to persist step before redirect', err); + } + window.location.assign(redirectUrl); + }, + + resume: async (url: string, options?: StepOptions) => { + const parsedUrl = new URL(url); + const code = parsedUrl.searchParams.get('code'); + const state = parsedUrl.searchParams.get('state'); + const form_post_entry = parsedUrl.searchParams.get('form_post_entry'); + const responsekey = parsedUrl.searchParams.get('responsekey'); + + let previousStep: Step | undefined; // Declare previousStep here - if (!redirectStepString) { - throw new Error('Error: could not retrieve original redirect information.'); + function requiresPreviousStep() { + return (code && state) || form_post_entry || responsekey; } - try { - previousStep = JSON.parse(redirectStepString); - } catch (err) { - throw new Error('Error: could not parse redirect params or step information'); + // Type guard for GenericError (assuming GenericError has 'error' and 'message' properties) + function isGenericError(obj: unknown): obj is GenericError { + return typeof obj === 'object' && obj !== null && 'error' in obj && 'message' in obj; } - localStorage.removeItem(this.previousStepKey); - } + // Type guard for { step: FRStep } + function isStoredStep(obj: unknown): obj is { step: Step } { + return ( + typeof obj === 'object' && + obj !== null && + 'step' in obj && + typeof (obj as any).step === 'object' + ); + } - /** - * Construct options object from the options parameter and key-value pairs from URL. - * Ensure query parameters from current URL are the last properties spread in the object. - */ - const nextOptions = { - ...options, - query: { - // Conditionally spread properties into object. Don't spread props with undefined/null. - ...(code && { code }), - ...(error && { error }), - ...(errorCode && { errorCode }), - ...(errorMessage && { errorMessage }), - ...(form_post_entry && { form_post_entry }), - ...(nonce && { nonce }), - ...(RelayState && { RelayState }), - ...(responsekey && { responsekey }), - ...(scope && { scope }), - ...(state && { state }), - ...(suspendedId && { suspendedId }), - // Allow developer to add or override params with their own. - ...(options && options.query), - }, - ...((options?.tree ?? authIndexValue) && { - tree: options?.tree ?? authIndexValue, - }), - }; - - return await this.next(previousStep, nextOptions); - } - - /** - * Requests the first step in the authentication tree. - * This is essentially an alias to calling FRAuth.next without a previous step. - * - * @param options Configuration overrides - * @return The next step in the authentication tree - */ - public static async start( - options?: StepOptions, - ): Promise { - return await FRAuth.next(undefined, options); - } -} + if (requiresPreviousStep()) { + const stored = await stepStorage.get(); + + if (stored) { + if (isGenericError(stored)) { + // If the stored item is a GenericError, it means something went wrong during storage/retrieval + // or the previous step was an error. + throw new Error(`Error retrieving previous step: ${stored.message || stored.error}`); + } else if (isStoredStep(stored)) { + previousStep = stored.step; + } + } + await stepStorage.remove(); + + if (!previousStep) { + throw new Error( + 'Error: previous step information not found in storage for resume operation.', + ); + } + } -export default FRAuth; + const nextOptions = { + ...options, + query: { + ...(options && options.query), // Spread options.query first + ...(code && { code }), + ...(state && { state }), + ...(form_post_entry && { form_post_entry }), + ...(responsekey && { responsekey }), + }, + }; + + if (previousStep) { + return await self.next(previousStep, nextOptions); + } else { + return await self.start(nextOptions); + } + }, + }; + return self; +} diff --git a/packages/journey-client/src/lib/journey.api.ts b/packages/journey-client/src/lib/journey.api.ts new file mode 100644 index 0000000000..765714f11a --- /dev/null +++ b/packages/journey-client/src/lib/journey.api.ts @@ -0,0 +1,131 @@ +import type { StepOptions } from '@forgerock/sdk-types'; +import type { ServerConfig } from '@forgerock/sdk-types'; +import { type logger as loggerFn } from '@forgerock/sdk-logger'; + +import { initQuery, RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import { type Step } from '@forgerock/sdk-types'; + +import { REQUESTED_WITH, getEndpointPath, stringify, resolve } from '@forgerock/sdk-utilities'; + +import type { JourneyClientConfig } from './config.types.js'; +import type { + BaseQueryApi, + BaseQueryFn, + FetchArgs, + FetchBaseQueryError, + FetchBaseQueryMeta, + QueryReturnValue, +} from '@reduxjs/toolkit/query'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; + +// Move these functions to the top, before journeyApi definition +function constructUrl( + serverConfig: ServerConfig, + realmPath?: string, + tree?: string, + query?: Record, +): string { + const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined; + const params: Record = { ...query, ...treeParams }; + const queryString = Object.keys(params).length > 0 ? `?${stringify(params)}` : ''; + const path = getEndpointPath({ + endpoint: 'authenticate', + realmPath, + customPaths: serverConfig.paths, + }); + const url = resolve(serverConfig.baseUrl, `${path}${queryString}`); + return url; +} + +function configureRequest(step?: Step): RequestInit { + const init: RequestInit = { + body: step ? JSON.stringify(step) : undefined, + credentials: 'include', + headers: new Headers({}), + method: 'POST', + }; + + return init; +} + +interface Extras { + requestMiddleware: RequestMiddleware[]; + logger: ReturnType; + config: JourneyClientConfig; +} + +export const journeyApi = createApi({ + reducerPath: 'journeyReducer', + baseQuery: fetchBaseQuery({ + baseUrl: '/', + prepareHeaders: (headers: Headers) => { + headers.set('Accept', 'application/json'); + headers.set('Accept-API-Version', 'protocol=1.0,resource=2.1'); + headers.set('Content-Type', 'application/json'); + headers.set('X-Requested-With', REQUESTED_WITH); + + return headers; + }, + }), + endpoints: (builder) => ({ + start: builder.mutation({ + queryFn: async ( + options: StepOptions | void, + api: BaseQueryApi, + _: unknown, + baseQuery: BaseQueryFn, + ) => { + const { config } = api.extra as Extras; + if (!config.serverConfig) { + throw new Error('Server configuration is missing.'); + } + const { realmPath, serverConfig, tree } = config; + + const query = options?.query || {}; + + const url = constructUrl(serverConfig, realmPath, tree, query); + const request = configureRequest(); + + const { requestMiddleware } = api.extra as Extras; + + const response = await initQuery({ ...request, url: url }, 'start') + .applyMiddleware(requestMiddleware) + .applyQuery(async (req: FetchArgs) => { + const result = await baseQuery(req, api, api.extra as Extras); + return result as QueryReturnValue; + }); + + return response as QueryReturnValue; + }, + }), + next: builder.mutation({ + queryFn: async ( + { step, options }: { step: Step; options?: StepOptions }, + api: BaseQueryApi, + _: unknown, + baseQuery: BaseQueryFn, + ) => { + const { config } = api.extra as Extras; + if (!config.serverConfig) { + throw new Error('Server configuration is missing.'); + } + const { realmPath, serverConfig, tree } = config; + const query = options?.query || {}; + + const url = constructUrl(serverConfig, realmPath, tree, query); + const request = configureRequest(step); + + const { requestMiddleware } = api.extra as Extras; + + const response = await initQuery({ ...request, url }, 'next') + .applyMiddleware(requestMiddleware) + .applyQuery(async (req: FetchArgs) => { + const result = await baseQuery(req, api, api.extra as Extras); + return result as QueryReturnValue; + }); + + return response as QueryReturnValue; + }, + }), + }), +}); diff --git a/packages/journey-client/src/lib/journey.slice.ts b/packages/journey-client/src/lib/journey.slice.ts new file mode 100644 index 0000000000..9a680f3bb7 --- /dev/null +++ b/packages/journey-client/src/lib/journey.slice.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit'; +import type { Step } from '@forgerock/sdk-types'; +import type { JourneyClientConfig } from './config.types.js'; + +export interface JourneyState { + authId?: string; + step?: Step; + error?: Error; + config?: JourneyClientConfig; +} + +const initialState: JourneyState = {}; + +export const journeySlice: Slice = createSlice({ + name: 'journey', + initialState, + reducers: { + setConfig: (state, action: PayloadAction) => { + state.config = action.payload; + }, + }, +}); + +export const { setConfig } = journeySlice.actions; diff --git a/packages/journey-client/src/lib/journey.store.ts b/packages/journey-client/src/lib/journey.store.ts new file mode 100644 index 0000000000..25410e1581 --- /dev/null +++ b/packages/journey-client/src/lib/journey.store.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { journeyApi } from './journey.api.js'; +import { journeySlice } from './journey.slice.js'; +import { JourneyClientConfig } from './config.types.js'; +import { logger as loggerFn } from '@forgerock/sdk-logger'; + +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; + +const rootReducer = combineReducers({ + [journeyApi.reducerPath]: journeyApi.reducer, + [journeySlice.name]: journeySlice.reducer, +}); + +export const createJourneyStore = ({ + requestMiddleware, + logger, + config, +}: { + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; + config: JourneyClientConfig; +}) => { + return configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: true, + thunk: { + extraArgument: { + requestMiddleware, + logger, + config, + }, + }, + }).concat(journeyApi.middleware), + }); +}; + +export type RootState = ReturnType; diff --git a/packages/journey-client/src/lib/shared/constants.ts b/packages/journey-client/src/lib/shared/constants.ts deleted file mode 100644 index 7a59f819e4..0000000000 --- a/packages/journey-client/src/lib/shared/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -const REQUESTED_WITH = 'forgerock-sdk'; -const X_REQUESTED_PLATFORM = 'javascript'; - -export { REQUESTED_WITH, X_REQUESTED_PLATFORM }; diff --git a/packages/journey-client/src/lib/utils/timeout.test.ts b/packages/journey-client/src/lib/utils/timeout.test.ts deleted file mode 100644 index ea623bca5c..0000000000 --- a/packages/journey-client/src/lib/utils/timeout.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @forgerock/javascript-sdk - * - * timeout.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import { withTimeout } from './timeout.js'; - -describe('withTimeout function', () => { - it('should return the promise passed', async () => { - const promise = new Promise((res) => res('ok')); - const result = await withTimeout(promise, 500); - expect(result).toBe('ok'); - }); - it('should return the promise passed if it rejects', async () => { - const promise = new Promise((_, rej) => rej('rejected')); - expect(withTimeout(promise, 500)).rejects.toBe('rejected'); - }); - it('should return the window timeout', async () => { - const promise = new Promise(() => 'ok'); - await withTimeout(promise, 1).catch((res) => expect(res).toEqual(new Error('Timeout'))); - }); -}); diff --git a/packages/journey-client/src/lib/utils/timeout.ts b/packages/journey-client/src/lib/utils/timeout.ts deleted file mode 100644 index 9132693897..0000000000 --- a/packages/journey-client/src/lib/utils/timeout.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * @forgerock/javascript-sdk - * - * timeout.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -const DEFAULT_TIMEOUT = 5 * 1000; - -/** - * @module - * @ignore - * These are private utility functions - */ -function withTimeout(promise: Promise, timeout: number = DEFAULT_TIMEOUT): Promise { - const effectiveTimeout = timeout || DEFAULT_TIMEOUT; - const timeoutP = new Promise((_, reject) => - setTimeout(() => reject(new Error('Timeout')), effectiveTimeout), - ); - - return Promise.race([promise, timeoutP]); -} - -export { withTimeout }; diff --git a/packages/journey-client/tsconfig.json b/packages/journey-client/tsconfig.json index b66329384c..341e0b5e7f 100644 --- a/packages/journey-client/tsconfig.json +++ b/packages/journey-client/tsconfig.json @@ -4,7 +4,7 @@ "include": [], "references": [ { - "path": "../sdk-effects/sdk-request-middleware" + "path": "../sdk-effects/storage" }, { "path": "../sdk-utilities" @@ -12,6 +12,9 @@ { "path": "../sdk-types" }, + { + "path": "../sdk-effects/sdk-request-middleware" + }, { "path": "../sdk-effects/logger" }, diff --git a/packages/journey-client/tsconfig.lib.json b/packages/journey-client/tsconfig.lib.json index 3b50b0ca7b..280db2ff6c 100644 --- a/packages/journey-client/tsconfig.lib.json +++ b/packages/journey-client/tsconfig.lib.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": ".", - "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", "emitDeclarationOnly": false, @@ -14,11 +13,14 @@ "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "types": ["node", "vitest/globals"] + "types": ["node"] }, "include": ["src/**/*.ts"], "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"], "references": [ + { + "path": "../sdk-effects/storage/tsconfig.lib.json" + }, { "path": "../sdk-effects/sdk-request-middleware/tsconfig.lib.json" }, diff --git a/packages/journey-client/tsconfig.spec.json b/packages/journey-client/tsconfig.spec.json index c8fc6b21fa..5f35a633f1 100644 --- a/packages/journey-client/tsconfig.spec.json +++ b/packages/journey-client/tsconfig.spec.json @@ -1,5 +1,5 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./out-tsc/vitest", "types": [ @@ -16,7 +16,8 @@ "importHelpers": true, "noImplicitOverride": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "declaration": true }, "include": [ "vite.config.ts", diff --git a/packages/journey-client/vite.config.ts b/packages/journey-client/vite.config.ts index a6b026895e..9f69157c7e 100644 --- a/packages/journey-client/vite.config.ts +++ b/packages/journey-client/vite.config.ts @@ -5,10 +5,18 @@ export default defineConfig(() => ({ cacheDir: '../../node_modules/.vite/packages/journey-client', plugins: [], test: { + deps: { + optimizer: { + web: { + include: ['vitest-canvas-mock'], + }, + }, + }, + setupFiles: ['./vitest.setup.ts'], watch: false, globals: true, passWithNoTests: true, - environment: 'node', + environment: 'jsdom', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { diff --git a/packages/journey-client/vitest.setup.ts b/packages/journey-client/vitest.setup.ts new file mode 100644 index 0000000000..644736e02a --- /dev/null +++ b/packages/journey-client/vitest.setup.ts @@ -0,0 +1 @@ +import 'vitest-canvas-mock'; diff --git a/packages/sdk-effects/iframe-manager/eslint.config.mjs b/packages/sdk-effects/iframe-manager/eslint.config.mjs index 8f5baa2d3a..cc5eaa196c 100644 --- a/packages/sdk-effects/iframe-manager/eslint.config.mjs +++ b/packages/sdk-effects/iframe-manager/eslint.config.mjs @@ -16,7 +16,7 @@ export default [ ], }, languageOptions: { - parser: await import('jsonc-eslint-parser'), + parser: (await import('jsonc-eslint-parser')).default, }, }, ]; diff --git a/packages/sdk-effects/logger/eslint.config.mjs b/packages/sdk-effects/logger/eslint.config.mjs index 8f5baa2d3a..cc5eaa196c 100644 --- a/packages/sdk-effects/logger/eslint.config.mjs +++ b/packages/sdk-effects/logger/eslint.config.mjs @@ -16,7 +16,7 @@ export default [ ], }, languageOptions: { - parser: await import('jsonc-eslint-parser'), + parser: (await import('jsonc-eslint-parser')).default, }, }, ]; diff --git a/packages/sdk-effects/storage/eslint.config.mjs b/packages/sdk-effects/storage/eslint.config.mjs index 1c878bcd48..4f6da4f811 100644 --- a/packages/sdk-effects/storage/eslint.config.mjs +++ b/packages/sdk-effects/storage/eslint.config.mjs @@ -22,7 +22,7 @@ export default [ ], }, languageOptions: { - parser: await import('jsonc-eslint-parser'), + parser: (await import('jsonc-eslint-parser')).default, }, }, ]; diff --git a/packages/sdk-types/eslint.config.mjs b/packages/sdk-types/eslint.config.mjs index 2dcaf60cbc..0f7a87237c 100644 --- a/packages/sdk-types/eslint.config.mjs +++ b/packages/sdk-types/eslint.config.mjs @@ -16,7 +16,7 @@ export default [ ], }, languageOptions: { - parser: await import('jsonc-eslint-parser'), + parser: (await import('jsonc-eslint-parser')).default, }, }, ]; diff --git a/packages/sdk-types/src/index.ts b/packages/sdk-types/src/index.ts index 7402cbf6e6..7d5ffabb36 100644 --- a/packages/sdk-types/src/index.ts +++ b/packages/sdk-types/src/index.ts @@ -6,6 +6,7 @@ */ export * from './lib/am-callback.types.js'; +export * from './lib/enums.js'; export * from './lib/error.types.js'; export * from './lib/legacy-config.types.js'; export * from './lib/legacy-mware.types.js'; @@ -13,3 +14,4 @@ export * from './lib/branded.types.js'; export * from './lib/tokens.types.js'; export * from './lib/config.types.js'; export * from './lib/authorize.types.js'; +export * from './lib/policy.types.js'; diff --git a/packages/sdk-types/src/lib/am-callback.types.ts b/packages/sdk-types/src/lib/am-callback.types.ts index a35acb18e6..8ee87effdd 100644 --- a/packages/sdk-types/src/lib/am-callback.types.ts +++ b/packages/sdk-types/src/lib/am-callback.types.ts @@ -5,8 +5,9 @@ * of the MIT license. See the LICENSE file for details. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const callbackType = { +import type { StepType } from './enums.js'; + +export const callbackType = { BooleanAttributeInputCallback: 'BooleanAttributeInputCallback', ChoiceCallback: 'ChoiceCallback', ConfirmationCallback: 'ConfirmationCallback', @@ -48,3 +49,87 @@ export interface Callback { output: NameValue[]; type: CallbackType; } + +/** + * Base interface for all types of authentication step responses. + */ +export interface AuthResponse { + type: StepType; +} + +/** + * Represents details of a failure in an authentication step. + */ +export interface FailureDetail { + failureUrl?: string; +} + +/** + * Represents the authentication tree API payload schema. + */ +export interface Step { + authId?: string; + callbacks?: Callback[]; + code?: number; + description?: string; + detail?: StepDetail; + header?: string; + message?: string; + ok?: string; + realm?: string; + reason?: string; + stage?: string; + status?: number; + successUrl?: string; + tokenId?: string; +} + +/** + * Represents details of a failure in an authentication step. + */ +export interface StepDetail { + failedPolicyRequirements?: FailedPolicyRequirement[]; + failureUrl?: string; + result?: boolean; +} + +/** + * Represents failed policies for a matching property. + */ +export interface FailedPolicyRequirement { + policyRequirements: PolicyRequirement[]; + property: string; +} + +/** + * Represents a failed policy policy and failed policy params. + */ +export interface PolicyRequirement { + params?: Partial; + policyRequirement: string; +} + +export interface PolicyParams { + [key: string]: unknown; + disallowedFields: string; + duplicateValue: string; + forbiddenChars: string; + maxLength: number; + minLength: number; + numCaps: number; + numNums: number; +} + +export type ConfigurablePaths = keyof CustomPathConfig; +/** + * Optional configuration for custom paths for actions + */ +export interface CustomPathConfig { + authenticate?: string; + authorize?: string; + accessToken?: string; + endSession?: string; + userInfo?: string; + revoke?: string; + sessions?: string; +} diff --git a/packages/journey-client/src/lib/enums.ts b/packages/sdk-types/src/lib/enums.ts similarity index 100% rename from packages/journey-client/src/lib/enums.ts rename to packages/sdk-types/src/lib/enums.ts diff --git a/packages/sdk-types/src/lib/legacy-config.types.ts b/packages/sdk-types/src/lib/legacy-config.types.ts index d5591a0e60..ccbd32f872 100644 --- a/packages/sdk-types/src/lib/legacy-config.types.ts +++ b/packages/sdk-types/src/lib/legacy-config.types.ts @@ -9,22 +9,10 @@ * Legacy configuration options for the SDK * **************************************************************/ -import type { Callback } from './am-callback.types.js'; +import type { Callback, CustomPathConfig } from './am-callback.types.js'; import type { LegacyRequestMiddleware } from './legacy-mware.types.js'; import { CustomStorageObject } from './tokens.types.js'; -/** - * Optional configuration for custom paths for actions - */ -export interface CustomPathConfig { - authenticate?: string; - authorize?: string; - accessToken?: string; - endSession?: string; - userInfo?: string; - revoke?: string; - sessions?: string; -} /** * Configuration settings for connecting to a server. */ @@ -47,8 +35,6 @@ export interface AsyncLegacyConfigOptions extends Omit(obj: { [key: string]: unknown } | undefined, prop: string, defaultValue: T): T { + if (!obj || obj[prop] === undefined) { + return defaultValue; + } + return obj[prop] as T; +} + +/** + * @method reduceToObject - goes one to two levels into source to collect attribute + * @param props - array of strings; can use dot notation for two level lookup + * @param src - source of attributes to check + */ +function reduceToObject( + props: string[], + src: Record, +): Record { + return props.reduce( + (prev, curr) => { + if (curr.includes('.')) { + const propArr = curr.split('.'); + const prop1 = propArr[0]; + const prop2 = propArr[1]; + const prop = (src[prop1] as Record)?.[prop2]; + prev[prop2] = prop != undefined ? (prop as string | number | null) : ''; + } else { + prev[curr] = src[curr] != undefined ? (src[curr] as string | number | null) : null; + } + return prev; + }, + {} as Record, + ); +} + +/** + * @method reduceToString - goes one level into source to collect attribute + * @param props - array of strings + * @param src - source of attributes to check + */ +function reduceToString(props: string[], src: Record): string { + return props.reduce((prev, curr) => { + prev = `${prev}${src[curr].filename};`; + return prev; + }, ''); +} + +export { getProp, reduceToObject, reduceToString }; diff --git a/packages/sdk-utilities/src/lib/strings/index.ts b/packages/sdk-utilities/src/lib/strings/index.ts new file mode 100644 index 0000000000..6b4af62026 --- /dev/null +++ b/packages/sdk-utilities/src/lib/strings/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +export * from './strings.utils.js'; diff --git a/packages/journey-client/src/lib/utils/strings.ts b/packages/sdk-utilities/src/lib/strings/strings.utils.ts similarity index 100% rename from packages/journey-client/src/lib/utils/strings.ts rename to packages/sdk-utilities/src/lib/strings/strings.utils.ts diff --git a/packages/sdk-utilities/src/lib/url/index.ts b/packages/sdk-utilities/src/lib/url/index.ts index d41b9fc1fd..d4caedeef2 100644 --- a/packages/sdk-utilities/src/lib/url/index.ts +++ b/packages/sdk-utilities/src/lib/url/index.ts @@ -6,3 +6,4 @@ */ export * from './am-url.utils.js'; +export * from './url.utils.js'; diff --git a/packages/journey-client/src/lib/utils/url.ts b/packages/sdk-utilities/src/lib/url/url.utils.ts similarity index 56% rename from packages/journey-client/src/lib/utils/url.ts rename to packages/sdk-utilities/src/lib/url/url.utils.ts index e5e2a2d70d..16bd8b829b 100644 --- a/packages/journey-client/src/lib/utils/url.ts +++ b/packages/sdk-utilities/src/lib/url/url.utils.ts @@ -8,9 +8,6 @@ * of the MIT license. See the LICENSE file for details. */ -import { getRealmUrlPath } from '@forgerock/sdk-utilities'; -import type { ConfigurablePaths, CustomPathConfig } from '../interfaces.js'; - /** * Returns the base URL including protocol, hostname and any non-standard port. * The returned URL does not include a trailing slash. @@ -20,34 +17,15 @@ function getBaseUrl(url: URL): string { (url.protocol === 'http:' && ['', '80'].indexOf(url.port) === -1) || (url.protocol === 'https:' && ['', '443'].indexOf(url.port) === -1); const port = isNonStandardPort ? `:${url.port}` : ''; - const baseUrl = `${url.protocol}//${url.hostname}${port}`; - return baseUrl; -} -function getEndpointPath( - endpoint: ConfigurablePaths, - realmPath?: string, - customPaths?: CustomPathConfig, -): string { - const realmUrlPath = getRealmUrlPath(realmPath); - const defaultPaths = { - authenticate: `json/${realmUrlPath}/authenticate`, - authorize: `oauth2/${realmUrlPath}/authorize`, - accessToken: `oauth2/${realmUrlPath}/access_token`, - endSession: `oauth2/${realmUrlPath}/connect/endSession`, - userInfo: `oauth2/${realmUrlPath}/userinfo`, - revoke: `oauth2/${realmUrlPath}/token/revoke`, - sessions: `json/${realmUrlPath}/sessions/`, - }; - if (customPaths && customPaths[endpoint]) { - // TypeScript is not correctly reading the condition above - // It's thinking that customPaths[endpoint] may result in undefined - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return customPaths[endpoint]; - } else { - return defaultPaths[endpoint]; + let hostname = url.hostname; + // Detect IPv6 hostnames and wrap them in square brackets + if (hostname.includes(':') && !hostname.startsWith('[') && !hostname.endsWith(']')) { + hostname = `[${hostname}]`; } + + const baseUrl = `${url.protocol}//${hostname}${port}`; + return baseUrl; } function resolve(baseUrl: string, path: string): string { @@ -81,4 +59,4 @@ function stringify(data: Record): string { return pairs.join('&'); } -export { getBaseUrl, getEndpointPath, parseQuery, resolve, stringify }; +export { getBaseUrl, parseQuery, resolve, stringify }; diff --git a/packages/sdk-utilities/tsconfig.lib.json b/packages/sdk-utilities/tsconfig.lib.json index 4f61993c1a..4919a8c11a 100644 --- a/packages/sdk-utilities/tsconfig.lib.json +++ b/packages/sdk-utilities/tsconfig.lib.json @@ -17,18 +17,7 @@ "sourceMap": true, "lib": ["es2022", "dom", "dom.iterable"] }, - "include": ["src/**/*.ts", "src/**/*.*.ts"], - "exclude": [ - "src/**/*.*.test-d.ts", - "src/**/*.test-d.ts", - "vite.config.ts", - "src/**/*.spec.ts", - "src/**/*.test.ts", - "src/**/*.*test.ts", - "src/**/*.test-d.ts", - "src/**/*.types.test-d.ts", - "src/**/*.utils.test-d.ts", - "src/lib/mock-data/*" - ], + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/lib/mock-data/*"], "references": [] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0da90a76f7..ca353af4da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,7 +184,7 @@ importers: version: 2.4.2 jsdom: specifier: 26.1.0 - version: 26.1.0 + version: 26.1.0(canvas@3.2.0) jsonc-eslint-parser: specifier: ^2.1.0 version: 2.4.0 @@ -238,7 +238,7 @@ importers: version: 6.3.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + version: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) vitest-canvas-mock: specifier: ^0.3.3 version: 0.3.3(vitest@3.2.4) @@ -314,7 +314,7 @@ importers: devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) + version: 0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) e2e/oidc-app: dependencies: @@ -367,7 +367,7 @@ importers: devDependencies: vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + version: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) packages/device-client: dependencies: @@ -396,9 +396,28 @@ importers: '@forgerock/sdk-utilities': specifier: workspace:* version: link:../sdk-utilities + '@forgerock/storage': + specifier: workspace:* + version: link:../sdk-effects/storage + '@reduxjs/toolkit': + specifier: 'catalog:' + version: 2.8.2 tslib: specifier: ^2.3.0 version: 2.8.1 + vite: + specifier: 6.3.4 + version: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vitest-canvas-mock: + specifier: ^0.3.3 + version: 0.3.3(vitest@1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0)) + devDependencies: + '@vitest/coverage-v8': + specifier: ^1.2.0 + version: 1.6.1(vitest@1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0)) + vitest: + specifier: ^1.2.0 + version: 1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0) packages/oidc-client: dependencies: @@ -429,7 +448,7 @@ importers: devDependencies: '@effect/vitest': specifier: catalog:effect - version: 0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) + version: 0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) msw: specifier: 'catalog:' version: 2.10.4(@types/node@22.14.1)(typescript@5.8.3) @@ -467,9 +486,9 @@ importers: scratchpad: dependencies: - '@forgerock/pingone-scripts': + '@forgerock/journey-client': specifier: workspace:* - version: link:../tools/user-scripts + version: link:../packages/journey-client devDependencies: dotenv: specifier: 17.2.0 @@ -503,14 +522,14 @@ importers: version: 3.17.7 vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + version: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) devDependencies: '@effect/language-service': specifier: catalog:effect version: 0.35.2 '@effect/vitest': specifier: catalog:effect - version: 0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) + version: 0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) packages: @@ -1494,6 +1513,12 @@ packages: '@emnapi/wasi-threads@1.0.4': resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.23.1': resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} engines: {node: '>=18'} @@ -1506,6 +1531,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.23.1': resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} engines: {node: '>=18'} @@ -1518,6 +1549,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.23.1': resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} engines: {node: '>=18'} @@ -1530,6 +1567,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.23.1': resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} engines: {node: '>=18'} @@ -1542,6 +1585,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.23.1': resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} engines: {node: '>=18'} @@ -1554,6 +1603,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.23.1': resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} engines: {node: '>=18'} @@ -1566,6 +1621,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} engines: {node: '>=18'} @@ -1578,6 +1639,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} engines: {node: '>=18'} @@ -1590,6 +1657,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.23.1': resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} engines: {node: '>=18'} @@ -1602,6 +1675,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.23.1': resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} engines: {node: '>=18'} @@ -1614,6 +1693,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.23.1': resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} engines: {node: '>=18'} @@ -1626,6 +1711,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.23.1': resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} engines: {node: '>=18'} @@ -1638,6 +1729,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.23.1': resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} engines: {node: '>=18'} @@ -1650,6 +1747,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.23.1': resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} engines: {node: '>=18'} @@ -1662,6 +1765,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.23.1': resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} engines: {node: '>=18'} @@ -1674,6 +1783,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.23.1': resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} engines: {node: '>=18'} @@ -1686,6 +1801,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.23.1': resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} engines: {node: '>=18'} @@ -1704,6 +1825,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} engines: {node: '>=18'} @@ -1728,6 +1855,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} engines: {node: '>=18'} @@ -1746,6 +1879,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.23.1': resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} engines: {node: '>=18'} @@ -1758,6 +1897,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.23.1': resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} engines: {node: '>=18'} @@ -1770,6 +1915,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.23.1': resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} engines: {node: '>=18'} @@ -1782,6 +1933,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.23.1': resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} engines: {node: '>=18'} @@ -3160,6 +3317,11 @@ packages: resolution: {integrity: sha512-efg/bunOUMVXV+MlljJCrpuT+OQRrQS4wJyGL92B3epUGlgZ8DXs+nxN5v59v1a6AocAdSKwHgZS0g9txmBhOg==} engines: {node: '>=18'} + '@vitest/coverage-v8@1.6.1': + resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} + peerDependencies: + vitest: 1.6.1 + '@vitest/coverage-v8@3.2.4': resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} peerDependencies: @@ -3169,6 +3331,9 @@ packages: '@vitest/browser': optional: true + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -3189,12 +3354,21 @@ packages: '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} @@ -3203,6 +3377,9 @@ packages: peerDependencies: vitest: 3.0.4 + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + '@vitest/utils@3.0.4': resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} @@ -3492,6 +3669,9 @@ packages: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -3726,9 +3906,17 @@ packages: caniuse-lite@1.0.30001733: resolution: {integrity: sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==} + canvas@3.2.0: + resolution: {integrity: sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==} + engines: {node: ^18.12.0 || >= 20.9.0} + caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + chai@5.2.1: resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} engines: {node: '>=18'} @@ -3755,10 +3943,16 @@ packages: chardet@2.1.0: resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -3887,6 +4081,9 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} @@ -4090,6 +4287,10 @@ packages: babel-plugin-macros: optional: true + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -4382,6 +4583,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} @@ -4592,6 +4798,10 @@ packages: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + expand-tilde@2.0.2: resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==} engines: {node: '>=0.10.0'} @@ -4853,6 +5063,9 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4898,6 +5111,9 @@ packages: engines: {node: '>=16'} hasBin: true + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5694,6 +5910,10 @@ packages: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -5775,6 +5995,9 @@ packages: resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==} engines: {node: '>=0.10.0'} + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@3.2.0: resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} @@ -5941,11 +6164,17 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + module-definition@6.0.1: resolution: {integrity: sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==} engines: {node: '>=18'} @@ -6017,6 +6246,9 @@ packages: engines: {node: ^18 || >=20} hasBin: true + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -6034,6 +6266,10 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + node-abi@3.77.0: + resolution: {integrity: sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==} + engines: {node: '>=10'} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -6227,6 +6463,10 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -6325,9 +6565,15 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} @@ -6389,6 +6635,9 @@ packages: piscina@4.9.2: resolution: {integrity: sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkginfo@0.4.1: resolution: {integrity: sha512-8xCNE/aT/EXKenuMDZ+xTVwkT8gsoHN2z/Q29l80u0ppGEXVvsKRzNMbtKhg8LS8k1tJLAHHylf6p4VFmP6XUQ==} engines: {node: '>= 0.4.0'} @@ -6425,6 +6674,11 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + precinct@12.2.0: resolution: {integrity: sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==} engines: {node: '>=18'} @@ -6857,6 +7111,12 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + sirv@3.0.1: resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} engines: {node: '>=18'} @@ -7049,6 +7309,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + strip-literal@3.0.0: resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} @@ -7098,6 +7361,9 @@ packages: resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} + tar-fs@2.1.3: + resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -7167,6 +7433,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -7175,6 +7445,10 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.3: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} @@ -7299,6 +7573,10 @@ packages: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} @@ -7364,6 +7642,9 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -7511,11 +7792,47 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + vite@6.3.4: resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -7601,6 +7918,31 @@ packages: peerDependencies: vitest: '*' + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@3.2.4: resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -9121,10 +9463,10 @@ snapshots: dependencies: effect: 3.17.7 - '@effect/vitest@0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1))': + '@effect/vitest@0.23.13(effect@3.17.7)(vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1))': dependencies: effect: 3.17.7 - vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) '@effect/workflow@0.5.1(@effect/platform@0.90.0(effect@3.17.7))(@effect/rpc@0.65.2(@effect/platform@0.90.0(effect@3.17.7))(effect@3.17.7))(effect@3.17.7)': dependencies: @@ -9145,102 +9487,153 @@ snapshots: dependencies: tslib: 2.8.1 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.23.1': optional: true '@esbuild/aix-ppc64@0.25.8': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.23.1': optional: true '@esbuild/android-arm64@0.25.8': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.23.1': optional: true '@esbuild/android-arm@0.25.8': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.23.1': optional: true '@esbuild/android-x64@0.25.8': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.23.1': optional: true '@esbuild/darwin-arm64@0.25.8': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.23.1': optional: true '@esbuild/darwin-x64@0.25.8': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.23.1': optional: true '@esbuild/freebsd-arm64@0.25.8': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.23.1': optional: true '@esbuild/freebsd-x64@0.25.8': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.23.1': optional: true '@esbuild/linux-arm64@0.25.8': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.23.1': optional: true '@esbuild/linux-arm@0.25.8': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.23.1': optional: true '@esbuild/linux-ia32@0.25.8': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.23.1': optional: true '@esbuild/linux-loong64@0.25.8': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.23.1': optional: true '@esbuild/linux-mips64el@0.25.8': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.23.1': optional: true '@esbuild/linux-ppc64@0.25.8': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.23.1': optional: true '@esbuild/linux-riscv64@0.25.8': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.23.1': optional: true '@esbuild/linux-s390x@0.25.8': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.23.1': optional: true @@ -9250,6 +9643,9 @@ snapshots: '@esbuild/netbsd-arm64@0.25.8': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.23.1': optional: true @@ -9262,6 +9658,9 @@ snapshots: '@esbuild/openbsd-arm64@0.25.8': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.23.1': optional: true @@ -9271,24 +9670,36 @@ snapshots: '@esbuild/openharmony-arm64@0.25.8': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.23.1': optional: true '@esbuild/sunos-x64@0.25.8': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.23.1': optional: true '@esbuild/win32-arm64@0.25.8': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.23.1': optional: true '@esbuild/win32-ia32@0.25.8': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.23.1': optional: true @@ -9951,7 +10362,7 @@ snapshots: semver: 7.7.2 tsconfig-paths: 4.2.0 vite: 6.3.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) - vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10933,6 +11344,25 @@ snapshots: minimatch: 7.4.6 semver: 7.7.1 + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 @@ -10948,10 +11378,16 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -10960,14 +11396,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.10.4(@types/node@22.14.1)(typescript@5.8.3) - vite: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vite: 6.3.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) '@vitest/pretty-format@3.0.4': dependencies: @@ -10977,18 +11413,34 @@ snapshots: dependencies: tinyrainbow: 2.0.0 + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 strip-literal: 3.0.0 + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.17 + pathe: 1.1.2 + pretty-format: 29.7.0 + '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 magic-string: 0.30.17 pathe: 2.0.3 + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.3 @@ -11002,7 +11454,14 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 '@vitest/utils@3.0.4': dependencies: @@ -11254,7 +11713,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -11392,6 +11851,8 @@ snapshots: assert-plus@1.0.0: {} + assertion-error@1.1.0: {} + assertion-error@2.0.1: {} ast-module-types@6.0.1: {} @@ -11679,8 +12140,24 @@ snapshots: caniuse-lite@1.0.30001733: {} + canvas@3.2.0: + dependencies: + node-addon-api: 7.1.1 + prebuild-install: 7.1.3 + optional: true + caseless@0.12.0: {} + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + chai@5.2.1: dependencies: assertion-error: 2.0.1 @@ -11708,8 +12185,15 @@ snapshots: chardet@2.1.0: {} + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + check-error@2.1.1: {} + chownr@1.1.4: + optional: true + chrome-trace-event@1.0.4: {} ci-info@3.9.0: {} @@ -11833,6 +12317,8 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + confusing-browser-globals@1.0.11: {} content-disposition@0.5.4: @@ -12015,6 +12501,10 @@ snapshots: optionalDependencies: babel-plugin-macros: 3.1.0 + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + deep-eql@5.0.2: {} deep-extend@0.6.0: {} @@ -12331,6 +12821,32 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + esbuild@0.23.1: optionalDependencies: '@esbuild/aix-ppc64': 0.23.1 @@ -12629,6 +13145,9 @@ snapshots: exit@0.1.2: {} + expand-template@2.0.3: + optional: true + expand-tilde@2.0.2: dependencies: homedir-polyfill: 1.0.3 @@ -12932,6 +13451,8 @@ snapshots: get-east-asian-width@1.3.0: {} + get-func-name@2.0.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -12984,6 +13505,9 @@ snapshots: meow: 12.1.1 split2: 4.2.0 + github-from-package@0.0.0: + optional: true + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -13212,7 +13736,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.0 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -13857,7 +14381,7 @@ snapshots: jsbn@0.1.1: {} - jsdom@26.1.0: + jsdom@26.1.0(canvas@3.2.0): dependencies: cssstyle: 4.6.0 data-urls: 5.0.0 @@ -13879,6 +14403,8 @@ snapshots: whatwg-url: 14.2.0 ws: 8.18.3 xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 3.2.0 transitivePeerDependencies: - bufferutil - supports-color @@ -14009,6 +14535,11 @@ snapshots: loader-runner@4.3.0: {} + local-pkg@0.5.1: + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -14076,6 +14607,10 @@ snapshots: longest@2.0.1: {} + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + loupe@3.2.0: {} lowdb@1.0.0: @@ -14219,8 +14754,18 @@ snapshots: minipass@7.1.2: {} + mkdirp-classic@0.5.3: + optional: true + mkdirp@1.0.4: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + module-definition@6.0.1: dependencies: ast-module-types: 6.0.1 @@ -14300,6 +14845,9 @@ snapshots: nanoid@5.1.5: {} + napi-build-utils@2.0.0: + optional: true + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -14310,6 +14858,11 @@ snapshots: nice-try@1.0.5: {} + node-abi@3.77.0: + dependencies: + semver: 7.7.2 + optional: true + node-addon-api@7.1.1: {} node-fetch@2.6.7: @@ -14560,6 +15113,10 @@ snapshots: dependencies: yocto-queue: 1.2.1 + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.1 + p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -14634,8 +15191,12 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@1.1.1: {} + pathval@2.0.1: {} peek-stream@1.1.3: @@ -14693,6 +15254,12 @@ snapshots: optionalDependencies: '@napi-rs/nice': 1.0.4 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + pkginfo@0.4.1: {} playwright-core@1.55.0: {} @@ -14727,6 +15294,22 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.0.4 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.77.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.3 + tunnel-agent: 0.6.0 + optional: true + precinct@12.2.0: dependencies: '@dependents/detective-less': 5.0.1 @@ -15212,6 +15795,16 @@ snapshots: signal-exit@4.1.0: {} + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + sirv@3.0.1: dependencies: '@polka/url': 1.0.0-next.29 @@ -15425,6 +16018,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 @@ -15467,6 +16064,14 @@ snapshots: tapable@2.2.3: {} + tar-fs@2.1.3: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + optional: true + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -15541,10 +16146,14 @@ snapshots: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@0.8.4: {} + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} + tinyspy@2.2.1: {} + tinyspy@4.0.3: {} tldts-core@6.1.86: {} @@ -15673,6 +16282,8 @@ snapshots: type-detect@4.0.8: {} + type-detect@4.1.0: {} + type-fest@0.20.2: {} type-fest@0.21.3: {} @@ -15750,6 +16361,8 @@ snapshots: uc.micro@2.1.0: {} + ufo@1.6.1: {} + uglify-js@3.19.3: optional: true @@ -15919,6 +16532,24 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 + vite-node@1.6.1(@types/node@22.14.1)(terser@5.44.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.20(@types/node@22.14.1)(terser@5.44.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-node@3.2.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -15940,6 +16571,16 @@ snapshots: - tsx - yaml + vite@5.4.20(@types/node@22.14.1)(terser@5.44.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.46.2 + optionalDependencies: + '@types/node': 22.14.1 + fsevents: 2.3.3 + terser: 5.44.0 + vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1): dependencies: esbuild: 0.25.8 @@ -15972,16 +16613,57 @@ snapshots: tsx: 4.17.0 yaml: 2.8.1 + vitest-canvas-mock@0.3.3(vitest@1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0)): + dependencies: + jest-canvas-mock: 2.5.2 + vitest: 1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0) + vitest-canvas-mock@0.3.3(vitest@3.2.4): dependencies: jest-canvas-mock: 2.5.2 - vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1): + vitest@1.6.1(@types/node@22.14.1)(@vitest/ui@3.0.4)(jsdom@26.1.0(canvas@3.2.0))(terser@5.44.0): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.4 + chai: 4.5.0 + debug: 4.4.1 + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.17 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.9.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.20(@types/node@22.14.1)(terser@5.44.0) + vite-node: 1.6.1(@types/node@22.14.1)(terser@5.44.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.14.1 + '@vitest/ui': 3.0.4(vitest@3.2.4) + jsdom: 26.1.0(canvas@3.2.0) + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vitest@3.2.4(@types/node@22.14.1)(@vitest/ui@3.0.4)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.2.0))(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.14.1)(typescript@5.8.3))(vite@6.3.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -15999,13 +16681,13 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) + vite: 6.3.6(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) vite-node: 3.2.4(@types/node@22.14.1)(jiti@2.4.2)(terser@5.44.0)(tsx@4.17.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.14.1 '@vitest/ui': 3.0.4(vitest@3.2.4) - jsdom: 26.1.0 + jsdom: 26.1.0(canvas@3.2.0) transitivePeerDependencies: - jiti - less diff --git a/scratchpad/package.json b/scratchpad/package.json index 2b61c3fcf4..910ce2ebcc 100644 --- a/scratchpad/package.json +++ b/scratchpad/package.json @@ -15,7 +15,7 @@ "serve": "tsx ./src/main.ts" }, "dependencies": { - "@forgerock/pingone-scripts": "workspace:*" + "@forgerock/journey-client": "workspace:*" }, "devDependencies": { "dotenv": "17.2.0", diff --git a/scratchpad/src/main.ts b/scratchpad/src/main.ts index a24a6e00cc..6d8b26c45d 100644 --- a/scratchpad/src/main.ts +++ b/scratchpad/src/main.ts @@ -1,4 +1,50 @@ -import { getUsersAndDelete } from '@forgerock/pingone-scripts'; -import 'dotenv/config'; +import { journey } from '@forgerock/journey-client'; +import { callbackType } from '@forgerock/sdk-types'; +import type { NameCallback, PasswordCallback } from '@forgerock/journey-client'; -await getUsersAndDelete('email', 'ipt8tglj@autogenerated.com'); +async function authenticateUser() { + const client = await journey({ + config: { + serverConfig: { baseUrl: 'https://your-am-instance.com' }, + realmPath: 'root', + tree: 'Login', + }, + }); + + try { + // Start the journey + let step = await client.start(); + + // Handle NameCallback + if (step.getCallbacksOfType(callbackType.NameCallback).length > 0) { + const nameCallback = step.getCallbackOfType(callbackType.NameCallback); + console.log('Prompt for username:', nameCallback.getPrompt()); + nameCallback.setName('demo'); // Set the username + step = await client.next({ step: step.payload }); // Submit the step + } + + // Handle PasswordCallback + if (step.getCallbacksOfType(callbackType.PasswordCallback).length > 0) { + const passwordCallback = step.getCallbackOfType( + callbackType.PasswordCallback, + ); + console.log('Prompt for password:', passwordCallback.getPrompt()); + passwordCallback.setPassword('password'); // Set the password + step = await client.next({ step: step.payload }); // Submit the step + } + + // Check for success or failure + if (step.type === 'LoginSuccess') { + console.log('Login successful!', step.getSessionToken()); + } else if (step.type === 'LoginFailure') { + console.error('Login failed:', step.getMessage()); + } else { + console.log('Next step requires further interaction:', step); + // Further logic to handle other callback types + } + } catch (error) { + console.error('An error occurred during the authentication journey:', error); + } +} + +authenticateUser(); diff --git a/scratchpad/tsconfig.json b/scratchpad/tsconfig.json index bda6314554..5c38b209af 100644 --- a/scratchpad/tsconfig.json +++ b/scratchpad/tsconfig.json @@ -21,7 +21,10 @@ }, "references": [ { - "path": "../tools/user-scripts" + "path": "../packages/sdk-types" + }, + { + "path": "../packages/journey-client" } ] } From 703e47f98ed3d1fdee44fa0f6baa65cd439ddf21 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 18 Sep 2025 11:14:06 -0600 Subject: [PATCH 03/11] ci: update-code-cov --- .github/workflows/ci.yml | 2 +- codecov.yml | 18 ++++++++---------- packages/davinci-client/vite.config.ts | 8 ++------ packages/device-client/vite.config.ts | Bin 812 -> 1715 bytes packages/journey-client/vite.config.ts | Bin 648 -> 1608 bytes packages/oidc-client/vite.config.ts | Bin 594 -> 1554 bytes packages/protect/vite.config.ts | Bin 563 -> 1523 bytes .../sdk-effects/iframe-manager/vite.config.ts | Bin 608 -> 1568 bytes packages/sdk-effects/logger/vite.config.ts | Bin 573 -> 1533 bytes packages/sdk-effects/oidc/vite.config.ts | Bin 598 -> 1558 bytes .../sdk-request-middleware/vite.config.ts | Bin 589 -> 1549 bytes packages/sdk-effects/storage/vite.config.ts | Bin 776 -> 1736 bytes packages/sdk-types/vite.config.ts | Bin 592 -> 1552 bytes packages/sdk-utilities/vite.config.ts | Bin 569 -> 1529 bytes 14 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 859ccee5b2..297dc84b64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - uses: codecov/codecov-action@v5 with: - files: ./packages/**/coverage/*.xml + directory: ./packages/ token: ${{ secrets.CODECOV_TOKEN }} - uses: actions/upload-artifact@v4 diff --git a/codecov.yml b/codecov.yml index 04660c41a9..07b19e90db 100644 --- a/codecov.yml +++ b/codecov.yml @@ -54,25 +54,23 @@ ignore: # Configure the comment that Codecov posts on pull requests. comment: - layout: 'header, diff, flags, files' + layout: 'header, diff, components, files' behavior: default require_changes: false # Post a comment even if there are no coverage changes require_head: true # Only post a comment if there is a coverage report for the head commit -# Configure how flags are managed. This allows for different coverage -# targets for different parts of the codebase. -flag_management: +component_management: default_rules: - # Carry forward coverage from previous commits for all flags. + # Carry forward coverage from previous commits for all components. carryforward: true - individual_flags: - # This rule applies to all flags that start with "package-". - # It is expected that the CI script uploads coverage reports with flags + individual_components: + # This rule applies to all components that start with "package-". + # It is expected that the CI script uploads coverage reports with components # like "package-davinci-client", "package-oidc-client", etc. # This makes the configuration future-proof for new packages. - - name: package-* + - component_id: package-* paths: - - packages/ # Only consider files in the packages directory for these flags + - packages/ # Only consider files in the packages directory for these components statuses: - type: project target: 80% # Higher target for packages diff --git a/packages/davinci-client/vite.config.ts b/packages/davinci-client/vite.config.ts index b12f08392f..b921b564ce 100644 --- a/packages/davinci-client/vite.config.ts +++ b/packages/davinci-client/vite.config.ts @@ -46,14 +46,10 @@ export default defineConfig({ '**/vitest.{workspace,projects}.[jt]s?(on)', '**/.{eslint,mocha,prettier}rc.{?(c|m)js,yml}', ], - reporter: [ - ['text', { skipEmpty: true }], - ['html', { skipEmpty: true }], - ['json', { skipEmpty: true }], - ], + reporter: ['text', 'html', 'json'], enabled: Boolean(process.env['CI']), reportsDirectory: './coverage', - provider: 'v8', + provider: 'v8' as const, }, }, }); diff --git a/packages/device-client/vite.config.ts b/packages/device-client/vite.config.ts index 30eeb5ca5794786a041a72d66d9a8d96ce73610e..457d7c89f30815285669c1de71451fba6c5acce1 100644 GIT binary patch literal 1715 zcmah~-;3im5bksSig|Nl9A!Bul-$E|-QL4OdlYV=l&~0C9>+>#$w;!3)b;;!okK32w=!!y?Hiq=1z#d~w3Q1E zOfl^LczyvT1!{*3z9K1*((lhtpTB(l424t(g|U#M=Tf;4d^M95A@tCZw2&H~j4tFB zdMK=^AZn#Y=T~BoMjvN0JPzul%p2uzjON`82$Az^2rGaC_o7@w!Ii^gkXQ`XXx$n% z(c~j~9nZGPWTd$UZ<|TI=Fa^kefh<_klULhVOq3&i|1GXQVZ4OB-ljGid7u1V%F_k z;$43Z0YKsb2%(GNZ__}@TVlxo9z7Dxfv=ErNGv4i!rA0`I1D5@IFaRnYd=~JaYw?= zu!!67M+}b(Jf7j5kKl^4GGc!LJJJ!Xt_nH#{D%sG_wOm30a4=78^@uX?nFeGHaVtL zb`9nD{&d4$Z=KX!?CAkh{59mPU2F*_904Q%4T=z9`aH@T;JEcrEv6|5WS zqkcM7s=tso^)Apv8#_C+pQmPu5{OD)sqo%8qpvN9nr900HT-IfLayfoOwfHY)a@pE zdX8Qf=RzHwy5FQF;KrWT5HXrXW9MHmkbNs@BGPI5O9b2jaY9@Ueb$e!9BUdM<5&VP zwU?FrCzzZ?G_5duyS*7G;}~2Du3PSwpQ;^ZyI6 diff --git a/packages/journey-client/vite.config.ts b/packages/journey-client/vite.config.ts index 9f69157c7e914c18ea67ee70f530366abf1edd08..8ff01c6adf0eb86dff12c5ea199413c7c72f1df6 100644 GIT binary patch literal 1608 zcmah}P0!je4BnYv;adTf^gT{Y8`9os(k?qqnkIy*kQxZ2NtL90tm}WD6I!55+ayvF zC$=9y{*Yy5tcL~)+(?bDMsMU6TG&`qLUNQIlXs&Lp5rTe!)xUSZ^z>ay#D~>W&~i3 z@pH)YLR!sBOi3iT*x@H>=a4X#(NCKK^U@TxLYJ}NAgj1I@GTwprnVX{God7Ep9K9% z)my3E9G0ta^ytV#@CAgTBKdU>U@9+5`4h=gF9JB>`tb-LwNQ0|WG=?${7i6tcwe-X1pH*`cqi&yuhQ6D$k(oo7+Zyz4ETxEH%QY`Aif1%RzGYdX!r+qx4~ z+_~@4@4lLEWMgoJ`Y5f@C9y!n?OkEY$i^T5XGJ!hW>eM#45_P7w5-{?w4^0ydD_wv zwD#^y$8*~Qv=|bKHiprqwZ9YQh?eqkXYPTuyVL04W)Og|bdnH6}fQNS&o;RKr38G^Tf5kYQ}6`i?N5rZeFr(hvz=2{S_oPoFTE174O=P z!;sedgMxeh&j&HXf!8MsJs=AF(QqN1e-cra24z1~pxY%|4Q02Mm}bHKV(cb=+MKc? z)LdVw6cm}F!tpg%^8qH3=lS{NCC{J5Zw3N!sVqA8BLl~T(G^3qUh z@k6b#5B&nk#)&V3$(eYxFjZ`d$qa$5S&UDI_~zs_?&e zM)!RSfSRur7IXM$j6$x*1WZsq8O@Vr^7g*8}!AdL_E{MYdLaSBeI@89jV^_A*Z58t0nMu>_rmzsz zX*~8kzHc77-Z>wkg%VfV;+?ZAy@n1}-qn!ob;RV|WP*qO7TuAJiKDyI=?va~fN47c z@Xo~r6h*1Ml{IE0Qc_j;N&5vPLge(buEe5tWn(bpf*s^LskU-WTc=AkS4N|a2`leR zv(`2&;Ng+Cc@E#Ka&=AT5Gx%(ui)q!kRG84+OFZr`7Qb&PPb3H1#oPn{K6jC!*_Ch zAHK?11%hyP(6OV4fpD-Ez>|!sTEI%0&;tTk8@Ht00-|pQ(N2c&L&xfiyC>Dr5!$`> z&T`De0i*As0cfjCQzC&&f>(K(=Ba4ea~5`}I?--IR?||nB7M?Qv^L>D+e4T15fkUg zsUgg4Xh_icOu0RJV)l&X6{o#}XM;pln)c8MeF{H`-%9Qgq60icv?WoGz=`8J;Ccvr zHo)l*SDKRBXXZ(As#voJdn&7QSe&K)N%P@rxJFRUM{pshc52(R5<@p|$l8|+rHt~Q z7nt3@T{s6sz+W0Jb%<9Y%F?KS#|m`%ARfoE=i*Ltc7Hn!vp;R7q7*T_^i8IyZekM?mUKYF5shc z2Bn=+V2bi7Xq-GGcb}8T84oIcP6Hs&jHSSxKk}r_&lUZgK+@mq5_t~q-zFdf5GTZC OZ=@f3^y+4_caz`r#lgS; delta 34 ncmbQlbBSfcd&bF!SVe_OQj1G;^Giz#N=x+1GC{n_*H~=;@1qT@ diff --git a/packages/protect/vite.config.ts b/packages/protect/vite.config.ts index e3e8b96b148ea537e7dd484c0788ef069166b4ca..a5d8fef12439752cc2e983ad6430bbe4a7b06fbc 100644 GIT binary patch literal 1523 zcmah}OOKl{5bim@Vr~$`nmtxowUN5L^wvXLsfr@xm`QK~c4V78MEUQXA+Xt`-4?|J zk3BQreDl!t-nsxS6u8y~pRHNz4Ro+}wub1S14d7i37+~}bVoK-xVan0Q~2-^;&uYy ztPL~B^Fli#YfMR`q$=^Vb~A{C$mnNmfq89z#pU*8Hv`5+%CGE!IsG8F-~3k@ zN>A`d2OT>K=qU!)0yxT`${DO><@;a&HkDn`ZU(_Mg9yz*J8Kvi1R}P7$lOz4Y-q^9c&6+D9Z@#| ze#2?+;Kd;EktBV56k-BDiNsPK5P}6f1vCZmi@=%TM03JDpAF&khigsQ>*+sA&IfDu z!CvZSgvF`opClW;hHDhccm!8+YO8iVE5UaIhpc_QP{<(vd4bve+l3Jj1^(J_p?$a$ zQI>8h^j3jR7h-uUd#T$2`9mzq=8r z^WLHNcdp(*a1lQwQ!$U#uX?)JhNYj!r|BJ{q!sBycP-L%g+`T^cP-tno5v5u|BU20 zyYqQ|hm9g@chc1|b+)RSeR{+dlb3R-^)G6TE%%2!$aE!FSf%Ey(jii63es6Mx++ri zl{O(I213y2I-&ZxC2kn%8D=P=KqFTb7Bl!{ZH3ar1WZvr1&xzM^!z1S yPI(&fa~S}hhA08I?#!b!8!P%Hfuw)X1@fRhypKS7AWn$O-bg>$=+#Z9Pm|x!hPQ(N delta 34 ncmey&y_sdh1;)vXSw)3PQj1G;^Giz#N=x+1GC{n_+gWV@@qP`E diff --git a/packages/sdk-effects/iframe-manager/vite.config.ts b/packages/sdk-effects/iframe-manager/vite.config.ts index 6ec07333ef41d8c225deb36090f3b6e18a2864bb..d66e3b2f0683acee102ea2552b9ccf01d6c096f1 100644 GIT binary patch literal 1568 zcmah}OOM(x5bl{@F}FYzoIO@lEmF6aUaB6pttzyNj58sI#74HmLuvkdXF^yW?Y4*n zJf7G0%|q8)>jJcpW1$T`T2ts1bWk{3L$uQYqj#eb9(pa(kxdnDbjRZfy#D~>b_C$8 z4Kv8HTstFcOh}}pD)EzcGl+ypPhVqm%xarA75Y?gjC3p2My}|HMS&{#R2PmsOlxUK z>>_rxt(uiKeg=zYKBg&rGs@OAngF5H9&`am+kkWkjn`%cd+Rpnyf|r}PBY+4$@t12 zn8SC<;>mxNq4Wgk9H8Ss0X?B&E3%Q^|InfQVxLG#xdhkrI05!DsB#8{to$HZRdz|I zYb7*0?X2OPi4i8EhX$aHs+t@LT;!cf<2a2)%aIemMb(LR?UR}wMNgo=^bk$S+8^k6 z=%PMjssM7K2s1kx%5}a|c88AGJY#t!XdmFkAyE~_J#<1J!%yP3lsklA0S^I9PShiC z4Y@10J3QYFaQe$aQ=R+D>?N0qEqk!1COfCaRq8)+I{X{d2+H{iF67i!ZF*LM?*1b-cy zTv6YIMpm<%oJgK!hlhtOyBEK?5vb$Vq4#&8UdiAjp2ia~AFE&WWU&sX)3jVW?-AQxPri2~V+*OzfJs zQ6~ltthpyaw1a!<$5LK`I3?N>eKb$Uy1Mk}FsYYW~}gx-paeT V5l9cj331sw>6ae8y2<3-=r=U*#)|*| delta 34 ncmZ3$^MGZ;U&hH7SVe_OQj1G;^Giz#N=x+1GC{n_PgrdL_G=CD diff --git a/packages/sdk-effects/logger/vite.config.ts b/packages/sdk-effects/logger/vite.config.ts index bba16a4c4ed9d68a3293edef10e5952e85da4b9b..d08c944cff5ee3986c25a2836f6de55019fa0bc4 100644 GIT binary patch literal 1533 zcmah}OOM(x5bl{@F}FaJb@o`XYLU9V^wvXLsX{0+&Oi)~jckX9(){<%gs?2M+aeP1 zcwXN(4_)t^kI+JiD{b-F*_B>H2P^MtNcK8n@-!OZvDYFU*_e2vJ04Hq!$%mmBLMGQ zoI+8Q+FMy;Mj|Cug`c&bLLx+d{#sXJQMBK*xb1213JD00$XWHHDQlp@#*qHf~9$DMa54BD8z$o#n&`E~D)82hdiT zrbI%J1h4Wm%~R2``z-8Gb)wybtfoiN6X`EK#6h+RM>-z6q)&_JMXmw?V@E?RE_cf9 z(G!E0gx?6-2Y7Kve57fQk07S-lSr)O9w9owV?YLa|Grh@)8G?=XWjrwwsL~ z%Ktej3Jw>F;x0Er)^4S*W#*kR%`Q9OlF7@s)%q8W#)juZ?q#-=OEj51scb+>LqRsn zMw>FjV6=@HF%Y9h-wD-kFmc0B&oHMVT3!Z$Vj-Eh4X>I`3>-LnPl9L%_tcM-yaw?~ zv}gKgo=&y7_UJIF*IDIJjWkS221ylb!@ry0>}75N(8{I3atfcEGbrtt5>r%9LF;6m zJby_R6W&JrIWK@f>y#3A{=`c)zjX9V0m*)^OXQWif1iL1K%5Yly_0^k(W{$Go<_d` DVgb6G delta 51 zcmey%y_aRfEk^aC)Pnq?l46(4qSWM){Gv)L1$90BlGNf7-Tcy$g3=QGvP=+f@OV diff --git a/packages/sdk-effects/oidc/vite.config.ts b/packages/sdk-effects/oidc/vite.config.ts index 294c9a0c7964e1adbd2fcfbcad75d99c3cb203e2..99123060f4adede789908358dbadc8ad279dca47 100644 GIT binary patch literal 1558 zcmah}OOM(x5bl{@F}H*$>+G?jYLU9V^iuV(ZB?OFWSoH*5*yhL52g9By!ESGsXLh4&vIZYKcF z+AxDWFSIkV#*{=#suDkGH-kus?EE#hz`V9aQ=!iU$H;b4ZRMJdxLTnKKC`+|5j)sb z%~~5jgT)gcvkbl&W$PMEfKX}=dIcxjfOH6r*Jcd|>$d2;IBTCzGvNHl_=P_($L|#T z(SMbp^aNuJ&~c!Ep0KbLz(EF8&R``g--7~JS9VFK83fl1qMh{qhYsZz`$(o&S7`Ry zS;ILK4-CJD2B3|qngR)2#98t;_dqa$iZEUyIZ13Wt_j}R>2F`y}kdIYW<_X78X z=eq$;f4S0B-M%sh$)#e;9_+cb#46Kh4$e_L{+-#;A;cAU5KY^*>iU%8Hb<8e)^}+L=+SQPPU^uDdj8 zxZEV&K4< zTM|S&xTk(BAT5q#dpH4 delta 34 ncmbQnbB$%gXU56LSVe_OQj1G;^Giz#N=x+1GC{n_w^(fe@ska} diff --git a/packages/sdk-effects/sdk-request-middleware/vite.config.ts b/packages/sdk-effects/sdk-request-middleware/vite.config.ts index 239527fba35591c96e5c1169df1eff2b22e2b77b..196bf47172ea18576600c4308d64198058e6af74 100644 GIT binary patch literal 1549 zcmah}OOM(x5bl{@F}FYzoIO@lEmF6a-g;;&RcI9%X8?m^Q`_O8H2=LbNmv%zZ4n80 zJdba_dFX2Ie1rx{TxpAs&aU(tT3C5kL2}R$lXs&Lo;oekk+q38y5sQ#-hY5`GXn6= z#TgVuslAmIW+YNlZSa%!Gf0HU`>%B+7L_Y&gCQ3jBi~E4lWRKSYK1C>Je0es$DcKZ zIIVP98azsm3A@~zdaZ4k!QzRJc@E#Ka#e*kLfq&8dIe`!1L;E@v|Yo|`5pQodhPx+ z14c~7FZ_W$eJ3s9q$y?EeNy;AByQvZAv(ZQL|YQS2wX630d9rB zcSAbe%o>?vVBKgU~wt>DEaVjP@_~XS8ye#a%$JH5<@$1$ktbdQbzet z1!nhe3Ku|>_)Ev74)IPzRk~^1TLZdXh^JfGb5o}|ho6tbxy-pTc~6n%p5u8<9=hYm;V6 zv})78E9rObd^|1x=cFh&Tque=Y?N8ElfIIfcgEEF?1)PyFXLA0UNjn8o)3AD*-|di zWcIAGAyXO(vRT&Jlo7#i%SLWKI!=zqkl}9zwFeMozn^+nC-3Dhba|?i0E)AA5_~@KL zX~zUiQ9T8%lST6QIeD7!HsVje00OO70`B~omu!CN=;s2G{Xv(=EBNp>0U3ZeAuc;7 N-Daa#JDI#2{RWNNz~2A> delta 51 zcmeC>Im@!)6{C7lYC(QcNwG_2QEGBYeo>{Bg1Vl5NosM4ZhmP=L1~G8Stf`#`68mhj zq#eLxK)=C8kQFxc-01+pwQ(ywLNE7F)5|rw)!2Zv4KzALuj?Vwdaw<8h#f*i-v>6} z+G+jk(=!;Y&;}In1xbOFetCNQ^xKzD&}xIwIu8v-sZC(#-kVr|@;m_L7ArYcH z{;X@TB6x$rPzv@@uBBSZ1ud~{QN>V(NW?8LVH;~REVK=CczNYzS;FsDxxOcs2_aFZ zTiCk+n7koy2DZ+x&@)8VKCb4#A=2`lJ+Qk!$o+HpEMpf4$=Sikjv@wv!>0hYGOBJ4 ztu$f61+Xx#rqvvxA5J2)8||Itzz8qHoZ<)2R+*teLXZTn$~-S~abWXBSfd)nVHt{^ zZbf&bPr8YOVi|U{+>OZ;7V(Rm1_H*0PRTg$DYrpp>>Th{oK6nj3=$tXRp9Z;;ZGv5 zlN+iV2e^w!g~t`d8RDFC?gQ_g;7prbR$-!lD>)y0asqoSp>tTAis_=7JARzh2+Da2 zF68u1ttM7t7*8DXsf$7*qx??=X7}F|&H)kd_lCK_ZbX!&t4h5-K)WyE^;-5;#d*o@ zAJUNh(`GIj5yN}mWQyu04${o8Vj_7}?e6cZ>Q?;Cl|b#+G*QBhtFJ_G6yN2Ucu3V> zefF}9ui+uxWj7J!2T|OOmm)1{w5mHkd+Ar>LweWzpOLC!ccH3oVk2aSmGr$VyfbE4 z7h9~Eyn;(@dQfXDxj*Da7PYL=6n3wQ6H;mlI#UnYGzA8uZ7hg^7&ZD)sA+_WD~2Y9 zITUf=VGzg{(h;}hSu=_g2hQG-APy(QZpq5 zBwg$czn8(;bKe4>m9@cU4nH|(P}-CNQjE^8<4PNRv}UE((89{u3ZlIZ7`+>f@YrdQj;zaYqdOi?;Qa>}HzNRN zZJ0rp<=PopVL~D$Re_(hn?WQ*N?&7h%qpALCHhowfOIF-R<7yj^X)X8cIYEE-IeuP z8$W}^Gau6wz8Ph!3Qd4eXb*Y?CtHJb2({N{4F~JC=)CB)`_l|KGcvyN2j=*l0zdh$ zG8CQwoCCBRD4-`4Y(+NG`yVpB}+d@)}>w2=~@ZRUOQ_z zXJUY{chCT|QDvPYfs4FTX&k4qXgG4>cc@y?YDEjzG%Q=QY|Ds`VY9sUh!1m%1M7ji1AwjC?Mw*!Z4eNo6|kpEO* zcK@buPKbbCJI=KacOt6NP5a&&(CtDz-^yNEJ5D+LeC#KG`iw;`Lf`W}BN4fxz6rG~ zXE!;KJj;#`4_S6Eesd#Gr=3IZ??SzjK`)-h6EPpFU)5x>3D15$K2EOLpvZ9}l4*Ns z(qxH775%%CZrjes)BJxAI;OLDla`cOzLG;c~k=pQnlUA&sGfq>$s&6E96e8XQSql=0G>81C2rk`_iTFZ=$8VL{a)wDTX_FA0_lM` RAuc;7-O{61JDI#2{RSZo!5IJm delta 34 ncmbQhbAe^UTgJ%;SVe_OQj1G;^Giz#N=x+1GC{n_S6FQT?yC)= diff --git a/packages/sdk-utilities/vite.config.ts b/packages/sdk-utilities/vite.config.ts index ed4a20837aaca4f3b89968de20ee13a015e89097..1add05b310f12102e5489990af11020fb6b5f09b 100644 GIT binary patch literal 1529 zcmah}OOM(x5bl{@F}FYzoIO^oTBL3-z4g#ms?aJj&Hx6-Mz+I4Y5sd>lCUha+eSFV zW6$IJ=Ao;-^AQ>-aiuLjIlIzpXkq1D1<652OddxgJaxC|j;u|*xjPgM2U5POfPi%H6b%+UTe;B&@zS z^;+97gT*s%^Blfg<*Ev8gt*ZG^a{?d2I&#%pzRut&hO9%(ckWOGvMe*`ISAeryu0} zTlgyDCJ=~=gO(jd41|QW0FE-M%?wu3gbotG+PEd{W)OWnh|nIica{Sqs0^};A3$4W z>JkY-61>XOG*3mt=CiOz)rw{tvWk|X73q_fqO}bt+Me2^3yY{l&Hw>pLqi@eXUZMW z6M>h2-*DPFcri$Pq-lqbAg1t>NZiN+LUe$qh_)nt5ja1bZccmPvjI+bxYCrvj{c+M ze6VH*wy&N`Se%OPN%P@rxJFPeM{p&la%$JH5<@$1$lBKnrHt~Q7nt3@UAP29z+W3K zb%=K&%F<1p-YU@PLOkEfUTQea+5LPRCV$#YMJZxm*gYc&nWDOhwKTJvm`Gj}r-z54 zxEH^>5vcRtV+ePy-iV+VPt%E*kJYbgve?GwFdv^Luh}5aX(O^}do9vziB@g;cP0I< zosXyG|BMs`y9-5e7aQ4ccG6ce^Uj!hpB-_@7)44L&8XY?I+N<2Ek(w#VAlbyq@c%YAd+A#Mv~p>%oWUpO3`#quz!c?E z&^TEnPhXPf2~Q(_`T-DVlv3c%pLw9>mx_K#An6};i9Bu(?-P&#h!f(nGtx~qdbN|u G Date: Thu, 18 Sep 2025 14:01:57 -0600 Subject: [PATCH 04/11] refactor(journey-client): rename frstep to journeystep and frcallback to journeycallback --- packages/journey-client/README.md | 18 ++++++------- .../lib/callbacks/attribute-input-callback.ts | 4 +-- .../src/lib/callbacks/choice-callback.ts | 4 +-- .../lib/callbacks/confirmation-callback.ts | 4 +-- .../lib/callbacks/device-profile-callback.ts | 4 +-- .../src/lib/callbacks/factory.test.ts | 6 ++--- .../src/lib/callbacks/factory.ts | 10 +++---- .../lib/callbacks/fr-auth-callback.test.ts | 6 ++--- .../lib/callbacks/hidden-value-callback.ts | 4 +-- .../journey-client/src/lib/callbacks/index.ts | 4 +-- .../src/lib/callbacks/kba-create-callback.ts | 4 +-- .../src/lib/callbacks/metadata-callback.ts | 4 +-- .../src/lib/callbacks/name-callback.ts | 4 +-- .../src/lib/callbacks/password-callback.ts | 4 +-- .../ping-protect-evaluation-callback.ts | 4 +-- .../ping-protect-initialize-callback.ts | 4 +-- .../lib/callbacks/polling-wait-callback.ts | 4 +-- .../src/lib/callbacks/recaptcha-callback.ts | 4 +-- .../recaptcha-enterprise-callback.ts | 4 +-- .../src/lib/callbacks/redirect-callback.ts | 4 +-- .../src/lib/callbacks/select-idp-callback.ts | 4 +-- .../terms-and-conditions-callback.ts | 4 +-- .../src/lib/callbacks/text-input-callback.ts | 4 +-- .../src/lib/callbacks/text-output-callback.ts | 4 +-- .../validated-create-password-callback.ts | 4 +-- .../validated-create-username-callback.ts | 4 +-- .../src/lib/fr-qrcode/fr-qrcode.test.ts | 12 ++++----- .../src/lib/fr-qrcode/fr-qrcode.ts | 10 +++---- .../src/lib/fr-recovery-codes/index.ts | 10 +++---- .../fr-recovery-codes/recovery-codes.test.ts | 10 +++---- .../src/lib/fr-webauthn/fr-webauthn.test.ts | 26 +++++++++---------- .../src/lib/fr-webauthn/index.ts | 18 ++++++------- .../src/lib/journey-client.test.ts | 16 ++++++------ .../journey-client/src/lib/journey-client.ts | 14 +++++----- .../{fr-step.test.ts => journey-step.test.ts} | 22 ++++++++-------- .../src/lib/{fr-step.ts => journey-step.ts} | 26 +++++++++---------- 36 files changed, 146 insertions(+), 146 deletions(-) rename packages/journey-client/src/lib/{fr-step.test.ts => journey-step.test.ts} (86%) rename packages/journey-client/src/lib/{fr-step.ts => journey-step.ts} (81%) diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md index d7e61d1188..e352fb2a39 100644 --- a/packages/journey-client/README.md +++ b/packages/journey-client/README.md @@ -92,26 +92,26 @@ authenticateUser(); The `journey()` factory function returns a client instance with the following methods: -- `client.start(options?: StepOptions): Promise` - Initiates a new authentication journey. Returns the first `FRStep` in the journey. +- `client.start(options?: StepOptions): Promise` + Initiates a new authentication journey. Returns the first `JourneyStep` in the journey. -- `client.next(options: { step: Step; options?: StepOptions }): Promise` - Submits the current `Step` payload (obtained from `FRStep.payload`) to the authentication API and retrieves the next `FRStep` in the journey. +- `client.next(options: { step: Step; options?: StepOptions }): Promise` + Submits the current `Step` payload (obtained from `JourneyStep.payload`) to the authentication API and retrieves the next `JourneyStep` in the journey. -- `client.redirect(step: FRStep): Promise` +- `client.redirect(step: JourneyStep): Promise` Handles `RedirectCallback`s by storing the current step and redirecting the browser to the specified URL. This is typically used for external authentication providers. -- `client.resume(url: string, options?: StepOptions): Promise` +- `client.resume(url: string, options?: StepOptions): Promise` Resumes an authentication journey after an external redirect (e.g., from an OAuth provider). It retrieves the previously stored step and combines it with URL parameters to continue the flow. ### Handling Callbacks -The `FRStep` object provides methods to easily access and manipulate callbacks: +The `JourneyStep` object provides methods to easily access and manipulate callbacks: -- `step.getCallbackOfType(type: CallbackType): T` +- `step.getCallbackOfType(type: CallbackType): T` Retrieves a single callback of a specific type. Throws an error if zero or more than one callback of that type is found. -- `step.getCallbacksOfType(type: CallbackType): T[]` +- `step.getCallbacksOfType(type: CallbackType): T[]` Retrieves all callbacks of a specific type as an array. - `callback.getPrompt(): string` (example for `NameCallback`, `PasswordCallback`) diff --git a/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts b/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts index c74b687830..d7879a1707 100644 --- a/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts +++ b/packages/journey-client/src/lib/callbacks/attribute-input-callback.ts @@ -6,7 +6,7 @@ */ import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; /** * Represents a callback used to collect attributes. @@ -14,7 +14,7 @@ import FRCallback from './index.js'; * @typeparam T Maps to StringAttributeInputCallback, NumberAttributeInputCallback and * BooleanAttributeInputCallback, respectively */ -class AttributeInputCallback extends FRCallback { +class AttributeInputCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/choice-callback.ts b/packages/journey-client/src/lib/callbacks/choice-callback.ts index cd6fa5984e..00ddb0d8e2 100644 --- a/packages/journey-client/src/lib/callbacks/choice-callback.ts +++ b/packages/journey-client/src/lib/callbacks/choice-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect an answer to a choice. */ -class ChoiceCallback extends FRCallback { +class ChoiceCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/confirmation-callback.ts b/packages/journey-client/src/lib/callbacks/confirmation-callback.ts index e06ce22037..3f985d726c 100644 --- a/packages/journey-client/src/lib/callbacks/confirmation-callback.ts +++ b/packages/journey-client/src/lib/callbacks/confirmation-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a confirmation to a message. */ -class ConfirmationCallback extends FRCallback { +class ConfirmationCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts index 48d0795e2e..6f13160cd2 100644 --- a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts +++ b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts @@ -4,14 +4,14 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; import type { DeviceProfileData } from '../fr-device/interfaces.js'; /** * Represents a callback used to collect device profile data. */ -class DeviceProfileCallback extends FRCallback { +class DeviceProfileCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/factory.test.ts b/packages/journey-client/src/lib/callbacks/factory.test.ts index bfc1542fe7..be7abdb731 100644 --- a/packages/journey-client/src/lib/callbacks/factory.test.ts +++ b/packages/journey-client/src/lib/callbacks/factory.test.ts @@ -32,7 +32,7 @@ import TextInputCallback from './text-input-callback.js'; import TextOutputCallback from './text-output-callback.js'; import ValidatedCreatePasswordCallback from './validated-create-password-callback.js'; import ValidatedCreateUsernameCallback from './validated-create-username-callback.js'; -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; describe('Callback Factory', () => { const testCases = [ @@ -76,10 +76,10 @@ describe('Callback Factory', () => { }); }); - it('should create a base FRCallback for an unknown type', () => { + it('should create a base JourneyCallback for an unknown type', () => { const payload: Callback = { type: 'UnknownCallback' as any, input: [], output: [] }; const callback = createCallback(payload); - expect(callback).toBeInstanceOf(FRCallback); + expect(callback).toBeInstanceOf(JourneyCallback); // Ensure it's not an instance of a more specific class expect(callback).not.toBeInstanceOf(NameCallback); }); diff --git a/packages/journey-client/src/lib/callbacks/factory.ts b/packages/journey-client/src/lib/callbacks/factory.ts index 82f723823a..52b9057d68 100644 --- a/packages/journey-client/src/lib/callbacks/factory.ts +++ b/packages/journey-client/src/lib/callbacks/factory.ts @@ -6,7 +6,7 @@ */ import { callbackType } from '@forgerock/sdk-types'; -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; import AttributeInputCallback from './attribute-input-callback.js'; import ChoiceCallback from './choice-callback.js'; @@ -31,12 +31,12 @@ import TextOutputCallback from './text-output-callback.js'; import ValidatedCreatePasswordCallback from './validated-create-password-callback.js'; import ValidatedCreateUsernameCallback from './validated-create-username-callback.js'; -type FRCallbackFactory = (callback: Callback) => FRCallback; +type JourneyCallbackFactory = (callback: Callback) => JourneyCallback; /** * @hidden */ -function createCallback(callback: Callback): FRCallback { +function createCallback(callback: Callback): JourneyCallback { switch (callback.type) { case callbackType.BooleanAttributeInputCallback: return new AttributeInputCallback(callback); @@ -87,9 +87,9 @@ function createCallback(callback: Callback): FRCallback { case callbackType.ValidatedCreateUsernameCallback: return new ValidatedCreateUsernameCallback(callback); default: - return new FRCallback(callback); + return new JourneyCallback(callback); } } export default createCallback; -export type { FRCallbackFactory }; +export type { JourneyCallbackFactory }; diff --git a/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts b/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts index c9744d44d1..2f3c609c33 100644 --- a/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts +++ b/packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts @@ -8,11 +8,11 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import { callbackType } from '@forgerock/sdk-types'; import type { Callback } from '@forgerock/sdk-types'; -describe('FRCallback', () => { +describe('JourneyCallback', () => { it('reads/writes basic properties', () => { const payload: Callback = { _id: 0, @@ -30,7 +30,7 @@ describe('FRCallback', () => { ], type: callbackType.NameCallback, }; - const cb = new FRCallback(payload); + const cb = new JourneyCallback(payload); cb.setInputValue('superman'); expect(cb.getType()).toBe('NameCallback'); diff --git a/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts b/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts index e2e0ccbb25..e27820c8ac 100644 --- a/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts +++ b/packages/journey-client/src/lib/callbacks/hidden-value-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect information indirectly from the user. */ -class HiddenValueCallback extends FRCallback { +class HiddenValueCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/index.ts b/packages/journey-client/src/lib/callbacks/index.ts index 86ddd41687..00f62424ad 100644 --- a/packages/journey-client/src/lib/callbacks/index.ts +++ b/packages/journey-client/src/lib/callbacks/index.ts @@ -10,7 +10,7 @@ import { type CallbackType, type Callback, type NameValue } from '@forgerock/sdk /** * Base class for authentication tree callback wrappers. */ -class FRCallback { +class JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ @@ -100,4 +100,4 @@ class FRCallback { } } -export default FRCallback; +export default JourneyCallback; diff --git a/packages/journey-client/src/lib/callbacks/kba-create-callback.ts b/packages/journey-client/src/lib/callbacks/kba-create-callback.ts index 017dcfe0f9..afbb00bc6d 100644 --- a/packages/journey-client/src/lib/callbacks/kba-create-callback.ts +++ b/packages/journey-client/src/lib/callbacks/kba-create-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect KBA-style security questions and answers. */ -class KbaCreateCallback extends FRCallback { +class KbaCreateCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/metadata-callback.ts b/packages/journey-client/src/lib/callbacks/metadata-callback.ts index cdd278f1fc..be523d73c2 100644 --- a/packages/journey-client/src/lib/callbacks/metadata-callback.ts +++ b/packages/journey-client/src/lib/callbacks/metadata-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to deliver and collect miscellaneous data. */ -class MetadataCallback extends FRCallback { +class MetadataCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/name-callback.ts b/packages/journey-client/src/lib/callbacks/name-callback.ts index b905675781..bd19ad5bb6 100644 --- a/packages/journey-client/src/lib/callbacks/name-callback.ts +++ b/packages/journey-client/src/lib/callbacks/name-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a username. */ -class NameCallback extends FRCallback { +class NameCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/password-callback.ts b/packages/journey-client/src/lib/callbacks/password-callback.ts index b43b47cd40..4d1e8685c7 100644 --- a/packages/journey-client/src/lib/callbacks/password-callback.ts +++ b/packages/journey-client/src/lib/callbacks/password-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a password. */ -class PasswordCallback extends FRCallback { +class PasswordCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts index 1fae82eb25..9e1dce3da7 100644 --- a/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * @class - Represents a callback used to complete and package up device and behavioral data. */ -class PingOneProtectEvaluationCallback extends FRCallback { +class PingOneProtectEvaluationCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts index 998dbfbbce..513d1d018f 100644 --- a/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts +++ b/packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * @class - Represents a callback used to initialize and start device and behavioral data collection. */ -class PingOneProtectInitializeCallback extends FRCallback { +class PingOneProtectInitializeCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts b/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts index aca5968a46..38c4be6bbe 100644 --- a/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts +++ b/packages/journey-client/src/lib/callbacks/polling-wait-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to instruct the system to poll while a backend process completes. */ -class PollingWaitCallback extends FRCallback { +class PollingWaitCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts b/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts index e75c89bb3e..28fc309c6c 100644 --- a/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts +++ b/packages/journey-client/src/lib/callbacks/recaptcha-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to integrate reCAPTCHA. */ -class ReCaptchaCallback extends FRCallback { +class ReCaptchaCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts index f31cbd98ef..4c6a05ebee 100644 --- a/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts +++ b/packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts @@ -8,13 +8,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to integrate reCAPTCHA. */ -class ReCaptchaEnterpriseCallback extends FRCallback { +class ReCaptchaEnterpriseCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/redirect-callback.ts b/packages/journey-client/src/lib/callbacks/redirect-callback.ts index 4992fd4f58..f90e0e180a 100644 --- a/packages/journey-client/src/lib/callbacks/redirect-callback.ts +++ b/packages/journey-client/src/lib/callbacks/redirect-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect an answer to a choice. */ -class RedirectCallback extends FRCallback { +class RedirectCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/select-idp-callback.ts b/packages/journey-client/src/lib/callbacks/select-idp-callback.ts index db6e701984..f1c9dafa10 100644 --- a/packages/journey-client/src/lib/callbacks/select-idp-callback.ts +++ b/packages/journey-client/src/lib/callbacks/select-idp-callback.ts @@ -4,7 +4,7 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; interface IdPValue { @@ -17,7 +17,7 @@ interface IdPValue { /** * Represents a callback used to collect an answer to a choice. */ -class SelectIdPCallback extends FRCallback { +class SelectIdPCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts index 9bc1cbe85d..7f04e34ac5 100644 --- a/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts +++ b/packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to collect acceptance of terms and conditions. */ -class TermsAndConditionsCallback extends FRCallback { +class TermsAndConditionsCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/text-input-callback.ts b/packages/journey-client/src/lib/callbacks/text-input-callback.ts index fa34ee7fc5..8df288830a 100644 --- a/packages/journey-client/src/lib/callbacks/text-input-callback.ts +++ b/packages/journey-client/src/lib/callbacks/text-input-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to retrieve input from the user. */ -class TextInputCallback extends FRCallback { +class TextInputCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/text-output-callback.ts b/packages/journey-client/src/lib/callbacks/text-output-callback.ts index 1a677e0486..8502db2354 100644 --- a/packages/journey-client/src/lib/callbacks/text-output-callback.ts +++ b/packages/journey-client/src/lib/callbacks/text-output-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; /** * Represents a callback used to display a message. */ -class TextOutputCallback extends FRCallback { +class TextOutputCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts index db99507945..f3098f78bd 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a valid platform password. */ -class ValidatedCreatePasswordCallback extends FRCallback { +class ValidatedCreatePasswordCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts index d982332657..805606ab05 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts @@ -4,13 +4,13 @@ * of the MIT license. See the LICENSE file for details. */ -import FRCallback from './index.js'; +import JourneyCallback from './index.js'; import type { Callback, PolicyRequirement } from '@forgerock/sdk-types'; /** * Represents a callback used to collect a valid platform username. */ -class ValidatedCreateUsernameCallback extends FRCallback { +class ValidatedCreateUsernameCallback extends JourneyCallback { /** * @param payload The raw payload returned by OpenAM */ diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts index 101350ae48..04a51a3ed2 100644 --- a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts +++ b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts @@ -8,7 +8,7 @@ * of the MIT license. See the LICENSE file for details. */ -import FRStep from '../fr-step.js'; +import JourneyStep from '../journey-step.js'; import FRQRCode from './fr-qrcode.js'; import { otpQRCodeStep, pushQRCodeStep } from './fr-qr-code.mock.data.js'; // import WebAuthn step as it's similar in structure for testing non-QR Code steps @@ -17,7 +17,7 @@ import { webAuthnRegJSCallback70 } from '../fr-webauthn/fr-webauthn.mock.data.js describe('Class for managing QR Codes', () => { it('should return true for step containing OTP QR Code callbacks', () => { const expected = true; - const step = new FRStep(otpQRCodeStep); + const step = new JourneyStep(otpQRCodeStep); const result = FRQRCode.isQRCodeStep(step); expect(result).toBe(expected); @@ -25,7 +25,7 @@ describe('Class for managing QR Codes', () => { it('should return true for step containing Push QR Code callbacks', () => { const expected = true; - const step = new FRStep(pushQRCodeStep); + const step = new JourneyStep(pushQRCodeStep); const result = FRQRCode.isQRCodeStep(step); expect(result).toBe(expected); @@ -33,7 +33,7 @@ describe('Class for managing QR Codes', () => { it('should return false for step containing WebAuthn step', () => { const expected = false; - const step = new FRStep(webAuthnRegJSCallback70); + const step = new JourneyStep(webAuthnRegJSCallback70); const result = FRQRCode.isQRCodeStep(step); expect(result).toBe(expected); @@ -49,7 +49,7 @@ describe('Class for managing QR Codes', () => { 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' + 'r=ForgeRock&period=30&digits=6&b=032b75', }; - const step = new FRStep(otpQRCodeStep); + const step = new JourneyStep(otpQRCodeStep); const result = FRQRCode.getQRCodeData(step); expect(result).toStrictEqual(expected); @@ -70,7 +70,7 @@ describe('Class for managing QR Codes', () => { 'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' + 'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75', }; - const step = new FRStep(pushQRCodeStep); + const step = new JourneyStep(pushQRCodeStep); const result = FRQRCode.getQRCodeData(step); expect(result).toStrictEqual(expected); diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts index db24ad9fab..3bdc6476c4 100644 --- a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts +++ b/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts @@ -9,7 +9,7 @@ */ import { callbackType } from '@forgerock/sdk-types'; -import FRStep from '../fr-step.js'; +import JourneyStep from '../journey-step.js'; import TextOutputCallback from '../callbacks/text-output-callback.js'; import HiddenValueCallback from '../callbacks/hidden-value-callback.js'; @@ -35,10 +35,10 @@ export type QRCodeData = { abstract class FRQRCode { /** * @method isQRCodeStep - determines if step contains QR Code callbacks - * @param {FRStep} step - step object from AM response + * @param {JourneyStep} step - step object from AM response * @returns {boolean} */ - public static isQRCodeStep(step: FRStep): boolean { + public static isQRCodeStep(step: JourneyStep): boolean { const hiddenValueCb = step.getCallbacksOfType(callbackType.HiddenValueCallback); // QR Codes step should have at least one HiddenValueCallback @@ -50,10 +50,10 @@ abstract class FRQRCode { /** * @method getQRCodeData - gets the necessary information from the QR Code callbacks - * @param {FRStep} step - step object from AM response + * @param {JourneyStep} step - step object from AM response * @returns {QRCodeData} */ - public static getQRCodeData(step: FRStep): QRCodeData { + public static getQRCodeData(step: JourneyStep): QRCodeData { const hiddenValueCb = step.getCallbacksOfType(callbackType.HiddenValueCallback); // QR Codes step should have at least one HiddenValueCallback diff --git a/packages/journey-client/src/lib/fr-recovery-codes/index.ts b/packages/journey-client/src/lib/fr-recovery-codes/index.ts index f43e8b20a9..05c1bd6ae9 100644 --- a/packages/journey-client/src/lib/fr-recovery-codes/index.ts +++ b/packages/journey-client/src/lib/fr-recovery-codes/index.ts @@ -10,7 +10,7 @@ import { callbackType } from '@forgerock/sdk-types'; import type TextOutputCallback from '../callbacks/text-output-callback.js'; -import type FRStep from '../fr-step.js'; +import type JourneyStep from '../journey-step.js'; import { parseDeviceNameText, parseDisplayRecoveryCodesText } from './script-parser.js'; /** @@ -28,7 +28,7 @@ import { parseDeviceNameText, parseDisplayRecoveryCodesText } from './script-par * ``` */ abstract class FRRecoveryCodes { - public static getDeviceName(step: FRStep): string { + public static getDeviceName(step: JourneyStep): string { const text = this.getDisplayCallback(step)?.getOutputByName('message', '') ?? ''; return parseDeviceNameText(text); } @@ -38,7 +38,7 @@ abstract class FRRecoveryCodes { * @param step The step to evaluate * @return Recovery Code values in array */ - public static getCodes(step: FRStep): string[] { + public static getCodes(step: JourneyStep): string[] { const text = this.getDisplayCallback(step)?.getOutputByName('message', ''); return parseDisplayRecoveryCodesText(text || ''); } @@ -49,7 +49,7 @@ abstract class FRRecoveryCodes { * @param step The step to evaluate * @return Is this step a Display Recovery Codes step */ - public static isDisplayStep(step: FRStep): boolean { + public static isDisplayStep(step: JourneyStep): boolean { return !!this.getDisplayCallback(step); } @@ -59,7 +59,7 @@ abstract class FRRecoveryCodes { * @param step The step to evaluate * @return gets the Display Recovery Codes' callback */ - private static getDisplayCallback(step: FRStep): TextOutputCallback | undefined { + private static getDisplayCallback(step: JourneyStep): TextOutputCallback | undefined { return step .getCallbacksOfType(callbackType.TextOutputCallback) .find((x) => { diff --git a/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts b/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts index f1ae9cc61a..1266415108 100644 --- a/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts +++ b/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts @@ -8,7 +8,7 @@ * of the MIT license. See the LICENSE file for details. */ -import FRStep from '../fr-step.js'; +import JourneyStep from '../journey-step.js'; import FRRecoveryCodes from './index.js'; import { displayRecoveryCodesResponse, @@ -19,24 +19,24 @@ import { describe('Class for managing the Display Recovery Codes node', () => { it('should return true if Display Recovery Codes step', () => { - const step = new FRStep(displayRecoveryCodesResponse); + const step = new JourneyStep(displayRecoveryCodesResponse); const isDisplayStep = FRRecoveryCodes.isDisplayStep(step); expect(isDisplayStep).toBe(true); }); it('should return false if not Display Recovery Codes step', () => { - const step = new FRStep(otherResponse); + const step = new JourneyStep(otherResponse); const isDisplayStep = FRRecoveryCodes.isDisplayStep(step); expect(isDisplayStep).toBe(false); }); it('should return the Recovery Codes as array of strings', () => { - const step = new FRStep(displayRecoveryCodesResponse); + const step = new JourneyStep(displayRecoveryCodesResponse); const recoveryCodes = FRRecoveryCodes.getCodes(step); expect(recoveryCodes).toStrictEqual(expectedRecoveryCodes); }); it('should return a display name from the getDisplayName method', () => { - const step = new FRStep(displayRecoveryCodesResponse); + const step = new JourneyStep(displayRecoveryCodesResponse); const displayName = FRRecoveryCodes.getDeviceName(step); expect(displayName).toStrictEqual(expectedDeviceName); }); diff --git a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts b/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts index b7796d2a15..bf5bd9936d 100644 --- a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts +++ b/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts @@ -22,30 +22,30 @@ import { webAuthnRegMetaCallback70StoredUsername, webAuthnAuthMetaCallback70StoredUsername, } from './fr-webauthn.mock.data.js'; -import FRStep from '../fr-step.js'; +import JourneyStep from '../journey-step.js'; describe('Test FRWebAuthn class with 6.5.3 "Passwordless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnRegJSCallback653 as any); + const step = new JourneyStep(webAuthnRegJSCallback653 as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnAuthJSCallback653 as any); + const step = new JourneyStep(webAuthnAuthJSCallback653 as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); // it('should return Registration type with register metadata callbacks', () => { // // eslint-disable-next-line - // const step = new FRStep(webAuthnRegMetaCallback653 as any); + // const step = new JourneyStep(webAuthnRegMetaCallback653 as any); // const stepType = FRWebAuthn.getWebAuthnStepType(step); // expect(stepType).toBe(WebAuthnStepType.Registration); // }); // it('should return Authentication type with authenticate metadata callbacks', () => { // // eslint-disable-next-line - // const step = new FRStep(webAuthnAuthMetaCallback653 as any); + // const step = new JourneyStep(webAuthnAuthMetaCallback653 as any); // const stepType = FRWebAuthn.getWebAuthnStepType(step); // expect(stepType).toBe(WebAuthnStepType.Authentication); // }); @@ -54,26 +54,26 @@ describe('Test FRWebAuthn class with 6.5.3 "Passwordless"', () => { describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnRegJSCallback70 as any); + const step = new JourneyStep(webAuthnRegJSCallback70 as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnAuthJSCallback70 as any); + const step = new JourneyStep(webAuthnAuthJSCallback70 as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); console.log('the step type', stepType, WebAuthnStepType.Authentication); expect(stepType).toBe(WebAuthnStepType.Authentication); }); it('should return Registration type with register metadata callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnRegMetaCallback70 as any); + const step = new JourneyStep(webAuthnRegMetaCallback70 as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate metadata callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnAuthMetaCallback70 as any); + const step = new JourneyStep(webAuthnAuthMetaCallback70 as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); @@ -82,25 +82,25 @@ describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => { describe('Test FRWebAuthn class with 7.0 "Usernameless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnRegJSCallback70StoredUsername as any); + const step = new JourneyStep(webAuthnRegJSCallback70StoredUsername as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnAuthJSCallback70StoredUsername as any); + const step = new JourneyStep(webAuthnAuthJSCallback70StoredUsername as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); it('should return Registration type with register metadata callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnRegMetaCallback70StoredUsername as any); + const step = new JourneyStep(webAuthnRegMetaCallback70StoredUsername as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate metadata callbacks', () => { // eslint-disable-next-line - const step = new FRStep(webAuthnAuthMetaCallback70StoredUsername as any); + const step = new JourneyStep(webAuthnAuthMetaCallback70StoredUsername as any); const stepType = FRWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); diff --git a/packages/journey-client/src/lib/fr-webauthn/index.ts b/packages/journey-client/src/lib/fr-webauthn/index.ts index 373cea0752..a57a96a3c9 100644 --- a/packages/journey-client/src/lib/fr-webauthn/index.ts +++ b/packages/journey-client/src/lib/fr-webauthn/index.ts @@ -12,7 +12,7 @@ import { callbackType } from '@forgerock/sdk-types'; import type HiddenValueCallback from '../callbacks/hidden-value-callback.js'; import type MetadataCallback from '../callbacks/metadata-callback.js'; -import type FRStep from '../fr-step.js'; +import type JourneyStep from '../journey-step.js'; import { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType } from './enums.js'; import { arrayBufferToString, @@ -67,7 +67,7 @@ abstract class FRWebAuthn { * @param step The step to evaluate * @return A WebAuthnStepType value */ - public static getWebAuthnStepType(step: FRStep): WebAuthnStepType { + public static getWebAuthnStepType(step: JourneyStep): WebAuthnStepType { const outcomeCallback = this.getOutcomeCallback(step); const metadataCallback = this.getMetadataCallback(step); const textOutputCallback = this.getTextOutputCallback(step); @@ -102,7 +102,7 @@ abstract class FRWebAuthn { * @param step The step that contains WebAuthn authentication data * @return The populated step */ - public static async authenticate(step: FRStep): Promise { + public static async authenticate(step: JourneyStep): Promise { const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step); if (hiddenCallback && (metadataCallback || textOutputCallback)) { let outcome: ReturnType; @@ -170,9 +170,9 @@ abstract class FRWebAuthn { // be inferred from the type so `typeof deviceName` will not just return string // but the actual name of the deviceName passed in as a generic. public static async register( - step: FRStep, + step: JourneyStep, deviceName?: T, - ): Promise { + ): Promise { const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step); if (hiddenCallback && (metadataCallback || textOutputCallback)) { let outcome: OutcomeWithName; @@ -239,7 +239,7 @@ abstract class FRWebAuthn { * @param step The step that contains WebAuthn callbacks * @return The WebAuthn callbacks */ - public static getCallbacks(step: FRStep): WebAuthnCallbacks { + public static getCallbacks(step: JourneyStep): WebAuthnCallbacks { const hiddenCallback = this.getOutcomeCallback(step); const metadataCallback = this.getMetadataCallback(step); const textOutputCallback = this.getTextOutputCallback(step); @@ -263,7 +263,7 @@ abstract class FRWebAuthn { * @param step The step that contains WebAuthn callbacks * @return The metadata callback */ - public static getMetadataCallback(step: FRStep): MetadataCallback | undefined { + public static getMetadataCallback(step: JourneyStep): MetadataCallback | undefined { return step.getCallbacksOfType(callbackType.MetadataCallback).find((x) => { const cb = x.getOutputByName('data', undefined); // eslint-disable-next-line no-prototype-builtins @@ -277,7 +277,7 @@ abstract class FRWebAuthn { * @param step The step that contains WebAuthn callbacks * @return The hidden value callback */ - public static getOutcomeCallback(step: FRStep): HiddenValueCallback | undefined { + public static getOutcomeCallback(step: JourneyStep): HiddenValueCallback | undefined { return step .getCallbacksOfType(callbackType.HiddenValueCallback) .find((x) => x.getOutputByName('id', '') === 'webAuthnOutcome'); @@ -290,7 +290,7 @@ abstract class FRWebAuthn { * @param step The step that contains WebAuthn callbacks * @return The metadata callback */ - public static getTextOutputCallback(step: FRStep): TextOutputCallback | undefined { + public static getTextOutputCallback(step: JourneyStep): TextOutputCallback | undefined { return step .getCallbacksOfType(callbackType.TextOutputCallback) .find((x) => { diff --git a/packages/journey-client/src/lib/journey-client.test.ts b/packages/journey-client/src/lib/journey-client.test.ts index 60cc5a98f4..228e7d998d 100644 --- a/packages/journey-client/src/lib/journey-client.test.ts +++ b/packages/journey-client/src/lib/journey-client.test.ts @@ -7,7 +7,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { journey } from './journey-client.js'; -import FRStep from './fr-step.js'; +import JourneyStep from './journey-step.js'; import { JourneyClientConfig } from './config.types.js'; import { callbackType, Step } from '@forgerock/sdk-types'; @@ -66,7 +66,7 @@ describe('journey-client', () => { const request = mockFetch.mock.calls[0][0] as Request; // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate expect(request.url).toBe('https://test.com/json/realms/root/authenticate'); - expect(step).toBeInstanceOf(FRStep); + expect(step).toBeInstanceOf(JourneyStep); expect(step && step.payload).toEqual(mockStepResponse); }); @@ -89,7 +89,7 @@ describe('journey-client', () => { }, ], }; - const initialStep = new FRStep(initialStepPayload); + const initialStep = new JourneyStep(initialStepPayload); mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); @@ -103,7 +103,7 @@ describe('journey-client', () => { expect(request.url).toBe('https://test.com/json/realms/root/authenticate'); expect(request.method).toBe('POST'); expect(await request.json()).toEqual(initialStep.payload); - expect(nextStep).toBeInstanceOf(FRStep); + expect(nextStep).toBeInstanceOf(JourneyStep); expect(nextStep && nextStep.payload).toEqual(nextStepPayload); }); @@ -117,7 +117,7 @@ describe('journey-client', () => { }, ], }; - const step = new FRStep(mockStepPayload); + const step = new JourneyStep(mockStepPayload); const assignMock = vi.fn(); const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({ @@ -163,7 +163,7 @@ describe('journey-client', () => { expect(request.method).toBe('POST'); expect(await request.json()).toEqual(previousStepPayload); - expect(step).toBeInstanceOf(FRStep); + expect(step).toBeInstanceOf(JourneyStep); expect(step && step.payload).toEqual(nextStepPayload); }); @@ -192,7 +192,7 @@ describe('journey-client', () => { const request = mockFetch.mock.calls[0][0] as Request; expect(request.method).toBe('POST'); expect(await request.json()).toEqual(plainStepPayload); // Expect the plain payload to be sent - expect(step).toBeInstanceOf(FRStep); // The returned step should still be an FRStep instance + expect(step).toBeInstanceOf(JourneyStep); // The returned step should still be an JourneyStep instance expect(step && step.payload).toEqual(nextStepPayload); }); @@ -228,7 +228,7 @@ describe('journey-client', () => { // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate const url = new URL(request.url); expect(url.origin + url.pathname).toBe('https://test.com/json/realms/root/authenticate'); - expect(step).toBeInstanceOf(FRStep); + expect(step).toBeInstanceOf(JourneyStep); expect(step && step.payload).toEqual(mockStepResponse); }); }); diff --git a/packages/journey-client/src/lib/journey-client.ts b/packages/journey-client/src/lib/journey-client.ts index 06cd29efdc..78eb6151f8 100644 --- a/packages/journey-client/src/lib/journey-client.ts +++ b/packages/journey-client/src/lib/journey-client.ts @@ -11,7 +11,7 @@ import { journeyApi } from './journey.api.js'; import { setConfig } from './journey.slice.js'; import { createStorage } from '@forgerock/storage'; import { GenericError, callbackType, type Step } from '@forgerock/sdk-types'; -import FRStep from './fr-step.js'; +import JourneyStep from './journey-step.js'; import RedirectCallback from './callbacks/redirect-callback.js'; import { logger as loggerFn, LogLevel, CustomLogger } from '@forgerock/sdk-logger'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; @@ -41,22 +41,22 @@ export async function journey({ const self = { start: async (options?: StepOptions) => { const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options)); - return data ? new FRStep(data) : undefined; + return data ? new JourneyStep(data) : undefined; }, /** - * Submits the current Step payload to the authentication API and retrieves the next FRStep in the journey. + * Submits the current Step payload to the authentication API and retrieves the next JourneyStep in the journey. * The `step` to be submitted is provided within the `options` object. * * @param options An object containing the current Step payload and optional StepOptions. - * @returns A Promise that resolves to the next FRStep in the journey, or undefined if the journey ends. + * @returns A Promise that resolves to the next JourneyStep in the journey, or undefined if the journey ends. */ next: async (step: Step, options?: StepOptions) => { const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options })); - return data ? new FRStep(data) : undefined; + return data ? new JourneyStep(data) : undefined; }, - redirect: async (step: FRStep) => { + redirect: async (step: JourneyStep) => { const cb = step.getCallbackOfType(callbackType.RedirectCallback) as RedirectCallback; if (!cb) { throw new Error('RedirectCallback not found on step'); @@ -87,7 +87,7 @@ export async function journey({ return typeof obj === 'object' && obj !== null && 'error' in obj && 'message' in obj; } - // Type guard for { step: FRStep } + // Type guard for { step: JourneyStep } function isStoredStep(obj: unknown): obj is { step: Step } { return ( typeof obj === 'object' && diff --git a/packages/journey-client/src/lib/fr-step.test.ts b/packages/journey-client/src/lib/journey-step.test.ts similarity index 86% rename from packages/journey-client/src/lib/fr-step.test.ts rename to packages/journey-client/src/lib/journey-step.test.ts index ad06d3c17a..3e131efa76 100644 --- a/packages/journey-client/src/lib/fr-step.test.ts +++ b/packages/journey-client/src/lib/journey-step.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect } from 'vitest'; import { callbackType, type Step } from '@forgerock/sdk-types'; -import FRStep from './fr-step.js'; +import JourneyStep from './journey-step.js'; import NameCallback from './callbacks/name-callback.js'; describe('fr-step.ts', () => { @@ -36,7 +36,7 @@ describe('fr-step.ts', () => { }; it('should correctly initialize with a payload', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); expect(step.payload).toEqual(stepPayload); expect(step.callbacks).toHaveLength(3); expect(step.callbacks[0]).toBeInstanceOf(NameCallback); @@ -44,26 +44,26 @@ describe('fr-step.ts', () => { it('should get a single callback of a specific type', () => { const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] }; - const step = new FRStep(singleCallbackPayload); + const step = new JourneyStep(singleCallbackPayload); const cb = step.getCallbackOfType(callbackType.PasswordCallback); expect(cb).toBeDefined(); expect(cb.getType()).toBe(callbackType.PasswordCallback); }); it('should throw an error if getCallbackOfType finds no matching callbacks', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); const err = `Expected 1 callback of type "TermsAndConditionsCallback", but found 0`; expect(() => step.getCallbackOfType(callbackType.TermsAndConditionsCallback)).toThrow(err); }); it('should throw an error if getCallbackOfType finds multiple matching callbacks', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); const err = `Expected 1 callback of type "NameCallback", but found 2`; expect(() => step.getCallbackOfType(callbackType.NameCallback)).toThrow(err); }); it('should get all callbacks of a specific type', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); const callbacks = step.getCallbacksOfType(callbackType.NameCallback); expect(callbacks).toHaveLength(2); expect(callbacks[0].getType()).toBe(callbackType.NameCallback); @@ -71,31 +71,31 @@ describe('fr-step.ts', () => { }); it('should return an empty array if getCallbacksOfType finds no matches', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); const callbacks = step.getCallbacksOfType(callbackType.TermsAndConditionsCallback); expect(callbacks).toHaveLength(0); }); it('should set the value of a specific callback', () => { const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] }; - const step = new FRStep(singleCallbackPayload); + const step = new JourneyStep(singleCallbackPayload); step.setCallbackValue(callbackType.PasswordCallback, 'password123'); const cb = step.getCallbackOfType(callbackType.PasswordCallback); expect(cb.getInputValue()).toBe('password123'); }); it('should return the description', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); expect(step.getDescription()).toBe('Step description'); }); it('should return the header', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); expect(step.getHeader()).toBe('Step header'); }); it('should return the stage', () => { - const step = new FRStep(stepPayload); + const step = new JourneyStep(stepPayload); expect(step.getStage()).toBe('Step stage'); }); }); diff --git a/packages/journey-client/src/lib/fr-step.ts b/packages/journey-client/src/lib/journey-step.ts similarity index 81% rename from packages/journey-client/src/lib/fr-step.ts rename to packages/journey-client/src/lib/journey-step.ts index 0d9341a37f..cb9763ec1d 100644 --- a/packages/journey-client/src/lib/fr-step.ts +++ b/packages/journey-client/src/lib/journey-step.ts @@ -11,15 +11,15 @@ import { type Step, type AuthResponse, } from '@forgerock/sdk-types'; -import type FRCallback from './callbacks/index.js'; -import type { FRCallbackFactory } from './callbacks/factory.js'; +import type JourneyCallback from './callbacks/index.js'; +import type { JourneyCallbackFactory } from './callbacks/factory.js'; import createCallback from './callbacks/factory.js'; import { StepType } from '@forgerock/sdk-types'; /** * Represents a single step of an authentication tree. */ -class FRStep implements AuthResponse { +class JourneyStep implements AuthResponse { /** * The type of step. */ @@ -28,15 +28,15 @@ class FRStep implements AuthResponse { /** * The callbacks contained in this step. */ - public callbacks: FRCallback[] = []; + public callbacks: JourneyCallback[] = []; /** * @param payload The raw payload returned by OpenAM - * @param callbackFactory A function that returns am implementation of FRCallback + * @param callbackFactory A function that returns am implementation of JourneyCallback */ constructor( public payload: Step, - callbackFactory?: FRCallbackFactory, + callbackFactory?: JourneyCallbackFactory, ) { if (payload.callbacks) { this.callbacks = this.convertCallbacks(payload.callbacks, callbackFactory); @@ -48,7 +48,7 @@ class FRStep implements AuthResponse { * * @param type The type of callback to find. */ - public getCallbackOfType(type: CallbackType): T { + public getCallbackOfType(type: CallbackType): T { const callbacks = this.getCallbacksOfType(type); if (callbacks.length !== 1) { throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`); @@ -61,7 +61,7 @@ class FRStep implements AuthResponse { * * @param type The type of callback to find. */ - public getCallbacksOfType(type: CallbackType): T[] { + public getCallbacksOfType(type: CallbackType): T[] { return this.callbacks.filter((x) => x.getType() === type) as T[]; } @@ -102,8 +102,8 @@ class FRStep implements AuthResponse { private convertCallbacks( callbacks: Callback[], - callbackFactory?: FRCallbackFactory, - ): FRCallback[] { + callbackFactory?: JourneyCallbackFactory, + ): JourneyCallback[] { const converted = callbacks.map((x: Callback) => { // This gives preference to the provided factory and falls back to our default implementation return (callbackFactory || createCallback)(x) || createCallback(x); @@ -115,7 +115,7 @@ class FRStep implements AuthResponse { /** * A function that can populate the provided authentication tree step. */ -type FRStepHandler = (step: FRStep) => void; +type JourneyStepHandler = (step: JourneyStep) => void; -export default FRStep; -export type { FRStepHandler }; +export default JourneyStep; +export type { JourneyStepHandler }; From 92ca83ce06f5672458d4858e360f754bdb0f7e0a Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 18 Sep 2025 14:33:29 -0600 Subject: [PATCH 05/11] refactor(journey-client): rename fr-webauthn to journey-webauthn and other fr prefixes to journey --- .../src/lib/fr-recovery-codes/index.ts | 8 +++---- .../fr-recovery-codes/recovery-codes.test.ts | 10 ++++---- .../journey-client/src/lib/journey-client.ts | 7 +++++- ...in-failure.ts => journey-login-failure.ts} | 4 ++-- ...in-success.ts => journey-login-success.ts} | 4 ++-- .../journey-qr-code.mock.data.ts} | 0 .../journey-qrcode.test.ts} | 16 ++++++------- .../journey-qrcode.ts} | 10 ++++---- .../enums.ts | 0 .../helpers.mock.data.ts | 9 ++++--- .../helpers.test.ts | 0 .../helpers.ts | 0 .../index.ts | 10 ++++---- .../interfaces.ts | 0 .../journey-webauthn.mock.data.ts} | 13 ---------- .../journey-webauthn.test.ts} | 24 +++++++++---------- .../script-parser.test.ts | 0 .../script-parser.ts | 0 .../script-text.mock.data.ts | 1 - 19 files changed, 53 insertions(+), 63 deletions(-) rename packages/journey-client/src/lib/{fr-login-failure.ts => journey-login-failure.ts} (93%) rename packages/journey-client/src/lib/{fr-login-success.ts => journey-login-success.ts} (91%) rename packages/journey-client/src/lib/{fr-qrcode/fr-qr-code.mock.data.ts => journey-qrcode/journey-qr-code.mock.data.ts} (100%) rename packages/journey-client/src/lib/{fr-qrcode/fr-qrcode.test.ts => journey-qrcode/journey-qrcode.test.ts} (83%) rename packages/journey-client/src/lib/{fr-qrcode/fr-qrcode.ts => journey-qrcode/journey-qrcode.ts} (92%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/enums.ts (100%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/helpers.mock.data.ts (96%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/helpers.test.ts (100%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/helpers.ts (100%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/index.ts (98%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/interfaces.ts (100%) rename packages/journey-client/src/lib/{fr-webauthn/fr-webauthn.mock.data.ts => journey-webauthn/journey-webauthn.mock.data.ts} (98%) rename packages/journey-client/src/lib/{fr-webauthn/fr-webauthn.test.ts => journey-webauthn/journey-webauthn.test.ts} (85%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/script-parser.test.ts (100%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/script-parser.ts (100%) rename packages/journey-client/src/lib/{fr-webauthn => journey-webauthn}/script-text.mock.data.ts (99%) diff --git a/packages/journey-client/src/lib/fr-recovery-codes/index.ts b/packages/journey-client/src/lib/fr-recovery-codes/index.ts index 05c1bd6ae9..ec77b83409 100644 --- a/packages/journey-client/src/lib/fr-recovery-codes/index.ts +++ b/packages/journey-client/src/lib/fr-recovery-codes/index.ts @@ -20,14 +20,14 @@ import { parseDeviceNameText, parseDisplayRecoveryCodesText } from './script-par * * ```js * // Determine if step is Display Recovery Codes step - * const isDisplayRecoveryCodesStep = FRRecoveryCodes.isDisplayStep(step); + * const isDisplayRecoveryCodesStep = JourneyRecoveryCodes.isDisplayStep(step); * if (isDisplayRecoveryCodesStep) { - * const recoveryCodes = FRRecoveryCodes.getCodes(step); + * const recoveryCodes = JourneyRecoveryCodes.getCodes(step); * // Do the UI needful * } * ``` */ -abstract class FRRecoveryCodes { +abstract class JourneyRecoveryCodes { public static getDeviceName(step: JourneyStep): string { const text = this.getDisplayCallback(step)?.getOutputByName('message', '') ?? ''; return parseDeviceNameText(text); @@ -69,4 +69,4 @@ abstract class FRRecoveryCodes { } } -export default FRRecoveryCodes; +export default JourneyRecoveryCodes; diff --git a/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts b/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts index 1266415108..54d5177377 100644 --- a/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts +++ b/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts @@ -9,7 +9,7 @@ */ import JourneyStep from '../journey-step.js'; -import FRRecoveryCodes from './index.js'; +import JourneyRecoveryCodes from './index.js'; import { displayRecoveryCodesResponse, expectedDeviceName, @@ -20,24 +20,24 @@ import { describe('Class for managing the Display Recovery Codes node', () => { it('should return true if Display Recovery Codes step', () => { const step = new JourneyStep(displayRecoveryCodesResponse); - const isDisplayStep = FRRecoveryCodes.isDisplayStep(step); + const isDisplayStep = JourneyRecoveryCodes.isDisplayStep(step); expect(isDisplayStep).toBe(true); }); it('should return false if not Display Recovery Codes step', () => { const step = new JourneyStep(otherResponse); - const isDisplayStep = FRRecoveryCodes.isDisplayStep(step); + const isDisplayStep = JourneyRecoveryCodes.isDisplayStep(step); expect(isDisplayStep).toBe(false); }); it('should return the Recovery Codes as array of strings', () => { const step = new JourneyStep(displayRecoveryCodesResponse); - const recoveryCodes = FRRecoveryCodes.getCodes(step); + const recoveryCodes = JourneyRecoveryCodes.getCodes(step); expect(recoveryCodes).toStrictEqual(expectedRecoveryCodes); }); it('should return a display name from the getDisplayName method', () => { const step = new JourneyStep(displayRecoveryCodesResponse); - const displayName = FRRecoveryCodes.getDeviceName(step); + const displayName = JourneyRecoveryCodes.getDeviceName(step); expect(displayName).toStrictEqual(expectedDeviceName); }); }); diff --git a/packages/journey-client/src/lib/journey-client.ts b/packages/journey-client/src/lib/journey-client.ts index 78eb6151f8..552e691ab2 100644 --- a/packages/journey-client/src/lib/journey-client.ts +++ b/packages/journey-client/src/lib/journey-client.ts @@ -12,6 +12,8 @@ import { setConfig } from './journey.slice.js'; import { createStorage } from '@forgerock/storage'; import { GenericError, callbackType, type Step } from '@forgerock/sdk-types'; import JourneyStep from './journey-step.js'; +import JourneyLoginSuccess from './journey-login-success.js'; +import JourneyLoginFailure from './journey-login-failure.js'; import RedirectCallback from './callbacks/redirect-callback.js'; import { logger as loggerFn, LogLevel, CustomLogger } from '@forgerock/sdk-logger'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; @@ -69,7 +71,10 @@ export async function journey({ window.location.assign(redirectUrl); }, - resume: async (url: string, options?: StepOptions) => { + resume: async ( + url: string, + options?: StepOptions, + ): Promise => { const parsedUrl = new URL(url); const code = parsedUrl.searchParams.get('code'); const state = parsedUrl.searchParams.get('state'); diff --git a/packages/journey-client/src/lib/fr-login-failure.ts b/packages/journey-client/src/lib/journey-login-failure.ts similarity index 93% rename from packages/journey-client/src/lib/fr-login-failure.ts rename to packages/journey-client/src/lib/journey-login-failure.ts index c1761eb600..af7b70358a 100644 --- a/packages/journey-client/src/lib/fr-login-failure.ts +++ b/packages/journey-client/src/lib/journey-login-failure.ts @@ -9,7 +9,7 @@ import { StepType } from '@forgerock/sdk-types'; import FRPolicy from './fr-policy/index.js'; import type { MessageCreator, ProcessedPropertyError } from './fr-policy/interfaces.js'; -class FRLoginFailure implements AuthResponse { +class JourneyLoginFailure implements AuthResponse { /** * The type of step. */ @@ -56,4 +56,4 @@ class FRLoginFailure implements AuthResponse { } } -export default FRLoginFailure; +export default JourneyLoginFailure; diff --git a/packages/journey-client/src/lib/fr-login-success.ts b/packages/journey-client/src/lib/journey-login-success.ts similarity index 91% rename from packages/journey-client/src/lib/fr-login-success.ts rename to packages/journey-client/src/lib/journey-login-success.ts index ff087596ba..5b39122967 100644 --- a/packages/journey-client/src/lib/fr-login-success.ts +++ b/packages/journey-client/src/lib/journey-login-success.ts @@ -7,7 +7,7 @@ import type { Step, AuthResponse } from '@forgerock/sdk-types'; import { StepType } from '@forgerock/sdk-types'; -class FRLoginSuccess implements AuthResponse { +class JourneyLoginSuccess implements AuthResponse { /** * The type of step. */ @@ -40,4 +40,4 @@ class FRLoginSuccess implements AuthResponse { } } -export default FRLoginSuccess; +export default JourneyLoginSuccess; diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts b/packages/journey-client/src/lib/journey-qrcode/journey-qr-code.mock.data.ts similarity index 100% rename from packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts rename to packages/journey-client/src/lib/journey-qrcode/journey-qr-code.mock.data.ts diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts similarity index 83% rename from packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts rename to packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts index 04a51a3ed2..af86aeab4f 100644 --- a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts +++ b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts @@ -9,16 +9,16 @@ */ import JourneyStep from '../journey-step.js'; -import FRQRCode from './fr-qrcode.js'; -import { otpQRCodeStep, pushQRCodeStep } from './fr-qr-code.mock.data.js'; +import JourneyQRCode from './journey-qrcode.js'; +import { otpQRCodeStep, pushQRCodeStep } from '../journey-qrcode/journey-qr-code.mock.data.js'; // import WebAuthn step as it's similar in structure for testing non-QR Code steps -import { webAuthnRegJSCallback70 } from '../fr-webauthn/fr-webauthn.mock.data.js'; +import { webAuthnRegJSCallback70 } from '../journey-webauthn/journey-webauthn.mock.data.js'; describe('Class for managing QR Codes', () => { it('should return true for step containing OTP QR Code callbacks', () => { const expected = true; const step = new JourneyStep(otpQRCodeStep); - const result = FRQRCode.isQRCodeStep(step); + const result = JourneyQRCode.isQRCodeStep(step); expect(result).toBe(expected); }); @@ -26,7 +26,7 @@ describe('Class for managing QR Codes', () => { it('should return true for step containing Push QR Code callbacks', () => { const expected = true; const step = new JourneyStep(pushQRCodeStep); - const result = FRQRCode.isQRCodeStep(step); + const result = JourneyQRCode.isQRCodeStep(step); expect(result).toBe(expected); }); @@ -34,7 +34,7 @@ describe('Class for managing QR Codes', () => { it('should return false for step containing WebAuthn step', () => { const expected = false; const step = new JourneyStep(webAuthnRegJSCallback70); - const result = FRQRCode.isQRCodeStep(step); + const result = JourneyQRCode.isQRCodeStep(step); expect(result).toBe(expected); }); @@ -50,7 +50,7 @@ describe('Class for managing QR Codes', () => { 'r=ForgeRock&period=30&digits=6&b=032b75', }; const step = new JourneyStep(otpQRCodeStep); - const result = FRQRCode.getQRCodeData(step); + const result = JourneyQRCode.getQRCodeData(step); expect(result).toStrictEqual(expected); }); @@ -71,7 +71,7 @@ describe('Class for managing QR Codes', () => { 'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75', }; const step = new JourneyStep(pushQRCodeStep); - const result = FRQRCode.getQRCodeData(step); + const result = JourneyQRCode.getQRCodeData(step); expect(result).toStrictEqual(expected); }); diff --git a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts similarity index 92% rename from packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts rename to packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts index 3bdc6476c4..21af7fe263 100644 --- a/packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts +++ b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts @@ -20,19 +20,19 @@ export type QRCodeData = { }; /** - * @class FRQRCode - A utility class for handling QR Code steps + * @class JourneyQRCode - A utility class for handling QR Code steps * * Example: * * ```js - * const isQRCodeStep = FRQRCode.isQRCodeStep(step); + * const isQRCodeStep = JourneyQRCode.isQRCodeStep(step); * let qrCodeData; * if (isQRCodeStep) { - * qrCodeData = FRQRCode.getQRCodeData(step); + * qrCodeData = JourneyQRCode.getQRCodeData(step); * } * ``` */ -abstract class FRQRCode { +abstract class JourneyQRCode { /** * @method isQRCodeStep - determines if step contains QR Code callbacks * @param {JourneyStep} step - step object from AM response @@ -93,4 +93,4 @@ abstract class FRQRCode { } } -export default FRQRCode; +export default JourneyQRCode; diff --git a/packages/journey-client/src/lib/fr-webauthn/enums.ts b/packages/journey-client/src/lib/journey-webauthn/enums.ts similarity index 100% rename from packages/journey-client/src/lib/fr-webauthn/enums.ts rename to packages/journey-client/src/lib/journey-webauthn/enums.ts diff --git a/packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts b/packages/journey-client/src/lib/journey-webauthn/helpers.mock.data.ts similarity index 96% rename from packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts rename to packages/journey-client/src/lib/journey-webauthn/helpers.mock.data.ts index 8e0dd24d74..1113a31dd6 100644 --- a/packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts +++ b/packages/journey-client/src/lib/journey-webauthn/helpers.mock.data.ts @@ -8,18 +8,17 @@ * of the MIT license. See the LICENSE file for details. */ -// eslint-disable-next-line export const allowCredentials70 = 'allowCredentials: [{ "type": "public-key", "id": new Int8Array([1, -16, 9, 79, 6, -2, -82, 51, 124, -94, 95, 23, -86, 70, -43, 89, 91, -9, 45, -22, 91, -51, 84, 93, 24, -64, 38, 101, 126, -53, 87, 70, -49, -88, -105, 116, 33, 75, -39, -92, -5, 115, 12, 52, 124, -100, 85, 104, -15, 5, -13, 25, -74, 101, 71, -115, -102, 16, 10, -9, -19, -110, 65, 118, -28, 89, -15, -115, -81, 22, -104, 123, 17, -92, 49, 109, -38, -51, 100, 96, -65, 25, -48, 28, 106, -45, 17, -45, -37, 46, -5, -6, -26, -23, -108, 13, -66, -55, -117, -107, 119, 7, -32, 34, 46, 0, -29, -111, -32, 45, -15, -113, 110, 123, -44, 6, 10, 65, 99, 25, 105, 69, -127, 76, 127, -33, -89, -56, 74, 25, 43, -43, -56, 9, 87, 80, 124, -32, -39, 115, 17, 18, 78, 121, 69, -36, -44, -28, -109, -126, 58, 64, 80, -4, 21, 63, -19]).buffer }]'; -// eslint-disable-next-line + export const allowMultipleCredentials70 = 'allowCredentials: [{ "type": "public-key", "id": new Int8Array([-33, 120, 18, 124, 16, 5, -127, -51, 68, 85, 76, -92, -3, 25, 116, 91, 71, 53, 106, -114, -86, 9, -81, -96, -32, -110, 66, -23, -5, 104, 96, 46, 43, -66, -110, 8, -70, 11, 51, -93, 19, 124, 81, 78, 58, -97, 89, -87, -26, -112, 95, -83, 118, -25, 118, 3, 35, -96, 120, -76, -87, 83, -101, 82]).buffer },{ "type": "public-key", "id": new Int8Array([1, 73, 82, 23, 76, -5, -53, -48, -104, -17, -42, 45, 7, 35, 35, -72, -7, 37, 9, 37, 117, 42, 66, 116, 58, 25, -68, 53, 113, -10, 102, 3, -60, -81, -74, 96, -5, 111, -56, 110, -101, -54, -31, -123, -100, 3, 37, -69, -114, -19, -25, -62, 18, 122, 39, 11, 83, 60, -58, 3, 116, 10, -80, -35, 6, -128, -51, -92, 100, -115, -22, -122, 21, -65, 97, 67, -49, 26, 42, -11, 90, 121, -63, 112, -16, 118, -99, -73, -89, -67, 72, -80, 18, -72, 109, 4, -14, 1, -93, 17, -17, -70, -2, -5, -116, 111, -128, 7, 62, -34, -43, 110, 89, 113, -65, -95, 113, -5, -104, -100, -73, 42, 2, 112, -21, 41, 41, 91, 108, -102, 47, -77, -52, 70, 107, -4, 25, -120, 114, 30, 23, 103, 120, 17, 55, 91, -110, -58, -110, 13, -56, 57, -126, 36, 40, 89, -9]).buffer }]'; -// eslint-disable-next-line + export const acceptableCredentials653 = '{ "type": "public-key", "id": new Int8Array([53, -21, 26, -99, 5, 4, -112, -76, -126, 90, -35, -7, -31, -92, 19, -71, -39, 73, 52, -10, -14, -7, -59, -7, -36, -111, 64, 101, 29, 89, 90, -56, 108, 42, 32, -19, -113, 118, -114, 49, 109, -70, 68, -89, 36, -36, -103, -128, 34, -24, -40, -71, -125, -120, -80, 63, 25, -33, 2, 26, 111, -52, -15, -52]).buffer }'; -// eslint-disable-next-line + export const acceptableMultipleCredentials653 = '{ "type": "public-key", "id": new Int8Array([17, 88, -12, 26, -50, -48, -38, 36, -69, -105, -68, -38, 66, -53, -37, -50, -109, -126, 122, 26, 25, -45, 96, 37, -124, 102, 124, 94, -98, -59, 113, 94, 115, -111, -69, 45, 37, -83, 118, -115, -4, -49, 34, 115, -24, -49, -37, -17, -127, -15, 62, 18, 93, 122, -109, 53, -52, 44, -63, -74, 109, 2, -110, 45]).buffer },{ "type": "public-key", "id": new Int8Array([1, 83, -98, 32, 110, -62, 78, 53, -63, -118, 12, 122, -72, -15, 85, 48, -39, -97, -73, 108, -122, -60, 56, -112, -89, -118, 111, 0, -4, 13, -50, -43, -53, 28, 114, 82, 22, -76, 15, 51, -95, 26, -90, -93, -51, 115, -28, 85, -105, -27, 111, 70, 106, -28, 45, 126, 44, 63, -16, 97, -55, 31, -24, 57, -92, 48, 26, 127, -39, 75, 12, 13, 100, -77, 13, -48, 49, 52, 31, 85, 9, 63, -122, -90, -54, -87, -110, -1, 115, -122, -69, -15, 83, 57, 95, -31, -92, -116, 89, -109, -98, 21, 24, -80, -28, 103, -28, -82, -39, 114, -29, -46, -76, 123, -69, -44, 124, 10, 53, -103, 19, -43, -12, 62, -83, -86, 95, -78, 70, -105, 116, -25, 106, 54, -72, -119, 91, 91, -71, -49, 22, 25, -108, 112, -14, 55, 9, 75, 89, -91, -59, 45]).buffer }'; -// eslint-disable-next-line + export const pubKeyCredParamsStr = '[ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ]'; diff --git a/packages/journey-client/src/lib/fr-webauthn/helpers.test.ts b/packages/journey-client/src/lib/journey-webauthn/helpers.test.ts similarity index 100% rename from packages/journey-client/src/lib/fr-webauthn/helpers.test.ts rename to packages/journey-client/src/lib/journey-webauthn/helpers.test.ts diff --git a/packages/journey-client/src/lib/fr-webauthn/helpers.ts b/packages/journey-client/src/lib/journey-webauthn/helpers.ts similarity index 100% rename from packages/journey-client/src/lib/fr-webauthn/helpers.ts rename to packages/journey-client/src/lib/journey-webauthn/helpers.ts diff --git a/packages/journey-client/src/lib/fr-webauthn/index.ts b/packages/journey-client/src/lib/journey-webauthn/index.ts similarity index 98% rename from packages/journey-client/src/lib/fr-webauthn/index.ts rename to packages/journey-client/src/lib/journey-webauthn/index.ts index a57a96a3c9..dffac4f881 100644 --- a/packages/journey-client/src/lib/fr-webauthn/index.ts +++ b/packages/journey-client/src/lib/journey-webauthn/index.ts @@ -50,17 +50,17 @@ type WebAuthnMetadata = WebAuthnAuthenticationMetadata | WebAuthnRegistrationMet * * ```js * // Determine if a step is a WebAuthn step - * const stepType = FRWebAuthn.getWebAuthnStepType(step); + * const stepType = JourneyWebAuthn.getWebAuthnStepType(step); * if (stepType === WebAuthnStepType.Registration) { * // Register a new device - * await FRWebAuthn.register(step); + * await JourneyWebAuthn.register(step); * } else if (stepType === WebAuthnStepType.Authentication) { * // Authenticate with a registered device - * await FRWebAuthn.authenticate(step); + * await JourneyWebAuthn.authenticate(step); * } * ``` */ -abstract class FRWebAuthn { +abstract class JourneyWebAuthn { /** * Determines if the given step is a WebAuthn step. * @@ -507,7 +507,7 @@ abstract class FRWebAuthn { } } -export default FRWebAuthn; +export default JourneyWebAuthn; export type { RelyingParty, WebAuthnAuthenticationMetadata, diff --git a/packages/journey-client/src/lib/fr-webauthn/interfaces.ts b/packages/journey-client/src/lib/journey-webauthn/interfaces.ts similarity index 100% rename from packages/journey-client/src/lib/fr-webauthn/interfaces.ts rename to packages/journey-client/src/lib/journey-webauthn/interfaces.ts diff --git a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts b/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts similarity index 98% rename from packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts rename to packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts index 996028d76e..3aedfc83b0 100644 --- a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts +++ b/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts @@ -19,7 +19,6 @@ export const webAuthnRegJSCallback653 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar publicKey = {\n challenge: new Int8Array([63, -71, 8, -32, 51, 11, 35, -85, -19, -93, -17, 9, -10, 104, 96, -5, -43, -94, 126, 123, 18, 44, -53, 27, 69, -59, -45, -113, 4, -120, -12, -17]).buffer,\n // Relying Party:\n rp: {\n \n name: "ForgeRock"\n },\n // User:\n user: {\n id: Uint8Array.from("sgsP5DNBy2TvEhwnWHu1BFRw2_LQepAdjkOfC1z6nxU", function (c) { return c.charCodeAt(0) }),\n name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629",\n displayName: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629"\n },\n pubKeyCredParams: [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257 } ],\n attestation: "none",\n timeout: 60000,\n excludeCredentials: [],\n authenticatorSelection: {"userVerification":"discouraged"}\n};\n\nnavigator.credentials.create({publicKey: publicKey})\n .then(function (newCredentialInfo) {\n var rawId = newCredentialInfo.id;\n var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON));\n var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString();\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + keyData + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', }, { name: 'messageType', value: '4' }, @@ -31,7 +30,6 @@ export const webAuthnRegJSCallback653 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', }, { name: 'messageType', value: '4' }, @@ -57,7 +55,6 @@ export const webAuthnAuthJSCallback653 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar acceptableCredentials = [\n { "type": "public-key", "id": new Int8Array([1, 97, 2, 123, -105, -19, -106, 10, -86, 82, -23, 5, 52, 63, 103, 110, -71, 53, 107, 104, 76, -42, -49, 96, 67, -114, -97, 19, -59, 89, -102, -115, -110, -101, -6, -98, 39, -75, 2, 74, 23, -105, 67, 6, -112, 21, -3, 36, -114, 52, 35, 75, 74, 82, -8, 115, -128, -34, -105, 110, 124, 41, -79, -53, -90, 81, -11, -7, 91, -45, -67, -82, 106, 74, 30, 112, 100, -47, 54, -12, 95, 81, 97, 36, 123, -15, -91, 87, -82, 87, -45, -103, -80, 109, -111, 82, 109, 58, 50, 19, -21, -102, 54, -108, -68, 12, -101, -53, -65, 11, -94, -36, 112, -101, -95, -90, -118, 68, 13, 8, -49, -77, -28, -82, -97, 126, -71, 33, -58, 19, 58, -118, 36, -28, 22, -55, 64, -72, -80, -9, -48, -50, 58, -52, 64, -64, -27, -5, -12, 110, -95, -17]).buffer }\n];\n\nvar options = {\n \n challenge: new Int8Array([109, 14, 35, -101, 97, -69, -105, -89, -58, 14, 108, 59, 45, 87, 109, -78, -51, 64, 90, 124, -97, 43, -84, -108, -69, -117, 101, -4, -36, -69, -106, 103]).buffer,\n timeout: 60000,\n userVerification: "discouraged",\n allowCredentials: acceptableCredentials\n};\n\nnavigator.credentials.get({ "publicKey" : options })\n .then(function (assertion) {\n var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON));\n var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString();\n var signature = new Int8Array(assertion.response.signature).toString();\n var rawId = assertion.id;\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', }, { name: 'messageType', value: '4' }, @@ -69,7 +66,6 @@ export const webAuthnAuthJSCallback653 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', }, { name: 'messageType', value: '4' }, @@ -95,7 +91,6 @@ export const webAuthnRegJSCallback70 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar publicKey = {\n challenge: new Int8Array([87, -95, 18, -17, -59, -3, -72, -9, -109, 77, -66, 67, 101, -59, -29, -92, -31, -58, 117, -14, 3, -123, 1, -54, -69, -122, 44, 111, 30, 49, 12, 81]).buffer,\n // Relying Party:\n rp: {\n id: "https://user.example.com:3002",\n name: "ForgeRock"\n },\n // User:\n user: {\n id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }),\n name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629",\n displayName: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629"\n },\n pubKeyCredParams: [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ],\n attestation: "none",\n timeout: 60000,\n excludeCredentials: [{ "type": "public-key", "id": new Int8Array([49, -96, -107, 113, 106, 5, 115, 22, 68, 121, -85, -27, 8, -58, -113, 127, -105, -37, -10, -12, -58, -25, 29, -82, -18, 69, -99, 125, 33, 82, 38, -66, -27, -128, -91, -86, 87, 68, 94, 0, -78, 70, -11, -70, -14, -53, 38, -60, 46, 27, 66, 46, 21, -125, -70, 123, -46, -124, 86, -2, 102, 70, -52, 54]).buffer },{ "type": "public-key", "id": new Int8Array([64, 17, -15, -123, -21, 127, 76, -120, 90, -112, -5, 54, 105, 93, 82, -104, -79, 107, -69, -3, -113, -94, -59, -4, 126, -33, 117, 32, -44, 122, -97, 8, -112, 105, -96, 96, 90, 44, -128, -121, 107, 79, -98, -68, -93, 11, -105, -47, 102, 13, 110, 84, 59, -91, -30, 37, -3, -22, 39, 111, -10, 87, -50, -35]).buffer }],\n authenticatorSelection: {"userVerification":"preferred"}\n};\n\nnavigator.credentials.create({publicKey: publicKey})\n .then(function (newCredentialInfo) {\n var rawId = newCredentialInfo.id;\n var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON));\n var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString();\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + keyData + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });', }, { name: 'messageType', value: '4' }, @@ -107,7 +102,6 @@ export const webAuthnRegJSCallback70 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', }, { name: 'messageType', value: '4' }, @@ -133,7 +127,6 @@ export const webAuthnAuthJSCallback70 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar options = {\n \n challenge: new Int8Array([-2, 85, 78, -80, -124, -6, -118, 15, 77, -30, -76, -27, -43, -19, -51, -68, 60, -80, -64, -102, 73, -103, 76, -77, -96, -28, 5, -23, -59, -36, 1, -1]).buffer,\n timeout: 60000,\n allowCredentials: [{ "type": "public-key", "id": new Int8Array([-107, 93, 68, -67, -5, 107, 18, 16, -25, -30, 80, 103, -75, -53, -2, -95, 102, 42, 47, 126, -1, 85, 93, 45, -85, 8, -108, 107, 47, -25, 66, 12, -96, 81, 104, -127, 26, -59, -69, -23, 75, 89, 58, 124, -93, 4, 28, -128, 121, 35, 39, 103, -86, -86, 123, -67, -7, -4, 79, -49, 127, -19, 7, 4]).buffer }]\n};\n\nnavigator.credentials.get({ "publicKey" : options })\n .then(function (assertion) {\n var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON));\n var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString();\n var signature = new Int8Array(assertion.response.signature).toString();\n var rawId = assertion.id;\n var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle));\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', }, { name: 'messageType', value: '4' }, @@ -145,7 +138,6 @@ export const webAuthnAuthJSCallback70 = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', }, { name: 'messageType', value: '4' }, @@ -171,7 +163,6 @@ export const webAuthnRegJSCallback70StoredUsername = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar publicKey = {\n challenge: new Int8Array([-90, -30, 14, -111, 43, -115, -125, 43, -96, 124, -109, -1, -100, -64, -52, -56, -15, -9, 41, 22, -111, -116, -65, -88, 108, -60, -58, 53, 62, -66, -34, 104]).buffer,\n // Relying Party:\n rp: {\n \n name: "ForgeRock"\n },\n // User:\n user: {\n id: Uint8Array.from("NTdhNWI0ZTQtNjk5OS00YjQ1LWJmODYtYTRmMmU1ZDRiNjI5", function (c) { return c.charCodeAt(0) }),\n name: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629",\n displayName: "57a5b4e4-6999-4b45-bf86-a4f2e5d4b629"\n },\n pubKeyCredParams: [ { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -7 } ],\n attestation: "none",\n timeout: 60000,\n excludeCredentials: [],\n authenticatorSelection: {"userVerification":"preferred","requireResidentKey":true}\n};\n\nnavigator.credentials.create({publicKey: publicKey})\n .then(function (newCredentialInfo) {\n var rawId = newCredentialInfo.id;\n var clientData = String.fromCharCode.apply(null, new Uint8Array(newCredentialInfo.response.clientDataJSON));\n var keyData = new Int8Array(newCredentialInfo.response.attestationObject).toString();\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + keyData + "::" + rawId;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });', }, { name: 'messageType', value: '4' }, @@ -183,7 +174,6 @@ export const webAuthnRegJSCallback70StoredUsername = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', }, { name: 'messageType', value: '4' }, @@ -251,7 +241,6 @@ export const webAuthnAuthMetaCallback70 = { challenge: 'J7kVW1EpFY3thLYVMAAJuR9dswysFhqgrBT6vvSuidE=', relyingPartyId: '', allowCredentials: - // eslint-disable-next-line 'allowCredentials: [{ "type": "public-key", "id": new Int8Array([1, 122, 110, -32, -105, -95, -90, 81, 20, -122, -96, -115, -115, 38, -7, 15, -127, 48, 48, 97, 94, -23, -54, 74, 3, -41, -118, -124, 112, 5, -77, 87, -11, 102, -86, 93, 27, 112, -128, 103, -58, -75, 68, -62, -62, 72, -27, 108, -59, 0, -124, -117, -121, -52, -97, -88, -112, 22, 122, 109, 104, -89, -10, 46, -95, 62, 64, 43, -42, 127, -53, -98, 88, -126, -68, -94, -5, 81, -71, -52, -54, -12, -55, 127, -125, 125, 53, -61, 61, 47, -66, -12, 25, 115, -24, -56, 95, 8, -20, -6, 4, 72, -45, -103, -52, 39, 123, 13, 9, -79, 99, -62, 84, -2, -41, 55, 125, 17, 126, -38, -80, -83, -28, 99, -26, -30, -18, 122, 92, -91, -128, -27, 4, 27, -39, 36, 117, 4, 120, 9, -24, -72, 84, 124, 25, 100, -40, 32, 63, -97, 119, 10, 73, 8, -46, 61, 56]).buffer },{ "type": "public-key", "id": new Int8Array([1, 15, 3, 3, 70, 54, 31, -27, -121, 121, 41, 83, -28, -49, 9, -113, -58, 117, -97, 18, 1, 100, -29, 6, -116, -93, 90, -91, 75, -120, -127, 50, 99, -37, -56, -41, 105, 42, 8, -87, -21, 37, -7, 96, -121, -125, -33, 79, 2, -10, 127, -117, 23, 46, 42, 29, 125, 91, 47, -101, 126, 44, 70, -84, -124, -94, -119, -87, 63, -116, 11, -28, 127, 76, -67, 36, -62, 62, -125, -82, 99, 71, 24, 32, -87, 93, 53, 97, -44, 18, -14, 77, 80, 77, 110, -80, -52, 18, 69, 127, 82, -27, -116, 42, -66, -53, -26, -29, 74, 75, 34, -88, -119, 118, -50, -70, -110, -68, -91, -15, 100, 113, 24, 13, -127, 39, -1, -85, 114, -125, 89, 89, -101, 94, -37, 82, -61, 15, -2, 3, -4, 9, 28, -75, -84, 96, 60, 85, -44, -98, -27, -29, 107, -115, -111, -3, -102]).buffer }]', timeout: '60000', }, @@ -461,7 +450,6 @@ export const webAuthnAuthJSCallback70StoredUsername = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018-2020 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n */\n\nif (!window.PublicKeyCredential) {\n document.getElementById(\'webAuthnOutcome\').value = "unsupported";\n document.getElementById("loginButton_0").click();\n}\n\nvar options = {\n \n challenge: new Int8Array([50, -11, 63, -112, 37, -61, 57, 126, 83, -127, 122, -42, -102, -82, 26, -95, -75, -37, 16, 52, 27, 54, -101, 124, -16, 99, 33, 92, 63, 10, -110, 102]).buffer,\n timeout: 60000,\n userVerification: "preferred",\n \n};\n\nnavigator.credentials.get({ "publicKey" : options })\n .then(function (assertion) {\n var clientData = String.fromCharCode.apply(null, new Uint8Array(assertion.response.clientDataJSON));\n var authenticatorData = new Int8Array(assertion.response.authenticatorData).toString();\n var signature = new Int8Array(assertion.response.signature).toString();\n var rawId = assertion.id;\n var userHandle = String.fromCharCode.apply(null, new Uint8Array(assertion.response.userHandle));\n document.getElementById(\'webAuthnOutcome\').value = clientData + "::" + authenticatorData + "::" + signature + "::" + rawId + "::" + userHandle;\n document.getElementById("loginButton_0").click();\n }).catch(function (err) {\n document.getElementById(\'webAuthnOutcome\').value = "ERROR" + "::" + err;\n document.getElementById("loginButton_0").click();\n });\n', }, { name: 'messageType', value: '4' }, @@ -473,7 +461,6 @@ export const webAuthnAuthJSCallback70StoredUsername = { { name: 'message', value: - // eslint-disable-next-line '/*\n * Copyright 2018 ForgeRock AS. All Rights Reserved\n *\n * Use of this code requires a commercial software license with ForgeRock AS.\n * or with one of its affiliates. All use shall be exclusively subject\n * to such license between the licensee and ForgeRock AS.\n *\n */\n\n/*\n * Note:\n *\n * When a ConfirmationCallback is used (e.g. during recovery code use), the XUI does not render a loginButton. However\n * the webAuthn script needs to call loginButton.click() to execute the appropriate data reformatting prior to sending\n * the request into AM. Here we query whether the loginButton exists and add it to the DOM if it doesn\'t.\n */\n\nvar newLocation = document.getElementById("wrapper");\n\nvar script = "
\\n" +\n "
\\n" +\n "
\\n" +\n "

Waiting for local device...

\\n" +\n "
\\n" +\n "
\\n";\n\nif (!document.getElementById("loginButton_0")) {\n script += "";\n} else {\n document.getElementById("loginButton_0").style.visibility=\'hidden\';\n}\n\nscript += "
";\n\nnewLocation.getElementsByTagName("fieldset")[0].innerHTML += script;\ndocument.body.appendChild(newLocation);', }, { name: 'messageType', value: '4' }, diff --git a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts b/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts similarity index 85% rename from packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts rename to packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts index bf5bd9936d..760adbcaae 100644 --- a/packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts +++ b/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts @@ -9,7 +9,7 @@ */ import { WebAuthnStepType } from './enums.js'; -import FRWebAuthn from './index.js'; +import JourneyWebAuthn from './index.js'; import { webAuthnRegJSCallback653, webAuthnAuthJSCallback653, @@ -21,20 +21,20 @@ import { webAuthnAuthJSCallback70StoredUsername, webAuthnRegMetaCallback70StoredUsername, webAuthnAuthMetaCallback70StoredUsername, -} from './fr-webauthn.mock.data.js'; +} from './journey-webauthn.mock.data.js'; import JourneyStep from '../journey-step.js'; describe('Test FRWebAuthn class with 6.5.3 "Passwordless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnRegJSCallback653 as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnAuthJSCallback653 as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); // it('should return Registration type with register metadata callbacks', () => { @@ -55,26 +55,26 @@ describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnRegJSCallback70 as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnAuthJSCallback70 as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); console.log('the step type', stepType, WebAuthnStepType.Authentication); expect(stepType).toBe(WebAuthnStepType.Authentication); }); it('should return Registration type with register metadata callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnRegMetaCallback70 as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate metadata callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnAuthMetaCallback70 as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); }); @@ -83,25 +83,25 @@ describe('Test FRWebAuthn class with 7.0 "Usernameless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnRegJSCallback70StoredUsername as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnAuthJSCallback70StoredUsername as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); it('should return Registration type with register metadata callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnRegMetaCallback70StoredUsername as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate metadata callbacks', () => { // eslint-disable-next-line const step = new JourneyStep(webAuthnAuthMetaCallback70StoredUsername as any); - const stepType = FRWebAuthn.getWebAuthnStepType(step); + const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); }); diff --git a/packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts b/packages/journey-client/src/lib/journey-webauthn/script-parser.test.ts similarity index 100% rename from packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts rename to packages/journey-client/src/lib/journey-webauthn/script-parser.test.ts diff --git a/packages/journey-client/src/lib/fr-webauthn/script-parser.ts b/packages/journey-client/src/lib/journey-webauthn/script-parser.ts similarity index 100% rename from packages/journey-client/src/lib/fr-webauthn/script-parser.ts rename to packages/journey-client/src/lib/journey-webauthn/script-parser.ts diff --git a/packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts b/packages/journey-client/src/lib/journey-webauthn/script-text.mock.data.ts similarity index 99% rename from packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts rename to packages/journey-client/src/lib/journey-webauthn/script-text.mock.data.ts index 9a22c381fe..462779ba7a 100644 --- a/packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts +++ b/packages/journey-client/src/lib/journey-webauthn/script-text.mock.data.ts @@ -8,7 +8,6 @@ * of the MIT license. See the LICENSE file for details. */ -/* eslint-disable max-len */ export const authenticateInputWithRpidAndAllowCredentials = `/* * Copyright 2018-2020 ForgeRock AS. All Rights Reserved * From 01a5d40c1f66906d6f002454484863a00a1dd301 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 18 Sep 2025 16:00:10 -0600 Subject: [PATCH 06/11] revert(journey-client): revert header changes --- .../src/lib/callbacks/device-profile-callback.ts | 2 +- .../src/lib/callbacks/validated-create-username-callback.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts index 6f13160cd2..17ec440d6a 100644 --- a/packages/journey-client/src/lib/callbacks/device-profile-callback.ts +++ b/packages/journey-client/src/lib/callbacks/device-profile-callback.ts @@ -6,7 +6,7 @@ import JourneyCallback from './index.js'; import type { Callback } from '@forgerock/sdk-types'; -import type { DeviceProfileData } from '../fr-device/interfaces.js'; +import type { DeviceProfileData } from '../journey-device/interfaces.js'; /** * Represents a callback used to collect device profile data. diff --git a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts index 805606ab05..f7f0d32dd6 100644 --- a/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts +++ b/packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts @@ -1,5 +1,6 @@ /* - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ From acb3df72a54db7835027c71773029bd73e258b94 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 18 Sep 2025 16:25:07 -0600 Subject: [PATCH 07/11] refactor(journey-client): rename fr-device, fr-policy, and fr-recovery-codes modules --- eslint.config.mjs | 3 + .../journey-client/src/lib/fr-device/index.ts | 276 ------------------ .../src/lib/journey-client.test.ts | 2 +- .../{fr-device => journey-device}/defaults.ts | 0 .../device-profile.mock.data.ts | 0 .../device-profile.test.ts | 51 +++- .../src/lib/journey-device/index.ts | 275 +++++++++++++++++ .../interfaces.ts | 0 .../sample-profile.json | 0 .../src/lib/journey-login-failure.ts | 6 +- .../{fr-policy => journey-policy}/index.ts | 4 +- .../interfaces.ts | 0 .../journey-policy.test.ts} | 16 +- .../message-creator.ts | 0 .../index.ts | 0 .../recovery-codes.test.ts | 0 .../script-parser.test.ts | 0 .../script-parser.ts | 0 .../script-text.mock.data.ts | 0 .../oidc/src/lib/state-pkce.effects.ts | 2 +- .../sdk-types/src/lib/legacy-config.types.ts | 4 +- 21 files changed, 341 insertions(+), 298 deletions(-) delete mode 100644 packages/journey-client/src/lib/fr-device/index.ts rename packages/journey-client/src/lib/{fr-device => journey-device}/defaults.ts (100%) rename packages/journey-client/src/lib/{fr-device => journey-device}/device-profile.mock.data.ts (100%) rename packages/journey-client/src/lib/{fr-device => journey-device}/device-profile.test.ts (65%) create mode 100644 packages/journey-client/src/lib/journey-device/index.ts rename packages/journey-client/src/lib/{fr-device => journey-device}/interfaces.ts (100%) rename packages/journey-client/src/lib/{fr-device => journey-device}/sample-profile.json (100%) rename packages/journey-client/src/lib/{fr-policy => journey-policy}/index.ts (98%) rename packages/journey-client/src/lib/{fr-policy => journey-policy}/interfaces.ts (100%) rename packages/journey-client/src/lib/{fr-policy/fr-policy.test.ts => journey-policy/journey-policy.test.ts} (89%) rename packages/journey-client/src/lib/{fr-policy => journey-policy}/message-creator.ts (100%) rename packages/journey-client/src/lib/{fr-recovery-codes => recovery-codes}/index.ts (100%) rename packages/journey-client/src/lib/{fr-recovery-codes => recovery-codes}/recovery-codes.test.ts (100%) rename packages/journey-client/src/lib/{fr-recovery-codes => recovery-codes}/script-parser.test.ts (100%) rename packages/journey-client/src/lib/{fr-recovery-codes => recovery-codes}/script-parser.ts (100%) rename packages/journey-client/src/lib/{fr-recovery-codes => recovery-codes}/script-text.mock.data.ts (100%) diff --git a/eslint.config.mjs b/eslint.config.mjs index 75854c1423..b38bd77ab9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,6 +24,9 @@ export default [ ignores: [ '**/dist', '**/docs', + '**/node_modules', + '**/coverage', + '**/tmp', '**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*', '**/out-tsc', diff --git a/packages/journey-client/src/lib/fr-device/index.ts b/packages/journey-client/src/lib/fr-device/index.ts deleted file mode 100644 index 98471e9913..0000000000 --- a/packages/journey-client/src/lib/fr-device/index.ts +++ /dev/null @@ -1,276 +0,0 @@ -/* - * @forgerock/javascript-sdk - * - * index.ts - * - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import { - browserProps, - configurableCategories, - delay, - devicePlatforms, - displayProps, - fontNames, - hardwareProps, - platformProps, -} from './defaults.js'; -import type { - BaseProfileConfig, - Category, - CollectParameters, - DeviceProfileData, - Geolocation, - ProfileConfigOptions, -} from './interfaces.js'; -import { reduceToObject, reduceToString } from '@forgerock/sdk-utilities'; -import { logger } from '@forgerock/sdk-logger'; - -const FRLogger = logger({ level: 'info' }); - -/** - * @class FRDevice - Collects user device metadata - * - * Example: - * - * ```js - * // Instantiate new device object (w/optional config, if needed) - * const device = new forgerock.FRDevice( - * // optional configuration - * ); - * // override any instance methods, if needed - * // e.g.: device.getDisplayMeta = () => {}; - * - * // Call getProfile with required argument obj of boolean properties - * // of location and metadata - * const profile = await device.getProfile({ - * location: isLocationRequired, - * metadata: isMetadataRequired, - * }); - * ``` - */ -class FRDevice { - config: BaseProfileConfig = { - fontNames, - devicePlatforms, - displayProps, - browserProps, - hardwareProps, - platformProps, - }; - - private prefix: string; - - constructor(config?: ProfileConfigOptions, prefix = 'forgerock') { - this.prefix = prefix; - if (config) { - Object.keys(config).forEach((key: string) => { - if (!configurableCategories.includes(key)) { - throw new Error('Device profile configuration category does not exist.'); - } - this.config[key as Category] = config[key as Category]; - }); - } - } - - getBrowserMeta(): Record { - if (typeof navigator === 'undefined') { - FRLogger.warn('Cannot collect browser metadata. navigator is not defined.'); - return {}; - } - return reduceToObject( - this.config.browserProps, - navigator as unknown as Record, - ); - } - - getBrowserPluginsNames(): string { - if (!(typeof navigator !== 'undefined' && navigator.plugins)) { - FRLogger.warn('Cannot collect browser plugin information. navigator.plugins is not defined.'); - return ''; - } - return reduceToString( - Object.keys(navigator.plugins), - navigator.plugins as unknown as Record, - ); - } - - getDeviceName(): string { - if (typeof navigator === 'undefined') { - FRLogger.warn('Cannot collect device name. navigator is not defined.'); - return ''; - } - const userAgent = navigator.userAgent; - const platform = navigator.platform; - - switch (true) { - case this.config.devicePlatforms.mac.includes(platform): - return 'Mac (Browser)'; - case this.config.devicePlatforms.ios.includes(platform): - return `${platform} (Browser)`; - case this.config.devicePlatforms.windows.includes(platform): - return 'Windows (Browser)'; - case /Android/.test(platform) || /Android/.test(userAgent): - return 'Android (Browser)'; - case /CrOS/.test(userAgent) || /Chromebook/.test(userAgent): - return 'Chrome OS (Browser)'; - case /Linux/.test(platform): - return 'Linux (Browser)'; - default: - return `${platform || 'Unknown'} (Browser)`; - } - } - - getDisplayMeta(): { [key: string]: string | number | null } { - if (typeof screen === 'undefined') { - FRLogger.warn('Cannot collect screen information. screen is not defined.'); - return {}; - } - return reduceToObject(this.config.displayProps, screen as unknown as Record); - } - - getHardwareMeta(): Record { - if (typeof navigator === 'undefined') { - FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.'); - return {}; - } - return reduceToObject( - this.config.hardwareProps, - navigator as unknown as Record, - ); - } - - getIdentifier(): string { - const storageKey = `${this.prefix}-DeviceID`; - - if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { - FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.'); - return ''; - } - if (!localStorage) { - FRLogger.warn('Cannot store profile ID. localStorage is not supported.'); - return ''; - } - let id = localStorage.getItem(storageKey); - if (!id) { - // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" - id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); - localStorage.setItem(storageKey, id); - } - return id; - } - - getInstalledFonts(): string { - if (typeof document === 'undefined') { - FRLogger.warn('Cannot collect font data. Global document object is undefined.'); - return ''; - } - const canvas = document.createElement('canvas'); - if (!canvas) { - FRLogger.warn('Cannot collect font data. Browser does not support canvas element'); - return ''; - } - const context = canvas.getContext && canvas.getContext('2d'); - - if (!context) { - FRLogger.warn('Cannot collect font data. Browser does not support 2d canvas context'); - return ''; - } - const text = 'abcdefghi0123456789'; - context.font = '72px Comic Sans'; - const baseWidth = context.measureText(text).width; - - const installedFonts = this.config.fontNames.reduce((prev: string, curr: string) => { - context.font = `72px ${curr}, Comic Sans`; - const newWidth = context.measureText(text).width; - - if (newWidth !== baseWidth) { - prev = `${prev}${curr};`; - } - return prev; - }, ''); - - return installedFonts; - } - - async getLocationCoordinates(): Promise> { - if (!(typeof navigator !== 'undefined' && navigator.geolocation)) { - FRLogger.warn( - 'Cannot collect geolocation information. navigator.geolocation is not defined.', - ); - return Promise.resolve({}); - } - return new Promise((resolve) => { - navigator.geolocation.getCurrentPosition( - (position) => - resolve({ - latitude: position.coords.latitude, - longitude: position.coords.longitude, - }), - () => { - FRLogger.warn('Cannot collect geolocation information. Geolocation API error.'); - resolve({}); - }, - { - enableHighAccuracy: true, - timeout: delay, - maximumAge: 0, - }, - ); - }); - } - - getOSMeta(): Record { - if (typeof navigator === 'undefined') { - FRLogger.warn('Cannot collect OS metadata. navigator is not defined.'); - return {}; - } - return reduceToObject( - this.config.platformProps, - navigator as unknown as Record, - ); - } - - async getProfile({ location, metadata }: CollectParameters): Promise { - const profile: DeviceProfileData = { - identifier: this.getIdentifier(), - }; - - if (metadata) { - profile.metadata = { - hardware: { - ...this.getHardwareMeta(), - display: this.getDisplayMeta(), - }, - browser: { - ...this.getBrowserMeta(), - plugins: this.getBrowserPluginsNames(), - }, - platform: { - ...this.getOSMeta(), - deviceName: this.getDeviceName(), - fonts: this.getInstalledFonts(), - timezone: this.getTimezoneOffset(), - }, - }; - } - if (location) { - profile.location = await this.getLocationCoordinates(); - } - return profile; - } - - getTimezoneOffset(): number | null { - try { - return new Date().getTimezoneOffset(); - } catch { - FRLogger.warn('Cannot collect timezone information. getTimezoneOffset is not defined.'); - return null; - } - } -} - -export default FRDevice; diff --git a/packages/journey-client/src/lib/journey-client.test.ts b/packages/journey-client/src/lib/journey-client.test.ts index 228e7d998d..f96c44596d 100644 --- a/packages/journey-client/src/lib/journey-client.test.ts +++ b/packages/journey-client/src/lib/journey-client.test.ts @@ -23,7 +23,7 @@ vi.mock('@forgerock/storage', () => ({ createStorage: vi.fn(() => mockStorageInstance), })); -vi.mock('./fr-device/index.js', () => ({ +vi.mock('./journey-device/index.js', () => ({ default: vi.fn(() => ({ getProfile: vi.fn().mockResolvedValue({}), })), diff --git a/packages/journey-client/src/lib/fr-device/defaults.ts b/packages/journey-client/src/lib/journey-device/defaults.ts similarity index 100% rename from packages/journey-client/src/lib/fr-device/defaults.ts rename to packages/journey-client/src/lib/journey-device/defaults.ts diff --git a/packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts b/packages/journey-client/src/lib/journey-device/device-profile.mock.data.ts similarity index 100% rename from packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts rename to packages/journey-client/src/lib/journey-device/device-profile.mock.data.ts diff --git a/packages/journey-client/src/lib/fr-device/device-profile.test.ts b/packages/journey-client/src/lib/journey-device/device-profile.test.ts similarity index 65% rename from packages/journey-client/src/lib/fr-device/device-profile.test.ts rename to packages/journey-client/src/lib/journey-device/device-profile.test.ts index 17c08fffab..f7d7b7198f 100644 --- a/packages/journey-client/src/lib/fr-device/device-profile.test.ts +++ b/packages/journey-client/src/lib/journey-device/device-profile.test.ts @@ -7,8 +7,8 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import { vi, expect, describe, it } from 'vitest'; -import FRDevice from './index.js'; +import { vi, expect, describe, it, afterEach, beforeEach, SpyInstance } from 'vitest'; +import { journeyDevice } from './index.js'; Object.defineProperty(window, 'crypto', { writable: true, @@ -19,7 +19,7 @@ Object.defineProperty(window, 'crypto', { describe('Test DeviceProfile', () => { it('should return basic metadata', async () => { - const device = new FRDevice(); + const device = journeyDevice(); const profile = await device.getProfile({ location: false, metadata: true, @@ -41,7 +41,7 @@ describe('Test DeviceProfile', () => { }); it('should return metadata without any display props', async () => { - const device = new FRDevice({ displayProps: [] }); + const device = journeyDevice({ displayProps: [] }); const profile = await device.getProfile({ location: false, metadata: true, @@ -57,7 +57,7 @@ describe('Test DeviceProfile', () => { }); it('should return metadata according to narrowed browser props', async () => { - const device = new FRDevice({ browserProps: ['userAgent'] }); + const device = journeyDevice({ browserProps: ['userAgent'] }); const profile = await device.getProfile({ location: false, metadata: true, @@ -77,4 +77,45 @@ describe('Test DeviceProfile', () => { expect(display).toHaveProperty('height'); expect(deviceName.length).toBeGreaterThan(1); }); + + describe('logLevel tests', () => { + let warnSpy: SpyInstance; + const originalNavigator = global.navigator; + + beforeEach(() => { + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + delete global.navigator; + }); + + afterEach(() => { + warnSpy.mockRestore(); + global.navigator = originalNavigator; + }); + + it('should not log warnings if logLevel is "error"', () => { + const device = journeyDevice(undefined, 'error'); + device.getBrowserMeta(); + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('should log warnings if logLevel is "warn"', () => { + const device = journeyDevice(undefined, 'warn'); + device.getBrowserMeta(); + expect(warnSpy).toHaveBeenCalledWith( + 'Cannot collect browser metadata. navigator is not defined.', + ); + }); + }); + + it('should use custom prefix for device identifier', () => { + const setItemSpy = vi.spyOn(Storage.prototype, 'setItem'); + const device = journeyDevice(undefined, 'info', 'my-custom-prefix'); + device.getIdentifier(); + + expect(setItemSpy).toHaveBeenCalledWith('my-custom-prefix-DeviceID', expect.any(String)); + + setItemSpy.mockRestore(); + }); }); diff --git a/packages/journey-client/src/lib/journey-device/index.ts b/packages/journey-client/src/lib/journey-device/index.ts new file mode 100644 index 0000000000..cca37ba959 --- /dev/null +++ b/packages/journey-client/src/lib/journey-device/index.ts @@ -0,0 +1,275 @@ +/* + * @forgerock/javascript-sdk + * + * index.ts + * + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { + browserProps, + configurableCategories, + delay, + devicePlatforms, + displayProps, + fontNames, + hardwareProps, + platformProps, +} from './defaults.js'; +import type { + BaseProfileConfig, + Category, + CollectParameters, + DeviceProfileData, + Geolocation, + ProfileConfigOptions, +} from './interfaces.js'; +import { reduceToObject, reduceToString } from '@forgerock/sdk-utilities'; +import { logger as loggerFn } from '@forgerock/sdk-logger'; +import type { LogLevel } from '@forgerock/sdk-logger'; + +/** + * @function journeyDevice - Collects user device metadata. + * + * Example: + * + * ```js + * // Instantiate new device object (w/optional config, if needed) + * const device = forgerock.journeyDevice({ + * // optional configuration + * }); + * + * // Call getProfile with required argument obj of boolean properties + * // of location and metadata + * const profile = await device.getProfile({ + * location: isLocationRequired, + * metadata: isMetadataRequired, + * }); + * ``` + */ +export function journeyDevice( + configOptions?: ProfileConfigOptions, + logLevel: LogLevel = 'info', + prefix = 'forgerock', +) { + const JourneyLogger = loggerFn({ level: logLevel }); + + const config: BaseProfileConfig = { + fontNames, + devicePlatforms, + displayProps, + browserProps, + hardwareProps, + platformProps, + }; + + if (configOptions) { + Object.keys(configOptions).forEach((key: string) => { + if (!configurableCategories.includes(key)) { + throw new Error('Device profile configuration category does not exist.'); + } + config[key as Category] = configOptions[key as Category]; + }); + } + + const device = { + getBrowserMeta: (): Record => { + if (typeof navigator === 'undefined') { + JourneyLogger.warn('Cannot collect browser metadata. navigator is not defined.'); + return {}; + } + return reduceToObject(config.browserProps, navigator as unknown as Record); + }, + + getBrowserPluginsNames: (): string => { + if (!(typeof navigator !== 'undefined' && navigator.plugins)) { + JourneyLogger.warn( + 'Cannot collect browser plugin information. navigator.plugins is not defined.', + ); + return ''; + } + return reduceToString( + Object.keys(navigator.plugins), + navigator.plugins as unknown as Record, + ); + }, + + getDeviceName: (): string => { + if (typeof navigator === 'undefined') { + JourneyLogger.warn('Cannot collect device name. navigator is not defined.'); + return ''; + } + const userAgent = navigator.userAgent; + const platform = navigator.platform; + + switch (true) { + case config.devicePlatforms.mac.includes(platform): + return 'Mac (Browser)'; + case config.devicePlatforms.ios.includes(platform): + return `${platform} (Browser)`; + case config.devicePlatforms.windows.includes(platform): + return 'Windows (Browser)'; + case /Android/.test(platform) || /Android/.test(userAgent): + return 'Android (Browser)'; + case /CrOS/.test(userAgent) || /Chromebook/.test(userAgent): + return 'Chrome OS (Browser)'; + case /Linux/.test(platform): + return 'Linux (Browser)'; + default: + return `${platform || 'Unknown'} (Browser)`; + } + }, + + getDisplayMeta: (): { [key: string]: string | number | null } => { + if (typeof screen === 'undefined') { + JourneyLogger.warn('Cannot collect screen information. screen is not defined.'); + return {}; + } + return reduceToObject(config.displayProps, screen as unknown as Record); + }, + + getHardwareMeta: (): Record => { + if (typeof navigator === 'undefined') { + JourneyLogger.warn('Cannot collect OS metadata. Navigator is not defined.'); + return {}; + } + return reduceToObject(config.hardwareProps, navigator as unknown as Record); + }, + + getIdentifier: (): string => { + const storageKey = `${prefix}-DeviceID`; + + if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { + JourneyLogger.warn( + 'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.', + ); + return ''; + } + if (!localStorage) { + JourneyLogger.warn('Cannot store profile ID. localStorage is not supported.'); + return ''; + } + let id = localStorage.getItem(storageKey); + if (!id) { + // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" + id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); + localStorage.setItem(storageKey, id); + } + return id; + }, + + getInstalledFonts: (): string => { + if (typeof document === 'undefined') { + JourneyLogger.warn('Cannot collect font data. Global document object is undefined.'); + return ''; + } + const canvas = document.createElement('canvas'); + if (!canvas) { + JourneyLogger.warn('Cannot collect font data. Browser does not support canvas element'); + return ''; + } + const context = canvas.getContext && canvas.getContext('2d'); + + if (!context) { + JourneyLogger.warn('Cannot collect font data. Browser does not support 2d canvas context'); + return ''; + } + const text = 'abcdefghi0123456789'; + context.font = '72px Comic Sans'; + const baseWidth = context.measureText(text).width; + + const installedFonts = config.fontNames.reduce((prev: string, curr: string) => { + context.font = `72px ${curr}, Comic Sans`; + const newWidth = context.measureText(text).width; + + if (newWidth !== baseWidth) { + prev = `${prev}${curr};`; + } + return prev; + }, ''); + + return installedFonts; + }, + + getLocationCoordinates: async (): Promise> => { + if (!(typeof navigator !== 'undefined' && navigator.geolocation)) { + JourneyLogger.warn( + 'Cannot collect geolocation information. navigator.geolocation is not defined.', + ); + return Promise.resolve({}); + } + return new Promise((resolve) => { + navigator.geolocation.getCurrentPosition( + (position) => + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }), + () => { + JourneyLogger.warn('Cannot collect geolocation information. Geolocation API error.'); + resolve({}); + }, + { + enableHighAccuracy: true, + timeout: delay, + maximumAge: 0, + }, + ); + }); + }, + + getOSMeta: (): Record => { + if (typeof navigator === 'undefined') { + JourneyLogger.warn('Cannot collect OS metadata. navigator is not defined.'); + return {}; + } + return reduceToObject(config.platformProps, navigator as unknown as Record); + }, + + getTimezoneOffset: (): number | null => { + try { + return new Date().getTimezoneOffset(); + } catch { + JourneyLogger.warn( + 'Cannot collect timezone information. getTimezoneOffset is not defined.', + ); + return null; + } + }, + + getProfile: async function ({ + location, + metadata, + }: CollectParameters): Promise { + const profile: DeviceProfileData = { + identifier: this.getIdentifier(), + }; + + if (metadata) { + profile.metadata = { + hardware: { + ...this.getHardwareMeta(), + display: this.getDisplayMeta(), + }, + browser: { + ...this.getBrowserMeta(), + plugins: this.getBrowserPluginsNames(), + }, + platform: { + ...this.getOSMeta(), + deviceName: this.getDeviceName(), + fonts: this.getInstalledFonts(), + timezone: this.getTimezoneOffset(), + }, + }; + } + if (location) { + profile.location = await this.getLocationCoordinates(); + } + return profile; + }, + }; + return device; +} diff --git a/packages/journey-client/src/lib/fr-device/interfaces.ts b/packages/journey-client/src/lib/journey-device/interfaces.ts similarity index 100% rename from packages/journey-client/src/lib/fr-device/interfaces.ts rename to packages/journey-client/src/lib/journey-device/interfaces.ts diff --git a/packages/journey-client/src/lib/fr-device/sample-profile.json b/packages/journey-client/src/lib/journey-device/sample-profile.json similarity index 100% rename from packages/journey-client/src/lib/fr-device/sample-profile.json rename to packages/journey-client/src/lib/journey-device/sample-profile.json diff --git a/packages/journey-client/src/lib/journey-login-failure.ts b/packages/journey-client/src/lib/journey-login-failure.ts index af7b70358a..c5d6cc55bb 100644 --- a/packages/journey-client/src/lib/journey-login-failure.ts +++ b/packages/journey-client/src/lib/journey-login-failure.ts @@ -6,8 +6,8 @@ import type { Step, AuthResponse, FailureDetail } from '@forgerock/sdk-types'; import { StepType } from '@forgerock/sdk-types'; -import FRPolicy from './fr-policy/index.js'; -import type { MessageCreator, ProcessedPropertyError } from './fr-policy/interfaces.js'; +import JourneyPolicy from './journey-policy/index.js'; +import type { MessageCreator, ProcessedPropertyError } from './journey-policy/interfaces.js'; class JourneyLoginFailure implements AuthResponse { /** @@ -45,7 +45,7 @@ class JourneyLoginFailure implements AuthResponse { * Gets processed failure message. */ public getProcessedMessage(messageCreator?: MessageCreator): ProcessedPropertyError[] { - return FRPolicy.parseErrors(this.payload, messageCreator); + return JourneyPolicy.parseErrors(this.payload, messageCreator); } /** diff --git a/packages/journey-client/src/lib/fr-policy/index.ts b/packages/journey-client/src/lib/journey-policy/index.ts similarity index 98% rename from packages/journey-client/src/lib/fr-policy/index.ts rename to packages/journey-client/src/lib/journey-policy/index.ts index b84fa614ae..783f20e58d 100644 --- a/packages/journey-client/src/lib/fr-policy/index.ts +++ b/packages/journey-client/src/lib/journey-policy/index.ts @@ -32,7 +32,7 @@ import defaultMessageCreator from './message-creator.js'; * const messagesClassMethod = FRPolicy.parseErrors(thisStep, messageCreator) * } */ -abstract class FRPolicy { +abstract class JourneyPolicy { /** * Parses policy errors and generates human readable error messages. * @@ -115,5 +115,5 @@ abstract class FRPolicy { } } -export default FRPolicy; +export default JourneyPolicy; export type { MessageCreator, ProcessedPropertyError }; diff --git a/packages/journey-client/src/lib/fr-policy/interfaces.ts b/packages/journey-client/src/lib/journey-policy/interfaces.ts similarity index 100% rename from packages/journey-client/src/lib/fr-policy/interfaces.ts rename to packages/journey-client/src/lib/journey-policy/interfaces.ts diff --git a/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts b/packages/journey-client/src/lib/journey-policy/journey-policy.test.ts similarity index 89% rename from packages/journey-client/src/lib/fr-policy/fr-policy.test.ts rename to packages/journey-client/src/lib/journey-policy/journey-policy.test.ts index 3b0f82b3f0..a7eecaf670 100644 --- a/packages/journey-client/src/lib/fr-policy/fr-policy.test.ts +++ b/packages/journey-client/src/lib/journey-policy/journey-policy.test.ts @@ -8,7 +8,7 @@ * of the MIT license. See the LICENSE file for details. */ -import FRPolicy, { MessageCreator } from './index.js'; +import JourneyPolicy, { MessageCreator } from './index.js'; import { PolicyKey } from '@forgerock/sdk-types'; describe('The IDM error handling', () => { @@ -21,7 +21,7 @@ describe('The IDM error handling', () => { policyRequirement: 'UNIQUE', }, }; - const message = FRPolicy.parsePolicyRequirement(property, test.policy); + const message = JourneyPolicy.parsePolicyRequirement(property, test.policy); expect(message).toBe(test.expectedString); }); @@ -35,7 +35,7 @@ describe('The IDM error handling', () => { policyRequirement: 'MIN_LENGTH', }, }; - const message = FRPolicy.parsePolicyRequirement(property, test.policy); + const message = JourneyPolicy.parsePolicyRequirement(property, test.policy); expect(message).toBe(test.expectedString); }); @@ -49,7 +49,7 @@ describe('The IDM error handling', () => { policyRequirement: 'SOME_UNKNOWN_POLICY', }, }; - const message = FRPolicy.parsePolicyRequirement(property, test.policy); + const message = JourneyPolicy.parsePolicyRequirement(property, test.policy); expect(message).toBe(test.expectedString); }); @@ -64,7 +64,7 @@ describe('The IDM error handling', () => { policyRequirement: 'CUSTOM_POLICY', }, }; - const message = FRPolicy.parsePolicyRequirement( + const message = JourneyPolicy.parsePolicyRequirement( property, test.policy, test.customMessage as MessageCreator, @@ -83,7 +83,7 @@ describe('The IDM error handling', () => { policyRequirement: 'UNIQUE', }, }; - const message = FRPolicy.parsePolicyRequirement(property, test.policy, test.customMessage); + const message = JourneyPolicy.parsePolicyRequirement(property, test.policy, test.customMessage); expect(message).toBe(test.expectedString); }); @@ -103,7 +103,7 @@ describe('The IDM error handling', () => { property: 'userName', }; - const messageArray = FRPolicy.parseFailedPolicyRequirement(policy); + const messageArray = JourneyPolicy.parseFailedPolicyRequirement(policy); expect(messageArray).toEqual([ 'userName must be unique', 'userName must be at least 6 characters', @@ -195,7 +195,7 @@ describe('The IDM error handling', () => { }, ]; - const errorObjArr = FRPolicy.parseErrors(errorResponse, customMessage); + const errorObjArr = JourneyPolicy.parseErrors(errorResponse, customMessage); expect(errorObjArr).toEqual(expected); }); }); diff --git a/packages/journey-client/src/lib/fr-policy/message-creator.ts b/packages/journey-client/src/lib/journey-policy/message-creator.ts similarity index 100% rename from packages/journey-client/src/lib/fr-policy/message-creator.ts rename to packages/journey-client/src/lib/journey-policy/message-creator.ts diff --git a/packages/journey-client/src/lib/fr-recovery-codes/index.ts b/packages/journey-client/src/lib/recovery-codes/index.ts similarity index 100% rename from packages/journey-client/src/lib/fr-recovery-codes/index.ts rename to packages/journey-client/src/lib/recovery-codes/index.ts diff --git a/packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts b/packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts similarity index 100% rename from packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts rename to packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts diff --git a/packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts b/packages/journey-client/src/lib/recovery-codes/script-parser.test.ts similarity index 100% rename from packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts rename to packages/journey-client/src/lib/recovery-codes/script-parser.test.ts diff --git a/packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts b/packages/journey-client/src/lib/recovery-codes/script-parser.ts similarity index 100% rename from packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts rename to packages/journey-client/src/lib/recovery-codes/script-parser.ts diff --git a/packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts b/packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts similarity index 100% rename from packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts rename to packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts diff --git a/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts b/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts index d29657c4f4..23d3e906f2 100644 --- a/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts +++ b/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts @@ -10,7 +10,7 @@ import { createVerifier, createState } from '@forgerock/sdk-utilities'; import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; function getStorageKey(clientId: string, prefix?: string) { - return `${prefix || 'FR-SDK'}-authflow-${clientId}`; + return `${prefix || 'Journey-SDK'}-authflow-${clientId}`; } /** diff --git a/packages/sdk-types/src/lib/legacy-config.types.ts b/packages/sdk-types/src/lib/legacy-config.types.ts index ccbd32f872..a6230ae33a 100644 --- a/packages/sdk-types/src/lib/legacy-config.types.ts +++ b/packages/sdk-types/src/lib/legacy-config.types.ts @@ -86,10 +86,10 @@ export interface WellKnownResponse { code_challenge_methods_supported?: string[]; } -type FRCallbackFactory = (callback: Callback) => any; +type JourneyCallbackFactory = (callback: Callback) => any; export interface LegacyConfigOptions { - callbackFactory?: FRCallbackFactory; + callbackFactory?: JourneyCallbackFactory; clientId?: string; middleware?: LegacyRequestMiddleware[]; realmPath?: string; From 0f3a34be924d35f5dec1a860ab905c19237ae8ed Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 23 Sep 2025 05:41:18 -0600 Subject: [PATCH 08/11] refactor(journey-client): refactor journey-step and related modules to functional API --- .../src/lib/journey-client.test.ts | 30 +++-- .../journey-client/src/lib/journey-client.ts | 19 ++- .../src/lib/journey-login-failure.ts | 59 --------- .../lib/journey-login-failure.utils.test.ts | 30 +++++ .../src/lib/journey-login-failure.utils.ts | 56 ++++++++ .../src/lib/journey-login-success.ts | 43 ------- .../lib/journey-login-success.utils.test.ts | 28 ++++ .../src/lib/journey-login-success.utils.ts | 44 +++++++ .../lib/journey-qrcode/journey-qrcode.test.ts | 12 +- .../src/lib/journey-qrcode/journey-qrcode.ts | 2 +- .../src/lib/journey-step.test.ts | 22 ++-- .../journey-client/src/lib/journey-step.ts | 121 ------------------ .../src/lib/journey-step.utils.ts | 104 +++++++++++++++ .../src/lib/journey-webauthn/index.ts | 2 +- .../journey-webauthn/journey-webauthn.test.ts | 39 +++--- .../journey-client/src/lib/journey.utils.ts | 37 ++++++ .../src/lib/recovery-codes/index.ts | 2 +- .../lib/recovery-codes/recovery-codes.test.ts | 10 +- 18 files changed, 369 insertions(+), 291 deletions(-) delete mode 100644 packages/journey-client/src/lib/journey-login-failure.ts create mode 100644 packages/journey-client/src/lib/journey-login-failure.utils.test.ts create mode 100644 packages/journey-client/src/lib/journey-login-failure.utils.ts delete mode 100644 packages/journey-client/src/lib/journey-login-success.ts create mode 100644 packages/journey-client/src/lib/journey-login-success.utils.test.ts create mode 100644 packages/journey-client/src/lib/journey-login-success.utils.ts delete mode 100644 packages/journey-client/src/lib/journey-step.ts create mode 100644 packages/journey-client/src/lib/journey-step.utils.ts create mode 100644 packages/journey-client/src/lib/journey.utils.ts diff --git a/packages/journey-client/src/lib/journey-client.test.ts b/packages/journey-client/src/lib/journey-client.test.ts index f96c44596d..cc77cc7496 100644 --- a/packages/journey-client/src/lib/journey-client.test.ts +++ b/packages/journey-client/src/lib/journey-client.test.ts @@ -7,7 +7,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { journey } from './journey-client.js'; -import JourneyStep from './journey-step.js'; +import { createJourneyStep, JourneyStep } from './journey-step.utils.js'; import { JourneyClientConfig } from './config.types.js'; import { callbackType, Step } from '@forgerock/sdk-types'; @@ -55,7 +55,7 @@ describe('journey-client', () => { }); test('start() should fetch and return the first step', async () => { - const mockStepResponse: Step = { callbacks: [] }; + const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] }; mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); const client = await journey({ config: mockConfig }); @@ -66,12 +66,13 @@ describe('journey-client', () => { const request = mockFetch.mock.calls[0][0] as Request; // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate expect(request.url).toBe('https://test.com/json/realms/root/authenticate'); - expect(step).toBeInstanceOf(JourneyStep); + expect(step).toHaveProperty('type', 'Step'); expect(step && step.payload).toEqual(mockStepResponse); }); test('next() should send the current step and return the next step', async () => { const initialStepPayload: Step = { + authId: 'test-auth-id', callbacks: [ { type: callbackType.NameCallback, @@ -81,6 +82,7 @@ describe('journey-client', () => { ], }; const nextStepPayload: Step = { + authId: 'test-auth-id', callbacks: [ { type: callbackType.PasswordCallback, @@ -89,12 +91,12 @@ describe('journey-client', () => { }, ], }; - const initialStep = new JourneyStep(initialStepPayload); + const initialStep = initialStepPayload; mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); const client = await journey({ config: mockConfig }); - const nextStep = await client.next(initialStep.payload, {}); + const nextStep = await client.next(initialStep, {}); expect(nextStep).toBeDefined(); expect(mockFetch).toHaveBeenCalledTimes(1); @@ -102,8 +104,8 @@ describe('journey-client', () => { // TODO: This should be /journeys?_action=next, but the current implementation calls /authenticate expect(request.url).toBe('https://test.com/json/realms/root/authenticate'); expect(request.method).toBe('POST'); - expect(await request.json()).toEqual(initialStep.payload); - expect(nextStep).toBeInstanceOf(JourneyStep); + expect(await request.json()).toEqual(initialStep); + expect(nextStep).toHaveProperty('type', 'Step'); expect(nextStep && nextStep.payload).toEqual(nextStepPayload); }); @@ -117,7 +119,7 @@ describe('journey-client', () => { }, ], }; - const step = new JourneyStep(mockStepPayload); + const step = createJourneyStep(mockStepPayload); const assignMock = vi.fn(); const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({ @@ -141,7 +143,7 @@ describe('journey-client', () => { }; mockStorageInstance.get.mockResolvedValue({ step: previousStepPayload }); - const nextStepPayload: Step = { callbacks: [] }; + const nextStepPayload: Step = { authId: 'test-auth-id', callbacks: [] }; mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); const client = await journey({ config: mockConfig }); @@ -163,7 +165,7 @@ describe('journey-client', () => { expect(request.method).toBe('POST'); expect(await request.json()).toEqual(previousStepPayload); - expect(step).toBeInstanceOf(JourneyStep); + expect(step).toHaveProperty('type', 'Step'); expect(step && step.payload).toEqual(nextStepPayload); }); @@ -176,7 +178,7 @@ describe('journey-client', () => { }; mockStorageInstance.get.mockResolvedValue({ step: plainStepPayload }); - const nextStepPayload: Step = { callbacks: [] }; + const nextStepPayload: Step = { authId: 'test-auth-id', callbacks: [] }; mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload))); const client = await journey({ config: mockConfig }); @@ -192,7 +194,7 @@ describe('journey-client', () => { const request = mockFetch.mock.calls[0][0] as Request; expect(request.method).toBe('POST'); expect(await request.json()).toEqual(plainStepPayload); // Expect the plain payload to be sent - expect(step).toBeInstanceOf(JourneyStep); // The returned step should still be an JourneyStep instance + expect(step).toHaveProperty('type', 'Step'); // The returned step should still be an JourneyStep instance expect(step && step.payload).toEqual(nextStepPayload); }); @@ -212,7 +214,7 @@ describe('journey-client', () => { test('should call start() with URL params when no previous step is required', async () => { mockStorageInstance.get.mockResolvedValue(undefined); - const mockStepResponse: Step = { callbacks: [] }; + const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] }; mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); const client = await journey({ config: mockConfig }); @@ -228,7 +230,7 @@ describe('journey-client', () => { // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate const url = new URL(request.url); expect(url.origin + url.pathname).toBe('https://test.com/json/realms/root/authenticate'); - expect(step).toBeInstanceOf(JourneyStep); + expect(step).toHaveProperty('type', 'Step'); expect(step && step.payload).toEqual(mockStepResponse); }); }); diff --git a/packages/journey-client/src/lib/journey-client.ts b/packages/journey-client/src/lib/journey-client.ts index 552e691ab2..f274453832 100644 --- a/packages/journey-client/src/lib/journey-client.ts +++ b/packages/journey-client/src/lib/journey-client.ts @@ -11,12 +11,19 @@ import { journeyApi } from './journey.api.js'; import { setConfig } from './journey.slice.js'; import { createStorage } from '@forgerock/storage'; import { GenericError, callbackType, type Step } from '@forgerock/sdk-types'; -import JourneyStep from './journey-step.js'; -import JourneyLoginSuccess from './journey-login-success.js'; -import JourneyLoginFailure from './journey-login-failure.js'; +import { JourneyStep } from './journey-step.utils.js'; +import { + createJourneyLoginSuccess, + JourneyLoginSuccess, +} from './journey-login-success.utils.js'; +import { + createJourneyLoginFailure, + JourneyLoginFailure, +} from './journey-login-failure.utils.js'; import RedirectCallback from './callbacks/redirect-callback.js'; import { logger as loggerFn, LogLevel, CustomLogger } from '@forgerock/sdk-logger'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import { createJourneyObject } from './journey.utils.js'; export async function journey({ config, @@ -43,7 +50,7 @@ export async function journey({ const self = { start: async (options?: StepOptions) => { const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options)); - return data ? new JourneyStep(data) : undefined; + return data ? createJourneyObject(data) : undefined; }, /** @@ -55,7 +62,7 @@ export async function journey({ */ next: async (step: Step, options?: StepOptions) => { const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options })); - return data ? new JourneyStep(data) : undefined; + return data ? createJourneyObject(data) : undefined; }, redirect: async (step: JourneyStep) => { @@ -74,7 +81,7 @@ export async function journey({ resume: async ( url: string, options?: StepOptions, - ): Promise => { + ): Promise | undefined> => { const parsedUrl = new URL(url); const code = parsedUrl.searchParams.get('code'); const state = parsedUrl.searchParams.get('state'); diff --git a/packages/journey-client/src/lib/journey-login-failure.ts b/packages/journey-client/src/lib/journey-login-failure.ts deleted file mode 100644 index c5d6cc55bb..0000000000 --- a/packages/journey-client/src/lib/journey-login-failure.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import type { Step, AuthResponse, FailureDetail } from '@forgerock/sdk-types'; -import { StepType } from '@forgerock/sdk-types'; -import JourneyPolicy from './journey-policy/index.js'; -import type { MessageCreator, ProcessedPropertyError } from './journey-policy/interfaces.js'; - -class JourneyLoginFailure implements AuthResponse { - /** - * The type of step. - */ - public readonly type = StepType.LoginFailure; - - /** - * @param payload The raw payload returned by OpenAM - */ - constructor(public payload: Step) {} - - /** - * Gets the error code. - */ - public getCode(): number { - return Number(this.payload.code); - } - - /** - * Gets the failure details. - */ - public getDetail(): FailureDetail | undefined { - return this.payload.detail; - } - - /** - * Gets the failure message. - */ - public getMessage(): string | undefined { - return this.payload.message; - } - - /** - * Gets processed failure message. - */ - public getProcessedMessage(messageCreator?: MessageCreator): ProcessedPropertyError[] { - return JourneyPolicy.parseErrors(this.payload, messageCreator); - } - - /** - * Gets the failure reason. - */ - public getReason(): string | undefined { - return this.payload.reason; - } -} - -export default JourneyLoginFailure; diff --git a/packages/journey-client/src/lib/journey-login-failure.utils.test.ts b/packages/journey-client/src/lib/journey-login-failure.utils.test.ts new file mode 100644 index 0000000000..313cbb54ae --- /dev/null +++ b/packages/journey-client/src/lib/journey-login-failure.utils.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, expect, it } from 'vitest'; +import { createJourneyLoginFailure } from './journey-login-failure.utils'; +import { StepType } from '@forgerock/sdk-types'; + +describe('createJourneyLoginFailure', () => { + it('should create a JourneyLoginFailure object', () => { + const step = { + code: 400, + detail: { failureUrl: 'failure-url' }, + message: 'Invalid credentials', + reason: 'INVALID_CREDENTIALS', + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const journeyLoginFailure = createJourneyLoginFailure(step); + + expect(journeyLoginFailure.type).toBe(StepType.LoginFailure); + expect(journeyLoginFailure.payload).toEqual(step); + expect(journeyLoginFailure.getCode()).toBe(400); + expect(journeyLoginFailure.getDetail()).toEqual({ failureUrl: 'failure-url' }); + expect(journeyLoginFailure.getMessage()).toBe('Invalid credentials'); + expect(journeyLoginFailure.getReason()).toBe('INVALID_CREDENTIALS'); + }); +}); diff --git a/packages/journey-client/src/lib/journey-login-failure.utils.ts b/packages/journey-client/src/lib/journey-login-failure.utils.ts new file mode 100644 index 0000000000..278d7109d8 --- /dev/null +++ b/packages/journey-client/src/lib/journey-login-failure.utils.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import type { Step, AuthResponse, FailureDetail } from '@forgerock/sdk-types'; +import { StepType } from '@forgerock/sdk-types'; +import JourneyPolicy from './journey-policy/index.js'; +import type { MessageCreator, ProcessedPropertyError } from './journey-policy/interfaces.js'; + +type JourneyLoginFailure = AuthResponse & { + payload: Step; + getCode: () => number; + getDetail: () => FailureDetail | undefined; + getMessage: () => string | undefined; + getProcessedMessage: (messageCreator?: MessageCreator) => ProcessedPropertyError[]; + getReason: () => string | undefined; +}; + +function getCode(payload: Step): number { + return Number(payload.code); +} + +function getDetail(payload: Step): FailureDetail | undefined { + return payload.detail; +} + +function getMessage(payload: Step): string | undefined { + return payload.message; +} + +function getProcessedMessage( + payload: Step, + messageCreator?: MessageCreator, +): ProcessedPropertyError[] { + return JourneyPolicy.parseErrors(payload, messageCreator); +} + +function getReason(payload: Step): string | undefined { + return payload.reason; +} + +function createJourneyLoginFailure(payload: Step): JourneyLoginFailure { + return { + payload, + type: StepType.LoginFailure, + getCode: () => getCode(payload), + getDetail: () => getDetail(payload), + getMessage: () => getMessage(payload), + getProcessedMessage: (messageCreator?: MessageCreator) => + getProcessedMessage(payload, messageCreator), + getReason: () => getReason(payload), + }; +} + +export { createJourneyLoginFailure, JourneyLoginFailure }; diff --git a/packages/journey-client/src/lib/journey-login-success.ts b/packages/journey-client/src/lib/journey-login-success.ts deleted file mode 100644 index 5b39122967..0000000000 --- a/packages/journey-client/src/lib/journey-login-success.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import type { Step, AuthResponse } from '@forgerock/sdk-types'; -import { StepType } from '@forgerock/sdk-types'; - -class JourneyLoginSuccess implements AuthResponse { - /** - * The type of step. - */ - public readonly type = StepType.LoginSuccess; - - /** - * @param payload The raw payload returned by OpenAM - */ - constructor(public payload: Step) {} - - /** - * Gets the step's realm. - */ - public getRealm(): string | undefined { - return this.payload.realm; - } - - /** - * Gets the step's session token. - */ - public getSessionToken(): string | undefined { - return this.payload.tokenId; - } - - /** - * Gets the step's success URL. - */ - public getSuccessUrl(): string | undefined { - return this.payload.successUrl; - } -} - -export default JourneyLoginSuccess; diff --git a/packages/journey-client/src/lib/journey-login-success.utils.test.ts b/packages/journey-client/src/lib/journey-login-success.utils.test.ts new file mode 100644 index 0000000000..53f0d8840e --- /dev/null +++ b/packages/journey-client/src/lib/journey-login-success.utils.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { describe, expect, it } from 'vitest'; +import { createJourneyLoginSuccess } from './journey-login-success.utils'; +import { StepType } from '@forgerock/sdk-types'; + +describe('createJourneyLoginSuccess', () => { + it('should create a JourneyLoginSuccess object', () => { + const step = { + realm: 'my-realm', + tokenId: 'my-token', + successUrl: 'my-success-url', + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const journeyLoginSuccess = createJourneyLoginSuccess(step); + + expect(journeyLoginSuccess.type).toBe(StepType.LoginSuccess); + expect(journeyLoginSuccess.payload).toEqual(step); + expect(journeyLoginSuccess.getRealm()).toBe('my-realm'); + expect(journeyLoginSuccess.getSessionToken()).toBe('my-token'); + expect(journeyLoginSuccess.getSuccessUrl()).toBe('my-success-url'); + }); +}); diff --git a/packages/journey-client/src/lib/journey-login-success.utils.ts b/packages/journey-client/src/lib/journey-login-success.utils.ts new file mode 100644 index 0000000000..a6e9caff9f --- /dev/null +++ b/packages/journey-client/src/lib/journey-login-success.utils.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import type { Step, AuthResponse } from '@forgerock/sdk-types'; +import { StepType } from '@forgerock/sdk-types'; + +type JourneyLoginSuccess = AuthResponse & { + payload: Step; + getRealm: () => string | undefined; + getSessionToken: () => string | undefined; + getSuccessUrl: () => string | undefined; +}; + +function getRealm(payload: Step): string | undefined { + return payload.realm; +} + +function getSessionToken(payload: Step): string | undefined { + return payload.tokenId; +} + +function getSuccessUrl(payload: Step): string | undefined { + return payload.successUrl; +} + +function createJourneyLoginSuccess(payload: Step): JourneyLoginSuccess { + return { + payload, + type: StepType.LoginSuccess, + getRealm: () => getRealm(payload), + getSessionToken: () => getSessionToken(payload), + getSuccessUrl: () => getSuccessUrl(payload), + }; +} + +export { + createJourneyLoginSuccess, + getRealm, + getSessionToken, + getSuccessUrl, + JourneyLoginSuccess, +}; diff --git a/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts index af86aeab4f..9ab92be556 100644 --- a/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts +++ b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts @@ -8,7 +8,7 @@ * of the MIT license. See the LICENSE file for details. */ -import JourneyStep from '../journey-step.js'; +import { createJourneyStep } from '../journey-step.utils.js'; import JourneyQRCode from './journey-qrcode.js'; import { otpQRCodeStep, pushQRCodeStep } from '../journey-qrcode/journey-qr-code.mock.data.js'; // import WebAuthn step as it's similar in structure for testing non-QR Code steps @@ -17,7 +17,7 @@ import { webAuthnRegJSCallback70 } from '../journey-webauthn/journey-webauthn.mo describe('Class for managing QR Codes', () => { it('should return true for step containing OTP QR Code callbacks', () => { const expected = true; - const step = new JourneyStep(otpQRCodeStep); + const step = createJourneyStep(otpQRCodeStep); const result = JourneyQRCode.isQRCodeStep(step); expect(result).toBe(expected); @@ -25,7 +25,7 @@ describe('Class for managing QR Codes', () => { it('should return true for step containing Push QR Code callbacks', () => { const expected = true; - const step = new JourneyStep(pushQRCodeStep); + const step = createJourneyStep(pushQRCodeStep); const result = JourneyQRCode.isQRCodeStep(step); expect(result).toBe(expected); @@ -33,7 +33,7 @@ describe('Class for managing QR Codes', () => { it('should return false for step containing WebAuthn step', () => { const expected = false; - const step = new JourneyStep(webAuthnRegJSCallback70); + const step = createJourneyStep(webAuthnRegJSCallback70); const result = JourneyQRCode.isQRCodeStep(step); expect(result).toBe(expected); @@ -49,7 +49,7 @@ describe('Class for managing QR Codes', () => { 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' + 'r=ForgeRock&period=30&digits=6&b=032b75', }; - const step = new JourneyStep(otpQRCodeStep); + const step = createJourneyStep(otpQRCodeStep); const result = JourneyQRCode.getQRCodeData(step); expect(result).toStrictEqual(expected); @@ -70,7 +70,7 @@ describe('Class for managing QR Codes', () => { 'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' + 'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75', }; - const step = new JourneyStep(pushQRCodeStep); + const step = createJourneyStep(pushQRCodeStep); const result = JourneyQRCode.getQRCodeData(step); expect(result).toStrictEqual(expected); diff --git a/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts index 21af7fe263..2c5675488b 100644 --- a/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts +++ b/packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts @@ -9,7 +9,7 @@ */ import { callbackType } from '@forgerock/sdk-types'; -import JourneyStep from '../journey-step.js'; +import { JourneyStep } from '../journey-step.utils.js'; import TextOutputCallback from '../callbacks/text-output-callback.js'; import HiddenValueCallback from '../callbacks/hidden-value-callback.js'; diff --git a/packages/journey-client/src/lib/journey-step.test.ts b/packages/journey-client/src/lib/journey-step.test.ts index 3e131efa76..fef55c6e46 100644 --- a/packages/journey-client/src/lib/journey-step.test.ts +++ b/packages/journey-client/src/lib/journey-step.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect } from 'vitest'; import { callbackType, type Step } from '@forgerock/sdk-types'; -import JourneyStep from './journey-step.js'; +import { createJourneyStep } from './journey-step.utils.js'; import NameCallback from './callbacks/name-callback.js'; describe('fr-step.ts', () => { @@ -36,7 +36,7 @@ describe('fr-step.ts', () => { }; it('should correctly initialize with a payload', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); expect(step.payload).toEqual(stepPayload); expect(step.callbacks).toHaveLength(3); expect(step.callbacks[0]).toBeInstanceOf(NameCallback); @@ -44,26 +44,26 @@ describe('fr-step.ts', () => { it('should get a single callback of a specific type', () => { const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] }; - const step = new JourneyStep(singleCallbackPayload); + const step = createJourneyStep(singleCallbackPayload); const cb = step.getCallbackOfType(callbackType.PasswordCallback); expect(cb).toBeDefined(); expect(cb.getType()).toBe(callbackType.PasswordCallback); }); it('should throw an error if getCallbackOfType finds no matching callbacks', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); const err = `Expected 1 callback of type "TermsAndConditionsCallback", but found 0`; expect(() => step.getCallbackOfType(callbackType.TermsAndConditionsCallback)).toThrow(err); }); it('should throw an error if getCallbackOfType finds multiple matching callbacks', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); const err = `Expected 1 callback of type "NameCallback", but found 2`; expect(() => step.getCallbackOfType(callbackType.NameCallback)).toThrow(err); }); it('should get all callbacks of a specific type', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); const callbacks = step.getCallbacksOfType(callbackType.NameCallback); expect(callbacks).toHaveLength(2); expect(callbacks[0].getType()).toBe(callbackType.NameCallback); @@ -71,31 +71,31 @@ describe('fr-step.ts', () => { }); it('should return an empty array if getCallbacksOfType finds no matches', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); const callbacks = step.getCallbacksOfType(callbackType.TermsAndConditionsCallback); expect(callbacks).toHaveLength(0); }); it('should set the value of a specific callback', () => { const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] }; - const step = new JourneyStep(singleCallbackPayload); + const step = createJourneyStep(singleCallbackPayload); step.setCallbackValue(callbackType.PasswordCallback, 'password123'); const cb = step.getCallbackOfType(callbackType.PasswordCallback); expect(cb.getInputValue()).toBe('password123'); }); it('should return the description', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); expect(step.getDescription()).toBe('Step description'); }); it('should return the header', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); expect(step.getHeader()).toBe('Step header'); }); it('should return the stage', () => { - const step = new JourneyStep(stepPayload); + const step = createJourneyStep(stepPayload); expect(step.getStage()).toBe('Step stage'); }); }); diff --git a/packages/journey-client/src/lib/journey-step.ts b/packages/journey-client/src/lib/journey-step.ts deleted file mode 100644 index cb9763ec1d..0000000000 --- a/packages/journey-client/src/lib/journey-step.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ - -import { - type CallbackType, - type Callback, - type Step, - type AuthResponse, -} from '@forgerock/sdk-types'; -import type JourneyCallback from './callbacks/index.js'; -import type { JourneyCallbackFactory } from './callbacks/factory.js'; -import createCallback from './callbacks/factory.js'; -import { StepType } from '@forgerock/sdk-types'; - -/** - * Represents a single step of an authentication tree. - */ -class JourneyStep implements AuthResponse { - /** - * The type of step. - */ - public readonly type = StepType.Step; - - /** - * The callbacks contained in this step. - */ - public callbacks: JourneyCallback[] = []; - - /** - * @param payload The raw payload returned by OpenAM - * @param callbackFactory A function that returns am implementation of JourneyCallback - */ - constructor( - public payload: Step, - callbackFactory?: JourneyCallbackFactory, - ) { - if (payload.callbacks) { - this.callbacks = this.convertCallbacks(payload.callbacks, callbackFactory); - } - } - - /** - * Gets the first callback of the specified type in this step. - * - * @param type The type of callback to find. - */ - public getCallbackOfType(type: CallbackType): T { - const callbacks = this.getCallbacksOfType(type); - if (callbacks.length !== 1) { - throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`); - } - return callbacks[0]; - } - - /** - * Gets all callbacks of the specified type in this step. - * - * @param type The type of callback to find. - */ - public getCallbacksOfType(type: CallbackType): T[] { - return this.callbacks.filter((x) => x.getType() === type) as T[]; - } - - /** - * Sets the value of the first callback of the specified type in this step. - * - * @param type The type of callback to find. - * @param value The value to set for the callback. - */ - public setCallbackValue(type: CallbackType, value: unknown): void { - const callbacks = this.getCallbacksOfType(type); - if (callbacks.length !== 1) { - throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`); - } - callbacks[0].setInputValue(value); - } - - /** - * Gets the step's description. - */ - public getDescription(): string | undefined { - return this.payload.description; - } - - /** - * Gets the step's header. - */ - public getHeader(): string | undefined { - return this.payload.header; - } - - /** - * Gets the step's stage. - */ - public getStage(): string | undefined { - return this.payload.stage; - } - - private convertCallbacks( - callbacks: Callback[], - callbackFactory?: JourneyCallbackFactory, - ): JourneyCallback[] { - const converted = callbacks.map((x: Callback) => { - // This gives preference to the provided factory and falls back to our default implementation - return (callbackFactory || createCallback)(x) || createCallback(x); - }); - return converted; - } -} - -/** - * A function that can populate the provided authentication tree step. - */ -type JourneyStepHandler = (step: JourneyStep) => void; - -export default JourneyStep; -export type { JourneyStepHandler }; diff --git a/packages/journey-client/src/lib/journey-step.utils.ts b/packages/journey-client/src/lib/journey-step.utils.ts new file mode 100644 index 0000000000..9b33ad6654 --- /dev/null +++ b/packages/journey-client/src/lib/journey-step.utils.ts @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import { + type CallbackType, + type Callback, + type Step, + type AuthResponse, +} from '@forgerock/sdk-types'; +import type JourneyCallback from './callbacks/index.js'; +import type { JourneyCallbackFactory } from './callbacks/factory.js'; +import createCallback from './callbacks/factory.js'; +import { StepType } from '@forgerock/sdk-types'; + +type JourneyStep = AuthResponse & { + payload: Step; + callbacks: JourneyCallback[]; + getCallbackOfType: (type: CallbackType) => T; + getCallbacksOfType: (type: CallbackType) => T[]; + setCallbackValue: (type: CallbackType, value: unknown) => void; + getDescription: () => string | undefined; + getHeader: () => string | undefined; + getStage: () => string | undefined; +}; + +function getCallbackOfType( + callbacks: JourneyCallback[], + type: CallbackType, +): T { + const callbacksOfType = getCallbacksOfType(callbacks, type); + if (callbacksOfType.length !== 1) { + throw new Error(`Expected 1 callback of type "${type}", but found ${callbacksOfType.length}`); + } + return callbacksOfType[0]; +} + +function getCallbacksOfType( + callbacks: JourneyCallback[], + type: CallbackType, +): T[] { + return callbacks.filter((x) => x.getType() === type) as T[]; +} + +function setCallbackValue(callbacks: JourneyCallback[], type: CallbackType, value: unknown): void { + const callbacksToUpdate = getCallbacksOfType(callbacks, type); + if (callbacksToUpdate.length !== 1) { + throw new Error(`Expected 1 callback of type "${type}", but found ${callbacksToUpdate.length}`); + } + callbacksToUpdate[0].setInputValue(value); +} + +function getDescription(payload: Step): string | undefined { + return payload.description; +} + +function getHeader(payload: Step): string | undefined { + return payload.header; +} + +function getStage(payload: Step): string | undefined { + return payload.stage; +} + +function convertCallbacks( + callbacks: Callback[], + callbackFactory?: JourneyCallbackFactory, +): JourneyCallback[] { + const converted = callbacks.map((x: Callback) => { + // This gives preference to the provided factory and falls back to our default implementation + return (callbackFactory || createCallback)(x) || createCallback(x); + }); + return converted; +} + +function createJourneyStep(payload: Step, callbackFactory?: JourneyCallbackFactory): JourneyStep { + const callbacks = payload.callbacks + ? convertCallbacks(payload.callbacks, callbackFactory) + : []; + return { + payload, + callbacks, + type: StepType.Step, + getCallbackOfType: (type: CallbackType) => + getCallbackOfType(callbacks, type), + getCallbacksOfType: (type: CallbackType) => + getCallbacksOfType(callbacks, type), + setCallbackValue: (type: CallbackType, value: unknown) => + setCallbackValue(callbacks, type, value), + getDescription: () => getDescription(payload), + getHeader: () => getHeader(payload), + getStage: () => getStage(payload), + }; +} + +/** + * A function that can populate the provided authentication tree step. + */ + +type JourneyStepHandler = (step: JourneyStep) => void; + +export { createJourneyStep, JourneyStep, JourneyStepHandler }; diff --git a/packages/journey-client/src/lib/journey-webauthn/index.ts b/packages/journey-client/src/lib/journey-webauthn/index.ts index dffac4f881..e2598a3817 100644 --- a/packages/journey-client/src/lib/journey-webauthn/index.ts +++ b/packages/journey-client/src/lib/journey-webauthn/index.ts @@ -12,7 +12,7 @@ import { callbackType } from '@forgerock/sdk-types'; import type HiddenValueCallback from '../callbacks/hidden-value-callback.js'; import type MetadataCallback from '../callbacks/metadata-callback.js'; -import type JourneyStep from '../journey-step.js'; +import type { JourneyStep } from '../journey-step.utils.js'; import { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType } from './enums.js'; import { arrayBufferToString, diff --git a/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts b/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts index 760adbcaae..66111608a7 100644 --- a/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts +++ b/packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts @@ -22,58 +22,50 @@ import { webAuthnRegMetaCallback70StoredUsername, webAuthnAuthMetaCallback70StoredUsername, } from './journey-webauthn.mock.data.js'; -import JourneyStep from '../journey-step.js'; +import { createJourneyStep } from '../journey-step.utils.js'; describe('Test FRWebAuthn class with 6.5.3 "Passwordless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnRegJSCallback653 as any); + const step = createJourneyStep(webAuthnRegJSCallback653 as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); + it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnAuthJSCallback653 as any); + const step = createJourneyStep(webAuthnAuthJSCallback653 as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); - // it('should return Registration type with register metadata callbacks', () => { - // // eslint-disable-next-line - // const step = new JourneyStep(webAuthnRegMetaCallback653 as any); - // const stepType = FRWebAuthn.getWebAuthnStepType(step); - // expect(stepType).toBe(WebAuthnStepType.Registration); - // }); - // it('should return Authentication type with authenticate metadata callbacks', () => { - // // eslint-disable-next-line - // const step = new JourneyStep(webAuthnAuthMetaCallback653 as any); - // const stepType = FRWebAuthn.getWebAuthnStepType(step); - // expect(stepType).toBe(WebAuthnStepType.Authentication); - // }); }); describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnRegJSCallback70 as any); + const step = createJourneyStep(webAuthnRegJSCallback70 as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); + it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnAuthJSCallback70 as any); + const step = createJourneyStep(webAuthnAuthJSCallback70 as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); console.log('the step type', stepType, WebAuthnStepType.Authentication); expect(stepType).toBe(WebAuthnStepType.Authentication); }); + it('should return Registration type with register metadata callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnRegMetaCallback70 as any); + const step = createJourneyStep(webAuthnRegMetaCallback70 as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); + it('should return Authentication type with authenticate metadata callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnAuthMetaCallback70 as any); + const step = createJourneyStep(webAuthnAuthMetaCallback70 as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); @@ -82,25 +74,26 @@ describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => { describe('Test FRWebAuthn class with 7.0 "Usernameless"', () => { it('should return Registration type with register text-output callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnRegJSCallback70StoredUsername as any); + const step = createJourneyStep(webAuthnRegJSCallback70StoredUsername as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); + it('should return Authentication type with authenticate text-output callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnAuthJSCallback70StoredUsername as any); + const step = createJourneyStep(webAuthnAuthJSCallback70StoredUsername as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); it('should return Registration type with register metadata callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnRegMetaCallback70StoredUsername as any); + const step = createJourneyStep(webAuthnRegMetaCallback70StoredUsername as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Registration); }); it('should return Authentication type with authenticate metadata callbacks', () => { // eslint-disable-next-line - const step = new JourneyStep(webAuthnAuthMetaCallback70StoredUsername as any); + const step = createJourneyStep(webAuthnAuthMetaCallback70StoredUsername as any); const stepType = JourneyWebAuthn.getWebAuthnStepType(step); expect(stepType).toBe(WebAuthnStepType.Authentication); }); diff --git a/packages/journey-client/src/lib/journey.utils.ts b/packages/journey-client/src/lib/journey.utils.ts new file mode 100644 index 0000000000..43f46645d2 --- /dev/null +++ b/packages/journey-client/src/lib/journey.utils.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { type Step, StepType } from '@forgerock/sdk-types'; +import { createJourneyStep, JourneyStep } from './journey-step.utils.js'; +import { createJourneyLoginSuccess, JourneyLoginSuccess } from './journey-login-success.utils.js'; +import { createJourneyLoginFailure, JourneyLoginFailure } from './journey-login-failure.utils.js'; + +function createJourneyObject( + step: Step, +): JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | undefined { + let type; + if (step.authId) { + type = StepType.Step; + } else if (step.successUrl) { + type = StepType.LoginSuccess; + } else { + type = StepType.LoginFailure; + } + + switch (type) { + case StepType.LoginSuccess: + return createJourneyLoginSuccess(step); + case StepType.LoginFailure: + return createJourneyLoginFailure(step); + case StepType.Step: + return createJourneyStep(step); + default: + return undefined; + } +} + +export { createJourneyObject }; diff --git a/packages/journey-client/src/lib/recovery-codes/index.ts b/packages/journey-client/src/lib/recovery-codes/index.ts index ec77b83409..dc4cf7da96 100644 --- a/packages/journey-client/src/lib/recovery-codes/index.ts +++ b/packages/journey-client/src/lib/recovery-codes/index.ts @@ -10,7 +10,7 @@ import { callbackType } from '@forgerock/sdk-types'; import type TextOutputCallback from '../callbacks/text-output-callback.js'; -import type JourneyStep from '../journey-step.js'; +import type { JourneyStep } from '../journey-step.utils.js'; import { parseDeviceNameText, parseDisplayRecoveryCodesText } from './script-parser.js'; /** diff --git a/packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts b/packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts index 54d5177377..067ba0613a 100644 --- a/packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts +++ b/packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts @@ -8,7 +8,7 @@ * of the MIT license. See the LICENSE file for details. */ -import JourneyStep from '../journey-step.js'; +import { createJourneyStep } from '../journey-step.utils.js'; import JourneyRecoveryCodes from './index.js'; import { displayRecoveryCodesResponse, @@ -19,24 +19,24 @@ import { describe('Class for managing the Display Recovery Codes node', () => { it('should return true if Display Recovery Codes step', () => { - const step = new JourneyStep(displayRecoveryCodesResponse); + const step = createJourneyStep(displayRecoveryCodesResponse); const isDisplayStep = JourneyRecoveryCodes.isDisplayStep(step); expect(isDisplayStep).toBe(true); }); it('should return false if not Display Recovery Codes step', () => { - const step = new JourneyStep(otherResponse); + const step = createJourneyStep(otherResponse); const isDisplayStep = JourneyRecoveryCodes.isDisplayStep(step); expect(isDisplayStep).toBe(false); }); it('should return the Recovery Codes as array of strings', () => { - const step = new JourneyStep(displayRecoveryCodesResponse); + const step = createJourneyStep(displayRecoveryCodesResponse); const recoveryCodes = JourneyRecoveryCodes.getCodes(step); expect(recoveryCodes).toStrictEqual(expectedRecoveryCodes); }); it('should return a display name from the getDisplayName method', () => { - const step = new JourneyStep(displayRecoveryCodesResponse); + const step = createJourneyStep(displayRecoveryCodesResponse); const displayName = JourneyRecoveryCodes.getDeviceName(step); expect(displayName).toStrictEqual(expectedDeviceName); }); From 15e7464d2761039769087ae304ca97a5eae794b8 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 23 Sep 2025 05:43:52 -0600 Subject: [PATCH 09/11] refactor(journey-client): refactor journey-step and related modules to functional API --- .../src/lib/journey-client.test.ts | 2 +- .../journey-client/src/lib/journey-client.ts | 10 +--------- .../lib/journey-login-failure.utils.test.ts | 2 +- .../lib/journey-login-success.utils.test.ts | 2 +- .../src/lib/journey-login-success.utils.ts | 2 +- .../src/lib/journey-step.utils.ts | 18 ++++++++---------- .../oidc/src/lib/state-pkce.effects.ts | 2 +- 7 files changed, 14 insertions(+), 24 deletions(-) diff --git a/packages/journey-client/src/lib/journey-client.test.ts b/packages/journey-client/src/lib/journey-client.test.ts index cc77cc7496..bc40675e64 100644 --- a/packages/journey-client/src/lib/journey-client.test.ts +++ b/packages/journey-client/src/lib/journey-client.test.ts @@ -7,7 +7,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { journey } from './journey-client.js'; -import { createJourneyStep, JourneyStep } from './journey-step.utils.js'; +import { createJourneyStep } from './journey-step.utils.js'; import { JourneyClientConfig } from './config.types.js'; import { callbackType, Step } from '@forgerock/sdk-types'; diff --git a/packages/journey-client/src/lib/journey-client.ts b/packages/journey-client/src/lib/journey-client.ts index f274453832..348ec07e1b 100644 --- a/packages/journey-client/src/lib/journey-client.ts +++ b/packages/journey-client/src/lib/journey-client.ts @@ -5,21 +5,13 @@ * of the MIT license. See the LICENSE file for details. */ +import type { JourneyStep } from './journey-step.utils.js'; import { createJourneyStore } from './journey.store.js'; import { JourneyClientConfig, StepOptions } from './config.types.js'; import { journeyApi } from './journey.api.js'; import { setConfig } from './journey.slice.js'; import { createStorage } from '@forgerock/storage'; import { GenericError, callbackType, type Step } from '@forgerock/sdk-types'; -import { JourneyStep } from './journey-step.utils.js'; -import { - createJourneyLoginSuccess, - JourneyLoginSuccess, -} from './journey-login-success.utils.js'; -import { - createJourneyLoginFailure, - JourneyLoginFailure, -} from './journey-login-failure.utils.js'; import RedirectCallback from './callbacks/redirect-callback.js'; import { logger as loggerFn, LogLevel, CustomLogger } from '@forgerock/sdk-logger'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; diff --git a/packages/journey-client/src/lib/journey-login-failure.utils.test.ts b/packages/journey-client/src/lib/journey-login-failure.utils.test.ts index 313cbb54ae..09fdfa9f56 100644 --- a/packages/journey-client/src/lib/journey-login-failure.utils.test.ts +++ b/packages/journey-client/src/lib/journey-login-failure.utils.test.ts @@ -5,7 +5,7 @@ */ import { describe, expect, it } from 'vitest'; -import { createJourneyLoginFailure } from './journey-login-failure.utils'; +import { createJourneyLoginFailure } from './journey-login-failure.utils.js'; import { StepType } from '@forgerock/sdk-types'; describe('createJourneyLoginFailure', () => { diff --git a/packages/journey-client/src/lib/journey-login-success.utils.test.ts b/packages/journey-client/src/lib/journey-login-success.utils.test.ts index 53f0d8840e..3c351c8aab 100644 --- a/packages/journey-client/src/lib/journey-login-success.utils.test.ts +++ b/packages/journey-client/src/lib/journey-login-success.utils.test.ts @@ -5,7 +5,7 @@ */ import { describe, expect, it } from 'vitest'; -import { createJourneyLoginSuccess } from './journey-login-success.utils'; +import { createJourneyLoginSuccess } from './journey-login-success.utils.js'; import { StepType } from '@forgerock/sdk-types'; describe('createJourneyLoginSuccess', () => { diff --git a/packages/journey-client/src/lib/journey-login-success.utils.ts b/packages/journey-client/src/lib/journey-login-success.utils.ts index a6e9caff9f..ff1b3aae9f 100644 --- a/packages/journey-client/src/lib/journey-login-success.utils.ts +++ b/packages/journey-client/src/lib/journey-login-success.utils.ts @@ -40,5 +40,5 @@ export { getRealm, getSessionToken, getSuccessUrl, - JourneyLoginSuccess, + type JourneyLoginSuccess, }; diff --git a/packages/journey-client/src/lib/journey-step.utils.ts b/packages/journey-client/src/lib/journey-step.utils.ts index 9b33ad6654..8df5664db6 100644 --- a/packages/journey-client/src/lib/journey-step.utils.ts +++ b/packages/journey-client/src/lib/journey-step.utils.ts @@ -26,6 +26,13 @@ type JourneyStep = AuthResponse & { getStage: () => string | undefined; }; +function getCallbacksOfType( + callbacks: JourneyCallback[], + type: CallbackType, +): T[] { + return callbacks.filter((x) => x.getType() === type) as T[]; +} + function getCallbackOfType( callbacks: JourneyCallback[], type: CallbackType, @@ -37,13 +44,6 @@ function getCallbackOfType( return callbacksOfType[0]; } -function getCallbacksOfType( - callbacks: JourneyCallback[], - type: CallbackType, -): T[] { - return callbacks.filter((x) => x.getType() === type) as T[]; -} - function setCallbackValue(callbacks: JourneyCallback[], type: CallbackType, value: unknown): void { const callbacksToUpdate = getCallbacksOfType(callbacks, type); if (callbacksToUpdate.length !== 1) { @@ -76,9 +76,7 @@ function convertCallbacks( } function createJourneyStep(payload: Step, callbackFactory?: JourneyCallbackFactory): JourneyStep { - const callbacks = payload.callbacks - ? convertCallbacks(payload.callbacks, callbackFactory) - : []; + const callbacks = payload.callbacks ? convertCallbacks(payload.callbacks, callbackFactory) : []; return { payload, callbacks, diff --git a/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts b/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts index 23d3e906f2..d29657c4f4 100644 --- a/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts +++ b/packages/sdk-effects/oidc/src/lib/state-pkce.effects.ts @@ -10,7 +10,7 @@ import { createVerifier, createState } from '@forgerock/sdk-utilities'; import type { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; function getStorageKey(clientId: string, prefix?: string) { - return `${prefix || 'Journey-SDK'}-authflow-${clientId}`; + return `${prefix || 'FR-SDK'}-authflow-${clientId}`; } /** From dba4466757d4f62b76947b43655991f63d8aa2ba Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 29 Sep 2025 08:16:51 -0600 Subject: [PATCH 10/11] chore: abstract-class-refactor --- .../lib/journey-device/device-profile.test.ts | 22 +- .../src/lib/journey-device/index.ts | 416 +++++++++--------- packages/oidc-client/tsconfig.json | 12 - 3 files changed, 227 insertions(+), 223 deletions(-) diff --git a/packages/journey-client/src/lib/journey-device/device-profile.test.ts b/packages/journey-client/src/lib/journey-device/device-profile.test.ts index f7d7b7198f..0bbcf1ddfa 100644 --- a/packages/journey-client/src/lib/journey-device/device-profile.test.ts +++ b/packages/journey-client/src/lib/journey-device/device-profile.test.ts @@ -8,18 +8,24 @@ * of the MIT license. See the LICENSE file for details. */ import { vi, expect, describe, it, afterEach, beforeEach, SpyInstance } from 'vitest'; -import { journeyDevice } from './index.js'; +import { JourneyDevice } from './index.js'; +// Patch window.crypto.getRandomValues to return Uint32Array for compatibility Object.defineProperty(window, 'crypto', { writable: true, value: { - getRandomValues: vi.fn().mockImplementation(() => ['714524572', '2799534390', '3707617532']), + getRandomValues: vi.fn().mockImplementation((arr: Uint32Array) => { + arr[0] = 714524572; + arr[1] = 2799534390; + arr[2] = 3707617532; + return arr; + }), }, }); describe('Test DeviceProfile', () => { it('should return basic metadata', async () => { - const device = journeyDevice(); + const device = new JourneyDevice(); const profile = await device.getProfile({ location: false, metadata: true, @@ -41,7 +47,7 @@ describe('Test DeviceProfile', () => { }); it('should return metadata without any display props', async () => { - const device = journeyDevice({ displayProps: [] }); + const device = new JourneyDevice({ displayProps: [] }); const profile = await device.getProfile({ location: false, metadata: true, @@ -57,7 +63,7 @@ describe('Test DeviceProfile', () => { }); it('should return metadata according to narrowed browser props', async () => { - const device = journeyDevice({ browserProps: ['userAgent'] }); + const device = new JourneyDevice({ browserProps: ['userAgent'] }); const profile = await device.getProfile({ location: false, metadata: true, @@ -95,13 +101,13 @@ describe('Test DeviceProfile', () => { }); it('should not log warnings if logLevel is "error"', () => { - const device = journeyDevice(undefined, 'error'); + const device = new JourneyDevice(undefined, 'error'); device.getBrowserMeta(); expect(warnSpy).not.toHaveBeenCalled(); }); it('should log warnings if logLevel is "warn"', () => { - const device = journeyDevice(undefined, 'warn'); + const device = new JourneyDevice(undefined, 'warn'); device.getBrowserMeta(); expect(warnSpy).toHaveBeenCalledWith( 'Cannot collect browser metadata. navigator is not defined.', @@ -111,7 +117,7 @@ describe('Test DeviceProfile', () => { it('should use custom prefix for device identifier', () => { const setItemSpy = vi.spyOn(Storage.prototype, 'setItem'); - const device = journeyDevice(undefined, 'info', 'my-custom-prefix'); + const device = new JourneyDevice(undefined, 'info', 'my-custom-prefix'); device.getIdentifier(); expect(setItemSpy).toHaveBeenCalledWith('my-custom-prefix-DeviceID', expect.any(String)); diff --git a/packages/journey-client/src/lib/journey-device/index.ts b/packages/journey-client/src/lib/journey-device/index.ts index cca37ba959..bd57cd1737 100644 --- a/packages/journey-client/src/lib/journey-device/index.ts +++ b/packages/journey-client/src/lib/journey-device/index.ts @@ -29,15 +29,16 @@ import type { import { reduceToObject, reduceToString } from '@forgerock/sdk-utilities'; import { logger as loggerFn } from '@forgerock/sdk-logger'; import type { LogLevel } from '@forgerock/sdk-logger'; +type Logger = ReturnType; /** - * @function journeyDevice - Collects user device metadata. + * @class JourneyDevice - Collects user device metadata. * * Example: * * ```js * // Instantiate new device object (w/optional config, if needed) - * const device = forgerock.journeyDevice({ + * const device = new forgerock.JourneyDevice({ * // optional configuration * }); * @@ -49,227 +50,236 @@ import type { LogLevel } from '@forgerock/sdk-logger'; * }); * ``` */ -export function journeyDevice( - configOptions?: ProfileConfigOptions, - logLevel: LogLevel = 'info', - prefix = 'forgerock', -) { - const JourneyLogger = loggerFn({ level: logLevel }); +class JourneyDevice { + private config: BaseProfileConfig; + private logger: Logger; + private prefix: string; - const config: BaseProfileConfig = { - fontNames, - devicePlatforms, - displayProps, - browserProps, - hardwareProps, - platformProps, - }; + constructor( + configOptions?: ProfileConfigOptions, + logLevel: LogLevel = 'info', + prefix = 'forgerock', + ) { + this.logger = loggerFn({ level: logLevel }); + this.prefix = prefix; + this.config = { + fontNames, + devicePlatforms, + displayProps, + browserProps, + hardwareProps, + platformProps, + }; - if (configOptions) { - Object.keys(configOptions).forEach((key: string) => { - if (!configurableCategories.includes(key)) { - throw new Error('Device profile configuration category does not exist.'); - } - config[key as Category] = configOptions[key as Category]; - }); + if (configOptions) { + Object.keys(configOptions).forEach((key: string) => { + if (!configurableCategories.includes(key)) { + throw new Error('Device profile configuration category does not exist.'); + } + this.config[key as Category] = configOptions[key as Category] as string[]; + }); + } } - const device = { - getBrowserMeta: (): Record => { - if (typeof navigator === 'undefined') { - JourneyLogger.warn('Cannot collect browser metadata. navigator is not defined.'); - return {}; - } - return reduceToObject(config.browserProps, navigator as unknown as Record); - }, + public getBrowserMeta(): Record { + if (typeof navigator === 'undefined') { + this.logger.warn('Cannot collect browser metadata. navigator is not defined.'); + return {}; + } + return reduceToObject( + this.config.browserProps, + navigator as unknown as Record, + ); + } - getBrowserPluginsNames: (): string => { - if (!(typeof navigator !== 'undefined' && navigator.plugins)) { - JourneyLogger.warn( - 'Cannot collect browser plugin information. navigator.plugins is not defined.', - ); - return ''; - } - return reduceToString( - Object.keys(navigator.plugins), - navigator.plugins as unknown as Record, + public getBrowserPluginsNames(): string { + if (!(typeof navigator !== 'undefined' && navigator.plugins)) { + this.logger.warn( + 'Cannot collect browser plugin information. navigator.plugins is not defined.', ); - }, + return ''; + } + return reduceToString( + Object.keys(navigator.plugins), + navigator.plugins as unknown as Record, + ); + } - getDeviceName: (): string => { - if (typeof navigator === 'undefined') { - JourneyLogger.warn('Cannot collect device name. navigator is not defined.'); - return ''; - } - const userAgent = navigator.userAgent; - const platform = navigator.platform; + public getDeviceName(): string { + if (typeof navigator === 'undefined') { + this.logger.warn('Cannot collect device name. navigator is not defined.'); + return ''; + } + const userAgent = navigator.userAgent; + const platform = navigator.platform; - switch (true) { - case config.devicePlatforms.mac.includes(platform): - return 'Mac (Browser)'; - case config.devicePlatforms.ios.includes(platform): - return `${platform} (Browser)`; - case config.devicePlatforms.windows.includes(platform): - return 'Windows (Browser)'; - case /Android/.test(platform) || /Android/.test(userAgent): - return 'Android (Browser)'; - case /CrOS/.test(userAgent) || /Chromebook/.test(userAgent): - return 'Chrome OS (Browser)'; - case /Linux/.test(platform): - return 'Linux (Browser)'; - default: - return `${platform || 'Unknown'} (Browser)`; - } - }, + switch (true) { + case this.config.devicePlatforms.mac.includes(platform): + return 'Mac (Browser)'; + case this.config.devicePlatforms.ios.includes(platform): + return `${platform} (Browser)`; + case this.config.devicePlatforms.windows.includes(platform): + return 'Windows (Browser)'; + case /Android/.test(platform) || /Android/.test(userAgent): + return 'Android (Browser)'; + case /CrOS/.test(userAgent) || /Chromebook/.test(userAgent): + return 'Chrome OS (Browser)'; + case /Linux/.test(platform): + return 'Linux (Browser)'; + default: + return `${platform || 'Unknown'} (Browser)`; + } + } - getDisplayMeta: (): { [key: string]: string | number | null } => { - if (typeof screen === 'undefined') { - JourneyLogger.warn('Cannot collect screen information. screen is not defined.'); - return {}; - } - return reduceToObject(config.displayProps, screen as unknown as Record); - }, + public getDisplayMeta(): { [key: string]: string | number | null } { + if (typeof screen === 'undefined') { + this.logger.warn('Cannot collect screen information. screen is not defined.'); + return {}; + } + return reduceToObject(this.config.displayProps, screen as unknown as Record); + } - getHardwareMeta: (): Record => { - if (typeof navigator === 'undefined') { - JourneyLogger.warn('Cannot collect OS metadata. Navigator is not defined.'); - return {}; - } - return reduceToObject(config.hardwareProps, navigator as unknown as Record); - }, + public getHardwareMeta(): Record { + if (typeof navigator === 'undefined') { + this.logger.warn('Cannot collect OS metadata. Navigator is not defined.'); + return {}; + } + return reduceToObject( + this.config.hardwareProps, + navigator as unknown as Record, + ); + } - getIdentifier: (): string => { - const storageKey = `${prefix}-DeviceID`; + public getIdentifier(): string { + const storageKey = `${this.prefix}-DeviceID`; - if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { - JourneyLogger.warn( - 'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.', - ); - return ''; - } - if (!localStorage) { - JourneyLogger.warn('Cannot store profile ID. localStorage is not supported.'); - return ''; - } - let id = localStorage.getItem(storageKey); - if (!id) { - // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" - id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); - localStorage.setItem(storageKey, id); - } - return id; - }, + if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { + this.logger.warn( + 'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.', + ); + return ''; + } + if (!localStorage) { + this.logger.warn('Cannot store profile ID. localStorage is not supported.'); + return ''; + } + let id = localStorage.getItem(storageKey); + if (!id) { + // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" + id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); + localStorage.setItem(storageKey, id); + } + return id; + } - getInstalledFonts: (): string => { - if (typeof document === 'undefined') { - JourneyLogger.warn('Cannot collect font data. Global document object is undefined.'); - return ''; - } - const canvas = document.createElement('canvas'); - if (!canvas) { - JourneyLogger.warn('Cannot collect font data. Browser does not support canvas element'); - return ''; - } - const context = canvas.getContext && canvas.getContext('2d'); + public getInstalledFonts(): string { + if (typeof document === 'undefined') { + this.logger.warn('Cannot collect font data. Global document object is undefined.'); + return ''; + } + const canvas = document.createElement('canvas'); + if (!canvas) { + this.logger.warn('Cannot collect font data. Browser does not support canvas element'); + return ''; + } + const context = canvas.getContext && canvas.getContext('2d'); - if (!context) { - JourneyLogger.warn('Cannot collect font data. Browser does not support 2d canvas context'); - return ''; - } - const text = 'abcdefghi0123456789'; - context.font = '72px Comic Sans'; - const baseWidth = context.measureText(text).width; + if (!context) { + this.logger.warn('Cannot collect font data. Browser does not support 2d canvas context'); + return ''; + } + const text = 'abcdefghi0123456789'; + context.font = '72px Comic Sans'; + const baseWidth = context.measureText(text).width; - const installedFonts = config.fontNames.reduce((prev: string, curr: string) => { - context.font = `72px ${curr}, Comic Sans`; - const newWidth = context.measureText(text).width; + const installedFonts = this.config.fontNames.reduce((prev: string, curr: string) => { + context.font = `72px ${curr}, Comic Sans`; + const newWidth = context.measureText(text).width; - if (newWidth !== baseWidth) { - prev = `${prev}${curr};`; - } - return prev; - }, ''); + if (newWidth !== baseWidth) { + prev = `${prev}${curr};`; + } + return prev; + }, ''); - return installedFonts; - }, + return installedFonts; + } - getLocationCoordinates: async (): Promise> => { - if (!(typeof navigator !== 'undefined' && navigator.geolocation)) { - JourneyLogger.warn( - 'Cannot collect geolocation information. navigator.geolocation is not defined.', - ); - return Promise.resolve({}); - } - return new Promise((resolve) => { - navigator.geolocation.getCurrentPosition( - (position) => - resolve({ - latitude: position.coords.latitude, - longitude: position.coords.longitude, - }), - () => { - JourneyLogger.warn('Cannot collect geolocation information. Geolocation API error.'); - resolve({}); - }, - { - enableHighAccuracy: true, - timeout: delay, - maximumAge: 0, - }, - ); - }); - }, + public async getLocationCoordinates(): Promise> { + if (!(typeof navigator !== 'undefined' && navigator.geolocation)) { + this.logger.warn( + 'Cannot collect geolocation information. navigator.geolocation is not defined.', + ); + return Promise.resolve({}); + } + return new Promise((resolve) => { + navigator.geolocation.getCurrentPosition( + (position) => + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }), + () => { + this.logger.warn('Cannot collect geolocation information. Geolocation API error.'); + resolve({}); + }, + { + enableHighAccuracy: true, + timeout: delay, + maximumAge: 0, + }, + ); + }); + } - getOSMeta: (): Record => { - if (typeof navigator === 'undefined') { - JourneyLogger.warn('Cannot collect OS metadata. navigator is not defined.'); - return {}; - } - return reduceToObject(config.platformProps, navigator as unknown as Record); - }, + public getOSMeta(): Record { + if (typeof navigator === 'undefined') { + this.logger.warn('Cannot collect OS metadata. navigator is not defined.'); + return {}; + } + return reduceToObject( + this.config.platformProps, + navigator as unknown as Record, + ); + } - getTimezoneOffset: (): number | null => { - try { - return new Date().getTimezoneOffset(); - } catch { - JourneyLogger.warn( - 'Cannot collect timezone information. getTimezoneOffset is not defined.', - ); - return null; - } - }, + public getTimezoneOffset(): number | null { + try { + return new Date().getTimezoneOffset(); + } catch { + this.logger.warn('Cannot collect timezone information. getTimezoneOffset is not defined.'); + return null; + } + } - getProfile: async function ({ - location, - metadata, - }: CollectParameters): Promise { - const profile: DeviceProfileData = { - identifier: this.getIdentifier(), - }; + public async getProfile({ location, metadata }: CollectParameters): Promise { + const profile: DeviceProfileData = { + identifier: this.getIdentifier(), + }; - if (metadata) { - profile.metadata = { - hardware: { - ...this.getHardwareMeta(), - display: this.getDisplayMeta(), - }, - browser: { - ...this.getBrowserMeta(), - plugins: this.getBrowserPluginsNames(), - }, - platform: { - ...this.getOSMeta(), - deviceName: this.getDeviceName(), - fonts: this.getInstalledFonts(), - timezone: this.getTimezoneOffset(), - }, - }; - } - if (location) { - profile.location = await this.getLocationCoordinates(); - } - return profile; - }, - }; - return device; + if (metadata) { + profile.metadata = { + hardware: { + ...this.getHardwareMeta(), + display: this.getDisplayMeta(), + }, + browser: { + ...this.getBrowserMeta(), + plugins: this.getBrowserPluginsNames(), + }, + platform: { + ...this.getOSMeta(), + deviceName: this.getDeviceName(), + fonts: this.getInstalledFonts(), + timezone: this.getTimezoneOffset(), + }, + }; + } + if (location) { + profile.location = await this.getLocationCoordinates(); + } + return profile; + } } + +export { JourneyDevice }; diff --git a/packages/oidc-client/tsconfig.json b/packages/oidc-client/tsconfig.json index f9c5bdebb9..9aae8f1878 100644 --- a/packages/oidc-client/tsconfig.json +++ b/packages/oidc-client/tsconfig.json @@ -21,18 +21,6 @@ { "path": "../sdk-effects/iframe-manager" }, - { - "path": "../sdk-effects/sdk-request-middleware" - }, - { - "path": "../sdk-effects/oidc" - }, - { - "path": "../sdk-effects/logger" - }, - { - "path": "../sdk-effects/iframe-manager" - }, { "path": "./tsconfig.lib.json" }, From 5366be3a8acd6d65c81036538c5ddb0660f91c11 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 29 Sep 2025 17:08:30 -0600 Subject: [PATCH 11/11] chore: export-modules-for-jc --- packages/journey-client/package.json | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json index 074af17955..5db24ad195 100644 --- a/packages/journey-client/package.json +++ b/packages/journey-client/package.json @@ -5,15 +5,20 @@ "type": "module", "exports": { ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" }, + "./journey-device": "./dist/src/lib/journey-device/index.js", + "./journey-policy": "./dist/src/lib/journey-policy/index.js", + "./journey-qrcode": "./dist/src/lib/journey-qrcode/journey-qrcode.js", + "./journey-recoverycodes": "./dist/src/lib/recovery-codes/index.js", + "./journey-webauthn": "./dist/src/lib/journey-webauthn/index.js", "./package.json": "./package.json" }, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", "scripts": { "build": "pnpm nx nxBuild", "lint": "pnpm nx nxLint",