diff --git a/.zed/tasks.json b/.zed/tasks.json new file mode 100644 index 0000000000..97bc19e5c9 --- /dev/null +++ b/.zed/tasks.json @@ -0,0 +1,90 @@ +// Project tasks configuration. See https://zed.dev/docs/tasks for documentation. +[ + { + "label": "nx build", + "command": "pnpm", + "args": ["nx", "affected", "-t build", "--parallel=3"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": false, + "allow_concurrent_runs": true, + "reveal": "no_focus" + }, + { + "label": "nx lint", + "command": "pnpm", + "args": ["nx", "affected", "-t lint", "--parallel=3"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": false, + "allow_concurrent_runs": true, + "reveal": "no_focus" + }, + { + "label": "nx test", + "command": "pnpm", + "args": ["nx", "affected", "-t test", "--parallel=3"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": false, + "allow_concurrent_runs": true, + "reveal": "no_focus" + }, + { + "label": "nx watch davinci-watch", + "command": "pnpm", + "args": ["watch", "@forgerock/davinci-app"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": true, + "allow_concurrent_runs": true, + "reveal": "always" + }, + { + "label": "nx watch oidc-e2e", + "command": "pnpm", + "args": ["watch", "@forgerock/oidc-app"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": true, + "allow_concurrent_runs": true, + "reveal": "no_focus" + }, + { + "label": "nx watch protect-watch", + "command": "pnpm", + "args": ["watch", "@forgerock/protect-app"], + "cwd": "$ZED_WORKTREE_ROOT", + "use_new_terminal": true, + "allow_concurrent_runs": true, + "reveal": "no_focus" + }, + { + "label": "nx typecheck repo", + "command": "pnpm", + "args": ["nx", "affected -t typecheck"], + "cwd": "$ZED_WORKTREE_ROOT", + "reveal": "no_focus" + }, + { + "label": "oidc e2e ui", + "command": "pnpm", + "args": ["nx", "e2e", "@forgerock/oidc-suites", "--ui", "--skipNxCache"], + "cwd": "$ZED_WORKTREE_ROOT" + }, + { + "label": "protect-app e2e ui", + "command": "pnpm", + "args": ["nx", "e2e", "@forgerock/protect-suites", "--ui", "--skipNxCache"], + "cwd": "$ZED_WORKTREE_ROOT", + "reveal": "no_focus" + }, + { + "label": "local release repo", + "command": "pnpm", + "args": ["release-local"], + "cwd": "$ZED_WORKTREE_ROOT", + "reveal": "no_focus" + }, + { + "label": "journey e2e ui", + "command": "pnpm", + "args": ["nx", "e2e", "@forgerock/journey-suites", "--ui", "--skipNxCache"], + "cwd": "$ZED_WORKTREE_ROOT" + } +] diff --git a/e2e/journey-app/components/webauthn.ts b/e2e/journey-app/components/webauthn.ts new file mode 100644 index 0000000000..73a34b9b2b --- /dev/null +++ b/e2e/journey-app/components/webauthn.ts @@ -0,0 +1,31 @@ +import { JourneyStep } from '@forgerock/journey-client/types'; +import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn'; + +export function webauthnComponent(journeyEl: HTMLDivElement, step: JourneyStep, idx: number) { + const container = document.createElement('div'); + container.id = `webauthn-container-${idx}`; + const info = document.createElement('p'); + info.innerText = 'Please complete the WebAuthn challenge using your authenticator.'; + container.appendChild(info); + journeyEl.appendChild(container); + + const webAuthnStepType = WebAuthn.getWebAuthnStepType(step); + + async function handleWebAuthn() { + try { + if (webAuthnStepType === WebAuthnStepType.Authentication) { + console.log('trying authentication'); + await WebAuthn.authenticate(step); + console.log('trying registration'); + } else if (WebAuthnStepType.Registration === webAuthnStepType) { + await WebAuthn.register(step); + } else { + return Promise.resolve(undefined); + } + } catch (error) { + console.error('WebAuthn error:', error); + } + } + + return handleWebAuthn(); +} diff --git a/e2e/journey-app/main.ts b/e2e/journey-app/main.ts index bc8a9eb0e4..b98e2f9262 100644 --- a/e2e/journey-app/main.ts +++ b/e2e/journey-app/main.ts @@ -9,19 +9,22 @@ import './style.css'; import { journey } from '@forgerock/journey-client'; import type { - RequestMiddleware, NameCallback, PasswordCallback, + RequestMiddleware, } from '@forgerock/journey-client/types'; -import textComponent from './components/text.js'; import passwordComponent from './components/password.js'; +import textComponent from './components/text.js'; import { serverConfigs } from './server-configs.js'; +import { webauthnComponent } from './components/webauthn.js'; +import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn'; const qs = window.location.search; const searchParams = new URLSearchParams(qs); -const config = serverConfigs[searchParams.get('clientId') || 'basic']; +const journeyName = searchParams.get('clientId') || 'UsernamePassword'; +const config = serverConfigs[journeyName]; const requestMiddleware: RequestMiddleware[] = [ (req, action, next) => { @@ -48,7 +51,7 @@ const requestMiddleware: RequestMiddleware[] = [ const formEl = document.getElementById('form') as HTMLFormElement; const journeyEl = document.getElementById('journey') as HTMLDivElement; - let step = await journeyClient.start(); + let step = await journeyClient.start({ ...config, journey: journeyName }); function renderComplete() { if (step?.type !== 'LoginSuccess') { @@ -103,9 +106,30 @@ const requestMiddleware: RequestMiddleware[] = [ header.innerText = formName || ''; journeyEl.appendChild(header); - const callbacks = step.callbacks; + const webAuthnStep = WebAuthn.getWebAuthnStepType(step); + + if ( + webAuthnStep === WebAuthnStepType.Authentication || + webAuthnStep === WebAuthnStepType.Registration + ) { + await webauthnComponent(journeyEl, step, 0); + step = await journeyClient.next(step); + if (step?.type === 'Step') { + await renderForm(); + } else if (step?.type === 'LoginSuccess') { + console.log('Basic login successful'); + renderComplete(); + } else if (step?.type === 'LoginFailure') { + renderForm(); + renderError(); + } else { + console.error('Unknown node status', step); + } + return; // prevent the rest of the function from running + } - callbacks.forEach((callback, idx) => { + const callbacks = step.callbacks; + callbacks.forEach(async (callback, idx) => { if (callback.getType() === 'NameCallback') { const cb = callback as NameCallback; textComponent( @@ -146,7 +170,7 @@ const requestMiddleware: RequestMiddleware[] = [ * Recursively render the form with the new state */ if (step?.type === 'Step') { - renderForm(); + await renderForm(); } else if (step?.type === 'LoginSuccess') { console.log('Basic login successful'); renderComplete(); diff --git a/e2e/journey-app/package.json b/e2e/journey-app/package.json index 57fba5801b..b8d4986a4d 100644 --- a/e2e/journey-app/package.json +++ b/e2e/journey-app/package.json @@ -16,6 +16,7 @@ "dependencies": { "@forgerock/journey-client": "workspace:*", "@forgerock/oidc-client": "workspace:*", - "@forgerock/sdk-logger": "workspace:*" + "@forgerock/sdk-logger": "workspace:*", + "@forgerock/sdk-types": "workspace:*" } } diff --git a/e2e/journey-app/server-configs.ts b/e2e/journey-app/server-configs.ts index c85ac80332..550a17409b 100644 --- a/e2e/journey-app/server-configs.ts +++ b/e2e/journey-app/server-configs.ts @@ -7,7 +7,37 @@ import type { JourneyClientConfig } from '@forgerock/journey-client/types'; export const serverConfigs: Record = { - basic: { + UsernamePassword: { + serverConfig: { + baseUrl: 'https://openam-sdks.forgeblocks.com/am/', + }, + realmPath: '/alpha', + }, + ['TEST_WebAuthn-Registration']: { + serverConfig: { + baseUrl: 'https://openam-sdks.forgeblocks.com/am/', + }, + realmPath: '/alpha', + }, + ['TEST_WebAuthnAuthentication_UsernamePassword']: { + serverConfig: { + baseUrl: 'https://openam-sdks.forgeblocks.com/am/', + }, + realmPath: '/alpha', + }, + ['TEST_WebAuthnAuthentication']: { + serverConfig: { + baseUrl: 'https://openam-sdks.forgeblocks.com/am/', + }, + realmPath: '/alpha', + }, + ['TEST_WebAuthn-Registration-UsernameToDevice']: { + serverConfig: { + baseUrl: 'https://openam-sdks.forgeblocks.com/am/', + }, + realmPath: '/alpha', + }, + ['TEST_WebAuthnAuthentication_Usernameless']: { serverConfig: { baseUrl: 'https://openam-sdks.forgeblocks.com/am/', }, diff --git a/e2e/journey-app/tsconfig.app.json b/e2e/journey-app/tsconfig.app.json index a2b6a0685a..cc1dff7ac2 100644 --- a/e2e/journey-app/tsconfig.app.json +++ b/e2e/journey-app/tsconfig.app.json @@ -18,6 +18,9 @@ { "path": "../../packages/oidc-client/tsconfig.lib.json" }, + { + "path": "../../packages/sdk-types/tsconfig.lib.json" + }, { "path": "../../packages/journey-client/tsconfig.lib.json" } diff --git a/e2e/journey-app/tsconfig.json b/e2e/journey-app/tsconfig.json index cc7b958851..5d303df9e5 100644 --- a/e2e/journey-app/tsconfig.json +++ b/e2e/journey-app/tsconfig.json @@ -20,6 +20,9 @@ { "path": "../../packages/oidc-client" }, + { + "path": "../../packages/sdk-types" + }, { "path": "../../packages/journey-client" }, diff --git a/e2e/journey-suites/playwright.config.ts b/e2e/journey-suites/playwright.config.ts index bbdad2cf75..20def3c1ef 100644 --- a/e2e/journey-suites/playwright.config.ts +++ b/e2e/journey-suites/playwright.config.ts @@ -26,7 +26,7 @@ const config: PlaywrightTestConfig = { webServer: [ process.env.CI == 'false' ? { - command: 'pnpm watch @forgerock/journey-app', + command: 'pnpm nx vite:watch-deps @forgerock/journey-app', port: 5829, ignoreHTTPSErrors: true, reuseExistingServer: !process.env.CI, diff --git a/e2e/journey-suites/src/webauthn-username-password.test.ts b/e2e/journey-suites/src/webauthn-username-password.test.ts new file mode 100644 index 0000000000..d488c8ce81 --- /dev/null +++ b/e2e/journey-suites/src/webauthn-username-password.test.ts @@ -0,0 +1,119 @@ +import { test, expect } from '@playwright/test'; +import type { CDPSession } from 'playwright'; +import { asyncEvents } from './utils/async-events.js'; +import { password, username } from './utils/demo-user.js'; + +test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available + +test('Register and authenticate with webauthn device (username + password journey)', async ({ + page, + context, +}) => { + let cdp: CDPSession | undefined; + let authenticatorId: string | undefined; + let webauthnEnabled = false; + let recordedCredentialIds: string[] = []; + + await test.step('Configure virtual authenticator', async () => { + cdp = await context.newCDPSession(page); + await cdp.send('WebAuthn.enable'); + webauthnEnabled = true; + + // A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys. + const response = await cdp.send('WebAuthn.addVirtualAuthenticator', { + options: { + protocol: 'ctap2', + transport: 'internal', // platform authenticator + hasResidentKey: true, // allow discoverable credentials (passkeys) + hasUserVerification: true, // device supports UV + isUserVerified: true, // simulate successful UV (PIN/biometric) + automaticPresenceSimulation: true, // auto "touch"/presence + }, + }); + authenticatorId = response.authenticatorId; + }); + + const { navigate } = asyncEvents(page); + + try { + // First, register a credential we can use later + await test.step('Navigate to registration journey', async () => { + await navigate('/?clientId=TEST_WebAuthn-Registration'); + await expect(page).toHaveURL('http://localhost:5829/?clientId=TEST_WebAuthn-Registration'); + }); + + await test.step('Complete primary credentials', async () => { + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Register WebAuthn credential', async () => { + // With the virtual authenticator present and presence auto-simulated, + // registration will complete without any OS prompts. + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + }); + + await test.step('Capture virtual authenticator credentials', async () => { + if (!cdp || !authenticatorId) { + throw new Error('Expected CDP session and authenticator to be configured'); + } + const { credentials } = await cdp.send('WebAuthn.getCredentials', { authenticatorId }); + recordedCredentialIds = (credentials || []).map((item) => item.credentialId); + expect(recordedCredentialIds.length).toBeGreaterThan(0); + }); + + await test.step('Logout after registration', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + + // Now authenticate via Username -> Password -> WebAuthn + await test.step('Navigate to username+password authentication journey', async () => { + await navigate('/?clientId=TEST_WebAuthnAuthentication_UsernamePassword'); + await expect(page).toHaveURL( + 'http://localhost:5829/?clientId=TEST_WebAuthnAuthentication_UsernamePassword', + ); + }); + + await test.step('Complete username credential', async () => { + await page.getByLabel('User Name').fill(username); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Complete password credential', async () => { + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Authenticate WebAuthn credential', async () => { + if (!cdp || !authenticatorId) { + throw new Error('Expected CDP session and authenticator to be configured'); + } + // Server has UV set to Discouraged; providing UV is fine. + await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: true }); + // With the virtual authenticator present and presence auto-simulated, + // authentication will complete without any OS prompts. + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + }); + + await test.step('Logout after authentication', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + } finally { + if (!cdp) { + return; + } + + const activeCdp = cdp; + await test.step('Remove virtual authenticator', async () => { + if (authenticatorId) { + await activeCdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }); + } + if (webauthnEnabled) { + await activeCdp.send('WebAuthn.disable'); + } + }); + } +}); diff --git a/e2e/journey-suites/src/webauthn-usernameless.test.ts b/e2e/journey-suites/src/webauthn-usernameless.test.ts new file mode 100644 index 0000000000..55fd74ed28 --- /dev/null +++ b/e2e/journey-suites/src/webauthn-usernameless.test.ts @@ -0,0 +1,98 @@ +import { test, expect } from '@playwright/test'; +import type { CDPSession } from 'playwright'; +import { asyncEvents } from './utils/async-events.js'; +import { password, username } from './utils/demo-user.js'; + +test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available + +test('Register and authenticate with webauthn device (usernameless)', async ({ page, context }) => { + let cdp: CDPSession | undefined; + let authenticatorId: string | undefined; + let webauthnEnabled = false; + + await test.step('Configure virtual authenticator', async () => { + cdp = await context.newCDPSession(page); + await cdp.send('WebAuthn.enable'); + webauthnEnabled = true; + + // A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys. + const response = await cdp.send('WebAuthn.addVirtualAuthenticator', { + options: { + protocol: 'ctap2', + transport: 'internal', // platform authenticator + hasResidentKey: true, // allow discoverable credentials (passkeys) + hasUserVerification: true, // device supports UV + isUserVerified: true, // simulate successful UV (PIN/biometric) + automaticPresenceSimulation: true, // auto "touch"/presence + }, + }); + authenticatorId = response.authenticatorId; + }); + + const { navigate } = asyncEvents(page); + + try { + // First, register a credential we can use later (resident passkey) + await test.step('Navigate to registration journey (usernameless)', async () => { + await navigate('/?clientId=TEST_WebAuthn-Registration-UsernameToDevice'); + await expect(page).toHaveURL( + 'http://localhost:5829/?clientId=TEST_WebAuthn-Registration-UsernameToDevice', + ); + }); + + await test.step('Complete primary credentials', async () => { + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Register WebAuthn credential (resident key)', async () => { + // With the virtual authenticator present and presence auto-simulated, + // registration will complete without any OS prompts. + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + }); + + await test.step('Logout after registration', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + + // Now authenticate via usernameless WebAuthn (username from device) + await test.step('Navigate to usernameless authentication journey', async () => { + await navigate('/?clientId=TEST_WebAuthnAuthentication_Usernameless'); + await expect(page).toHaveURL( + 'http://localhost:5829/?clientId=TEST_WebAuthnAuthentication_Usernameless', + ); + }); + + await test.step('Authenticate WebAuthn credential (usernameless)', async () => { + if (!cdp || !authenticatorId) { + throw new Error('Expected CDP session and authenticator to be configured'); + } + // Server UV is Discouraged; UV=true on the device is acceptable. + await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: true }); + // With the virtual authenticator present and presence auto-simulated, + // authentication will complete without any OS prompts. + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + }); + + await test.step('Logout after authentication', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + } finally { + if (!cdp) { + return; + } + + const activeCdp = cdp; + await test.step('Remove virtual authenticator', async () => { + if (authenticatorId) { + await activeCdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }); + } + if (webauthnEnabled) { + await activeCdp.send('WebAuthn.disable'); + } + }); + } +}); diff --git a/e2e/journey-suites/src/webauthn.test.ts b/e2e/journey-suites/src/webauthn.test.ts new file mode 100644 index 0000000000..0d7f1f9b91 --- /dev/null +++ b/e2e/journey-suites/src/webauthn.test.ts @@ -0,0 +1,109 @@ +import { test, expect } from '@playwright/test'; +import type { CDPSession } from 'playwright'; +import { asyncEvents } from './utils/async-events.js'; +import { password, username } from './utils/demo-user.js'; + +test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available + +test('Register and authenticate with webauthn device', async ({ page, context }) => { + let cdp: CDPSession | undefined; + let authenticatorId: string | undefined; + let webauthnEnabled = false; + let recordedCredentialIds: string[] = []; + + await test.step('Configure virtual authenticator', async () => { + cdp = await context.newCDPSession(page); + await cdp.send('WebAuthn.enable'); + webauthnEnabled = true; + + // A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys. + const response = await cdp.send('WebAuthn.addVirtualAuthenticator', { + options: { + protocol: 'ctap2', + transport: 'internal', // platform authenticator + hasResidentKey: true, // allow discoverable credentials (passkeys) + hasUserVerification: true, // device supports UV + isUserVerified: true, // simulate successful UV (PIN/biometric) + automaticPresenceSimulation: true, // auto "touch"/presence + }, + }); + authenticatorId = response.authenticatorId; + + // (Optional) If your server demands toggling UV during tests: + // await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: true }); + }); + + const { navigate } = asyncEvents(page); + + try { + await test.step('Navigate to registration journey', async () => { + await navigate('/?clientId=TEST_WebAuthn-Registration'); + await expect(page).toHaveURL('http://localhost:5829/?clientId=TEST_WebAuthn-Registration'); + }); + + await test.step('Complete primary credentials', async () => { + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Register WebAuthn credential', async () => { + // With the virtual authenticator present and presence auto-simulated, + // registration will complete without any OS prompts. + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + }); + + await test.step('Capture virtual authenticator credentials', async () => { + if (!cdp || !authenticatorId) { + throw new Error('Expected CDP session and authenticator to be configured'); + } + const { credentials } = await cdp.send('WebAuthn.getCredentials', { authenticatorId }); + recordedCredentialIds = (credentials || []).map((item) => item.credentialId); + expect(recordedCredentialIds.length).toBeGreaterThan(0); + }); + + await test.step('Logout after registration', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + + await test.step('Navigate to authentication journey', async () => { + await navigate('/?clientId=TEST_WebAuthnAuthentication'); + await expect(page).toHaveURL('http://localhost:5829/?clientId=TEST_WebAuthnAuthentication'); + }); + + await test.step('Complete username credential', async () => { + await page.getByLabel('User Name').fill(username); + await page.getByRole('button', { name: 'Submit' }).click(); + }); + + await test.step('Authenticate WebAuthn credential', async () => { + if (!cdp || !authenticatorId) { + throw new Error('Expected CDP session and authenticator to be configured'); + } + await cdp.send('WebAuthn.setUserVerified', { authenticatorId, isUserVerified: true }); + // With the virtual authenticator present and presence auto-simulated, + // authentication will complete without any OS prompts. + await expect(page.getByRole('button', { name: 'Logout' })).toBeVisible(); + }); + + await test.step('Logout after authentication', async () => { + await page.getByRole('button', { name: 'Logout' }).click(); + await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + }); + } finally { + if (!cdp) { + return; + } + + const activeCdp = cdp; + await test.step('Remove virtual authenticator', async () => { + if (authenticatorId) { + await activeCdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }); + } + if (webauthnEnabled) { + await activeCdp.send('WebAuthn.disable'); + } + }); + } +}); diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json index 6be9cf4bd2..89dc608eeb 100644 --- a/packages/journey-client/package.json +++ b/packages/journey-client/package.json @@ -18,7 +18,6 @@ "types": "./dist/src/index.d.ts", "scripts": { "build": "pnpm nx nxBuild", - "lint": "pnpm nx nxLint", "test": "pnpm nx nxTest", "test:watch": "pnpm nx nxTest --watch" }, diff --git a/packages/journey-client/src/lib/journey.store.ts b/packages/journey-client/src/lib/journey.store.ts index f9ba9e9ce3..3b74f1f695 100644 --- a/packages/journey-client/src/lib/journey.store.ts +++ b/packages/journey-client/src/lib/journey.store.ts @@ -5,22 +5,22 @@ * of the MIT license. See the LICENSE file for details. */ -import { logger as loggerFn, LogLevel, CustomLogger } from '@forgerock/sdk-logger'; +import { CustomLogger, logger as loggerFn, LogLevel } from '@forgerock/sdk-logger'; import { callbackType } from '@forgerock/sdk-types'; import type { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { GenericError, Step } from '@forgerock/sdk-types'; -import { createJourneyStore } from './journey.store.utils.js'; +import { createStorage } from '@forgerock/storage'; import { journeyApi } from './journey.api.js'; import { setConfig } from './journey.slice.js'; -import { createStorage } from '@forgerock/storage'; +import { createJourneyStore } from './journey.store.utils.js'; import { createJourneyObject } from './journey.utils.js'; -import type { JourneyStep } from './step.utils.js'; -import type { JourneyClientConfig } from './config.types.js'; import type { RedirectCallback } from './callbacks/redirect-callback.js'; -import { NextOptions, StartParam, ResumeOptions } from './interfaces.js'; +import type { JourneyClientConfig } from './config.types.js'; +import { NextOptions, ResumeOptions, StartParam } from './interfaces.js'; +import type { JourneyStep } from './step.utils.js'; export async function journey({ config, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b26150b676..68b2e0bb5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,6 +295,9 @@ importers: '@forgerock/sdk-logger': specifier: workspace:* version: link:../../packages/sdk-effects/logger + '@forgerock/sdk-types': + specifier: workspace:* + version: link:../../packages/sdk-types e2e/journey-suites: {}