From bc3da80184b6ba8e46845fc98708463584fc3212 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 21 Oct 2025 14:14:55 -0400 Subject: [PATCH 1/2] chore: migrate-old-mock-api migrate old mock api to repo chore: fix-tests --- .node-version | 2 +- e2e/am-mock-api/.babelrc | 3 + e2e/am-mock-api/.eslintrc.json | 30 + e2e/am-mock-api/package.json | 19 + e2e/am-mock-api/project.json | 50 + e2e/am-mock-api/src/app/.gitkeep | 0 e2e/am-mock-api/src/app/app.auth.js | 36 + e2e/am-mock-api/src/app/constants.js | 66 + e2e/am-mock-api/src/app/env.config.js | 71 + .../src/app/response.registration.js | 224 +++ e2e/am-mock-api/src/app/responses.js | 1349 +++++++++++++++++ e2e/am-mock-api/src/app/routes.auth.js | 644 ++++++++ e2e/am-mock-api/src/app/routes.resource.js | 191 +++ e2e/am-mock-api/src/app/wait.js | 15 + e2e/am-mock-api/src/assets/.gitkeep | 0 .../src/environments/environment.prod.ts | 3 + .../src/environments/environment.ts | 9 + e2e/am-mock-api/src/index.js | 57 + e2e/am-mock-api/tsconfig.app.json | 13 + e2e/am-mock-api/tsconfig.json | 13 + e2e/am-mock-api/tsconfig.spec.json | 9 + .../src/phone-number-field.test.ts | 4 +- e2e/journey-app/main.ts | 10 +- e2e/journey-app/server-configs.ts | 10 +- e2e/journey-suites/playwright.config.ts | 10 +- .../src/choice-confirm-poll.test.ts | 6 +- e2e/journey-suites/src/custom-paths.test.ts | 2 +- e2e/journey-suites/src/device-profile.test.ts | 2 +- e2e/journey-suites/src/email-suspend.test.ts | 2 +- e2e/journey-suites/src/login.test.ts | 4 +- e2e/journey-suites/src/no-session.test.ts | 4 +- e2e/journey-suites/src/otp-register.test.ts | 2 +- e2e/journey-suites/src/protect.test.ts | 2 +- e2e/journey-suites/src/registration.test.ts | 4 +- .../src/request-middleware.test.ts | 4 +- e2e/journey-suites/src/utils/demo-user.ts | 4 +- package.json | 3 +- .../src/lib/client.store.test.ts | 59 + .../journey-client/src/lib/client.store.ts | 27 +- pnpm-lock.yaml | 313 ++++ tsconfig.json | 3 + 41 files changed, 3246 insertions(+), 33 deletions(-) create mode 100644 e2e/am-mock-api/.babelrc create mode 100644 e2e/am-mock-api/.eslintrc.json create mode 100644 e2e/am-mock-api/package.json create mode 100644 e2e/am-mock-api/project.json create mode 100644 e2e/am-mock-api/src/app/.gitkeep create mode 100644 e2e/am-mock-api/src/app/app.auth.js create mode 100644 e2e/am-mock-api/src/app/constants.js create mode 100644 e2e/am-mock-api/src/app/env.config.js create mode 100644 e2e/am-mock-api/src/app/response.registration.js create mode 100644 e2e/am-mock-api/src/app/responses.js create mode 100644 e2e/am-mock-api/src/app/routes.auth.js create mode 100644 e2e/am-mock-api/src/app/routes.resource.js create mode 100644 e2e/am-mock-api/src/app/wait.js create mode 100644 e2e/am-mock-api/src/assets/.gitkeep create mode 100644 e2e/am-mock-api/src/environments/environment.prod.ts create mode 100644 e2e/am-mock-api/src/environments/environment.ts create mode 100644 e2e/am-mock-api/src/index.js create mode 100644 e2e/am-mock-api/tsconfig.app.json create mode 100644 e2e/am-mock-api/tsconfig.json create mode 100644 e2e/am-mock-api/tsconfig.spec.json diff --git a/.node-version b/.node-version index a45fd52cc..2bd5a0a98 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -24 +22 diff --git a/e2e/am-mock-api/.babelrc b/e2e/am-mock-api/.babelrc new file mode 100644 index 000000000..aed1b9711 --- /dev/null +++ b/e2e/am-mock-api/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nx/js/babel", { "useBuiltIns": "entry" }]] +} diff --git a/e2e/am-mock-api/.eslintrc.json b/e2e/am-mock-api/.eslintrc.json new file mode 100644 index 000000000..80cfb89e9 --- /dev/null +++ b/e2e/am-mock-api/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "ignorePatterns": [ + "node_modules", + "*.md", + "LICENSE", + ".babelrc", + ".env*", + ".bin", + "dist", + ".eslintignore" + ] + }, + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/e2e/am-mock-api/package.json b/e2e/am-mock-api/package.json new file mode 100644 index 000000000..bb3816e1b --- /dev/null +++ b/e2e/am-mock-api/package.json @@ -0,0 +1,19 @@ +{ + "name": "am-mock-api", + "version": "0.0.0", + "private": true, + "description": "", + "keywords": [], + "license": "ISC", + "author": "", + "main": "./index.js", + "dependencies": { + "@types/express": "^4.17.17", + "body-parser": "^2.2.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.5", + "express": "^4.21.2", + "superagent": "^10.2.3", + "uuid": "^13.0.0" + } +} diff --git a/e2e/am-mock-api/project.json b/e2e/am-mock-api/project.json new file mode 100644 index 000000000..8dfbab107 --- /dev/null +++ b/e2e/am-mock-api/project.json @@ -0,0 +1,50 @@ +{ + "name": "am-mock-api", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "e2e/am-mock-api/src", + "projectType": "application", + "tags": ["scope:e2e"], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{projectRoot}/dist"], + "options": { + "outputPath": "e2e/am-mock-api/dist", + "main": "e2e/am-mock-api/src/index.js", + "clean": true, + "tsConfig": "e2e/am-mock-api/tsconfig.app.json", + "assets": ["e2e/am-mock-api/src/assets"] + }, + "configurations": { + "development": { + "watch": true + }, + "production": { + "optimization": true, + "extractLicenses": true, + "inspect": false, + "fileReplacements": [ + { + "replace": "e2e/am-mock-api/src/environments/environment.ts", + "with": "e2e/am-mock-api/src/environments/environment.prod.ts" + } + ] + } + } + }, + "serve": { + "executor": "@nx/js:node", + "outputs": ["{projectRoot}/dist"], + "options": { + "buildTarget": "am-mock-api:build" + } + }, + "lint": { + "options": { + "fix": true, + "ignore-path": ".eslintignore", + "args": ["**/*.ts"] + } + } + } +} diff --git a/e2e/am-mock-api/src/app/.gitkeep b/e2e/am-mock-api/src/app/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/am-mock-api/src/app/app.auth.js b/e2e/am-mock-api/src/app/app.auth.js new file mode 100644 index 000000000..71d160dd0 --- /dev/null +++ b/e2e/am-mock-api/src/app/app.auth.js @@ -0,0 +1,36 @@ +/* + * @forgerock/javascript-sdk + * + * app.auth.js + * + * 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 request from 'superagent'; +import { AM_URL, REALM_PATH } from './env.config.js'; + +export let session; + +export async function authorizeApp({ un, pw }) { + try { + const response = await request + .post(`${AM_URL}/json/realms/${REALM_PATH}/authenticate`) + .set('Content-Type', 'application/json') + .set('Accept-API-Version', 'resource=2.0, protocol=1.0') + .set('X-OpenAM-Username', un) + .set('X-OpenAM-Password', pw) + .send({}); + + session = response.body; + + console.log(`REST app identity token: ${session.tokenId}`); + + return session; + } catch (error) { + console.warn('\n###################################################'); + console.warn('WARNING: REST app user for Step Up/Txn Auth missing'); + console.warn('###################################################\n'); + } +} diff --git a/e2e/am-mock-api/src/app/constants.js b/e2e/am-mock-api/src/app/constants.js new file mode 100644 index 000000000..6e7b8240a --- /dev/null +++ b/e2e/am-mock-api/src/app/constants.js @@ -0,0 +1,66 @@ +/* + * @forgerock/javascript-sdk + * + * constants.js + * + * 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. + */ + +export const authPaths = { + tokenExchange: [ + '/am/auth/tokenExchange', + '/am/oauth2/realms/root/access_token', + '/am/oauth2/realms/root/realms/middleware/access_token', + '/am/oauth2/realms/root/realms/middleware-modern/access_token', + '/am/oauth2/realms/root/realms/tokens-expiring-soon/access_token', + '/am/oauth2/realms/root/realms/tokens-expired/access_token', + ], + authenticate: [ + '/am/auth/authenticate', + '/am/json/realms/root/authenticate', + '/am/json/realms/root/realms/middleware/authenticate', + '/am/json/realms/root/realms/tokens-expiring-soon/authenticate', + '/am/json/realms/root/realms/tokens-expired/authenticate', + ], + htmlAuthenticate: ['/am/'], + authorize: [ + '/am/auth/authorize', + '/am/oauth2/realms/root/authorize', + '/am/oauth2/realms/root/realms/middleware/authorize', + '/am/oauth2/realms/root/realms/middleware-modern/authorize', + '/am/oauth2/realms/root/realms/tokens-expiring-soon/authorize', + '/am/oauth2/realms/root/realms/tokens-expired/authorize', + ], + endSession: [ + '/am/auth/endSession', + '/am/oauth2/realms/root/connect/endSession', + '/am/oauth2/realms/root/connect/idpEndSession', + '/am/oauth2/realms/root/realms/middleware/connect/endSession', + '/am/oauth2/realms/root/realms/tokens-expiring-soon/connect/endSession', + '/am/oauth2/realms/root/realms/tokens-expired/connect/endSession', + ], + userInfo: [ + '/am/auth/userInfo', + '/am/oauth2/realms/root/userinfo', + '/am/oauth2/realms/root/realms/middleware/userinfo', + '/am/oauth2/realms/root/realms/tokens-expiring-soon/userinfo', + '/am/oauth2/realms/root/realms/tokens-expired/userinfo', + ], + revoke: [ + '/am/auth/revoke', + '/am/oauth2/realms/root/token/revoke', + '/am/oauth2/realms/root/realms/middleware/token/revoke', + '/am/oauth2/realms/root/realms/tokens-expiring-soon/token/revoke', + '/am/oauth2/realms/root/realms/tokens-expired/token/revoke', + ], + sessions: [ + '/am/auth/sessions', + '/am/json/realms/root/sessions', + '/am/json/realms/root/realms/middleware/sessions', + '/am/json/realms/root/realms/tokens-expiring-soon/sessions', + '/am/json/realms/root/realms/tokens-expired/sessions', + ], + accounts: ['/o/oauth2/v2/auth', '/SAMLFailure', '/SAMLTest'], +}; diff --git a/e2e/am-mock-api/src/app/env.config.js b/e2e/am-mock-api/src/app/env.config.js new file mode 100644 index 000000000..e6bc01a1c --- /dev/null +++ b/e2e/am-mock-api/src/app/env.config.js @@ -0,0 +1,71 @@ +/* + * @forgerock/javascript-sdk + * + * env.config.js + * + * 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 { env } from 'process'; + +/** + * Configure your environment defaults below. + */ +const oauth = { + client: 'WebOAuthClient', + scope: 'openid profile me.read', +}; +const origins = { + // Ensure all domains are added to the security cert creation + app: process.env.NODE_ENV === 'LIVE' ? 'https://sdkapp.petrov.ca' : 'http://localhost', + forgeops: 'https://default.forgeops.petrov.ca', + mock: 'http://localhost', + resource: 'http://localhost', +}; +const paths = { + am: '/am', +}; +const ports = { + app: '8443', + forgeops: '443', + mock: '9443', + resource: '9443', +}; +const realm = 'root'; +const testUsers = [ + { + // Already exists in forgeops... + pw: 'password', + un: 'sdkuser', + }, +]; + +/** + * The below will be composed of the above values. + * Do not edit unless you know what you're doing. + */ +let amUrl; +let amPort; + +if (env.LIVE) { + amUrl = origins.forgeops; + amPort = ports.forgeops; +} else { + amUrl = origins.mock; + amPort = ports.mock; +} + +export const APP_PORT = ports.app; +export const AM_PORT = amPort; +export const MOCK_PORT = process.env.PORT || ports.mock; + +export const AM_URL = `${amUrl}:${amPort}${paths.am}`; +export const BASE_URL = `${origins.app}:${ports.app}`; +export const CLIENT_ID = oauth.client; +export const FORGEOPS = origins.forgeops; +export const REALM_PATH = realm; +export const RESOURCE_URL = `${origins.resource}:${ports.resource}`; +export const SCOPE = oauth.scope; +export const USERS = testUsers; diff --git a/e2e/am-mock-api/src/app/response.registration.js b/e2e/am-mock-api/src/app/response.registration.js new file mode 100644 index 000000000..527147371 --- /dev/null +++ b/e2e/am-mock-api/src/app/response.registration.js @@ -0,0 +1,224 @@ +/* + * @forgerock/javascript-sdk + * + * response.registration.js + * + * 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. + */ + +export default { + authId: 'foo', + callbacks: [ + { + type: 'ValidatedCreateUsernameCallback', + output: [ + { + name: 'policies', + value: [ + { policyId: 'unique' }, + { policyId: 'no-internal-user-conflict' }, + { policyId: 'cannot-contain-characters', params: { forbiddenChars: ['/'] } }, + ], + }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'prompt', value: 'Username' }, + ], + input: [ + { name: 'IDToken1', value: '' }, + { name: 'IDToken1validateOnly', value: false }, + ], + _id: 0, + }, + { + type: 'StringAttributeInputCallback', + output: [ + { name: 'name', value: 'givenName' }, + { name: 'prompt', value: 'First Name' }, + { name: 'required', value: true }, + { name: 'policies', value: [] }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'value', value: '' }, + ], + input: [ + { name: 'IDToken2', value: '' }, + { name: 'IDToken2validateOnly', value: false }, + ], + _id: 1, + }, + { + type: 'StringAttributeInputCallback', + output: [ + { name: 'name', value: 'sn' }, + { name: 'prompt', value: 'Last Name' }, + { name: 'required', value: true }, + { name: 'policies', value: [] }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'value', value: '' }, + ], + input: [ + { name: 'IDToken3', value: '' }, + { name: 'IDToken3validateOnly', value: false }, + ], + _id: 2, + }, + // { + // type: 'NumberAttributeInputCallback', + // input: [ + // { name: 'IDToken4', value: null }, + // { name: 'IDToken4validateOnly', value: false }, + // ], + // output: [ + // { name: 'name', value: 'age' }, + // { name: 'prompt', value: 'Age' }, + // { name: 'required', value: true }, + // { + // name: 'policies', + // value: { + // policyRequirements: ['VALID_TYPE'], + // fallbackPolicies: null, + // name: 'age', + // policies: [ + // { + // policyRequirements: ['VALID_TYPE'], + // policyId: 'valid-type', + // params: { types: ['number'] }, + // }, + // ], + // conditionalPolicies: null, + // }, + // }, + // { name: 'failedPolicies', value: [] }, + // { name: 'validateOnly', value: false }, + // { name: 'value', value: null }, + // ], + // }, + { + type: 'StringAttributeInputCallback', + output: [ + { name: 'name', value: 'mail' }, + { name: 'prompt', value: 'Email Address' }, + { name: 'required', value: true }, + { name: 'policies', value: [{ policyId: 'valid-email-address-format' }] }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'value', value: '' }, + ], + input: [ + { name: 'IDToken4', value: '' }, + { name: 'IDToken4validateOnly', value: false }, + ], + _id: 3, + }, + { + type: 'BooleanAttributeInputCallback', + output: [ + { name: 'name', value: 'preferences/marketing' }, + { name: 'prompt', value: 'Send me special offers and services' }, + { name: 'required', value: true }, + { name: 'policies', value: [] }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'value', value: false }, + ], + input: [ + { name: 'IDToken5', value: false }, + { name: 'IDToken5validateOnly', value: false }, + ], + _id: 4, + }, + { + type: 'BooleanAttributeInputCallback', + output: [ + { name: 'name', value: 'preferences/updates' }, + { name: 'prompt', value: 'Send me news and updates' }, + { name: 'required', value: true }, + { name: 'policies', value: [] }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'value', value: false }, + ], + input: [ + { name: 'IDToken6', value: false }, + { name: 'IDToken6validateOnly', value: false }, + ], + _id: 5, + }, + { + type: 'ValidatedCreatePasswordCallback', + output: [ + { name: 'echoOn', value: false }, + { + name: 'policies', + value: [ + { policyId: 'at-least-X-capitals', params: { numCaps: 1 } }, + { policyId: 'at-least-X-numbers', params: { numNums: 1 } }, + { + policyId: 'cannot-contain-others', + params: { disallowedFields: ['userName', 'givenName', 'sn'] }, + }, + ], + }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'prompt', value: 'Password' }, + ], + input: [ + { name: 'IDToken7', value: '' }, + { name: 'IDToken7validateOnly', value: false }, + ], + _id: 6, + }, + { + type: 'KbaCreateCallback', + output: [ + { name: 'prompt', value: 'Select a security question' }, + { + name: 'predefinedQuestions', + value: [`What's your favorite color?`, 'Who was your first employer?'], + }, + ], + input: [ + { name: 'IDToken8question', value: '' }, + { name: 'IDToken8answer', value: '' }, + ], + _id: 7, + }, + { + type: 'KbaCreateCallback', + output: [ + { name: 'prompt', value: 'Select a security question' }, + { + name: 'predefinedQuestions', + value: [`What's your favorite color?`, 'Who was your first employer?'], + }, + ], + input: [ + { name: 'IDToken9question', value: '' }, + { name: 'IDToken9answer', value: '' }, + ], + _id: 8, + }, + { + type: 'TermsAndConditionsCallback', + output: [ + { name: 'version', value: '0.0' }, + { + name: 'terms', + // eslint-disable-next-line + value: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + { name: 'createDate', value: '2019-10-28T04:20:11.320Z' }, + ], + input: [{ name: 'IDToken10', value: false }], + _id: 9, + }, + ], + header: 'Sign Up', + description: 'Already have an account? Sign In', +}; diff --git a/e2e/am-mock-api/src/app/responses.js b/e2e/am-mock-api/src/app/responses.js new file mode 100644 index 000000000..ccd1052fe --- /dev/null +++ b/e2e/am-mock-api/src/app/responses.js @@ -0,0 +1,1349 @@ +/* + * @forgerock/javascript-sdk + * + * responses.js + * + * 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 { AM_URL, RESOURCE_URL } from './env.config.js'; + +export const oauthTokens = { + access_token: 'baz', + refresh_token: 'qux', + scope: 'openid profile me.read', + id_token: 'mox', + token_type: 'Bearer', + expires_in: 3598, +}; + +export const oauthTokensExpiringSoon = { + access_token: 'baz', + refresh_token: 'qux', + scope: 'openid profile me.read', + id_token: 'mox', + token_type: 'Bearer', + expires_in: 20, +}; + +export const oauthTokensExpired = { + access_token: 'baz', + refresh_token: 'qux', + scope: 'openid profile me.read', + id_token: 'mox', + token_type: 'Bearer', + expires_in: 1, +}; + +export const authFail = { + code: 401, + message: 'Authentication Failed For Given Credentials', +}; + +export const authSuccess = { + tokenId: 'bar', + successUrl: '/console', + realm: '/', +}; + +export const createTxnStepUpUrl = (url) => { + console.log(url); + // Grab the client's desired AM URL + const referer = new URL(url); + const amUrl = referer.searchParams.get('amUrl'); + // Create the redirect URL + const redirectUrl = new URL(amUrl || AM_URL); + redirectUrl.searchParams.set('goto', `${RESOURCE_URL}/ig`); + redirectUrl.searchParams.set('realm', '/'); + redirectUrl.searchParams.set('authIndexType', 'composite_advice'); + redirectUrl.searchParams.set( + 'authIndexValue', + // eslint-disable-next-line max-len + '%3CAdvices%3E%3CAttributeValuePair%3E%3CAttribute%20name%3D%22TransactionConditionAdvice%22/%3E%3CValue%3E39dfdd15-59a3-473c-a7fc-ecda3bbc3bc8%3C/Value%3E%3C/AttributeValuePair%3E%3C/Advices%3E', + ); + + return redirectUrl.toString(); +}; + +export const createTreeStepUpUrl = (url) => { + // Grab the client's desired AM URL + const referer = new URL(url); + const amUrl = referer.searchParams.get('amUrl'); + // Create the redirect URL + const redirectUrl = new URL(amUrl || AM_URL); + redirectUrl.searchParams.set('goto', `${RESOURCE_URL}/ig`); + redirectUrl.searchParams.set('realm', '/'); + redirectUrl.searchParams.set('authIndexType', 'composite_advice'); + redirectUrl.searchParams.set( + 'authIndexValue', + // eslint-disable-next-line max-len + '%3CAdvices%3E%3CAttributeValuePair%3E%3CAttribute%20name=%22AuthenticateToServiceConditionAdvice%22/%3E%3CValue%3E/sdk:ConfirmPassword%3C/Value%3E%3C/AttributeValuePair%3E%3C/Advices%3E', + ); + + return redirectUrl.toString(); +}; + +export const createTxnStepUpHeader = (url) => { + // Grab the client's desired AM URL + const referer = new URL(url); + const amUrl = referer.searchParams.get('amUrl') || AM_URL; + + // Base 64 of {"TransactionConditionAdvice":["39dfdd15-59a3-473c-a7fc-ecda3bbc3bc8"]} + const advices = + 'eyJUcmFuc2FjdGlvbkNvbmRpdGlvbkFkdmljZSI6WyIzOWRmZGQxNS01OWEzLTQ3M2MtYTdmYy1lY2RhM2JiYzNiYzgiXX0='; + const realm = '/'; + const headerValue = `SSOADVICE realm="${realm}",advices="${advices}",am_uri="${amUrl}"`; + return headerValue; +}; + +export const createTreeStepUpHeader = (url) => { + // Grab the client's desired AM URL + const referer = new URL(url); + const amUrl = referer.searchParams.get('amUrl') || AM_URL; + + // Base 64 of {"AuthenticateToServiceConditionAdvice":["/sdk:ConfirmPassword"]} + const advices = + 'eyJBdXRoZW50aWNhdGVUb1NlcnZpY2VDb25kaXRpb25BZHZpY2UiOlsiL3NkazpDb25maXJtUGFzc3dvcmQiXX0='; + const realm = '/'; + const headerValue = `SSOADVICE realm="${realm}",advices="${advices}",am_uri="${amUrl}"`; + return headerValue; +}; + +export const authByTreeResponse = { + resource: '', + actions: {}, + attributes: {}, + advices: { + AuthenticateToServiceConditionAdvice: ['/sdk:ConfirmPassword'], + }, + ttl: 0, +}; + +export const authByTxnResponse = { + resource: '', + actions: {}, + attributes: {}, + advices: { + TransactionConditionAdvice: ['39dfdd15-59a3-473c-a7fc-ecda3bbc3bc8'], + }, + ttl: 0, +}; + +export const emailSuspend = { + authId: 'foo', + callbacks: [ + { + type: 'SuspendedTextOutputCallback', + output: [ + { + name: 'message', + value: + // eslint-disable-next-line max-len + 'An email has been sent to the address you entered. Click the link in that email to proceed.', + }, + { name: 'messageType', value: '0' }, + ], + }, + ], +}; + +export const idpChoiceCallback = { + authId: 'foo', + callbacks: [ + { + type: 'ChoiceCallback', + output: [ + { name: 'prompt', value: 'Select Provider' }, + { name: 'choices', value: ['google', 'facebook'] }, + { name: 'defaultChoice', value: 0 }, + ], + input: [{ name: 'IDToken1', value: 0 }], + }, + ], +}; +export const nameCallback = { + authId: 'foo', + callbacks: [ + { + type: 'NameCallback', + output: [{ name: 'prompt', value: 'User Name' }], + input: [{ name: 'IDToken1', value: '' }], + _id: 0, + }, + ], +}; +export const textInputCallback = { + authId: 'foo', + callbacks: [ + { + type: 'TextInputCallback', + output: [ + { + name: 'prompt', + value: 'Provide a nickname for this account', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }, + ], +}; + +export const initialBasicLogin = { + authId: 'foo', + callbacks: [ + { + type: 'NameCallback', + output: [{ name: 'prompt', value: 'User Name' }], + input: [{ name: 'IDToken1', value: '' }], + _id: 0, + }, + { + type: 'PasswordCallback', + output: [{ name: 'prompt', value: 'Password' }], + input: [{ name: 'IDToken2', value: '' }], + _id: 1, + }, + ], + stage: 'UsernamePassword', +}; + +export const initialLoginWithEmailResponse = { + authId: 'foo', + callbacks: [ + { + type: 'ValidatedCreateUsernameCallback', + output: [ + { name: 'policies', value: {} }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'prompt', value: 'Username' }, + ], + input: [ + { name: 'IDToken1', value: '' }, + { name: 'IDToken1validateOnly', value: false }, + ], + }, + ], +}; + +export const initialPlatformLogin = { + authId: 'foo', + callbacks: [ + { + type: 'ValidatedCreateUsernameCallback', + input: [ + { name: 'IDToken1', value: '' }, + { name: 'IDToken1validateOnly', value: false }, + ], + output: [ + { name: 'policies', value: [] }, + { name: 'failedPolicies', value: [] }, + { name: 'validateOnly', value: false }, + { name: 'prompt', value: 'Username' }, + ], + _id: 0, + }, + { + type: 'ValidatedCreatePasswordCallback', + output: [{ name: 'prompt', value: 'Password' }], + input: [ + { name: 'IDToken2', value: '' }, + { name: 'IDToken2validateOnly', value: false }, + ], + _id: 1, + }, + ], + stage: 'UsernamePassword', +}; + +export const initialMiscCallbacks = { + authId: 'foo', + callbacks: [ + { + type: 'NameCallback', + output: [{ name: 'prompt', value: 'User Name' }], + input: [{ name: 'IDToken1', value: '' }], + }, + ], +}; + +export const passwordCallback = { + authId: 'foo', + callbacks: [ + { + type: 'PasswordCallback', + output: [{ name: 'prompt', value: 'Password' }], + input: [{ name: 'IDToken1', value: '' }], + }, + ], +}; + +export const pingProtectEvaluate = { + authId: 'foo', + callbacks: [ + { + type: 'PingOneProtectEvaluationCallback', + output: [ + { + name: 'pauseBehavioralData', + value: true, + }, + ], + input: [ + { + name: 'IDToken1signals', + value: '', + }, + { + name: 'IDToken1clientError', + value: '', + }, + ], + }, + ], +}; + +export const pingProtectInitialize = { + authId: 'foo', + callbacks: [ + { + type: 'PingOneProtectInitializeCallback', + output: [ + { + name: 'envId', + value: '02fb1243-189a-4bc7-9d6c-a919edf6447', + }, + { + name: 'consoleLogEnabled', + value: true, + }, + { + name: 'deviceAttributesToIgnore', + value: ['userAgent'], + }, + { + name: 'customHost', + value: 'http://localhost', + }, + { + name: 'lazyMetadata', + value: false, + }, + { + name: 'behavioralDataCollection', + value: true, + }, + { + name: 'deviceKeyRsyncIntervals', + value: 14, + }, + { + name: 'enableTrust', + value: false, + }, + { + name: 'disableTags', + value: false, + }, + { + name: 'disableHub', + value: false, + }, + ], + input: [ + { + name: 'IDToken1clientError', + value: '', + }, + ], + }, + ], +}; + +export const choiceCallback = { + authId: 'foo', + callbacks: [ + { + type: 'ChoiceCallback', + output: [ + { name: 'prompt', value: 'Choose your color' }, + { name: 'choices', value: ['red', 'green', 'blue'] }, + { name: 'defaultChoice', value: 0 }, + ], + input: [{ name: 'IDToken1', value: 0 }], + }, + ], +}; + +export const secondFactorChoiceCallback = { + authId: 'foo', + callbacks: [ + { + type: 'ChoiceCallback', + output: [ + { name: 'prompt', value: 'Choose Second Factor' }, + { name: 'choices', value: ['Email', 'SMS'] }, + { name: 'defaultChoice', value: 0 }, + ], + input: [{ name: 'IDToken1', value: 0 }], + }, + ], +}; + +export const messageCallback = { + authId: 'foo', + callbacks: [ + { + type: 'TextOutputCallback', + output: [ + { name: 'message', value: 'Is it true?' }, + { name: 'messageType', value: '0' }, + ], + }, + { + type: 'ConfirmationCallback', + output: [ + { name: 'prompt', value: '' }, + { name: 'messageType', value: 0 }, + { name: 'options', value: ['Yes', 'No'] }, + { name: 'optionType', value: -1 }, + { name: 'defaultOption', value: 1 }, + ], + input: [{ name: 'IDToken2', value: 0 }], + }, + ], +}; + +export const noSessionSuccess = { successUrl: '/am/console', realm: '/' }; + +export const pollingCallback = { + authId: 'foo', + callbacks: [ + { + type: 'PollingWaitCallback', + output: [ + { name: 'waitTime', value: '1000' }, + { name: 'message', value: 'Waiting for response...' }, + ], + }, + ], +}; + +export const selectIdPCallback = { + authId: 'foo', + callbacks: [ + { + type: 'SelectIdPCallback', + output: [ + { + name: 'providers', + value: [ + { + provider: 'google', + uiConfig: { + buttonImage: 'images/g-logo.png', + buttonCustomStyle: 'background-color: #fff; color: #757575; border-color: #ddd;', + buttonClass: '', + buttonCustomStyleHover: + 'color: #6d6d6d; background-color: #eee; border-color: #ccc;', + buttonDisplayName: 'Google', + iconFontColor: 'white', + iconClass: 'fa-google', + iconBackground: '#4184f3', + }, + }, + { + provider: 'facebook', + uiConfig: { + buttonImage: '', + buttonCustomStyle: 'background-color: #3b5998;border-color: #3b5998; color: white;', + buttonClass: 'fa-facebook-official', + buttonCustomStyleHover: + 'background-color: #334b7d;border-color: #334b7d; color: white;', + buttonDisplayName: 'Facebook', + iconFontColor: 'white', + iconClass: 'fa-facebook', + iconBackground: '#3b5998', + }, + }, + { provider: 'localAuthentication' }, + ], + }, + { name: 'value', value: '' }, + ], + input: [{ name: 'IDToken1', value: '' }], + _id: 0, + }, + ], +}; + +export const secondFactorCallback = { + authId: 'foo', + callbacks: [ + { + type: 'PasswordCallback', + output: [{ name: 'prompt', value: 'One Time Password' }], + input: [{ name: 'IDToken1', value: '' }], + _id: 0, + }, + ], + stage: 'OneTimePasswordEmail', +}; + +export const otpQRCodeCallbacks = { + authId: 'foo', + callbacks: [ + { + type: '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: 'TextOutputCallback', + output: [ + { + name: 'message', + value: + // eslint-disable-next-line quotes + "window.QRCodeReader.createCode({\n id: 'callback_0',\n text: 'otpauth\\x3A\\x2F\\x2Ftotp\\x2FForgeRock\\x3Ajlowery\\x3Fperiod\\x3D30\\x26b\\x3D032b75\\x26digits\\x3D6\\x26secret\\QITSTC234FRIU8DD987DW3VPICFY\\x3D\\x3D\\x3D\\x3D\\x3D\\x3D\\x26issuer\\x3DForgeRock',\n version: '20',\n code: 'L'\n});", + }, + { + name: 'messageType', + value: '4', + }, + ], + }, + { + type: 'HiddenValueCallback', + output: [ + { + name: 'value', + value: + 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issuer=ForgeRock&period=30&digits=6&b=032b75', + }, + { + name: 'id', + value: 'mfaDeviceRegistration', + }, + ], + input: [ + { + name: 'IDToken3', + value: 'mfaDeviceRegistration', + }, + ], + }, + { + type: '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 redirectCallback = { + authId: 'foo', + callbacks: [ + { + type: 'RedirectCallback', + output: [ + { + name: 'redirectUrl', + value: + // eslint-disable-next-line max-len + 'http://localhost:9443/o/oauth2/v2/auth?nonce=ko7fdf2v3b6yctgq35bdpndel0p9qiq&response_type=code&client_id=546064052569-ke17g9ufsmvda3kgg7s5kp2hpf3gnqi8.apps.googleusercontent.com&scope=openid%20profile%20email&code_challenge=Bh_6aMiI04KGI1wVILtEamByklmXnQY9JKhKhlwsIxk&code_challenge_method=S256&state=rtu8pz65dbg6baw985d532myfbbnf5v', + }, + { name: 'redirectMethod', value: 'GET' }, + { name: 'trackingCookie', value: true }, + ], + }, + ], +}; +export const redirectCallbackSaml = { + authId: 'foo', + callbacks: [ + { + type: 'RedirectCallback', + output: [ + { + name: 'redirectUrl', + value: + // eslint-disable-next-line max-len + 'http://localhost:9443/SAMLTest/', + }, + { name: 'redirectMethod', value: 'GET' }, + { name: 'trackingCookie', value: true }, + ], + }, + ], +}; +export const redirectCallbackFailureSaml = { + authId: 'foo', + callbacks: [ + { + type: 'RedirectCallback', + output: [ + { + name: 'redirectUrl', + value: + // eslint-disable-next-line max-len + 'http://localhost:9443/SAMLFailure', + }, + { name: 'redirectMethod', value: 'GET' }, + { name: 'trackingCookie', value: true }, + ], + }, + ], +}; +export const txnAuthz = { + authId: 'bar', + callbacks: [ + { + type: 'PasswordCallback', + output: [{ name: 'prompt', value: 'Password' }], + input: [{ name: 'IDToken2', value: '' }], + _id: 1, + }, + ], + stage: 'TransactionAuthorization', +}; +export const treeAuthz = { + authId: 'bar', + callbacks: [ + { + type: 'PasswordCallback', + output: [{ name: 'prompt', value: 'Password' }], + input: [{ name: 'IDToken2', value: '' }], + _id: 1, + }, + ], + stage: 'TreeBasedAuthorization', +}; + +export const userInfo = { + family_name: 'Tester', + given_name: 'Bob', + name: 'Bob Tester', + updated_at: 1575671644, + sub: 'thetester', +}; + +export const requestDeviceProfile = { + authId: 'foo', + callbacks: [ + { + type: 'DeviceProfileCallback', + output: [ + { + name: 'metadata', + value: true, + }, + { + name: 'location', + value: true, + }, + { + name: 'message', + value: 'Collecting profile ...', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }, + ], +}; + +export const wellKnownForgeRock = { + request_parameter_supported: true, + pushed_authorization_request_endpoint: 'http://localhost:9443/am/oauth2/realms/root/par', + introspection_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'RSA-OAEP', + 'ECDH-ES+A128KW', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + claims_parameter_supported: false, + introspection_endpoint: 'http://localhost:9443/am/oauth2/realms/root/introspect', + issuer: 'http://localhost:9443/am/oauth2/realms/root', + id_token_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + userinfo_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + authorization_endpoint: 'http://localhost:9443/am/oauth2/realms/root/authorize', + authorization_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'RSA-OAEP', + 'ECDH-ES+A128KW', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + introspection_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + claims_supported: [], + rcs_request_signing_alg_values_supported: [ + 'PS384', + 'ES384', + 'RS384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + token_endpoint_auth_methods_supported: [ + 'client_secret_post', + 'private_key_jwt', + 'self_signed_tls_client_auth', + 'tls_client_auth', + 'none', + 'client_secret_basic', + ], + tls_client_certificate_bound_access_tokens: true, + response_modes_supported: [ + 'fragment.jwt', + 'form_post', + 'form_post.jwt', + 'jwt', + 'fragment', + 'query.jwt', + 'query', + ], + backchannel_logout_session_supported: true, + token_endpoint: 'http://localhost:9443/am/oauth2/realms/root/access_token', + response_types_supported: [ + 'code token id_token', + 'code', + 'code id_token', + 'device_code', + 'id_token', + 'code token', + 'token', + 'token id_token', + ], + authorization_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + revocation_endpoint_auth_methods_supported: [ + 'client_secret_post', + 'private_key_jwt', + 'self_signed_tls_client_auth', + 'tls_client_auth', + 'none', + 'client_secret_basic', + ], + request_uri_parameter_supported: true, + grant_types_supported: [ + 'implicit', + 'urn:ietf:params:oauth:grant-type:saml2-bearer', + 'refresh_token', + 'password', + 'client_credentials', + 'urn:ietf:params:oauth:grant-type:device_code', + 'authorization_code', + 'urn:openid:params:grant-type:ciba', + 'urn:ietf:params:oauth:grant-type:uma-ticket', + 'urn:ietf:params:oauth:grant-type:jwt-bearer', + ], + version: '3.0', + prompt_values_supported: ['none', 'login', 'consent'], + userinfo_endpoint: 'http://localhost:9443/am/oauth2/realms/root/userinfo', + require_request_uri_registration: true, + code_challenge_methods_supported: ['plain', 'S256'], + id_token_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'RSA-OAEP', + 'ECDH-ES+A128KW', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + authorization_signing_alg_values_supported: [ + 'PS384', + 'RS384', + 'EdDSA', + 'ES384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + request_object_signing_alg_values_supported: [ + 'PS384', + 'ES384', + 'RS384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + request_object_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'ECDH-ES+A128KW', + 'RSA-OAEP', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + rcs_response_signing_alg_values_supported: [ + 'PS384', + 'ES384', + 'RS384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + introspection_signing_alg_values_supported: [ + 'PS384', + 'RS384', + 'EdDSA', + 'ES384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + check_session_iframe: 'http://localhost:9443/am/oauth2/realms/root/connect/checkSession', + scopes_supported: [ + 'address', + 'phone', + 'openid', + 'profile', + 'fr:idm:*', + 'am-introspect-all-tokens', + 'email', + ], + backchannel_logout_supported: true, + acr_values_supported: [], + request_object_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + rcs_request_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'RSA-OAEP', + 'ECDH-ES+A128KW', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + userinfo_signing_alg_values_supported: [ + 'ES384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + ], + require_pushed_authorization_requests: false, + rcs_response_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + userinfo_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'RSA-OAEP', + 'ECDH-ES+A128KW', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + end_session_endpoint: 'http://localhost:9443/am/oauth2/realms/root/connect/endSession', + rcs_request_encryption_enc_values_supported: [ + 'A256GCM', + 'A192GCM', + 'A128GCM', + 'A128CBC-HS256', + 'A192CBC-HS384', + 'A256CBC-HS512', + ], + revocation_endpoint: 'http://localhost:9443/am/oauth2/realms/root/token/revoke', + rcs_response_encryption_alg_values_supported: [ + 'ECDH-ES+A256KW', + 'ECDH-ES+A192KW', + 'ECDH-ES+A128KW', + 'RSA-OAEP', + 'RSA-OAEP-256', + 'A128KW', + 'A256KW', + 'ECDH-ES', + 'dir', + 'A192KW', + ], + token_endpoint_auth_signing_alg_values_supported: [ + 'PS384', + 'ES384', + 'RS384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + jwks_uri: 'http://localhost:9443/am/oauth2/realms/root/connect/jwk_uri', + subject_types_supported: ['public', 'pairwise'], + id_token_signing_alg_values_supported: [ + 'PS384', + 'ES384', + 'RS384', + 'HS256', + 'HS512', + 'ES256', + 'RS256', + 'HS384', + 'ES512', + 'PS256', + 'PS512', + 'RS512', + ], + registration_endpoint: 'http://localhost:9443/am/oauth2/realms/root/register', +}; + +// NOT USED +export const wellKnownPing = { + issuer: 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as', + authorization_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize', + pushed_authorization_request_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/par', + token_endpoint: 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token', + userinfo_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo', + jwks_uri: 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/jwks', + end_session_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff', + introspection_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/introspect', + revocation_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke', + device_authorization_endpoint: + 'https://ping.example.com:9443/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/device_authorization', + claims_parameter_supported: false, + request_parameter_supported: true, + request_uri_parameter_supported: false, + require_pushed_authorization_requests: false, + scopes_supported: ['openid', 'profile', 'email', 'address', 'phone'], + response_types_supported: [ + 'code', + 'id_token', + 'token id_token', + 'code id_token', + 'code token', + 'code token id_token', + ], + response_modes_supported: ['pi.flow', 'query', 'fragment', 'form_post'], + grant_types_supported: [ + 'authorization_code', + 'implicit', + 'client_credentials', + 'refresh_token', + 'urn:ietf:params:oauth:grant-type:device_code', + ], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + userinfo_signing_alg_values_supported: ['none'], + request_object_signing_alg_values_supported: [ + 'none', + 'HS256', + 'HS384', + 'HS512', + 'RS256', + 'RS384', + 'RS512', + ], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + 'client_secret_jwt', + 'private_key_jwt', + ], + token_endpoint_auth_signing_alg_values_supported: [ + 'HS256', + 'HS384', + 'HS512', + 'RS256', + 'RS384', + 'RS512', + ], + claim_types_supported: ['normal'], + claims_supported: [ + 'sub', + 'iss', + 'auth_time', + 'acr', + 'name', + 'given_name', + 'family_name', + 'middle_name', + 'preferred_username', + 'profile', + 'picture', + 'zoneinfo', + 'phone_number', + 'updated_at', + 'address', + 'email', + 'locale', + ], + code_challenge_methods_supported: ['plain', 'S256'], +}; + +export const newPiWellKnown = { + issuer: 'http://localhost:9443/am', + authorization_endpoint: 'http://localhost:9443/am/oauth2/realms/root/authorize', + token_endpoint: 'http://localhost:9443/am/oauth2/realms/root/access_token', + userinfo_endpoint: 'http://localhost:9443/am/oauth2/realms/root/userinfo', + jwks_uri: 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/jwks', + end_session_endpoint: 'http://localhost:9443/am/oauth2/realms/root/connect/endSession', + ping_end_idp_session_endpoint: + 'http://localhost:9443/am/oauth2/realms/root/connect/idpEndSession', + introspection_endpoint: 'http://localhost:9443/am/oauth2/realms/root/introspect', + revocation_endpoint: 'http://localhost:9443/am/oauth2/realms/root/token/revoke', + claims_parameter_supported: false, + request_parameter_supported: true, + request_uri_parameter_supported: false, + require_pushed_authorization_requests: false, + scopes_supported: ['openid', 'profile', 'email', 'address', 'phone', 'offline_access'], + response_types_supported: [ + 'code', + 'id_token', + 'token id_token', + 'code id_token', + 'code token', + 'code token id_token', + ], + response_modes_supported: ['pi.flow', 'query', 'fragment', 'form_post'], + grant_types_supported: [ + 'authorization_code', + 'implicit', + 'client_credentials', + 'refresh_token', + 'urn:ietf:params:oauth:grant-type:device_code', + ], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['RS256'], + userinfo_signing_alg_values_supported: ['none'], + request_object_signing_alg_values_supported: [ + 'none', + 'HS256', + 'HS384', + 'HS512', + 'RS256', + 'RS384', + 'RS512', + ], + token_endpoint_auth_methods_supported: [ + 'client_secret_basic', + 'client_secret_post', + 'client_secret_jwt', + 'private_key_jwt', + ], + token_endpoint_auth_signing_alg_values_supported: [ + 'HS256', + 'HS384', + 'HS512', + 'RS256', + 'RS384', + 'RS512', + ], + claim_types_supported: ['normal'], + claims_supported: [ + 'sub', + 'iss', + 'auth_time', + 'acr', + 'name', + 'given_name', + 'family_name', + 'middle_name', + 'preferred_username', + 'profile', + 'picture', + 'zoneinfo', + 'phone_number', + 'updated_at', + 'address', + 'email', + 'locale', + ], + code_challenge_methods_supported: ['plain', 'S256'], +}; + +export const MetadataMarketPlaceInitialize = { + authId: 'foo', + callbacks: [ + { + type: 'MetadataCallback', + output: [ + { + name: 'data', + value: { + _type: 'PingOneProtect', + _action: 'protect_initialize', + envId: 'some_id', + consoleLogEnabled: true, + deviceAttributesToIgnore: [], + customHost: '', + lazyMetadata: true, + behavioralDataCollection: true, + disableHub: true, + deviceKeyRsyncIntervals: 10, + enableTrust: true, + disableTags: true, + }, + }, + ], + }, + { + type: 'HiddenValueCallback', + output: [ + { + name: 'value', + value: '', + }, + { + name: 'id', + value: 'clientError', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }, + ], +}; + +export const MetadataMarketPlacePingOneEvaluation = { + authId: 'foo', + callbacks: [ + { + type: 'MetadataCallback', + output: [ + { + name: 'data', + value: { + _type: 'PingOneProtect', + _action: 'protect_risk_evaluation', + envId: 'some_id', + pauseBehavioralData: true, + }, + }, + ], + }, + { + type: 'HiddenValueCallback', + output: [ + { + name: 'value', + value: '', + }, + { + name: 'id', + value: 'pingone_risk_evaluation_signals', + }, + ], + input: [ + { + name: 'IDToken1', + value: 'pingone_risk_evaluation_signals', + }, + ], + }, + { + type: 'HiddenValueCallback', + output: [ + { + name: 'value', + value: '', + }, + { + name: 'id', + value: 'clientError', + }, + ], + input: [ + { + name: 'IDToken1', + value: '', + }, + ], + }, + ], +}; + +export const recaptchaEnterpriseCallback = { + authId: 'foo', + callbacks: [ + { + type: '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: '', + }, + ], + }, + ], +}; diff --git a/e2e/am-mock-api/src/app/routes.auth.js b/e2e/am-mock-api/src/app/routes.auth.js new file mode 100644 index 000000000..3d13208ea --- /dev/null +++ b/e2e/am-mock-api/src/app/routes.auth.js @@ -0,0 +1,644 @@ +/* + * @forgerock/javascript-sdk + * + * routes.auth.js + * + * 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 { v4 } from 'uuid'; +import { authPaths } from './constants.js'; +import { AM_URL, USERS } from './env.config.js'; +import { + oauthTokens, + authFail, + authSuccess, + emailSuspend, + idpChoiceCallback, + initialBasicLogin, + initialLoginWithEmailResponse, + initialMiscCallbacks, + initialPlatformLogin, + passwordCallback, + choiceCallback, + messageCallback, + noSessionSuccess, + pollingCallback, + pingProtectEvaluate, + pingProtectInitialize, + redirectCallback, + redirectCallbackSaml, + requestDeviceProfile, + secondFactorCallback, + secondFactorChoiceCallback, + selectIdPCallback, + userInfo, + oauthTokensExpiringSoon, + oauthTokensExpired, + nameCallback, + redirectCallbackFailureSaml, + textInputCallback, + treeAuthz, + txnAuthz, + otpQRCodeCallbacks, + wellKnownForgeRock, + recaptchaEnterpriseCallback, + MetadataMarketPlaceInitialize, + MetadataMarketPlacePingOneEvaluation, + newPiWellKnown, +} from './responses.js'; +import initialRegResponse from './response.registration.js'; +import wait from './wait.js'; + +console.log(`Your user password from 'env.config' file: ${USERS[0].pw}`); + +export const baz = { + canWithdraw: false, +}; + +export default function (app) { + app.post(authPaths.authenticate, wait, async (req, res) => { + if (!req.body.callbacks) { + if (req.query.authIndexType === 'composite_advice') { + if (req.query.authIndexValue.includes('TransactionConditionAdvice')) { + res.json(txnAuthz); + } else if (req.query.authIndexValue.includes('AuthenticateToServiceConditionAdvice')) { + res.json(treeAuthz); + } + } else if (req.query.authIndexValue === 'MiscCallbacks') { + res.json(initialMiscCallbacks); + } else if (req.query.authIndexValue === 'PlatformUsernamePasswordTest') { + res.json(initialPlatformLogin); + } else if (req.query.authIndexValue === 'Registration') { + res.json(initialRegResponse); + } else if (req.query.authIndexValue === 'LoginWithEmail') { + if (typeof req.query.suspendedId === 'string' && req.query.suspendedId.length) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + res.json(initialLoginWithEmailResponse); + } + } else if ( + req.query.authIndexValue === 'SAMLTest' || + req.query.authIndexValue === 'SAMLFailure' + ) { + res.json(nameCallback); + } else if (req.query.authIndexValue === 'TEST_LoginPingProtect') { + res.json(pingProtectInitialize); + } else if (req.query.authIndexValue === 'IDMSocialLogin') { + res.json(selectIdPCallback); + } else if (req.query.authIndexValue === 'TEST_MetadataMarketPlace') { + res.json(MetadataMarketPlaceInitialize); + } else if (req.query.authIndexValue === 'AMSocialLogin') { + res.json(idpChoiceCallback); + } else if (req.query.authIndexValue === 'RecaptchaEnterprise') { + res.json(initialBasicLogin); + } else { + if (req.path.includes('middleware')) { + if ( + req.query['start-authenticate-middleware'] === 'start-authentication' && + req.headers['x-start-authenticate-middleware'] === 'start-authentication' && + !req.headers['x-logout-middleware'] && + !req.query['logout-middleware'] + ) { + res.json(initialBasicLogin); + } else { + res.status(406).send('Middleware additions are missing.'); + } + } else { + res.json(initialBasicLogin); + } + } + } else if (req.query.authIndexValue === 'LoginWithEmail') { + res.json(emailSuspend); + } else if (req.query.authIndexValue === 'RecaptchaEnterprise') { + console.log(req.body.callbacks); + if (req.body.callbacks[0].type === 'NameCallback') { + const [username, password] = req.body.callbacks; + if (username && username.type === 'NameCallback' && username.input[0].value === 'demo') { + if ( + password && + password.type === 'PasswordCallback' && + password.input[0].value === 'Password' + ) { + res.json(recaptchaEnterpriseCallback); + } + } + } else { + const [captcha] = req.body.callbacks; + if (captcha && captcha.input[0].value === '123') { + res.json(authSuccess); + } + } + } else if (req.query.authIndexValue === 'TEST_MetadataMarketPlace') { + if (req.body.callbacks.find((cb) => cb.type === 'MetadataCallback')) { + const metadataCb = req.body.callbacks.find((cb) => cb.type === 'MetadataCallback'); + const action = metadataCb.output[0].value._action; + console.log('the action', action); + if (action === 'protect_initialize') { + if (req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback')) { + const hiddenCb = req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback'); + if (hiddenCb.input[0].value === 'we had an error') { + return res.json(authFail); + } + return res.json(MetadataMarketPlacePingOneEvaluation); + } + } + if (action === 'protect_risk_evaluation') { + if (req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback')) { + const hiddenCb = req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback'); + if (hiddenCb.input[0].value === 'we had an error') { + return res.json(authFail); + } + return res.json(authSuccess); + } + } + } else { + if (req.body.callbacks.find((cb) => cb.type === 'PingOneEvaluationCallback')) { + const cb = req.body.callbacks.find((cb) => cb.type === 'PingOneEvaluationCallback'); + if (cb.input[0].value === 'the value to set') { + return res.json(authSuccess); + } else { + return res.json(authFail); + } + } + } + return res.json(MetadataMarketPlacePingOneEvaluation); + } else if (req.query.authIndexValue === 'QRCodeTest') { + // If QR Code callbacks are being returned, return success + if (req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback')) { + return res.json(authSuccess); + } + // Client is returning callbacks from username password, so return QR Code callbacks + res.json(otpQRCodeCallbacks); + } else if (req.query.authIndexValue === 'SAMLTestFailure') { + if (req.body.callbacks.find((cb) => cb.type === 'RedirectCallback')) { + if ( + req.query.error === 'true' && + req.query.errorCode === '401' && + req.body.errorMessage === 'errorSaml' + ) { + res.json(authFail); + } else { + res.json(authSuccess); + } + } else { + res.json(redirectCallbackFailureSaml); + } + } else if (req.query.authIndexValue === 'SAMLTest') { + if (req.body.callbacks.find((cb) => cb.type === 'RedirectCallback')) { + if ( + req.query.RelayState === 'https://forgerock.com' && + req.query.responsekey === '885cae87-f88b-4d75-a0fd-0ae1400b766f' && + req.body.authId === 'foo' + ) { + res.json(authSuccess); + } else { + res.json(authFail); + } + } else { + res.json(redirectCallbackSaml); + } + } else if (req.query.authIndexValue === 'MiscCallbacks') { + if (req.body.callbacks.find((cb) => cb.type === 'NameCallback')) { + const cb = req.body.callbacks.find((cb) => cb.type === 'NameCallback'); + if (cb.input[0].value !== USERS[0].un) { + res.json(authFail); + } else { + res.json(textInputCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'TextInputCallback')) { + const cb = req.body.callbacks.find((cb) => cb.type === 'TextInputCallback'); + if (cb.input[0].value !== 'Text Input String') { + res.json(authFail); + } else { + res.json(passwordCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'PasswordCallback')) { + const cb = req.body.callbacks.find((cb) => cb.type === 'PasswordCallback'); + if (cb.input[0].value !== USERS[0].pw) { + res.json(authFail); + } else { + res.json(choiceCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'ChoiceCallback')) { + const cb = req.body.callbacks.find((cb) => cb.type === 'ChoiceCallback'); + if (cb.input[0].value !== 1) { + res.json(authFail); + } else { + res.json(messageCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'ConfirmationCallback')) { + const cb = req.body.callbacks.find((cb) => cb.type === 'ConfirmationCallback'); + if (cb.input[0].value !== 0) { + res.json(authFail); + } else { + res.json(pollingCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'PollingCallback')) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + res.json(authFail); + } + } else if (req.query.authIndexValue === 'PlatformUsernamePasswordTest') { + const pwCb = req.body.callbacks.find((cb) => cb.type === 'ValidatedCreatePasswordCallback'); + // If validate only, return callbacks + if (pwCb.input[1].value) { + res.json(initialPlatformLogin); + } else if (pwCb.input[0].value !== USERS[0].pw) { + res.status(401).json(authFail); + } else { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } + } else if (req.query.authIndexValue === 'Registration') { + const un = req.body.callbacks.find((cb) => cb.type === 'ValidatedCreateUsernameCallback'); + const [fn, ln, em] = req.body.callbacks.filter( + (cb) => cb.type === 'StringAttributeInputCallback', + ); + // const age = req.body.callbacks.find((cb) => cb.type === 'NumberAttributeInputCallback'); + const [mktg, update] = req.body.callbacks.filter( + (cb) => cb.type === 'BooleanAttributeInputCallback', + ); + const pw = req.body.callbacks.find((cb) => cb.type === 'ValidatedCreatePasswordCallback'); + const [kba1, kba2] = req.body.callbacks.filter((cb) => cb.type === 'KbaCreateCallback'); + const terms = req.body.callbacks.find((cb) => cb.type === 'TermsAndConditionsCallback'); + if ( + un.input[0].value.length && + fn.input[0].value === 'Sally' && + ln.input[0].value === 'Tester' && + // age.input[0].value === 40 && + em.input[0].value.length && + mktg.input[0].value === false && + update.input[0].value === false && + pw.input[0].value === USERS[0].pw && + kba1.input[0].value === 'What is your favorite color?' && + kba1.input[1].value === 'Red' && + kba2.input[0].value === 'Who was your first employer?' && + kba2.input[1].value === 'AAA Engineering' && + terms.input[0].value === true + ) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + res.status(401).json(authFail); + } + } else if (req.query.authIndexValue === 'SecondFactor') { + if (req.body.callbacks.find((cb) => cb.type === 'NameCallback')) { + res.json(secondFactorChoiceCallback); + } else if (req.body.callbacks.find((cb) => cb.type === 'ChoiceCallback')) { + res.json(secondFactorCallback); + } else if (req.body.callbacks.find((cb) => cb.type === 'PasswordCallback')) { + const pwCb = req.body.callbacks.find((cb) => cb.type === 'PasswordCallback'); + if (pwCb.input[0].value !== 'abc123') { + res.status(401).json(authFail); + } else { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } + } + } else if (req.query.authIndexValue === 'IDMSocialLogin') { + if (req.body.callbacks.find((cb) => cb.type === 'SelectIdPCallback')) { + const idPCb = req.body.callbacks.find((cb) => cb.type === 'SelectIdPCallback'); + if (idPCb.input[0].value !== 'google') { + res.status(401).json(authFail); + } else { + res.json(redirectCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'RedirectCallback')) { + if (req.body.authId && req.query.code && req.query.state) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + res.status(401).json(authFail); + } + } + } else if (req.query.authIndexValue === 'AMSocialLogin') { + if (req.body.callbacks.find((cb) => cb.type === 'ChoiceCallback')) { + const idPCb = req.body.callbacks.find((cb) => cb.type === 'ChoiceCallback'); + if (idPCb.input[0].value !== 0) { + res.status(401).json(authFail); + } else { + res.json(redirectCallback); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'RedirectCallback')) { + if (req.body.authId && req.query.code && req.query.state) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + res.status(401).json(authFail); + } + } + } else if (req.query.authIndexValue === 'TEST_LoginPingProtect') { + const protectInitCb = req.body.callbacks.find( + (cb) => cb.type === 'PingOneProtectInitializeCallback', + ); + const usernameCb = req.body.callbacks.find((cb) => cb.type === 'NameCallback'); + const protectEvalCb = req.body.callbacks.find( + (cb) => cb.type === 'PingOneProtectEvaluationCallback', + ); + if (protectInitCb) { + res.json(initialBasicLogin); + } else if (usernameCb && usernameCb.input[0].value) { + res.json(pingProtectEvaluate); + } else if (protectEvalCb && protectEvalCb.input[0].value) { + res.json(authSuccess); + } else { + res.status(401).json(authFail); + } + } else if (req.body.callbacks.find((cb) => cb.type === 'PasswordCallback')) { + const pwCb = req.body.callbacks.find((cb) => cb.type === 'PasswordCallback'); + if (pwCb.input[0].value !== USERS[0].pw) { + res.status(401).json(authFail); + } else { + if (req.query.authIndexValue === 'DeviceProfileCallbackTest') { + res.json(requestDeviceProfile); + } else { + if ( + req.body.stage === 'TransactionAuthorization' || + req.body.stage === 'TreeBasedAuthorization' + ) { + baz.canWithdraw = true; + } + if (req.path.includes('middleware')) { + if ( + req.query['authenticate-middleware'] === 'authentication' && + req.headers['x-authenticate-middleware'] === 'authentication' && + !req.headers['x-logout-middleware'] && + !req.query['logout-middleware'] + ) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + res.status(406).send('Middleware additions are missing.'); + } + } else { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(req.query.noSession === 'true' ? noSessionSuccess : authSuccess); + } + } + } + } else if (req.body.callbacks.find((cb) => cb.type === 'DeviceProfileCallback')) { + const deviceCb = req.body.callbacks.find((cb) => cb.type === 'DeviceProfileCallback') || {}; + const inputArr = deviceCb.input || []; + const input = inputArr[0] || {}; + const value = JSON.parse(input.value); + const location = value.location || {}; + const metadata = value.metadata || {}; + // location is not allowed in some browser automation + // const location = value.location || {}; + + // We just need property existence to ensure profile is generated + // We don't care about values since they are unique per browser + if ( + location && + location.latitude && + location.longitude && + metadata.browser && + metadata.browser.userAgent && + metadata.platform && + metadata.platform.deviceName && + metadata.platform.fonts && + metadata.platform.fonts.length > 0 && + metadata.platform.timezone && + value.identifier && + value.identifier.length > 0 + ) { + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain: 'localhost' }); + res.json(authSuccess); + } else { + // Just failing the auth for testing, but in reality, + // an additional auth callback would be sent, like OTP + res.json(authFail); + } + } + }); + + app.post(authPaths.tokenExchange, wait, async (req, res) => { + // eslint-disable-next-line + const access_token = v4(); + const refresh_token = v4(); + // eslint-disable-next-line + const tokens = { ...oauthTokens, access_token, refresh_token }; + + if (req.path.includes('middleware')) { + if ( + req.query['exchange-token-middleware'] === 'exchange-token' && + req.headers['x-exchange-token-middleware'] === 'exchange-token' && + !req.headers['x-logout-middleware'] && + !req.query['logout-middleware'] + ) { + res.json(tokens); + } else { + res.status(406).send('Middleware header is missing.'); + } + } else if (req.path.includes('tokens-expiring-soon')) { + const tokensExpiringSoon = { ...oauthTokensExpiringSoon, access_token, refresh_token }; + res.json(tokensExpiringSoon); + } else if (req.path.includes('tokens-expired')) { + const tokensExpired = { ...oauthTokensExpired, access_token, refresh_token }; + res.json(tokensExpired); + } else { + res.json(tokens); + } + }); + + app.get(authPaths.accounts, wait, async (req, res) => { + if (req.url.includes('SAMLFailure')) { + const referrer = new URL(req.get('Referer')); + const additionalQueryParams = 'error=true&errorCode=401&errorMessage=errorSaml'; + const redirectUrl = `${referrer.href}${ + referrer.href.includes('?') ? '&' : '?' + }${additionalQueryParams}`; + return res.redirect(redirectUrl); + } else if (req.url.includes('SAMLTest')) { + const referrer = new URL(req.get('Referer')); + const additionalQueryParams = + 'responsekey=885cae87-f88b-4d75-a0fd-0ae1400b766f&RelayState=https://forgerock.com'; + const redirectUrl = `${referrer.href}${ + referrer.href.includes('?') ? '&' : '?' + }${additionalQueryParams}`; + return res.redirect(redirectUrl); + } else { + const referrer = new URL(req.get('Referer')); + const additionalQueryParams = + // eslint-disable-next-line max-len + 'state=rtu8pz65dbg6baw985d532myfbbnf5v&code=4%2F0AY0e-g5vHGhzfggdAuIofxnblW-iR1Y30G5lN5RvbrU8Zv5ZmtUVheTzSX7YMJF_usbzUA&scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&authuser=0&hd=forgerock.com&prompt=none'; + const redirectUrl = `${referrer.href}${ + referrer.href.includes('?') ? '&' : '?' + }${additionalQueryParams}`; + res.redirect(redirectUrl); + } + }); + + app.get(authPaths.authorize, wait, async (req, res) => { + const loginUrl = new URL(`${req.protocol}://${req.headers.host}/login`); + loginUrl.searchParams.set('client_id', req.query.client_id); + loginUrl.searchParams.set('acr_values', req.query.acr_values); + loginUrl.searchParams.set('redirect_uri', req.query.redirect_uri); + loginUrl.searchParams.set('state', req.query.state); + + // Detect if Central Login to enforce ACR value presence + if ( + req.query.client_id === 'CentralLoginOAuthClient' && + req.query.acr_values !== 'SpecificTree' + ) { + return res.status(400).json({ message: 'acr_values did not match "SpecificTree"' }); + } + if (req.path.includes('middleware-modern')) { + if ( + req.query['authorize-middleware'] === 'authorization' && + req.headers['x-authorize-middleware'] === 'authorization' && + !req.query['logout-middleware'] && + !req.headers['x-logout-middleware'] + ) { + res.redirect(loginUrl); + } else { + res.status(406).send('Middleware additions are missing.'); + } + } else if (req.path.includes('middleware')) { + if ( + req.query['authorize-middleware'] === 'authorization' && + !req.query['logout-middleware'] + ) { + res.redirect(loginUrl); + } else { + res.status(406).send('Middleware additions are missing.'); + } + } else { + if (req.cookies.iPlanetDirectoryPro) { + const redirectUrl = new URL(`${req.query.redirect_uri}`); + + console.log(`Request URL: ${req.query.client_id}`); + + redirectUrl.searchParams.set('client_id', req.query.client_id); + redirectUrl.searchParams.set('code', 'foo'); + redirectUrl.searchParams.set('iss', `${AM_URL}/oauth2`); + redirectUrl.searchParams.set('state', req.query.state); + + res.redirect(redirectUrl); + } else if ( + req.cookies.redirected === 'true' || + req.query['acr_values'] === 'skipBackgroundRequest' + ) { + res.redirect(loginUrl); + } else { + res.cookie('redirected', 'true'); + + const interactionNeeded = 'The request requires some interaction that is not allowed.'; + const redirectErrorUrl = new URL( + `${req.query.redirect_uri}?error_description=${interactionNeeded}`, + ); + + res.redirect(redirectErrorUrl); + } + } + }); + + app.get('/login', async (req, res) => { + const domain = req.url.includes('localhost') ? 'localhost' : 'example.com'; + + res.clearCookie('redirected'); + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain, sameSite: 'none', secure: true }); + + const url = new URL(`${req.protocol}://${req.headers.host}${authPaths.authorize[1]}`); + url.searchParams.set('client_id', req.query.client_id); + url.searchParams.set('acr_values', req.query.acr_values); + url.searchParams.set('redirect_uri', req.query.redirect_uri); + url.searchParams.set('state', req.query.state); + + res.redirect(url); + }); + + app.get(authPaths.userInfo, wait, async (req, res) => { + if (req.headers['authorization'] && req.path.includes('middleware')) { + if ( + req.query['userinfo-middleware'] === 'userinfo' && + req.headers['x-userinfo-middleware'] === 'userinfo' && + !req.headers['x-logout-middleware'] && + !req.query['logout-middleware'] + ) { + res.json(userInfo); + } else { + res.status(406).send('Middleware additions are missing.'); + } + } else if (req.headers['authorization']) { + res.json(userInfo); + } else { + res.status(401).send('Unauthorized'); + } + }); + + app.get(authPaths.endSession, wait, async (req, res) => { + if (req.path.includes('middleware')) { + if ( + req.query['end-session-middleware'] === 'end-session' && + req.headers['x-end-session-middleware'] === 'end-session' && + !req.headers['x-logout-middleware'] && + !req.query['logout-middleware'] + ) { + res.status(204).send(); + } else { + res.status(406).send('Middleware additions are missing missing.'); + } + } else { + res.status(204).send(); + } + }); + + app.post(authPaths.revoke, wait, async (req, res) => { + if (req.path.includes('middleware')) { + if ( + req.query['revoke-token-middleware'] === 'revoke-token' && + req.headers['x-revoke-token-middleware'] === 'revoke-token' && + !req.headers['x-logout-middleware'] && + !req.query['logout-middleware'] + ) { + res.status(200).send(); + } else { + res.status(406).send('Middleware header is missing.'); + } + } else { + res.status(200).send(); + } + }); + + app.all(authPaths.htmlAuthenticate, wait, async (req, res) => { + res.type('html'); + res.status(200).send(''); + }); + + app.post(authPaths.sessions, wait, async (req, res) => { + if (req.query._action === 'logout') { + if (req.path.includes('middleware')) { + if ( + req.query['logout-middleware'] === 'logout' && + req.headers['x-logout-middleware'] === 'logout' && + !req.headers['x-auth-middleware'] && + !req.query['auth-middleware'] + ) { + res.clearCookie('iPlanetDirectoryPro', { domain: 'localhost', path: '/' }); + res.status(204).send(); + } else { + res.status(406).send('Middleware header is missing.'); + } + } else { + res.clearCookie('iPlanetDirectoryPro', { domain: 'localhost', path: '/' }); + res.status(204).send(); + } + } + }); + + app.get('/callback', (req, res) => res.status(200).send('ok')); + + app.get('/am/.well-known/oidc-configuration', (req, res) => { + res.send(wellKnownForgeRock); + }); + + app.get('/as/.well-known/new-oidc-configuration', (req, res) => { + res.send(newPiWellKnown); + }); +} diff --git a/e2e/am-mock-api/src/app/routes.resource.js b/e2e/am-mock-api/src/app/routes.resource.js new file mode 100644 index 000000000..d10dc278a --- /dev/null +++ b/e2e/am-mock-api/src/app/routes.resource.js @@ -0,0 +1,191 @@ +/* + * @forgerock/javascript-sdk + * + * routes.resource.js + * + * 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 { env } from 'process'; +import request from 'superagent'; +import { session } from './app.auth.js'; +import { AM_URL, AM_PORT, FORGEOPS, REALM_PATH } from './env.config.js'; +import { + authByTreeResponse, + authByTxnResponse, + createTxnStepUpHeader, + createTreeStepUpHeader, + createTxnStepUpUrl, + createTreeStepUpUrl, +} from './responses.js'; +import { baz } from './routes.auth.js'; +import wait from './wait.js'; + +async function authorization(req, res, next) { + if (env.NODE_ENV === 'LIVE' && req.hostname !== FORGEOPS) { + const fullURL = `${req.protocol}://${req.host}:${AM_PORT}${req.url}`; + let realms; + const body = { + application: req.path.includes('authz-by-txn') ? 'TxnBasedPolicy' : 'TreeBasedPolicy', + resources: [fullURL], + subject: { + ssoToken: req.headers['x-idtoken'] || req.cookies.iPlanetDirectoryPro, + }, + }; + if (req.headers['x-txid']) { + body.environment = { + TxId: [req.headers['x-txid']], + }; + } + if (REALM_PATH !== 'root') { + realms = `realms/root/realms/${REALM_PATH}`; + } else { + realms = 'realms/root'; + } + const response = await request + // eslint-disable-next-line + .post(`${AM_URL}/json/${realms}/policies/?_action=evaluate`) + // .key(key) + // .cert(cert) + .set('Content-Type', 'application/json') + .set('Accept-API-Version', 'resource=2.1') + .set('iPlanetDirectoryPro', session.tokenId) + .send(body); + + req.access = response.body[0] || {}; + next(); + } else { + next(); + } +} + +export default function (app) { + // Passthrough route that enforces authentication + app.all('/resource/*', async (req, res, next) => { + if (env.NODE_ENV === 'LIVE' && req.hostname === FORGEOPS) { + // Only enforce authentication if IG is not used + // In other words, the call comes directly from app + let response; + if (req.headers.authorization) { + // Using OAuth + const authHeaderArr = req.headers.authorization.split(' '); + response = await request + .post(`${AM_URL}/oauth2/introspect`) + .set('Content-Type', 'application/json') + .set('iPlanetDirectoryPro', session.tokenId) + .set('Accept-API-Version', 'resource=1.2') + .send({ token: authHeaderArr[1] }); + } else { + // Using SSO + response = await request + .post(`${AM_URL}/json/sessions/?_action=validate`) + .set('Content-Type', 'application/json') + .set('iPlanetDirectoryPro', session.tokenId) + .set('Accept-API-Version', 'resource=2.1, protocol=1.0') + .send({ tokenId: req.cookies.iPlanetDirectoryPro }); + } + + if (response.body.active || response.body.valid) { + next(); + } else { + res.status(401).send(); + } + } else { + // Call came from a proxy, so proxy (e.g. IG) will enforce auth + next(); + } + }); + + app.get('/resource/reflect-authz-header', wait, authorization, async (req, res) => { + // Respond with the value of the authorization header to assist in testing http client + res.json({ message: req.headers['authorization'] }); + }); + + app.get('/resource/ig/authz-by-txn', wait, authorization, async (req, res) => { + if (req.hostname === FORGEOPS) { + // Calls are coming from IG, so Auth is already enforced + res.json({ message: 'Successfully retrieved resource!' }); + } else { + // Calls are coming directly from app, so let's mocks IG's behavior + if ( + req.cookies.iPlanetDirectoryPro === 'abcd1234' && + baz.canWithdraw && + req.query._txid === authByTxnResponse.advices.TransactionConditionAdvice[0] + ) { + baz.canWithdraw = false; + res.json({ message: 'Successfully retrieved resource!' }); + } else { + console.log(req.headers['x-authenticate-response']); + if ( + req.headers['x-authenticate-response'] && + req.headers['x-authenticate-response'] === 'header' && + req.headers.referer.includes('json') + ) { + res.setHeader('WWW-Authenticate', createTxnStepUpHeader(req.headers.referer)); + res.send(401, null); + } else { + res.redirect(307, createTxnStepUpUrl(req.headers.referer)); + } + } + } + }); + + app.get('/resource/ig/authz-by-tree', wait, authorization, async (req, res) => { + if (req.hostname === FORGEOPS) { + // Calls are coming from IG, so Auth is already enforced + res.json({ message: 'Successfully retrieved resource!' }); + } else { + // Calls are coming directly from app, so let's mocks IG's behavior + if (req.cookies.iPlanetDirectoryPro === 'abcd1234' && baz.canWithdraw) { + baz.canWithdraw = false; + res.json({ message: 'Successfully retrieved resource!' }); + } else { + if ( + req.headers['x-authenticate-response'] && + req.headers['x-authenticate-response'] === 'header' && + req.headers.referer.includes('json') + ) { + res.setHeader('WWW-Authenticate', createTreeStepUpHeader(req.headers.referer)); + res.send(401, null); + } else { + res.redirect(307, createTreeStepUpUrl(req.headers.referer)); + } + } + } + }); + + app.get('/resource/rest/*', wait, authorization, async (req, res) => { + if (env.NODE_ENV === 'live') { + if (req.access.actions && req.access.actions.GET) { + res.json({ message: 'Successfully retrieved resource!' }); + } else if ( + req.access.advices && + (req.access.advices.TransactionConditionAdvice || + req.access.advices.AuthenticateToServiceConditionAdvice) + ) { + res.json(req.access); + } else { + res.status(401).send(); + } + } else { + if ( + req.cookies.iPlanetDirectoryPro === 'abcd1234' && + baz.canWithdraw && + (req.headers['x-txid'] === authByTxnResponse.advices.TransactionConditionAdvice[0] || + req.headers['x-tree'] === + authByTreeResponse.advices.AuthenticateToServiceConditionAdvice[0]) + ) { + baz.canWithdraw = false; + res.json({ message: 'Successfully retrieved resource!' }); + } else { + if (req.path.includes('authz-by-txn')) { + res.json(authByTxnResponse); + } else if (req.path.includes('authz-by-tree')) { + res.json(authByTreeResponse); + } + } + } + }); +} diff --git a/e2e/am-mock-api/src/app/wait.js b/e2e/am-mock-api/src/app/wait.js new file mode 100644 index 000000000..3738de1fa --- /dev/null +++ b/e2e/am-mock-api/src/app/wait.js @@ -0,0 +1,15 @@ +/* + * @forgerock/javascript-sdk + * + * wait.js + * + * 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 delay = 0; + +export default function wait(req, res, next) { + setTimeout(() => next(), delay); +} diff --git a/e2e/am-mock-api/src/assets/.gitkeep b/e2e/am-mock-api/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/e2e/am-mock-api/src/environments/environment.prod.ts b/e2e/am-mock-api/src/environments/environment.prod.ts new file mode 100644 index 000000000..c9669790b --- /dev/null +++ b/e2e/am-mock-api/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/e2e/am-mock-api/src/environments/environment.ts b/e2e/am-mock-api/src/environments/environment.ts new file mode 100644 index 000000000..a24f6ba2c --- /dev/null +++ b/e2e/am-mock-api/src/environments/environment.ts @@ -0,0 +1,9 @@ +export const environment = { + AM_URL: 'https://openam-crbrl-01.forgeblocks.com/am/', + REALM_PATH: 'alpha', + WEB_OAUTH_CLIENT: 'WebOAuthClient', + JOURNEY_LOGIN: 'UsernamelessWebAuthn', + JOURNEY_REGISTER: 'Registration', + API_URL: 'http://localhost:9443', + production: 'development', +}; diff --git a/e2e/am-mock-api/src/index.js b/e2e/am-mock-api/src/index.js new file mode 100644 index 000000000..316259834 --- /dev/null +++ b/e2e/am-mock-api/src/index.js @@ -0,0 +1,57 @@ +/* + * @forgerock/javascript-sdk + * + * index.js + * + * 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 * as dns from 'dns'; +import cors from 'cors'; +import express from 'express'; +import cookieParser from 'cookie-parser'; +import { createServer } from 'http'; +import { env } from 'process'; +import path from 'path'; +import { authorizeApp } from './app/app.auth.js'; +import { MOCK_PORT } from './app/env.config.js'; +import authRoutes from './app/routes.auth.js'; +import resourceRoutes from './app/routes.resource.js'; + +dns.setDefaultResultOrder('ipv4first'); + +const app = express(); + +app.use(express.json()); +app.use('/am/XUI/images', express.static(path.join(__dirname, 'public'))); +app.use(cookieParser()); +app.use( + cors({ + exposedHeaders: ['www-authenticate'], + credentials: true, + origin: function (origin, callback) { + return callback(null, true); + }, + }), +); +app.use((req, res, next) => { + console.log(`${req.method} ${req.path}`); + next(); +}); + +if (env.NODE_ENV === 'LIVE') { + authorizeApp({ + un: '9190fcce-d6d7-4473-9449-412f281f9bc6', + pw: '7fh9sj7*NP$%F6978', + }); +} + +authRoutes(app); +resourceRoutes(app); + +app.get('/healthcheck', (req, res) => res.status(200).send('ok')); + +env['NODE_TLS_REJECT_UNAUTHORIZED'] = 0; +createServer(app).listen(MOCK_PORT); +console.log(`Listening to HTTP on secure port: ${MOCK_PORT}`); diff --git a/e2e/am-mock-api/tsconfig.app.json b/e2e/am-mock-api/tsconfig.app.json new file mode 100644 index 000000000..a3b8102e3 --- /dev/null +++ b/e2e/am-mock-api/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES6", + "allowJs": true, + "types": ["node", "express"] + }, + "exclude": ["**/*.spec.ts", "**/*.test.ts", "dist"], + "include": ["**/*.ts", "**/*.js"] +} diff --git a/e2e/am-mock-api/tsconfig.json b/e2e/am-mock-api/tsconfig.json new file mode 100644 index 000000000..63dbe35fb --- /dev/null +++ b/e2e/am-mock-api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/e2e/am-mock-api/tsconfig.spec.json b/e2e/am-mock-api/tsconfig.spec.json new file mode 100644 index 000000000..c5b810b20 --- /dev/null +++ b/e2e/am-mock-api/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["node"] + }, + "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] +} diff --git a/e2e/davinci-suites/src/phone-number-field.test.ts b/e2e/davinci-suites/src/phone-number-field.test.ts index 77617e2b7..b4d9070ca 100644 --- a/e2e/davinci-suites/src/phone-number-field.test.ts +++ b/e2e/davinci-suites/src/phone-number-field.test.ts @@ -106,7 +106,9 @@ test.describe('Device registration tests', () => { await expect(page.getByText('SDK Automation - Enter Phone Number')).toBeVisible(); await page.getByRole('textbox', { name: 'Enter Phone Number' }).fill('3035550100'); await page.getByRole('button', { name: 'Submit' }).click(); - await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible(); + await expect( + async () => await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible(), + ).toPass(); await page.getByRole('button', { name: 'Continue' }).click(); }); }); diff --git a/e2e/journey-app/main.ts b/e2e/journey-app/main.ts index 30bb36b8d..45ad9979b 100644 --- a/e2e/journey-app/main.ts +++ b/e2e/journey-app/main.ts @@ -18,6 +18,7 @@ const searchParams = new URLSearchParams(qs); const config = serverConfigs[searchParams.get('clientId') || 'basic']; +const tree = searchParams.get('tree') ?? 'UsernamePassword'; let requestMiddleware: RequestMiddleware[] = []; if (searchParams.get('middleware') === 'true') { @@ -48,16 +49,13 @@ if (searchParams.get('middleware') === 'true') { } (async () => { - const journeyClient = await journey({ config, requestMiddleware }); + const journeyClient = await journey({ config: config, requestMiddleware }); const errorEl = document.getElementById('error') as HTMLDivElement; const formEl = document.getElementById('form') as HTMLFormElement; const journeyEl = document.getElementById('journey') as HTMLDivElement; - let step = await journeyClient.start({ - journey: searchParams.get('journey') || '', - query: { noSession: searchParams.get('no-session') || 'false' }, - }); + let step = await journeyClient.start({ journey: tree }); function renderComplete() { if (step?.type !== 'LoginSuccess') { @@ -81,7 +79,7 @@ if (searchParams.get('middleware') === 'true') { console.log('Logout successful'); - step = await journeyClient.start(); + step = await journeyClient.start({ journey: tree }); renderForm(); }); diff --git a/e2e/journey-app/server-configs.ts b/e2e/journey-app/server-configs.ts index c85ac8033..4bc08a1b1 100644 --- a/e2e/journey-app/server-configs.ts +++ b/e2e/journey-app/server-configs.ts @@ -9,8 +9,14 @@ import type { JourneyClientConfig } from '@forgerock/journey-client/types'; export const serverConfigs: Record = { basic: { serverConfig: { - baseUrl: 'https://openam-sdks.forgeblocks.com/am/', + baseUrl: 'http://localhost:9443/am', }, - realmPath: '/alpha', + realmPath: 'root', + }, + tenant: { + serverConfig: { + baseUrl: 'https://openam-sdks.forgeblocks.com/am', + }, + realmPath: 'alpha', }, }; diff --git a/e2e/journey-suites/playwright.config.ts b/e2e/journey-suites/playwright.config.ts index bbdad2cf7..85229ca84 100644 --- a/e2e/journey-suites/playwright.config.ts +++ b/e2e/journey-suites/playwright.config.ts @@ -27,9 +27,6 @@ const config: PlaywrightTestConfig = { process.env.CI == 'false' ? { command: 'pnpm watch @forgerock/journey-app', - port: 5829, - ignoreHTTPSErrors: true, - reuseExistingServer: !process.env.CI, cwd: workspaceRoot, } : undefined, @@ -40,6 +37,13 @@ const config: PlaywrightTestConfig = { reuseExistingServer: !process.env.CI, cwd: workspaceRoot, }, + { + command: 'pnpm nx serve am-mock-api', + port: 9443, + ignoreHTTPSErrors: true, + reuseExistingServer: !process.env.CI, + cwd: workspaceRoot, + }, ].filter(Boolean), }; diff --git a/e2e/journey-suites/src/choice-confirm-poll.test.ts b/e2e/journey-suites/src/choice-confirm-poll.test.ts index 24db06047..61034a3ca 100644 --- a/e2e/journey-suites/src/choice-confirm-poll.test.ts +++ b/e2e/journey-suites/src/choice-confirm-poll.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=TEST_LoginWithMiscCallbacks'); + await navigate('/?tree=TEST_LoginWithMiscCallbacks&clientId=tenant'); const messageArray: string[] = []; @@ -21,7 +21,9 @@ test('Test happy paths on test page', async ({ page }) => { return Promise.resolve(true); }); - expect(page.url()).toBe('http://localhost:5829/?journey=TEST_LoginWithMiscCallbacks'); + expect(page.url()).toBe( + 'http://localhost:5829/?tree=TEST_LoginWithMiscCallbacks&clientId=tenant', + ); // Perform basic login await page.getByLabel('User Name').fill(username); diff --git a/e2e/journey-suites/src/custom-paths.test.ts b/e2e/journey-suites/src/custom-paths.test.ts index f848f8f1d..5bdee9bf7 100644 --- a/e2e/journey-suites/src/custom-paths.test.ts +++ b/e2e/journey-suites/src/custom-paths.test.ts @@ -12,7 +12,7 @@ import { username, password } from './utils/demo-user.js'; // Skipping test until AM Mock API is available that supports custom paths test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?paths=true&journey=Login'); + await navigate('/?paths=true&tree=Login&clientId=tenant'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/device-profile.test.ts b/e2e/journey-suites/src/device-profile.test.ts index 65c8c348e..2d44d7c30 100644 --- a/e2e/journey-suites/src/device-profile.test.ts +++ b/e2e/journey-suites/src/device-profile.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=TEST_DeviceProfile'); + await navigate('/?tree=TEST_DeviceProfile'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/email-suspend.test.ts b/e2e/journey-suites/src/email-suspend.test.ts index 900d61b0a..e2519a29f 100644 --- a/e2e/journey-suites/src/email-suspend.test.ts +++ b/e2e/journey-suites/src/email-suspend.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=TEST_LoginSuspendEmail'); + await navigate('/?tree=TEST_LoginSuspendEmail&clientId=tenant'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/login.test.ts b/e2e/journey-suites/src/login.test.ts index f048e6de1..168c69eaf 100644 --- a/e2e/journey-suites/src/login.test.ts +++ b/e2e/journey-suites/src/login.test.ts @@ -7,11 +7,11 @@ import { expect, test } from '@playwright/test'; import { asyncEvents } from './utils/async-events.js'; -import { username, password } from './utils/demo-user.js'; +import { password, username } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=Login'); + await navigate('/?tree=Login'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/no-session.test.ts b/e2e/journey-suites/src/no-session.test.ts index 679e6e9cb..729f67cc7 100644 --- a/e2e/journey-suites/src/no-session.test.ts +++ b/e2e/journey-suites/src/no-session.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=Login&no-session=true'); + await navigate('/?tree=Login&no-session=true'); const messageArray: string[] = []; @@ -21,7 +21,7 @@ test('Test happy paths on test page', async ({ page }) => { return Promise.resolve(true); }); - expect(page.url()).toBe('http://localhost:5829/?journey=Login&no-session=true'); + expect(page.url()).toBe('http://localhost:5829/?tree=Login&no-session=true'); // Perform basic login await page.getByLabel('User Name').fill(username); diff --git a/e2e/journey-suites/src/otp-register.test.ts b/e2e/journey-suites/src/otp-register.test.ts index fc21076e0..ef934b3b4 100644 --- a/e2e/journey-suites/src/otp-register.test.ts +++ b/e2e/journey-suites/src/otp-register.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=TEST_OTPRegistration'); + await navigate('/?tree=TEST_OTPRegistration&clientId=tenant'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/protect.test.ts b/e2e/journey-suites/src/protect.test.ts index bb7eb3ee1..abb8b25d0 100644 --- a/e2e/journey-suites/src/protect.test.ts +++ b/e2e/journey-suites/src/protect.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=TEST_Protect'); + await navigate('/?tree=TEST_Protect'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/registration.test.ts b/e2e/journey-suites/src/registration.test.ts index 242ae0427..3d455e3bf 100644 --- a/e2e/journey-suites/src/registration.test.ts +++ b/e2e/journey-suites/src/registration.test.ts @@ -11,7 +11,7 @@ import { password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?journey=Registration'); + await navigate('/?tree=Registration&clientId=tenant'); // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" const id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); @@ -23,7 +23,7 @@ test('Test happy paths on test page', async ({ page }) => { return Promise.resolve(true); }); - expect(page.url()).toBe('http://localhost:5829/?journey=Registration'); + expect(page.url()).toBe('http://localhost:5829/?tree=Registration&clientId=tenant'); // Perform registration await page.getByLabel('Username').fill('testuser+' + id); diff --git a/e2e/journey-suites/src/request-middleware.test.ts b/e2e/journey-suites/src/request-middleware.test.ts index be0e26911..84d58d946 100644 --- a/e2e/journey-suites/src/request-middleware.test.ts +++ b/e2e/journey-suites/src/request-middleware.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?middleware=true&journey=Login'); + await navigate('/?middleware=true&tree=Login'); const headerArray: Headers[] = []; const messageArray: string[] = []; @@ -33,7 +33,7 @@ test.skip('Test happy paths on test page', async ({ page }) => { headerArray.push(new Headers(headers)); }); - expect(page.url()).toBe('http://localhost:5829/?middleware=true&journey=Login'); + expect(page.url()).toBe('http://localhost:5829/?middleware=true&tree=Login'); // Perform basic login await page.getByLabel('User Name').fill(username); diff --git a/e2e/journey-suites/src/utils/demo-user.ts b/e2e/journey-suites/src/utils/demo-user.ts index b9e9fd45c..406caf673 100644 --- a/e2e/journey-suites/src/utils/demo-user.ts +++ b/e2e/journey-suites/src/utils/demo-user.ts @@ -4,7 +4,7 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -export const username = 'demouser'; -export const password = 'U.QPDWEN47ZMyJhCDmhGLK*nr'; +export const username = 'sdkuser'; +export const password = 'password'; export const phoneNumber1 = '888123456'; export const phoneNumber2 = '888123457'; diff --git a/package.json b/package.json index 437aba867..43559a1bc 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test": "CI=true nx affected:test", "test:e2e": "CI=true nx affected:e2e", "verdaccio": "nx local-registry", - "watch": "nx watch-deps" + "watch": "nx vite:watch-deps" }, "lint-staged": { "*": [ @@ -61,6 +61,7 @@ "@nx/devkit": "21.2.3", "@nx/eslint": "21.2.3", "@nx/eslint-plugin": "21.2.3", + "@nx/express": "21.2.3", "@nx/jest": "21.2.3", "@nx/js": "21.2.3", "@nx/playwright": "21.2.3", diff --git a/packages/journey-client/src/lib/client.store.test.ts b/packages/journey-client/src/lib/client.store.test.ts index ea7038c30..dfd8b3151 100644 --- a/packages/journey-client/src/lib/client.store.test.ts +++ b/packages/journey-client/src/lib/client.store.test.ts @@ -238,6 +238,65 @@ describe('journey-client', () => { }); }); + describe('baseUrl normalization', () => { + test('should add trailing slash to baseUrl without one', async () => { + const configWithoutSlash: JourneyClientConfig = { + serverConfig: { + baseUrl: 'http://localhost:9443/am', + }, + realmPath: 'root', + }; + + const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); + + const client = await journey({ config: configWithoutSlash }); + await client.start(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + expect(request.url).toBe('http://localhost:9443/am/json/realms/root/authenticate'); + }); + + test('should preserve trailing slash if already present', async () => { + const configWithSlash: JourneyClientConfig = { + serverConfig: { + baseUrl: 'http://localhost:9443/am/', + }, + realmPath: 'root', + }; + + const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); + + const client = await journey({ config: configWithSlash }); + await client.start(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + expect(request.url).toBe('http://localhost:9443/am/json/realms/root/authenticate'); + }); + + test('should work with baseUrl without context path', async () => { + const configNoContext: JourneyClientConfig = { + serverConfig: { + baseUrl: 'http://localhost:9443', + }, + realmPath: 'root', + }; + + const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] }; + mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse))); + + const client = await journey({ config: configNoContext }); + await client.start(); + + expect(mockFetch).toHaveBeenCalledTimes(1); + const request = mockFetch.mock.calls[0][0] as Request; + expect(request.url).toBe('http://localhost:9443/json/realms/root/authenticate'); + }); + }); + // TODO: Add tests for endSession when the test environment AbortSignal issue is resolved // test('endSession() should call the sessions endpoint with DELETE method', async () => { // mockFetch.mockResolvedValue(new Response('', { status: 200 })); diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 531675417..9288cd0dc 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -22,6 +22,26 @@ import type { JourneyClientConfig } from './config.types.js'; import type { RedirectCallback } from './callbacks/redirect-callback.js'; import { NextOptions, StartParam, ResumeOptions } from './interfaces.js'; +/** + * Normalizes the serverConfig to ensure baseUrl has a trailing slash. + * This is required for the resolve() function to work correctly with context paths like /am. + */ +function normalizeConfig(config: JourneyClientConfig): JourneyClientConfig { + if (config.serverConfig?.baseUrl) { + const url = config.serverConfig.baseUrl; + if (url.charAt(url.length - 1) !== '/') { + return { + ...config, + serverConfig: { + ...config.serverConfig, + baseUrl: url + '/', + }, + }; + } + } + return config; +} + export async function journey({ config, requestMiddleware, @@ -36,8 +56,11 @@ export async function journey({ }) { const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); - const store = createJourneyStore({ requestMiddleware, logger: log, config }); - store.dispatch(setConfig(config)); + // Normalize config to ensure baseUrl has trailing slash + const normalizedConfig = normalizeConfig(config); + + const store = createJourneyStore({ requestMiddleware, logger: log, config: normalizedConfig }); + store.dispatch(setConfig(normalizedConfig)); const stepStorage = createStorage<{ step: Step }>({ type: 'sessionStorage', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 010611e4c..9ebd94f45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ importers: '@nx/eslint-plugin': specifier: 21.2.3 version: 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0)) + '@nx/express': + specifier: 21.2.3 + version: 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.1(jiti@2.6.1))(express@4.21.2)(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0)) '@nx/jest': specifier: 21.2.3 version: 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(babel-plugin-macros@3.1.0)(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0)) @@ -248,6 +251,30 @@ importers: specifier: ^0.3.3 version: 0.3.3(vitest@3.2.4) + e2e/am-mock-api: + dependencies: + '@types/express': + specifier: ^4.17.17 + version: 4.17.23 + body-parser: + specifier: ^2.2.0 + version: 2.2.0 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.21.2 + version: 4.21.2 + superagent: + specifier: ^10.2.3 + version: 10.2.3 + uuid: + specifier: ^13.0.0 + version: 13.0.0 + e2e/davinci-app: dependencies: '@forgerock/davinci-client': @@ -2177,6 +2204,10 @@ packages: '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2212,6 +2243,14 @@ packages: '@zkochan/js-yaml': optional: true + '@nx/express@21.2.3': + resolution: {integrity: sha512-XUHDpH8ilLUYkuHobm3UZbtkY+09AfjrlR5xfb/fDCimvjyPb/E8MvcYPya+jvUTkNQ5Z8PL51qG/2F5SnUDBw==} + peerDependencies: + express: ^4.21.2 + peerDependenciesMeta: + express: + optional: true + '@nx/jest@21.2.3': resolution: {integrity: sha512-lkH+tX8c1XSRjDa1g/lnYiC4zgs+8tZsj9yUVR2/1x+OO2SYDL8KVld6ZkWzXhRW8ZKXPHkDMWMUNBqsYlAWHA==} @@ -2223,6 +2262,9 @@ packages: verdaccio: optional: true + '@nx/node@21.2.3': + resolution: {integrity: sha512-5ivOTIYyXHwZSwpCR3AnKFCzjjzKHMfmVnMLQbiDhYB7nd9RJXsKsPAMdEVFCP/JBTPmQkufXElw/Kxfww7dnA==} + '@nx/nx-darwin-arm64@21.2.3': resolution: {integrity: sha512-5WgOjoX4vqG286A8abYoLCScA2ZF5af/2ZBjaM5EHypgbJLGQuMcP2ahzX66FYohT4wdAej1D0ILkEax71fAKw==} cpu: [arm64] @@ -2486,6 +2528,9 @@ packages: cpu: [x64] os: [win32] + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -2935,9 +2980,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + '@types/express@5.0.5': resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} @@ -3510,6 +3561,9 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + asn1@0.2.6: resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} @@ -3681,6 +3735,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3939,6 +3997,9 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -3987,6 +4048,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} @@ -3994,10 +4059,17 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.0.2: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + core-js-compat@3.46.0: resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} @@ -4112,6 +4184,15 @@ packages: supports-color: optional: true + debug@4.3.1: + resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -4291,6 +4372,9 @@ packages: peerDependencies: typescript: ^5.4.4 + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4729,6 +4813,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -4861,6 +4948,10 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -4968,6 +5059,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-them-args@1.3.2: + resolution: {integrity: sha512-LRn8Jlk+DwZE4GTlDbT3Hikd1wSHgLMme/+7ddlqKd7ldwR6LjJgTVWzBnR01wnYGe4KgrXjg287RaI22UHmAw==} + get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} @@ -5267,6 +5361,10 @@ packages: resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} engines: {node: '>= 0.10'} + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -5480,6 +5578,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is2@2.0.9: + resolution: {integrity: sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==} + engines: {node: '>=v0.10.0'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -5743,6 +5845,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kill-port@1.6.1: + resolution: {integrity: sha512-un0Y55cOM7JKGaLnGja28T38tDDop0AQ8N0KlAdyh+B1nmMoX8AnNmqPNZbS3mUMgiST51DCVqmbFT1gNJpVNw==} + hasBin: true + kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -5946,6 +6052,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -5983,6 +6093,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -6083,6 +6197,9 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6678,6 +6795,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -6953,6 +7074,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-exec@1.0.2: + resolution: {integrity: sha512-jyVd+kU2X+mWKMmGhx4fpWbPsjvD53k9ivqetutVW/BQ+WIZoDoP4d8vUMGezV6saZsiNoW2f9GIhg9Dondohg==} + shelljs@0.9.2: resolution: {integrity: sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==} engines: {node: '>=18'} @@ -7203,6 +7327,10 @@ packages: engines: {node: '>=18'} hasBin: true + superagent@10.2.3: + resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} + engines: {node: '>=14.18.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -7243,6 +7371,9 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tcp-port-used@1.0.2: + resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==} + term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} @@ -7469,6 +7600,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -7624,6 +7759,10 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -9871,6 +10010,8 @@ snapshots: '@emnapi/runtime': 1.7.0 '@tybys/wasm-util': 0.9.0 + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9941,6 +10082,30 @@ snapshots: - supports-color - verdaccio + '@nx/express@21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.1(jiti@2.6.1))(express@4.21.2)(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0))': + dependencies: + '@nx/devkit': 21.2.3(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))) + '@nx/js': 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(verdaccio@6.2.1(typanion@3.14.0)) + '@nx/node': 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.1(jiti@2.6.1))(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0)) + tslib: 2.8.1 + optionalDependencies: + express: 4.21.2 + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - '@types/node' + - '@zkochan/js-yaml' + - babel-plugin-macros + - debug + - eslint + - node-notifier + - nx + - supports-color + - ts-node + - typescript + - verdaccio + '@nx/jest@21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(babel-plugin-macros@3.1.0)(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0))': dependencies: '@jest/reporters': 29.7.0 @@ -10013,6 +10178,31 @@ snapshots: - nx - supports-color + '@nx/node@21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.1(jiti@2.6.1))(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0))': + dependencies: + '@nx/devkit': 21.2.3(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))) + '@nx/eslint': 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(verdaccio@6.2.1(typanion@3.14.0)) + '@nx/jest': 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(babel-plugin-macros@3.1.0)(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(ts-node@10.9.2(@swc/core@1.11.21(@swc/helpers@0.5.17))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.2.1(typanion@3.14.0)) + '@nx/js': 21.2.3(@babel/traverse@7.28.5)(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17))(nx@21.2.3(@swc-node/register@1.10.10(@swc/core@1.11.21(@swc/helpers@0.5.17))(@swc/types@0.1.25)(typescript@5.8.3))(@swc/core@1.11.21(@swc/helpers@0.5.17)))(verdaccio@6.2.1(typanion@3.14.0)) + kill-port: 1.6.1 + tcp-port-used: 1.0.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - '@types/node' + - '@zkochan/js-yaml' + - babel-plugin-macros + - debug + - eslint + - node-notifier + - nx + - supports-color + - ts-node + - typescript + - verdaccio + '@nx/nx-darwin-arm64@21.2.3': optional: true @@ -10321,6 +10511,10 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@5.3.0': optional: true + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -10706,6 +10900,13 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 24.9.2 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + '@types/express-serve-static-core@5.1.0': dependencies: '@types/node': 24.9.2 @@ -10713,6 +10914,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.1 + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + '@types/express@5.0.5': dependencies: '@types/body-parser': 1.19.6 @@ -11577,6 +11785,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + asap@2.0.6: {} + asn1@0.2.6: dependencies: safer-buffer: 2.1.2 @@ -11786,6 +11996,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -12044,6 +12268,8 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + component-emitter@1.3.1: {} + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -12095,12 +12321,21 @@ snapshots: convert-source-map@2.0.0: {} + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + cookie-signature@1.0.6: {} cookie@0.7.1: {} + cookie@0.7.2: {} + cookie@1.0.2: {} + cookiejar@2.1.4: {} + core-js-compat@3.46.0: dependencies: browserslist: 4.27.0 @@ -12226,6 +12461,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.1: + dependencies: + ms: 2.1.2 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -12377,6 +12616,11 @@ snapshots: transitivePeerDependencies: - supports-color + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + diff-sequences@29.6.3: {} diff@4.0.2: {} @@ -12969,6 +13213,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} fastq@1.19.1: @@ -13114,6 +13360,12 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -13219,6 +13471,8 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-them-args@1.3.2: {} + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -13586,6 +13840,8 @@ snapshots: interpret@1.4.0: {} + ip-regex@4.3.0: {} + ipaddr.js@1.9.1: {} is-array-buffer@3.0.5: @@ -13762,6 +14018,12 @@ snapshots: dependencies: is-docker: 2.2.1 + is2@2.0.9: + dependencies: + deep-is: 0.1.4 + ip-regex: 4.3.0 + is-url: 1.2.4 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -14233,6 +14495,11 @@ snapshots: dependencies: json-buffer: 3.0.1 + kill-port@1.6.1: + dependencies: + get-them-args: 1.3.2 + shell-exec: 1.0.2 + kind-of@6.0.3: {} leven@3.1.0: {} @@ -14436,6 +14703,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + meow@12.1.1: {} merge-descriptors@1.0.3: {} @@ -14461,6 +14730,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.6.0: {} @@ -14536,6 +14809,8 @@ snapshots: ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -15172,6 +15447,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -15497,6 +15779,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-exec@1.0.2: {} + shelljs@0.9.2: dependencies: execa: 1.0.0 @@ -15778,6 +16062,20 @@ snapshots: dependencies: commander: 12.1.0 + superagent@10.2.3: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.1 + fast-safe-stringify: 2.1.1 + form-data: 4.0.4 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.0 + transitivePeerDependencies: + - supports-color + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -15823,6 +16121,13 @@ snapshots: - bare-abort-controller - react-native-b4a + tcp-port-used@1.0.2: + dependencies: + debug: 4.3.1 + is2: 2.0.9 + transitivePeerDependencies: + - supports-color + term-size@2.2.1: {} terser-webpack-plugin@5.3.14(@swc/core@1.11.21(@swc/helpers@0.5.17))(webpack@5.102.1(@swc/core@1.11.21(@swc/helpers@0.5.17))): @@ -16036,6 +16341,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -16180,6 +16491,8 @@ snapshots: uuid@11.1.0: {} + uuid@13.0.0: {} + uuid@8.3.2: {} v8-compile-cache-lib@3.0.1: {} diff --git a/tsconfig.json b/tsconfig.json index 0bd1c48eb..92f092123 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -75,6 +75,9 @@ }, { "path": "./e2e/journey-suites" + }, + { + "path": "./e2e/am-mock-api" } ] } From 5dd242025e069dd27ff0626208d926423eafe821 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 13 Nov 2025 09:11:41 -0700 Subject: [PATCH 2/2] chore: revert-tree-to-journey --- e2e/davinci-suites/src/phone-number-field.test.ts | 6 +++--- e2e/journey-app/main.ts | 6 +++--- e2e/journey-suites/src/choice-confirm-poll.test.ts | 4 ++-- e2e/journey-suites/src/custom-paths.test.ts | 2 +- e2e/journey-suites/src/device-profile.test.ts | 2 +- e2e/journey-suites/src/email-suspend.test.ts | 2 +- e2e/journey-suites/src/login.test.ts | 2 +- e2e/journey-suites/src/no-session.test.ts | 4 ++-- e2e/journey-suites/src/otp-register.test.ts | 2 +- e2e/journey-suites/src/protect.test.ts | 2 +- e2e/journey-suites/src/registration.test.ts | 4 ++-- e2e/journey-suites/src/request-middleware.test.ts | 4 ++-- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/e2e/davinci-suites/src/phone-number-field.test.ts b/e2e/davinci-suites/src/phone-number-field.test.ts index b4d9070ca..05cbcdd5d 100644 --- a/e2e/davinci-suites/src/phone-number-field.test.ts +++ b/e2e/davinci-suites/src/phone-number-field.test.ts @@ -106,9 +106,9 @@ test.describe('Device registration tests', () => { await expect(page.getByText('SDK Automation - Enter Phone Number')).toBeVisible(); await page.getByRole('textbox', { name: 'Enter Phone Number' }).fill('3035550100'); await page.getByRole('button', { name: 'Submit' }).click(); - await expect( - async () => await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible(), - ).toPass(); + + await expect(page.getByText('SMS/Voice MFA Registered')).toBeVisible(); + await page.getByRole('button', { name: 'Continue' }).click(); }); }); diff --git a/e2e/journey-app/main.ts b/e2e/journey-app/main.ts index 45ad9979b..f1eb3c6a9 100644 --- a/e2e/journey-app/main.ts +++ b/e2e/journey-app/main.ts @@ -18,7 +18,7 @@ const searchParams = new URLSearchParams(qs); const config = serverConfigs[searchParams.get('clientId') || 'basic']; -const tree = searchParams.get('tree') ?? 'UsernamePassword'; +const journeyName = searchParams.get('journey') ?? 'UsernamePassword'; let requestMiddleware: RequestMiddleware[] = []; if (searchParams.get('middleware') === 'true') { @@ -55,7 +55,7 @@ if (searchParams.get('middleware') === 'true') { const formEl = document.getElementById('form') as HTMLFormElement; const journeyEl = document.getElementById('journey') as HTMLDivElement; - let step = await journeyClient.start({ journey: tree }); + let step = await journeyClient.start({ journey: journeyName }); function renderComplete() { if (step?.type !== 'LoginSuccess') { @@ -79,7 +79,7 @@ if (searchParams.get('middleware') === 'true') { console.log('Logout successful'); - step = await journeyClient.start({ journey: tree }); + step = await journeyClient.start({ journey: journeyName }); renderForm(); }); diff --git a/e2e/journey-suites/src/choice-confirm-poll.test.ts b/e2e/journey-suites/src/choice-confirm-poll.test.ts index 61034a3ca..4c800011f 100644 --- a/e2e/journey-suites/src/choice-confirm-poll.test.ts +++ b/e2e/journey-suites/src/choice-confirm-poll.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=TEST_LoginWithMiscCallbacks&clientId=tenant'); + await navigate('/?journey=TEST_LoginWithMiscCallbacks&clientId=tenant'); const messageArray: string[] = []; @@ -22,7 +22,7 @@ test('Test happy paths on test page', async ({ page }) => { }); expect(page.url()).toBe( - 'http://localhost:5829/?tree=TEST_LoginWithMiscCallbacks&clientId=tenant', + 'http://localhost:5829/?journey=TEST_LoginWithMiscCallbacks&clientId=tenant', ); // Perform basic login diff --git a/e2e/journey-suites/src/custom-paths.test.ts b/e2e/journey-suites/src/custom-paths.test.ts index 5bdee9bf7..8cd43b786 100644 --- a/e2e/journey-suites/src/custom-paths.test.ts +++ b/e2e/journey-suites/src/custom-paths.test.ts @@ -12,7 +12,7 @@ import { username, password } from './utils/demo-user.js'; // Skipping test until AM Mock API is available that supports custom paths test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?paths=true&tree=Login&clientId=tenant'); + await navigate('/?paths=true&journey=Login&clientId=tenant'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/device-profile.test.ts b/e2e/journey-suites/src/device-profile.test.ts index 2d44d7c30..65c8c348e 100644 --- a/e2e/journey-suites/src/device-profile.test.ts +++ b/e2e/journey-suites/src/device-profile.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=TEST_DeviceProfile'); + await navigate('/?journey=TEST_DeviceProfile'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/email-suspend.test.ts b/e2e/journey-suites/src/email-suspend.test.ts index e2519a29f..4e4d180d8 100644 --- a/e2e/journey-suites/src/email-suspend.test.ts +++ b/e2e/journey-suites/src/email-suspend.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=TEST_LoginSuspendEmail&clientId=tenant'); + await navigate('/?journey=TEST_LoginSuspendEmail&clientId=tenant'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/login.test.ts b/e2e/journey-suites/src/login.test.ts index 168c69eaf..796381ea0 100644 --- a/e2e/journey-suites/src/login.test.ts +++ b/e2e/journey-suites/src/login.test.ts @@ -11,7 +11,7 @@ import { password, username } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=Login'); + await navigate('/?journey=Login'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/no-session.test.ts b/e2e/journey-suites/src/no-session.test.ts index 729f67cc7..679e6e9cb 100644 --- a/e2e/journey-suites/src/no-session.test.ts +++ b/e2e/journey-suites/src/no-session.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=Login&no-session=true'); + await navigate('/?journey=Login&no-session=true'); const messageArray: string[] = []; @@ -21,7 +21,7 @@ test('Test happy paths on test page', async ({ page }) => { return Promise.resolve(true); }); - expect(page.url()).toBe('http://localhost:5829/?tree=Login&no-session=true'); + expect(page.url()).toBe('http://localhost:5829/?journey=Login&no-session=true'); // Perform basic login await page.getByLabel('User Name').fill(username); diff --git a/e2e/journey-suites/src/otp-register.test.ts b/e2e/journey-suites/src/otp-register.test.ts index ef934b3b4..e743d4301 100644 --- a/e2e/journey-suites/src/otp-register.test.ts +++ b/e2e/journey-suites/src/otp-register.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=TEST_OTPRegistration&clientId=tenant'); + await navigate('/?journey=TEST_OTPRegistration&clientId=tenant'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/protect.test.ts b/e2e/journey-suites/src/protect.test.ts index abb8b25d0..bb7eb3ee1 100644 --- a/e2e/journey-suites/src/protect.test.ts +++ b/e2e/journey-suites/src/protect.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=TEST_Protect'); + await navigate('/?journey=TEST_Protect'); const messageArray: string[] = []; diff --git a/e2e/journey-suites/src/registration.test.ts b/e2e/journey-suites/src/registration.test.ts index 3d455e3bf..a536c8f05 100644 --- a/e2e/journey-suites/src/registration.test.ts +++ b/e2e/journey-suites/src/registration.test.ts @@ -11,7 +11,7 @@ import { password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?tree=Registration&clientId=tenant'); + await navigate('/?journey=Registration&clientId=tenant'); // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" const id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); @@ -23,7 +23,7 @@ test('Test happy paths on test page', async ({ page }) => { return Promise.resolve(true); }); - expect(page.url()).toBe('http://localhost:5829/?tree=Registration&clientId=tenant'); + expect(page.url()).toBe('http://localhost:5829/?journey=Registration&clientId=tenant'); // Perform registration await page.getByLabel('Username').fill('testuser+' + id); diff --git a/e2e/journey-suites/src/request-middleware.test.ts b/e2e/journey-suites/src/request-middleware.test.ts index 84d58d946..be0e26911 100644 --- a/e2e/journey-suites/src/request-middleware.test.ts +++ b/e2e/journey-suites/src/request-middleware.test.ts @@ -11,7 +11,7 @@ import { username, password } from './utils/demo-user.js'; test.skip('Test happy paths on test page', async ({ page }) => { const { clickButton, navigate } = asyncEvents(page); - await navigate('/?middleware=true&tree=Login'); + await navigate('/?middleware=true&journey=Login'); const headerArray: Headers[] = []; const messageArray: string[] = []; @@ -33,7 +33,7 @@ test.skip('Test happy paths on test page', async ({ page }) => { headerArray.push(new Headers(headers)); }); - expect(page.url()).toBe('http://localhost:5829/?middleware=true&tree=Login'); + expect(page.url()).toBe('http://localhost:5829/?middleware=true&journey=Login'); // Perform basic login await page.getByLabel('User Name').fill(username);