Skip to content

Create journey package#412

Merged
ryanbas21 merged 13 commits intomainfrom
create-journey-package
Sep 29, 2025
Merged

Create journey package#412
ryanbas21 merged 13 commits intomainfrom
create-journey-package

Conversation

@ryanbas21
Copy link
Collaborator

@ryanbas21 ryanbas21 commented Sep 16, 2025

JIRA Ticket

https://pingidentity.atlassian.net/browse/SDKS-4044

Description

Initial conversion of Legacy journey module, to new journey module.

693ab22 - is the conversion pre-config changes

Summary by CodeRabbit

  • New Features
    • Introduced Journey Client: a stateful auth client with start/next/redirect/resume flows.
    • Added WebAuthn support (registration/authentication), QR code handling (OTP/Push), and Recovery Codes utilities.
    • Provided rich callback handling (username, password, choices, terms, reCAPTCHA, device profile, etc.).
    • Device profiling utilities to collect browser/device metadata.
  • Documentation
    • New README with installation, usage, and examples.
  • Tests
    • Extensive unit tests for client, callbacks, WebAuthn, QR codes, recovery codes.
  • Chores
    • Improved CI coverage thresholds, ESLint configs, and repository ignores.

@changeset-bot
Copy link

changeset-bot bot commented Sep 16, 2025

🦋 Changeset detected

Latest commit: 5366be3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@forgerock/storage Minor
@forgerock/sdk-utilities Minor
@forgerock/iframe-manager Minor
@forgerock/journey-client Minor
@forgerock/sdk-types Minor
@forgerock/sdk-logger Minor
@forgerock/davinci-client Minor
@forgerock/oidc-client Minor
@forgerock/sdk-oidc Minor
@forgerock/protect Minor
@forgerock/sdk-request-middleware Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Sep 16, 2025

Walkthrough

Introduces a new @forgerock/journey-client package with Redux Toolkit/RTK Query-based journey flow, callback wrappers, WebAuthn, QR Code, Recovery Codes, and device profiling utilities. Updates sdk-types and sdk-utilities exports/types, adjusts ESLint configs, CI/Codecov, tsconfigs, scratchpad usage, and removes legacy docs.

Changes

Cohort / File(s) Summary
Workspace config
nx.json, tsconfig.json, .whitesource, .gitignore, package.json, .github/workflows/ci.yml, codecov.yml
Added Nx TS plugin entry for journey-client; added TS project ref; updated SAST base branch; ignored GEMINI.md and preserved in clean; switched Codecov upload pathing and tightened CI/coverage thresholds.
Changesets
.changeset/*
Added release notes for multiple packages, including new journey-client feature entry and minor bumps.
Scratchpad integration
scratchpad/package.json, scratchpad/src/main.ts, scratchpad/tsconfig.json
Replaced dependency and demo script to use journey-client; added project refs.
Journey Client package setup
packages/journey-client/package.json, README.md, eslint.config.mjs, vite.config.ts, vitest.setup.ts, tsconfig*.json, src/index.ts
New package with ESM config, exports, build/test tooling, linting, and top-level barrel export.
Journey core (API/store/utils)
packages/journey-client/src/lib/journey.api.ts, .../journey.store.ts, .../journey.slice.ts, .../journey-client.ts, .../journey-step.utils.ts, .../journey.utils.ts
Implemented RTK Query endpoints (start/next), Redux store/slice, async journey() factory with start/next/redirect/resume, JourneyStep creation/helpers, and step-type dispatcher.
Callback base and factory
packages/journey-client/src/lib/callbacks/index.ts, .../factory.ts
Added base JourneyCallback with input/output helpers and a factory mapping callback types to concrete classes.
Callback implementations
packages/journey-client/src/lib/callbacks/*-callback.ts
Added concrete callback wrappers: AttributeInput, Choice, Confirmation, DeviceProfile, HiddenValue, KBA Create, Metadata, Name, Password, PingOneProtect (init/eval), PollingWait, ReCaptcha (std/enterprise), Redirect, SelectIdP, SuspendedTextOutput, TermsAndConditions, TextInput, TextOutput, ValidatedCreate(Password/Username).
Callback tests
packages/journey-client/src/lib/callbacks/*.test.ts
Unit tests covering read/write behavior, validation, and special handling across callback types and factory mappings.
Device profiling
packages/journey-client/src/lib/journey-device/*
Added JourneyDevice implementation, defaults, interfaces, sample/mocks, and tests for metadata, logging, identifier, and configuration handling.
WebAuthn utilities
packages/journey-client/src/lib/journey-webauthn/*
Added WebAuthn enums, interfaces, script parsers, helpers, main JourneyWebAuthn class, extensive mocks, and tests for multiple versions/paths.
QR Code utilities
packages/journey-client/src/lib/journey-qrcode/*
Added JourneyQRCode detection/extraction utility, mocks, and tests for OTP/Push flows.
Recovery Codes utilities
packages/journey-client/src/lib/recovery-codes/*
Added JourneyRecoveryCodes with script parsers, mocks, and tests for display/device-name parsing.
Journey result helpers
packages/journey-client/src/lib/journey-login-*.utils.ts + tests
Added factories/types for LoginSuccess/LoginFailure with accessors and policy message processing.
sdk-types updates
packages/sdk-types/src/lib/am-callback.types.ts, .../lib/enums.ts, .../lib/policy.types.ts, .../lib/legacy-config.types.ts, .../src/index.ts, eslint.config.mjs
Exported callbackType map/union; added StepType and PolicyKey; moved/typed CustomPathConfig; renamed callback factory type; re-exported modules; ESLint parser default export fix.
sdk-utilities updates
packages/sdk-utilities/src/index.ts, .../lib/url/index.ts, .../lib/url/url.utils.ts, .../lib/object.utils.ts, .../lib/strings/*, .../lib/constants/index.ts, package.json, tsconfig.lib.json, eslint.config.mjs
Added new utilities (URL parsing/resolve, object helpers, strings.plural, constants), updated exports and package export map; tweaked tsconfig includes; ESLint parser default export fix.
ESLint parser fixes
packages/*/*/eslint.config.mjs
Switched jsonc-eslint-parser usage to its default export across several packages.
Other package tweaks
packages/davinci-client/vite.config.ts, packages/oidc-client/tsconfig.json
Simplified coverage reporters; deduplicated TS project references.
Docs removal
CLAUDE.md
Deleted repository guidance document.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant JC as journey()
  participant Store as Redux Store
  participant API as RTK Query (journeyApi)
  participant AM as Auth Server
  participant Sess as sessionStorage
  Note over JC,Store: Initialization
  App->>JC: journey({ config, requestMiddleware, logger })
  JC->>Store: configureStore(extra: {config, mw, logger})
  JC-->>App: { start, next, redirect, resume }

  rect rgba(200,230,255,0.3)
  Note right of App: Start
  App->>JC: start(options?)
  JC->>API: start.initiate(options)
  API->>AM: POST /json/realms/.../authenticate
  AM-->>API: Step JSON
  API-->>JC: data: Step
  JC-->>App: JourneyStep
  end

  rect rgba(200,255,200,0.3)
  Note right of App: Next
  App->>JC: next(step, options?)
  JC->>API: next.initiate({step, options})
  API->>AM: POST /json/realms/.../authenticate (with step)
  AM-->>API: Step/Outcome JSON
  API-->>JC: data: Step
  JC-->>App: JourneyStep/LoginSuccess/LoginFailure
  end

  rect rgba(255,245,200,0.5)
  Note right of App: Redirect
  App->>JC: redirect(step)
  JC->>Sess: setItem('previousStep', step.payload)
  JC->>App: window.location.assign(redirectUrl)
  end

  rect rgba(255,220,220,0.4)
  Note right of App: Resume
  App->>JC: resume(url, options?)
  JC->>Sess: getItem('previousStep')
  alt previousStep exists
    JC->>API: next.initiate({ step: previousStep, options+query })
  else no previousStep
    JC->>API: start.initiate(options+query)
  end
  API->>AM: POST authenticate
  AM-->>API: Step/Outcome JSON
  API-->>JC: data: Step
  JC->>Sess: removeItem('previousStep')
  JC-->>App: JourneyStep/LoginSuccess/LoginFailure
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Poem

In burrows of code, I hop with delight,
New journeys await in Redux-lit night.
WebAuthn whispers, QR glows bright,
Policies parsed, devices in sight.
With tests like carrots, crisp and tight—
Ship it, my friends; the path feels right. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description includes the JIRA ticket and a brief summary of converting the legacy module, but it does not follow the repository’s template by omitting any confirmation of an added changeset and lacks detail on the specific configuration and code changes introduced. It fails to address the “Did you add a changeset?” prompt and does not elaborate on high-level or file-level changes beyond referencing a single commit. As a result, reviewers lack the structured overview required by the template. Please update the description to match the template by adding a “Did you add a changeset?” section (or confirming none was needed) and summarizing the key changes, such as the files and features introduced, configuration updates, and any impact on existing workflows.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title “Create journey package” is a concise, single sentence that directly summarizes the primary change of introducing the new journey package. It clearly reflects the main intent of the pull request without extraneous detail. A teammate reviewing history would understand at a glance that the PR adds or establishes the journey package.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch create-journey-package

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14ef786 and 5366be3.

📒 Files selected for processing (1)
  • packages/journey-client/package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/journey-client/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

🧹 Nitpick comments (99)
packages/journey-client/README.md (1)

5-7: Round out README with usage/testing basics

Add quick “Install/Usage” and “Test” sections so consumers know how to run and integrate the package.

 ## Building

 Run `nx build journey-client` to build the library.
+
+## Testing
+
+Run `nx test journey-client` to execute the unit tests with Vitest.
+
+## Usage
+
+Install and import in ESM projects:
+
+```sh
+pnpm add @forgerock/journey-client
+```
+
+```ts
+import { journey } from '@forgerock/journey-client';
+const client = journey({ /* config */ });
+```
GEMINI.md (1)

7-15: Enforce “import type” via tooling

Since the KB mandates import type, add lint + TS settings to keep this consistent.

  • ESLint (in packages/journey-client/eslint.config.mjs):
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default [
  {
    files: ['**/*.ts'],
    plugins: { '@typescript-eslint': tsPlugin },
    rules: {
      '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
    },
  },
];
  • TS (tsconfig for ESM correctness):
{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}
packages/journey-client/src/lib/fr-policy/helpers.ts (1)

11-18: Tighten types and guard for own-properties

Current cast obj[prop] as T is unchecked and prototype props may leak in. Make the map typed to T | undefined and check own-properties.

-function getProp<T>(obj: { [key: string]: unknown } | undefined, prop: string, defaultValue: T): T {
-  if (!obj || obj[prop] === undefined) {
-    return defaultValue;
-  }
-  return obj[prop] as T;
-}
+function getProp<T>(
+  obj: Record<string, T | undefined> | undefined,
+  prop: string,
+  defaultValue: T,
+): T {
+  if (!obj) return defaultValue;
+  if (!Object.prototype.hasOwnProperty.call(obj, prop)) return defaultValue;
+  const value = obj[prop];
+  return value === undefined ? defaultValue : value;
+}
packages/journey-client/src/lib/config.types.ts (2)

10-13: Broaden query type and document serialization semantics.

Journeys often need arrays and non-string values. Recommend supporting string | number | boolean | string[] and clarifying URL encoding/merging behavior.

 export interface JourneyClientConfig extends LegacyConfigOptions {
-  query?: Record<string, string>;
-  // Add any journey-specific config options here
+  /**
+   * Optional query params appended to journey requests.
+   * Values are serialized with URLSearchParams; arrays repeat the key.
+   */
+  query?: Readonly<Record<string, string | number | boolean | string[]>>;
+  // Add any journey-specific config options here.
 }

15-15: Re-export looks good; ensure it’s surfaced at the package root.

Re-exporting StepOptions here is fine. Also export it from packages/journey-client/src/index.ts to keep the public API centralized.

packages/journey-client/GEMINI.md (2)

26-33: Document FRDevice fallback/opt‑out.

Add a short note on behavior when device APIs are denied/unavailable (e.g., skip profile, partial profile, or fail request).


71-74: Add config/usage examples.

Include a minimal code snippet showing journey({ realmPath, tree, query }) and how query is serialized.

packages/journey-client/src/lib/fr-device/sample-profile.json (1)

41-44: Sanitize or clearly mark location as dummy data.

If this file is used in examples/tests, ensure coordinates aren’t real or add a comment/readme stating they’re synthetic.

packages/journey-client/src/lib/fr-device/collector.ts (3)

31-35: Unify null/undefined handling for dotted props.

reduceToObject returns '' for missing dotted props but null for top‑level props. This inconsistency will surprise consumers and can coerce nullish values to empty strings. Prefer consistent nullish semantics.

Apply:

-          const prop = (src[prop1] as Record<string, unknown>)?.[prop2];
-          prev[prop2] = prop != undefined ? (prop as string | number | null) : '';
+          const prop = (src[prop1] as Record<string, unknown>)?.[prop2];
+          prev[prop2] =
+            prop !== undefined && prop !== null ? (prop as string | number | null) : null;

27-33: Avoid key collisions and fragile two‑level split.

  • Using prop2 as the key can overwrite prior entries if two inputs share the same second segment (e.g., a.id and b.id).
  • split('.') only supports exactly two segments; a.b.c will silently ignore deeper nesting.

Consider using the full path as the key (e.g., prev[curr]) or a safer accessor that walks all segments, and document/validate against collisions.


47-52: Guard against missing entries in reduceToString.

src[curr] can be undefined and will throw on .filename. Skip absent entries.

Apply:

-  return props.reduce((prev, curr) => {
-    prev = `${prev}${src[curr].filename};`;
-    return prev;
-  }, '');
+  return props.reduce((prev, curr) => {
+    const filename = src[curr]?.filename;
+    return filename ? `${prev}${filename};` : prev;
+  }, '');
packages/journey-client/src/lib/url.utils.ts (3)

24-36: Leverage WHATWG URL resolution.

Manual path splicing misses relative segments like ./ and .. and edge cases. Let URL do the work.

Apply:

-function resolve(baseUrl: string, path: string): string {
-  const url = new URL(baseUrl);
-
-  if (path.startsWith('/')) {
-    return `${getBaseUrl(url)}${path}`;
-  }
-
-  const basePath = url.pathname.split('/');
-  const destPath = path.split('/').filter((x) => !!x);
-  const newPath = [...basePath.slice(0, -1), ...destPath].join('/');
-
-  return `${getBaseUrl(url)}${newPath}`;
-}
+function resolve(baseUrl: string, path: string): string {
+  return new URL(path, baseUrl).toString();
+}

45-53: Encode keys and restrict to own properties.

Current stringify only encodes values and iterates inherited props.

Apply:

-function stringify(data: Record<string, string | undefined>): string {
-  const pairs: string[] = [];
-  for (const k in data) {
-    if (data[k]) {
-      pairs.push(k + '=' + encodeURIComponent(data[k] as string));
-    }
-  }
-  return pairs.join('&');
-}
+function stringify(data: Record<string, string | undefined>): string {
+  const params = new URLSearchParams();
+  for (const [k, v] of Object.entries(data)) {
+    if (v !== undefined && v !== null) params.append(k, v);
+  }
+  return params.toString();
+}

55-55: Deduplicate URL helpers across packages.

This module duplicates packages/sdk-utilities/src/lib/url/url.utils.ts. Prefer a single source (sdk-utilities) and re-export from here to prevent drift.

packages/sdk-utilities/src/lib/url/url.utils.ts (3)

24-36: Prefer standard URL resolution.

Mirror the resolve refactor suggested for journey-client to handle ./ and ...

-function resolve(baseUrl: string, path: string): string {
-  const url = new URL(baseUrl);
-  if (path.startsWith('/')) {
-    return `${getBaseUrl(url)}${path}`;
-  }
-  const basePath = url.pathname.split('/');
-  const destPath = path.split('/').filter((x) => !!x);
-  const newPath = [...basePath.slice(0, -1), ...destPath].join('/');
-  return `${getBaseUrl(url)}${newPath}`;
-}
+function resolve(baseUrl: string, path: string): string {
+  return new URL(path, baseUrl).toString();
+}

45-53: Encode keys and iterate own props only.

Align stringify with the standard approach.

Same diff as proposed for journey-client’s stringify.


55-55: Make this the canonical URL utils and re-export elsewhere.

Given both modules are identical, keep this copy in sdk-utilities and import/re-export from journey-client to avoid drift.

packages/journey-client/src/lib/fr-policy/enums.ts (1)

11-33: Enum looks good; minor naming polish optional.

Consider AtLeastCapitalLetters / AtLeastNumbers to mirror values AT_LEAST_*, but this is cosmetic.

packages/sdk-types/src/lib/am-callback.types.ts (2)

70-85: Make Step an AuthResponse for consistency.

AuthResponse introduces type: StepType; Step should include it to unify response handling.

Apply:

-export interface Step {
+export interface Step extends AuthResponse {
   authId?: string;
   callbacks?: Callback[];
   code?: number;
   description?: string;
   detail?: StepDetail;
   header?: string;
   message?: string;
   ok?: string;
   realm?: string;
   reason?: string;
   stage?: string;
   status?: number;
   successUrl?: string;
   tokenId?: string;
 }

90-94: Reuse FailureDetail in StepDetail.

Avoid duplicating failureUrl.

Apply:

-export interface StepDetail {
-  failedPolicyRequirements?: FailedPolicyRequirement[];
-  failureUrl?: string;
-  result?: boolean;
-}
+export interface StepDetail extends FailureDetail {
+  failedPolicyRequirements?: FailedPolicyRequirement[];
+  result?: boolean;
+}
packages/journey-client/src/lib/interfaces.ts (1)

10-13: Prefer readonly fields to prevent accidental mutation

Safer for consumers; no behavioral change.

-export interface NextOptions {
-  step: Step;
-  options?: StepOptions;
-}
+export interface NextOptions {
+  readonly step: Step;
+  readonly options?: StepOptions;
+}
packages/sdk-types/src/lib/legacy-config.types.ts (4)

54-56: Consider widening query value types

Real‑world query params are often numbers/booleans or repeated values.

-export interface StepOptions extends LegacyConfigOptions {
-  query?: Record<string, string>;
-}
+export interface StepOptions extends LegacyConfigOptions {
+  query?: Record<string, string | number | boolean | string[]>;
+}

89-89: Avoid any in factory return type

Generic or unknown improves type safety without constraining consumers.

-type FRCallbackFactory = (callback: Callback) => any;
+type FRCallbackFactory<T = unknown> = (callback: Callback) => T;

39-41: Orphaned doc block

This comment references custom paths but no adjacent type remains after the refactor. Remove or move it next to CustomPathConfig usage.


47-48: Stray TODO

The “needs logger?” note should be tracked or removed.

I can open an issue proposing a minimal logger interface and wiring—want me to draft it?

packages/sdk-utilities/src/index.ts (1)

3-3: License header typo

Should be “All rights reserved.”

- * Copyright © 2025 Ping Identity Corporation. All right reserved.
+ * Copyright © 2025 Ping Identity Corporation. All rights reserved.
packages/journey-client/src/lib/strings.utils.ts (2)

16-21: De-duplicate: reuse shared plural() from sdk-utilities

This implementation duplicates the same helper added under sdk-utilities. Centralize to the shared package to avoid drift and inconsistencies.

Apply this diff:

-/**
- * @module
- * @ignore
- * These are private utility functions
- */
-export function plural(n: number, singularText: string, pluralText?: string): string {
-  if (n === 1) {
-    return singularText;
-  }
-  return pluralText !== undefined ? pluralText : singularText + 's';
-}
+// Delegate to shared implementation to avoid duplication
+export { plural } from '@forgerock/sdk-utilities';

4-4: Fix header filename to match source

Update comment from "strings.ts" to "strings.utils.ts" to avoid confusion.

- * strings.ts
+ * strings.utils.ts
packages/journey-client/src/lib/callbacks/text-input-callback.test.ts (1)

4-4: Correct the file header

Header says "attribute-input-callback.test.ts"; update to the actual filename.

- * attribute-input-callback.test.ts
+ * text-input-callback.test.ts
packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (1)

18-33: Align test payload with typical AM NameCallback input name

Real NameCallback payloads usually use "IDToken1" as the input name. Using that here will make the test closer to production payloads.

-          name: 'userName',
+          name: 'IDToken1',
packages/sdk-utilities/src/lib/strings/strings.utils.ts (2)

4-4: Fix header filename to match source

Rename header from "strings.ts" to "strings.utils.ts".

- * strings.ts
+ * strings.utils.ts

16-21: Consider minimal English inflection or document limitation

The naive "add 's'" rule can produce awkward plurals (e.g., "classs"). Either document that this is English‑only and simplistic, or extend to a minimal set of rules, and add unit tests.

I can add a focused unit test file (e.g., strings.utils.test.ts) with edge cases if you want.

packages/journey-client/tsconfig.spec.json (1)

4-21: Don’t emit declarations in test builds

Generating d.ts for tests slows CI and clutters outputs. Disable declaration here; keep it enabled in lib config.

-    "declaration": true
+    "declaration": false
packages/journey-client/tsconfig.json (1)

5-27: Remove duplicate cross-package references from packages/journey-client/tsconfig.json

packages/journey-client/tsconfig.json duplicates package refs that are already declared in tsconfig.lib.json; keep only ./tsconfig.lib.json and ./tsconfig.spec.json.

  "references": [
-    {
-      "path": "../sdk-effects/storage"
-    },
-    {
-      "path": "../sdk-effects/sdk-request-middleware"
-    },
-    {
-      "path": "../sdk-utilities"
-    },
-    {
-      "path": "../sdk-types"
-    },
-    {
-      "path": "../sdk-effects/logger"
-    },
     {
       "path": "./tsconfig.lib.json"
     },
     {
       "path": "./tsconfig.spec.json"
     }
   ]
packages/journey-client/src/lib/shared/constants.ts (1)

1-4: Inline export the constants with literal types for better tree‑shaking and type narrowing

This avoids a separate export block and preserves the literal types.

-const REQUESTED_WITH = 'forgerock-sdk';
-const X_REQUESTED_PLATFORM = 'javascript';
-
-export { REQUESTED_WITH, X_REQUESTED_PLATFORM };
+export const REQUESTED_WITH = 'forgerock-sdk' as const;
+export const X_REQUESTED_PLATFORM = 'javascript' as const;
packages/journey-client/src/lib/callbacks/redirect-callback.ts (1)

24-26: Don’t default missing redirectUrl to empty string

Returning '' blurs “absent” vs “present” and can cause accidental no‑op navigations. Prefer undefined and a union return type.

-  public getRedirectUrl(): string {
-    return this.getOutputByName<string>('redirectUrl', '');
-  }
+  public getRedirectUrl(): string | undefined {
+    return this.getOutputByName<string | undefined>('redirectUrl', undefined);
+  }
packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts (1)

16-59: Avoid cross‑test mutation by creating a fresh payload per test

The shared top‑level payload is mutated, coupling tests. Use a factory to generate a new payload each time.

-  const payload: Callback = {
+  const makePayload = (): Callback => ({
     type: callbackType.ValidatedCreateUsernameCallback,
     output: [
       {
         name: 'echoOn',
         value: false,
       },
       {
         name: 'required',
         value: true,
       },
       {
         name: 'policies',
         value: {
           policyRequirements: ['a', 'b'],
           name: 'username',
           policies: [],
         },
       },
       {
         name: 'failedPolicies',
         value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })],
       },
       {
         name: 'validateOnly',
         value: false,
       },
       {
         name: 'prompt',
         value: 'Username',
       },
     ],
     input: [
       {
         name: 'IDToken2',
         value: '',
       },
       {
         name: 'IDToken2validateOnly',
         value: false,
       },
     ],
     _id: 1,
-  };
+  });
 
   it('reads/writes basic properties with "validate only"', () => {
-    const cb = new ValidatedCreateUsernameCallback(payload);
+    const cb = new ValidatedCreateUsernameCallback(makePayload());
@@
   it('writes validate only to `false` for submission', () => {
-    const cb = new ValidatedCreateUsernameCallback(payload);
+    const cb = new ValidatedCreateUsernameCallback(makePayload());

Also applies to: 62-62, 77-77

packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts (1)

11-15: Isolate tests and use type‑only import

Use beforeEach with a fresh payload to avoid shared state; import Callback as a type for cleaner ESM.

-import { describe, expect, it, beforeAll } from 'vitest';
+import { describe, expect, it, beforeEach } from 'vitest';
@@
-import { Callback } from '@forgerock/sdk-types';
+import type { Callback } from '@forgerock/sdk-types';
@@
-beforeAll(() => {
-  callback = new ReCaptchaEnterpriseCallback(recaptchaCallback);
-});
+beforeEach(() => {
+  const payload = JSON.parse(JSON.stringify(recaptchaCallback));
+  callback = new ReCaptchaEnterpriseCallback(payload);
+});

Also applies to: 53-55

packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts (2)

4-4: Fix filename in header comment

Minor typo: “intitialize” → “initialize”.

- * ping-protect-intitialize-callback.test.ts
+ * ping-protect-initialize-callback.test.ts

75-79: Drop trivial spy on a method you invoke directly

Asserting the spy was called right after calling getConfig() adds no value—assert the returned object only.

-    const mock = vi.spyOn(callback, 'getConfig');
-    const config = callback.getConfig();
-    expect(mock).toHaveBeenCalled();
+    const config = callback.getConfig();
packages/journey-client/src/lib/fr-policy/interfaces.ts (1)

9-16: Tighten types for better readability and intent.

Consider Record aliases and readonly arrays to make usage clearer.

-interface MessageCreator {
-  [key: string]: (propertyName: string, params?: { [key: string]: unknown }) => string;
-}
+type MessageCreator = Record<
+  string,
+  (propertyName: string, params?: Record<string, unknown>) => string
+>;
 
-interface ProcessedPropertyError {
-  detail: FailedPolicyRequirement;
-  messages: string[];
-}
+interface ProcessedPropertyError {
+  detail: FailedPolicyRequirement;
+  messages: readonly string[];
+}
packages/journey-client/src/lib/fr-device/device-profile.test.ts (1)

35-37: Avoid brittle assertions tied to jsdom defaults.

Vendor/appName/appVersion vary by environment. Prefer truthiness/type checks to reduce flakiness.

-    expect(appName).toBe('Netscape');
-    expect(appVersion).toBe('4.0');
-    expect(vendor).toBe('Apple Computer, Inc.');
+    expect(typeof appName).toBe('string');
+    expect(typeof appVersion).toBe('string');
+    expect(typeof vendor).toBe('string');
+    expect(appName.length).toBeGreaterThan(0);
+    expect(appVersion.length).toBeGreaterThan(0);
+    expect(vendor.length).toBeGreaterThan(0);
packages/journey-client/src/lib/callbacks/metadata-callback.ts (1)

24-26: Defaulting to {} risks shape mismatches; prefer nullable/undefined fallback.

If data can be absent, returning {} typed as T can mask errors. Consider returning T | undefined (or T | null) and letting callers handle absence.

Can FRCallback.getOutputByName omit the default argument? If so, a safer variant would be:

  • public getData<T = unknown>(): T | undefined and delegate without a default; otherwise, pass undefined as unknown as T (or null) explicitly.
packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts (1)

61-77: Avoid shared mutable fixtures between tests; use fresh payloads and constant type checks.

Tests currently mutate a shared payload, coupling test order. Also prefer enum/const for type string comparison.

 describe('AttributeInputCallback', () => {
-  const payload: Callback = {
+  const payload: Callback = {
@@
-  it('reads/writes basic properties with "validate only"', () => {
-    const cb = new AttributeInputCallback<string>(payload);
+  const makePayload = (): Callback => JSON.parse(JSON.stringify(payload)) as Callback;
+
+  it('reads/writes basic properties with "validate only"', () => {
+    const cb = new AttributeInputCallback<string>(makePayload());
@@
-    expect(cb.getType()).toBe('StringAttributeInputCallback');
+    expect(cb.getType()).toBe(callbackType.StringAttributeInputCallback);
@@
   });
 
   it('writes validate only to `false` for submission', () => {
-    const cb = new AttributeInputCallback<string>(payload);
+    const cb = new AttributeInputCallback<string>(makePayload());
@@
   });

Also applies to: 79-86

packages/journey-client/src/lib/callbacks/polling-wait-callback.ts (1)

31-33: Nit: unnecessary Number() coercion if backend guarantees numeric waitTime.

If AM always returns a number for waitTime, drop the Number(...) to avoid hiding bad data. If it can be a string, keep as-is.

packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (1)

34-45: Confirm setInputValue supports RegExp selectors; otherwise use explicit name matching.

Passing /signals/ and /clientError/ assumes the base supports regex-based targeting. If it expects exact names or paths, these calls may no-op.

If regex is not supported, target by suffix or exact name (example using suffix strings):

-  public setData(data: string): void {
-    this.setInputValue(data, /signals/);
-  }
+  public setData(data: string): void {
+    this.setInputValue(data, 'signals'); // or 'IDToken1signals' if exact matching is required
+  }
@@
-  public setClientError(errorMessage: string): void {
-    this.setInputValue(errorMessage, /clientError/);
-  }
+  public setClientError(errorMessage: string): void {
+    this.setInputValue(errorMessage, 'clientError'); // or exact input name
+  }
packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1)

46-48: Accept pre‑stringified profiles to avoid double JSON encoding

Allow strings in addition to objects; normalize before setting input.

-  public setProfile(profile: DeviceProfileData): void {
-    this.setInputValue(JSON.stringify(profile));
-  }
+  public setProfile(profile: DeviceProfileData | string): void {
+    const value = typeof profile === 'string' ? profile : JSON.stringify(profile);
+    this.setInputValue(value);
+  }
packages/journey-client/src/lib/callbacks/text-output-callback.ts (1)

31-33: messageType may be numeric in AM; normalize to string to avoid type drift

Some servers emit numeric codes (0/1/2). Coerce safely to string for consistent API.

-  public getMessageType(): string {
-    return this.getOutputByName<string>('messageType', '');
-  }
+  public getMessageType(): string {
+    const v = this.getOutputByName<unknown>('messageType', '');
+    return typeof v === 'string' ? v : String(v ?? '');
+  }
packages/journey-client/src/lib/fr-login-success.ts (1)

19-19: Prevent accidental mutation: make payload readonly

Small safety improvement; mirrors other immutable wrappers.

-  constructor(public payload: Step) {}
+  constructor(public readonly payload: Step) {}
packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts (2)

19-27: Strengthen assertion: verify returned value, not just that method was called

Spying on the same instance method adds little. Assert the boolean directly.

-  it('should test that the pauseBehavior method can be called', () => {
+  it('should return pauseBehavioralData flag', () => {
     const callback = new PingOneProtectEvaluationCallback({
       type: callbackType.PingOneProtectEvaluationCallback,
       output: [{ name: 'pauseBehavioralData', value: true }],
     });
-    const mock = vi.spyOn(callback, 'getPauseBehavioralData');
-    callback.getPauseBehavioralData();
-    expect(mock).toHaveBeenCalled();
+    expect(callback.getPauseBehavioralData()).toBe(true);
   });

43-46: Remove unnecessary spies; assert outcomes instead

Direct assertions keep tests lean and focused on behavior.

-    const mock = vi.spyOn(callback, 'setData');
-    callback.setData('data');
-    expect(mock).toHaveBeenCalledWith('data');
+    callback.setData('data');
     expect(callback.getInputValue('IDToken1signals')).toBe('data');
-    const mock = vi.spyOn(callback, 'setClientError');
-    callback.setClientError('error i just set');
-    expect(mock).toHaveBeenCalledWith('error i just set');
+    callback.setClientError('error i just set');
     expect(callback.getInputValue('IDToken1clientError')).toBe('error i just set');

Also applies to: 63-66

packages/journey-client/src/lib/callbacks/factory.ts (1)

39-91: Optional: replace switch with constructor map for easier extensibility

A lookup map reduces churn when adding new callbacks and helps avoid sprawling switches. Behavior remains identical.

-function createCallback(callback: Callback): FRCallback {
-  switch (callback.type) {
-    case callbackType.BooleanAttributeInputCallback:
-      return new AttributeInputCallback<boolean>(callback);
-    case callbackType.ChoiceCallback:
-      return new ChoiceCallback(callback);
-    case callbackType.ConfirmationCallback:
-      return new ConfirmationCallback(callback);
-    case callbackType.DeviceProfileCallback:
-      return new DeviceProfileCallback(callback);
-    case callbackType.HiddenValueCallback:
-      return new HiddenValueCallback(callback);
-    case callbackType.KbaCreateCallback:
-      return new KbaCreateCallback(callback);
-    case callbackType.MetadataCallback:
-      return new MetadataCallback(callback);
-    case callbackType.NameCallback:
-      return new NameCallback(callback);
-    case callbackType.NumberAttributeInputCallback:
-      return new AttributeInputCallback<number>(callback);
-    case callbackType.PasswordCallback:
-      return new PasswordCallback(callback);
-    case callbackType.PingOneProtectEvaluationCallback:
-      return new PingOneProtectEvaluationCallback(callback);
-    case callbackType.PingOneProtectInitializeCallback:
-      return new PingOneProtectInitializeCallback(callback);
-    case callbackType.PollingWaitCallback:
-      return new PollingWaitCallback(callback);
-    case callbackType.ReCaptchaCallback:
-      return new ReCaptchaCallback(callback);
-    case callbackType.ReCaptchaEnterpriseCallback:
-      return new ReCaptchaEnterpriseCallback(callback);
-    case callbackType.RedirectCallback:
-      return new RedirectCallback(callback);
-    case callbackType.SelectIdPCallback:
-      return new SelectIdPCallback(callback);
-    case callbackType.StringAttributeInputCallback:
-      return new AttributeInputCallback<string>(callback);
-    case callbackType.SuspendedTextOutputCallback:
-      return new SuspendedTextOutputCallback(callback);
-    case callbackType.TermsAndConditionsCallback:
-      return new TermsAndConditionsCallback(callback);
-    case callbackType.TextInputCallback:
-      return new TextInputCallback(callback);
-    case callbackType.TextOutputCallback:
-      return new TextOutputCallback(callback);
-    case callbackType.ValidatedCreatePasswordCallback:
-      return new ValidatedCreatePasswordCallback(callback);
-    case callbackType.ValidatedCreateUsernameCallback:
-      return new ValidatedCreateUsernameCallback(callback);
-    default:
-      return new FRCallback(callback);
-  }
-}
+const ctorMap = {
+  [callbackType.BooleanAttributeInputCallback]: AttributeInputCallback<boolean>,
+  [callbackType.ChoiceCallback]: ChoiceCallback,
+  [callbackType.ConfirmationCallback]: ConfirmationCallback,
+  [callbackType.DeviceProfileCallback]: DeviceProfileCallback,
+  [callbackType.HiddenValueCallback]: HiddenValueCallback,
+  [callbackType.KbaCreateCallback]: KbaCreateCallback,
+  [callbackType.MetadataCallback]: MetadataCallback,
+  [callbackType.NameCallback]: NameCallback,
+  [callbackType.NumberAttributeInputCallback]: AttributeInputCallback<number>,
+  [callbackType.PasswordCallback]: PasswordCallback,
+  [callbackType.PingOneProtectEvaluationCallback]: PingOneProtectEvaluationCallback,
+  [callbackType.PingOneProtectInitializeCallback]: PingOneProtectInitializeCallback,
+  [callbackType.PollingWaitCallback]: PollingWaitCallback,
+  [callbackType.ReCaptchaCallback]: ReCaptchaCallback,
+  [callbackType.ReCaptchaEnterpriseCallback]: ReCaptchaEnterpriseCallback,
+  [callbackType.RedirectCallback]: RedirectCallback,
+  [callbackType.SelectIdPCallback]: SelectIdPCallback,
+  [callbackType.StringAttributeInputCallback]: AttributeInputCallback<string>,
+  [callbackType.SuspendedTextOutputCallback]: SuspendedTextOutputCallback,
+  [callbackType.TermsAndConditionsCallback]: TermsAndConditionsCallback,
+  [callbackType.TextInputCallback]: TextInputCallback,
+  [callbackType.TextOutputCallback]: TextOutputCallback,
+  [callbackType.ValidatedCreatePasswordCallback]: ValidatedCreatePasswordCallback,
+  [callbackType.ValidatedCreateUsernameCallback]: ValidatedCreateUsernameCallback,
+} as const;
+
+function createCallback(callback: Callback): FRCallback {
+  const Ctor = (ctorMap as Record<string, new (c: Callback) => FRCallback>)[callback.type];
+  return Ctor ? new Ctor(callback) : new FRCallback(callback);
+}
packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts (2)

16-59: Avoid shared mutable fixture between tests

Both tests mutate the same payload object, creating order-coupling. Use a factory to return a fresh payload per test.

Apply this diff:

-  const payload: Callback = {
+  const makePayload = (): Callback => ({
     type: callbackType.ValidatedCreatePasswordCallback,
     output: [
       {
         name: 'echoOn',
         value: false,
       },
       {
         name: 'required',
         value: true,
       },
       {
         name: 'policies',
         value: {
           policyRequirements: ['a', 'b'],
           name: 'password',
           policies: [],
         },
       },
       {
         name: 'failedPolicies',
         value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })],
       },
       {
         name: 'validateOnly',
         value: false,
       },
       {
         name: 'prompt',
         value: 'Password',
       },
     ],
     input: [
       {
         name: 'IDToken2',
         value: '',
       },
       {
         name: 'IDToken2validateOnly',
         value: false,
       },
     ],
     _id: 1,
-  };
+  });
@@
-    const cb = new ValidatedCreatePasswordCallback(payload);
+    const cb = new ValidatedCreatePasswordCallback(makePayload());
@@
-    const cb = new ValidatedCreatePasswordCallback(payload);
+    const cb = new ValidatedCreatePasswordCallback(makePayload());

Also applies to: 62-64, 77-79


71-74: Prefer explicit assertions over throwing in tests

Use expect guards instead of manual throws for undefined input to keep failures readable.

Example:

-    if (!cb.payload.input) throw new Error('Input is not defined');
-    expect(cb.payload.input[0].value).toBe('abcd123');
+    expect(cb.payload.input).toBeDefined();
+    expect(cb.payload.input![0].value).toBe('abcd123');

Also applies to: 79-81

packages/journey-client/src/lib/callbacks/index.ts (2)

32-34: Allow RegExp selectors for getInputValue/getOutputValue (symmetry with setInputValue)

setInputValue already supports RegExp; make getters consistent.

Apply this diff:

-  public getInputValue(selector: number | string = 0): unknown {
+  public getInputValue(selector: number | string | RegExp = 0): unknown {
     return this.getArrayElement(this.payload.input, selector).value;
   }
@@
-  public getOutputValue(selector: number | string = 0): unknown {
+  public getOutputValue(selector: number | string | RegExp = 0): unknown {
     return this.getArrayElement(this.payload.output, selector).value;
   }

Also applies to: 52-54


82-88: Neutralize error text for input/output reuse

getArrayElement is used for both input and output. Error says “input entry” even when searching outputs.

Apply this diff:

-      if (!input) {
-        throw new Error(`Missing callback input entry "${selector}"`);
-      }
+      if (!input) {
+        throw new Error(`Missing callback entry "${selector}"`);
+      }
@@
-      if (!input) {
-        throw new Error(`Missing callback input entry "${selector}"`);
-      }
+      if (!input) {
+        throw new Error(`Missing callback entry "${selector}"`);
+      }

Also applies to: 90-97

packages/journey-client/src/lib/callbacks/recaptcha-callback.ts (1)

31-33: Make setResult selector‑aware

Inputs can shift; exposing an optional selector improves robustness without breaking callers.

Apply this diff:

-  public setResult(result: string): void {
-    this.setInputValue(result);
-  }
+  public setResult(result: string, selector: number | string | RegExp = 0): void {
+    this.setInputValue(result, selector);
+  }

Please confirm whether the reCAPTCHA input is always at index 0. If not, we can default to a safer selector (e.g., /^IDToken\d+$/) or the exact input name used by AM for this callback.

packages/journey-client/src/lib/fr-login-failure.ts (1)

26-28: Avoid returning NaN when code is absent

Number(undefined) yields NaN. Return a sane default or propagate undefined.

Apply this minimal diff (default to 0):

-  public getCode(): number {
-    return Number(this.payload.code);
-  }
+  public getCode(): number {
+    return this.payload.code ?? 0;
+  }

Alternatively (clearer API), change signature to number | undefined and return this.payload.code;. Let me know if you prefer that; I can update call sites.

packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (2)

46-48: Make setAccepted selector‑aware

Mirror FRCallback’s selector to guard against input order/name changes.

Apply this diff:

-  public setAccepted(accepted = true): void {
-    this.setInputValue(accepted);
-  }
+  public setAccepted(accepted = true, selector: number | string | RegExp = 0): void {
+    this.setInputValue(accepted, selector);
+  }

38-41: Clarify invalid/missing createDate handling

new Date('') yields an Invalid Date object. If createDate can be absent or non‑ISO, consider returning Date | undefined or documenting the behavior.

Do we have guarantees from AM that createDate is always a valid ISO string? If not, I can adjust the API and add tests.

packages/journey-client/src/lib/journey.api.ts (3)

65-66: Rename reducerPath to avoid collisions with non-API slice.

reducerPath: 'journeyReducer' can collide/confuse with your journey state slice. Use a unique API key (convention: '*Api').

-reducerPath: 'journeyReducer',
+reducerPath: 'journeyApi',

Ensure store setup mounts both the API reducer and the journey slice under distinct keys.


114-124: Avoid N duplicated device-profile lookups; fetch once and fan out.

If multiple DeviceProfileCallbacks exist, you call frDevice.getProfile for each. Aggregate requirements, fetch once, and apply to all callbacks.

Apply this diff in both onQueryStarted blocks:

-          const deviceProfileCallbacks = frStepInstance.getCallbacksOfType<DeviceProfileCallback>(
+          const deviceProfileCallbacks = frStepInstance.getCallbacksOfType<DeviceProfileCallback>(
             callbackType.DeviceProfileCallback,
           );
-          for (const cb of deviceProfileCallbacks) {
-            const profile = await frDevice.getProfile({
-              location: cb.isLocationRequired(),
-              metadata: cb.isMetadataRequired(),
-            });
-            cb.setProfile(profile);
-          }
+          const needs = deviceProfileCallbacks.reduce(
+            (acc, cb) => ({
+              location: acc.location || cb.isLocationRequired(),
+              metadata: acc.metadata || cb.isMetadataRequired(),
+            }),
+            { location: false, metadata: false },
+          );
+          if (needs.location || needs.metadata) {
+            const profile = await frDevice.getProfile(needs);
+            for (const cb of deviceProfileCallbacks) {
+              cb.setProfile(profile);
+            }
+          }

Also applies to: 166-176


107-129: Error object shape in state may be inconsistent.

_error can be FetchBaseQueryError or SerializedError, not always Error. Casting to Error may drop detail codes/messages.

  • Introduce a small normalizer (e.g., toJourneyError(e)) to preserve HTTP status, server detail, and message.
  • Store a discriminated union in journey state for better UI handling.

Also applies to: 159-181

packages/journey-client/src/lib/callbacks/choice-callback.ts (2)

45-51: Tighten OOB error to include valid range.

Improves debuggability without changing behavior.

-    if (index < 0 || index > length - 1) {
-      throw new Error(`${index} is out of bounds`);
-    }
+    if (index < 0 || index > length - 1) {
+      throw new Error(`${index} is out of bounds (valid: 0..${length - 1})`);
+    }

56-62: Include available choices in invalid-value error.

Helps clients surface actionable messages.

-    const index = this.getChoices().indexOf(value);
+    const choices = this.getChoices();
+    const index = choices.indexOf(value);
     if (index === -1) {
-      throw new Error(`"${value}" is not a valid choice`);
+      throw new Error(`"${value}" is not a valid choice. Available: ${choices.join(', ')}`);
     }
     this.setInputValue(index);
packages/journey-client/src/lib/callbacks/select-idp-callback.ts (2)

17-23: Fix class docstring (copy/paste from Choice).

Make it IdP-specific.

-/**
- * Represents a callback used to collect an answer to a choice.
- */
+/**
+ * Represents an IdP selection callback. Provides available providers and
+ * allows choosing one to submit back to AM.
+ */

38-44: Improve invalid-provider error to list available options.

Small DX win; keeps behavior intact.

-    const item = this.getProviders().find((item) => item.provider === value);
+    const providers = this.getProviders();
+    const item = providers.find((p) => p.provider === value);
     if (!item) {
-      throw new Error(`"${value}" is not a valid choice`);
+      throw new Error(
+        `"${value}" is not a valid provider. Available: ${providers.map((p) => p.provider).join(', ')}`
+      );
     }
     this.setInputValue(item.provider);
packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts (1)

24-36: Type the config and harden number parsing

Return a typed config and coerce the interval to a number to handle string outputs from AM. Also trims envId.

Please confirm the output key names (e.g., deviceKeyRsyncIntervals vs. deviceKeyResyncIntervals, disableTags, disableHub) match AM.

-  public getConfig() {
+  public getConfig(): PingOneProtectInitConfig {
     const config = {
-      envId: this.getOutputByName<string>('envId', ''),
+      envId: this.getOutputByName<string>('envId', '').trim(),
       consoleLogEnabled: this.getOutputByName<boolean>('consoleLogEnabled', false),
       deviceAttributesToIgnore: this.getOutputByName<string[]>('deviceAttributesToIgnore', []),
       customHost: this.getOutputByName<string>('customHost', ''),
       lazyMetadata: this.getOutputByName<boolean>('lazyMetadata', false),
       behavioralDataCollection: this.getOutputByName<boolean>('behavioralDataCollection', true),
-      deviceKeyRsyncIntervals: this.getOutputByName<number>('deviceKeyRsyncIntervals', 14),
+      deviceKeyRsyncIntervals: Number(
+        this.getOutputByName<number | string>('deviceKeyRsyncIntervals', 14),
+      ),
       enableTrust: this.getOutputByName<boolean>('enableTrust', false),
       disableTags: this.getOutputByName<boolean>('disableTags', false),
       disableHub: this.getOutputByName<boolean>('disableHub', false),
     };
     return config;
   }

Add this type near the top of the file:

interface PingOneProtectInitConfig {
  envId: string;
  consoleLogEnabled: boolean;
  deviceAttributesToIgnore: string[];
  customHost: string;
  lazyMetadata: boolean;
  behavioralDataCollection: boolean;
  deviceKeyRsyncIntervals: number;
  enableTrust: boolean;
  disableTags: boolean;
  disableHub: boolean;
}
packages/journey-client/src/lib/journey.slice.ts (2)

13-18: Store serializable errors in Redux state

Error objects aren’t serializable and may trip middleware. Store a plain shape.

 export interface JourneyState {
   authId?: string;
   step?: Step;
-  error?: Error;
+  error?: { name: string; message: string; stack?: string };
   config?: JourneyClientConfig;
 }

41-41: Type the selector explicitly

Tiny polish for better inference at call sites.

-export const selectStep = createSelector(selectJourney, (journey) => journey.step);
+export const selectStep: (state: RootState) => Step | undefined = createSelector(
+  selectJourney,
+  (journey) => journey.step,
+);
packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts (3)

11-51: Annotate fixtures with DeviceProfileData for drift protection

Typing these fixtures will catch shape regressions in tests.

Add this import at the top of the file:

import type { DeviceProfileData } from './interfaces.js';

Then annotate the constants:

-const expectedJsdom = {
+const expectedJsdom: DeviceProfileData = {

53-88: Apply the same typing to the second fixture

-const expectedJsdomWithoutDisplay = {
+const expectedJsdomWithoutDisplay: DeviceProfileData = {

90-120: Apply the same typing to the third fixture

-const expectedJsdomWithNarrowedBrowserProps = {
+const expectedJsdomWithNarrowedBrowserProps: DeviceProfileData = {
packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts (1)

54-70: Add explicit return types to mutators

Consistency and clearer API surface.

-  public setClientError(error: string) {
+  public setClientError(error: string): void {
     this.setInputValue(error, 'IDToken1clientError');
   }
@@
-  public setPayload(payload: unknown) {
+  public setPayload(payload: unknown): void {
     this.setInputValue(payload, 'IDToken1payload');
   }
@@
-  public setAction(action: string) {
+  public setAction(action: string): void {
     this.setInputValue(action, 'IDToken1action');
   }
packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (2)

31-43: Make failedPolicies parsing robust (supports string or object)

Handle both JSON strings and already-parsed objects; fix the generic type to reflect actual server output.

-  public getFailedPolicies(): PolicyRequirement[] {
-    const failedPolicies = this.getOutputByName<PolicyRequirement[]>(
-      'failedPolicies',
-      [],
-    ) as unknown as string[];
-    try {
-      return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[];
-    } catch {
-      throw new Error(
-        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreateUsernameCallback` was either malformed or missing.',
-      );
-    }
-  }
+  public getFailedPolicies(): PolicyRequirement[] {
+    const raw = this.getOutputByName<unknown[]>('failedPolicies', []);
+    try {
+      return raw.map((v) => {
+        if (typeof v === 'string') return JSON.parse(v) as PolicyRequirement;
+        return v as PolicyRequirement;
+      });
+    } catch {
+      throw new Error(
+        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreateUsernameCallback` was either malformed or missing.',
+      );
+    }
+  }

49-52: Avoid any in getPolicies

Use unknown for policies surface; callers can narrow as needed.

-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  public getPolicies(): Record<string, any> {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    return this.getOutputByName<Record<string, any>>('policies', {});
-  }
+  public getPolicies(): Record<string, unknown> {
+    return this.getOutputByName<Record<string, unknown>>('policies', {});
+  }
packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (2)

24-36: Make failedPolicies parsing robust (supports string or object)

Match the username variant and tolerate both shapes.

-  public getFailedPolicies(): PolicyRequirement[] {
-    const failedPolicies = this.getOutputByName<PolicyRequirement[]>(
-      'failedPolicies',
-      [],
-    ) as unknown as string[];
-    try {
-      return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[];
-    } catch {
-      throw new Error(
-        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.',
-      );
-    }
-  }
+  public getFailedPolicies(): PolicyRequirement[] {
+    const raw = this.getOutputByName<unknown[]>('failedPolicies', []);
+    try {
+      return raw.map((v) => {
+        if (typeof v === 'string') return JSON.parse(v) as PolicyRequirement;
+        return v as PolicyRequirement;
+      });
+    } catch {
+      throw new Error(
+        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.',
+      );
+    }
+  }

41-45: Avoid any in getPolicies

Use unknown for the policies map.

-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  public getPolicies(): Record<string, any> {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    return this.getOutputByName<Record<string, any>>('policies', {});
-  }
+  public getPolicies(): Record<string, unknown> {
+    return this.getOutputByName<Record<string, unknown>>('policies', {});
+  }
packages/journey-client/src/lib/journey-client.ts (4)

40-44: Storage key collision risk; use prefix

Use config.prefix to namespace the sessionStorage key.

   const stepStorage = createStorage<{ step: FRStep }>({
     type: 'sessionStorage',
     name: 'journey-step',
+    prefix: config.prefix || 'forgerock',
   });

89-106: Await removal of stored step

remove() is async; ensure cleanup completes (and test expectations remain deterministic).

-        stepStorage.remove();
+        await stepStorage.remove();

108-117: URL params should override caller-provided options

Give precedence to values derived from the resume URL.

-      const nextOptions = {
+      const nextOptions = {
         ...options,
         query: {
-          ...(code && { code }),
-          ...(state && { state }),
-          ...(form_post_entry && { form_post_entry }),
-          ...(responsekey && { responsekey }),
-          ...(options && options.query),
+          ...(options && options.query),
+          ...(code && { code }),
+          ...(state && { state }),
+          ...(form_post_entry && { form_post_entry }),
+          ...(responsekey && { responsekey }),
         },
       };

64-76: Guard invalid resume URL inputs

new URL(url) throws on invalid input; surface a clearer error.

-      const parsedUrl = new URL(url);
+      let parsedUrl: URL;
+      try {
+        parsedUrl = new URL(url);
+      } catch {
+        throw new Error('Invalid URL passed to resume()');
+      }
packages/journey-client/src/lib/journey-client.test.ts (3)

113-138: Await redirect() to ensure persistence happens before navigation

If redirect() becomes async to await storage.set, tests should await it to avoid flakiness.

-    const client = await journey({ config: mockConfig });
-    client.redirect(step);
+    const client = await journey({ config: mockConfig });
+    await client.redirect(step);

32-35: Prefer vi.stubGlobal for fetch

Cleaner than assigning global.fetch directly and restores automatically.

-const mockFetch = vi.fn();
-global.fetch = mockFetch;
+const mockFetch = vi.fn();
+vi.stubGlobal('fetch', mockFetch);

141-173: Add a test for storage.set failure path

Simulate createStorage().set returning a GenericError and assert we still navigate and log a warning.

I can draft this test if you want.

packages/journey-client/src/lib/fr-policy/index.ts (3)

92-115: Accept string policy payloads and handle malformed JSON gracefully

AM may return JSON strings; widen the type and guard JSON.parse.

-  public static parsePolicyRequirement(
-    property: string,
-    policy: PolicyRequirement,
-    messageCreator: MessageCreator = {},
-  ): string {
-    // AM is returning policy requirement failures as JSON strings
-    const policyObject = typeof policy === 'string' ? JSON.parse(policy) : { ...policy };
+  public static parsePolicyRequirement(
+    property: string,
+    policy: PolicyRequirement | string,
+    messageCreator: MessageCreator = {},
+  ): string {
+    // AM sometimes returns policy requirement failures as JSON strings
+    let policyObject: PolicyRequirement;
+    if (typeof policy === 'string') {
+      try {
+        policyObject = JSON.parse(policy) as PolicyRequirement;
+      } catch {
+        return defaultMessageCreator[PolicyKey.UnknownPolicy](property, {
+          policyRequirement: PolicyKey.UnknownPolicy,
+        });
+      }
+    } else {
+      policyObject = { ...policy };
+    }

48-58: Avoid using Array.map for side effects

Use for...of for clarity and no unused array allocation.

-    if (err.detail && err.detail.failedPolicyRequirements) {
-      err.detail.failedPolicyRequirements.map((x: FailedPolicyRequirement) => {
-        errors.push.apply(errors, [
-          {
-            detail: x,
-            messages: this.parseFailedPolicyRequirement(x, messageCreator),
-          },
-        ]);
-      });
-    }
+    if (err.detail?.failedPolicyRequirements) {
+      for (const x of err.detail.failedPolicyRequirements) {
+        errors.push({
+          detail: x,
+          messages: this.parseFailedPolicyRequirement(x, messageCreator),
+        });
+      }
+    }

74-81: Same here: iterate policies with for...of

Minor clarity/readability tweak.

-    const errors: string[] = [];
-    failedPolicy.policyRequirements.map((policyRequirement: PolicyRequirement) => {
-      errors.push(
-        this.parsePolicyRequirement(failedPolicy.property, policyRequirement, messageCreator),
-      );
-    });
-    return errors;
+    const errors: string[] = [];
+    for (const pr of failedPolicy.policyRequirements) {
+      errors.push(this.parsePolicyRequirement(failedPolicy.property, pr, messageCreator));
+    }
+    return errors;
packages/journey-client/src/lib/callbacks/attribute-input-callback.ts (1)

49-58: Be resilient to partially malformed failedPolicies

Don’t fail the entire callback if one entry is malformed; parse per‑item and skip bad ones.

-  public getFailedPolicies(): PolicyRequirement[] {
-    const failedPoliciesJsonStrings = this.getOutputByName<string[]>('failedPolicies', []);
-    try {
-      return failedPoliciesJsonStrings.map((v) => JSON.parse(v)) as PolicyRequirement[];
-    } catch {
-      throw new Error(
-        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `AttributeInputCallback` was either malformed or missing.',
-      );
-    }
-  }
+  public getFailedPolicies(): PolicyRequirement[] {
+    const failedPoliciesJsonStrings = this.getOutputByName<string[]>('failedPolicies', []);
+    const parsed: PolicyRequirement[] = [];
+    for (const v of failedPoliciesJsonStrings) {
+      try {
+        parsed.push(JSON.parse(v) as PolicyRequirement);
+      } catch {
+        // ignore malformed entries instead of failing the entire callback
+      }
+    }
+    return parsed;
+  }
packages/journey-client/src/lib/fr-device/defaults.ts (1)

24-30: Keep Category keys in sync with config by deriving the type from BaseProfileConfig.

To avoid drift between this list and the Category union, derive Category from BaseProfileConfig (excluding devicePlatforms) and keep this constant as the runtime allow‑list.

Apply in interfaces.ts (see that file’s comment for the type change). No code change required here.

packages/journey-client/src/lib/fr-step.ts (2)

34-36: Doc nit: article typo.

“am implementation” → “an implementation”.

- * @param callbackFactory A function that returns am implementation of FRCallback
+ * @param callbackFactory A function that returns an implementation of FRCallback

51-57: Offer a non-throwing accessor.

getCallbackOfType throws unless exactly one exists. Consider a tryGetCallbackOfType that returns T | undefined to simplify optional flows.

+  public tryGetCallbackOfType<T extends FRCallback>(type: CallbackType): T | undefined {
+    const list = this.getCallbacksOfType<T>(type);
+    return list.length === 1 ? list[0] : undefined;
+  }
packages/journey-client/src/lib/fr-device/interfaces.ts (2)

11-12: Make Category type source‑of‑truth and tighten config typing.

Derive Category from BaseProfileConfig and use it in ProfileConfigOptions to prevent invalid keys at compile time.

-type Category = 'fontNames' | 'displayProps' | 'browserProps' | 'hardwareProps' | 'platformProps';
+// Place Category after BaseProfileConfig so it can be derived from its keys.
@@
-interface BaseProfileConfig {
+interface BaseProfileConfig {
   fontNames: string[];
   devicePlatforms: {
     mac: string[];
     windows: string[];
     ios: string[];
   };
   displayProps: string[];
   browserProps: string[];
   hardwareProps: string[];
   platformProps: string[];
 }
 
-interface ProfileConfigOptions {
-  [key: string]: string[];
-}
+type Category = Exclude<keyof BaseProfileConfig, 'devicePlatforms'>;
+
+type ProfileConfigOptions = Partial<Record<Category, string[]>>;

Also applies to: 43-54, 56-58


38-41: Avoid clashing with DOM lib types.

Geolocation conflicts conceptually with the DOM Geolocation interface (navigator.geolocation). Rename to Coordinates to reduce confusion.

-interface Geolocation {
+interface Coordinates {
   latitude: number;
   longitude: number;
 }
@@
-  location?: Geolocation | Record<string, unknown>;
+  location?: Coordinates | Record<string, unknown>;

Also update usages in fr-device/index.ts (getLocationCoordinates return type and imports).

packages/journey-client/src/lib/fr-policy/message-creator.ts (2)

17-24: Grammar tweaks and stray space.

Add “the” and remove double space.

-    return `${property} must not contain following characters: "${forbiddenChars}"`;
+    return `${property} must not contain the following characters: "${forbiddenChars}"`;
@@
-    return `${property}  must not contain duplicates: "${duplicateValue}"`;
+    return `${property} must not contain duplicates: "${duplicateValue}"`;

29-46: Unnatural defaults (0).

Messages like “at least 0 characters” read oddly when params are missing. Default to 1.

-    const numCaps = getProp<number>(params, 'numCaps', 0);
+    const numCaps = getProp<number>(params, 'numCaps', 1);
@@
-    const numNums = getProp<number>(params, 'numNums', 0);
+    const numNums = getProp<number>(params, 'numNums', 1);
@@
-    const maxLength = getProp<number>(params, 'maxLength', 0);
+    const maxLength = getProp<number>(params, 'maxLength', 1);
@@
-    const minLength = getProp<number>(params, 'minLength', 0);
+    const minLength = getProp<number>(params, 'minLength', 1);
packages/journey-client/src/lib/fr-device/index.ts (3)

141-142: Log message mismatch.

This path gathers hardware metadata, not OS metadata.

-      FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.');
+      FRLogger.warn('Cannot collect hardware metadata. navigator is not defined.');

271-278: Unnecessary try/catch and wording.

new Date().getTimezoneOffset() won’t throw in supported environments; if you keep the guard, adjust the log wording.

-    try {
-      return new Date().getTimezoneOffset();
-    } catch {
-      FRLogger.warn('Cannot collect timezone information. getTimezoneOffset is not defined.');
-      return null;
-    }
+    return new Date().getTimezoneOffset();

110-126: Device name mapping nit.

Optional: unify iOS case as “iOS (Browser)” rather than echoing platform literal to reduce cardinality.

-      case this.config.devicePlatforms.ios.includes(platform):
-        return `${platform} (Browser)`;
+      case this.config.devicePlatforms.ios.includes(platform):
+        return 'iOS (Browser)';

Comment on lines +59 to +65
public setOptionIndex(index: number): void {
if (index !== 0 && index !== 1) {
throw new Error(`"${index}" is not a valid choice`);
}
this.setInputValue(index);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

setOptionIndex only allows 0/1; breaks when more than two options exist

Validate against options.length and ensure integer indices.

-  public setOptionIndex(index: number): void {
-    if (index !== 0 && index !== 1) {
-      throw new Error(`"${index}" is not a valid choice`);
-    }
-    this.setInputValue(index);
-  }
+  public setOptionIndex(index: number): void {
+    const options = this.getOptions();
+    if (!Number.isInteger(index) || index < 0 || index >= options.length) {
+      throw new Error(`"${index}" is not a valid choice (0..${Math.max(0, options.length - 1)})`);
+    }
+    this.setInputValue(index);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public setOptionIndex(index: number): void {
if (index !== 0 && index !== 1) {
throw new Error(`"${index}" is not a valid choice`);
}
this.setInputValue(index);
}
public setOptionIndex(index: number): void {
const options = this.getOptions();
if (!Number.isInteger(index) || index < 0 || index >= options.length) {
throw new Error(`"${index}" is not a valid choice (0..${Math.max(0, options.length - 1)})`);
}
this.setInputValue(index);
}
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/confirmation-callback.ts around
lines 59 to 65, setOptionIndex currently only accepts 0 or 1 which fails for
callbacks with more than two options; update the validation to check that index
is an integer and within 0..(this.options.length - 1) (use
Number.isInteger(index) and this.options.length), and throw a clear error
mentioning the valid range if invalid, then call this.setInputValue(index) as
before.

Comment on lines +11 to +20
/**
* Types of steps returned by the authentication tree.
*/
enum StepType {
LoginFailure = 'LoginFailure',
LoginSuccess = 'LoginSuccess',
Step = 'Step',
}

export { StepType };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Canonical StepType definition—LGTM

Use this as the single source of truth; other packages should import or re-export from here as suggested.

After updating journey-client to re-export from sdk-types, run a quick search to ensure no other StepType enums exist:


🏁 Script executed:

#!/bin/bash
rg -nP '^\s*enum\s+StepType\b' -g '!**/node_modules/**'

Length of output: 148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for enum StepType declarations across all files (including ignored), with context.
rg -n --hidden -uu -S -C3 '^\s*(export\s+)?(const\s+)?enum\s+StepType\b' || true

# Also search for other StepType definitions (type aliases / unions) that might duplicate intent.
rg -n --hidden -uu -S -C3 '\b(type\s+StepType\s*=|export\s+type\s+StepType\s*=|StepType\s*=\s*"\w+")' || true

Length of output: 907


Remove duplicate StepType in journey-client; re-export from sdk-types.

rg found enum StepType in packages/sdk-types/src/lib/enums.ts (canonical) and packages/journey-client/src/lib/enums.ts — delete the journey-client definition and have journey-client import/re-export StepType from packages/sdk-types; update any local imports.

🤖 Prompt for AI Agents
In packages/sdk-types/src/lib/enums.ts around lines 11–20, StepType is the
canonical enum; remove the duplicate enum in
packages/journey-client/src/lib/enums.ts, replace that file with (or change it
to) a single re-export that imports StepType from packages/sdk-types and exports
it, and update any local imports inside packages/journey-client to import
StepType from "packages/sdk-types/src/lib/enums" (or from the sdk-types package
public entry) so there are no local duplicates and the project builds cleanly.

@ryanbas21 ryanbas21 force-pushed the create-journey-package branch from b70ccef to 11c3bcd Compare September 16, 2025 18:55
@nx-cloud
Copy link
Contributor

nx-cloud bot commented Sep 16, 2025

View your CI Pipeline Execution ↗ for commit 5366be3

Command Status Duration Result
nx run-many -t build ✅ Succeeded <1s View ↗
nx affected -t build typecheck lint test e2e-ci ✅ Succeeded 1m 6s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2025-09-29 23:11:44 UTC

@codecov-commenter
Copy link

codecov-commenter commented Sep 16, 2025

Codecov Report

❌ Patch coverage is 80.67493% with 733 lines in your changes missing coverage. Please review.
✅ Project coverage is 18.77%. Comparing base (4fbb22c) to head (5366be3).
⚠️ Report is 12 commits behind head on main.

Files with missing lines Patch % Lines
...s/journey-client/src/lib/journey-webauthn/index.ts 43.32% 293 Missing ⚠️
...rney-client/src/lib/journey-webauthn/interfaces.ts 0.00% 125 Missing ⚠️
...ges/journey-client/src/lib/journey-device/index.ts 74.03% 74 Missing ⚠️
...journey-client/src/lib/journey-webauthn/helpers.ts 64.51% 44 Missing ⚠️
packages/sdk-utilities/src/lib/url/url.utils.ts 2.63% 37 Missing ⚠️
packages/sdk-utilities/src/lib/object.utils.ts 3.03% 32 Missing ⚠️
packages/sdk-types/src/lib/policy.types.ts 4.34% 22 Missing ⚠️
...y-client/src/lib/journey-policy/message-creator.ts 71.64% 19 Missing ⚠️
packages/journey-client/src/lib/callbacks/index.ts 90.29% 10 Missing ⚠️
packages/journey-client/src/lib/journey-client.ts 95.13% 7 Missing ⚠️
... and 19 more

❌ Your project status has failed because the head coverage (18.77%) is below the target coverage (40.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff             @@
##             main     #412       +/-   ##
===========================================
- Coverage   55.63%   18.77%   -36.86%     
===========================================
  Files          32      138      +106     
  Lines        2051    27384    +25333     
  Branches      344      958      +614     
===========================================
+ Hits         1141     5142     +4001     
- Misses        910    22242    +21332     
Files with missing lines Coverage Δ
...ourney-client/src/lib/callbacks/choice-callback.ts 100.00% <100.00%> (ø)
...-client/src/lib/callbacks/confirmation-callback.ts 100.00% <100.00%> (ø)
...lient/src/lib/callbacks/device-profile-callback.ts 100.00% <100.00%> (ø)
...ckages/journey-client/src/lib/callbacks/factory.ts 100.00% <100.00%> (ø)
...-client/src/lib/callbacks/hidden-value-callback.ts 100.00% <100.00%> (ø)
...rney-client/src/lib/callbacks/metadata-callback.ts 100.00% <100.00%> (ø)
.../journey-client/src/lib/callbacks/name-callback.ts 100.00% <100.00%> (ø)
...rney-client/src/lib/callbacks/password-callback.ts 100.00% <100.00%> (ø)
.../lib/callbacks/ping-protect-evaluation-callback.ts 100.00% <100.00%> (ø)
.../lib/callbacks/ping-protect-initialize-callback.ts 100.00% <100.00%> (ø)
... and 47 more

... and 51 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Contributor

github-actions bot commented Sep 16, 2025

Deployed da7b33e to https://ForgeRock.github.io/ping-javascript-sdk/pr-412/da7b33e691298c572458a9f89b4f9b2f16f06f83 branch gh-pages in ForgeRock/ping-javascript-sdk

@github-actions
Copy link
Contributor

github-actions bot commented Sep 16, 2025

📦 Bundle Size Analysis

📦 Bundle Size Analysis

🆕 New Packages

🆕 @forgerock/device-client - 9.2 KB (new)
🆕 @forgerock/oidc-client - 23.1 KB (new)
🆕 @forgerock/protect - 150.1 KB (new)
🆕 @forgerock/sdk-utilities - 7.5 KB (new)
🆕 @forgerock/journey-client - 0.0 KB (new)
🆕 @forgerock/journey-client - 82.0 KB (new)
🆕 @forgerock/sdk-types - 8.0 KB (new)
🆕 @forgerock/storage - 1.4 KB (new)
🆕 @forgerock/sdk-logger - 1.6 KB (new)
🆕 @forgerock/iframe-manager - 2.4 KB (new)
🆕 @forgerock/sdk-request-middleware - 4.4 KB (new)
🆕 @forgerock/sdk-oidc - 2.5 KB (new)
🆕 @forgerock/davinci-client - 34.5 KB (new)


13 packages analyzed • Baseline from latest main build

Legend

🆕 New package
🔺 Size increased
🔻 Size decreased
➖ No change

ℹ️ How bundle sizes are calculated
  • Current Size: Total gzipped size of all files in the package's dist directory
  • Baseline: Comparison against the latest build from the main branch
  • Files included: All build outputs except source maps and TypeScript build cache
  • Exclusions: .map, .tsbuildinfo, and .d.ts.map files

🔄 Updated automatically on each push to this PR

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (3)
nx.json (1)

136-140: LGTM: recursive include fixed.

The journey-client plugin now correctly matches nested files. This addresses the prior shallow glob feedback.

packages/journey-client/src/lib/fr-device/defaults.ts (1)

37-37: ‘orientation.angle’ is flattened by collector; fix dotted-path handling or adjust key
Current collector flattens to angle, losing ownership by orientation. Either preserve nesting in reduceToObject or change the config to a supported shape.

Options:

  • Preserve nesting in collector for dotted paths (preferred).
  • Or change here to an agreed key (e.g., 'orientationAngle') and update consumers accordingly.
packages/journey-client/src/lib/journey-client.ts (1)

45-54: start()/next(): awaiting dispatch doesn’t wait for network or state; race with onQueryStarted
Awaiting dispatch(...initiate()) resolves immediately. Use .unwrap() and wait until the slice sets the step.

+  // Wait until the slice sets the current step (after onQueryStarted completes)
+  const waitForStep = (timeout = 5000) =>
+    new Promise<FRStep>((resolve, reject) => {
+      const started = Date.now();
+      const unsubscribe = store.subscribe(() => {
+        const step = selectStep(store.getState());
+        if (step) {
+          unsubscribe();
+          resolve(step as FRStep);
+        } else if (Date.now() - started > timeout) {
+          unsubscribe();
+          reject(new Error('Timed out waiting for step'));
+        }
+      });
+    });
@@
-    start: async (options?: StepOptions) => {
-      await store.dispatch(journeyApi.endpoints.start.initiate(options));
-      return self.getStep();
-    },
+    start: async (options?: StepOptions) => {
+      const action = await store.dispatch(journeyApi.endpoints.start.initiate(options));
+      await action.unwrap();
+      return await waitForStep();
+    },
@@
-    next: async (options: NextOptions) => {
-      await store.dispatch(journeyApi.endpoints.next.initiate(options));
-      return self.getStep();
-    },
+    next: async (options: NextOptions) => {
+      const action = await store.dispatch(journeyApi.endpoints.next.initiate(options));
+      await action.unwrap();
+      return await waitForStep();
+    },
🧹 Nitpick comments (23)
packages/sdk-utilities/src/lib/constants/index.ts (2)

1-4: Clarify constant intent (header value vs header name) and fix inconsistent naming.

These look like header values, but X_REQUESTED_PLATFORM reads like a header name and doesn’t match REQUESTED_WITH. Either drop the X_ prefix or add separate header-name constants to avoid confusion.

Example refactor (values + explicit header-name keys):

-const REQUESTED_WITH = 'forgerock-sdk';
-const X_REQUESTED_PLATFORM = 'javascript';
-
-export { REQUESTED_WITH, X_REQUESTED_PLATFORM };
+// Header names
+export const X_REQUESTED_WITH_HEADER = 'X-Requested-With';
+export const X_REQUESTED_PLATFORM_HEADER = 'X-Requested-Platform';
+// Header values
+export const REQUESTED_WITH_VALUE = 'forgerock-sdk';
+export const REQUESTED_PLATFORM_VALUE = 'javascript';
+
+// (Optional) Back-compat aliases if already referenced elsewhere:
+// export const REQUESTED_WITH = REQUESTED_WITH_VALUE;
+// export const X_REQUESTED_PLATFORM = REQUESTED_PLATFORM_VALUE;

1-4: Inline exports for tree‑shaking/readability.

Minor nit: exporting at declaration is a little cleaner.

-const REQUESTED_WITH = 'forgerock-sdk';
-const X_REQUESTED_PLATFORM = 'javascript';
-
-export { REQUESTED_WITH, X_REQUESTED_PLATFORM };
+export const REQUESTED_WITH = 'forgerock-sdk';
+export const X_REQUESTED_PLATFORM = 'javascript';
packages/sdk-utilities/package.json (1)

18-22: Consider adding a top‑level "types" field for broader tooling compatibility.

Some tools still rely on "types": "./dist/src/index.d.ts" even with the "exports.types" condition. Safe, non‑breaking addition.

Also applies to: 24-28

packages/journey-client/vitest.setup.ts (1)

1-1: Option: centralize or co-locate setup.

If multiple packages need Canvas, consider a root vitest.setup.ts shared via workspace test config; otherwise keep it local to avoid bleed‑through.

packages/sdk-effects/logger/eslint.config.mjs (1)

19-20: Approve — parser default export is correct; toolchain OK.

Root package.json: Node ^20 || ^22 (TLA supported); ESLint ^9.8.0; jsonc-eslint-parser ^2.1.0. The change to (await import('jsonc-eslint-parser')).default in packages/sdk-effects/logger/eslint.config.mjs is correct.

Repo note — many flat configs still use await import('jsonc-eslint-parser') (no .default); consider standardizing to (await import('jsonc-eslint-parser')).default in these files: tools/user-scripts/eslint.config.mjs, packages/protect/eslint.config.mjs, packages/oidc-client/eslint.config.mjs, packages/device-client/eslint.config.mjs, packages/sdk-effects/sdk-request-middleware/eslint.config.mjs, packages/sdk-effects/oidc/eslint.config.mjs, eslint.config.mjs (root), packages/davinci-client/eslint.config.mjs.

packages/journey-client/src/lib/fr-login-success.ts (3)

7-8: Consolidate imports to a single statement.

Slight tidy-up; keeps type-only imports tree‑shakable.

-import type { Step, AuthResponse } from '@forgerock/sdk-types';
-import { StepType } from '@forgerock/sdk-types';
+import { StepType, type Step, type AuthResponse } from '@forgerock/sdk-types';

17-19: Use product-neutral terminology in JSDoc.

Suggest “AM (Access Management)” instead of “OpenAM.”

- * @param payload The raw payload returned by OpenAM
+ * @param payload The raw payload returned by AM (Access Management)

19-19: Make payload immutable to prevent accidental mutation.

Expose the raw payload but mark it readonly.

-constructor(public payload: Step) {}
+constructor(public readonly payload: Readonly<Step>) {}
packages/sdk-types/src/lib/policy.types.ts (2)

11-33: Enum looks solid; consider direct export and consistent naming.

  • Optional: export the enum inline to reduce extra export noise.
  • Naming nit: LeastNumbers reads oddly next to LeastCapitalLetters. Consider AtLeastXNumbers for symmetry (only if non‑breaking).

Apply if desired:

- enum PolicyKey {
+ export enum PolicyKey {
   CannotContainCharacters = 'CANNOT_CONTAIN_CHARACTERS',
   ...
   ValidType = 'VALID_TYPE',
 }

Then remove the re‑export at the bottom.


35-35: Remove redundant re‑export if inlining export.

If you adopt inline export enum PolicyKey { ... }, this line becomes unnecessary.

-export { PolicyKey };
packages/sdk-utilities/src/lib/object.utils.ts (2)

27-41: Inconsistent defaults and potential key collisions in reduceToObject.

  • Dotted path defaults to '' while top‑level defaults to null. Pick one for predictability.
  • Using prop2 as the key can collide across different parents (e.g., a.id and b.id).

Unify defaults to null and keep behavior otherwise:

-        prev[prop2] = prop != undefined ? (prop as string | number | null) : '';
+        prev[prop2] = prop != undefined ? (prop as string | number | null) : null;

If collisions are a concern, allow custom aliasing, e.g., support label=parent.child syntax, or return nested objects.


49-54: Guard missing keys and avoid trailing delimiter in reduceToString.

Current code throws if src[curr] is missing and always leaves a trailing ;.

Use map/join and optional chaining:

-function reduceToString(props: string[], src: Record<string, { filename: string }>): string {
-  return props.reduce((prev, curr) => {
-    prev = `${prev}${src[curr].filename};`;
-    return prev;
-  }, '');
-}
+function reduceToString(props: string[], src: Record<string, { filename: string }>): string {
+  return props
+    .map((p) => src[p]?.filename)
+    .filter((v): v is string => typeof v === 'string')
+    .join(';');
+}
packages/journey-client/src/lib/fr-policy/message-creator.ts (2)

16-23: Fix grammar and stray double space in messages.

  • Add “the” in “contain the following characters”.
  • Remove extra space before “must not contain duplicates”.

Apply:

-    return `${property} must not contain following characters: "${forbiddenChars}"`;
+    return `${property} must not contain the following characters: "${forbiddenChars}"`;
...
-    return `${property}  must not contain duplicates: "${duplicateValue}"`;
+    return `${property} must not contain duplicates: "${duplicateValue}"`;

15-65: Make the mapping exhaustiveness-checked.

Use satisfies to ensure all PolicyKey members are covered without widening the object’s type.

-const defaultMessageCreator: MessageCreator = {
+const defaultMessageCreator = {
   /* mapping */
-};
+} satisfies MessageCreator;

This keeps key names intact and prevents accidental omissions.

packages/journey-client/src/lib/callbacks/device-profile-callback.ts (2)

22-27: Docstring mismatch.

Comment says “Gets the callback's data” but method returns the “message” output.

-  /**
-   * Gets the callback's data.
-   */
+  /**
+   * Gets the callback's message.
+   */

46-48: Optional: handle stringify failures defensively.

If DeviceProfileData ever includes circular refs, JSON.stringify will throw.

-  public setProfile(profile: DeviceProfileData): void {
-    this.setInputValue(JSON.stringify(profile));
-  }
+  public setProfile(profile: DeviceProfileData): void {
+    let payload = '{}';
+    try {
+      payload = JSON.stringify(profile);
+    } catch {
+      // no-op; leave as '{}'
+    }
+    this.setInputValue(payload);
+  }
packages/journey-client/src/lib/callbacks/factory.ts (2)

39-91: Optional: object map over switch for maintainability.

A lookup map reduces boilerplate and makes additions one‑liners. Keep the default to FRCallback.

Example sketch (non‑blocking):

const registry: Record<string, (c: Callback) => FRCallback> = {
  [callbackType.BooleanAttributeInputCallback]: (c) => new AttributeInputCallback<boolean>(c),
  // ...
};

function createCallback(callback: Callback): FRCallback {
  return (registry[callback.type] ?? ((c) => new FRCallback(c)))(callback);
}

39-91: Verified — factory covers all callbackType keys

Ran the provided diff script: no uncovered callbackType keys and no extra cases found. Optional: add a small unit/build assertion to detect future drift.

packages/journey-client/src/lib/journey-client.ts (3)

73-76: Confirm resume trigger conditions
Today requiresPreviousStep mandates both code and state. Some servers may return code without state. Consider treating presence of any of code | form_post_entry | responsekey as requiring previous step, and optionally validating state separately.


21-32: Why is journey() async?
No awaits inside; returning a Promise may needlessly complicate consumers. If not intentional, drop async.

-export async function journey({
+export function journey({
@@
-  return self;
+  return self;

Also applies to: 132-133


128-130: Type the return of getStep()
If selectStep returns FRStep | undefined, type it so consumers don’t need casts.

packages/journey-client/src/lib/fr-device/defaults.ts (1)

11-23: Non-standard/legacy props
Properties like appMinorVersion, buildID, browserLanguage, oscpu, userLanguage, systemLanguage are legacy and often undefined. Consider gating collection per‑browser or documenting expected nulls to reduce noise.

Also applies to: 72-79

packages/sdk-types/src/lib/am-callback.types.ts (1)

60-66: Unused type ‘FailureDetail’?
FailureDetail isn’t referenced. If unused, remove to reduce surface; otherwise, link it in StepDetail or docs.

Also applies to: 87-95

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b70ccef and 11c3bcd.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (84)
  • GEMINI.md (1 hunks)
  • nx.json (1 hunks)
  • packages/journey-client/GEMINI.md (1 hunks)
  • packages/journey-client/README.md (1 hunks)
  • packages/journey-client/eslint.config.mjs (1 hunks)
  • packages/journey-client/package.json (1 hunks)
  • packages/journey-client/src/index.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/attribute-input-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/choice-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/confirmation-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/factory.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/hidden-value-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/index.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/kba-create-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/metadata-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/name-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/password-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/polling-wait-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/recaptcha-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/redirect-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/select-idp-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/text-input-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/text-input-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/text-output-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (1 hunks)
  • packages/journey-client/src/lib/config.types.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/defaults.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/device-profile.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/index.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/sample-profile.json (1 hunks)
  • packages/journey-client/src/lib/fr-login-failure.ts (1 hunks)
  • packages/journey-client/src/lib/fr-login-success.ts (1 hunks)
  • packages/journey-client/src/lib/fr-policy/fr-policy.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-policy/index.ts (1 hunks)
  • packages/journey-client/src/lib/fr-policy/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/fr-policy/message-creator.ts (1 hunks)
  • packages/journey-client/src/lib/fr-step.ts (1 hunks)
  • packages/journey-client/src/lib/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.ts (1 hunks)
  • packages/journey-client/src/lib/journey.api.ts (1 hunks)
  • packages/journey-client/src/lib/journey.slice.ts (1 hunks)
  • packages/journey-client/src/lib/journey.store.ts (1 hunks)
  • packages/journey-client/tsconfig.json (1 hunks)
  • packages/journey-client/tsconfig.lib.json (1 hunks)
  • packages/journey-client/tsconfig.spec.json (1 hunks)
  • packages/journey-client/vite.config.ts (1 hunks)
  • packages/journey-client/vitest.setup.ts (1 hunks)
  • packages/sdk-effects/iframe-manager/eslint.config.mjs (1 hunks)
  • packages/sdk-effects/logger/eslint.config.mjs (1 hunks)
  • packages/sdk-effects/storage/eslint.config.mjs (1 hunks)
  • packages/sdk-types/eslint.config.mjs (1 hunks)
  • packages/sdk-types/src/index.ts (1 hunks)
  • packages/sdk-types/src/lib/am-callback.types.ts (2 hunks)
  • packages/sdk-types/src/lib/enums.ts (1 hunks)
  • packages/sdk-types/src/lib/legacy-config.types.ts (1 hunks)
  • packages/sdk-types/src/lib/policy.types.ts (1 hunks)
  • packages/sdk-utilities/eslint.config.mjs (1 hunks)
  • packages/sdk-utilities/package.json (1 hunks)
  • packages/sdk-utilities/src/index.ts (1 hunks)
  • packages/sdk-utilities/src/lib/constants/index.ts (1 hunks)
  • packages/sdk-utilities/src/lib/object.utils.ts (1 hunks)
  • packages/sdk-utilities/src/lib/strings/index.ts (1 hunks)
  • packages/sdk-utilities/src/lib/strings/strings.utils.ts (1 hunks)
  • packages/sdk-utilities/src/lib/url/index.ts (1 hunks)
  • packages/sdk-utilities/src/lib/url/url.utils.ts (1 hunks)
  • packages/sdk-utilities/tsconfig.lib.json (1 hunks)
  • tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (57)
  • packages/journey-client/src/index.ts
  • packages/sdk-utilities/src/lib/strings/strings.utils.ts
  • packages/journey-client/README.md
  • packages/sdk-utilities/src/index.ts
  • packages/journey-client/src/lib/journey.store.ts
  • packages/journey-client/src/lib/config.types.ts
  • packages/sdk-types/src/lib/enums.ts
  • packages/journey-client/src/lib/callbacks/attribute-input-callback.ts
  • packages/journey-client/src/lib/callbacks/attribute-input-callback.test.ts
  • packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts
  • packages/sdk-types/src/index.ts
  • packages/journey-client/src/lib/fr-login-failure.ts
  • packages/journey-client/GEMINI.md
  • packages/journey-client/src/lib/fr-policy/interfaces.ts
  • packages/journey-client/src/lib/callbacks/kba-create-callback.ts
  • packages/journey-client/src/lib/interfaces.ts
  • packages/journey-client/src/lib/callbacks/redirect-callback.ts
  • packages/journey-client/src/lib/journey-client.test.ts
  • packages/journey-client/src/lib/fr-policy/fr-policy.test.ts
  • packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts
  • packages/journey-client/src/lib/callbacks/metadata-callback.ts
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts
  • packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts
  • packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts
  • packages/journey-client/src/lib/fr-device/device-profile.mock.data.ts
  • packages/sdk-utilities/src/lib/url/index.ts
  • packages/journey-client/src/lib/callbacks/text-input-callback.test.ts
  • GEMINI.md
  • packages/journey-client/src/lib/journey.slice.ts
  • packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts
  • packages/journey-client/tsconfig.spec.json
  • packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.test.ts
  • packages/journey-client/src/lib/callbacks/text-input-callback.ts
  • packages/journey-client/src/lib/fr-policy/index.ts
  • packages/journey-client/src/lib/callbacks/text-output-callback.ts
  • packages/journey-client/src/lib/fr-device/device-profile.test.ts
  • packages/journey-client/src/lib/callbacks/recaptcha-callback.ts
  • packages/sdk-utilities/src/lib/strings/index.ts
  • packages/journey-client/src/lib/fr-device/sample-profile.json
  • packages/sdk-utilities/src/lib/url/url.utils.ts
  • packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts
  • packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts
  • packages/journey-client/src/lib/callbacks/choice-callback.ts
  • packages/journey-client/src/lib/fr-device/interfaces.ts
  • packages/journey-client/src/lib/callbacks/select-idp-callback.ts
  • packages/journey-client/tsconfig.lib.json
  • packages/journey-client/tsconfig.json
  • packages/journey-client/src/lib/callbacks/confirmation-callback.ts
  • packages/journey-client/package.json
  • packages/journey-client/src/lib/callbacks/polling-wait-callback.ts
  • packages/sdk-types/src/lib/legacy-config.types.ts
  • packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.test.ts
  • tsconfig.json
  • packages/journey-client/vite.config.ts
  • packages/journey-client/src/lib/fr-step.ts
  • packages/journey-client/eslint.config.mjs
🧰 Additional context used
🧬 Code graph analysis (14)
packages/journey-client/src/lib/callbacks/device-profile-callback.ts (2)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/fr-device/interfaces.ts (1)
  • DeviceProfileData (64-64)
packages/journey-client/src/lib/fr-login-success.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • AuthResponse (56-58)
  • Step (70-85)
packages/journey-client/src/lib/callbacks/hidden-value-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/index.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (3)
  • Callback (46-51)
  • CallbackType (37-37)
  • NameValue (39-42)
packages/journey-client/src/lib/callbacks/password-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • PolicyRequirement (107-110)
packages/journey-client/src/lib/callbacks/name-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/journey.api.ts (8)
packages/sdk-types/src/lib/legacy-config.types.ts (2)
  • ServerConfig (19-23)
  • StepOptions (54-56)
packages/sdk-utilities/src/lib/url/url.utils.ts (2)
  • stringify (55-55)
  • resolve (55-55)
packages/sdk-utilities/src/lib/url/am-url.utils.ts (1)
  • getEndpointPath (39-59)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Step (70-85)
  • callbackType (10-35)
packages/journey-client/src/lib/config.types.ts (2)
  • JourneyClientConfig (10-13)
  • StepOptions (15-15)
packages/sdk-utilities/src/lib/constants/index.ts (1)
  • REQUESTED_WITH (4-4)
packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.effects.ts (1)
  • initQuery (61-88)
packages/journey-client/src/lib/interfaces.ts (1)
  • NextOptions (10-13)
packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • PolicyRequirement (107-110)
packages/journey-client/src/lib/journey-client.ts (10)
packages/journey-client/src/lib/config.types.ts (2)
  • JourneyClientConfig (10-13)
  • StepOptions (15-15)
packages/sdk-effects/logger/src/lib/logger.types.ts (1)
  • LogLevel (16-16)
packages/sdk-effects/logger/src/lib/logger.effects.ts (1)
  • logger (10-61)
packages/journey-client/src/lib/journey.store.ts (1)
  • createJourneyStore (21-46)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • createStorage (34-139)
packages/sdk-types/src/lib/legacy-config.types.ts (1)
  • StepOptions (54-56)
packages/journey-client/src/lib/journey.api.ts (1)
  • journeyApi (85-177)
packages/journey-client/src/lib/interfaces.ts (1)
  • NextOptions (10-13)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey.slice.ts (1)
  • selectStep (41-41)
packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/fr-device/index.ts (1)
packages/sdk-effects/logger/src/lib/logger.effects.ts (1)
  • logger (10-61)
packages/journey-client/src/lib/fr-policy/message-creator.ts (5)
packages/journey-client/src/lib/fr-policy/index.ts (1)
  • MessageCreator (119-119)
packages/journey-client/src/lib/fr-policy/interfaces.ts (1)
  • MessageCreator (18-18)
packages/sdk-types/src/lib/policy.types.ts (1)
  • PolicyKey (35-35)
packages/sdk-utilities/src/lib/object.utils.ts (1)
  • getProp (56-56)
packages/sdk-utilities/src/lib/strings/strings.utils.ts (1)
  • plural (16-21)
packages/journey-client/src/lib/callbacks/factory.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/sdk-types/src/lib/am-callback.types.ts (1)
packages/sdk-types/src/lib/enums.ts (1)
  • StepType (20-20)
🪛 Biome (2.1.2)
packages/journey-client/src/lib/fr-device/index.ts

[error] 207-224: Promise executor functions should not be async.

(lint/suspicious/noAsyncPromiseExecutor)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (29)
packages/sdk-utilities/package.json (1)

18-22: Exports map addition looks good; verify build output paths exist.

The new ./constants subpath and the multi-entry ./types export are consistent with the root export. Please confirm the build emits:

  • dist/src/lib/constants/index.js and index.d.ts
  • dist/src/types.js and types.d.ts

Also applies to: 24-28

packages/journey-client/vitest.setup.ts (2)

1-1: LGTM: Canvas mock setup is correct.

Side‑effect import is appropriate for Vitest setup files.


1-1: Confirmed: vitest setup file registered & environment = 'jsdom'

  • packages/journey-client/vite.config.ts — contains setupFiles: ['./vitest.setup.ts'] and environment: 'jsdom'.
  • packages/journey-client/tsconfig.json — compilerOptions.lib does not include "DOM"; add "DOM" if you need TypeScript DOM/Canvas types for tests.
packages/sdk-utilities/eslint.config.mjs (1)

37-38: Consistent and correct use of jsonc-eslint-parser default export.

Matches the pattern used elsewhere; safe with ESLint flat config and Node ESM.

packages/sdk-effects/storage/eslint.config.mjs (1)

25-26: ESM default export usage LGTM.

Keeps parser wiring correct for JSON files and aligns with other package configs.

packages/sdk-effects/iframe-manager/eslint.config.mjs (1)

19-20: LGTM: default export of jsonc-eslint-parser.

Uniform with the rest of the repo; no issues spotted.

packages/sdk-types/eslint.config.mjs (1)

19-20: LGTM: switch to parser default export.

Change is correct and consistent across packages.

packages/journey-client/src/lib/fr-login-success.ts (3)

1-5: Header/license looks good.

Copyright range and license
notice are consistent with repo standards.


10-15: AuthResponse conformance is correct.

Constant type = StepType.LoginSuccess cleanly models the response discriminator.


24-26: Getter is straightforward and safe.

Returning realm as optional aligns with Step.

packages/sdk-utilities/tsconfig.lib.json (1)

20-21: Tighten exclude glob for mock data; confirm .mts/.cts usage

Exclude nested mock-data with a recursive glob; add .mts/.cts to includes only if this package uses NodeNext file extensions.

File: packages/sdk-utilities/tsconfig.lib.json

Apply:

-  "include": ["src/**/*.ts"],
-  "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/lib/mock-data/*"],
+  "include": ["src/**/*.ts"],
+  "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/lib/mock-data/**"],

Check for .mts/.cts files (run from repo root):

rg -n --hidden --glob '!.git' '\.(mts|cts)$' packages/sdk-utilities || true
packages/sdk-utilities/src/lib/object.utils.ts (1)

11-16: LGTM on getProp.

Clear and typesafe defaulting behavior.

packages/journey-client/src/lib/callbacks/hidden-value-callback.ts (1)

13-22: LGTM.

Straightforward wrapper; constructor correctly forwards payload.

packages/journey-client/src/lib/callbacks/name-callback.ts (1)

24-33: LGTM; APIs align with expectations.

getPrompt and setName match AM semantics.

packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (1)

34-45: Confirm getArrayElement supports RegExp selectors used by setInputValue.
setInputValue accepts number|string|RegExp (packages/journey-client/src/lib/callbacks/index.ts — ~line 42), but I couldn't locate getArrayElement's implementation; verify it treats a RegExp selector as a name-pattern (matches input.name). If it doesn't, implement regex support or change callers to pass an explicit name/index (so /signals/ and /clientError/ will actually set values).

packages/journey-client/src/lib/callbacks/index.ts (3)

32-34: Support RegExp in getInputValue for API symmetry.

setInputValue accepts RegExp selectors; getInputValue should too.
[ suggest_optional_refactor ]
Apply:

-  public getInputValue(selector: number | string = 0): unknown {
+  public getInputValue(selector: number | string | RegExp = 0): unknown {
     return this.getArrayElement(this.payload.input, selector).value;
   }

52-54: Support RegExp in getOutputValue for completeness.

Mirror the selector types supported elsewhere.
[ suggest_optional_refactor ]

-  public getOutputValue(selector: number | string = 0): unknown {
+  public getOutputValue(selector: number | string | RegExp = 0): unknown {
     return this.getArrayElement(this.payload.output, selector).value;
   }

82-99: Use instanceof RegExp and neutral error messages.

Cleaner type‑narrowing and messages that fit both input/output arrays.
[ suggest_nitpick_refactor ]

-    if (typeof selector === 'string') {
+    if (typeof selector === 'string') {
       const input = array.find((x) => x.name === selector);
       if (!input) {
-        throw new Error(`Missing callback input entry "${selector}"`);
+        throw new Error(`Missing callback entry "${selector}"`);
       }
       return input;
     }
 
-    // Duck typing for RegEx
-    if (typeof selector === 'object' && selector.test && Boolean(selector.exec)) {
+    // RegExp selector
+    if (selector instanceof RegExp) {
       const input = array.find((x) => selector.test(x.name));
       if (!input) {
-        throw new Error(`Missing callback input entry "${selector}"`);
+        throw new Error(`Missing callback entry "${selector}"`);
       }
       return input;
     }
packages/journey-client/src/lib/journey.api.ts (4)

3-3: Import StepOptions from sdk-types (fix compile error).

Local config.types.js doesn’t export StepOptions.
[ raise_critical_issue ][ duplicate_comment ]

-import type { StepOptions } from './config.types.js';
+import type { StepOptions } from '@forgerock/sdk-types';

71-83: Avoid duplicate device profile collection per step.

Compute once using the union of requirements and set on all callbacks.
[ suggest_recommended_refactor ]

-  const deviceProfileCallbacks = frStepInstance.getCallbacksOfType<DeviceProfileCallback>(
-    callbackType.DeviceProfileCallback,
-  );
-  for (const cb of deviceProfileCallbacks) {
-    const profile = await frDevice.getProfile({
-      location: cb.isLocationRequired(),
-      metadata: cb.isMetadataRequired(),
-    });
-    cb.setProfile(profile);
-  }
+  const deviceProfileCallbacks =
+    frStepInstance.getCallbacksOfType<DeviceProfileCallback>(callbackType.DeviceProfileCallback);
+  if (deviceProfileCallbacks.length) {
+    const needsLocation = deviceProfileCallbacks.some((cb) => cb.isLocationRequired());
+    const needsMetadata = deviceProfileCallbacks.some((cb) => cb.isMetadataRequired());
+    const profile = await frDevice.getProfile({ location: needsLocation, metadata: needsMetadata });
+    for (const cb of deviceProfileCallbacks) cb.setProfile(profile);
+  }

128-136: Preserve RTK Query error shape.

Casting to Error loses FetchBaseQueryError/SerializedError info; downstream handlers may break.
[ raise_minor_issue ][ request_verification ]
Consider storing the raw rejection (e.g., dispatch(setJourneyState({ error: (await queryFulfilled).error })) via try/catch destructure) or enhancing the slice to accept unknown and normalize centrally.

Also applies to: 166-174


85-97: Verify extras injection via thunk extraArgument.

api.extra is used for config/middleware/device. Ensure the store wires Redux‑Thunk’s extraArgument.
[ request_verification ]

#!/bin/bash
# Expect to see configureStore with thunk.extraArgument providing { requestMiddleware, logger, frDevice, config }
rg -nP 'configureStore\(|thunk\s*:\s*\{[^}]*extraArgument' -C3
rg -nP '\bextraArgument\b' -C2
packages/journey-client/src/lib/callbacks/password-callback.ts (1)

24-26: Normalize failedPolicies (strings → objects).

AM often returns JSON strings; current code may surface strings and break consumers.
[ raise_major_issue ][ duplicate_comment ]

-  public getFailedPolicies(): PolicyRequirement[] {
-    return this.getOutputByName<PolicyRequirement[]>('failedPolicies', []);
-  }
+  public getFailedPolicies(): PolicyRequirement[] {
+    const raw = this.getOutputByName<unknown[]>('failedPolicies', []);
+    try {
+      return raw.map((v) =>
+        typeof v === 'string' ? (JSON.parse(v) as PolicyRequirement) : (v as PolicyRequirement),
+      );
+    } catch {
+      throw new Error(
+        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `PasswordCallback` was either malformed or missing.',
+      );
+    }
+  }
packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1)

24-36: Harden failedPolicies parsing (support objects and strings).

Server may return PolicyRequirement objects directly; current JSON.parse on all items will throw.
[ raise_major_issue ]

-  public getFailedPolicies(): PolicyRequirement[] {
-    const failedPolicies = this.getOutputByName<PolicyRequirement[]>(
-      'failedPolicies',
-      [],
-    ) as unknown as string[];
-    try {
-      return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[];
-    } catch {
-      throw new Error(
-        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.',
-      );
-    }
-  }
+  public getFailedPolicies(): PolicyRequirement[] {
+    const raw = this.getOutputByName<unknown[]>('failedPolicies', []);
+    try {
+      return raw.map((v) =>
+        typeof v === 'string' ? (JSON.parse(v) as PolicyRequirement) : (v as PolicyRequirement),
+      );
+    } catch {
+      throw new Error(
+        'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.',
+      );
+    }
+  }
packages/journey-client/src/lib/fr-device/index.ts (4)

146-164: SSR crash risk and no fallback when localStorage is unavailable.

Direct localStorage access throws in SSR and returns empty ID when storage is missing.
[ raise_major_issue ]
Apply and add a backing field for an ephemeral ID:

   private prefix: string;
+  private ephemeralId?: string;

   getIdentifier(): string {
     const storageKey = `${this.prefix}-DeviceID`;

-    if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
+    if (!globalThis.crypto?.getRandomValues) {
       FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.');
       return '';
     }
-    if (!localStorage) {
-      FRLogger.warn('Cannot store profile ID. localStorage is not supported.');
-      return '';
-    }
-    let id = localStorage.getItem(storageKey);
+    const storage: Storage | null =
+      typeof globalThis !== 'undefined' && 'localStorage' in globalThis
+        ? (globalThis.localStorage as Storage)
+        : null;
+
+    if (!storage) {
+      FRLogger.warn('Cannot store profile ID. localStorage is not supported; using in-memory ID.');
+      this.ephemeralId ||= globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
+      return this.ephemeralId;
+    }
+    let id = storage.getItem(storageKey);
     if (!id) {
       // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532"
       id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
-      localStorage.setItem(storageKey, id);
+      storage.setItem(storageKey, id);
     }
     return id;
   }

206-224: Async Promise executor (Biome error).

Remove async on the executor; no await is needed.
[ raise_minor_issue ]

-    // eslint-disable-next-line no-async-promise-executor
-    return new Promise(async (resolve) => {
+    return new Promise((resolve) => {
       navigator.geolocation.getCurrentPosition(
         (position) =>
           resolve({
             latitude: position.coords.latitude,
             longitude: position.coords.longitude,
           }),

238-265: Privacy/compliance: gate persistent ID and fingerprinting fields behind explicit opt‑in.

Identifier persistence, fonts/plugins/timezone/location can be fingerprinting. Add config flags (e.g., persistId, collectFonts, collectPlugins, collectTimezone, collectLocation; default false) and respect them in getIdentifier/getProfile.
[ suggest_recommended_refactor ][ request_verification ]
I can draft the config/types and wiring if you want a quick follow‑up patch.


135-139: Message nit: hardware vs OS.

Log says “OS metadata” in getHardwareMeta; should be “hardware metadata.”
[ suggest_nitpick_refactor ]

-      FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.');
+      FRLogger.warn('Cannot collect hardware metadata. navigator is not defined.');
packages/sdk-types/src/lib/am-callback.types.ts (1)

10-37: LGTM on callback registry and derived union
The exported callbackType and CallbackType union look good.

Comment on lines 136 to 151
},
{
"plugin": "@nx/js/typescript",
"include": ["packages/journey-client/**"],
"options": {
"typecheck": {
"targetName": "typecheck"
},
"build": {
"targetName": "build",
"configName": "tsconfig.lib.json",
"options": {
"format": ["esm"]
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Avoid double TypeScript inference for journey-client.

This dedicated @nx/js/typescript entry overlaps the root @nx/js/typescript plugin include, potentially creating conflicting/duplicated inferred targets (“nxBuild” vs “build”). Exclude journey-client from the root plugin to make this block the sole source of inference.

Apply to the first @nx/js/typescript plugin block:

   {
     "plugin": "@nx/js/typescript",
     "options": {
       "typecheck": {
         "targetName": "typecheck"
       },
       "build": {
         "targetName": "nxBuild",
         "configName": "tsconfig.lib.json"
       }
     },
-    "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"]
+    "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"],
+    "exclude": ["packages/journey-client/**"]
   },

Verify target name consistency (scripts vs inferred targets):


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "nx.json plugin entries referencing @nx/js/typescript:"
rg -nC2 '"plugin":\s*"@nx/js/typescript"|"(?:include|exclude)":' nx.json
echo
echo "journey-client scripts and nx targets:"
if [ -f packages/journey-client/package.json ]; then
  jq -r '.scripts // {}' packages/journey-client/package.json
  jq -r '.nx.targets // {}' packages/journey-client/package.json
else
  echo "packages/journey-client/package.json not found"
fi

Length of output: 1360


Exclude journey-client from the root @nx/js/typescript plugin and align build target names.

  • nx.json currently scopes @nx/js/typescript to ["e2e///","packages///","tools///*"] and also has a second entry for ["packages/journey-client/"] — this duplicates inference; add "exclude": ["packages/journey-client/"] to the root plugin so the dedicated entry is the sole source of inference.
  • packages/journey-client/package.json scripts use "pnpm nx nxBuild" while the dedicated plugin infers a build target named "build" — make names consistent: either change the journey-client plugin's options.build.targetName to "nxBuild" or update the package scripts to call the project’s "build" target.
  • Verify Nx version/flags (or set NX_BUILDABLE_LIBRARIES_TASK_GRAPH) before renaming build targets because some @nx/js executors historically assume the target name "build".
🤖 Prompt for AI Agents
In nx.json around lines 136 to 151, the root @nx/js/typescript plugin currently
duplicates inference for packages/journey-client and must exclude it; add
"exclude": ["packages/journey-client/**"] to the root plugin entry so the
dedicated plugin remains the sole inference source, then make build target names
consistent by either changing the journey-client plugin options.build.targetName
to "nxBuild" or updating packages/journey-client/package.json scripts to call
the project's "build" target (pick one approach and apply consistently), and
finally verify Nx version/flags or set NX_BUILDABLE_LIBRARIES_TASK_GRAPH before
renaming targets because some @nx/js executors assume the target name "build".

Comment on lines 31 to 33
public getSessionToken(): string | undefined {
return this.payload.tokenId;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

🧩 Analysis chain

Confirm downstream doesn’t assume non‑undefined values.

Both tokenId and successUrl are optional in Step; verify callers guard for undefined or refine the type upstream for LoginSuccess steps.

Also applies to: 38-40


🏁 Script executed:

#!/bin/bash
# Locate call sites for manual checks that they handle undefined safely.
rg -nP --type=ts -C3 '\.getSessionToken\s*\('
rg -nP --type=ts -C3 '\.getSuccessUrl\s*\('

Length of output: 592


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for call sites..."
rg -nP --type=ts -C3 '\.getSessionToken\s*\(' || true
rg -nP --type=ts -C3 '\.getSuccessUrl\s*\(' || true

echo
echo "=== packages/journey-client/src/lib/fr-login-success.ts (lines 1-120) ==="
sed -n '1,120p' packages/journey-client/src/lib/fr-login-success.ts | nl -ba -v1 -w3 -s': '

echo
echo "=== e2e/protect-app/src/protect-native.ts (lines 140-200) ==="
sed -n '140,200p' e2e/protect-app/src/protect-native.ts | nl -ba -v1 -w3 -s': '

Length of output: 737


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "rg results for getSessionToken and getSuccessUrl:"
rg -nP --type=ts -C3 '\.getSessionToken\s*\(' || true
rg -nP --type=ts -C3 '\.getSuccessUrl\s*\(' || true

echo
echo "---- packages/journey-client/src/lib/fr-login-success.ts (full file) ----"
sed -n '1,240p' packages/journey-client/src/lib/fr-login-success.ts || true

echo
echo "---- e2e/protect-app/src/protect-native.ts (lines 140-200) ----"
sed -n '140,200p' e2e/protect-app/src/protect-native.ts || true

Length of output: 3349


Guard optional session token/successUrl or tighten Step type.

Found unguarded use: e2e/protect-app/src/protect-native.ts:166 — step.getSessionToken() is read/used without checking for undefined; no getSuccessUrl call sites found. Either guard the returned value before use or make FRLoginSuccess/Step guarantee tokenId/successUrl for LoginSuccess steps.

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-login-success.ts around lines 31 to 33,
getSessionToken currently returns a possibly undefined payload.tokenId which
callers treat as always present; update the implementation and types so callers
no longer get an undefined: either (preferred) tighten the FRLoginSuccess/Step
types to require tokenId (and successUrl if applicable) for LoginSuccess steps
and change getSessionToken's return type to string, or keep the current optional
payload and update getSessionToken to throw a clear error when tokenId is
missing and update all call sites to handle/guard that error; ensure the chosen
approach is applied consistently (type changes or guards) across
e2e/protect-app/src/protect-native.ts and any other consumers.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
packages/sdk-utilities/src/lib/url/url.utils.ts (1)

21-25: IPv6 bracket handling looks good (resolves prior comment).

Wrapping hostname in [] when it contains : is correct and fixes invalid IPv6 origins. Thanks for addressing this.

nx.json (2)

139-139: Glob depth fix looks good.

"packages/journey-client/" correctly captures nested paths (e.g., src/). Thanks for addressing the earlier shallow glob.


136-148: Prevent duplicate inference for packages/journey-client — exclude from root plugin and unify build target

Root @nx/js/typescript (nx.json lines 96–106) includes packages///* while there's a dedicated plugin for packages/journey-client (nx.json lines 138–146), producing two inferred build targets (nxBuild vs build).

  • Exclude packages/journey-client/** from the root plugin include.
  • Change the dedicated plugin's build targetName from "build" to "nxBuild".

Apply:

-        "build": {
-          "targetName": "build",
-          "configName": "tsconfig.lib.json"
-        }
+        "build": {
+          "targetName": "nxBuild",
+          "configName": "tsconfig.lib.json"
+        }

And update the root plugin entry:

   {
     "plugin": "@nx/js/typescript",
     "options": {
       "typecheck": {
         "targetName": "typecheck"
       },
       "build": {
         "targetName": "nxBuild",
         "configName": "tsconfig.lib.json"
       }
     },
-    "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"]
+    "include": ["e2e/**/**/*", "packages/**/**/*", "tools/**/**/*"],
+    "exclude": ["packages/journey-client/**"]
   },
packages/journey-client/src/lib/journey.api.ts (1)

3-4: Import source for StepOptions fixed — good.

This resolves the earlier compile-time break flagged in prior review.

🧹 Nitpick comments (9)
packages/sdk-utilities/src/lib/url/url.utils.ts (3)

16-19: Include ports for non-HTTP(S) schemes; don’t drop specified ports.

Currently, any non-HTTP(S) URL with an explicit port (e.g., ftp://host:2121) will have its port omitted. Include the port whenever it’s set, except when it matches the default for HTTP/HTTPS.

Apply this diff:

-  const isNonStandardPort =
-    (url.protocol === 'http:' && ['', '80'].indexOf(url.port) === -1) ||
-    (url.protocol === 'https:' && ['', '443'].indexOf(url.port) === -1);
-  const port = isNonStandardPort ? `:${url.port}` : '';
+  const defaultPort =
+    url.protocol === 'http:' ? '80' :
+    url.protocol === 'https:' ? '443' :
+    undefined;
+  const includePort = url.port !== '' && (defaultPort ? url.port !== defaultPort : true);
+  const port = includePort ? `:${url.port}` : '';

31-43: Leverage URL for path resolution (handles ., .., empty path).

Use the platform resolver to avoid edge cases and simplify logic.

Apply this diff:

-function resolve(baseUrl: string, path: string): string {
-  const url = new URL(baseUrl);
-
-  if (path.startsWith('/')) {
-    return `${getBaseUrl(url)}${path}`;
-  }
-
-  const basePath = url.pathname.split('/');
-  const destPath = path.split('/').filter((x) => !!x);
-  const newPath = [...basePath.slice(0, -1), ...destPath].join('/');
-
-  return `${getBaseUrl(url)}${newPath}`;
-}
+function resolve(baseUrl: string, path: string): string {
+  const base = new URL(baseUrl);
+  const resolved = new URL(path, base);
+  return `${getBaseUrl(base)}${resolved.pathname}`;
+}

52-60: Encode keys and avoid inherited props; prefer URLSearchParams.

Current loop doesn’t encode keys and may traverse inherited properties. URLSearchParams is safer and concise.

Apply this diff:

-function stringify(data: Record<string, string | undefined>): string {
-  const pairs: string[] = [];
-  for (const k in data) {
-    if (data[k]) {
-      pairs.push(k + '=' + encodeURIComponent(data[k] as string));
-    }
-  }
-  return pairs.join('&');
-}
+function stringify(data: Record<string, string | undefined>): string {
+  const params = new URLSearchParams();
+  for (const [k, v] of Object.entries(data)) {
+    if (v !== undefined && v !== '') {
+      params.set(k, v);
+    }
+  }
+  return params.toString();
+}
packages/journey-client/src/lib/journey.api.ts (6)

86-87: Use a unique reducerPath to avoid store key collision.

journeyReducer likely collides/confuses with the slice reducer name; prefer journeyApi.

Apply this diff:

-export const journeyApi = createApi({
-  reducerPath: 'journeyReducer',
+export const journeyApi = createApi({
+  reducerPath: 'journeyApi',

46-52: Set Content-Type only when sending a body to reduce CORS preflights.

Avoid JSON content type on the initial POST with no body.

Apply this diff:

 function configureRequest(step?: Step): RequestInit {
   const init: RequestInit = {
     body: step ? JSON.stringify(step) : undefined,
     credentials: 'include',
-    headers: new Headers({}),
+    headers: new Headers(step ? { 'Content-Type': 'application/json' } : {}),
     method: 'POST',
   };

90-94: Complementary change: remove unconditional Content-Type from prepareHeaders.

Let configureRequest set it when needed.

Apply this diff:

   prepareHeaders: (headers: Headers) => {
     headers.set('Accept', 'application/json');
     headers.set('Accept-API-Version', 'protocol=1.0,resource=2.1');
-    headers.set('Content-Type', 'application/json');
     headers.set('X-Requested-With', REQUESTED_WITH);

74-80: Parallelize device profile resolution; don’t fail the whole step on a single callback error.

Slight perf win; more robust if one profile fetch fails.

Apply this diff:

-  for (const cb of deviceProfileCallbacks) {
-    const profile = await frDevice.getProfile({
-      location: cb.isLocationRequired(),
-      metadata: cb.isMetadataRequired(),
-    });
-    cb.setProfile(profile);
-  }
+  await Promise.allSettled(
+    deviceProfileCallbacks.map(async (cb) => {
+      const profile = await frDevice.getProfile({
+        location: cb.isLocationRequired(),
+        metadata: cb.isMetadataRequired(),
+      });
+      cb.setProfile(profile);
+    }),
+  );

129-136: Log failures via injected logger (start).

Use the provided logger in extra for observability.

Apply this diff:

       try {
         const { data } = await queryFulfilled;
         const { frDevice } = extra as Extras;
         await handleDeviceProfileCallbacks(data, dispatch, frDevice);
       } catch (_error) {
-        dispatch(setJourneyState({ error: _error as Error }));
+        const { logger } = extra as Extras;
+        logger?.error?.('journey.start failed', _error);
+        dispatch(setJourneyState({ error: _error as Error }));
       }

166-174: Log failures via injected logger (next).

Mirror the start handler for consistency.

Apply this diff:

       try {
         const { data } = await queryFulfilled;
         const { frDevice } = extra as Extras;
         await handleDeviceProfileCallbacks(data, dispatch, frDevice);
       } catch (_error) {
-        dispatch(setJourneyState({ error: _error as Error }));
+        const { logger } = extra as Extras;
+        logger?.error?.('journey.next failed', _error);
+        dispatch(setJourneyState({ error: _error as Error }));
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11c3bcd and fb70a49.

📒 Files selected for processing (7)
  • nx.json (1 hunks)
  • packages/journey-client/src/lib/fr-device/index.ts (1 hunks)
  • packages/journey-client/src/lib/fr-device/sample-profile.json (1 hunks)
  • packages/journey-client/src/lib/journey-client.ts (1 hunks)
  • packages/journey-client/src/lib/journey.api.ts (1 hunks)
  • packages/journey-client/src/lib/journey.store.ts (1 hunks)
  • packages/sdk-utilities/src/lib/url/url.utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/journey-client/src/lib/journey.store.ts
  • packages/journey-client/src/lib/fr-device/index.ts
  • packages/journey-client/src/lib/fr-device/sample-profile.json
  • packages/journey-client/src/lib/journey-client.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/journey-client/src/lib/journey.api.ts (8)
packages/sdk-types/src/lib/legacy-config.types.ts (2)
  • ServerConfig (19-23)
  • StepOptions (54-56)
packages/sdk-utilities/src/lib/url/url.utils.ts (2)
  • stringify (62-62)
  • resolve (62-62)
packages/sdk-utilities/src/lib/url/am-url.utils.ts (1)
  • getEndpointPath (39-59)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Step (70-85)
  • callbackType (10-35)
packages/journey-client/src/lib/config.types.ts (2)
  • JourneyClientConfig (10-13)
  • StepOptions (15-15)
packages/sdk-utilities/src/lib/constants/index.ts (1)
  • REQUESTED_WITH (4-4)
packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.effects.ts (1)
  • initQuery (61-88)
packages/journey-client/src/lib/interfaces.ts (1)
  • NextOptions (10-13)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (3)
packages/sdk-utilities/src/lib/url/url.utils.ts (1)

45-50: Confirm intended behavior for duplicate query keys.

URLSearchParams keeps the last value; duplicates collapse (e.g., ?a=1&a=2 → { a: '2' }). If arrays are needed later, we’ll need getAll.

Optional tidy-up for readability (no behavior change):

-  const url = new URL(fullUrl);
-  const query: Record<string, string> = {};
-  url.searchParams.forEach((v, k) => (query[k] = v));
-  return query;
+  const url = new URL(fullUrl);
+  return Object.fromEntries(url.searchParams.entries()) as Record<string, string>;
packages/journey-client/src/lib/journey.api.ts (2)

25-26: Confirm whether you want RTK Query core or React hooks.

If hooks are needed (e.g., useStartMutation), import from @reduxjs/toolkit/query/react. If not, current import is fine.


85-176: Store wiring and reducer keys verified

  • configureStore sets thunk.extraArgument with requestMiddleware, logger, frDevice and config (packages/journey-client/src/lib/journey.store.ts, args ≈ lines 28–40).
  • rootReducer mounts [journeyApi.reducerPath]: journeyApi.reducer and [journeySlice.name]: journeySlice.reducer — no key collision (packages/journey-client/src/lib/journey.store.ts, lines ≈16–19).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (10)
.changeset/bumpy-cows-hang.md (5)

12-16: Clarify “replacing FRAuth” = removal or deprecation; adjust semver/message accordingly.

If FRAuth is removed or behavior changes externally, this warrants a major bump and a BREAKING section. If only added alongside FRAuth (deprecated), keep minor but document deprecation and migration.

Proposed note to add:

-This release introduces a new, stateful `journey-client` package, replacing the legacy `FRAuth` class.
+This release introduces a new, stateful `journey-client` package.
+
+### Deprecations
+- `FRAuth` is deprecated in favor of `journey()`; it remains available in this release. See Migration below.
+
+### Migration
+- Replace `FRAuth.next()` usage with `journey().start()` / `journey().resume(...)` patterns. Link: JIRA SDKS-4044 / docs.

If FRAuth is removed, rename “Deprecations” to “BREAKING CHANGES” and bump to major.


17-18: Persistence/back-compat note for resume payloads.

State that storage keys/shape are backward-compatible (or provide migration). Otherwise users with existing serialized Steps may fail to resume after upgrading.

Add a short note indicating:

  • where Step payloads are stored (localStorage/sessionStorage/indexedDB/in-memory),
  • key names/namespaces before vs after,
  • compatibility behavior (auto-upgrade or clear-on-incompat).

20-22: Inlining constants: confirm no API surface change.

If REQUESTED_WITH / X_REQUESTED_PLATFORM exports were renamed, moved, or changed value/type, this is breaking. If unchanged, add a line explicitly noting exports are stable and tree-shaking only improves bundles.

Suggested wording:

-  - Inlined `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` constants with literal types for better tree-shaking and type narrowing.
+  - Inlined `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` with `as const` literal types for better tree-shaking. Export names and values are unchanged.

23-25: TypeScript compatibility: call out minimum TS version (verbatimModuleSyntax).

verbatimModuleSyntax generally requires TS ≥ 5.x. Document the minimum supported TS version (and Node if ESM-only packages were introduced). If consumers must upgrade TS, consider marking as BREAKING.

Add to the notes:

+  - Minimum supported TypeScript: >= 5.x (due to `verbatimModuleSyntax`), tested with <your version>. Consumers on older TS must upgrade.

If any packages flipped to "type": "module", also specify Node ≥ 18 LTS and CJS interop guidance.


23-24: Consistent package family naming in prose.

Use either repository path style (sdk-effects/iframe-manager, sdk-effects/logger, sdk-effects/storage) or published names (@forgerock/iframe-manager, @forgerock/sdk-logger, @forgerock/storage) consistently across the note to avoid confusion.

packages/journey-client/src/lib/journey-client.test.ts (5)

60-61: Set Content-Type to application/json on mocked responses

RTK Query’s fetchBaseQuery typically parses JSON only when the Content-Type includes application/json. Without it, responses may be treated as text and break FRStep wrapping.

Apply this diff at each mocked fetch response:

- mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse)));
+ mockFetch.mockResolvedValue(
+   new Response(JSON.stringify(mockStepResponse), {
+     status: 200,
+     headers: { 'Content-Type': 'application/json' },
+   }),
+ );

- mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload)));
+ mockFetch.mockResolvedValue(
+   new Response(JSON.stringify(nextStepPayload), {
+     status: 200,
+     headers: { 'Content-Type': 'application/json' },
+   }),
+ );

- mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload)));
+ mockFetch.mockResolvedValue(
+   new Response(JSON.stringify(nextStepPayload), {
+     status: 200,
+     headers: { 'Content-Type': 'application/json' },
+   }),
+ );

- mockFetch.mockResolvedValue(new Response(JSON.stringify(nextStepPayload)));
+ mockFetch.mockResolvedValue(
+   new Response(JSON.stringify(nextStepPayload), {
+     status: 200,
+     headers: { 'Content-Type': 'application/json' },
+   }),
+ );

- mockFetch.mockResolvedValue(new Response(JSON.stringify(mockStepResponse)));
+ mockFetch.mockResolvedValue(
+   new Response(JSON.stringify(mockStepResponse), {
+     status: 200,
+     headers: { 'Content-Type': 'application/json' },
+   }),
+ );

Also applies to: 96-97, 149-150, 185-186, 222-223


65-69: Make fetch assertions robust to Request|string inputs

RTK Query often calls fetch(url, init) (string URL) rather than a Request instance. Current assertions assume a Request. Use a small helper to normalize:

@@
-    const request = mockFetch.mock.calls[0][0] as Request;
-    // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate
-    expect(request.url).toBe('https://test.com/json/realms/root/authenticate');
+    const [input, init] = mockFetch.mock.calls[0] as [RequestInfo, RequestInit?];
+    const url = typeof input === 'string' ? input : input.url;
+    // TODO: This should be /journeys?_action=start, but the current implementation calls /authenticate
+    expect(url).toBe('https://test.com/json/realms/root/authenticate');

For method/body checks:

@@
-    const request = mockFetch.mock.calls[0][0] as Request;
-    // TODO: This should be /journeys?_action=next, but the current implementation calls /authenticate
-    expect(request.url).toBe('https://test.com/json/realms/root/authenticate');
-    expect(request.method).toBe('POST');
-    expect(await request.json()).toEqual(initialStep.payload);
+    const [input, init] = mockFetch.mock.calls[0] as [RequestInfo, RequestInit?];
+    const url = typeof input === 'string' ? input : input.url;
+    const method = typeof input === 'string' ? (init?.method ?? 'GET') : input.method;
+    const body = typeof input === 'string'
+      ? (init?.body ? JSON.parse(init.body as string) : undefined)
+      : await (input as Request).clone().json();
+    // TODO: This should be /journeys?_action=next, but the current implementation calls /authenticate
+    expect(url).toBe('https://test.com/json/realms/root/authenticate');
+    expect(method).toBe('POST');
+    expect(body).toEqual(initialStep.payload);

Apply the same pattern in other spots in this file where request.url/request.method/request.json() are used.

I can push a follow-up commit that introduces a shared normalizeFetchCall() helper and refactors all tests here to use it; want me to do that?

Also applies to: 101-107, 159-169, 196-199, 231-236


44-46: Restore global fetch and use stronger mock resets to avoid cross-test leakage

  • Restore the original global.fetch after the suite.
  • Prefer vi.resetAllMocks() for isolation (clears implementations and calls).

Apply:

@@
-import { afterEach, describe, expect, test, vi } from 'vitest';
+import { afterAll, afterEach, describe, expect, test, vi } from 'vitest';
@@
-const mockFetch = vi.fn();
-global.fetch = mockFetch;
+const originalFetch = global.fetch;
+const mockFetch = vi.fn();
+global.fetch = mockFetch;
@@
-  afterEach(() => {
-    vi.clearAllMocks();
-  });
+  afterEach(() => {
+    vi.resetAllMocks();
+  });
+  afterAll(() => {
+    global.fetch = originalFetch;
+  });

Also applies to: 8-8, 33-35


218-220: Remove unnecessary stub to match the assertion

This test asserts get is not called but still sets a resolved value for it. Drop the stub to reduce confusion:

-      mockStorageInstance.get.mockResolvedValue(undefined);
-
       const mockStepResponse: Step = { callbacks: [] };

21-24: Optionally assert storage factory usage

To ensure the client uses the intended storage, consider asserting createStorage is invoked with { type: 'sessionStorage', name: 'journey-step' }. This tightens the contract around redirect/resume persistence.

I can add an expectation for createStorage arguments in the initialization test if you’d like.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fb70a49 and 5309087.

📒 Files selected for processing (5)
  • .changeset/bumpy-cows-hang.md (1 hunks)
  • packages/journey-client/src/lib/journey-client.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.ts (1 hunks)
  • packages/journey-client/tsconfig.json (1 hunks)
  • packages/sdk-utilities/src/lib/constants/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/journey-client/tsconfig.json
  • packages/journey-client/src/lib/journey-client.ts
  • packages/sdk-utilities/src/lib/constants/index.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/journey-client/src/lib/journey-client.test.ts (3)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/journey-client/src/lib/journey-client.ts (1)
  • journey (21-141)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Step (70-85)
  • callbackType (10-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (1)
.changeset/bumpy-cows-hang.md (1)

2-8: No action required — changeset keys match package.json names.
Workspace shows the packages exist: @forgerock/iframe-manager -> packages/sdk-effects/iframe-manager/package.json; @forgerock/sdk-logger -> packages/sdk-effects/logger/package.json; @forgerock/storage -> packages/sdk-effects/storage/package.json.

Comment on lines +125 to +130
const assignMock = vi.fn();
const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({
...window.location,
assign: assignMock,
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify jsdom environment for window.location mocking

These assertions rely on window being available (jsdom). If the repo’s Vitest env isn’t jsdom, this test will fail.


🏁 Script executed:

#!/bin/bash
# Check Vitest environment configuration
# 1) Look for vitest.config.* files and print any 'environment' settings
fd -a -HI 'vitest.config.*' | while read -r f; do
  echo "---- $f ----"
  rg -n 'environment\s*:' "$f" -n -C2 || true
done

# 2) Check package.json for a Vitest 'environment' override
fd -a -HI package.json | while read -r f; do
  echo "---- $f ----"
  jq -r '.vitest.environment // empty' "$f" 2>/dev/null || true
done

Length of output: 1751


Set Vitest env to jsdom or remove reliance on global window

tools/user-scripts/vitest.config.ts sets environment: 'node' (line 11) and no package.json overrides were found. The test's vi.spyOn(window, 'location', 'get') will fail under Node — either change Vitest to environment: 'jsdom' (globally, per-package, or per-test) or refactor the test to stub/inject a Location-like object (e.g., vi.stubGlobal or explicit mock).

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-client.test.ts around lines 125 to
130, the test uses vi.spyOn(window, 'location', 'get') which fails because
Vitest is configured to run in the Node environment; either switch the Vitest
environment to jsdom for this test (set environment: 'jsdom' in
tools/user-scripts/vitest.config.ts or add a per-package or per-test override)
or stop relying on the real global window by refactoring the test to stub a
Location-like object (use vi.stubGlobal('location', { ... }) or inject/mock a
location object via dependency injection) so the test no longer depends on a
browser global.

@ryanbas21 ryanbas21 force-pushed the create-journey-package branch from 850e239 to 6a3a765 Compare September 17, 2025 02:19
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
.changeset/kind-guests-sneeze.md (1)

5-11: Tighten changeset body style (drop “feat:” prefix; keep bullets imperative).

Changelogs often add their own “feat:” labels; repeating it in the body can be noisy. Suggest using a short sentence summary line and imperative bullet points.

-feat: Implement new journey client
+Implement new journey client

-- Implemented a new `journey()` factory function for creating stateful client instances.
+- Add `journey()` factory for stateful client instances.
 - Integrated Redux Toolkit and RTK Query for robust state management and API interactions.
-- Refactored `resume` logic to correctly persist and retrieve plain `Step` payloads, resolving prototype loss issues during serialization.
+- Refactor `resume` logic to persist/retrieve plain `Step` payloads (fix prototype loss during serialization).
 - Improved error handling and type safety within the client.
-- Updated internal callback handling and device profiling integration.
+- Update internal callback handling and device profiling integration.
GEMINI.md (6)

7-15: Augment guidance with re‑exports and TS config nuance.

Mention export type for re‑exports and note interaction with verbatimModuleSyntax to prevent accidental value imports.

 - **Type-Only Imports**: When importing only TypeScript types or interfaces, always use the `import type` syntax. This improves clarity and can provide build-time optimizations.
+ - **Type-Only Imports**: When importing only TypeScript types or interfaces, always use `import type`. For re‑exports, prefer `export type { ... }`. This avoids bringing types into value space and plays well with `compilerOptions.verbatimModuleSyntax: true`.
+
+  ```ts
+  // Re-export types (good)
+  export type { Step, Callback } from '@forgerock/sdk-types';
+  ```

23-24: Clarify Nx affected range for local vs CI.

Using --base=main may differ across environments. Recommend explicit remote ref and an alternative using print-affected.

-You can use `pnpm exec nx show projects --affected --base=main --head=HEAD` to get a list of affected projects, then filter for the ones you directly modified.
+You can use:
+`pnpm exec nx show projects --affected --base=origin/main --head=HEAD`
+or (alternative):
+`pnpm exec nx print-affected --base=origin/main --head=HEAD`
+Choose the variant that matches your CI/local setup.

25-31: Offer interactive alternative to empty changesets.

For most flows, the interactive prompt reduces errors and selects packages explicitly.

-For each directly modified package, generate an empty changeset file using the `changeset` CLI:
+For each directly modified package, either create an empty file or use the interactive CLI to select packages:
 
-```bash
-pnpm changeset add --empty
-```
+```bash
+# interactive (recommended)
+pnpm changeset
+
+# or empty (advanced)
+pnpm changeset add --empty
+```

33-44: Template tweak: show multi‑package front‑matter and quoting.

Even when preferring atomic changesets, include an example for multiple entries to document the format.

     ```markdown
     ---
     '<package-name>': <version-type>
+    # Example for multiple (if ever needed):
+    '@forgerock/a': minor
+    '@forgerock/b': patch
     ---

46-51: Align body style with release tooling.

Advise against “feat:”/“fix:” prefixes in the body to avoid duplication in generated changelogs.

-    - Replace `<change-type>` with `feat`, `fix`, `chore`, etc., following conventional commit guidelines.
-    - Provide a brief, concise description of the change.
+    - Keep the body descriptive; avoid prefixes like `feat:`/`fix:` since changelogs add labels.
+    - Provide a brief, imperative description of the change.

52-53: Add a validation step to preview releases.

Include a quick status preview to catch mistakes before pushing.

 4.  **Remove Combined Changesets**: If you initially created a single changeset file that covered multiple packages, ensure you remove it after creating the atomic changesets.
+
+5.  **Preview Release Plan**: Validate the upcoming versions and changelog entries.
+
+    ```bash
+    pnpm changeset status --since=origin/main
+    ```
.changeset/bright-lights-yawn.md (1)

5-7: Tighten the summary (drop "feat:" and be explicit for release notes).

Prefer imperative mood without a Conventional Commit prefix and call out the new subpath export to aid consumers.

Apply this diff:

-feat: Update SDK utilities
+Inline REQUESTED_WITH and X_REQUESTED_PLATFORM as literal types and expose a "./constants" subpath export.
 
-- Inlined `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` constants with literal types for better tree-shaking and type narrowing.
+- Inlines `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` with literal types for better tree‑shaking and type narrowing.
+- Adds `./constants` subpath export for direct imports.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3a765 and 3eaa636.

📒 Files selected for processing (7)
  • .changeset/breezy-actors-sell.md (1 hunks)
  • .changeset/bright-lights-yawn.md (1 hunks)
  • .changeset/good-games-accept.md (1 hunks)
  • .changeset/kind-guests-sneeze.md (1 hunks)
  • .changeset/tender-schools-scream.md (1 hunks)
  • .changeset/wild-items-stop.md (1 hunks)
  • GEMINI.md (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • .changeset/tender-schools-scream.md
  • .changeset/good-games-accept.md
  • .changeset/breezy-actors-sell.md
  • .changeset/wild-items-stop.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (3)
.changeset/kind-guests-sneeze.md (1)

1-3: Front‑matter looks correct for Changesets.

Package key and bump type are valid; this will generate a minor release for @forgerock/journey-client.

Confirm no other packages in this PR need coordinated bumps to avoid inconsistent versions.

.changeset/bright-lights-yawn.md (2)

1-3: Front‑matter looks correct for a minor bump.


8-8: Remove stray '8' from .changeset/bright-lights-yawn.md

Standalone "8" at the end of the file will break Changesets parsing and CI—remove it.

Location: .changeset/bright-lights-yawn.md (line 8)

Apply this diff:

-8

Validate with:

npx changeset status --since=origin/main

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (11)
packages/oidc-client/GEMINI.md (11)

7-7: Clarify supported/unsupported OIDC flows to prevent misuse.

Explicitly state that only Authorization Code + PKCE is supported; discourage Implicit/Hybrid.

-`@forgerock/oidc-client` is a generic OpenID Connect (OIDC) client designed to handle standard authentication flows, primarily the Authorization Code Flow with PKCE. It is built to be provider-agnostic and can be configured to work with any OIDC-compliant identity provider.
+`@forgerock/oidc-client` is a generic OpenID Connect (OIDC) client designed to handle the Authorization Code Flow with PKCE. Implicit and Hybrid flows are not supported. It is provider‑agnostic and can be configured to work with any OIDC‑compliant identity provider.

13-14: Discovery: document caching, validation, and timeouts.

Add guidance to cache .well-known/openid-configuration, validate the issuer, and set HTTP timeouts/retries.

 - **Asynchronous Factory (`oidc`)**: The main entry point is an `async` factory function, `oidc(config)`, which must be awaited. It performs an initial OIDC discovery by fetching the `.well-known` configuration endpoint to set up the necessary API endpoints before the client is returned.
+ - **Asynchronous Factory (`oidc`)**: The main entry is an `async` factory `oidc(config)` that fetches `.well-known/openid-configuration`, validates the `issuer`, and derives endpoints. Implement caching of discovery metadata, enforce request timeouts, and retry with jitter. If discovery fails, surface a typed error with provider URL and cause (redacted).

15-16: Update naming: Effect‑TS → Effect (library has been renamed).

Use current library name and link to avoid confusion.

-- **Effect-TS for Core Logic**: The primary business logic for handling authorization, token exchange, and logout is composed using the [Effect-TS](https://effect.website/) library. This provides a robust, type-safe, and functional approach to managing complex asynchronous operations and error handling.
+- **Effect for Core Logic**: The primary business logic is composed using the [Effect](https://effect.website/) library, providing typed, functional orchestration for async operations and error handling.

17-18: HTTP layer: specify RTK Query base settings (timeouts, retries, headers).

Document baseQuery configuration (timeout, retry/backoff, credentials mode) and security for Authorization headers.

 - **RTK Query for HTTP Requests**: While Effect-TS orchestrates the logic, the underlying HTTP requests to the OIDC provider are executed via an RTK Query API service defined in `src/lib/oidc.api.ts`.
+ - **RTK Query for HTTP Requests**: HTTP calls are performed via an RTK Query service (e.g., `src/lib/oidc.api.ts`) with an explicit `baseQuery` that sets timeouts, limited retries with backoff, and consistent Authorization header handling. Avoid sending tokens to non‑issuer origins; prefer absolute issuer URLs.

20-23: Token storage: add guidance on XSS risk and storage backends.

State preferred backends (in‑memory for SPAs) and caution against localStorage unless risks are accepted.

-  - `@forgerock/storage`: Used for abstracting the storage of OAuth tokens.
+  - `@forgerock/storage`: Abstracts token storage. Recommended: in‑memory (default) or session‑scoped storage for SPAs. Avoid persistent `localStorage` due to XSS exfiltration risk; if used, document compensating controls (CSP, strict typing, no inline scripts).

30-31: Include nonce/state handling and validation.

Document that .url() persists and later validates state (CSRF) and nonce (ID token replay).

-- **`.url(options?)`**: Asynchronously builds a complete authorization URL for a redirect-based login. It handles the creation and storage of `state` and PKCE parameters.
+- **`.url(options?)`**: Builds the redirect URL, creating and storing `state`, `nonce`, and PKCE parameters. The client validates returned `state` and `nonce` during code exchange to prevent CSRF/replay.

31-32: Silent auth caveats: third‑party cookie blocks and fallbacks.

Note that iframe silent auth requires an IdP session and is often blocked; document timeouts and fallback.

-**`.background(options?)`**: Attempts to perform the authorization flow silently in a hidden iframe. It returns a promise that resolves with the `code` and `state` on success, or an error on failure.
+**`.background(options?)`**: Attempts silent auth (`prompt=none`) in a hidden iframe. Modern browsers frequently block this without first‑party cookies; implement a short timeout and graceful fallback to interactive login.

35-37: Clarify renew semantics and precedence.

Describe how forceRenew vs backgroundRenew behaves, and whether refresh tokens or iframe are used.

-**`.get(options?)`**: Retrieves tokens from storage. It includes options for forcing a renewal (`forceRenew: true`) or performing a silent renewal (`backgroundRenew: true`).
+**`.get(options?)`**: Retrieves tokens from storage. `forceRenew` uses refresh‑token (if present) or falls back to interactive auth; `backgroundRenew` first attempts iframe silent auth, then refresh‑token if available. Document precedence when both are set.

37-38: Revocation: specify RFC 7009 and ID token behavior.

ID tokens aren’t revocable; revocation applies to access/refresh tokens and may require client auth.

-**`.revoke()`**: Revokes the stored access token.
+**`.revoke()`**: Calls the RFC 7009 revocation endpoint (if advertised) to revoke access and/or refresh tokens (client‑auth as required). ID tokens cannot be revoked; they are simply discarded locally.

41-43: Logout: end_session_endpoint is optional; document fallbacks.

Describe RP‑Initiated Logout behavior and non‑supporting providers.

-**`.logout()`**: Performs a full logout by revoking tokens and redirecting to the `end_session_endpoint`.
+**`.logout()`**: If `end_session_endpoint` is present, performs RP‑Initiated Logout; otherwise, revokes tokens (where supported) and clears local storage without redirect. Some providers require an `id_token_hint` and `post_logout_redirect_uri`.

44-49: Add Quick Start and Config shape for faster adoption.

Provide a minimal code sample and typed config to reduce onboarding friction.

 ## 4. Development Workflow
@@
 - **Build**: `pnpm nx build @forgerock/oidc-client`
 - **Test**: `pnpm nx test @forgerock/oidc-client`
 - **Lint**: `pnpm nx lint @forgerock/oidc-client`
+
+## 5. Quick Start
+
+```ts
+import { oidc } from '@forgerock/oidc-client';
+
+const client = await oidc({
+  issuer: 'https://idp.example.com',
+  clientId: 'spa-client',
+  redirectUri: 'https://app.example.com/callback',
+  scopes: ['openid', 'profile', 'email'],
+});
+
+const url = await client.authorize.url();
+window.location.assign(url);
+```
+
+## 6. Configuration
+
+```ts
+type OidcConfig = {
+  issuer: string;                // Base issuer URL
+  clientId: string;
+  redirectUri: string;
+  scopes?: string[];
+  clockSkewSeconds?: number;     // default 0
+  storage?: 'memory' | 'session' | 'local'; // default 'memory'
+};
+```

I can draft these edits as a follow‑up PR if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3eaa636 and 22b5f48.

📒 Files selected for processing (15)
  • CLAUDE.md (0 hunks)
  • GEMINI.md (1 hunks)
  • e2e/GEMINI.md (1 hunks)
  • packages/davinci-client/GEMINI.md (1 hunks)
  • packages/device-client/GEMINI.md (1 hunks)
  • packages/journey-client/GEMINI.md (1 hunks)
  • packages/oidc-client/GEMINI.md (1 hunks)
  • packages/protect/GEMINI.md (1 hunks)
  • packages/sdk-effects/iframe-manager/GEMINI.md (1 hunks)
  • packages/sdk-effects/logger/GEMINI.md (1 hunks)
  • packages/sdk-effects/oidc/GEMINI.md (1 hunks)
  • packages/sdk-effects/sdk-request-middleware/GEMINI.md (1 hunks)
  • packages/sdk-effects/storage/GEMINI.md (1 hunks)
  • packages/sdk-types/GEMINI.md (1 hunks)
  • packages/sdk-utilities/GEMINI.md (1 hunks)
💤 Files with no reviewable changes (1)
  • CLAUDE.md
✅ Files skipped from review due to trivial changes (12)
  • packages/sdk-effects/logger/GEMINI.md
  • packages/davinci-client/GEMINI.md
  • packages/sdk-effects/iframe-manager/GEMINI.md
  • packages/sdk-types/GEMINI.md
  • packages/device-client/GEMINI.md
  • packages/sdk-effects/storage/GEMINI.md
  • packages/sdk-effects/sdk-request-middleware/GEMINI.md
  • e2e/GEMINI.md
  • packages/sdk-effects/oidc/GEMINI.md
  • packages/sdk-utilities/GEMINI.md
  • packages/protect/GEMINI.md
  • GEMINI.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/journey-client/GEMINI.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (1)
packages/oidc-client/GEMINI.md (1)

1-3: Good addition: clear doc intent for the Gemini agent.

Intro is concise and helpful.

@ryanbas21 ryanbas21 force-pushed the create-journey-package branch 2 times, most recently from d5c5700 to cabd78d Compare September 17, 2025 16:15
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (37)
packages/journey-client/src/lib/callbacks/text-output-callback.test.ts (1)

13-26: Prefer satisfies to preserve literal types and stricter checks

This avoids widening while ensuring the payload conforms to Callback.

-  const payload: Callback = {
+  const payload = {
     type: callbackType.TextOutputCallback,
     output: [
       {
         name: 'message',
         value: 'This is a message',
       },
       {
         name: 'messageType',
         value: '0',
       },
     ],
     input: [],
-  };
+  } satisfies Callback;
packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts (3)

8-11: Add base‑class import to assert inheritance explicitly

Import TextOutputCallback so the test can verify prototype inheritance.

 import { describe, it, expect } from 'vitest';
 import { callbackType, type Callback } from '@forgerock/sdk-types';
 import SuspendedTextOutputCallback from './suspended-text-output-callback.js';
+import TextOutputCallback from './text-output-callback.js';

14-27: Prefer satisfies for payload typing

Keeps literal values intact and tightens conformance checks.

-    const payload: Callback = {
+    const payload = {
       type: callbackType.SuspendedTextOutputCallback,
       output: [
         {
           name: 'message',
           value: 'Suspended message',
         },
         {
           name: 'messageType',
           value: '0',
         },
       ],
       input: [],
-    };
+    } satisfies Callback;

28-31: Assert inheritance explicitly

Strengthens the “inherits from TextOutputCallback” claim.

     const cb = new SuspendedTextOutputCallback(payload);
     expect(cb.getMessage()).toBe('Suspended message');
     expect(cb.getMessageType()).toBe('0');
+    expect(cb).toBeInstanceOf(SuspendedTextOutputCallback);
+    expect(cb).toBeInstanceOf(TextOutputCallback);
packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts (4)

13-27: Avoid shared mutable fixture across tests

The top‑level payload object can be mutated by setResult(); prefer per‑test construction to prevent hidden coupling if tests are reordered or run concurrently.

Apply this diff to replace the shared object with a factory:

-  const payload: Callback = {
-    type: callbackType.ReCaptchaCallback,
-    output: [
-      {
-        name: 'recaptchaSiteKey',
-        value: 'test-site-key',
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken1',
-        value: '',
-      },
-    ],
-  };
+  const createPayload = (): Callback => ({
+    type: callbackType.ReCaptchaCallback,
+    output: [{ name: 'recaptchaSiteKey', value: 'test-site-key' }],
+    input: [{ name: 'IDToken1', value: '' }],
+  });

29-38: Use factory when instantiating to keep tests isolated

Follow‑up to the previous comment: construct a fresh payload per test.

-  it('should allow getting the site key', () => {
-    const cb = new ReCaptchaCallback(payload);
+  it('should allow getting the site key', () => {
+    const cb = new ReCaptchaCallback(createPayload());
     expect(cb.getSiteKey()).toBe('test-site-key');
   });
 
-  it('should allow setting the result', () => {
-    const cb = new ReCaptchaCallback(payload);
+  it('should allow setting the result', () => {
+    const cb = new ReCaptchaCallback(createPayload());
     cb.setResult('recaptcha-response-token');
     expect(cb.getInputValue()).toBe('recaptcha-response-token');
   });

30-30: Nit: prefer a descriptive local name

Rename cb → callback for readability.

-    const cb = new ReCaptchaCallback(createPayload());
+    const callback = new ReCaptchaCallback(createPayload());
-    expect(cb.getSiteKey()).toBe('test-site-key');
+    expect(callback.getSiteKey()).toBe('test-site-key');
-    const cb = new ReCaptchaCallback(createPayload());
-    cb.setResult('recaptcha-response-token');
-    expect(cb.getInputValue()).toBe('recaptcha-response-token');
+    const callback = new ReCaptchaCallback(createPayload());
+    callback.setResult('recaptcha-response-token');
+    expect(callback.getInputValue()).toBe('recaptcha-response-token');

Also applies to: 35-35


29-39: Consider a couple more edge‑case tests

  • Missing recaptchaSiteKey should throw or return a sensible default.
  • setResult('') or undefined should clear/not set the value per spec.

I can add these cases mirroring behavior in recaptcha-callback.ts—want a follow‑up PR patch?

packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts (1)

14-18: Use satisfies for stronger compile‑time shape checking.

This keeps runtime the same while validating the payload conforms to Callback without widening its type.

Apply:

-    const payload: Callback = {
+    const payload = {
       type: callbackType.HiddenValueCallback,
       output: [{ name: 'value', value: 'some-hidden-value' }],
       input: [{ name: 'IDToken1', value: '' }],
-    };
+    } satisfies Callback;
packages/journey-client/src/lib/callbacks/choice-callback.test.ts (2)

58-62: Make throw assertions resilient; assert by type/regex rather than exact strings.

Exact message matching is brittle. Prefer error class and/or regex on message to reduce spurious failures if wording changes.

   it('should throw an error for an out-of-bounds index', () => {
-    const cb = new ChoiceCallback(payload);
-    expect(() => cb.setChoiceIndex(3)).toThrow('3 is out of bounds');
-    expect(() => cb.setChoiceIndex(-1)).toThrow('-1 is out of bounds');
+    const cb = new ChoiceCallback(makePayload());
+    const act3 = () => cb.setChoiceIndex(3);
+    const actNeg1 = () => cb.setChoiceIndex(-1);
+    // If implementation throws RangeError, keep this; otherwise drop it and keep regex assertions.
+    // expect(act3).toThrowError(RangeError);
+    // expect(actNeg1).toThrowError(RangeError);
+    expect(act3).toThrow(/out of bounds/i);
+    expect(actNeg1).toThrow(/out of bounds/i);
   });
@@
   it('should throw an error for an invalid choice value', () => {
-    const cb = new ChoiceCallback(payload);
-    expect(() => cb.setChoiceValue('four')).toThrow('"four" is not a valid choice');
+    const cb = new ChoiceCallback(makePayload());
+    const actInvalid = () => cb.setChoiceValue('four');
+    // If a specific error type is used (e.g., TypeError), assert it here.
+    // expect(actInvalid).toThrowError(TypeError);
+    expect(actInvalid).toThrow(/not a valid choice/i);
   });

Also applies to: 70-73


29-35: Optional: also assert initial input value to catch unexpected mutations.

A quick check that getInputValue() equals the payload’s initial index (0) before any setter calls would guard against regressions.

Example:

const cb = new ChoiceCallback(makePayload());
expect(cb.getInputValue()).toBe(0);

Also applies to: 52-56

packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts (1)

13-35: Avoid shared mutable payload across tests (state leakage).

setQuestion/setAnswer likely mutate the payload. Reusing a single payload across it blocks can cause order‑dependent failures. Create a fresh payload per test.

-  const payload: Callback = {
-    type: callbackType.KbaCreateCallback,
-    output: [
-      {
-        name: 'prompt',
-        value: 'What is your favorite color?',
-      },
-      {
-        name: 'predefinedQuestions',
-        value: ['Question 1', 'Question 2'],
-      },
-    ],
-    input: [
-      { name: 'IDToken1question', value: '' },
-      { name: 'IDToken1answer', value: '' },
-    ],
-  };
+  const makePayload = (): Callback => ({
+    type: callbackType.KbaCreateCallback,
+    output: [
+      { name: 'prompt', value: 'What is your favorite color?' },
+      { name: 'predefinedQuestions', value: ['Question 1', 'Question 2'] },
+    ],
+    input: [
+      { name: 'IDToken1question', value: '' },
+      { name: 'IDToken1answer', value: '' },
+    ],
+  });
@@
-    const cb = new KbaCreateCallback(payload);
+    const cb = new KbaCreateCallback(makePayload());
@@
-    const cb = new KbaCreateCallback(payload);
+    const cb = new KbaCreateCallback(makePayload());

Also applies to: 38-38, 44-44

packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts (3)

13-26: Avoid shared test state; use a payload factory (and strengthen typing with “satisfies”)

Sharing a mutable object across tests can cause cross-test coupling if the class mutates payload. Also, “satisfies” preserves literal inference while ensuring the shape matches Callback.

Apply:

-  const payload: Callback = {
+  const buildPayload = (): Callback => ({
     type: callbackType.PollingWaitCallback,
     output: [
       {
         name: 'message',
         value: 'Please wait...',
       },
       {
         name: 'waitTime',
         value: '5000',
       },
     ],
     input: [],
-  };
+  } satisfies Callback);

28-32: Tighten assertions and use fresh payload per test

Add a type/shape assertion for waitTime and instantiate with a fresh payload to prevent mutation leaks.

-  it('should allow getting the message and wait time', () => {
-    const cb = new PollingWaitCallback(payload);
+  it('should allow getting the message and wait time', () => {
+    const cb = new PollingWaitCallback(buildPayload());
     expect(cb.getMessage()).toBe('Please wait...');
     expect(cb.getWaitTime()).toBe(5000);
+    expect(typeof cb.getWaitTime()).toBe('number');
+    expect(Number.isInteger(cb.getWaitTime())).toBe(true);
   });

28-33: Add edge-case tests for robustness

Please add tests for:

  • missing waitTime output
  • non-numeric waitTime (e.g., '5s' or '')
  • missing message

Assert the intended behavior (throw, default, or clamp) per PollingWaitCallback contract.

I can draft these tests once you confirm the expected behavior (throw vs default).

packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts (2)

44-47: Make the error assertion resilient

String‑exact matching (including quotes) is brittle. Use a case‑insensitive regex.

-    expect(() => cb.setProvider('twitter')).toThrow('"twitter" is not a valid choice');
+    expect(() => cb.setProvider('twitter')).toThrow(/not a valid choice/i);

32-36: Minor test cleanup and a quick precondition check

Reduce duplicate calls and assert initial input value before mutation.

-    const cb = new SelectIdPCallback(newPayload());
-    expect(cb.getProviders()).toHaveLength(2);
-    expect(cb.getProviders()[0].provider).toBe('google');
+    const cb = new SelectIdPCallback(newPayload());
+    const providers = cb.getProviders();
+    expect(providers).toHaveLength(2);
+    expect(providers[0].provider).toBe('google');
@@
-    const cb = new SelectIdPCallback(newPayload());
+    const cb = new SelectIdPCallback(newPayload());
+    expect(cb.getInputValue()).toBe('');
     cb.setProvider('facebook');
     expect(cb.getInputValue()).toBe('facebook');

Also applies to: 38-42

packages/journey-client/src/lib/callbacks/factory.test.ts (3)

37-69: Guard against drift between callbackType and tested cases

Add a test that asserts every callbackType value is covered by testCases. This keeps the suite in sync when new callbacks are introduced.

   const testCases = [
@@
     { type: callbackType.ValidatedCreateUsernameCallback, class: ValidatedCreateUsernameCallback },
   ];
 
+  it('covers all callbackType entries', () => {
+    const covered = new Set(testCases.map((t) => t.type));
+    const missing = Object.values(callbackType).filter((t) => !covered.has(t));
+    expect(missing).toEqual([]);
+  });

71-77: Use test.each for clearer per‑case reporting

Improves failure readability without behavior change.

-  testCases.forEach((testCase) => {
-    it(`should create an instance of ${testCase.class.name} for type ${testCase.type}`, () => {
-      const payload: Callback = { type: testCase.type, input: [], output: [] };
-      const callback = createCallback(payload);
-      expect(callback).toBeInstanceOf(testCase.class);
-    });
-  });
+  test.each(testCases)(
+    'should create an instance of %s for type %s',
+    (testCase) => {
+      const payload: Callback = { type: testCase.type, input: [], output: [] };
+      const callback = createCallback(payload);
+      expect(callback).toBeInstanceOf(testCase.class);
+    }
+  );

79-85: Optional: Validate optional input handling

Callback.input is optional in the type. Add a case ensuring the factory doesn’t assume it exists.

   it('should create a base FRCallback for an unknown type', () => {
@@
     expect(callback).not.toBeInstanceOf(NameCallback);
   });
+
+  it('should handle payloads without input property', () => {
+    const payload = { type: callbackType.NameCallback, output: [] } as Callback;
+    const callback = createCallback(payload);
+    expect(callback).toBeInstanceOf(NameCallback);
+  });
packages/journey-client/src/lib/callbacks/metadata-callback.test.ts (2)

15-24: Use “satisfies” for stricter payload typing.

Keeps literal inference while ensuring structural conformance to Callback.

Apply:

-    const payload: Callback = {
+    const payload = {
       type: callbackType.MetadataCallback,
       output: [
         {
           name: 'data',
           value: mockData,
         },
       ],
       input: [],
-    };
+    } satisfies Callback;

12-28: Add a few edge‑case tests to lock behavior.

Cover no “data” output, multiple outputs, and primitive data.

Apply:

 describe('MetadataCallback', () => {
   it('should allow getting the data', () => {
@@
     const cb = new MetadataCallback(payload);
     expect(cb.getData()).toEqual(mockData);
   });
+
+  it('should return undefined when no "data" output is present', () => {
+    const payload = {
+      type: callbackType.MetadataCallback,
+      output: [{ name: 'other', value: 'noop' }],
+      input: [],
+    } satisfies Callback;
+    const cb = new MetadataCallback(payload);
+    expect(cb.getData()).toBeUndefined();
+  });
+
+  it('should prefer the "data" output when multiple outputs exist', () => {
+    const payload = {
+      type: callbackType.MetadataCallback,
+      output: [
+        { name: 'foo', value: 1 },
+        { name: 'data', value: { hello: 'world' } },
+        { name: 'bar', value: 2 },
+      ],
+      input: [],
+    } satisfies Callback;
+    const cb = new MetadataCallback(payload);
+    expect(cb.getData()).toEqual({ hello: 'world' });
+  });
+
+  it('should support primitive data values', () => {
+    const payload = {
+      type: callbackType.MetadataCallback,
+      output: [{ name: 'data', value: 'primitive' }],
+      input: [],
+    } satisfies Callback;
+    const cb = new MetadataCallback(payload);
+    expect(cb.getData()).toBe('primitive');
+  });
 });
packages/journey-client/src/lib/callbacks/name-callback.test.ts (1)

13-27: Avoid shared mutable payload between tests

The tests mutate the shared payload via setters, which can create order-dependent flakiness. Instantiate a fresh payload per test.

Apply this diff:

-  const payload: Callback = {
-    type: callbackType.NameCallback,
-    output: [
-      {
-        name: 'prompt',
-        value: 'Username',
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken1',
-        value: '',
-      },
-    ],
-  };
+  const makePayload = (): Callback => ({
+    type: callbackType.NameCallback,
+    output: [{ name: 'prompt', value: 'Username' }],
+    input: [{ name: 'IDToken1', value: '' }],
+  });
@@
-    const cb = new NameCallback(payload);
+    const cb = new NameCallback(makePayload());
@@
-    const cb = new NameCallback(payload);
+    const cb = new NameCallback(makePayload());

Also applies to: 29-38

packages/journey-client/src/lib/callbacks/password-callback.test.ts (1)

13-35: Isolate payload state per test

Same concern here—mutating a shared payload can leak state across tests. Use a factory for a clean payload each time.

Apply this diff:

-  const payload: Callback = {
-    type: callbackType.PasswordCallback,
-    output: [
-      {
-        name: 'prompt',
-        value: 'Password',
-      },
-      {
-        name: 'policies',
-        value: ['policy1', 'policy2'],
-      },
-      {
-        name: 'failedPolicies',
-        value: ['failedPolicy1'],
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken1',
-        value: '',
-      },
-    ],
-  };
+  const makePayload = (): Callback => ({
+    type: callbackType.PasswordCallback,
+    output: [
+      { name: 'prompt', value: 'Password' },
+      { name: 'policies', value: ['policy1', 'policy2'] },
+      { name: 'failedPolicies', value: ['failedPolicy1'] },
+    ],
+    input: [{ name: 'IDToken1', value: '' }],
+  });
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());

Also applies to: 38-56

packages/journey-client/src/lib/callbacks/redirect-callback.test.ts (1)

24-27: Consider adding a negative-path test

Add a case for missing or malformed redirectUrl to lock in error/undefined behavior and prevent regressions.

packages/journey-client/src/lib/journey.api.test.ts (2)

23-26: Stabilize fetch mocking and request assertions

  • Restore global.fetch after each test to avoid cross-suite leakage.
  • Make assertions resilient whether fetch is called with Request or (url, init) by normalizing to a Request.

Apply this diff:

@@
-// Mock global fetch
-const mockFetch = vi.fn();
-global.fetch = mockFetch;
+// Mock global fetch
+const mockFetch = vi.fn();
+const originalFetch = global.fetch;
@@
   beforeEach(() => {
     // Reset mocks before each test
     mockFetch.mockClear();
+    global.fetch = mockFetch as any;
@@
   afterEach(() => {
     vi.clearAllMocks();
+    global.fetch = originalFetch as any;
   });
@@
-      expect(mockFetch).toHaveBeenCalledTimes(1);
-      const request = mockFetch.mock.calls[0][0] as Request;
-      expect(request.url).toBe(
+      expect(mockFetch).toHaveBeenCalledTimes(1);
+      const [arg0, arg1] = mockFetch.mock.calls[0]!;
+      const req =
+        typeof arg0 === 'string' || arg0 instanceof URL ? new Request(arg0, arg1) : (arg0 as Request);
+      expect(req.url).toBe(
         'https://am.example.com/json/realms/root/authenticate?authIndexType=service&authIndexValue=test-tree',
       );
-      expect(request.method).toBe('POST');
+      expect(req.method).toBe('POST');
@@
-      expect(mockFetch).toHaveBeenCalledTimes(1);
-      const request = mockFetch.mock.calls[0][0] as Request;
-      expect(request.url).toBe(
+      expect(mockFetch).toHaveBeenCalledTimes(1);
+      const [arg0, arg1] = mockFetch.mock.calls[0]!;
+      const req =
+        typeof arg0 === 'string' || arg0 instanceof URL ? new Request(arg0, arg1) : (arg0 as Request);
+      expect(req.url).toBe(
         'https://am.example.com/json/realms/root/authenticate?authIndexType=service&authIndexValue=test-tree',
       );
-      expect(request.method).toBe('POST');
-      expect(await request.json()).toEqual(initialStep);
+      expect(req.method).toBe('POST');
+      expect(await req.json()).toEqual(initialStep);

Also applies to: 38-56, 66-72, 91-98


12-12: Prefer callbackType constant over string literal

Use the exported enum-like constant to reduce typo risk and aid refactors.

Apply this diff:

-import { Step } from '@forgerock/sdk-types';
+import { Step, callbackType } from '@forgerock/sdk-types';
@@
-      const mockStepResponse: Step = {
-        callbacks: [{ type: 'NameCallback', input: [], output: [] }],
-      };
+      const mockStepResponse: Step = {
+        callbacks: [{ type: callbackType.NameCallback, input: [], output: [] }],
+      };

Also applies to: 84-85

packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts (4)

13-35: Avoid shared mutable test fixture; clone a fresh payload per test

setProfile likely mutates input. Reusing a top‑level payload can couple tests. Introduce a basePayload and makePayload() factory.

-  const payload: Callback = {
+  const basePayload: Callback = {
     type: callbackType.DeviceProfileCallback,
     output: [
       {
         name: 'message',
         value: 'Collecting device profile...',
       },
       {
         name: 'metadata',
         value: true,
       },
       {
         name: 'location',
         value: false,
       },
     ],
     input: [
       {
         name: 'IDToken1',
         value: '',
       },
     ],
-  };
+  };
+
+  const makePayload = (): Callback =>
+    JSON.parse(JSON.stringify(basePayload)) as Callback;

38-39: Use a fresh payload instance in each test

Swap payload for makePayload() to prevent cross‑test state bleed.

-    const cb = new DeviceProfileCallback(payload);
+    const cb = new DeviceProfileCallback(makePayload());
@@
-    const cb = new DeviceProfileCallback(payload);
+    const cb = new DeviceProfileCallback(makePayload());
@@
-    const cb = new DeviceProfileCallback(payload);
+    const cb = new DeviceProfileCallback(makePayload());
@@
-    const cb = new DeviceProfileCallback(payload);
+    const cb = new DeviceProfileCallback(makePayload());

Also applies to: 43-44, 48-49, 53-54


52-64: Make the assertion robust to key ordering; type the profile

Compare parsed JSON instead of raw string, and annotate profile.

-    const cb = new DeviceProfileCallback(makePayload());
-    const profile = {
+    const cb = new DeviceProfileCallback(makePayload());
+    const profile: DeviceProfileData = {
       identifier: 'test-id',
       metadata: {
         hardware: { display: {} },
         browser: {},
         platform: {},
       },
     };
     cb.setProfile(profile);
-    expect(cb.getInputValue()).toBe(JSON.stringify(profile));
+    expect(JSON.parse(cb.getInputValue() as string)).toStrictEqual(profile);

47-50: Optional: add a sanity check for initial input value

Catches regressions where the callback prepopulates input unexpectedly.

   it('should allow getting the location requirement', () => {
     const cb = new DeviceProfileCallback(makePayload());
     expect(cb.isLocationRequired()).toBe(false);
   });
+
+  it('should have empty input value initially', () => {
+    const cb = new DeviceProfileCallback(makePayload());
+    expect(cb.getInputValue()).toBe('');
+  });
packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts (1)

12-36: Stabilize date parsing and avoid shared mutable test state.

  • Using new Date().toString() relies on non‑standard parsing; switch to ISO 8601 and assert via toISOString() to avoid flakiness across environments.
  • The single payload object is reused and mutated between tests via setAccepted, which risks inter‑test coupling. Build a fresh payload per test.

Apply:

 describe('TermsAndConditionsCallback', () => {
-  const date = new Date().toString();
-  const payload: Callback = {
-    type: callbackType.TermsAndConditionsCallback,
-    output: [
-      {
-        name: 'terms',
-        value: 'Lorem ipsum...',
-      },
-      {
-        name: 'version',
-        value: '1.0',
-      },
-      {
-        name: 'createDate',
-        value: date,
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken1',
-        value: false,
-      },
-    ],
-  };
+  const dateIso = '2025-01-02T03:04:05.678Z';
+  const makePayload = (): Callback => ({
+    type: callbackType.TermsAndConditionsCallback,
+    output: [
+      { name: 'terms', value: 'Lorem ipsum...' },
+      { name: 'version', value: '1.0' },
+      { name: 'createDate', value: dateIso },
+    ],
+    input: [{ name: 'IDToken1', value: false }],
+  });
 
   it('should allow getting terms, version, and date', () => {
-    const cb = new TermsAndConditionsCallback(payload);
+    const cb = new TermsAndConditionsCallback(makePayload());
     expect(cb.getTerms()).toBe('Lorem ipsum...');
     expect(cb.getVersion()).toBe('1.0');
-    expect(cb.getCreateDate()).toEqual(new Date(date));
+    expect(cb.getCreateDate().toISOString()).toBe(dateIso);
   });
 
   it('should allow setting acceptance', () => {
-    const cb = new TermsAndConditionsCallback(payload);
+    const cb = new TermsAndConditionsCallback(makePayload());
+    expect(cb.getInputValue()).toBe(false);
     cb.setAccepted(true);
     expect(cb.getInputValue()).toBe(true);
     cb.setAccepted(false);
     expect(cb.getInputValue()).toBe(false);
   });

Also applies to: 38-43, 45-51

packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts (5)

13-43: Avoid shared mutable fixture; create a fresh payload per test.
The wrapper likely mutates input via setters. Reusing the same payload object across tests can introduce order-coupled flakiness. Use a factory.

-  const payload: Callback = {
-    type: callbackType.ConfirmationCallback,
-    output: [
-      { name: 'prompt', value: 'Are you sure?' },
-      { name: 'messageType', value: 0 },
-      { name: 'options', value: ['Yes', 'No'] },
-      { name: 'optionType', value: -1 },
-      { name: 'defaultOption', value: 1 },
-    ],
-    input: [
-      { name: 'IDToken1', value: 0 },
-    ],
-  };
+  const makePayload = (): Callback => ({
+    type: callbackType.ConfirmationCallback,
+    output: [
+      { name: 'prompt', value: 'Are you sure?' },
+      { name: 'messageType', value: 0 },
+      { name: 'options', value: ['Yes', 'No'] },
+      { name: 'optionType', value: -1 },
+      { name: 'defaultOption', value: 1 },
+    ],
+    input: [{ name: 'IDToken1', value: 0 }],
+  });
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
@@
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());

Also applies to: 46-46, 51-51, 56-56, 61-61, 66-66, 71-71, 77-77, 82-82, 88-88


76-79: Make failure assertions resilient; also assert state is unchanged after errors.
String‑matching full messages is brittle; prefer regex and verify no mutation on failure.

 it('should throw an error for an invalid index', () => {
-  const cb = new ConfirmationCallback(payload);
-  expect(() => cb.setOptionIndex(2)).toThrow('"2" is not a valid choice');
+  const cb = new ConfirmationCallback(makePayload());
+  expect(() => cb.setOptionIndex(2)).toThrow(/not a valid choice/);
+  expect(cb.getInputValue()).toBe(0);
 });
@@
 it('should throw an error for an invalid value', () => {
-  const cb = new ConfirmationCallback(payload);
-  expect(() => cb.setOptionValue('Maybe')).toThrow('"Maybe" is not a valid choice');
+  const cb = new ConfirmationCallback(makePayload());
+  expect(() => cb.setOptionValue('Maybe')).toThrow(/not a valid choice/);
+  expect(cb.getInputValue()).toBe(0);
 });

Also applies to: 87-90


69-69: Add a test for the initial input value.
Covers getter without prior mutation.

   it('should allow getting the default option', () => {
     const cb = new ConfirmationCallback(payload);
     expect(cb.getDefaultOption()).toBe(1);
   });
+
+  it('should expose the current input value', () => {
+    const cb = new ConfirmationCallback(makePayload());
+    expect(cb.getInputValue()).toBe(0);
+  });

70-74: Add boundary test for index 0.
Currently only tests index 1 and out-of-range 2.

   it('should allow setting the option by index', () => {
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
     cb.setOptionIndex(1);
     expect(cb.getInputValue()).toBe(1);
   });
+
+  it('should allow setting the first option (index 0)', () => {
+    const cb = new ConfirmationCallback(makePayload());
+    cb.setOptionIndex(0);
+    expect(cb.getInputValue()).toBe(0);
+  });

81-85: Also test mapping for 'Yes' → index 0.
Complements the existing 'No' case.

   it('should allow setting the option by value', () => {
-    const cb = new ConfirmationCallback(payload);
+    const cb = new ConfirmationCallback(makePayload());
     cb.setOptionValue('No');
     expect(cb.getInputValue()).toBe(1);
   });
+
+  it('should allow setting the option by value "Yes"', () => {
+    const cb = new ConfirmationCallback(makePayload());
+    cb.setOptionValue('Yes');
+    expect(cb.getInputValue()).toBe(0);
+  });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22b5f48 and cabd78d.

📒 Files selected for processing (18)
  • packages/journey-client/src/lib/callbacks/choice-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/factory.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/metadata-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/name-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/password-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/redirect-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/text-output-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-step.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey.api.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (18)
packages/journey-client/src/lib/callbacks/redirect-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/password-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/text-output-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/metadata-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/choice-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/journey.api.test.ts (3)
packages/journey-client/src/lib/journey.store.ts (1)
  • createJourneyStore (21-47)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Step (70-85)
packages/journey-client/src/lib/callbacks/factory.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • callbackType (10-35)
  • Callback (46-51)
packages/journey-client/src/lib/fr-step.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Step (70-85)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/name-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
🔇 Additional comments (23)
packages/journey-client/src/lib/callbacks/text-output-callback.test.ts (2)

21-23: Confirm expected type for messageType

AM often emits messageType as a number (e.g., 0). If the implementation returns a number, asserting '0' could be brittle. Please confirm the SDK’s contract (string vs number) and align both payload and assertions accordingly.


28-32: LGTM: covers primary getters

Solid happy‑path test for getMessage() and getMessageType().

packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts (1)

22-24: Confirm messageType value type

Same note as the sibling test: if the contract is numeric, consider using 0 rather than '0' in payload and assertions.

packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts (3)

29-32: LGTM: validates site key retrieval correctly

Test accurately asserts getSiteKey() behavior.


34-38: LGTM: validates result write‑back

Test confirms setResult() updates the input value as expected.


10-10: ESM import extension is correct for this package

packages/journey-client/tsconfig.lib.json and tsconfig.spec.json set "module" and "moduleResolution" to "nodenext", so the import of "./recaptcha-callback.js" from a .ts test file is valid — no change required.

packages/journey-client/src/lib/callbacks/hidden-value-callback.test.ts (2)

19-22: LGTM: Happy‑path coverage is clear and accurate.

Instantiation and output retrieval assertions look correct for HiddenValueCallback.


10-10: OK to keep .js extension — journey-client uses NodeNext module resolution.

packages/journey-client/tsconfig.lib.json and packages/journey-client/tsconfig.spec.json set "module" and "moduleResolution" to "nodenext", so explicit .js imports are supported; no change required.

packages/journey-client/src/lib/callbacks/choice-callback.test.ts (1)

37-50: LGTM: happy‑path coverage is clear and readable.

Prompt, choices, and default index assertions match expected behavior and improve confidence in the callback wrapper.

Also applies to: 42-45

packages/journey-client/src/lib/callbacks/kba-create-callback.test.ts (3)

37-41: LGTM: getters

Covers prompt and predefined questions; assertions are precise.


43-49: LGTM: setters

Validates that setters write to expected input names.


8-11: Confirm ESM/NodeNext resolution for .js extension in TS import.
Found NodeNext settings in packages/sdk-utilities (tsconfig.lib.json, tsconfig.spec.json), tools/user-scripts/tsconfig.lib.json, and e2e/mock-api-v2/tsconfig.app.json — no tsconfig detected for packages/journey-client. Ensure the journey-client package (or root/Vitest config) sets module: "NodeNext" and moduleResolution: "nodenext", or change the import to an extension-less/TS path so resolution doesn't rely on NodeNext.

packages/journey-client/src/lib/callbacks/polling-wait-callback.test.ts (2)

12-26: LGTM: payload shape and callback type are correct

The test builds a valid PollingWaitCallback payload and covers string→number conversion. Nice.


8-10: Confirm ESM import resolution for ".js" extension in TS

  • Findings: packages/journey-client/package.json has "type": "module"; root package.json pins TypeScript 5.8.3; packages/journey-client/tsconfig.json and tsconfig.json (root) do not set compilerOptions.module / moduleResolution / verbatimModuleSyntax; allowImportingTs is not set.
  • Action: either (A) set packages/journey-client/tsconfig.json compilerOptions to use NodeNext/Bundler semantics (e.g. moduleResolution: "nodenext" or "bundler" and module appropriate to your build) and consider verbatimModuleSyntax so imports like ./polling-wait-callback.js resolve at compile time, or (B) change the test import to the TS source (or remove the extension). Ensure the test runner (vitest) loads the intended tsconfig.
  • Files to check: package.json (root), packages/journey-client/package.json, packages/journey-client/tsconfig.json, tsconfig.json (root).
packages/journey-client/src/lib/callbacks/metadata-callback.test.ts (2)

1-6: LGTM on header/license.

Header is correct and consistent with the repo’s convention.


8-11: ESM import with “.js” extension — confirm TS/Vitest config

Couldn’t verify configs (script errors). Ensure ESM-style imports are supported before merging:

  • Affected: packages/journey-client/src/lib/callbacks/metadata-callback.test.ts (import './metadata-callback.js').
  • Check packages/journey-client/tsconfig.json: compilerOptions.module should be NodeNext or ESNext; moduleResolution should be NodeNext or Bundler.
  • Check package.json (package and/or repo root) has "type":"module" or that vitest config is ESM (vitest.config.mjs/.mts or otherwise run under ESM).
  • If project is not ESM-aware, change the import to extensionless ('./metadata-callback') or import the TS source, or update tsconfig/vitest accordingly.
packages/journey-client/src/lib/fr-step.test.ts (1)

13-101: LGTM: Comprehensive FRStep coverage

Covers init, type filtering (single/multi/none), mutation, and accessors. Solid.

packages/journey-client/src/lib/callbacks/device-profile-callback.test.ts (2)

37-45: Tests read clean; coverage hits key behaviors

Happy path coverage for message/flags looks good.


8-11: DeviceProfileData import already present; .js ESM imports are correct

packages/journey-client/src/lib/callbacks/device-profile-callback.ts already has import type { DeviceProfileData } from '../fr-device/interfaces.js'; (line 9). packages/journey-client/package.json declares "type": "module" and the repo consistently uses .js extensions for ESM imports — no change required.

Likely an incorrect or invalid review comment.

packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.test.ts (1)

10-10: Confirmed: ESM resolution is configured for explicit .js imports

packages/journey-client/tsconfig.lib.json and tsconfig.spec.json set "module" and "moduleResolution" to "nodenext", and packages/journey-client/package.json has "type": "module" — the explicit './terms-and-conditions-callback.js' import in the test is supported; no changes required.

packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts (3)

1-6: License header present and correct.


45-68: Overall: solid happy-path and error coverage.
Test cases clearly exercise getters and setters for the wrapper. With the small robustness tweaks above, this will be rock‑solid.

Also applies to: 70-85


8-11: ESM import with .js extension — verified; no change required. packages/journey-client/package.json has "type":"module" and packages/journey-client/tsconfig.lib.json & tsconfig.spec.json set "module":"nodenext" and "moduleResolution":"nodenext", so using './confirmation-callback.js' in the TS test is correct.

Comment on lines +12 to +35
describe('ChoiceCallback', () => {
const payload: Callback = {
type: callbackType.ChoiceCallback,
output: [
{
name: 'prompt',
value: 'Select an option',
},
{
name: 'choices',
value: ['one', 'two', 'three'],
},
{
name: 'defaultChoice',
value: 1,
},
],
input: [
{
name: 'IDToken1',
value: 0,
},
],
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid shared mutable test fixture; create a fresh payload per test.

The single payload object is mutated by setters (e.g., setChoiceIndex, setChoiceValue). Reusing it across tests can introduce order‑dependent flakiness. Provide each test with a fresh payload instance.

Apply this refactor:

@@
 describe('ChoiceCallback', () => {
-  const payload: Callback = {
-    type: callbackType.ChoiceCallback,
-    output: [
-      {
-        name: 'prompt',
-        value: 'Select an option',
-      },
-      {
-        name: 'choices',
-        value: ['one', 'two', 'three'],
-      },
-      {
-        name: 'defaultChoice',
-        value: 1,
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken1',
-        value: 0,
-      },
-    ],
-  };
+  const makePayload = (): Callback => ({
+    type: callbackType.ChoiceCallback,
+    output: [
+      { name: 'prompt', value: 'Select an option' },
+      { name: 'choices', value: ['one', 'two', 'three'] },
+      { name: 'defaultChoice', value: 1 },
+    ],
+    input: [{ name: 'IDToken1', value: 0 }],
+  });
@@
   it('should allow setting the choice by index', () => {
-    const cb = new ChoiceCallback(payload);
+    const cb = new ChoiceCallback(makePayload());
     cb.setChoiceIndex(2);
     expect(cb.getInputValue()).toBe(2);
   });
@@
   it('should allow setting the choice by value', () => {
-    const cb = new ChoiceCallback(payload);
+    const cb = new ChoiceCallback(makePayload());
     cb.setChoiceValue('two');
     expect(cb.getInputValue()).toBe(1);
   });

Also applies to: 52-56, 64-68

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/choice-callback.test.ts around
lines 12 to 35 (and similarly at 52-56 and 64-68), the test suite uses a single
shared mutable `payload` object that gets mutated by setters, causing
order-dependent flakiness; change tests to create a fresh payload instance per
test (e.g., build the same object inside each test or add a helper function that
returns a new payload) and replace references to the shared `payload` with calls
to that helper so each test operates on an independent payload.

Comment on lines +13 to +31
const payload: Callback = {
type: callbackType.SelectIdPCallback,
output: [
{
name: 'providers',
value: [
{ provider: 'google', uiConfig: {} },
{ provider: 'facebook', uiConfig: {} },
],
},
],
input: [
{
name: 'IDToken1',
value: '',
},
],
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid shared mutable fixture; create a fresh payload per test

The module-scoped payload can be mutated by setProvider, causing inter-test coupling/flakiness under concurrent execution. Prefer a factory to return a new payload for each test and use it at call sites.

Apply this diff:

-  const payload: Callback = {
-    type: callbackType.SelectIdPCallback,
-    output: [
-      {
-        name: 'providers',
-        value: [
-          { provider: 'google', uiConfig: {} },
-          { provider: 'facebook', uiConfig: {} },
-        ],
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken1',
-        value: '',
-      },
-    ],
-  };
+  const newPayload = (): Callback => ({
+    type: callbackType.SelectIdPCallback,
+    output: [
+      {
+        name: 'providers',
+        value: [
+          { provider: 'google', uiConfig: {} },
+          { provider: 'facebook', uiConfig: {} },
+        ],
+      },
+    ],
+    input: [
+      {
+        name: 'IDToken1',
+        value: '',
+      },
+    ],
+  });
@@
-    const cb = new SelectIdPCallback(payload);
+    const cb = new SelectIdPCallback(newPayload());
@@
-    const cb = new SelectIdPCallback(payload);
+    const cb = new SelectIdPCallback(newPayload());
@@
-    const cb = new SelectIdPCallback(payload);
+    const cb = new SelectIdPCallback(newPayload());

Also applies to: 33-36, 39-42, 45-47

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/select-idp-callback.test.ts around
lines 13-31 (also affecting 33-36, 39-42, 45-47), the test uses a module-scoped
mutable `payload` that `setProvider` mutates and can cause inter-test flakiness;
replace it with a factory function (e.g., makeSelectIdpPayload()) that returns a
fresh deep-cloned payload object each time a test needs one, and update all test
call sites to call the factory instead of reusing the shared `payload` so each
test operates on an independent object.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

♻️ Duplicate comments (2)
packages/journey-client/src/lib/journey.api.ts (1)

1-3: Resolved: StepOptions import path.

Importing StepOptions from @forgerock/sdk-types fixes the prior compile error.

packages/journey-client/src/lib/journey-client.ts (1)

41-45: Propagate network errors via unwrap().

Awaiting dispatch(...initiate()) without .unwrap() can hide errors. Use unwrap() to throw on failure.

Apply:

-    start: async (options?: StepOptions) => {
-      const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
-      return data ? new FRStep(data) : undefined;
-    },
+    start: async (options?: StepOptions) => {
+      const action = store.dispatch(journeyApi.endpoints.start.initiate(options));
+      const data = await action.unwrap();
+      return new FRStep(data);
+    },
🧹 Nitpick comments (48)
.changeset/orange-peaches-warn.md (2)

5-11: Changelog clarity: separate feat vs fix bullets

Content reads well. For cleaner generated changelogs, explicitly tag the bug fix bullet as “fix:” alongside the “feat:” headline, or split into two changesets (one feat, one fix) if you want distinct entries.

-feat(journey-client): Add WebAuthn, QR Code, and Recovery Code support
+feat(journey-client): Add WebAuthn, QR code, and recovery code support

- Adds comprehensive parsing and handling for WebAuthn registration and authentication steps, including a fix for a type error where `TextOutputCallback` was being incorrectly inferred as `TextInputCallback`.
+ - Adds comprehensive parsing/handling for WebAuthn registration and authentication steps.
+fix: Correct type inference where `TextOutputCallback` was incorrectly inferred as `TextInputCallback`.

- Implements support for displaying QR codes (for both OTP and Push) and for displaying and using recovery codes.
+ - Implements support for displaying QR codes (OTP and Push) and for displaying/using recovery codes.

7-9: Terminology consistency

Use consistent casing (“QR code”, “recovery codes”) and specify product terms if needed (e.g., “OATH OTP”, “ForgeRock Push”) for unambiguous release notes.

packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts (1)

38-42: Rename test description to match API (getDeviceName).

Avoid confusion between “display” and “device” name.

-  it('should return a display name from the getDisplayName method', () => {
+  it('should return a device name from the getDeviceName method', () => {
packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts (1)

19-34: Add edge-case test for multiple tags and missing tag.

Helps guard parser against greedy matches and fallback behavior.

+  it('should pick the first device name when multiple <em> tags exist', () => {
+    const text = '<em>Primary</em> ... <em>Secondary</em>';
+    expect(parseDeviceNameText(text)).toBe('Primary');
+  });
+
+  it('should return the default when no device name is present', () => {
+    const text = 'no emphasized name here';
+    expect(parseDeviceNameText(text)).toBe('New Security Key');
+  });
packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts (2)

11-29: Make recovery-code parsing stricter and simpler.

Current regex is brittle (\s[\w\W]"([\w]*)\\) and may yield empty strings. Match the quoted code followed by an escaped newline directly.

-function parseDisplayRecoveryCodesText(text: string): string[] {
-  /**
-   * e.g. ` ...
-   *    "<div class=\"text-center\">\n" +
-   *    "iZmEtxvQ00\n" +
-   *    "</div>\n" +
-   * ... `
-   */
-
-  const recoveryCodesMatches = text.match(/\s[\w\W]"([\w]*)\\/g);
-  const recoveryCodes =
-    Array.isArray(recoveryCodesMatches) &&
-    recoveryCodesMatches.map((substr: string) => {
-      // e.g. `"iZmEtxvQ00\`
-      const arr = substr.match(/"([\w]*)\\/);
-      return Array.isArray(arr) ? arr[1] : '';
-    });
-  return recoveryCodes || [];
-}
+function parseDisplayRecoveryCodesText(text: string): string[] {
+  if (!text) return [];
+  // Match: "CODE\\n"
+  const codes = Array.from(text.matchAll(/"([A-Za-z0-9]{8,})\\n"/g), (m) => m[1]);
+  return codes;
+}

36-50: Fix greedy matching and handle attributes robustly.

Greedy .* can over-capture across tags; literal replacements don’t remove <em ...> with attributes.

-function parseDeviceNameText(text: string): string {
-  /**
-   * We default the device name to 'New Security Key'
-   * If the user has a device name, it will be wrapped in <em> tags
-   * e.g. ` ... <em>My Security Key</em> ... `
-   * We want to remove the <em> tags and just return the device name
-   * e.g. ` ... My Security Key ... `
-   */
-  const displayName =
-    text
-      ?.match(/<em\s*.*>\s*.*<\/em>/g)?.[0]
-      ?.replace('<em>', '')
-      ?.replace('</em>', '') ?? 'New Security Key';
-  return displayName;
-}
+function parseDeviceNameText(text: string): string {
+  // Capture inner text of the first <em ...>...</em>
+  const m = text?.match(/<em\b[^>]*>\s*([^<]+?)\s*<\/em>/i);
+  return m?.[1] ?? 'New Security Key';
+}
packages/journey-client/src/lib/fr-recovery-codes/index.ts (3)

30-30: Drop abstract (class only exposes static methods).

Not harmful, but unnecessary.

-abstract class FRRecoveryCodes {
+class FRRecoveryCodes {

52-54: Consider case-insensitive and script-type checks in isDisplayStep.

Aligns with getDisplayCallback improvements; reduces false positives and i18n brittleness.

-  public static isDisplayStep(step: FRStep): boolean {
-    return !!this.getDisplayCallback(step);
-  }
+  public static isDisplayStep(step: FRStep): boolean {
+    return !!this.getDisplayCallback(step);
+  }

(Keep signature; logic will follow the next change.)


62-69: Harden callback detection: check messageType and use case-insensitive match.

Avoids misclassification and supports varied casing/locales.

-  private static getDisplayCallback(step: FRStep): TextOutputCallback | undefined {
-    return step
-      .getCallbacksOfType<TextOutputCallback>(callbackType.TextOutputCallback)
-      .find((x) => {
-        const cb = x.getOutputByName<string | undefined>('message', undefined);
-        return cb && (cb.includes('Recovery Codes') || cb.includes('recovery codes'));
-      });
-  }
+  private static getDisplayCallback(step: FRStep): TextOutputCallback | undefined {
+    return step
+      .getCallbacksOfType<TextOutputCallback>(callbackType.TextOutputCallback)
+      .find((cb) => {
+        const msg = cb.getOutputByName<string | undefined>('message', undefined);
+        const type = cb.getOutputByName<string | undefined>('messageType', undefined);
+        if (type !== '4' || !msg) return false; // 4 = script
+        return /recovery codes/i.test(msg);
+      });
+  }
package.json (1)

21-21: Fix git clean exclude patterns (remove "!" and split patterns).

git clean -e adds exclude patterns; using ! negates and the comma combines two patterns into one, so neither exclusion works as intended. Exclude each pattern with its own -e and drop !.

Apply this diff:

-    "clean": "shx rm -rf ./{coverage,dist,docs,node_modules,tmp}/ ./{packages,e2e}/*/{dist,node_modules}/ && git clean -fX -e \"!.env*,nx-cloud.env\" -e \"!**/GEMINI.md\"",
+    "clean": "shx rm -rf ./{coverage,dist,docs,node_modules,tmp}/ ./{packages,e2e}/*/{dist,node_modules}/ && git clean -fX -e \".env*\" -e \"nx-cloud.env\" -e \"GEMINI.md\" -e \"**/GEMINI.md\"",
packages/journey-client/src/lib/journey.api.ts (4)

40-49: Consider always sending a JSON body ({} for start) for AM compatibility.

Some AM deployments expect a JSON body on authenticate POST. Safe to send {} for start and the provided step for next.

Apply this diff:

-function configureRequest(step?: Step): RequestInit {
+function configureRequest(step?: Step): RequestInit {
   const init: RequestInit = {
-    body: step ? JSON.stringify(step) : undefined,
+    body: JSON.stringify(step ?? {}),
     credentials: 'include',
     headers: new Headers({}),
     method: 'POST',
   };
   return init;
 }
@@
-        const request = configureRequest();
+        const request = configureRequest({});
@@
-        const request = configureRequest(step);
+        const request = configureRequest(step);

Also applies to: 86-88, 115-117


51-55: Extras.requestMiddleware should be optional to match store wiring.

createJourneyStore passes requestMiddleware?; mark it optional in Extras to avoid misleading types.

Apply this diff:

-interface Extras {
-  requestMiddleware: RequestMiddleware[];
+interface Extras {
+  requestMiddleware?: RequestMiddleware[];
   logger: ReturnType<typeof loggerFn>;
   config: JourneyClientConfig;
 }

No code changes needed elsewhere—applyMiddleware handles undefined.

Also applies to: 89-93, 118-122


1-7: Minor import tidy: consolidate sdk-types imports.

Purely cosmetic; reduces duplicate import lines.

Apply this diff:

-import type { StepOptions } from '@forgerock/sdk-types';
-import type { ServerConfig } from '@forgerock/sdk-types';
+import type { Step, StepOptions, ServerConfig } from '@forgerock/sdk-types';
@@
-import { type Step } from '@forgerock/sdk-types';

21-21: Remove editorial comment.

The “Move these functions…” comment is stale noise.

Apply this diff:

-// Move these functions to the top, before journeyApi definition
packages/journey-client/src/lib/journey.store.ts (1)

46-47: Optional types for consumers: export AppStore/AppDispatch.

Helps downstream typing without inferring from configureStore.

Apply this diff:

 export type RootState = ReturnType<typeof rootReducer>;
+export type AppStore = ReturnType<typeof createJourneyStore>;
+export type AppDispatch = AppStore['dispatch'];
packages/journey-client/src/lib/journey.slice.ts (3)

15-16: Make error state serializable (avoid storing native Error).

Redux serializableCheck is enabled; storing Error objects can trigger warnings. Store a plain, serializable shape instead.

Apply:

-  error?: Error;
+  error?: { message: string; code?: string; detail?: unknown };

8-8: Nit: Drop unused generic import.

Slice type import is unnecessary; createSlice infers types.

Apply:

-import { createSlice, PayloadAction, Slice } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';

21-29: Nit: Let createSlice infer the Slice type.

The explicit Slice<JourneyState> is redundant.

Apply:

-export const journeySlice: Slice<JourneyState> = createSlice({
+export const journeySlice = createSlice({
scratchpad/src/main.ts (1)

16-17: Guard against undefined start() result.

Be defensive in sample code.

Apply:

-    let step = await client.start();
+    let step = await client.start();
+    if (!step) throw new Error('No step returned from start()');
packages/journey-client/README.md (1)

31-33: Use public imports (avoid src paths).

Import callback types from the package root (after ensuring they’re exported).

Apply:

-import type { NameCallback, PasswordCallback } from '@forgerock/journey-client/src/lib/callbacks';
+import type { NameCallback, PasswordCallback } from '@forgerock/journey-client';
packages/journey-client/src/lib/journey-client.ts (3)

86-88: Broaden GenericError guard to include “code” errors.

Storage.set() failures return { code, message, type }, not { error, message }. The current guard misses those.

Apply:

-      function isGenericError(obj: unknown): obj is GenericError {
-        return typeof obj === 'object' && obj !== null && 'error' in obj && 'message' in obj;
-      }
+      function isGenericError(obj: unknown): obj is GenericError {
+        return (
+          typeof obj === 'object' &&
+          obj !== null &&
+          ('error' in (obj as any) || 'code' in (obj as any)) &&
+          'message' in (obj as any)
+        );
+      }

81-83: Nit: return a boolean.

Make intent explicit.

Apply:

-      function requiresPreviousStep() {
-        return (code && state) || form_post_entry || responsekey;
-      }
+      function requiresPreviousStep(): boolean {
+        return Boolean((code && state) || form_post_entry || responsekey);
+      }

19-35: Optional: export a typed client interface.

Helps consumers with intellisense and future changes.

Example:

export interface JourneyClient {
  start(options?: StepOptions): Promise<FRStep>;
  next(step: Step, options?: StepOptions): Promise<FRStep>;
  next(args: { step: Step; options?: StepOptions }): Promise<FRStep>;
  redirect(step: FRStep): Promise<void>;
  resume(url: string, options?: StepOptions): Promise<FRStep>;
}

Then type self as JourneyClient.

packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts (2)

191-195: Missing comma in authenticatorSelection makes the snippet invalid JS

Between userVerification and authenticatorAttachment there’s no comma. This can break parsers that aren’t regex‑based and increases fixture fragility.

-    authenticatorSelection: {
-      userVerification: "preferred"
-      authenticatorAttachment:"cross-platform"
-    }
+    authenticatorSelection: {
+      userVerification: "preferred",
+      authenticatorAttachment: "cross-platform"
+    }

456-458: Broken string literal due to unescaped quotes in HTML snippet

The innerHTML assignment uses double quotes inside a double‑quoted JS string, producing invalid JS in the fixture.

-                    prev.getElementsByTagName("div")[0].innerHTML = "<i class="fa fa-times-circle text-primary"> "
-                        + err + "</i>";
+                    prev.getElementsByTagName("div")[0].innerHTML = "<i class='fa fa-times-circle text-primary'> "
+                        + err + "</i>";
packages/journey-client/src/lib/fr-webauthn/interfaces.ts (1)

64-77: Use AttestationType enum instead of string union

You already define AttestationType; use it to tighten types and avoid drift.

-interface WebAuthnRegistrationMetadata {
-  attestationPreference: 'none' | 'indirect' | 'direct';
+interface WebAuthnRegistrationMetadata {
+  attestationPreference: AttestationType;
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (2)

61-66: Remove stray console.log from test

Leftover debug noise; please drop to keep test output clean.

-    console.log('the step type', stepType, WebAuthnStepType.Authentication);

40-51: Consider deleting commented‑out tests or adding an issue to restore them

Commented blocks for 6.5.3 metadata callbacks can confuse future readers.

packages/journey-client/src/lib/fr-webauthn/helpers.test.ts (2)

21-33: Prefer instanceof ArrayBuffer over toString() for type assertion

More direct and robust.

-    expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[0].id instanceof ArrayBuffer).toBe(true);

Also explicitly assert the array length.

+    expect(credentials).toHaveLength(1);

35-47: Apply same assertions for multi‑credential cases

Use instanceof and assert count.

-    expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials).toHaveLength(2);
+    expect(credentials[0].id instanceof ArrayBuffer).toBe(true);
     expect(credentials[0].type).toBe('public-key');
-    expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[1].id instanceof ArrayBuffer).toBe(true);
     expect(credentials[1].type).toBe('public-key');
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts (5)

4-5: Header filename is incorrect

The banner says index.ts; should reflect fr-qrcode.ts.

- * index.ts
+ * fr-qrcode.ts

16-20: Narrow use type to a union

Constrain to 'otp' | 'push' for stronger typing.

-export type QRCodeData = {
+export type QRCodeData = {
   message: string;
-  use: string;
+  use: 'otp' | 'push';
   uri: string;
 };

67-70: Handle unknown/missing URI schemes explicitly

Current fallback classifies any non‑otpauth as push. Check for pushauth explicitly and consider throwing if neither is found.

-    const qrCodeUse =
-      typeof outputValue === 'string' && outputValue.includes('otpauth://') ? 'otp' : 'push';
+    const isString = typeof outputValue === 'string';
+    const qrCodeUse: 'otp' | 'push' =
+      isString && outputValue.includes('otpauth://')
+        ? 'otp'
+        : isString && outputValue.includes('pushauth://')
+        ? 'push'
+        : (() => {
+            throw new Error('Unknown QR Code URI scheme; expected otpauth:// or pushauth://');
+          })();

71-75: Normalize messageType comparison

getMessageType may be numeric or string in different fixtures. Coerce to string before comparing.

-    const displayMessageCb = messageCbs.find((cb) => {
-      const textOutputCallback = cb as TextOutputCallback;
-      return textOutputCallback.getMessageType() !== '4';
-    }) as TextOutputCallback | null;
+    const displayMessageCb = messageCbs.find((cb) => {
+      const textOutputCallback = cb as TextOutputCallback;
+      return String(textOutputCallback.getMessageType()) !== '4';
+    }) as TextOutputCallback | null;

83-93: Minor: simplify includes checks

Optional chaining isn’t needed after typeof guard.

-      if (typeof outputValue === 'string') {
-        return outputValue?.includes('otpauth://') || outputValue?.includes('pushauth://');
-      }
+      if (typeof outputValue === 'string') {
+        return outputValue.includes('otpauth://') || outputValue.includes('pushauth://');
+      }
packages/journey-client/src/lib/fr-webauthn/enums.ts (1)

11-36: Enums look good; consider const-object + string unions to avoid emitted JS.

Using TS enums emits runtime objects. A const object + type union keeps the surface the same while trimming bundle size.

packages/journey-client/src/lib/fr-webauthn/script-parser.ts (3)

147-166: Reuse helpers to parse allowCredentials instead of manual string splitting.

This reduces regex fragility and keeps behavior consistent with helpers.ts.

Apply this diff:

-  if (allowCredentialsText) {
-    // Splitting objects in array in case the user has multiple keys
-    const allowCredentialArr = allowCredentialsText.split('},') || [allowCredentialsText];
-    // Iterating over array of substrings
-    allowCredentials = allowCredentialArr.map((str) => {
-      // e.g. `{ \"type\": \"public-key\",
-      const type = getIndexOne(str.match(/type"{0,}:\s{0,}"([\w-]+)"/)) as 'public-key';
-      // e.g. \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer
-      const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/));
-      // e.g. `[-107, 93, 68, -67, ... -19, 7, 4]`
-      const idJSON = JSON.parse(idArr[2]);
-      // e.g. [-107, 93, 68, -67, ... -19, 7, 4]
-      const id = new Int8Array(idJSON).buffer;
-
-      return {
-        type,
-        id,
-      };
-    });
-  }
+  if (allowCredentialsText) {
+    allowCredentials = parseAllowCredentialsArray(allowCredentialsText);
+  }

And add the import:

-import { ensureArray, getIndexOne, parsePubKeyArray, parseCredentials } from './helpers.js';
+import { ensureArray, getIndexOne, parsePubKeyArray, parseCredentials, parseAllowCredentialsArray } from './helpers.js';

Also applies to: 12-15


178-181: Harden rpId extraction.

The current regex can overmatch when escaped quotes are present. Use a tighter pattern or delegate to a helper.

Apply this diff:

-  const rpId = getIndexOne(text.match(/rpId"{0,}:\s{0,}\\{0,}"([^"\\]*)/));
+  const rpId = getIndexOne(text.match(/rpId"{0,}:\s{0,}"([^"]*)"/));

85-93: Add defensive checks for challenge parsing.

When the regex doesn’t match, challengeArr[2] is undefined and JSON.parse will throw a generic error. Convert to a DataError for consistency.

Example:

-  const challengeJSON = JSON.parse(challengeArr[2]);
+  if (!challengeArr[2]) {
+    const e = new Error('Missing or invalid challenge');
+    e.name = WebAuthnOutcomeType.DataError;
+    throw e;
+  }
+  const challengeJSON = JSON.parse(challengeArr[2]);

Also applies to: 171-179

packages/journey-client/src/lib/fr-webauthn/helpers.ts (3)

59-65: Support Uint8Array and make the match non-greedy.

This makes parseNumberArray resilient to either Int8Array or Uint8Array and avoids overmatching.

Apply this diff:

-function parseNumberArray(value: string): number[] {
-  const matches = /new Int8Array\((.+)\)/.exec(value);
+function parseNumberArray(value: string): number[] {
+  const matches = /new (?:Uint|Int)8Array\((.+?)\)/.exec(value);
   if (matches === null || matches.length < 2) {
     return [];
   }
   return JSON.parse(matches[1]);
 }

77-79: Quote only unquoted object keys; avoid touching keys inside strings.

Current /(\w+):/g can misbehave in edge cases. Restrict to keys at object starts or after commas.

Apply this diff to both places:

-  value = value.replace(/(\w+):/g, '"$1":');
+  value = value.replace(/([{,]\s*)([A-Za-z_]\w*)\s*:/g, '$1"$2":');

Also applies to: 96-98


107-113: Tighten relying party ID parsing (avoid greedy .+).

Prevents capturing across quotes.

Apply this diff:

-  if (relyingPartyId.includes('rpId')) {
-    return relyingPartyId.replace(/rpId: "(.+)",/, '$1');
-  } else {
-    return relyingPartyId.replace(/id: "(.+)",/, '$1');
-  }
+  if (relyingPartyId.includes('rpId')) {
+    return relyingPartyId.replace(/rpId:\s*"([^"]*)",?/, '$1');
+  } else {
+    return relyingPartyId.replace(/id:\s*"([^"]*)",?/, '$1');
+  }
packages/journey-client/src/lib/fr-webauthn/index.ts (6)

287-294: Fix JSDoc: this returns a TextOutputCallback, not metadata.

Minor doc accuracy.

-   * Returns the WebAuthn metadata callback containing data to pass to the browser
-   * Web Authentication API.
+   * Returns the TextOutputCallback carrying WebAuthn-encoded data for the browser
+   * Web Authentication API.

446-456: Add base64url helper (shared by auth/registration).

Introduce a small utility to normalize base64url and support SSR (falls back to Buffer if atob is absent).

Add inside the class (near the bottom, before the closing brace):

// Helper: base64url -> ArrayBuffer (SSR/browser-safe)
private static fromBase64Url(input: string): ArrayBuffer {
  const b64 = input.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(input.length / 4) * 4, '=');
  if (typeof atob === 'function') {
    const bin = atob(b64);
    const bytes = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
    return bytes.buffer;
  }
  // Node/SSR fallback
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const buf = (globalThis as any).Buffer?.from(b64, 'base64');
  if (buf) return new Uint8Array(buf).buffer;
  const e = new Error('Base64 decoder not available');
  e.name = WebAuthnOutcomeType.EncodingError;
  throw e;
}

Also applies to: 492-506


266-272: Metadata callback detection is brittle.

Relying only on hasOwnProperty('relyingPartyId') may misclassify future payloads. Consider also checking for challenge.

-      return cb && cb.hasOwnProperty('relyingPartyId');
+      return !!cb && ('relyingPartyId' in cb || 'challenge' in (cb as Record<string, unknown>));

340-351: userHandle may be null; ensure safe handling.

If arrayBufferToString throws on null/undefined, guard first.

-      // @ts-ignore
-      const userHandle = arrayBufferToString(credential.response.userHandle);
+      // @ts-ignore - lib.dom types vary by TS version
+      const rawHandle = credential.response.userHandle;
+      const userHandle = rawHandle ? arrayBufferToString(rawHandle) : '';

242-257: getCallbacks returns only one of metadata/textOutput even if both exist.

If both are present (misconfigured step), preferring metadata is fine, but consider warning/logging for diagnosability.

Would you like a debug log hook here?


169-176: Generic register<T>: deviceName empty check can be simplified.

Minor readability.

-    deviceName?: T,
+    deviceName?: T,

And later:

-      hiddenCallback.setInputValue(
-        deviceName && deviceName.length > 0 ? `${outcome}::${deviceName}` : outcome,
-      );
+      hiddenCallback.setInputValue(deviceName ? `${outcome}::${deviceName}` : outcome);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cabd78d and efea18e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (31)
  • .changeset/orange-peaches-warn.md (1 hunks)
  • .gitignore (1 hunks)
  • package.json (1 hunks)
  • packages/journey-client/README.md (1 hunks)
  • packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/index.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/enums.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/helpers.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/helpers.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/index.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/script-parser.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.ts (1 hunks)
  • packages/journey-client/src/lib/journey.api.ts (1 hunks)
  • packages/journey-client/src/lib/journey.slice.ts (1 hunks)
  • packages/journey-client/src/lib/journey.store.ts (1 hunks)
  • scratchpad/package.json (1 hunks)
  • scratchpad/src/main.ts (1 hunks)
  • scratchpad/tsconfig.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/journey-client/src/lib/journey-client.test.ts
🧰 Additional context used
🧬 Code graph analysis (21)
packages/journey-client/src/lib/fr-webauthn/enums.ts (1)
packages/journey-client/src/lib/fr-webauthn/index.ts (2)
  • WebAuthnOutcome (517-517)
  • WebAuthnStepType (517-517)
packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts (2)
packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts (4)
  • displayRecoveryCodesResponse (114-114)
  • otherResponse (117-117)
  • expectedRecoveryCodes (116-116)
  • expectedDeviceName (115-115)
packages/journey-client/src/lib/fr-recovery-codes/index.ts (1)
  • isDisplayStep (52-54)
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (2)
packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (2)
  • otpQRCodeStep (13-101)
  • pushQRCodeStep (103-184)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1)
  • webAuthnRegJSCallback70 (89-125)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (3)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (9)
  • webAuthnRegJSCallback653 (13-49)
  • webAuthnAuthJSCallback653 (51-87)
  • webAuthnRegJSCallback70 (89-125)
  • webAuthnAuthJSCallback70 (127-163)
  • webAuthnRegMetaCallback70 (203-238)
  • webAuthnAuthMetaCallback70 (240-270)
  • webAuthnRegJSCallback70StoredUsername (165-201)
  • webAuthnRegMetaCallback70StoredUsername (272-307)
  • webAuthnAuthMetaCallback70StoredUsername (309-337)
packages/journey-client/src/lib/fr-webauthn/index.ts (1)
  • WebAuthnStepType (517-517)
packages/journey-client/src/lib/fr-webauthn/enums.ts (1)
  • WebAuthnStepType (36-36)
packages/journey-client/src/lib/fr-webauthn/helpers.test.ts (2)
packages/journey-client/src/lib/fr-webauthn/helpers.ts (2)
  • parseCredentials (119-119)
  • parsePubKeyArray (122-122)
packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts (5)
  • allowCredentials70 (12-13)
  • allowMultipleCredentials70 (15-16)
  • acceptableCredentials653 (18-19)
  • acceptableMultipleCredentials653 (21-22)
  • pubKeyCredParamsStr (24-25)
packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts (2)
packages/journey-client/src/lib/fr-webauthn/script-parser.ts (2)
  • parseWebAuthnAuthenticateText (192-192)
  • parseWebAuthnRegisterText (192-192)
packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts (10)
  • authenticateInputWithRpidAndAllowCredentials (12-44)
  • authenticateInputWithRpidAllowCredentialsAndQuotes (46-78)
  • authenticateInputWithAcceptableCredentialsWithoutRpid (113-149)
  • authenticateInputWithoutRpidAndAllowCredentials (80-110)
  • registerInputWithRpid (151-207)
  • registerOutputWithRpid (254-276)
  • registerInputWithRpidAndQuotes (209-252)
  • registerInputWithoutRpid (278-320)
  • registerOutputWithoutRpid (322-341)
  • registerInputWithExcludeCreds (343-385)
packages/journey-client/src/lib/fr-recovery-codes/script-parser.test.ts (2)
packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts (2)
  • parseDisplayRecoveryCodesText (51-51)
  • parseDeviceNameText (51-51)
packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts (4)
  • displayRecoveryCodes (113-113)
  • expectedRecoveryCodes (116-116)
  • securityKeyResponse (119-119)
  • securityKeyCustomNameResponse (118-118)
packages/journey-client/src/lib/fr-webauthn/helpers.ts (2)
packages/journey-client/src/lib/fr-webauthn/interfaces.ts (1)
  • ParsedCredential (116-116)
packages/journey-client/src/lib/fr-webauthn/enums.ts (1)
  • WebAuthnOutcomeType (36-36)
packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
scratchpad/src/main.ts (2)
packages/journey-client/src/lib/journey-client.ts (1)
  • journey (19-140)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/fr-webauthn/interfaces.ts (1)
packages/journey-client/src/lib/fr-webauthn/index.ts (4)
  • RelyingParty (512-512)
  • WebAuthnRegistrationMetadata (515-515)
  • WebAuthnAuthenticationMetadata (513-513)
  • WebAuthnCallbacks (514-514)
packages/journey-client/src/lib/journey.store.ts (3)
packages/journey-client/src/lib/journey.api.ts (1)
  • journeyApi (57-131)
packages/journey-client/src/lib/journey.slice.ts (2)
  • journeySlice (21-29)
  • journeySlice (31-31)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/journey-client/src/lib/journey-client.ts (5)
packages/journey-client/src/lib/config.types.ts (2)
  • JourneyClientConfig (10-13)
  • StepOptions (15-15)
packages/journey-client/src/lib/journey.store.ts (1)
  • createJourneyStore (21-44)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • createStorage (34-139)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Step (70-85)
  • callbackType (10-35)
packages/journey-client/src/lib/journey.api.ts (1)
  • journeyApi (57-131)
packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey.slice.ts (2)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Step (70-85)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/journey-client/src/lib/fr-recovery-codes/index.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey.api.ts (6)
packages/sdk-types/src/lib/legacy-config.types.ts (2)
  • ServerConfig (19-23)
  • StepOptions (54-56)
packages/sdk-utilities/src/lib/url/am-url.utils.ts (1)
  • getEndpointPath (39-59)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Step (70-85)
packages/journey-client/src/lib/config.types.ts (2)
  • JourneyClientConfig (10-13)
  • StepOptions (15-15)
packages/sdk-utilities/src/lib/constants/index.ts (1)
  • REQUESTED_WITH (1-1)
packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.effects.ts (1)
  • initQuery (61-88)
packages/journey-client/src/lib/fr-webauthn/index.ts (5)
packages/journey-client/src/lib/fr-webauthn/interfaces.ts (6)
  • AttestationType (125-125)
  • WebAuthnAuthenticationMetadata (121-121)
  • WebAuthnRegistrationMetadata (122-122)
  • WebAuthnTextOutputRegistration (123-123)
  • WebAuthnCallbacks (120-120)
  • RelyingParty (117-117)
packages/journey-client/src/lib/fr-webauthn/enums.ts (3)
  • WebAuthnStepType (36-36)
  • WebAuthnOutcomeType (36-36)
  • WebAuthnOutcome (36-36)
packages/journey-client/src/lib/fr-webauthn/script-parser.ts (2)
  • parseWebAuthnAuthenticateText (192-192)
  • parseWebAuthnRegisterText (192-192)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/fr-webauthn/helpers.ts (4)
  • arrayBufferToString (117-117)
  • parseRelyingPartyId (123-123)
  • parseCredentials (119-119)
  • parsePubKeyArray (122-122)
packages/journey-client/src/lib/fr-webauthn/script-parser.ts (3)
packages/journey-client/src/lib/fr-webauthn/helpers.ts (4)
  • getIndexOne (118-118)
  • parsePubKeyArray (122-122)
  • parseCredentials (119-119)
  • ensureArray (116-116)
packages/journey-client/src/lib/fr-webauthn/interfaces.ts (2)
  • AttestationType (125-125)
  • UserVerificationType (125-125)
packages/journey-client/src/lib/fr-webauthn/enums.ts (1)
  • WebAuthnOutcomeType (36-36)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
🪛 Gitleaks (8.27.2)
packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts

[high] 14-14: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 104-104: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts

[high] 14-14: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 52-52: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 90-90: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 128-128: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 166-166: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 204-204: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 241-241: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 273-273: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 310-310: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 456-456: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 341-341: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


[high] 387-387: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🪛 Biome (2.1.2)
packages/journey-client/src/lib/fr-webauthn/script-parser.ts

[error] 49-49: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 65-65: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 79-79: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 131-131: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 139-139: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (21)
.changeset/orange-peaches-warn.md (1)

1-3: Release type/package targeting: confirm correctness

Minor bump for '@forgerock/journey-client' looks right for additive features. If this is the package’s first public release, confirm initial versioning strategy (e.g., 0.x vs 1.0) and whether any other touched packages (sdk-types, sdk-utilities) also need their own changesets.

packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts (1)

20-37: LGTM: happy-path coverage is clear and readable.

Positive/negative checks for isDisplayStep and value extraction look good.

packages/journey-client/src/lib/fr-recovery-codes/script-text.mock.data.ts (1)

12-120: LGTM: fixtures are comprehensive and representative.

Good coverage for positive, negative, and custom-name scenarios.

If upstream AM ever localizes “Recovery Codes,” consider adding a localized fixture to ensure detection still holds with the messageType === '4' gate.

scratchpad/package.json (1)

18-18: LGTM: dependency swap aligns scratchpad with the new package.

No issues; resolves to the workspace package as expected.

packages/journey-client/src/lib/journey.api.ts (1)

57-69: Nice: base headers centralized via prepareHeaders.

Good defaults for AM (Accept-API-Version, JSON, requested-with).

packages/journey-client/src/lib/journey.store.ts (2)

16-19: LGTM: reducer composition with RTK Query slice and journey slice.

Clean composition; respects journeyApi.reducerPath.


33-35: No action required — serializability confirmed. The code only stores plain Step payloads (e.g., redirect() persists step.payload in packages/journey-client/src/lib/journey-client.ts and RTK endpoints are typed to Step in packages/journey-client/src/lib/journey.api.ts); FRStep instances are created on demand, so leaving serializableCheck: true is correct.

packages/journey-client/src/lib/journey-client.ts (1)

69-70: SSR‑safe redirect.

Guard window for non‑browser environments.
[suggest_minor_issue]
Apply:

-      window.location.assign(redirectUrl);
+      if (typeof window !== 'undefined' && window.location) {
+        window.location.assign(redirectUrl);
+      } else {
+        log.warn('Redirect skipped: window is not available', { redirectUrl });
+      }
scratchpad/src/main.ts (1)

3-3: Confirm public exports for NameCallback/PasswordCallback

Import at scratchpad/src/main.ts:3 assumes these types are re‑exported from @forgerock/journey-client root; the provided ripgrep run returned no matches. Actions:

  • Preferred: re‑export NameCallback and PasswordCallback from packages/journey-client/src/index.ts so the root import works.
  • Alternative: import them from the package's public types module (where they are actually declared).

Verify with: rg -nC2 -S '\bexport\b.*\b(NameCallback|PasswordCallback)\b' packages/journey-client || rg -nC2 -S '\b(NameCallback|PasswordCallback)\b' packages/journey-client

packages/journey-client/src/lib/fr-webauthn/interfaces.ts (1)

97-112: Good call documenting ArrayBuffer choice for credential IDs

The explicit ArrayBuffer with rationale reduces TS friction across environments.

packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (1)

17-77: Tests read well and validate both OTP and Push paths

Happy path and negative detection covered; expected objects match parser output.

packages/journey-client/src/lib/fr-webauthn/script-parser.test.ts (1)

26-123: Solid coverage of script parsing variants

Covers RP ID/no RP ID, quoted keys, acceptable/allow credentials, and recovery‑code path.

packages/journey-client/src/lib/fr-webauthn/helpers.test.ts (1)

49-53: Nice verification of pubKeyCredParams parsing

Covers both ES256 and RS256 entries.

packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (1)

14-15: Gitleaks false‑positive on mock tokens — expand allowlist and annotate test data

Static JWT-like strings and a mock push secret are test values and trigger scanners; they appear in:

  • packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (lines 14, 104)
  • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (multiple occurrences: 14, 52, 90, 128, 166, 204, 241, 273, 310, 341, 387, 456)

Add an allowlist entry (or broaden the path) and add an inline comment above the tokens marking them as mock/test-only.

Proposed .gitleaks.toml snippet:

[allowlist]
description = "Allow mock test tokens in journey-client mock files"
regexes = [
  '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9''',
  '''FoxEr5uAzrys1yBmuygPbxrVjysElmzsmqifi6eO_AI'''
]
paths = [
  '''packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts''',
  '''packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts'''
]

Quick check command:

rg -n --hidden --no-ignore-vcs -S "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9|FoxEr5uAzrys1yBmuygPbxrVjysElmzsmqifi6eO_AI" packages/journey-client/src/lib -C1

Likely an incorrect or invalid review comment.

packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts (1)

11-25: LGTM for mock fixtures.

Pure data, no runtime side effects.

packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1)

11-49: Approve — confirm JWT-like test tokens are safe

ripgrep found JWT-like strings; confirm these are harmless fixtures or redact/rotate them:

  • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts: lines 341, 387
  • e2e/mock-api-v2/src/responses/token/token.ts: lines 18, 22, 25
packages/journey-client/src/lib/fr-webauthn/index.ts (5)

99-162: Auth JSON branch: preserve legacyData when supportsJsonResponse.

Looks good overall.

Please confirm server-side supports the authenticatorAttachment + legacyData JSON envelope for both paths and version-gate it if necessary.


426-456: Auth options builder looks correct (with base64url fix).

Parsing allowCredentials and optional fields is solid.


236-257: Callback discovery is clean.

Returning a minimal shape with hidden+one driver callback is reasonable.


1-18: Header and exports: good structure.

API surface is clear and consistent.


1-518: No action needed: parser functions and types align with FRWebAuthn usage. parseWebAuthnRegisterText returns a valid PublicKeyCredentialCreationOptions and parseWebAuthnAuthenticateText returns a valid PublicKeyCredentialRequestOptions; AttestationType matches the enum and TextOutputCallback.getMessage() returns a string.


export const webAuthnAuthMetaCallbackJsonResponse = {
authId:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoSW5kZXhWYWx1ZSI6IndlYmF1dGhuIiwib3RrIjoicXN1dTA0anNxZ2hmcGpubjFiM2IxdDh0NTQiLCJhdXRoSW5kZXhUeXBlIjoic2VydmljZSIsInJlYWxtIjoiLyIsInNlc3Npb25JZCI6IipBQUpUU1FBQ01ERUFCSFI1Y0dVQUNFcFhWRjlCVlZSSUFBSlRNUUFBKmV5SjBlWEFpT2lKS1YxUWlMQ0pqZEhraU9pSktWMVFpTENKaGJHY2lPaUpJVXpJMU5pSjkuWlhsS01HVllRV2xQYVVwTFZqRlJhVXhEU214aWJVMXBUMmxLUWsxVVNUUlJNRXBFVEZWb1ZFMXFWVEpKYVhkcFdWZDRia2xxYjJsYVIyeDVTVzR3TGk0M01qbDZSRXhsU0hwM1pXbFVPRll6VDB4cFgycEJMbGxpV1Y5WVl6WTJPVWxJU0MxT01VTk5Va05MZVROdFZFaHliVlJSVDJoV2VVSTBUVUUzYUVoRmFISjFTRU5SVWpWeWVsQnRZVzFpYlRNeFFqQlhSMlZxVEU5allUWllWMEUwTVROSlozRnVXV2gwV0hadlRHaE9VMkpDYUdkUlJFaGFSV1pLVmxaeWNsOTBTRUpJU1Y5elIyMVhNMEZYWm5wTk9WZElXblJMTm10ZmFITm9hemRNUTBkSVkwbzVhVGQ2Wnpob2FUbFROVm8wTVVkbk1rZFpXSFJJTnpoWlRGOVVaVTl5VWtOc1ozTlBkbDlWTWpGRFJ6STBReTFMWVRJNVMxRm9ibFYwTTNCVlozWldiamRCVUhWdWRFdEdaR2h6VERselJ6QlBTVnAwYzNoV1NqTXRPVmc1TmpSM2VrVldSV2w2Vnprek9XNTRlVm94VjNWVVgzWmpTRzFFV2t4eE0yWTJXamswVlVWTU5VNDJjMjVMTWs5U01XeDBOR3BrVld3eWVVMVRaVXR6YzJkb2VIRmlNMnhoZW1WQlVtMDBWM3BUVld4c1JFUTRVVVJuWDNoSFozSktlVmhQZWxCa1RWUnZYeTB0U1c0MFkxVjBTeloxTVdWSVkxOWhZbkZLTlhsRVpWcEpaM04yYkU5eE5qVTJkVU5KV0dzNU9GbElWMHBEWkhSR1MzcGtWV1k1ZG0xNlJIWk9iMmxXTlZnd2RXd3hiRzlTV0dOaFZtTkhVMDlaTlVGNFdITkJkSGd5UVhkVlVXUnViR0pmYTJodWN5MUhXblowZUVOM1lYRldlR2h1T1RsdVVWY3ljWFphUjNCTk1raFVPRzFMYUU5SVIyOVJOQzFWVkZrNVVWbDBNbGcwZGpaZlQzSk5kemxUZEdwSVl6RnRjMTkxVTI1VWVtVmpUR2RYUkVZdFVFNXNVM0J1Ymkxc1EyRlljRXREVmxsS1FVeDVUbWhoWDBJeGQwNTRSRzV0WW5vNVdYVjFXakZMYzFWTkxVZHJjVlJZYkY5c2JqUlBMVEpXVUhoTVFYSjFVblZOZW0xaVgxQndRMjlqY1d4T1Z6Sm1jWHBPV25seVlteE9RVEZXUkdaM04yYzJNMnhmTkhvd05UWkhlRXhOVjBOck5rOTZVQzFMY1RJMVlXTmxSa0ZQWWpGd1JtMXBkVGgzWW5kUGVITmtZa0ZLVW0xSWMxVlJWVzlQWm5aQlpURldORmcyUW5veGNFeG9SV3d0UzNGblkwMDBjMjluYTFab1YyRkhZWFpyVFUxSVFTNVlWRVpOV1d4R2F6bHFSV1V5VG5CamFIZDVZVzVuLmYyS2t1RlhnM05MUU1NbGNnMU1HU2Y2YTZQVmdJalhtUC1wcmJhQTNtTnciLCJleHAiOjE2MTM0OTc0OTksImlhdCI6MTYxMzQ5NzE5OX0.EuDmsY3C6I6vc_x7KlkW4rSQJY1FWevbGGmxkSu4HVU',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Sanitize JWT-like authId values in mocks to satisfy secret scanners.

Gitleaks flags these as real JWTs. Replace with unmistakably fake placeholders or add an allowlist.

Apply this diff:

-  authId:
-    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoSW5kZXhWYWx1ZSI6IndlYmF1dGhuIiwib3RrIjoicXN1dTA0anNxZ2hmcGpubjFiM2IxdDh0NTQiLCJhdXRoSW5kZXhUeXBlIjoic2VydmljZSIsInJlYWxtIjoiLyIsInNlc3Npb25JZCI6IipB...HVU',
+  authId: 'header.payload.signature.TEST_ONLY',
-  authId:
-    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ3aGl0ZWxpc3Qtc3RhdGUiOiJjMjMzNDRkMC04ZTlhLTRhM2QtODZkMS1mNTIwNTExZmM3NjciLCJhdXRoSW5kZXhWYWx1ZSI6IkFuZHlXZWJBdXRobiIsIm90ayI6ImFzdTNjMmo4YThta2w0aWQyN3FndGFuaTVqIiwiYXV0aEluZGV4VHlwZSI6InNlcnZpY2UiLCJyZWFsbSI6Ii9hbHBoYSIsInNlc3Npb25JZCI6IipB...0Cg',
+  authId: 'header.payload.signature.TEST_ONLY',

If you prefer keeping realistic shapes, add a repo-level gitleaks allowlist for **/*.mock.data.ts.

Also applies to: 387-387

🧰 Tools
🪛 Gitleaks (8.27.2)

[high] 341-341: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts around
lines 341 and 387, the string values are actual-looking JWTs that trigger secret
scanners; replace them with unmistakably fake placeholders (e.g.
"REDACTED_MOCK_JWT" or "<FAKE_JWT_PAYLOAD>") or neutral test tokens, and update
all similar entries in this mock file to the same pattern, or alternatively add
a repo-level gitleaks allowlist that includes **/*.mock.data.ts; make sure no
real secrets remain and run the scanner locally to verify the warning is
resolved.

Comment on lines +86 to +96
if (outcomeCallback && textOutputCallback) {
const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration;

if (message.includes('pubKeyCredParams')) {
return WebAuthnStepType.Registration;
}

return WebAuthnStepType.Authentication;
} else {
return WebAuthnStepType.None;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix union misuse: .includes on possibly non-string message.

message is typed as string | WebAuthnTextOutputRegistration, but .includes assumes string. This will fail type-checking and can throw at runtime if the parser returns an object.

Apply this diff to properly narrow:

-    if (outcomeCallback && textOutputCallback) {
-      const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration;
-
-      if (message.includes('pubKeyCredParams')) {
-        return WebAuthnStepType.Registration;
-      }
-
-      return WebAuthnStepType.Authentication;
-    } else {
+    if (outcomeCallback && textOutputCallback) {
+      const raw = textOutputCallback.getMessage() as unknown;
+      if (typeof raw === 'string') {
+        if (raw.includes('pubKeyCredParams')) {
+          return WebAuthnStepType.Registration;
+        }
+        return WebAuthnStepType.Authentication;
+      }
+      if (raw && typeof raw === 'object' && 'pubKeyCredParams' in (raw as Record<string, unknown>)) {
+        return WebAuthnStepType.Registration;
+      }
+      return WebAuthnStepType.Authentication;
+    } else {
       return WebAuthnStepType.None;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (outcomeCallback && textOutputCallback) {
const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration;
if (message.includes('pubKeyCredParams')) {
return WebAuthnStepType.Registration;
}
return WebAuthnStepType.Authentication;
} else {
return WebAuthnStepType.None;
}
if (outcomeCallback && textOutputCallback) {
const raw = textOutputCallback.getMessage() as unknown;
if (typeof raw === 'string') {
if (raw.includes('pubKeyCredParams')) {
return WebAuthnStepType.Registration;
}
return WebAuthnStepType.Authentication;
}
if (raw && typeof raw === 'object' && 'pubKeyCredParams' in (raw as Record<string, unknown>)) {
return WebAuthnStepType.Registration;
}
return WebAuthnStepType.Authentication;
} else {
return WebAuthnStepType.None;
}
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-webauthn/index.ts around lines 86 to 96,
the code calls message.includes(...) but message is typed string |
WebAuthnTextOutputRegistration; add a proper type guard to avoid calling
.includes on an object: first check typeof message === 'string' and use
.includes only then, otherwise inspect the object shape (e.g. check for
message.pubKeyCredParams or another discriminator) to determine Registration vs
Authentication; update the conditional to narrow the union safely and return
WebAuthnStepType.None when neither applies.

Comment on lines +121 to +128
} else if (textOutputCallback) {
publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());

credential = await this.getAuthenticationCredential(
publicKey as PublicKeyCredentialRequestOptions,
);
outcome = this.getAuthenticationOutcome(credential);
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Text-output auth path sends invalid WebAuthn options (challenge not ArrayBuffer).

parseWebAuthnAuthenticateText(...) returns strings that must be base64url-decoded to ArrayBuffers; passing raw strings to navigator.credentials.get will fail.

Apply this diff and use a base64url→ArrayBuffer helper (see separate comment adding fromBase64Url):

-        } else if (textOutputCallback) {
-          publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());
-
-          credential = await this.getAuthenticationCredential(
-            publicKey as PublicKeyCredentialRequestOptions,
-          );
-          outcome = this.getAuthenticationOutcome(credential);
-        } else {
+        } else if (textOutputCallback) {
+          const parsed: any = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());
+          const normalized: PublicKeyCredentialRequestOptions = {
+            ...parsed,
+            challenge: FRWebAuthn.fromBase64Url(parsed.challenge),
+            ...(Array.isArray(parsed.allowCredentials) && {
+              allowCredentials: parsed.allowCredentials.map((d: any) => ({
+                ...d,
+                id: FRWebAuthn.fromBase64Url(d.id),
+              })),
+            }),
+          };
+          publicKey = normalized;
+          credential = await this.getAuthenticationCredential(publicKey);
+          outcome = this.getAuthenticationOutcome(credential);
+        } else {
           throw new Error('No Credential found from Public Key');
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (textOutputCallback) {
publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());
credential = await this.getAuthenticationCredential(
publicKey as PublicKeyCredentialRequestOptions,
);
outcome = this.getAuthenticationOutcome(credential);
} else {
} else if (textOutputCallback) {
const parsed: any = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());
const normalized: PublicKeyCredentialRequestOptions = {
...parsed,
challenge: FRWebAuthn.fromBase64Url(parsed.challenge),
...(Array.isArray(parsed.allowCredentials) && {
allowCredentials: parsed.allowCredentials.map((d: any) => ({
...d,
id: FRWebAuthn.fromBase64Url(d.id),
})),
}),
};
publicKey = normalized;
credential = await this.getAuthenticationCredential(publicKey);
outcome = this.getAuthenticationOutcome(credential);
} else {
throw new Error('No Credential found from Public Key');
}
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-webauthn/index.ts around lines 121-128,
the text-output authentication path passes raw base64url strings from
parseWebAuthnAuthenticateText into navigator.credentials.get causing failures;
convert publicKey.challenge from base64url string to an ArrayBuffer using the
fromBase64Url helper and also convert each allowCredentials[i].id (if present)
from base64url to ArrayBuffer before calling
this.getAuthenticationCredential/publicKey usage, ensuring the resulting
PublicKeyCredentialRequestOptions contains ArrayBuffer values where required.

Comment on lines 47 to 57
/**
* Submits the current Step payload to the authentication API and retrieves the next FRStep in the journey.
* The `step` to be submitted is provided within the `options` object.
*
* @param options An object containing the current Step payload and optional StepOptions.
* @returns A Promise that resolves to the next FRStep in the journey, or undefined if the journey ends.
*/
next: async (step: Step, options?: StepOptions) => {
const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options }));
return data ? new FRStep(data) : undefined;
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Support both call styles for next() and use unwrap().

Docs/samples use next({ step, options }); code uses (step, options?). Support both to avoid breakage.

Apply:

-    /**
-     * Submits the current Step payload to the authentication API and retrieves the next FRStep in the journey.
-     * The `step` to be submitted is provided within the `options` object.
-     *
-     * @param options An object containing the current Step payload and optional StepOptions.
-     * @returns A Promise that resolves to the next FRStep in the journey, or undefined if the journey ends.
-     */
-    next: async (step: Step, options?: StepOptions) => {
-      const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options }));
-      return data ? new FRStep(data) : undefined;
-    },
+    /**
+     * Submits the current Step payload and retrieves the next FRStep.
+     */
+    next: async (
+      arg1: Step | { step: Step; options?: StepOptions },
+      arg2?: StepOptions,
+    ) => {
+      const isObj = typeof arg1 === 'object' && arg1 !== null && 'step' in (arg1 as any);
+      const payload = (isObj ? (arg1 as { step: Step }).step : (arg1 as Step));
+      const options = (isObj ? (arg1 as { options?: StepOptions }).options : arg2);
+      const action = store.dispatch(journeyApi.endpoints.next.initiate({ step: payload, options }));
+      const data = await action.unwrap();
+      return new FRStep(data);
+    },

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +28 to +31
const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined;
const params: Record<string, string | undefined> = { ...query, ...treeParams };
const queryString = Object.keys(params).length > 0 ? `?${stringify(params)}` : '';
const path = getEndpointPath({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: object spread of undefined throws at runtime.

When tree is falsy, treeParams is undefined and { ...query, ...treeParams } throws. Use an empty object instead and tighten the type.

Apply this diff:

-  const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined;
-  const params: Record<string, string | undefined> = { ...query, ...treeParams };
+  const params: Record<string, string> = {
+    ...query,
+    ...(tree ? { authIndexType: 'service', authIndexValue: tree } : {}),
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined;
const params: Record<string, string | undefined> = { ...query, ...treeParams };
const queryString = Object.keys(params).length > 0 ? `?${stringify(params)}` : '';
const path = getEndpointPath({
const params: Record<string, string> = {
...query,
...(tree ? { authIndexType: 'service', authIndexValue: tree } : {}),
};
const queryString = Object.keys(params).length > 0 ? `?${stringify(params)}` : '';
const path = getEndpointPath({
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey.api.ts around lines 28 to 31, the
current code spreads treeParams which can be undefined causing a runtime throw;
change treeParams to default to an empty object when tree is falsy (e.g. tree ?
{ authIndexType: 'service', authIndexValue: tree } : {}) and keep params typed
as Record<string, string | undefined> so the spread is always safe and types
remain correct. Also ensure stringify call still handles undefined values as
intended.

Comment on lines +23 to +24
step = await client.next({ step: step.payload }); // Submit the step
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix API mismatch: next() expects (step, options?) not an object.

Update calls to pass the Step payload directly.

Apply:

-      step = await client.next({ step: step.payload }); // Submit the step
+      step = await client.next(step.payload); // Submit the step
-      step = await client.next({ step: step.payload }); // Submit the step
+      step = await client.next(step.payload); // Submit the step

Also applies to: 33-34

🤖 Prompt for AI Agents
In scratchpad/src/main.ts around lines 23-24 (and similarly lines 33-34), the
call to client.next is using an object with a step property but the API expects
the Step payload as the first argument (and optional options second); change the
calls to pass step.payload directly as the first parameter (e.g.,
client.next(step.payload)) and remove the surrounding object wrapper so the
function signature matches the API.

Comment on lines +24 to 28
"path": "../packages/sdk-types"
},
{
"path": "../packages/journey-client"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verify referenced projects are composite builds.

Project references require the referenced projects to set "composite": true (the referencing project can remain non‑composite). Please confirm sdk-types and journey-client tsconfigs are composite.

Run:


🏁 Script executed:

#!/bin/bash
# Expect to find "composite": true in referenced library tsconfigs
fd -a 'tsconfig*.json' packages/sdk-types packages/journey-client \
  | while read -r f; do echo "=== $f"; rg -n '"composite"\s*:\s*true' "$f" || echo "MISSING composite:true"; done

Length of output: 162


Add "compilerOptions.composite": true to referenced tsconfigs

Project references require referenced projects to be composite. Set "compilerOptions.composite": true in:

  • packages/journey-client/tsconfig.json
  • packages/sdk-types/tsconfig.json
🤖 Prompt for AI Agents
In scratchpad/tsconfig.json around lines 24-28, the referenced projects require
their tsconfigs to be composite; open packages/journey-client/tsconfig.json and
packages/sdk-types/tsconfig.json and add "compilerOptions": { "composite": true
} (or set composite: true inside existing compilerOptions), saving the files so
both referenced projects are marked as composite for TypeScript project
references to work correctly.

@ryanbas21 ryanbas21 force-pushed the create-journey-package branch from efea18e to b7acf08 Compare September 18, 2025 15:33
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (16)
packages/journey-client/src/lib/callbacks/kba-create-callback.ts (1)

49-59: Don’t mutate payload; delegate to setInputValue with a suffix selector

Use FRCallback’s helper to preserve invariants and error semantics.

-  private setValue(type: 'question' | 'answer', value: string): void {
-    if (!this.payload.input) {
-      throw new Error('KBA payload is missing input');
-    }
-
-    const input = this.payload.input.find((x) => x.name.endsWith(type));
-    if (!input) {
-      throw new Error(`No input has name ending in "${type}"`);
-    }
-    input.value = value;
-  }
+  private setValue(type: 'question' | 'answer', value: string): void {
+    // Match inputs whose name ends with "question" or "answer"
+    this.setInputValue(value, new RegExp(`${type}$`));
+  }
packages/journey-client/src/lib/fr-device/sample-profile.json (1)

2-2: Identifier typo fix looks good.

Key now matches expected identifier. Nice catch and cleanup.

packages/journey-client/src/lib/fr-login-success.ts (2)

38-40: Success URL remains optional — verify all call sites guard for undefined.

If product requirements guarantee a success URL on login success, tighten the type similarly; otherwise ensure callers null-check.

#!/bin/bash
rg -nP --type=ts -C2 '\bgetSuccessUrl\s*\('

31-33: Make session token non-optional (tighten type or throw).

Downstream treats this as always present; returning string | undefined risks runtime bugs. Prefer a non-optional return and enforce the invariant.

Apply this diff:

-  public getSessionToken(): string | undefined {
-    return this.payload.tokenId;
-  }
+  public getSessionToken(): string {
+    return this.payload.tokenId;
+  }

Optionally pair with the constructor/type changes in my other comments to guarantee the invariant at construction time.

Run to find call sites that assume a defined token:

#!/bin/bash
rg -nP --type=ts -C2 '\bgetSessionToken\s*\('
packages/journey-client/src/lib/callbacks/confirmation-callback.ts (1)

59-64: Fix: validate arbitrary option indices, not just 0/1

Current check breaks callbacks with >2 options and allows non-integer indices.

Apply:

-  public setOptionIndex(index: number): void {
-    if (index !== 0 && index !== 1) {
-      throw new Error(`"${index}" is not a valid choice`);
-    }
-    this.setInputValue(index);
-  }
+  public setOptionIndex(index: number): void {
+    const options = this.getOptions();
+    if (!Number.isInteger(index) || index < 0 || index >= options.length) {
+      const max = Math.max(0, options.length - 1);
+      throw new Error(`"${index}" is not a valid choice (0..${max})`);
+    }
+    this.setInputValue(index);
+  }
packages/journey-client/src/lib/fr-device/index.ts (2)

146-164: Critical: SSR crash and brittle storage in getIdentifier (use safe checks + ephemeral fallback).

Directly referencing localStorage will throw in SSR/Non‑DOM. Also guard storage operations (Safari ITP/Private Mode can throw) and return a stable in‑memory ID when persistence is unavailable. This was flagged previously; repeating with a concrete fix.

Apply:

@@
-  getIdentifier(): string {
-    const storageKey = `${this.prefix}-DeviceID`;
-
-    if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
-      FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.');
-      return '';
-    }
-    if (!localStorage) {
-      FRLogger.warn('Cannot store profile ID. localStorage is not supported.');
-      return '';
-    }
-    let id = localStorage.getItem(storageKey);
-    if (!id) {
-      // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532"
-      id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
-      localStorage.setItem(storageKey, id);
-    }
-    return id;
-  }
+  getIdentifier(): string {
+    const storageKey = `${this.prefix}-DeviceID`;
+    // Prefer WebCrypto; fallback to non‑crypto only when absent
+    const genId = () =>
+      (globalThis.crypto?.getRandomValues
+        ? globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-')
+        : `${Date.now()}-${Math.random().toString(36).slice(2)}`);
+
+    // Ensure a stable per‑instance ID if we cannot persist
+    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+    if ((this as any).ephemeralId) return (this as any).ephemeralId as string;
+
+    const storage: Storage | null =
+      typeof globalThis !== 'undefined' && 'localStorage' in globalThis
+        ? (globalThis.localStorage as Storage)
+        : null;
+
+    if (!storage) {
+      FRLogger.warn('Cannot store profile ID. localStorage is not supported; using in‑memory ID.');
+      (this as any).ephemeralId = genId();
+      return (this as any).ephemeralId as string;
+    }
+
+    try {
+      const existing = storage.getItem(storageKey);
+      if (existing) return existing;
+    } catch {
+      FRLogger.warn('localStorage getItem failed; using in‑memory ID.');
+      (this as any).ephemeralId = genId();
+      return (this as any).ephemeralId as string;
+    }
+
+    const id = genId();
+    try {
+      storage.setItem(storageKey, id);
+      return id;
+    } catch {
+      FRLogger.warn('localStorage setItem failed; using in‑memory ID.');
+      (this as any).ephemeralId = id;
+      return id;
+    }
+  }

Also add a backing field near the other fields:

@@
   private prefix: string;
+  private ephemeralId?: string;

237-264: Privacy/compliance: gate persistent ID and fingerprinting fields behind explicit opt‑in.

Currently always returns a persistent identifier and collects fonts/plugins/timezone, which can constitute fingerprinting. Make these opt‑in with safe defaults (off), and document.

Minimal approach inside this file:

@@
-class FRDevice {
+class FRDevice {
   config: BaseProfileConfig = {
@@
-  private prefix: string;
+  private prefix: string;
+  private options: {
+    persistId: boolean;
+    collectFonts: boolean;
+    collectPlugins: boolean;
+    collectTimezone: boolean;
+  } = { persistId: false, collectFonts: false, collectPlugins: false, collectTimezone: false };
@@
-  constructor(config?: ProfileConfigOptions, prefix = 'forgerock') {
+  constructor(
+    config?: ProfileConfigOptions,
+    prefix = 'forgerock',
+    options?: Partial<FRDevice['options']>,
+  ) {
     this.prefix = prefix;
+    if (options) this.options = { ...this.options, ...options };
@@
-  getIdentifier(): string {
+  getIdentifier(): string {
@@
-    const storage: Storage | null =
+    const storage: Storage | null =
       typeof globalThis !== 'undefined' && 'localStorage' in globalThis
         ? (globalThis.localStorage as Storage)
         : null;
 
-    if (!storage) {
+    if (!this.options.persistId || !storage) {
       FRLogger.warn('Cannot store profile ID. localStorage is not supported; using in‑memory ID.');
       (this as any).ephemeralId = genId();
       return (this as any).ephemeralId as string;
     }

And conditionally include fields:

@@
-        platform: {
+        platform: {
           ...this.getOSMeta(),
           deviceName: this.getDeviceName(),
-          fonts: this.getInstalledFonts(),
-          timezone: this.getTimezoneOffset(),
+          ...(this.options.collectFonts ? { fonts: this.getInstalledFonts() } : {}),
+          ...(this.options.collectTimezone ? { timezone: this.getTimezoneOffset() } : {}),
         },
@@
-          plugins: this.getBrowserPluginsNames(),
+          ...(this.options.collectPlugins ? { plugins: this.getBrowserPluginsNames() } : {}),

If desired, I can follow up with interface updates and README verbiage.

packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1)

341-342: JWT-like authId in JSON-response fixtures: sanitize or allowlist.

Same issue previously noted; still present here. Replace with clearly fake token or add a repo-level gitleaks allowlist for mock fixtures.

Apply this diff:

-  authId:
-    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdXRoSW5kZXhWYWx1ZSI6IndlYmF1dGhuIiwib3RrIjoicXN1dTA0anNxZ2hmcGpubjFiM2IxdDh0NTQiLCJhdXRoSW5kZXhUeXBlIjoic2VydmljZSIsInJlYWxtIjoiLyIsInNlc3Npb25JZCI6IipB...HVU',
+  authId: 'header.payload.signature.TEST_ONLY',
-  authId:
-    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ3aGl0ZWxpc3Qtc3RhdGUiOiJjMjMzNDRkMC04ZTlhLTRhM2QtODZkMS1mNTIwNTExZmM3NjciLCJhdXRoSW5kZXhWYWx1ZSI6IkFuZHlXZWJBdXRobiIsIm90ayI6ImFzdTNjMmo4YThta2w0aWQyN3FndGFuaTVqIiwiYXV0aEluZGV4VHlwZSI6InNlcnZpY2UiLCJyZWFsbSI6Ii9hbHBoYSIsInNlc3Npb25JZCI6IipB...0Cg',
+  authId: 'header.payload.signature.TEST_ONLY',

Also applies to: 387-388

packages/journey-client/src/lib/fr-device/device-profile.test.ts (1)

10-18: Mock Web Crypto correctly and restore after tests to avoid pollution.

Current mock returns strings and doesn’t follow getRandomValues semantics; also not restored after tests.

Apply:

-import { vi, expect, describe, it } from 'vitest';
+import { vi, expect, describe, it, beforeAll, afterAll } from 'vitest';
@@
-Object.defineProperty(window, 'crypto', {
-  writable: true,
-  value: {
-    getRandomValues: vi.fn().mockImplementation(() => ['714524572', '2799534390', '3707617532']),
-  },
-});
+const originalCrypto = globalThis.crypto;
+beforeAll(() => {
+  Object.defineProperty(globalThis, 'crypto', {
+    configurable: true,
+    value: {
+      // Fill the provided typed array per spec and return it
+      getRandomValues: vi.fn(<T extends ArrayBufferView>(arr: T) => {
+        const view = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
+        const seed = Uint8Array.from([0x2a, 0xb3, 0x71, 0x45, 0x24, 0x57, 0x29, 0xfe]);
+        for (let i = 0; i < view.length; i++) view[i] = seed[i % seed.length];
+        return arr;
+      }),
+    } as Crypto,
+  });
+});
+afterAll(() => {
+  Object.defineProperty(globalThis, 'crypto', {
+    configurable: true,
+    value: originalCrypto,
+  });
+});
packages/journey-client/src/lib/fr-webauthn/index.ts (6)

210-213: Fix metadata type in registration JSON branch.

Cast to registration metadata.

Apply:

-      if (metadataCallback) {
-        const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata;
+      if (metadataCallback) {
+        const meta = metadataCallback.getOutputValue('data') as WebAuthnRegistrationMetadata;

415-419: Type fix: remove AttestationType.Direct and return typed template string.

Aligns with AttestationType union and declared return type.

Apply:

-      const attestationObject = new Int8Array(
-        attestationResponse.attestationObject,
-      ).toString() as AttestationType.Direct;
-      return `${clientDataJSON}::${attestationObject}::${credential.id}`;
+      const attestationObject = new Int8Array(
+        attestationResponse.attestationObject,
+      ).toString() as AttestationType;
+      return `${clientDataJSON}::${attestationObject}::${credential.id}` as OutcomeWithName<
+        string,
+        AttestationType,
+        PublicKeyCredential
+      >;

86-96: Type guard: don’t call .includes on union (string | object).

Prevents runtime/type errors when message is an object.

Apply:

-    if (outcomeCallback && textOutputCallback) {
-      const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration;
-
-      if (message.includes('pubKeyCredParams')) {
-        return WebAuthnStepType.Registration;
-      }
-
-      return WebAuthnStepType.Authentication;
-    } else {
+    if (outcomeCallback && textOutputCallback) {
+      const raw = textOutputCallback.getMessage() as unknown;
+      if (typeof raw === 'string') {
+        return raw.includes('pubKeyCredParams')
+          ? WebAuthnStepType.Registration
+          : WebAuthnStepType.Authentication;
+      }
+      if (raw && typeof raw === 'object' && 'pubKeyCredParams' in (raw as Record<string, unknown>)) {
+        return WebAuthnStepType.Registration;
+      }
+      return WebAuthnStepType.Authentication;
+    } else {
       return WebAuthnStepType.None;
     }

181-189: Registration path: wrong publicKey type; remove unnecessary casts.

Use CreationOptions end-to-end.

Apply:

-      try {
-        let publicKey: PublicKeyCredentialRequestOptions;
+      try {
+        let publicKey: PublicKeyCredentialCreationOptions;
@@
-          credential = await this.getRegistrationCredential(
-            publicKey as PublicKeyCredentialCreationOptions,
-          );
+          credential = await this.getRegistrationCredential(publicKey);
@@
-          publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage());
-          credential = await this.getRegistrationCredential(
-            publicKey as PublicKeyCredentialCreationOptions,
-          );
+          publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage());
+          credential = await this.getRegistrationCredential(publicKey);

Also applies to: 191-195


311-319: SSR safety: guard window/navigator.credentials. checks.*

Avoid ReferenceErrors under SSR and validate APIs exist.

Apply:

-    if (!window.PublicKeyCredential) {
+    if (
+      typeof window === 'undefined' ||
+      !('PublicKeyCredential' in window) ||
+      typeof navigator === 'undefined' ||
+      !navigator.credentials ||
+      typeof navigator.credentials.get !== 'function'
+    ) {
       const e = new Error('PublicKeyCredential not supported by this browser');
       e.name = WebAuthnOutcomeType.NotSupportedError;
       throw e;
     }
-    if (!window.PublicKeyCredential) {
+    if (
+      typeof window === 'undefined' ||
+      !('PublicKeyCredential' in window) ||
+      typeof navigator === 'undefined' ||
+      !navigator.credentials ||
+      typeof navigator.credentials.create !== 'function'
+    ) {
       const e = new Error('PublicKeyCredential not supported by this browser');
       e.name = WebAuthnOutcomeType.NotSupportedError;
       throw e;
     }

Also applies to: 385-395


447-455: Decode base64url challenges; set user.name to identifier.

atob expects base64 (not base64url). Also prefer user.name = userName.

Apply:

-    return {
-      challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer,
+    return {
+      challenge: FRWebAuthn.fromBase64Url(challenge),
       timeout,
-    return {
+    return {
       attestation: attestationPreference,
       authenticatorSelection: JSON.parse(authenticatorSelection),
-      challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer,
+      challenge: FRWebAuthn.fromBase64Url(challenge),
       ...(excludeCredentials.length && { excludeCredentials }),
       pubKeyCredParams,
       rp,
       timeout,
       user: {
-        displayName: displayName || userName,
-        id: Int8Array.from(userId.split('').map((c: string) => c.charCodeAt(0))),
-        name: displayName || userName,
+        displayName: displayName || userName,
+        id: Int8Array.from(userId.split('').map((c: string) => c.charCodeAt(0))),
+        name: userName,
       },
     };

Add helper inside FRWebAuthn:

// Place inside FRWebAuthn class
private static toBase64(b64url: string): string {
  const pad = b64url.length % 4 === 0 ? '' : '='.repeat(4 - (b64url.length % 4));
  return b64url.replace(/-/g, '+').replace(/_/g, '/') + pad;
}
public static fromBase64Url(b64url: string): ArrayBuffer {
  const b64 = FRWebAuthn.toBase64(b64url);
  if (typeof atob === 'function') {
    const bin = atob(b64);
    const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
    return bytes.buffer;
  }
  // Fallback for non-browser environments
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  const buf = typeof Buffer !== 'undefined' ? Buffer.from(b64, 'base64') : new Uint8Array();
  return (buf.buffer?.slice?.(buf.byteOffset, buf.byteOffset + buf.byteLength) ?? buf.buffer) as ArrayBuffer;
}

Also applies to: 493-506

packages/journey-client/src/lib/fr-webauthn/script-parser.ts (1)

49-55: Fix unsafe regexes: replace [^] hacks with [\s\S]*? and make groups non-greedy.

Current patterns overmatch and trigger Biome errors; they can parse wrong blocks.

Apply:

-  const user = getIndexOne(text.match(/user"{0,}:\s{0,}{([^]{0,})},/)).trim();
+  const user = getIndexOne(text.match(/user"{0,}:\s{0,}{([\s\S]*?)},/)).trim();

-    text.match(/pubKeyCredParams"*:\s*\[([^]+\d\s*})\s*]/),
+    text.match(/pubKeyCredParams"*:\s*\[([\s\S]*?\d\s*})\s*]/),

-    text.match(/excludeCredentials"{0,}:\s{0,}\[([^]+)\s{0,}]/),
+    text.match(/excludeCredentials"{0,}:\s{0,}\[([\s\S]*?)\s{0,}]/),

-      text.match(/acceptableCredentials"*\s*=\s*\[([^]+)\s*]/),
+      text.match(/acceptableCredentials"*\s*=\s*\[([\s\S]*?)\s*]/),

-      text.match(/allowCredentials"{0,}:\s{0,}\[([^]+)\s{0,}]/),
+      text.match(/allowCredentials"{0,}:\s{0,}\[([\s\S]*?)\s{0,}]/),

Also applies to: 60-66, 78-81, 130-133, 138-141

🧹 Nitpick comments (74)
.changeset/tender-schools-scream.md (1)

1-9: Changeset content doesn't match PR objectives and file changes.

The changeset describes minor SDK type updates and ESLint configuration changes, but the PR objectives indicate this is about creating a new journey package with significant functionality including client factory, RTK Query API, Redux slice/store, callback wrappers, device profiling, WebAuthn utilities, etc. The changeset should reflect the actual scope and impact of the changes being made.

Update the changeset to accurately reflect the journey package creation:

---
-'@forgerock/sdk-types': minor
+'@forgerock/journey-client': minor
+'@forgerock/sdk-types': minor
---

-feat: Update SDK types
+feat: Create journey package

-- Updated ESLint configurations for consistent code style and linting rules.
-- Ensured compatibility with `verbatimModuleSyntax` by correcting type-only imports and module exports.
+- Created new @forgerock/journey-client package with client factory, RTK Query API, and Redux slice/store
+- Added comprehensive FRCallback wrappers and factories for various authentication flows
+- Implemented device profiling, WebAuthn, QR code, and recovery code utilities
+- Added policy helpers and extensive test coverage
+- Configured TypeScript, Vite, and ESLint for the new package
+- Updated SDK types and utilities with related exports
+- Fixed ESLint parser configurations for improved compatibility
packages/journey-client/src/lib/fr-policy/message-creator.ts (2)

22-22: Fix double space in error message.

There's a double space in the error message that should be corrected for consistency.

-    return `${property}  must not contain duplicates: "${duplicateValue}"`;
+    return `${property} must not contain duplicates: "${duplicateValue}"`;

36-36: Consider making generic policy messages more user-friendly.

The messages for MatchRegexp, MaximumNumber, and MinimumNumber expose internal policy names which may not be meaningful to end users. Consider providing more descriptive messages.

-  [PolicyKey.MatchRegexp]: (property: string) => `${property} has failed the "MATCH_REGEXP" policy`,
+  [PolicyKey.MatchRegexp]: (property: string) => `${property} format is invalid`,
-  [PolicyKey.MaximumNumber]: (property: string) =>
-    `${property} has failed the "MAXIMUM_NUMBER_VALUE" policy`,
+  [PolicyKey.MaximumNumber]: (property: string, params?: { maxValue?: number }) => {
+    const maxValue = getProp<number>(params, 'maxValue', 0);
+    return `${property} must be at most ${maxValue}`;
+  },
-  [PolicyKey.MinimumNumber]: (property: string) =>
-    `${property} has failed the "MINIMUM_NUMBER_VALUE" policy`,
+  [PolicyKey.MinimumNumber]: (property: string, params?: { minValue?: number }) => {
+    const minValue = getProp<number>(params, 'minValue', 0);
+    return `${property} must be at least ${minValue}`;
+  },

Also applies to: 41-42, 47-48

packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts (2)

61-63: Consider adding type safety for the payload parameter.

The unknown type for the payload parameter provides no type safety. Consider defining a more specific type if the reCAPTCHA payload structure is known, or add JSDoc documentation explaining the expected structure.

-  public setPayload(payload: unknown) {
+  public setPayload(payload: unknown) {

Consider adding JSDoc documentation:

/**
 * Set the recaptcha payload
 * @param payload The reCAPTCHA response payload (typically an object with token and action)
 */
public setPayload(payload: unknown) {
  this.setInputValue(payload, 'IDToken1payload');
}

52-52: Fix typo in JSDoc comment.

There's a duplicate word "client" in the JSDoc comment.

-   * Set client client error
+   * Set client error
packages/journey-client/src/lib/callbacks/suspended-text-output-callback.ts (1)

10-12: Consider enhancing the JSDoc documentation.

The current JSDoc is quite generic and doesn't explain what makes this callback "suspended" or how it differs from a regular TextOutputCallback.

 /**
- * Represents a callback used to display a message.
+ * Represents a callback used to display a message for suspended text output.
+ * This is typically used when the authentication flow is temporarily suspended
+ * and needs to display status information to the user.
  */
packages/journey-client/src/lib/callbacks/kba-create-callback.ts (2)

17-19: Drop the parameter‑property override for payload

Redundant override/reassignment; rely on base class storage.

-  constructor(public override payload: Callback) {
+  constructor(payload: Callback) {
     super(payload);
   }

15-16: Nit: modernize wording

Prefer “AM (Access Management)” over “OpenAM” in docs.

-   * @param payload The raw payload returned by OpenAM
+   * @param payload The raw AM (Access Management) callback payload
packages/journey-client/src/lib/fr-device/sample-profile.json (6)

38-44: Timezone and coordinates are inconsistent.

Offset 300 (UTC-5) suggests Eastern Time, but longitude −94.889905 is Central. Pick one and align.

Option A (keep coordinates → Central):

-      "timezone": 300
+      "timezone": 360

Option B (keep Eastern offset → adjust longitude to FL):

-    "longitude": -94.889905
+    "longitude": -81.686783

18-21: Stale/edge‑case UA.

Edge/80 is from 2020. If this sample is used in tests or analytics, consider updating to a contemporary Chromium UA or annotate that it’s illustrative to avoid churn in expectations.


11-15: Portrait 1080×1920 with angle 270 vs. MacIntel platform.

Looks mobile‑ish while platform is desktop. If intentional, all good; otherwise adjust to a desktop‑like orientation (angle 0, e.g., 1920×1080).

Also applies to: 32-35


5-5: Nulls vs. consumer types.

Several fields are null. Confirm downstream types allow nullables; if not, prefer omitting keys or using empty strings to avoid schema mismatches.

Also applies to: 9-9, 22-24, 28-28, 35-36


29-29: Remove obsolete NaCl plugin.

NaCl is long deprecated; drop it from the sample.

Apply:

-      "plugins": "internal-pdf-viewer;mhjfbmdgcfjbbpaeojofohoefgiehjai;internal-nacl-plugin;"
+      "plugins": "internal-pdf-viewer;mhjfbmdgcfjbbpaeojofohoefgiehjai;"

29-29: Consider arrays for list‑like fields.

plugins and fonts are semicolon‑delimited strings. If the schema permits, arrays improve clarity and safer parsing. If clients depend on strings, ignore.

Also applies to: 37-37

packages/journey-client/src/lib/fr-login-success.ts (3)

7-9: Introduce a narrowed payload type for LoginSuccess steps.

Guarantee tokenId at the type level; keep successUrl optional if not guaranteed by AM.

Apply right after imports:

 import type { Step, AuthResponse } from '@forgerock/sdk-types';
 import { StepType } from '@forgerock/sdk-types';
 
+// Narrow the payload for LoginSuccess responses
+type LoginSuccessPayload = Step & { tokenId: string; successUrl?: string };

19-19: Enforce invariant at construction and avoid payload rebinding.

Make payload readonly and assert presence of tokenId (defense-in-depth against malformed network data).

-  constructor(public payload: Step) {}
+  constructor(public readonly payload: LoginSuccessPayload) {
+    if (!payload.tokenId) {
+      throw new Error('FRLoginSuccess requires a tokenId on the payload');
+    }
+  }

Optional: shallow-freeze to discourage mutation of the payload object.

Object.freeze(this.payload);

14-14: Nit: narrow the type constant.

Minor typing improvement; keeps the literal enum member.

-  public readonly type = StepType.LoginSuccess;
+  public readonly type = StepType.LoginSuccess as const;
packages/journey-client/src/lib/callbacks/confirmation-callback.ts (2)

38-40: Optional: return a defensive copy of options

Prevents external mutation of the underlying payload array.

-  public getOptions(): string[] {
-    return this.getOutputByName<string[]>('options', []);
-  }
+  public getOptions(): string[] {
+    const opts = this.getOutputByName<string[]>('options', []);
+    return Array.isArray(opts) ? [...opts] : [];
+  }

69-75: Nit: include valid choices in error for easier debugging

Improves DX without behavior change.

-    if (index === -1) {
-      throw new Error(`"${value}" is not a valid choice`);
-    }
+    if (index === -1) {
+      throw new Error(`"${value}" is not a valid choice. Valid options: ${this.getOptions().join(', ')}`);
+    }
packages/journey-client/src/lib/fr-device/index.ts (4)

90-99: Nit: simplify double‑negative check and harden plugin enumeration.

Use optional chaining for readability and avoid relying on Object.keys of PluginArray.

-  if (!(typeof navigator !== 'undefined' && navigator.plugins)) {
+  if (!navigator?.plugins?.length) {
     FRLogger.warn('Cannot collect browser plugin information. navigator.plugins is not defined.');
     return '';
   }
-  return reduceToString(
-      Object.keys(navigator.plugins),
-      navigator.plugins as unknown as Record<string, { filename: string }>,
-    );
+  const plugins = Array.from(navigator.plugins);
+  return plugins.map((p) => p.filename ?? p.name ?? '').filter(Boolean).join(';');

Note: if reduceToString is required for consistency, consider adding an overload that accepts arrays.


135-144: Minor: log message mismatch in getHardwareMeta.

Warn mentions “OS metadata” but function collects hardware props.

-  FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.');
+  FRLogger.warn('Cannot collect hardware metadata. navigator is not defined.');

101-125: Optional: improve iPadOS/iOS detection in getDeviceName.

Modern iPadOS can report MacIntel platform; consider UA hints to avoid mislabeling as “Mac (Browser)”.

-  case this.config.devicePlatforms.mac.includes(platform):
-    return 'Mac (Browser)';
+  case this.config.devicePlatforms.mac.includes(platform) &&
+       !/iPad|iPhone|iPod/.test(userAgent):
+    return 'Mac (Browser)';

Please verify against Safari on iPadOS 16+.


32-33: Optional: make FRLogger level configurable via consumer config.

Hard‑coding 'info' can be noisy in prod; consider wiring through SDK/global config.

packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.test.ts (5)

19-27: Assert behavior, not just method invocation; fix test name.

Spying on the very method you call is tautological. Assert the returned value and rename for clarity.

-it('should test that the pauseBehavior method can be called', () => {
+it('getPauseBehavioralData returns true when pauseBehavioralData is true', () => {
   const callback = new PingOneProtectEvaluationCallback({
     type: callbackType.PingOneProtectEvaluationCallback,
     output: [{ name: 'pauseBehavioralData', value: true }],
   });
-  const mock = vi.spyOn(callback, 'getPauseBehavioralData');
-  callback.getPauseBehavioralData();
-  expect(mock).toHaveBeenCalled();
+  expect(callback.getPauseBehavioralData()).toBe(true);
 });

28-47: Drop tautological spy in setData test; keep the state assertion.

You’re already validating the side effect; the spy adds no signal.

-  const mock = vi.spyOn(callback, 'setData');
-  callback.setData('data');
-  expect(mock).toHaveBeenCalledWith('data');
+  callback.setData('data');
   expect(callback.getInputValue('IDToken1signals')).toBe('data');

48-67: Same here: remove self-spy for setClientError.

Keep the observable assertion only.

-  const mock = vi.spyOn(callback, 'setClientError');
-  callback.setClientError('error i just set');
-  expect(mock).toHaveBeenCalledWith('error i just set');
+  callback.setClientError('error i just set');
   expect(callback.getInputValue('IDToken1clientError')).toBe('error i just set');

11-11: Remove unused vi import if you apply the above refactors.

-import { vi, describe, it, expect } from 'vitest';
+import { describe, it, expect } from 'vitest';

15-68: Add one negative/edge test for resilience.

Consider a case where:

  • pauseBehavioralData is absent or false, expect getPauseBehavioralData() to be false (or default).
  • inputs don’t include IDToken1clientError or IDToken1signals, expect graceful no-op or thrown error per contract.

Also consider deriving input keys from the test data you construct (e.g., store the array and use input[0].name) to avoid coupling to ID numbering.

packages/journey-client/src/lib/fr-webauthn/helpers.test.ts (6)

21-25: Stronger type checks for ArrayBuffer and explicit length assertion.

Prefer instance checks over stringification; also assert the array length.

Apply:

 it('should parse the one credential in the MetadataCallback 7.0', () => {
   const credentials = parseCredentials(allowCredentials70);
-  expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+  expect(credentials).toHaveLength(1);
+  expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
+  expect(new Uint8Array(credentials[0].id).byteLength).toBeGreaterThan(0);
   expect(credentials[0].type).toBe('public-key');
 });

27-33: Validate count and use toBeInstanceOf for both credentials.

Catches regressions where one descriptor is dropped or malformed.

Apply:

 it('should parse the two credentials in the MetadataCallback 7.0', () => {
   const credentials = parseCredentials(allowMultipleCredentials70);
-  expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+  expect(credentials).toHaveLength(2);
+  expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
   expect(credentials[0].type).toBe('public-key');
-  expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]');
+  expect(credentials[1].id).toBeInstanceOf(ArrayBuffer);
   expect(credentials[1].type).toBe('public-key');
 });

35-39: Same as above: assert length and ArrayBuffer type.

Apply:

 it('should parse the one credential in the MetadataCallback 6.5', () => {
   const credentials = parseCredentials(acceptableCredentials653);
-  expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+  expect(credentials).toHaveLength(1);
+  expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
   expect(credentials[0].type).toBe('public-key');
 });

41-47: Same as above for the two-credential 6.5 case.

Apply:

 it('should parse the two credentials in the MetadataCallback 6.5', () => {
   const credentials = parseCredentials(acceptableMultipleCredentials653);
-  expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+  expect(credentials).toHaveLength(2);
+  expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
   expect(credentials[0].type).toBe('public-key');
-  expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]');
+  expect(credentials[1].id).toBeInstanceOf(ArrayBuffer);
   expect(credentials[1].type).toBe('public-key');
 });

49-53: Assert expected count for pubKeyCredParams.

Guards against accidental extras or parsing duplicates.

Apply:

 it('should parse the pubKeyCredParams in the MetadataCallback 7.0 & 6.5.3', () => {
   const pubKeyCredParams = parsePubKeyArray(pubKeyCredParamsStr);
+  expect(pubKeyCredParams).toHaveLength(2);
   expect(pubKeyCredParams).toContainEqual({ type: 'public-key', alg: -7 });
   expect(pubKeyCredParams).toContainEqual({ type: 'public-key', alg: -257 });
 });

20-54: Reduce duplication with parameterized tests.

Use test.each to cover 6.5.x/7.0 single/multiple cases with one body; easier to extend and maintain.

Example:

type Case = { name: string; src: string; count: number };
test.each<Case>([
  { name: '7.0 single', src: allowCredentials70, count: 1 },
  { name: '7.0 multiple', src: allowMultipleCredentials70, count: 2 },
  { name: '6.5 single', src: acceptableCredentials653, count: 1 },
  { name: '6.5 multiple', src: acceptableMultipleCredentials653, count: 2 },
])('parseCredentials %s', ({ src, count }) => {
  const creds = parseCredentials(src);
  expect(creds).toHaveLength(count);
  creds.forEach((c) => {
    expect(c.type).toBe('public-key');
    expect(c.id).toBeInstanceOf(ArrayBuffer);
  });
});
packages/journey-client/src/lib/callbacks/password-callback.test.ts (2)

13-35: Avoid shared mutable test fixture; create a fresh payload per test.

setPassword likely mutates input[0].value. Reusing payload across tests risks inter-test coupling if order changes or tests are run concurrently.

Apply this diff:

-  const payload: Callback = {
+  const makePayload = (): Callback => ({
     type: callbackType.PasswordCallback,
     output: [
       {
         name: 'prompt',
         value: 'Password',
       },
       {
         name: 'policies',
         value: ['policy1', 'policy2'],
       },
       {
         name: 'failedPolicies',
         value: ['failedPolicy1'],
       },
     ],
     input: [
       {
         name: 'IDToken1',
         value: '',
       },
     ],
-  };
+  });
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());
@@
-    const cb = new PasswordCallback(payload);
+    const cb = new PasswordCallback(makePayload());

Also applies to: 38-39, 43-44, 48-49, 53-55


52-56: Add a negative-path test.

Consider a test that asserts a clear error if input is missing or malformed, e.g., no IDToken* entry. This guards regressions in parsing.

Happy to draft the additional test if you want.

packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (2)

344-345: Use callbackType constants for type fields (consistency).

Unify with the rest of the file by using enum constants instead of string literals.

-      type: 'MetadataCallback',
+      type: callbackType.MetadataCallback,
-      type: 'HiddenValueCallback',
+      type: callbackType.HiddenValueCallback,

Also applies to: 364-364, 390-390, 434-434


13-49: Consider exporting fixtures as readonly to improve type safety.

Appending “as const” narrows literal types and prevents accidental mutation of test data.

-export const webAuthnRegJSCallback653 = {
+export const webAuthnRegJSCallback653 = {
   /* ... */
-};
+} as const;

If any tests rely on mutating these objects, skip this change or clone before mutation.

packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts (4)

7-13: Add a typed config interface for clarity and API stability.

Define and export a config interface used by getConfig() to prevent shape drift and improve DX.

 import FRCallback from './index.js';
 import type { Callback } from '@forgerock/sdk-types';
 
+export interface PingOneProtectInitConfig {
+  envId: string;
+  consoleLogEnabled?: boolean;
+  deviceAttributesToIgnore?: string[];
+  customHost?: string;
+  lazyMetadata?: boolean;
+  behavioralDataCollection?: boolean;
+  deviceKeyRsyncIntervals?: number;
+  enableTrust?: boolean;
+  disableTags?: boolean;
+  disableHub?: boolean;
+}
+
 /**
  * @class - Represents a callback used to initialize and start device and behavioral data collection.
  */

40-42: Anchor the input filter regex.

Prevents accidental matches like clientErrorCode.

-    this.setInputValue(errorMessage, /clientError/);
+    this.setInputValue(errorMessage, /^clientError$/);

24-38: Only include keys present in the payload to avoid overriding server defaults.

Right now defaults are injected even when AM doesn’t send a field. Consider building the object conditionally from payload.output.

Would you like a patch that inspects this.payload.output and selectively sets known keys only when present?


13-13: Naming consistency nit: file vs class.

Class is PingOneProtectInitializeCallback but filename is ping-protect-initialize-callback.ts. Consider aligning to ping-one-protect-... for discoverability.

packages/journey-client/src/lib/callbacks/redirect-callback.test.ts (1)

24-27: Add test cases for edge cases and validation.

While the happy path test is good, consider adding tests for edge cases to ensure robust error handling.

Consider adding these additional test cases:

it('should handle missing redirectUrl in output', () => {
  const payloadWithoutUrl: Callback = {
    type: callbackType.RedirectCallback,
    output: [],
    input: [],
  };
  const cb = new RedirectCallback(payloadWithoutUrl);
  expect(cb.getRedirectUrl()).toBeUndefined(); // or throw error, depending on implementation
});

it('should handle empty redirectUrl value', () => {
  const payloadWithEmptyUrl: Callback = {
    type: callbackType.RedirectCallback,
    output: [{ name: 'redirectUrl', value: '' }],
    input: [],
  };
  const cb = new RedirectCallback(payloadWithEmptyUrl);
  expect(cb.getRedirectUrl()).toBe('');
});

it('should handle malformed URL', () => {
  const payloadWithMalformedUrl: Callback = {
    type: callbackType.RedirectCallback,
    output: [{ name: 'redirectUrl', value: 'not-a-valid-url' }],
    input: [],
  };
  const cb = new RedirectCallback(payloadWithMalformedUrl);
  expect(cb.getRedirectUrl()).toBe('not-a-valid-url'); // or validate URL format
});
packages/journey-client/src/lib/interfaces.ts (2)

10-13: Make fields readonly and add TSDoc for intent.

Shallow immutability helps callers and tooling; TSDoc clarifies that a hydrated Step instance is expected.

Apply:

-export interface NextOptions {
-  step: Step;
-  options?: StepOptions;
-}
+/**
+ * Input for advancing a journey step.
+ * Expects a hydrated `Step` instance (not a serialized payload).
+ */
+export interface NextOptions {
+  readonly step: Step;
+  readonly options?: Readonly<StepOptions>;
+}

8-8: Type‑only import: re‑export Step/StepOptions from the public barrel (optional).

  • Findings: StepOptions is exported in packages/journey-client/src/lib/config.types.ts but NOT re‑exported from packages/journey-client/src/index.ts; NextOptions is declared in packages/journey-client/src/lib/interfaces.ts but not exported; Step is not re‑exported from the package public API.
  • Action: add type re‑exports to packages/journey-client/src/index.ts (e.g. export type { StepOptions } from './lib/config.types.js'; export type { NextOptions } from './lib/interfaces.js'; export type { Step } from '@forgerock/sdk-types';) so consumers can import types from @forgerock/journey-client instead of depending on @forgerock/sdk-types.
packages/journey-client/src/lib/fr-device/device-profile.test.ts (2)

34-37: Reduce brittleness: avoid strict equality on environment-dependent navigator fields.

Hard-coding appName/appVersion/vendor can flake across jsdom/Node versions.

Consider:

-    expect(appName).toBe('Netscape');
-    expect(appVersion).toBe('4.0');
-    expect(vendor).toBe('Apple Computer, Inc.');
+    expect(appName).toBeTruthy();
+    expect(appVersion).toBeTruthy();
+    expect(typeof vendor).toBe('string');

72-75: Same here: loosen navigator field assertions to avoid environment flakiness.

Suggested:

-    expect(appName).toBeFalsy();
-    expect(appVersion).toBeFalsy();
-    expect(vendor).toBeFalsy();
+    expect(appName ?? null).toBeFalsy();
+    expect(appVersion ?? null).toBeFalsy();
+    expect(vendor ?? null).toBeFalsy();
packages/journey-client/eslint.config.mjs (2)

16-17: Avoid top‑level await in ESLint config; use a static import of the parser.

Static import sidesteps TLA requirements across tooling.

Apply:

-import baseConfig from '../../eslint.config.mjs';
+import baseConfig from '../../eslint.config.mjs';
+import jsoncParser from 'jsonc-eslint-parser';
@@
-    languageOptions: {
-      parser: (await import('jsonc-eslint-parser')).default,
-    },
+    languageOptions: {
+      parser: jsoncParser,
+    },

6-6: Include JSONC files too (common in Nx/ESLint/tooling configs).

Apply:

-    files: ['**/*.json'],
+    files: ['**/*.json', '**/*.jsonc'],
packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (6)

35-41: Garbled QR script string; simplify and sanitize.

The backtick/concat splits create broken hex escape sequences (e.g., \\x split) and make the payload hard to read. Use a single clean template string with obvious placeholders.

-          value:
-            `window.QRCodeReader.createCode({\n    id: 'callback_0',\n    text: 'otpauth\\x3A\\x` +
-            `2F\\x2Ftotp\\x2FForgeRock\\x3Ajlowery\\x3Fperiod\\x3D30\\x26b\\x3D032b75\\x26` +
-            `digits\\x3D6\\x26secret\\QITSTC234FRIU8DD987DW3VPICFY\\x3D\\x3D\\x3D\\x3D\\x3` +
-            `D\\x',\n  3D\\x26issuer\\x3DForgeRock  version: '20',\n    code: 'L'\n});`,
+          value: `window.QRCodeReader.createCode({
+  id: 'callback_0',
+  text: 'otpauth://totp/ForgeRock:exampleuser?secret=EXAMPLESECRET&issuer=ForgeRock&period=30&digits=6&b=032b75',
+  version: '20',
+  code: 'L'
+});`,

27-27: Normalize messageType to numbers for consistency.

Use numeric literals for all messageType values to match other callbacks.

-          value: '0',
+          value: 0,
...
-          value: '4',
+          value: 4,
...
-          value: '0',
+          value: 0,
...
-          value: '4',
+          value: 4,

If downstream parsing expects strings, keep as‑is and ignore this suggestion.

Also applies to: 44-44, 117-117, 140-140


127-137: Sanitize push QR script string, mirror the HiddenValue changes.

Keep the window.QRCodeReader.createCode shape if tests rely on it, but swap in safe placeholders and a clean template string.

-          value:
-            `window.QRCodeReader.createCode({\n    id: 'callback_0',\n    text: 'pushauth\\x` +
-            `3A\\x2F\\x2Fpush\\x2Fforgerock\\x3AJustin\\x2520Lowery\\x3Fa\\x3DaHR0cHM6Ly9vc` +
-            `GVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXN` +
-            `oL3Nucy9tZXNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl\\x26r\\x3DaHR0cHM6Ly9vcGVuYW0tZm` +
-            `9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9t` +
-            `ZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI\\x26b\\x3D032b75\\x26s\\x3DFoxEr5uAzrys1yBmuyg` +
-            `PbxrVjysElmzsmqifi6eO_AI\\x26c\\x3DXD\\x2DMxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5` +
-            `kjgw\\x26l\\x3DYW1sYmNvb2tpZT0wMQ\\x26m\\x3DREGISTER\\x3A53b85112\\x2D8ba9\\x2` +
-            `D4b7e\\x2D9107\\x2Decbca2d65f7b1695151603616\\x26issuer\\x3DRm9yZ2VSb2Nr',\n  ` +
-            `  version: '20',\n    code: 'L'\n});`,
+          value: `window.QRCodeReader.createCode({
+  id: 'callback_0',
+  text: 'pushauth://push/forgerock:Example%20User?a=https://example.com/alpha/push/sns/message?_action=authenticate&r=https://example.com/alpha/push/sns/message?_action=register&b=032b75&s=EXAMPLESIGNATURE&c=EXAMPLECIPHERTEXT&l=YW1sYmNvb2tpZT0wMQ&m=REGISTER:00000000-0000-0000-0000-000000000000&issuer=Rm9yZ2VSb2Nr',
+  version: '20',
+  code: 'L'
+});`,

101-101: Narrow literal types for the mocks.

Mark the objects as const to keep fields readonly and improve type inference in tests.

-};
+} as const;

Also applies to: 184-184


1-1: Naming consistency: folder vs file.

Path mixes fr-qrcode/ (folder) with fr-qr-code.mock.data.ts (file). Consider standardizing on one form (fr-qrcode or fr-qr-code) for discoverability.


1-9: Optional: add an explicit “test-only data” note.

Add a short note to help future readers and static scanners understand these are placeholders.

 /*
  * @forgerock/javascript-sdk
  *
  * fr-qr-code.mock.data.ts
  *
  * Copyright (c) 2024 - 2025 Ping Identity Corporation. All rights reserved.
  * This software may be modified and distributed under the terms
  * of the MIT license. See the LICENSE file for details.
  */
+
+/**
+ * Test-only mock data for QR-code based journeys.
+ * All identifiers and secrets are placeholders; do not reuse in production.
+ */
packages/journey-client/src/lib/callbacks/index.ts (1)

67-100: Use instanceof RegExp and neutral error messages (works for input/output)

Duck-typing isn’t necessary; also avoid “input entry” when used for output lookups.

-    // Duck typing for RegEx
-    if (typeof selector === 'object' && selector.test && Boolean(selector.exec)) {
-      const input = array.find((x) => selector.test(x.name));
-      if (!input) {
-        throw new Error(`Missing callback input entry "${selector}"`);
-      }
-      return input;
-    }
+    if (selector instanceof RegExp) {
+      const entry = array.find((x) => selector.test(x.name));
+      if (!entry) {
+        throw new Error(`Missing callback entry matching ${selector}`);
+      }
+      return entry;
+    }
 
-    throw new Error('Invalid selector value type');
+    throw new Error('Invalid selector type');
packages/journey-client/src/lib/callbacks/validated-create-password-callback.test.ts (3)

15-59: Avoid shared mutable test payload; create a fresh payload per test

Reuse mutates the same object across tests. Build a new payload each time.

-describe('ValidatedCreatePasswordCallback', () => {
-  const payload: Callback = {
+describe('ValidatedCreatePasswordCallback', () => {
+  const makePayload = (): Callback => ({
     type: callbackType.ValidatedCreatePasswordCallback,
     output: [
@@
     ],
     input: [
@@
     ],
     _id: 1,
-  };
+  });

61-74: Prefer constants over string literals and avoid positional input assertions

Use callbackType enum and read inputs by name to reduce brittleness.

-    const cb = new ValidatedCreatePasswordCallback(payload);
+    const cb = new ValidatedCreatePasswordCallback(makePayload());
@@
-    expect(cb.getType()).toBe('ValidatedCreatePasswordCallback');
+    expect(cb.getType()).toBe(callbackType.ValidatedCreatePasswordCallback);
@@
-    if (!cb.payload.input) throw new Error('Input is not defined');
-    expect(cb.payload.input[0].value).toBe('abcd123');
-    expect(cb.payload.input[1].value).toBe(true);
+    expect(cb.getInputValue('IDToken2')).toBe('abcd123');
+    expect(cb.getInputValue('IDToken2validateOnly')).toBe(true);

76-81: Clone payload for this test and assert by name

Same reasoning as above.

-    const cb = new ValidatedCreatePasswordCallback(payload);
+    const cb = new ValidatedCreatePasswordCallback(makePayload());
@@
-    if (!cb.payload.input) throw new Error('Input is not defined');
-    expect(cb.payload.input[1].value).toBe(false);
+    expect(cb.getInputValue('IDToken2validateOnly')).toBe(false);
packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (2)

38-41: Handle missing/invalid dates; return null when not parseable

new Date('') yields Invalid Date. Safer contract: Date | null.

-  public getCreateDate(): Date {
-    const date = this.getOutputByName<string>('createDate', '');
-    return new Date(date);
-  }
+  public getCreateDate(): Date | null {
+    const dateStr = this.getOutputByName<string>('createDate', '');
+    const d = new Date(dateStr);
+    return isNaN(d.getTime()) ? null : d;
+  }

46-48: Optional: name the input for clarity

If multiple inputs exist, targeting by index can drift. Consider allowing a selector.

-  public setAccepted(accepted = true): void {
-    this.setInputValue(accepted);
-  }
+  public setAccepted(accepted = true, selector: number | string | RegExp = 0): void {
+    this.setInputValue(accepted, selector);
+  }
packages/journey-client/src/lib/journey.store.ts (1)

8-15: Prefer type‑only imports where possible

Slight bundle hygiene; these are used for types only.

-import { JourneyClientConfig } from './config.types.js';
-import { logger as loggerFn } from '@forgerock/sdk-logger';
+import type { JourneyClientConfig } from './config.types.js';
+import { logger as loggerFn } from '@forgerock/sdk-logger';
@@
-import { RequestMiddleware } from '@forgerock/sdk-request-middleware';
+import type { RequestMiddleware } from '@forgerock/sdk-request-middleware';
packages/journey-client/src/lib/journey-client.ts (5)

41-45: Propagate RTK Query errors; use unwrap()

Awaiting dispatch and reading { data } can swallow errors. unwrap() throws on reject.

-    start: async (options?: StepOptions) => {
-      const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
-      return data ? new FRStep(data) : undefined;
-    },
+    start: async (options?: StepOptions) => {
+      const action = await store.dispatch(journeyApi.endpoints.start.initiate(options));
+      const data = await action.unwrap();
+      return data ? new FRStep(data) : undefined;
+    },

54-57: Same for next(): unwrap the mutation result

Ensures network errors surface to callers.

-    next: async (step: Step, options?: StepOptions) => {
-      const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options }));
-      return data ? new FRStep(data) : undefined;
-    },
+    next: async (step: Step, options?: StepOptions) => {
+      const action = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options }));
+      const data = await action.unwrap();
+      return data ? new FRStep(data) : undefined;
+    },

54-57: Optional: support both call styles for next()

Back‑compat with { step, options } objects used in docs/samples.

-    next: async (step: Step, options?: StepOptions) => {
-      const action = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options }));
+    next: async (
+      arg1: Step | { step: Step; options?: StepOptions },
+      arg2?: StepOptions,
+    ) => {
+      const isObj = typeof arg1 === 'object' && arg1 !== null && 'step' in (arg1 as any);
+      const payload = isObj ? (arg1 as any).step as Step : (arg1 as Step);
+      const options = isObj ? (arg1 as any).options as StepOptions | undefined : arg2;
+      const action = await store.dispatch(journeyApi.endpoints.next.initiate({ step: payload, options }));
       const data = await action.unwrap();
       return data ? new FRStep(data) : undefined;
     },

19-30: Function need not be async

Currently no top‑level awaits; returning Promise may surprise callers. Consider making it sync (or keep async if you plan async init).

-export async function journey({
+export function journey({

8-13: Use type-only import for config types; storage import verified

  • Change the config types import to a type-only import.
  • '@forgerock/storage' already exports createStorage (packages/sdk-effects/storage/src/lib/storage.effects.ts); no change to that import.
-import { JourneyClientConfig, StepOptions } from './config.types.js';
+import type { JourneyClientConfig, StepOptions } from './config.types.js';
packages/journey-client/src/lib/fr-webauthn/script-parser.ts (2)

86-92: Guard required challenge parsing; throw DataError on missing/invalid.

Avoid JSON.parse on undefined; fail fast with a typed error.

Apply:

-  const challengeJSON = JSON.parse(challengeArr[2]);
+  if (!challengeArr[2]) {
+    const e = new Error('Missing or invalid challenge');
+    e.name = WebAuthnOutcomeType.DataError;
+    throw e;
+  }
+  const challengeJSON = JSON.parse(challengeArr[2]);

Also applies to: 171-178


1-1: Optional: remove file-wide eslint disable once regexes are fixed.

Prefer localized eslint disables only where necessary.

packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts (3)

11-23: Scope ESLint disable to the offending rule (max-len).

Don’t blanket-disable all ESLint rules. Limit to max-len (or remove if you reflow strings).

-// eslint-disable-next-line
+// eslint-disable-next-line max-len
-// eslint-disable-next-line
+// eslint-disable-next-line max-len
-// eslint-disable-next-line
+// eslint-disable-next-line max-len
-// eslint-disable-next-line
+// eslint-disable-next-line max-len
-// eslint-disable-next-line
+// eslint-disable-next-line max-len

13-22: Prefer Uint8Array for WebAuthn credential IDs.

IDs are raw bytes; Int8Array with negative values works but is misleading. Uint8Array better reflects the spec and avoids signed-number confusion in reviews and future maintenance.


12-25: Name the string fragments consistently with a Str suffix

Rename these exports in packages/journey-client/src/lib/fr-webauthn/helpers.mock.data.ts and update all usages in packages/journey-client/src/lib/fr-webauthn/helpers.test.ts (imports at lines 13–16; parseCredentials calls at lines 22, 28, 36, 42).

-export const allowCredentials70 =
+export const allowCredentials70Str =
 ...
-export const allowMultipleCredentials70 =
+export const allowMultipleCredentials70Str =
 ...
-export const acceptableCredentials653 =
+export const acceptableCredentials653Str =
 ...
-export const acceptableMultipleCredentials653 =
+export const acceptableMultipleCredentials653Str =
packages/journey-client/src/lib/fr-recovery-codes/index.ts (1)

62-69: Make display-step detection case-insensitive and simpler.

Current check duplicates cases and is fragile. Use a single case-insensitive test.

   private static getDisplayCallback(step: FRStep): TextOutputCallback | undefined {
     return step
       .getCallbacksOfType<TextOutputCallback>(callbackType.TextOutputCallback)
       .find((x) => {
-        const cb = x.getOutputByName<string | undefined>('message', undefined);
-        return cb && (cb.includes('Recovery Codes') || cb.includes('recovery codes'));
+        const msg = x.getOutputByName<string | undefined>('message', undefined);
+        return typeof msg === 'string' && /recovery codes/i.test(msg);
       });
   }

Note: If this message can be localized, consider a more reliable heuristic (metadata/flag) later.

packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts (1)

36-50: Extract inner <em> text robustly; current replace fails with attributes and greedy .*.

The regex can overmatch and the literal <em> replace won’t strip tags with attributes. Capture the inner text instead.

-function parseDeviceNameText(text: string): string {
+function parseDeviceNameText(text: string): string {
   /**
    * We default the device name to 'New Security Key'
    * If the user has a device name, it will be wrapped in <em> tags
    * e.g. ` ... <em>My Security Key</em> ... `
    * We want to remove the <em> tags and just return the device name
    * e.g. ` ... My Security Key ... `
    */
-  const displayName =
-    text
-      ?.match(/<em\s*.*>\s*.*<\/em>/g)?.[0]
-      ?.replace('<em>', '')
-      ?.replace('</em>', '') ?? 'New Security Key';
-  return displayName;
+  const m = text?.match(/<em\b[^>]*>([\s\S]*?)<\/em>/i);
+  return m?.[1]?.trim() || 'New Security Key';
 }

Please add a test with <em class="highlight">Device</em> and one with no <em> present.

Comment on lines +32 to +34
public getInputValue(selector: number | string = 0): unknown {
return this.getArrayElement(this.payload.input, selector).value;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Accept RegExp in getInputValue to match getArrayElement and usage

Signature/doc allow RegExp elsewhere but not here. Align them.

-  public getInputValue(selector: number | string = 0): unknown {
+  public getInputValue(selector: number | string | RegExp = 0): unknown {
     return this.getArrayElement(this.payload.input, selector).value;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public getInputValue(selector: number | string = 0): unknown {
return this.getArrayElement(this.payload.input, selector).value;
}
public getInputValue(selector: number | string | RegExp = 0): unknown {
return this.getArrayElement(this.payload.input, selector).value;
}
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/index.ts around lines 32 to 34,
getInputValue currently types the selector as number | string but should accept
RegExp to match getArrayElement and existing usages; update the method signature
to accept selector: number | string | RegExp (and adjust any related overloads
or imports if needed), update the doc comment to mention RegExp is supported,
and ensure the implementation still passes the selector through to
this.getArrayElement(this.payload.input, selector).value so callers using a
RegExp continue to compile and work.

Comment on lines +42 to +50
it('should allow getting policies', () => {
const cb = new PasswordCallback(payload);
expect(cb.getPolicies()).toEqual(['policy1', 'policy2']);
});

it('should allow getting failed policies', () => {
const cb = new PasswordCallback(payload);
expect(cb.getFailedPolicies()).toEqual(['failedPolicy1']);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Confirm policies on PasswordCallback are intentional (not just for ValidatedCreatePasswordCallback).

AM/ID Cloud typically returns policies/failedPolicies with ValidatedCreatePasswordCallback. If this API surface is unified across callbacks by design, all good; otherwise consider moving these assertions to the validated create password tests.


🌐 Web query:

Do ForgeRock AM/ID Cloud PasswordCallback payloads include "policies" and "failedPolicies", or are these fields specific to ValidatedCreatePasswordCallback?

💡 Result:

Short answer: policies and failedPolicies are part of the ValidatedCreatePasswordCallback (and other "AttributeInput"/Validated callbacks), not the simple PasswordCallback. Documentation shows PasswordCallback only returns a prompt (and echoOn), while ValidatedCreatePasswordCallback includes policies and failedPolicies. [1][2]

Sources:

  • PasswordCallback and ValidatedCreatePasswordCallback docs. [1]
  • Supported/interactive callbacks (ValidatedCreatePasswordCallback description showing policies/failedPolicies). [2]

Remove policies/failedPolicies assertions from PasswordCallback tests — they're for ValidatedCreatePasswordCallback.
policies/failedPolicies are part of ValidatedCreatePasswordCallback, not PasswordCallback; update packages/journey-client/src/lib/callbacks/password-callback.test.ts (lines 42–50) to remove these expectations or move them into the validated-create-password tests.

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/password-callback.test.ts around
lines 42 to 50, the test asserts policies and failedPolicies which belong to
ValidatedCreatePasswordCallback; remove the two test cases (or at minimum delete
the expect lines) that call getPolicies() and getFailedPolicies() from
PasswordCallback tests, and if coverage is needed move equivalent assertions
into the validated-create-password callback test file so PasswordCallback tests
only cover its actual API.

Comment on lines +24 to +38
public getConfig() {
const config = {
envId: this.getOutputByName<string>('envId', ''),
consoleLogEnabled: this.getOutputByName<boolean>('consoleLogEnabled', false),
deviceAttributesToIgnore: this.getOutputByName<string[]>('deviceAttributesToIgnore', []),
customHost: this.getOutputByName<string>('customHost', ''),
lazyMetadata: this.getOutputByName<boolean>('lazyMetadata', false),
behavioralDataCollection: this.getOutputByName<boolean>('behavioralDataCollection', true),
deviceKeyRsyncIntervals: this.getOutputByName<number>('deviceKeyRsyncIntervals', 14),
enableTrust: this.getOutputByName<boolean>('enableTrust', false),
disableTags: this.getOutputByName<boolean>('disableTags', false),
disableHub: this.getOutputByName<boolean>('disableHub', false),
};
return config;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Make envId required; don’t default to empty; default behavioral collection to false.

Avoid silently proceeding with an invalid envId, and don’t opt users into behavioral collection without an explicit server signal.

-  public getConfig() {
-    const config = {
-      envId: this.getOutputByName<string>('envId', ''),
+  public getConfig(): PingOneProtectInitConfig {
+    const envId = this.getOutputByName<string>('envId', '');
+    if (!envId) {
+      throw new Error('PingOneProtectInitializeCallback: "envId" is required');
+    }
+    const config: PingOneProtectInitConfig = {
+      envId,
       consoleLogEnabled: this.getOutputByName<boolean>('consoleLogEnabled', false),
       deviceAttributesToIgnore: this.getOutputByName<string[]>('deviceAttributesToIgnore', []),
       customHost: this.getOutputByName<string>('customHost', ''),
       lazyMetadata: this.getOutputByName<boolean>('lazyMetadata', false),
-      behavioralDataCollection: this.getOutputByName<boolean>('behavioralDataCollection', true),
+      behavioralDataCollection: this.getOutputByName<boolean>('behavioralDataCollection', false),
       deviceKeyRsyncIntervals: this.getOutputByName<number>('deviceKeyRsyncIntervals', 14),
       enableTrust: this.getOutputByName<boolean>('enableTrust', false),
       disableTags: this.getOutputByName<boolean>('disableTags', false),
       disableHub: this.getOutputByName<boolean>('disableHub', false),
     };
     return config;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public getConfig() {
const config = {
envId: this.getOutputByName<string>('envId', ''),
consoleLogEnabled: this.getOutputByName<boolean>('consoleLogEnabled', false),
deviceAttributesToIgnore: this.getOutputByName<string[]>('deviceAttributesToIgnore', []),
customHost: this.getOutputByName<string>('customHost', ''),
lazyMetadata: this.getOutputByName<boolean>('lazyMetadata', false),
behavioralDataCollection: this.getOutputByName<boolean>('behavioralDataCollection', true),
deviceKeyRsyncIntervals: this.getOutputByName<number>('deviceKeyRsyncIntervals', 14),
enableTrust: this.getOutputByName<boolean>('enableTrust', false),
disableTags: this.getOutputByName<boolean>('disableTags', false),
disableHub: this.getOutputByName<boolean>('disableHub', false),
};
return config;
}
public getConfig(): PingOneProtectInitConfig {
const envId = this.getOutputByName<string>('envId', '');
if (!envId) {
throw new Error('PingOneProtectInitializeCallback: "envId" is required');
}
const config: PingOneProtectInitConfig = {
envId,
consoleLogEnabled: this.getOutputByName<boolean>('consoleLogEnabled', false),
deviceAttributesToIgnore: this.getOutputByName<string[]>('deviceAttributesToIgnore', []),
customHost: this.getOutputByName<string>('customHost', ''),
lazyMetadata: this.getOutputByName<boolean>('lazyMetadata', false),
behavioralDataCollection: this.getOutputByName<boolean>('behavioralDataCollection', false),
deviceKeyRsyncIntervals: this.getOutputByName<number>('deviceKeyRsyncIntervals', 14),
enableTrust: this.getOutputByName<boolean>('enableTrust', false),
disableTags: this.getOutputByName<boolean>('disableTags', false),
disableHub: this.getOutputByName<boolean>('disableHub', false),
};
return config;
}
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts
around lines 24 to 38, make envId required and change the default for
behavioralDataCollection to false: remove the empty-string default when calling
this.getOutputByName for 'envId' (so the call returns undefined when missing),
add a runtime check after reading envId that throws an Error (or otherwise fails
initialization) if envId is falsy, and change the default argument for
'behavioralDataCollection' from true to false; keep other outputs as-is.

import { callbackType } from '@forgerock/sdk-types';

export const otpQRCodeStep = {
authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace JWT‑like authId literals to avoid secret scanner hits (Gitleaks).

Gitleaks flags these as generic API keys. Since this is mock data, use unmistakably fake placeholders to prevent false positives.

Apply this diff:

-  authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9',
+  authId: 'TEST_AUTH_ID',
...
-  authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9',
+  authId: 'TEST_AUTH_ID',

Also applies to: 104-104

🧰 Tools
🪛 Gitleaks (8.27.2)

[high] 14-14: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts around
lines 14 and 104, the mock authId values look like real JWTs and trigger
Gitleaks; replace those JWT‑like literals with clearly fake placeholders (e.g.,
"MOCK_AUTH_ID_PLACEHOLDER" or "fake-auth-id-12345") across both locations so
scanners won't flag them, and ensure the new strings cannot be mistaken for real
tokens.

Comment on lines +54 to +56
'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' +
'r=ForgeRock&period=30&digits=6&b=032b75',
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Sanitize PII/secrets in HiddenValue values.

These look like real user identifiers and secrets. Replace with neutral placeholders to avoid leakage and reduce scanner noise.

-            'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' +
-            'r=ForgeRock&period=30&digits=6&b=032b75',
+            'otpauth://totp/ForgeRock:exampleuser?secret=EXAMPLESECRET&issuer=ForgeRock&period=30&digits=6&b=032b75',
-            'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' +
-            '2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' +
-            '1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' +
-            '=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' +
-            '9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' +
-            'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' +
-            'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75',
+            'pushauth://push/forgerock:Example%20User?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' +
+            '2Nr&m=REGISTER:00000000-0000-0000-0000-000000000000&s=EXAMPLESIGNATURE' +
+            '&c=EXAMPLECIPHERTEXT&r=https://example.com/alpha/push/sns/message?_action=register' +
+            '&a=https://example.com/alpha/push/sns/message?_action=authenticate&b=032b75',

Also applies to: 150-156

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts around
lines 54-56 and also lines 150-156, the HiddenValue entries contain real-looking
user identifiers and OTP secrets; replace those sensitive values with neutral
placeholders (e.g.,
'otpauth://totp/ORG:username?secret=PLACEHOLDERSECRET&period=30&digits=6' or
similarly generic strings) so no real usernames, emails, or secrets remain, and
ensure the placeholders preserve format (scheme, query keys) so tests that
validate structure still pass.

Comment on lines +11 to +29
function parseDisplayRecoveryCodesText(text: string): string[] {
/**
* e.g. ` ...
* "<div class=\"text-center\">\n" +
* "iZmEtxvQ00\n" +
* "</div>\n" +
* ... `
*/

const recoveryCodesMatches = text.match(/\s[\w\W]"([\w]*)\\/g);
const recoveryCodes =
Array.isArray(recoveryCodesMatches) &&
recoveryCodesMatches.map((substr: string) => {
// e.g. `"iZmEtxvQ00\`
const arr = substr.match(/"([\w]*)\\/);
return Array.isArray(arr) ? arr[1] : '';
});
return recoveryCodes || [];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Regex likely misses codes; simplify and explicitly target "CODE\n" literals.

The current pattern requires an extra char before the opening quote and only looks for a backslash, which can fail to match "iZmEtxvQ00\n" lines. Extract codes by capturing alphanumerics before \n (optionally \r\n).

-function parseDisplayRecoveryCodesText(text: string): string[] {
+function parseDisplayRecoveryCodesText(text: string): string[] {
   /**
    * e.g. ` ...
    *    "<div class=\"text-center\">\n" +
    *    "iZmEtxvQ00\n" +
    *    "</div>\n" +
    * ... `
    */
-
-  const recoveryCodesMatches = text.match(/\s[\w\W]"([\w]*)\\/g);
-  const recoveryCodes =
-    Array.isArray(recoveryCodesMatches) &&
-    recoveryCodesMatches.map((substr: string) => {
-      // e.g. `"iZmEtxvQ00\`
-      const arr = substr.match(/"([\w]*)\\/);
-      return Array.isArray(arr) ? arr[1] : '';
-    });
-  return recoveryCodes || [];
+  const codes: string[] = [];
+  // Match lines like: "iZmEtxvQ00\n"
+  const re = /"([A-Za-z0-9]+)\\r?\\n"/g;
+  let m: RegExpExecArray | null;
+  while ((m = re.exec(text)) !== null) {
+    codes.push(m[1]);
+  }
+  return codes;
 }

Add/adjust tests to cover: multiple codes, CRLF (\r\n), and non-alphanumeric noise around.


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== locate function ==="
rg -n "function parseDisplayRecoveryCodesText" || true

echo
echo "=== show file (if exists) ==="
sed -n '1,240p' packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts || true

echo
echo "=== test regex behavior (node preferred, python fallback) ==="
if command -v node >/dev/null 2>&1; then
  node <<'NODE'
const samples = [
  '"iZmEtxvQ00\\n"',
  "\"iZmEtxvQ00\n\"",
  '\n"iZmEtxvQ00\\n"\n',
  ' "iZmEtxvQ00\\n"',
  '"iZmEtxvQ00\\r\\n"',
  '"iZmEtxvQ00\\n" +',
  "\"iZmEtxvQ00\\n\" +\n\"</div>\\n\"",
];
const orig = /\s[\w\W]"([\w]*)\\/g;
const news = /"([A-Za-z0-9]+)\\r?\\n"/g;
for (const s of samples) {
  console.log("SAMPLE:", JSON.stringify(s));
  const om = s.match(orig);
  console.log("orig match:", om);
  const found = [];
  let m;
  while ((m = news.exec(s)) !== null) found.push(m[1]);
  console.log("new match:", found);
  console.log("---");
}
NODE
else
  python3 - <<'PY'
import re
samples = [
    r'"iZmEtxvQ00\\n"',
    '"iZmEtxvQ00\n"',
    '\n"iZmEtxvQ00\\n"\n',
    ' "iZmEtxvQ00\\n"',
    r'"iZmEtxvQ00\\r\\n"',
    r'"iZmEtxvQ00\\n" +',
    '"iZmEtxvQ00\\n" +\n"</div>\\n"',
]
orig = re.compile(r'\s[\w\W]"([\w]*)\\')
new = re.compile(r'"([A-Za-z0-9]+)\\r?\\n"')
for s in samples:
    print("SAMPLE:", repr(s))
    print("orig match:", orig.findall(s))
    print("new match:", new.findall(s))
    print("---")
PY
fi

Length of output: 2398


Fix regex in parseDisplayRecoveryCodesText — match codes followed by escaped newline (CRLF optional).

Current patterns /\s[\w\W]"([\w])\/g and /"([\w])\/ miss the escaped newline and can fail to extract codes like "iZmEtxvQ00\n". Replace with a regex that captures alphanumerics before \n (optionally \r\n).

File: packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts (lines 11–29)

-function parseDisplayRecoveryCodesText(text: string): string[] {
+function parseDisplayRecoveryCodesText(text: string): string[] {
   /**
    * e.g. ` ...
    *    "<div class=\"text-center\">\n" +
    *    "iZmEtxvQ00\n" +
    *    "</div>\n" +
    * ... `
    */
 
-  const recoveryCodesMatches = text.match(/\s[\w\W]"([\w]*)\\/g);
-  const recoveryCodes =
-    Array.isArray(recoveryCodesMatches) &&
-    recoveryCodesMatches.map((substr: string) => {
-      // e.g. `"iZmEtxvQ00\`
-      const arr = substr.match(/"([\w]*)\\/);
-      return Array.isArray(arr) ? arr[1] : '';
-    });
-  return recoveryCodes || [];
+  const codes: string[] = [];
+  // Match lines like: "iZmEtxvQ00\n"
+  const re = /"([A-Za-z0-9]+)\\r?\\n"/g;
+  let m: RegExpExecArray | null;
+  while ((m = re.exec(text)) !== null) {
+    codes.push(m[1]);
+  }
+  return codes;
 }

Add tests for: multiple codes, CRLF (\r\n), and surrounding non-alphanumeric noise.

🤖 Prompt for AI Agents
packages/journey-client/src/lib/fr-recovery-codes/script-parser.ts around lines
11–29: the current regexes /\s[\w\W]"([\w]*)\\/g and /"([\w]*)\\/ fail to
require the escaped newline sequence and may miss codes like `"iZmEtxvQ00\n"`,
so replace both patterns with a single global regex that captures one or more
alphanumeric chars immediately before an escaped newline allowing optional CR
(e.g. /"([A-Za-z0-9]+)\\r?\\n/g), use it to extract all matches' capture group 1
into the recoveryCodes array (ensuring an empty array fallback), and add unit
tests that cover multiple codes, CRLF (`\r\n`), and surrounding non-alphanumeric
noise.

import { callbackType } from '@forgerock/sdk-types';

export const webAuthnRegJSCallback653 = {
authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Replace authId values with unmistakably fake placeholders to satisfy secret scanners.

These JWT-shaped strings trigger Gitleaks as generic API keys. Use a neutral placeholder consistently.

Apply this diff (repeat pattern for all occurrences shown in the selected ranges):

-  authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ',
+  authId: 'header.payload.signature.TEST_ONLY',

Run to ensure no JWT-like patterns remain:

Also applies to: 52-52, 90-90, 128-128, 166-166, 204-204, 241-241, 273-273, 310-310, 456-456


🏁 Script executed:

#!/usr/bin/env bash
rg -nP '(?i)\bauthId\s*:\s*[\'"]eyJ0eXAiOiJKV1Qi' -C0 || echo "OK: no JWT-looking authId values"

Length of output: 68


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

file='packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts'

if [[ -f "$file" ]]; then
  echo "Found: $file"
  echo "--- lines with 'authId' ---"
  rg -n -C0 'authId' "$file" || echo "No authId matches"
  echo "--- lines with JWT header prefix 'eyJ0eXAiOiJKV1Qi' ---"
  rg -n -C0 'eyJ0eXAiOiJKV1Qi' "$file" || echo "No JWT-prefix matches"
  echo "--- lines where authId value contains JWT prefix ---"
  rg -n -C0 "authId\\s*:\\s*['\"]eyJ0eXAiOiJKV1Qi" "$file" || echo "No combined matches"
else
  echo "File not found: $file"
  echo "Searching repository for occurrences..."
  rg -n --hidden --no-ignore 'authId' -S || true
  rg -n --hidden --no-ignore 'eyJ0eXAiOiJKV1Qi' -S || true
fi

Length of output: 6258


Replace JWT-looking authId values with unmistakably fake placeholders.

JWT-shaped strings trigger secret scanners (Gitleaks); replace every authId entry in the mock file.

File: packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts — replace authId at lines 14, 52, 90, 128, 166, 204, 241, 273, 310, 340–341, 386–387, 456.

Apply this diff (repeat pattern for all occurrences shown above):

-  authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ',
+  authId: 'header.payload.signature.TEST_ONLY',

Run to ensure no JWT-like tokens remain:

#!/usr/bin/env bash
set -euo pipefail
rg -n --hidden --no-ignore 'eyJ0eXAiOiJKV1Qi' || echo "OK: no JWT-looking tokens found"
🧰 Tools
🪛 Gitleaks (8.27.2)

[high] 14-14: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts around
lines 14, 52, 90, 128, 166, 204, 241, 273, 310, 340–341, 386–387, 456, replace
each authId value that looks like a JWT with a clearly fake non-JWT placeholder
(e.g. "REDACTED_AUTH_ID_1", incrementing or descriptive per entry) so no value
begins with the JWT header pattern; update all listed occurrences consistently
and preserve surrounding formatting/commas; then run the provided ripgrep check
(rg -n --hidden --no-ignore 'eyJ0eXAiOiJKV1Qi' || echo "OK: no JWT-looking
tokens found") to confirm no JWT-like tokens remain.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
codecov.yml (1)

32-53: Ignore globs look aggressive; confirm intent.

  • Ignoring all JSON (**/*.json) will exclude any source JSON imported via TS resolveJsonModule (if used).
  • Ignoring tools/**/* and scripts/**/* hides coverage for publish/runtime scripts if they’re shipped.
  • Redundant mocks patterns (**/*.mock* plus **/*.mock-data/*) can be consolidated.

Consider narrowing:

-  - '**/*.json'
+  - '**/coverage*.json'
+  - '**/tsconfig*.json'
+  - '**/typedoc.json'

…and only ignore tools/ or scripts/ if they’re never executed in production paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7acf08 and 9cc862e.

📒 Files selected for processing (1)
  • codecov.yml (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (5)
codecov.yml (5)

3-8: Requiring CI success before Codecov is fine; double‑check job ordering.

If Codecov upload runs in a matrix, ensure the upload step depends on all test jobs so require_ci_to_pass: true and notify.wait_for_ci: true don’t delay or suppress the PR comment.


27-31: Verify range syntax; Codecov expects two dots.

Docs use range: "70..100". The triple‑dot form may be ignored or parsed incorrectly.

Apply:

-  range: '70...100'
+  range: '70..100'

57-61: PR comment settings look good.

layout including flags and require_head: true are sensible defaults. Keeping require_changes: false avoids missing context on no‑diff PRs.


84-86: Confirm Codecov support for bundle_analysis.

If unsupported in your plan/app version, this block will be ignored or could cause config errors. Verify in a dry run or CI logs.


66-83: Flag rule over‑scopes: package-* currently maps to the whole packages/ tree — narrow it or rely on per-upload flags.

File: codecov.yml (lines 66–83). Mapping package-* to paths: - packages/ causes every package-... flag to be evaluated against the same fileset (packages/), diluting per-package signals; either remove the paths entry and rely on flagged uploads, or add explicit per-package paths. (docs.codecov.com)

Two options:

A) Trust flagged uploads (simplest):

   individual_flags:
-    - name: package-*
-      paths:
-        - packages/
+    - name: package-*
       statuses:
         - type: project
           target: 80%
           threshold: 2%
         - type: patch
           target: 90%
           threshold: 0%

B) Add explicit entry for the new package:

   individual_flags:
+    - name: package-journey-client
+      paths:
+        - packages/journey-client/**
+      statuses:
+        - type: project
+          target: 80%
+          threshold: 2%
+        - type: patch
+          target: 90%
+          threshold: 0%
     - name: package-*
-      paths:
-        - packages/
       statuses:
         - type: project
           target: 80%
           threshold: 2%
         - type: patch
           target: 90%
           threshold: 0%

Ensure CI uploads use matching flags (e.g. -F package-journey-client). (docs.codecov.com)

feat(journey-client): introduce robust tests and fix type errors

Overhauls the testing strategy for the new journey client by replacing
the heavily mocked original test file with a new suite that performs
more of an integration-style test against the Redux store.

- Rewrites tests to validate the full data flow, mocking only
  external boundaries (fetch, storage).
- Systematically debugs and resolves multiple layers of TypeScript
  errors by correcting mock data shapes to align with actual types.
- Introduces a GEMINI.md file to document architecture, development
  workflows, and troubleshooting steps for the package.
- Moves the getProp utility function from @forgerock/journey-client/src/lib/fr-policy/helpers.ts
to @forgerock/sdk-utilities/src/lib/object.utils.ts. This centralizes a generic
utility function, improving reusability and reducing code duplication across the SDK.
- Updates imports in journey-client to reflect the new location.
- Removed redundant cross-package references from `packages/journey-client/tsconfig.json`,
keeping only the references to `tsconfig.lib.json` and `tsconfig.spec.json`.
These references are already handled by the respective `tsconfig.json` files,
simplifying the configuration and avoiding potential conflicts.
- Inlined the export of `REQUESTED_WITH` and `X_REQUESTED_PLATFORM` constants
in `packages/sdk-utilities/src/lib/constants/index.ts`. This change
leverages TypeScript's literal types for improved type narrowing and
enables better tree-shaking during bundling, optimizing the SDK's footprint.
- Storing the plain `Step` payload instead of the `FRStep` instance in session storage.
- Updating type guards (`isStoredStep`) to correctly handle plain `Step` objects.
- Making the `redirect` method asynchronous and awaiting storage operations.
- Adding null/undefined checks for `RedirectCallback` in the `redirect` method.
- Ensuring `stepStorage.remove()` is awaited to prevent race conditions.
- Updating the `resume` method to correctly use the retrieved `Step` payload.
- Adding a new test case to verify `resume` functionality with plain `Step` objects from storage.
- Introduces new capabilities to the `@forgerock/journey-client` for handling advanced authentication methods:
- **WebAuthn:** Adds support for Web Authentication (WebAuthn) flows, including parsing WebAuthn registration and authentication options. This includes a fix for a TypeScript type incompatibility where `ParsedCredential.id` was narrowed to `ArrayBuffer` to align with the WebAuthn API's `BufferSource` expectation.
- **QR Code:** Implements functionality for QR code-based authentication flows.
- **Recovery Codes:** Adds support for managing and utilizing recovery codes.
- Increases project and patch coverage targets.
- Sets higher, more stringent targets for all packages.
- Requires successful CI builds for Codecov processing.
- Improves PR comment layout by including coverage flags.
- Expands the ignore list to exclude non-source directories.
@ryanbas21 ryanbas21 force-pushed the create-journey-package branch from 4301fa4 to b0f4368 Compare September 18, 2025 16:33
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (11)
packages/journey-client/src/lib/fr-webauthn/script-parser.ts (1)

49-56: Fix overbroad regex ([^]) and add non‑greedy quantifiers to prevent overmatching; resolves Biome errors.

These patterns can consume across sections and are flagged by Biome. Replace [^] with [\s\S] and prefer *? over {0,}+greedy. This also tightens bracketed captures.

-  const user = getIndexOne(text.match(/user"{0,}:\s{0,}{([^]{0,})},/)).trim();
+  const user = getIndexOne(text.match(/user"*:\s*{([\s\S]*?)},/)).trim();

-    text.match(/pubKeyCredParams"*:\s*\[([^]+\d\s*})\s*]/),
+    text.match(/pubKeyCredParams"*:\s*\[([\s\S]*?\d\s*})\s*]/),

-  const excludeCredentialsString = getIndexOne(
-    text.match(/excludeCredentials"{0,}:\s{0,}\[([^]+)\s{0,}]/),
-  ).trim();
+  const excludeCredentialsString = getIndexOne(
+    text.match(/excludeCredentials"*:\s*\[([\s\S]*?)\s*]/),
+  ).trim();

-    allowCredentialsText = getIndexOne(
-      text.match(/acceptableCredentials"*\s*=\s*\[([^]+)\s*]/),
-    ).trim();
+    allowCredentialsText = getIndexOne(
+      text.match(/acceptableCredentials"*\s*=\s*\[([\s\S]*?)\s*]/),
+    ).trim();

-    allowCredentialsText = getIndexOne(
-      text.match(/allowCredentials"{0,}:\s{0,}\[([^]+)\s{0,}]/),
-    ).trim();
+    allowCredentialsText = getIndexOne(
+      text.match(/allowCredentials"*:\s*\[([\s\S]*?)\s*]/),
+    ).trim();

Also applies to: 65-66, 79-81, 131-133, 139-141

codecov.yml (1)

16-21: Project target 70% will hard‑fail given current baseline 55.63%. Make project informational or base on auto.

As-is, every PR will fail the project check. Either relax the global gate or set it to auto and enforce via patch/flags.

Apply one of these diffs (Option A preferred):

 coverage:
   status:
     project:
       default:
-        target: 70%
-        threshold: 2% # Allow for a small drop in coverage
+        target: auto
+        threshold: 2%
+        informational: true
     patch:
       default:
-        target: 80% # New code should be well-tested
-        threshold: 0% # Do not allow new code to decrease coverage
+        target: 80%
+        threshold: 1%        # avoid rounding flake
+        only_pulls: true     # don't gate pushes

If you want to keep project gating, use Option B:

     project:
       default:
-        target: 70%
+        target: auto
         threshold: 2%
packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (3)

13-15: Replace JWT-like authId with unmistakably fake placeholder to silence Gitleaks.

These JWT-shaped literals trigger generic API key detections. Use a clear placeholder.

Apply this diff:

-  authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9',
+  authId: 'TEST_AUTH_ID',

Also applies to: 103-105


49-56: Sanitize mock OTP URI: remove real-looking username/secret and fix “issuer” split.

Replace personal identifiers and secrets with neutral placeholders and keep the format stable for tests.

-          value:
-            'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' +
-            'r=ForgeRock&period=30&digits=6&b=032b75',
+          value:
+            'otpauth://totp/ForgeRock:exampleuser?secret=EXAMPLESECRET&issuer=ForgeRock&period=30&digits=6&b=032b75',

145-156: Sanitize mock Push URI: remove real-looking name, UUID, signature, ciphertext, and external endpoints.

Use example.com and generic placeholders to avoid leaking PII/secrets and scanner noise.

-          value:
-            'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' +
-            '2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' +
-            '1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' +
-            '=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' +
-            '9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' +
-            'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' +
-            'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75',
+          value:
+            'pushauth://push/forgerock:Example%20User?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb2Nr' +
+            '&m=REGISTER:00000000-0000-0000-0000-000000000000&s=EXAMPLESIGNATURE' +
+            '&c=EXAMPLECIPHERTEXT&r=https://example.com/alpha/push/sns/message?_action=register' +
+            '&a=https://example.com/alpha/push/sns/message?_action=authenticate&b=032b75',
packages/journey-client/src/lib/journey.api.ts (1)

28-31: Bug: spreading undefined throws at runtime.

Use an empty object when tree is falsy and narrow to Record<string, string>.

-  const treeParams = tree ? { authIndexType: 'service', authIndexValue: tree } : undefined;
-  const params: Record<string, string | undefined> = { ...query, ...treeParams };
+  const params: Record<string, string> = {
+    ...query,
+    ...(tree ? { authIndexType: 'service', authIndexValue: tree } : {}),
+  };
packages/journey-client/src/lib/fr-device/index.ts (2)

199-224: Good fix: removed async Promise executor.

Executor is now sync; geolocation options use configured timeout. LGTM.


65-66: SSR-safe identifier + persistence hardening (fix crash + handle storage failures).

Directly referencing localStorage throws in SSR/non-DOM and some browsers can throw on getItem/setItem. Also the crypto check can be simplified. Use a safe storage probe, memoize an ephemeral ID, and wrap storage I/O in try/catch.

Apply this diff:

   private prefix: string;
+  private ephemeralId?: string;

   getIdentifier(): string {
-    const storageKey = `${this.prefix}-DeviceID`;
-
-    if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
-      FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.');
-      return '';
-    }
-    if (!localStorage) {
-      FRLogger.warn('Cannot store profile ID. localStorage is not supported.');
-      return '';
-    }
-    let id = localStorage.getItem(storageKey);
-    if (!id) {
-      // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532"
-      id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
-      localStorage.setItem(storageKey, id);
-    }
-    return id;
+    const storageKey = `${this.prefix}-DeviceID`;
+
+    // Prefer persisted ID when storage is available; otherwise use an ephemeral per-instance ID.
+    const storage: Storage | null =
+      typeof globalThis !== 'undefined' && 'localStorage' in globalThis
+        ? (globalThis.localStorage as Storage)
+        : null;
+
+    // Return memoized ephemeral ID if already generated
+    if (this.ephemeralId) return this.ephemeralId;
+
+    // Generate a new ID
+    const rng = globalThis.crypto?.getRandomValues?.bind(globalThis.crypto);
+    const newId =
+      rng?.(new Uint32Array(3)).join('-') ??
+      `${Date.now()}-${Math.random().toString(36).slice(2)}-${Math.random()
+        .toString(36)
+        .slice(2)}`;
+
+    if (!storage) {
+      FRLogger.warn('Cannot persist profile ID. localStorage is not available; using in-memory ID.');
+      this.ephemeralId = newId;
+      return this.ephemeralId;
+    }
+
+    try {
+      const existing = storage.getItem(storageKey);
+      if (existing) return existing;
+    } catch (e) {
+      FRLogger.warn('Error reading DeviceID from localStorage; using in-memory ID.', e as unknown);
+      this.ephemeralId = newId;
+      return this.ephemeralId;
+    }
+
+    try {
+      storage.setItem(storageKey, newId);
+      return newId;
+    } catch (e) {
+      FRLogger.warn('Error persisting DeviceID to localStorage; using in-memory ID.', e as unknown);
+      this.ephemeralId = newId;
+      return this.ephemeralId;
+    }
   }

Run to catch any other direct localStorage touches:

#!/bin/bash
rg -n -C2 '\blocalStorage\b' packages/journey-client | sed -n '1,200p'

Also applies to: 146-164

packages/journey-client/src/lib/callbacks/password-callback.ts (1)

24-26: Normalize failedPolicies: AM often returns JSON strings; parse to objects

Return consistent PolicyRequirement[] by parsing string elements; throw on malformed JSON.

-  public getFailedPolicies(): PolicyRequirement[] {
-    return this.getOutputByName<PolicyRequirement[]>('failedPolicies', []);
-  }
+  public getFailedPolicies(): PolicyRequirement[] {
+    const raw = this.getOutputByName<unknown>('failedPolicies', []);
+    const arr = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
+    try {
+      return arr
+        .filter((v) => v != null)
+        .map((v) => (typeof v === 'string' ? JSON.parse(v) : v)) as PolicyRequirement[];
+    } catch {
+      throw new Error(
+        'Unable to parse "failedPolicies" from the ForgeRock server. The JSON within `PasswordCallback` was either malformed or missing.',
+      );
+    }
+  }
packages/journey-client/src/lib/callbacks/confirmation-callback.ts (1)

59-64: Fix index validation: allow any integer within options length

Current logic only accepts 0/1 and fails with more options.

-  public setOptionIndex(index: number): void {
-    if (index !== 0 && index !== 1) {
-      throw new Error(`"${index}" is not a valid choice`);
-    }
-    this.setInputValue(index);
-  }
+  public setOptionIndex(index: number): void {
+    const options = this.getOptions();
+    if (!Number.isInteger(index) || index < 0 || index >= options.length) {
+      throw new Error(`"${index}" is not a valid choice (0..${Math.max(0, options.length - 1)})`);
+    }
+    this.setInputValue(index);
+  }
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1)

14-14: Replace JWT‑looking authId values with unambiguous test placeholders

These trigger secret scanners (Gitleaks). Use a neutral non‑JWT string.

Apply pattern across all occurrences:

-  authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 ... ',
+  authId: 'header.payload.signature.TEST_ONLY',

For multi‑line entries:

-  authId:
-    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....',
+  authId: 'header.payload.signature.TEST_ONLY',

Verification script:

#!/usr/bin/env bash
set -euo pipefail
# Fail if any JWT-like prefixes remain
rg -nP "(?i)\\bauthId\\s*:\\s*['\"]eyJ0eXAiOiJKV1Qi" -C0 && { echo "Found JWT-like authId"; exit 1; } || echo "OK: no JWT-looking authId values"
# Also ensure no bare JWT prefix remains anywhere
rg -nP "(?i)\\beyJ0eXAiOiJKV1Qi" -C0 || echo "OK: no JWT-looking tokens found"

Also applies to: 52-52, 90-90, 128-128, 166-166, 204-204, 241-241, 273-273, 310-310, 340-341, 386-387, 456-456

🧹 Nitpick comments (45)
.coderabbit.yaml (1)

4-7: Wire guideline files via knowledge_base.code_guidelines instead of a non‑schema 'guidelines' key.

The commented example suggests guidelines under reviews, which isn’t a defined property. If you intend to enforce team guidelines, add them under knowledge_base.code_guidelines.filePatterns (and keep your comments accordingly).

Apply this diff to enable it:

 reviews:
   profile: chill
   poem: false
   # You can specify coding guideline documents here.
   # CodeRabbit will scan these files to understand your team’s standards.
-  # guidelines:
-  #   - "**/GEMINI.md"
+
+knowledge_base:
+  code_guidelines:
+    enabled: true
+    filePatterns:
+      - "**/GEMINI.md"
.changeset/orange-peaches-warn.md (2)

5-11: Make the changeset consumer‑facing; drop internal/test details and keep scope clear.

Recommend tightening wording and avoiding implementation/testing chatter. Proposed edit:

-feat(journey-client): Add WebAuthn, QR Code, and Recovery Code support
-
-- Introduces new utility modules (`FRWebAuthn`, `FRQRCode`, `FRRecoveryCodes`) to handle advanced authentication methods within authentication journeys.
-- Adds comprehensive parsing and handling for WebAuthn registration and authentication steps, including a fix for a type error where `TextOutputCallback` was being incorrectly inferred as `TextInputCallback`.
-- Implements support for displaying QR codes (for both OTP and Push) and for displaying and using recovery codes.
-- Includes extensive unit tests for the new callback types and utility modules to ensure correctness.
-- Updates documentation to reflect the new capabilities and architectural changes.
+Add WebAuthn, QR Code, and Recovery Code support.
+
+- New utilities: `FRWebAuthn`, `FRQRCode`, and `FRRecoveryCodes` for advanced authentication in journeys.
+- WebAuthn: registration and authentication for platform and cross‑platform authenticators.
+- QR codes: render and parse OTP and Push QR flows.
+- Recovery codes: display and redeem recovery codes.
+- Fix: correct callback type inference (`TextOutputCallback` no longer inferred as `TextInputCallback`).
+- Docs updated to cover the new capabilities.

12-12: Remove stray trailing line.

There’s an orphaned “12” at EOF; likely an annotation artifact. Please delete.

-12
packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (2)

36-36: Assert against the source of truth constant

Use the exported enum-like constant instead of a string literal for resilience.

-expect(cb.getType()).toBe('NameCallback');
+expect(cb.getType()).toBe(callbackType.NameCallback);

4-4: File name nit: align with subject under test

Consider renaming to “fr-callback.test.ts” to match FRCallback and improve discoverability.

packages/journey-client/src/lib/fr-webauthn/script-parser.ts (4)

179-181: Tighten rpId extraction; current pattern is awkward and lacks a closing quote.

\\{0,}" is odd and there’s no terminal ". Anchor to a closing quote and normalize quantifiers.

-  const rpId = getIndexOne(text.match(/rpId"{0,}:\s{0,}\\{0,}"([^"\\]*)/));
+  const rpId = getIndexOne(text.match(/rpId"*:\s*"([^"]*)"/));

147-166: Reuse parseCredentials for allowCredentials to avoid brittle split/map logic.

The current split('},') approach is fragile with whitespace/newlines. parseCredentials already handles this format.

-  if (allowCredentialsText) {
-    // Splitting objects in array in case the user has multiple keys
-    const allowCredentialArr = allowCredentialsText.split('},') || [allowCredentialsText];
-    // Iterating over array of substrings
-    allowCredentials = allowCredentialArr.map((str) => {
-      // e.g. `{ \"type\": \"public-key\",
-      const type = getIndexOne(str.match(/type"{0,}:\s{0,}"([\w-]+)"/)) as 'public-key';
-      // e.g. \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer
-      const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/));
-      // e.g. `[-107, 93, 68, -67, ... -19, 7, 4]`
-      const idJSON = JSON.parse(idArr[2]);
-      // e.g. [-107, 93, 68, -67, ... -19, 7, 4]
-      const id = new Int8Array(idJSON).buffer;
-
-      return {
-        type,
-        id,
-      };
-    });
-  }
+  if (allowCredentialsText) {
+    allowCredentials = parseCredentials(allowCredentialsText);
+  }

122-125: Add explicit types for allowCredentials variables.

Improves readability and catches accidental shape mismatches at compile time.

-function parseWebAuthnAuthenticateText(text: string): PublicKeyCredentialRequestOptions {
-  let allowCredentials;
-  let allowCredentialsText;
+function parseWebAuthnAuthenticateText(text: string): PublicKeyCredentialRequestOptions {
+  let allowCredentials: PublicKeyCredentialDescriptor[] | undefined;
+  let allowCredentialsText = '';

19-36: Replace {0,} with * across patterns (you already noted this TODO).

This simplifies expressions and avoids readability issues. Apply consistently to all occurrences (attestation, timeout, userVerification, requireResidentKey, authenticatorAttachment, etc.).

Example:

-  const attestation = getIndexOne(text.match(/attestation"{0,}:\s{0,}"(\w+)"/)) as AttestationType;
+  const attestation = getIndexOne(text.match(/attestation"*:\s*"(\w+)"/)) as AttestationType;
codecov.yml (3)

27-31: Range 70...100 skews visualization; consider full 0...100.

This only affects the color scale, but 0...100 gives a truer view while baseline improves.

-  range: '70...100'
+  range: '0...100'

32-53: Ignore set is broadly fine; tighten JSON ignore to avoid accidental source exclusion.

Ignoring all “**/*.json” can hide JSON-based source artifacts if any exist. Prefer explicit config files you already list.

-  - '**/*.json'

55-61: Consider delaying PR comment until parallel uploads finish.

If CI uploads multiple reports (matrix), set after_n_builds to reduce noisy interim comments.

 comment:
   layout: 'header, diff, flags, files'
   behavior: default
   require_changes: false # Post a comment even if there are no coverage changes
   require_head: true # Only post a comment if there is a coverage report for the head commit
+  after_n_builds: 2
packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts (3)

16-59: Isolate test state: avoid shared mutable payload across tests

The two tests mutate the same payload object by reference. Create a factory for a fresh payload per test to prevent inter-test coupling.

-  const payload: Callback = {
-    type: callbackType.ValidatedCreateUsernameCallback,
-    output: [
-      {
-        name: 'echoOn',
-        value: false,
-      },
-      {
-        name: 'required',
-        value: true,
-      },
-      {
-        name: 'policies',
-        value: {
-          policyRequirements: ['a', 'b'],
-          name: 'username',
-          policies: [],
-        },
-      },
-      {
-        name: 'failedPolicies',
-        value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })],
-      },
-      {
-        name: 'validateOnly',
-        value: false,
-      },
-      {
-        name: 'prompt',
-        value: 'Username',
-      },
-    ],
-    input: [
-      {
-        name: 'IDToken2',
-        value: '',
-      },
-      {
-        name: 'IDToken2validateOnly',
-        value: false,
-      },
-    ],
-    _id: 1,
-  };
+  const makePayload = (): Callback => ({
+    type: callbackType.ValidatedCreateUsernameCallback,
+    output: [
+      { name: 'echoOn', value: false },
+      { name: 'required', value: true },
+      {
+        name: 'policies',
+        value: {
+          policyRequirements: ['a', 'b'],
+          name: 'username',
+          policies: [],
+        },
+      },
+      { name: 'failedPolicies', value: [JSON.stringify({ failedPolicies: { c: 'c', d: 'd' } })] },
+      { name: 'validateOnly', value: false },
+      { name: 'prompt', value: 'Username' },
+    ],
+    input: [
+      { name: 'IDToken2', value: '' },
+      { name: 'IDToken2validateOnly', value: false },
+    ],
+    _id: 1,
+  });
@@
-    const cb = new ValidatedCreateUsernameCallback(payload);
+    const cb = new ValidatedCreateUsernameCallback(makePayload());
@@
-    const cb = new ValidatedCreateUsernameCallback(payload);
+    const cb = new ValidatedCreateUsernameCallback(makePayload());

Also applies to: 62-62, 77-77


66-66: Prefer constant over string literal for type assertion

Use the exported constant to avoid drift.

-    expect(cb.getType()).toBe('ValidatedCreateUsernameCallback');
+    expect(cb.getType()).toBe(callbackType.ValidatedCreateUsernameCallback);

72-73: Make assertions resilient to input ordering

Find inputs by name instead of relying on index positions.

-    expect(cb.payload.input[0].value).toBe('abcd123');
-    expect(cb.payload.input[1].value).toBe(true);
+    expect(cb.payload.input?.find((i) => i.name === 'IDToken2')?.value).toBe('abcd123');
+    expect(cb.payload.input?.find((i) => i.name === 'IDToken2validateOnly')?.value).toBe(true);
@@
-    expect(cb.payload.input[1].value).toBe(false);
+    expect(cb.payload.input?.find((i) => i.name === 'IDToken2validateOnly')?.value).toBe(false);

Also applies to: 80-80

packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (3)

32-41: Optional: simplify/sanitize QRCodeReader text for OTP snippet as well.

Not functionally used, but it embeds the same identifiers/secrets. Consider replacing with a minimal placeholder payload to prevent future scanner hits.

-          value:
-            `window.QRCodeReader.createCode({\n    id: 'callback_0',\n    text: 'otpauth\\x3A\\x` +
-            `2F\\x2Ftotp\\x2FForgeRock\\x3Ajlowery\\x3Fperiod\\x3D30\\x26b\\x3D032b75\\x26` +
-            `digits\\x3D6\\x26secret\\QITSTC234FRIU8DD987DW3VPICFY\\x3D\\x3D\\x3D\\x3D\\x3` +
-            `D\\x',\n  3D\\x26issuer\\x3DForgeRock  version: '20',\n    code: 'L'\n});`,
+          value:
+            "window.QRCodeReader.createCode({ id: 'callback_0', text: 'otpauth://totp/ForgeRock:exampleuser?secret=EXAMPLESECRET&issuer=ForgeRock&period=30&digits=6&b=032b75', version: '20', code: 'L' });",

122-137: Optional: sanitize QRCodeReader text for Push snippet.

Mirror the HiddenValue replacements here to keep messages consistent and scanner-safe.

-          value:
-            `window.QRCodeReader.createCode({\n    id: 'callback_0',\n    text: 'pushauth\\x` +
-            `3A\\x2F\\x2Fpush\\x2Fforgerock\\x3AJustin\\x2520Lowery\\x3Fa\\x3DaHR0cHM6Ly9vc` +
-            `GVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXN` +
-            `oL3Nucy9tZXNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl\\x26r\\x3DaHR0cHM6Ly9vcGVuYW0tZm` +
-            `9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9t` +
-            `ZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI\\x26b\\x3D032b75\\x26s\\x3DFoxEr5uAzrys1yBmuyg` +
-            `PbxrVjysElmzsmqifi6eO_AI\\x26c\\x3DXD\\x2DMxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5` +
-            `kjgw\\x26l\\x3DYW1sYmNvb2tpZT0wMQ\\x26m\\x3DREGISTER\\x3A53b85112\\x2D8ba9\\x2` +
-            `D4b7e\\x2D9107\\x2Decbca2d65f7b1695151603616\\x26issuer\\x3DRm9yZ2VSb2Nr',\n  ` +
-            `  version: '20',\n    code: 'L'\n});`,
+          value:
+            "window.QRCodeReader.createCode({ id: 'callback_0', text: 'pushauth://push/forgerock:Example%20User?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb2Nr&m=REGISTER:00000000-0000-0000-0000-000000000000&s=EXAMPLESIGNATURE&c=EXAMPLECIPHERTEXT&r=https://example.com/alpha/push/sns/message?_action=register&a=https://example.com/alpha/push/sns/message?_action=authenticate&b=032b75', version: '20', code: 'L' });",

76-79: Minor: unify messageType value types.

You mix string ('0','4') and number (0). Consider using a consistent type across callbacks to prevent brittle assertions.

Also applies to: 116-118, 139-141

packages/journey-client/src/lib/journey.api.ts (7)

21-21: Remove leftover TODO-style comment.

This in-code guidance should be dropped.

-// Move these functions to the top, before journeyApi definition

40-46: Minor: avoid instantiating Headers you immediately override.

Let baseQuery/prepareHeaders manage headers.

   const init: RequestInit = {
     body: step ? JSON.stringify(step) : undefined,
     credentials: 'include',
-    headers: new Headers({}),
     method: 'POST',
   };

79-81: Prefer returning an RTK Query error over throwing.

Throwing inside queryFn can bypass RTKQ’s error typing/serialization.

-        if (!config.serverConfig) {
-          throw new Error('Server configuration is missing.');
-        }
+        if (!config.serverConfig) {
+          return {
+            error: { status: 'CUSTOM_ERROR', data: 'Server configuration is missing.' } as unknown as FetchBaseQueryError,
+          };
+        }

84-86: Merge global (config) and per-call query params.

Preserve defaults from config.query, allow call-level overrides.

-        const query = options?.query || {};
+        const { config: cfg } = api.extra as Extras;
+        const query = { ...(cfg.query ?? {}), ...(options?.query ?? {}) };

109-112: Same as start(): return a typed error instead of throwing.

-        if (!config.serverConfig) {
-          throw new Error('Server configuration is missing.');
-        }
+        if (!config.serverConfig) {
+          return {
+            error: { status: 'CUSTOM_ERROR', data: 'Server configuration is missing.' } as unknown as FetchBaseQueryError,
+          };
+        }

113-116: Same as start(): merge config.query with options?.query.

-        const query = options?.query || {};
+        const { config: cfg } = api.extra as Extras;
+        const query = { ...(cfg.query ?? {}), ...(options?.query ?? {}) };

51-55: Make requestMiddleware optional to match .applyMiddleware signature.

API already accepts undefined; avoid forcing callers to pass [].

 interface Extras {
-  requestMiddleware: RequestMiddleware[];
+  requestMiddleware?: RequestMiddleware[];
   logger: ReturnType<typeof loggerFn>;
   config: JourneyClientConfig;
 }
packages/journey-client/src/lib/fr-device/index.ts (2)

90-99: Robust plugin enumeration (avoid relying on enumerable keys).

Object.keys(navigator.plugins) is brittle; iterate the PluginArray instead and fall back to name when filename is absent.

   getBrowserPluginsNames(): string {
-    if (!(typeof navigator !== 'undefined' && navigator.plugins)) {
+    if (typeof navigator === 'undefined' || !navigator.plugins) {
       FRLogger.warn('Cannot collect browser plugin information. navigator.plugins is not defined.');
       return '';
     }
-    return reduceToString(
-      Object.keys(navigator.plugins),
-      navigator.plugins as unknown as Record<string, { filename: string }>,
-    );
+    const plugins = Array.from(navigator.plugins);
+    return plugins.map((p) => p.filename || p.name).filter(Boolean).join(';');
   }

135-139: Correct log message context.

This path reads hardware props; the warning says “OS metadata” and “Navigator” capitalized.

-    if (typeof navigator === 'undefined') {
-      FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.');
+    if (typeof navigator === 'undefined') {
+      FRLogger.warn('Cannot collect hardware metadata. navigator is not defined.');
packages/journey-client/src/lib/fr-policy/message-creator.ts (7)

16-27: Polish wording + fix double space.

Tighten phrasing and remove the extra space in the duplicates message.

Apply this diff:

-    return `${property} must not contain following characters: "${forbiddenChars}"`;
+    return `${property} must not contain the following characters: "${forbiddenChars}"`;
@@
-    return `${property}  must not contain duplicates: "${duplicateValue}"`;
+    return `${property} must not contain duplicates: "${duplicateValue}"`;
@@
-    return `${property} must not contain: "${disallowedFields}"`;
+    return `${property} must not contain the following: "${disallowedFields}"`;

28-35: Avoid odd “0”/empty-parameter messages; add sane fallbacks.

If params are missing or 0, messages like “at least 0” or empty quotes read oddly. Use defensive defaults.

Apply this diff:

   [PolicyKey.LeastCapitalLetters]: (property: string, params?: { numCaps?: number }) => {
-    const numCaps = getProp<number>(params, 'numCaps', 0);
-    return `${property} must contain at least ${numCaps} capital ${plural(numCaps, 'letter')}`;
+    const numCaps = getProp<number | undefined>(params, 'numCaps', undefined);
+    const minCaps = typeof numCaps === 'number' && numCaps > 0 ? numCaps : 1;
+    return `${property} must contain at least ${minCaps} capital ${plural(minCaps, 'letter')}`;
   },
   [PolicyKey.LeastNumbers]: (property: string, params?: { numNums?: number }) => {
-    const numNums = getProp<number>(params, 'numNums', 0);
-    return `${property} must contain at least ${numNums} numeric ${plural(numNums, 'value')}`;
+    const numNums = getProp<number | undefined>(params, 'numNums', undefined);
+    const minNums = typeof numNums === 'number' && numNums > 0 ? numNums : 1;
+    return `${property} must contain at least ${minNums} ${plural(minNums, 'digit')}`;
   },
@@
   [PolicyKey.MaximumLength]: (property: string, params?: { maxLength?: number }) => {
-    const maxLength = getProp<number>(params, 'maxLength', 0);
-    return `${property} must be at most ${maxLength} ${plural(maxLength, 'character')}`;
+    const maxLength = getProp<number | undefined>(params, 'maxLength', undefined);
+    return typeof maxLength === 'number' && maxLength > 0
+      ? `${property} must be at most ${maxLength} ${plural(maxLength, 'character')}`
+      : `${property} must not exceed the allowed length`;
   },
@@
   [PolicyKey.MinimumLength]: (property: string, params?: { minLength?: number }) => {
-    const minLength = getProp<number>(params, 'minLength', 0);
-    return `${property} must be at least ${minLength} ${plural(minLength, 'character')}`;
+    const minLength = getProp<number | undefined>(params, 'minLength', undefined);
+    return typeof minLength === 'number' && minLength > 0
+      ? `${property} must be at least ${minLength} ${plural(minLength, 'character')}`
+      : `${property} must meet the minimum length requirement`;
   },

Also applies to: 37-40, 43-46


41-48: Include thresholds for number policies when available.

Provide clearer guidance by reflecting min/max values from params.

Apply this diff:

-  [PolicyKey.MaximumNumber]: (property: string) =>
-    `${property} has failed the "MAXIMUM_NUMBER_VALUE" policy`,
+  [PolicyKey.MaximumNumber]: (property: string, params?: { maximum?: number }) => {
+    const maximum = getProp<number | undefined>(params, 'maximum', undefined);
+    return typeof maximum === 'number'
+      ? `${property} must be at most ${maximum}`
+      : `${property} exceeds the maximum allowed value`;
+  },
@@
-  [PolicyKey.MinimumNumber]: (property: string) =>
-    `${property} has failed the "MINIMUM_NUMBER_VALUE" policy`,
+  [PolicyKey.MinimumNumber]: (property: string, params?: { minimum?: number }) => {
+    const minimum = getProp<number | undefined>(params, 'minimum', undefined);
+    return typeof minimum === 'number'
+      ? `${property} must be at least ${minimum}`
+      : `${property} is below the minimum allowed value`;
+  },

36-36: Prefer user-friendly messages over policy identifiers.

Replace “has failed the ... policy” with actionable phrasing.

Apply this diff:

-  [PolicyKey.MatchRegexp]: (property: string) => `${property} has failed the "MATCH_REGEXP" policy`,
+  [PolicyKey.MatchRegexp]: (property: string) => `${property} must match the required pattern`,
@@
-  [PolicyKey.ValidArrayItems]: (property: string) =>
-    `${property} has failed the "VALID_ARRAY_ITEMS" policy`,
+  [PolicyKey.ValidArrayItems]: (property: string) => `${property} contains invalid items`,
@@
-  [PolicyKey.ValidQueryFilter]: (property: string) =>
-    `${property} has failed the "VALID_QUERY_FILTER" policy`,
-  [PolicyKey.ValidType]: (property: string) => `${property} has failed the "VALID_TYPE" policy`,
+  [PolicyKey.ValidQueryFilter]: (property: string) => `${property} must be a valid query filter`,
+  [PolicyKey.ValidType]: (property: string) => `${property} has an invalid type`,

Also applies to: 55-56, 62-64


58-61: Minor wording tweaks for consistency.

Use “must be a valid …” to align with other messages.

Apply this diff:

-  [PolicyKey.ValidNumber]: (property: string) => `${property} has an invalid number`,
-  [PolicyKey.ValidPhoneFormat]: (property: string) => `${property} has an invalid phone number`,
+  [PolicyKey.ValidNumber]: (property: string) => `${property} must be a valid number`,
+  [PolicyKey.ValidPhoneFormat]: (property: string) => `${property} must be a valid phone number`,

15-65: Consider freezing the exported map to prevent accidental runtime mutation.

Optional hardening so consumers don’t mutate the default map.

Apply this diff:

-export default defaultMessageCreator;
+export default Object.freeze(defaultMessageCreator) as unknown as MessageCreator;

16-27: Optional: More graceful fallbacks when list params are empty.

If forbiddenChars/duplicateValue/disallowedFields are missing, avoid empty quotes and show a generic message.

Apply this diff:

-    const forbiddenChars = getProp<string>(params, 'forbiddenChars', '');
-    return `${property} must not contain the following characters: "${forbiddenChars}"`;
+    const forbiddenChars = getProp<string | undefined>(params, 'forbiddenChars', undefined);
+    return forbiddenChars && forbiddenChars.length > 0
+      ? `${property} must not contain the following characters: "${forbiddenChars}"`
+      : `${property} must not contain illegal characters`;
@@
-    const duplicateValue = getProp<string>(params, 'duplicateValue', '');
-    return `${property} must not contain duplicates: "${duplicateValue}"`;
+    const duplicateValue = getProp<string | undefined>(params, 'duplicateValue', undefined);
+    return duplicateValue && duplicateValue.length > 0
+      ? `${property} must not contain the duplicate value: "${duplicateValue}"`
+      : `${property} must not contain duplicate values`;
@@
-    const disallowedFields = getProp<string>(params, 'disallowedFields', '');
-    return `${property} must not contain the following: "${disallowedFields}"`;
+    const disallowedFields = getProp<string | undefined>(params, 'disallowedFields', undefined);
+    return disallowedFields && disallowedFields.length > 0
+      ? `${property} must not contain the following: "${disallowedFields}"`
+      : `${property} must not contain disallowed fields`;
packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts (1)

33-41: Consider adding edge-case assertions

Add a case for non-display steps (e.g., getCodes/getDeviceName behavior) to lock in expected output for unsupported steps.

packages/journey-client/src/lib/callbacks/name-callback.ts (1)

28-33: Optional: symmetry helper getName()

A tiny getter mirroring setName() can improve ergonomics in UIs.

   /**
    * Sets the username.
    */
   public setName(name: string): void {
     this.setInputValue(name);
   }
+
+  /**
+   * Gets the username.
+   */
+  public getName(): string {
+    return this.getInputValue<string>('');
+  }
packages/journey-client/src/lib/callbacks/confirmation-callback.test.ts (1)

76-90: Make error expectations less brittle

Asserting exact strings is fragile; match on a stable substring/regex instead.

-    expect(() => cb.setOptionIndex(2)).toThrow('"2" is not a valid choice');
+    expect(() => cb.setOptionIndex(2)).toThrow(/not a valid choice/);
@@
-    expect(() => cb.setOptionValue('Maybe')).toThrow('"Maybe" is not a valid choice');
+    expect(() => cb.setOptionValue('Maybe')).toThrow(/not a valid choice/);
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (2)

61-67: Remove debug logging from tests

The console.log will spam CI output.

-    console.log('the step type', stepType, WebAuthnStepType.Authentication);

40-51: Clarify intent of commented-out 6.5.3 metadata tests

If unsupported, drop these. If supported, add the 6.5.3 metadata mocks and enable.

packages/journey-client/src/lib/callbacks/metadata-callback.ts (1)

24-26: Optional: avoid shape-lying default for generic return

Returning {} as T can mask type issues. Prefer undefined default and a union.

-  public getData<T>(): T {
-    return this.getOutputByName<T>('data', {} as T);
-  }
+  public getData<T = unknown>(): T | undefined {
+    return this.getOutputByName<T | undefined>('data', undefined);
+  }
packages/journey-client/src/lib/callbacks/factory.ts (1)

89-91: Optional: dev-only warning on unknown callback types

Helpful during integration without affecting prod.

-    default:
-      return new FRCallback(callback);
+    default: {
+      if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
+        // eslint-disable-next-line no-console
+        console.warn('[journey-client] Unhandled callback type:', callback.type);
+      }
+      return new FRCallback(callback);
+    }
packages/journey-client/src/lib/fr-step.ts (4)

47-57: API/doc mismatch: method says “first callback” but requires exactly one

Either rename or return the first when multiple exist. Recommend returning the first.

-  public getCallbackOfType<T extends FRCallback>(type: CallbackType): T {
-    const callbacks = this.getCallbacksOfType<T>(type);
-    if (callbacks.length !== 1) {
-      throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`);
-    }
-    return callbacks[0];
-  }
+  public getCallbackOfType<T extends FRCallback>(type: CallbackType): T {
+    const callbacks = this.getCallbacksOfType<T>(type);
+    if (callbacks.length < 1) {
+      throw new Error(`Expected at least 1 callback of type "${type}", but found 0`);
+    }
+    return callbacks[0];
+  }

69-80: Same mismatch in setter: set “first” callback but enforces exactly one

Align with “first” semantics.

-  public setCallbackValue(type: CallbackType, value: unknown): void {
-    const callbacks = this.getCallbacksOfType(type);
-    if (callbacks.length !== 1) {
-      throw new Error(`Expected 1 callback of type "${type}", but found ${callbacks.length}`);
-    }
-    callbacks[0].setInputValue(value);
-  }
+  public setCallbackValue(type: CallbackType, value: unknown): void {
+    const callbacks = this.getCallbacksOfType(type);
+    if (callbacks.length < 1) {
+      throw new Error(`Expected at least 1 callback of type "${type}", but found 0`);
+    }
+    callbacks[0].setInputValue(value);
+  }

103-111: Avoid double factory invocation; simplify callback conversion

Prefer a single resolved factory for clarity and micro‑perf.

-  private convertCallbacks(
-    callbacks: Callback[],
-    callbackFactory?: FRCallbackFactory,
-  ): FRCallback[] {
-    const converted = callbacks.map((x: Callback) => {
-      // This gives preference to the provided factory and falls back to our default implementation
-      return (callbackFactory || createCallback)(x) || createCallback(x);
-    });
-    return converted;
-  }
+  private convertCallbacks(
+    callbacks: Callback[],
+    callbackFactory?: FRCallbackFactory,
+  ): FRCallback[] {
+    const factory = callbackFactory ?? createCallback;
+    return callbacks.map((x) => factory(x));
+  }

35-36: Nit: grammar in docstring

“an implementation” (not “am”).

-   * @param callbackFactory A function that returns am implementation of FRCallback
+   * @param callbackFactory A function that returns an implementation of FRCallback

codecov.yml Outdated
Comment on lines 62 to 76
# Configure how flags are managed. This allows for different coverage
# targets for different parts of the codebase.
flag_management:
default_rules:
# Carry forward coverage from previous commits for all flags.
carryforward: true
statuses:
- type: project
target: 40%
- type: patch
target: 40%
individual_flags:
# This rule applies to all flags that start with "package-".
# It is expected that the CI script uploads coverage reports with flags
# like "package-davinci-client", "package-oidc-client", etc.
# This makes the configuration future-proof for new packages.
- name: package-*
paths:
- packages/*/
carryforward: true
- packages/ # Only consider files in the packages directory for these flags
statuses:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Flag strategy: good foundation; add only_pulls and verify CI uploads with matching flags.

Statuses under flags can be restricted to PRs, and CI must pass “-F package-xyz”.

   individual_flags:
     - name: package-*
       paths:
         - packages/ # Only consider files in the packages directory for these flags
       statuses:
         - type: project
           target: 80% # Higher target for packages
           threshold: 2%
+          only_pulls: true
         - type: patch
           target: 90% # New code in packages should be very well-tested
-          threshold: 0%
+          threshold: 1%
+          only_pulls: true

Run this to confirm flags are uploaded in workflows:


🏁 Script executed:

#!/bin/bash
# Verify Codecov uploads include flags (-F) for packages
rg -nC2 -iP 'codecov|upload-coverage' -g '!**/node_modules/**' -g '!**/dist/**' . \
| rg -nC1 -iP '\-F\s*package\-'

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== 1) Search repo for Codecov uploads / actions / bash uploader ==="
rg -nC3 -S -g '!**/node_modules/**' -g '!**/dist/**' 'codecov|codecov.io|codecov-action|curl -s https://codecov.io/bash|bash <\(curl -s https://codecov.io/bash\)|upload-coverage|upload_coverage' || true

echo
echo "=== 2) Search for -F flags (literal -F usage) ==="
rg -nC3 -S -g '!**/node_modules/**' -g '!**/dist/**' '\-F' || true

echo
echo "=== 3) Search for 'flags:' input (e.g. in GH Actions) ==="
rg -nC3 -S -g '!**/node_modules/**' -g '!**/dist/**' '\bflags\s*:' || true

echo
echo "=== 4) Search for occurrences of 'package-' (package flag hint) ==="
rg -nC3 -S -g '!**/node_modules/**' -g '!**/dist/**' 'package-' || true

echo
echo "=== 5) List .github/workflows files (first 200 lines each) ==="
if [ -d .github/workflows ]; then
  for f in .github/workflows/*.{yml,yaml}; do
    [ -e "$f" ] || continue
    echo "---- $f ----"
    sed -n '1,200p' "$f"
    echo
  done
else
  echo "No .github/workflows directory found"
fi

echo "=== DONE ==="

Length of output: 20224


Flag strategy: add only_pulls and make CI upload with package flags (-F)

  • Add only_pulls: true to the package-* statuses in codecov.yml (individual_flags) and adjust the patch threshold to 1% as suggested. (file: codecov.yml — ~lines 62–76)
  • Update .github/workflows/ci.yml and .github/workflows/publish.yml: codecov/codecov-action@v5 is used but does not pass flags — change the upload step to include a flags input or invoke the uploader per-package (e.g., flags: package- or pass -F package-) so Codecov maps coverage to the package-* flags.
  • Verified: workflows contain codecov action calls with files/token but no flags; CI must be changed before relying on the package-* rules.
🤖 Prompt for AI Agents
In codecov.yml around lines 62 to 76, the package-* individual_flag lacks
only_pulls and an appropriate patch threshold; add "only_pulls: true" under the
package-* statuses and set the patch threshold to 1% for those flags, and then
update .github/workflows/ci.yml and .github/workflows/publish.yml to pass
per-package flags when uploading coverage (either use the codecov action's
"flags" input per package or run the uploader with -F package-<name> / invoke
the action once per package) so Codecov maps coverage to the package-* flags.

Comment on lines 56 to 63
config: BaseProfileConfig = {
fontNames,
devicePlatforms,
displayProps,
browserProps,
hardwareProps,
platformProps,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Privacy/compliance: gate fingerprinting surfaces behind opt‑in flags.

Persistent ID, fonts, plugins, timezone, and location are fingerprinting surfaces. Add explicit, configurable consent flags (safe defaults false) and honor them in getProfile.

Apply these changes here (types added below):

   config: BaseProfileConfig = {
     fontNames,
     devicePlatforms,
     displayProps,
     browserProps,
     hardwareProps,
     platformProps,
+    privacy: {
+      persistId: false,
+      collectFonts: false,
+      collectPlugins: false,
+      collectTimezone: false,
+      collectLocation: false,
+    },
   };
@@
   async getProfile({ location, metadata }: CollectParameters): Promise<DeviceProfileData> {
-    const profile: DeviceProfileData = {
-      identifier: this.getIdentifier(),
-    };
+    const { privacy = {} } = this.config;
+    const profile: DeviceProfileData = {
+      // Consider omitting identifier entirely when !privacy.persistId, or keep ephemeral value
+      identifier: this.getIdentifier(),
+    };
@@
-        browser: {
-          ...this.getBrowserMeta(),
-          plugins: this.getBrowserPluginsNames(),
-        },
+        browser: {
+          ...this.getBrowserMeta(),
+          plugins: privacy.collectPlugins ? this.getBrowserPluginsNames() : '',
+        },
@@
-          fonts: this.getInstalledFonts(),
-          timezone: this.getTimezoneOffset(),
+          fonts: privacy.collectFonts ? this.getInstalledFonts() : '',
+          timezone: privacy.collectTimezone ? this.getTimezoneOffset() : null,
         },
       };
     }
-    if (location) {
-      profile.location = await this.getLocationCoordinates();
-    }
+    if (location && privacy.collectLocation) {
+      profile.location = await this.getLocationCoordinates();
+    }
     return profile;
   }

Follow-up: also update getIdentifier() to skip persistence when privacy.persistId !== true (return the ephemeral ID from the earlier patch instead of writing to storage). I can provide that diff once the interface below is accepted.

Types to add in interfaces.ts (outside this file):

export interface PrivacyFlags {
  persistId?: boolean;
  collectFonts?: boolean;
  collectPlugins?: boolean;
  collectTimezone?: boolean;
  collectLocation?: boolean;
}

export interface BaseProfileConfig {
  // existing fields...
  privacy?: PrivacyFlags;
}

Do you want me to push a follow-up patch and adjust tests/docs accordingly?

Also applies to: 237-264

Comment on lines +1 to +184
name: 'optionType',
value: -1,
},
{
name: 'defaultOption',
value: 0,
},
],
input: [
{
name: 'IDToken4',
value: 0,
},
],
},
],
};

export const pushQRCodeStep = {
authId: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9',
callbacks: [
{
type: callbackType.TextOutputCallback,
output: [
{
name: 'message',
value:
'Scan the QR code image below with the ForgeRock Authenticator app to register ' +
'your device with your login.',
},
{
name: 'messageType',
value: '0',
},
],
},
{
type: callbackType.TextOutputCallback,
output: [
{
name: 'message',
value:
`window.QRCodeReader.createCode({\n id: 'callback_0',\n text: 'pushauth\\x` +
`3A\\x2F\\x2Fpush\\x2Fforgerock\\x3AJustin\\x2520Lowery\\x3Fa\\x3DaHR0cHM6Ly9vc` +
`GVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXN` +
`oL3Nucy9tZXNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl\\x26r\\x3DaHR0cHM6Ly9vcGVuYW0tZm` +
`9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9t` +
`ZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI\\x26b\\x3D032b75\\x26s\\x3DFoxEr5uAzrys1yBmuyg` +
`PbxrVjysElmzsmqifi6eO_AI\\x26c\\x3DXD\\x2DMxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5` +
`kjgw\\x26l\\x3DYW1sYmNvb2tpZT0wMQ\\x26m\\x3DREGISTER\\x3A53b85112\\x2D8ba9\\x2` +
`D4b7e\\x2D9107\\x2Decbca2d65f7b1695151603616\\x26issuer\\x3DRm9yZ2VSb2Nr',\n ` +
` version: '20',\n code: 'L'\n});`,
},
{
name: 'messageType',
value: '4',
},
],
},
{
type: callbackType.HiddenValueCallback,
output: [
{
name: 'value',
value:
'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' +
'2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' +
'1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' +
'=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' +
'9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' +
'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' +
'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75',
},
{
name: 'id',
value: 'mfaDeviceRegistration',
},
],
input: [
{
name: 'IDToken3',
value: 'mfaDeviceRegistration',
},
],
},
{
type: callbackType.PollingWaitCallback,
output: [
{
name: 'waitTime',
value: '5000',
},
{
name: 'message',
value: 'Waiting for response...',
},
],
},
],
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Verification: scan for residual secrets/PII and brittle test expectations.

Run the following to confirm no more scanner hits and no tests rely on the old literals.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for old identifiers/tokens…"
rg -nP -C0 'jlowery|Justin%20Lowery|FoxEr5uAzrys|openam-forgerock-sdks|53b85112-8ba9-4b7e-9107-ecbca2d65f7b|QITSTC234FRIU8DD987DW3VPICFY|aHR0cHM6Ly9vcGVuYW0' -- packages || true

echo "Run Gitleaks if available…"
if command -v gitleaks >/dev/null 2>&1; then
  gitleaks detect --no-git -v --redact || true
else
  echo "gitleaks not installed in CI sandbox."
fi

echo "Check tests referencing exact strings…"
rg -nP -C2 'ForgeRock:exampleuser|EXAMPLESECRET|Example%20User|EXAMPLESIGNATURE|EXAMPLECIPHERTEXT' -- packages || true

Length of output: 9905


Residual secrets/PII found in mocks/tests — redact and update brittle tests

  • Scanner hits present in:

    • packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts
    • packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts
    • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts
    • e2e/mock-api-v2/src/responses/token/token.ts (gitleaks: JWTs)
    • packages/davinci-client/src/lib/mock-data/davinci.success.mock.ts
    • packages/davinci-client/src/lib/node.reducer.test.ts
  • Actions required:

    • Remove/redact literal secrets/JWTs/API keys in mocks; replace with clearly synthetic placeholders (EXAMPLE_* or REDACTED_*) or derive fixtures at runtime.
    • Update tests to avoid exact-token equality (use structure checks, regex, or partial assertions instead of full URI/string matches).
    • Re-run gitleaks/rg/CI scanners and confirm zero findings before merging.
🧰 Tools
🪛 Gitleaks (8.27.2)

[high] 14-14: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 104-104: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts lines
1-184, the mock contains literal secrets/PII (JWTs, otpauth/pushauth URIs and
secrets); redact those values and replace them with synthetic placeholders
(e.g., EXAMPLE_JWT, REDACTED_OTPAUTH_URI, EXAMPLE_SECRET) or generate them at
test runtime, update any callback IDs to non-sensitive deterministic
placeholders, and ensure no real credentials remain; then update related tests
to avoid exact-token equality by asserting structure/fields with regex or
partial matches instead of full string equality, run gitleaks/CI scanners
locally and confirm zero findings before committing.

@@ -0,0 +1,131 @@
import type { StepOptions } from '@forgerock/sdk-types';
import type { ServerConfig } from '@forgerock/sdk-types';
import { type logger as loggerFn } from '@forgerock/sdk-logger';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix: value import needed for logger (current import breaks ReturnType<typeof …>).

import type erases the value; typeof loggerFn won’t resolve. Import the value instead.

-import { type logger as loggerFn } from '@forgerock/sdk-logger';
+import { logger as loggerFn } from '@forgerock/sdk-logger';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { type logger as loggerFn } from '@forgerock/sdk-logger';
import { logger as loggerFn } from '@forgerock/sdk-logger';
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey.api.ts around line 3, the import uses
"import type" which erases the runtime value and breaks uses like
ReturnType<typeof loggerFn>; change the import to a value import (remove the
"type" keyword) so the logger symbol is available at runtime; keep the same
identifier (loggerFn) so existing typeof usages resolve correctly and update any
ESLint/TS imports if they complain about unused types.

@ryanbas21 ryanbas21 force-pushed the create-journey-package branch from ae760ce to 703e47f Compare September 18, 2025 17:20
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
codecov.yml (2)

21-21: Patch threshold 0% is brittle.

Round‑off can cause noisy fails; use 1%.


16-21: Global 70% gate will hard‑fail CI at current 55.63% baseline — relax or base on baseline.

This repeats a prior concern: keep project gating informational or base it on baseline to unblock PRs. Also avoid 0% patch threshold brittleness.

Apply Option A (recommended — informational project status + PR‑only patch gate):

 coverage:
   status:
     project:
       default:
-        target: 70%
-        threshold: 2% # Allow for a small drop in coverage
+        target: auto
+        threshold: 2% # small allowed variance
+        informational: true
+        only_pulls: true
     patch:
       default:
-        target: 80% # New code should be well-tested
-        threshold: 0% # Do not allow new code to decrease coverage
+        target: 80% # New code should be well-tested
+        threshold: 1% # avoid rounding brittleness
+        only_pulls: true
🧹 Nitpick comments (4)
packages/davinci-client/vite.config.ts (1)

49-49: Flattening reporters drops previous skipEmpty behavior; consider preserving it (at least for text).

If you relied on skipping empty files in coverage output, this change removes it. To keep signal/noise the same, restore per-reporter options for the text reporter.

Apply this diff:

-      reporter: ['text', 'html', 'json'],
+      reporter: [
+        ['text', { skipEmpty: true }],
+        'html',
+        'json',
+      ],
codecov.yml (2)

28-31: Optional: widen UI range to reflect true baseline.

Consider 0...100 to avoid compressing visuals below 70%.

-  range: '70...100'
+  range: '0...100'

32-53: Ignore patterns: consider narrowing the global JSON ignore.

Blanket '**/*.json' can unexpectedly exclude source‑adjacent JSON assets; narrow to known configs or explicitly keep src JSON if used.

-  - '**/*.json'
+  # Keep common configs ignored but avoid blanket JSON exclusion
+  - '**/package.json'
+  - '**/tsconfig*.json'
+  - '**/typedoc.json'
+  - '**/eslint*.json'
+  - '**/vitest*.json'
+  # If JSON under src is intentionally not counted, keep as-is; otherwise avoid ignoring src/**/*.json
.github/workflows/ci.yml (1)

64-68: Optional: enable GitHub OIDC for Codecov uploads and remove the token

Codecov Action v5 supports tokenless uploads via GitHub OIDC. To switch:

  • Add workflow/job permission:
    permissions:
    id-token: write

  • Replace the action inputs (remove token, add use_oidc):

    • uses: codecov/codecov-action@v5
      with:
      directory: ./packages/
      use_oidc: true

Any explicit token is ignored when use_oidc: true. Confirm your Codecov org’s Global Upload Token / token requirement (public-repo behavior) before deleting the CODECOV_TOKEN secret. File: .github/workflows/ci.yml (around lines 64–68).

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b0f4368 and ae760ce.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml (1 hunks)
  • codecov.yml (2 hunks)
  • packages/davinci-client/vite.config.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (7)
packages/davinci-client/vite.config.ts (1)

52-52: LGTM on const-asserted provider.

Harmless and consistent with typed config; no action needed.

codecov.yml (5)

3-5: CI synchronization settings LGTM.

require_ci_to_pass and wait_for_ci are sensible defaults.

Also applies to: 7-8


23-26: Parser setting LGTM.

include_full_missed_files: true is helpful for catching untracked files.


55-60: PR comment settings LGTM.

Layout and require_head/require_changes look good.


82-84: Bundle analysis set to informational — good.

Non‑blocking signal is appropriate.


62-81: Limit component checks to PRs and soften patch threshold to 1%

Add only_pulls: true to component statuses and change patch threshold 0% → 1% to reduce flakiness; repo search found no codecov-action calls or explicit flags usage — confirm CI uploads use Codecov Action v5 (components via paths) before merging.

 component_management:
   default_rules:
     carryforward: true
   individual_components:
     - component_id: package-*
       paths:
         - packages/ # Only consider files in the packages directory for these components
       statuses:
         - type: project
           target: 80% # Higher target for packages
           threshold: 2%
+          only_pulls: true
         - type: patch
           target: 90% # New code in packages should be very well-tested
-          threshold: 0%
+          threshold: 1%
+          only_pulls: true
.github/workflows/ci.yml (1)

64-68: Codecov: verify coverage files are under ./packages/ and harden the upload step

Quick check found zero coverage artifacts under packages/ (Count: 0). Ensure reports are generated under packages/ or remove the directory: override so Codecov can auto-discover; also upload on failure and fail fast on upload errors.

File: .github/workflows/ci.yml Lines: 64-68

Apply:

-      - uses: codecov/codecov-action@v5
+      - uses: codecov/codecov-action@v5
+        if: always()
         with:
           directory: ./packages/
           token: ${{ secrets.CODECOV_TOKEN }}
+          fail_ci_if_error: true
+          verbose: true

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
codecov.yml (1)

16-21: Project 70% target will keep failing (baseline ~55.63%); switch to target: auto (and scope to PRs).

This resurrects the earlier gating issue noted previously; recommend basing the check on the PR base, not a fixed 70%. Also make patch threshold 1% to avoid rounding brittleness.

Apply:

 coverage:
   status:
     project:
       default:
-        target: 70%
-        threshold: 2% # Allow for a small drop in coverage
+        target: auto
+        threshold: 2% # allow small wiggle while raising the floor over time
+        only_pulls: true
     patch:
       default:
-        target: 80% # New code should be well-tested
-        threshold: 0% # Do not allow new code to decrease coverage
+        target: 80% # new code should be well-tested
+        threshold: 1% # 0% is brittle due to rounding
+        only_pulls: true

Refs: Codecov status config supports target: auto and only_pulls. (docs.codecov.com)

🧹 Nitpick comments (2)
codecov.yml (2)

32-53: Over-broad ignores; avoid blanket JSON ignore and redundant test patterns.

  • '**/.json' may exclude app JSON modules when resolveJsonModule is enabled; prefer targeted ignores you already list (tsconfig., typedoc.json, etc.).
  • '/tests//.ts' is redundant with '.test.ts'/'*.spec.ts'.

Apply:

 ignore:
   - '**/dist/**/*'
   - '**/*.mock*'
   - '**/*.mock-data/*'
   - '**/*.data.*'
   - '**/node_modules/**/*'
   - '**/*.d.ts'
-  - '**/tests/**/*.ts'
   - '**/*.test.ts'
   - '**/*.spec.ts'
   - '**/vitest.config.*'
   - '**/tsconfig.*.json'
   - '**/tsconfig.json'
   - '**/typedoc.json'
-  - '**/*.md'
+  - '**/*.md'
   - '**/LICENSE'
-  - '**/*.json'
   - 'e2e/**/*' # E2E tests are not unit tests
   - 'tools/**/*' # Tooling scripts
   - 'scripts/**/*' # Other scripts

71-81: Tighten component statuses: PR‑only and less brittle patch threshold.

Add only_pulls to avoid non‑PR noise and make patch threshold 1%.

       statuses:
         - type: project
           target: 80% # Higher target for packages
           threshold: 2%
+          only_pulls: true
         - type: patch
           target: 90% # New code in packages should be very well-tested
-          threshold: 0%
+          threshold: 1%
+          only_pulls: true

Refs: Status checks support only_pulls; 0% thresholds are prone to rounding flakes. (docs.codecov.com)

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae760ce and 703e47f.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml (1 hunks)
  • codecov.yml (2 hunks)
  • packages/davinci-client/vite.config.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/davinci-client/vite.config.ts
  • .github/workflows/ci.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (5)
codecov.yml (5)

3-5: Good: gate Codecov on successful CI.

require_ci_to_pass and wait_for_ci are sane defaults; keeps noise out of PRs.

Also applies to: 7-8


23-26: Include full missed files: good choice.

This helps surface gaps when files aren’t touched by tests.


27-31: UI tuning looks fine.

range/round/precision are standard; no action.


55-61: PR comment settings look good.

layout + require_head keep comments relevant; require_changes=false is fine during rollout.


82-84: Bundle Analysis: informational is a safe default.

Consider setting a warning_threshold later if you want proactive alerts. Config shape matches docs.

Refs: Bundle Analysis YAML supports status: "informational". (docs.codecov.com)

Comment on lines +62 to +66
component_management:
default_rules:
# Carry forward coverage from previous commits for all components.
carryforward: true
statuses:
- type: project
target: 40%
- type: patch
target: 40%
individual_flags:
- name: package-*
individual_components:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

carryforward under component_management is likely ignored; move carryforward to flags or drop it.

Docs show component_management supports paths/flag_regexes/statuses; carryforward is a Flags concept. If you need carryforward, configure it under flag_management and upload with flags, or remove here.

Suggested removal:

 component_management:
   default_rules:
-    # Carry forward coverage from previous commits for all components.
-    carryforward: true

If you do want carryforward, introduce flags:

flag_management:
  default_rules:
    carryforward: true
    statuses:
      - type: project
        target: auto
        threshold: 2%
      - type: patch
        target: 90%
        threshold: 1%

Refs: Components don’t require upload-time flags and their statuses mirror flag rules but don’t accept flags; carryforward is documented with flags. (docs.codecov.com)

Run a quick YAML validation:


🏁 Script executed:

#!/bin/bash
# Validate current codecov.yml against Codecov API
curl -sS https://api.codecov.io/validate \
  -H "Content-Type: text/plain" \
  --data-binary @codecov.yml | jq .

Length of output: 56


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---- codecov.yml (first 240 lines) ----"
if [ ! -f codecov.yml ]; then
  echo "codecov.yml not found"
  exit 0
fi
nl -ba codecov.yml | sed -n '1,240p'

echo
echo "---- occurrences of 'carryforward' ----"
rg -n "carryforward" -S || true

echo
echo "---- occurrences of 'component_management' ----"
rg -n "component_management" -S || true

echo
echo "---- show component_management context (if present) ----"
cm_line=$(rg -n "component_management" -S | head -n1 | cut -d: -f1 || true)
if [ -n "$cm_line" ]; then
  echo "component_management appears at line: $cm_line"
  nl -ba codecov.yml | sed -n "$((cm_line-3)),$((cm_line+40))p"
fi

Length of output: 80


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ ! -f codecov.yml ]; then
  echo "codecov.yml not found"
  exit 0
fi

echo "---- codecov.yml (first 240 lines with numbers) ----"
awk '{printf "%5d: %s\n", NR, $0}' codecov.yml | sed -n '1,240p' || true

echo
echo "---- search for 'carryforward' and 'component_management' (with context) ----"
if command -v rg >/dev/null 2>&1; then
  rg -n -C3 -S "carryforward|component_management" codecov.yml || true
else
  grep -n -C3 -E "carryforward|component_management" codecov.yml || true
fi

echo
echo "---- component_management block (first match, +200 lines) ----"
if command -v rg >/dev/null 2>&1; then
  cm_match=$(rg -n "^\s*component_management\s*:" -S codecov.yml | head -n1 || true)
else
  cm_match=$(grep -n "^\s*component_management\s*:" codecov.yml | head -n1 || true)
fi

if [ -n "${cm_match:-}" ]; then
  line=$(echo "$cm_match" | cut -d: -f1)
  start=$((line-3)); if [ "$start" -lt 1 ]; then start=1; fi
  end=$((line+200))
  awk "NR>=$start && NR<=$end {printf \"%5d: %s\n\", NR, \$0}" codecov.yml || true
fi

Length of output: 5205


Move/remove carryforward from component_management — it's ignored there.

carryforward is a Flags/flag_management feature; component_management.default_rules accepts paths/flag_regexes/statuses and does not support carryforward. (docs.codecov.com)

Action: remove this key or move it to flag_management.default_rules (or per-flag under flags). Location: codecov.yml lines 62–65.

🤖 Prompt for AI Agents
In codecov.yml around lines 62 to 66, the component_management.default_rules
block contains a carryforward key which is invalid there; remove the
carryforward entry from component_management.default_rules and either place it
under flag_management.default_rules or under individual flag entries in flags
(or omit it entirely) so that carryforward is defined in the
Flags/flag_management section where Codecov supports it.

Copy link
Collaborator

@cerebrl cerebrl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good. I've seen quite a bit of drift regarding our naming schemes for files. I've shared an image showing the difference in file names. Can we start working on aligning these? I know the Journey Client is very unique as it's mostly legacy files being moved over, but at least the top-level files should be consistent with the newer patterns.

CleanShot 2025-09-22 at 16 12 44

"version": "0.0.1",
"private": true,
"type": "module",
"exports": {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are we exporting the new modules? I thought we'd need to add them here, like for QR Code, WebAuthn, etc?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (21)
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts (2)

41-49: Validate HiddenValueCallback before casting.

The code casts the result to boolean unnecessarily and should validate that getQRCodeURICb returns a valid callback before proceeding.

   public static isQRCodeStep(step: JourneyStep): boolean {
     const hiddenValueCb = step.getCallbacksOfType(callbackType.HiddenValueCallback);

     // QR Codes step should have at least one HiddenValueCallback
     if (hiddenValueCb.length === 0) {
       return false;
     }
-    return !!this.getQRCodeURICb(hiddenValueCb);
+    return this.getQRCodeURICb(hiddenValueCb) !== undefined;
   }

83-94: Improve return type specificity and null safety.

The method should have a more specific return type and handle potential null values better.

-  private static getQRCodeURICb(hiddenValueCbs: HiddenValueCallback[]) {
+  private static getQRCodeURICb(hiddenValueCbs: HiddenValueCallback[]): HiddenValueCallback | undefined {
     // Look for a HiddenValueCallback with an OTP URI
     return hiddenValueCbs.find((cb) => {
       const outputValue = cb.getOutputValue('value');

       if (typeof outputValue === 'string') {
-        return outputValue?.includes('otpauth://') || outputValue?.includes('pushauth://');
+        return outputValue.includes('otpauth://') || outputValue.includes('pushauth://');
       }
       return false;
     });
   }
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (3)

42-56: Consider extracting expected values to constants.

The hardcoded expected values make the test brittle and harder to maintain. Consider extracting them to constants or deriving them from the mock data.

+  const EXPECTED_OTP_MESSAGE = 
+    'Scan the QR code image below with the ForgeRock Authenticator app to register your ' +
+    'device with your login.';
+  const EXPECTED_OTP_URI = 
+    'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' +
+    'r=ForgeRock&period=30&digits=6&b=032b75';

   it('should return an object with OTP QR Code data', () => {
     const expected = {
-      message:
-        'Scan the QR code image below with the ForgeRock Authenticator app to register your ' +
-        'device with your login.',
+      message: EXPECTED_OTP_MESSAGE,
       use: 'otp',
-      uri:
-        'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' +
-        'r=ForgeRock&period=30&digits=6&b=032b75',
+      uri: EXPECTED_OTP_URI,
     };

58-77: Apply similar constant extraction for Push QR code test.

Similar to the OTP test, the Push QR code test would benefit from extracting the expected values to constants.

+  const EXPECTED_PUSH_MESSAGE = 
+    'Scan the QR code image below with the ForgeRock Authenticator app to register ' +
+    'your device with your login.';
+  const EXPECTED_PUSH_URI = 
+    'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' +
+    '2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' +
+    '1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' +
+    '=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' +
+    '9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' +
+    'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' +
+    'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75';

   it('should return an object with Push QR Code data', () => {
     const expected = {
-      message:
-        'Scan the QR code image below with the ForgeRock Authenticator app to register ' +
-        'your device with your login.',
+      message: EXPECTED_PUSH_MESSAGE,
       use: 'push',
-      uri:
-        'pushauth://push/forgerock:Justin%20Lowery?l=YW1sYmNvb2tpZT0wMQ&issuer=Rm9yZ2VSb' +
-        '2Nr&m=REGISTER:53b85112-8ba9-4b7e-9107-ecbca2d65f7b1695151603616&s=FoxEr5uAzrys' +
-        '1yBmuygPbxrVjysElmzsmqifi6eO_AI&c=XD-MxsK2sRGa7sUw7kinSKoUDf_eNYMZUV2f0z5kjgw&r' +
-        '=aHR0cHM6Ly9vcGVuYW0tZm9yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi' +
-        '9hbHBoYS9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI&a=aHR0cHM6Ly9vcGVuYW0tZm9' +
-        'yZ2Vycm9jay1zZGtzLmZvcmdlYmxvY2tzLmNvbTo0NDMvYW0vanNvbi9hbHBoYS9wdXNoL3Nucy9tZ' +
-        'XNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&b=032b75',
+      uri: EXPECTED_PUSH_URI,
     };

11-16: Add error handling test cases.

The tests are missing coverage for error conditions, particularly testing the behavior when getQRCodeData is called on a non-QR code step.

 import JourneyStep from '../journey-step.js';
 import FRQRCode from './fr-qrcode.js';
 import { otpQRCodeStep, pushQRCodeStep } from './fr-qr-code.mock.data.js';
 // import WebAuthn step as it's similar in structure for testing non-QR Code steps
 import { webAuthnRegJSCallback70 } from '../fr-webauthn/fr-webauthn.mock.data.js';
+import { expect } from 'vitest';

 describe('Class for managing QR Codes', () => {
+  describe('error handling', () => {
+    it('should throw error when getQRCodeData is called on non-QR step', () => {
+      const step = new JourneyStep(webAuthnRegJSCallback70);
+      expect(() => FRQRCode.getQRCodeData(step)).toThrow(
+        'QR Code step must contain a HiddenValueCallback with otpauth:// or pushauth:// URI. Use `FRQRCode.isQRCodeStep` to guard.'
+      );
+    });
+  });
packages/journey-client/src/lib/callbacks/redirect-callback.ts (1)

11-24: Docstring references “choice” instead of redirect.
Nit, but reading this comment is confusing when scanning the callback list.

- * Represents a callback used to collect an answer to a choice.
+ * Represents a callback used to handle redirect instructions.
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (1)

61-66: Drop stray console logging in tests.
Keeps test output clean and avoids masking real diagnostics.

-    const stepType = FRWebAuthn.getWebAuthnStepType(step);
-    console.log('the step type', stepType, WebAuthnStepType.Authentication);
-    expect(stepType).toBe(WebAuthnStepType.Authentication);
+    const stepType = FRWebAuthn.getWebAuthnStepType(step);
+    expect(stepType).toBe(WebAuthnStepType.Authentication);
packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1)

46-48: Consider adding input validation for the profile parameter.

While the current implementation is functional, adding basic validation could help catch integration issues early.

 public setProfile(profile: DeviceProfileData): void {
+  if (!profile) {
+    throw new Error('Profile data is required');
+  }
   this.setInputValue(JSON.stringify(profile));
 }
packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (1)

38-41: Potential issue with invalid date handling.

The new Date() constructor will create an invalid Date object if the input string is malformed, but this won't throw an error - it will silently return a Date object with Invalid Date as its string representation.

 public getCreateDate(): Date {
   const date = this.getOutputByName<string>('createDate', '');
+  const parsedDate = new Date(date);
+  if (isNaN(parsedDate.getTime())) {
+    throw new Error('Invalid date format in createDate field');
+  }
-  return new Date(date);
+  return parsedDate;
 }
packages/journey-client/src/lib/callbacks/factory.ts (1)

34-34: Consider making the type alias more descriptive.

The type alias could benefit from more descriptive naming to clarify its purpose in the codebase.

-type JourneyCallbackFactory = (callback: Callback) => JourneyCallback;
+type CallbackFactoryFunction = (callback: Callback) => JourneyCallback;

Or even better:

-type JourneyCallbackFactory = (callback: Callback) => JourneyCallback;
+type CallbackInstantiator = (callback: Callback) => JourneyCallback;
packages/journey-client/src/lib/journey-step.ts (1)

103-113: Robust fallback logic in convertCallbacks.

The conversion logic properly handles the case where a custom factory might return undefined and falls back to the default factory. However, the logic (callbackFactory || createCallback)(x) || createCallback(x) could be simplified.

 private convertCallbacks(
   callbacks: Callback[],
   callbackFactory?: JourneyCallbackFactory,
 ): JourneyCallback[] {
   const converted = callbacks.map((x: Callback) => {
-    // This gives preference to the provided factory and falls back to our default implementation
-    return (callbackFactory || createCallback)(x) || createCallback(x);
+    // Use provided factory or fall back to default
+    const factory = callbackFactory || createCallback;
+    return factory(x);
   });
   return converted;
 }

The current implementation has redundant fallback logic since createCallback should always return a valid JourneyCallback (it has a default case).

packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (1)

31-43: Good error handling for JSON parsing but could be more specific.

The error handling for malformed JSON is good, but the error message could be more specific about which policy failed to parse.

 public getFailedPolicies(): PolicyRequirement[] {
   const failedPolicies = this.getOutputByName<PolicyRequirement[]>(
     'failedPolicies',
     [],
   ) as unknown as string[];
   try {
     return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[];
-  } catch {
+  } catch (error) {
     throw new Error(
-      'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreateUsernameCallback` was either malformed or missing.',
+      `Unable to parse "failed policies" from the ForgeRock server. ${error instanceof Error ? error.message : 'Invalid JSON format'}.`,
     );
   }
 }
packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1)

24-36: Consider the same JSON parsing improvement as in username callback.

Similar to ValidatedCreateUsernameCallback, the error handling could provide more specific information about JSON parsing failures.

 public getFailedPolicies(): PolicyRequirement[] {
   const failedPolicies = this.getOutputByName<PolicyRequirement[]>(
     'failedPolicies',
     [],
   ) as unknown as string[];
   try {
     return failedPolicies.map((v) => JSON.parse(v)) as PolicyRequirement[];
-  } catch {
+  } catch (error) {
     throw new Error(
-      'Unable to parse "failed policies" from the ForgeRock server. The JSON within `ValidatedCreatePasswordCallback` was either malformed or missing.',
+      `Unable to parse "failed policies" from the ForgeRock server. ${error instanceof Error ? error.message : 'Invalid JSON format'}.`,
     );
   }
 }
packages/journey-client/src/lib/journey-client.ts (1)

124-124: Verify optional chaining is needed.

The code uses ...(options && options.query), but since options is already spread on line 122, this could be simplified.

-        ...(options && options.query), // Spread options.query first
+        ...(options?.query), // Spread options.query first
packages/journey-client/src/lib/journey-client.test.ts (2)

1-235: Add test coverage for edge cases and error scenarios.

The test suite is missing coverage for several important scenarios:

  • Error handling when fetch fails
  • Timeout scenarios for storage operations
  • Invalid URL formats in resume()
  • Missing callbacks in redirect()
  • Storage errors (get/set/remove failures)

Would you like me to generate additional test cases covering these edge cases and error scenarios?


67-68: Document API endpoint mismatch with TODO comments.

The tests correctly note that the current implementation uses /authenticate instead of the expected journey endpoints. These TODOs should be tracked.

Would you like me to create GitHub issues to track these TODO items for updating the endpoints to use /journeys?_action=start and /journeys?_action=next?

Also applies to: 102-103, 158-159, 228-229

packages/journey-client/src/lib/callbacks/name-callback.ts (1)

17-19: Add runtime type assertion to guard misuse

Assert the payload type to fail fast if the wrong callback shape is passed.

Apply this diff:

-import type { Callback } from '@forgerock/sdk-types';
+import { callbackType, type Callback } from '@forgerock/sdk-types';
@@
-  constructor(public override payload: Callback) {
-    super(payload);
-  }
+  constructor(public override payload: Callback) {
+    if (payload.type !== callbackType.NameCallback) {
+      throw new TypeError(`Invalid payload type for NameCallback: ${payload.type}`);
+    }
+    super(payload);
+  }
packages/journey-client/src/lib/journey-step.test.ts (3)

13-13: Rename describe label for consistency

Update suite name to match the file under test.

-describe('fr-step.ts', () => {
+describe('journey-step.ts', () => {

55-56: Make error assertion resilient: use regex instead of exact string

Avoid brittle coupling to the exact error message text.

-    const err = `Expected 1 callback of type "TermsAndConditionsCallback", but found 0`;
-    expect(() => step.getCallbackOfType(callbackType.TermsAndConditionsCallback)).toThrow(err);
+    expect(() => step.getCallbackOfType(callbackType.TermsAndConditionsCallback)).toThrow(
+      /Expected 1 callback of type "TermsAndConditionsCallback".*found 0/
+    );

61-63: Make error assertion resilient: use regex instead of exact string

Same rationale as above.

-    const err = `Expected 1 callback of type "NameCallback", but found 2`;
-    expect(() => step.getCallbackOfType(callbackType.NameCallback)).toThrow(err);
+    expect(() => step.getCallbackOfType(callbackType.NameCallback)).toThrow(
+      /Expected 1 callback of type "NameCallback".*found 2/
+    );
packages/journey-client/src/lib/callbacks/text-input-callback.ts (1)

17-19: Add runtime type assertion to guard misuse

Validate payload type for early, clear errors.

-import type { Callback } from '@forgerock/sdk-types';
+import { callbackType, type Callback } from '@forgerock/sdk-types';
@@
-  constructor(public override payload: Callback) {
-    super(payload);
-  }
+  constructor(public override payload: Callback) {
+    if (payload.type !== callbackType.TextInputCallback) {
+      throw new TypeError(`Invalid payload type for TextInputCallback: ${payload.type}`);
+    }
+    super(payload);
+  }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 703e47f and 236f0d4.

📒 Files selected for processing (36)
  • packages/journey-client/README.md (1 hunks)
  • packages/journey-client/src/lib/callbacks/attribute-input-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/choice-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/confirmation-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/factory.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/factory.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/hidden-value-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/index.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/kba-create-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/metadata-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/name-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/password-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/polling-wait-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/recaptcha-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/redirect-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/select-idp-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/text-input-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/text-output-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (1 hunks)
  • packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/index.ts (1 hunks)
  • packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (1 hunks)
  • packages/journey-client/src/lib/fr-webauthn/index.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.ts (1 hunks)
  • packages/journey-client/src/lib/journey-step.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-step.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (15)
  • packages/journey-client/README.md
  • packages/journey-client/src/lib/callbacks/text-output-callback.ts
  • packages/journey-client/src/lib/callbacks/choice-callback.ts
  • packages/journey-client/src/lib/callbacks/password-callback.ts
  • packages/journey-client/src/lib/callbacks/polling-wait-callback.ts
  • packages/journey-client/src/lib/fr-recovery-codes/index.ts
  • packages/journey-client/src/lib/fr-recovery-codes/recovery-codes.test.ts
  • packages/journey-client/src/lib/callbacks/recaptcha-enterprise-callback.ts
  • packages/journey-client/src/lib/callbacks/kba-create-callback.ts
  • packages/journey-client/src/lib/callbacks/metadata-callback.ts
  • packages/journey-client/src/lib/callbacks/select-idp-callback.ts
  • packages/journey-client/src/lib/callbacks/factory.test.ts
  • packages/journey-client/src/lib/callbacks/ping-protect-initialize-callback.ts
  • packages/journey-client/src/lib/callbacks/hidden-value-callback.ts
  • packages/journey-client/src/lib/callbacks/attribute-input-callback.ts
🧰 Additional context used
📓 Path-based instructions (1)
packages/**/?(*.)test.@(ts|tsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Write unit tests with Vitest for package code

Files:

  • packages/journey-client/src/lib/journey-client.test.ts
  • packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts
  • packages/journey-client/src/lib/journey-step.test.ts
  • packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts
  • packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: ForgeRock/ping-javascript-sdk#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-23T21:39:19.092Z
Learning: Applies to packages/davinci-client/** : Use Redux Toolkit for state management within the davinci-client package
📚 Learning: 2025-09-23T21:39:19.092Z
Learnt from: CR
PR: ForgeRock/ping-javascript-sdk#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-23T21:39:19.092Z
Learning: Applies to packages/**/?(*.)test.@(ts|tsx) : Write unit tests with Vitest for package code

Applied to files:

  • packages/journey-client/src/lib/journey-client.test.ts
🧬 Code graph analysis (21)
packages/journey-client/src/lib/callbacks/factory.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-client.test.ts (3)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/journey-client/src/lib/journey-client.ts (1)
  • journey (19-140)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/index.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (3)
  • Callback (46-51)
  • CallbackType (37-37)
  • NameValue (39-42)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.ts (1)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (10)
  • webAuthnRegJSCallback653 (13-49)
  • webAuthnAuthJSCallback653 (51-87)
  • webAuthnRegJSCallback70 (89-125)
  • webAuthnAuthJSCallback70 (127-163)
  • webAuthnRegMetaCallback70 (203-238)
  • webAuthnAuthMetaCallback70 (240-270)
  • webAuthnRegJSCallback70StoredUsername (165-201)
  • webAuthnAuthJSCallback70StoredUsername (455-491)
  • webAuthnRegMetaCallback70StoredUsername (272-307)
  • webAuthnAuthMetaCallback70StoredUsername (309-337)
packages/journey-client/src/lib/journey-step.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-step.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (3)
  • AuthResponse (56-58)
  • CallbackType (37-37)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/confirmation-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/redirect-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/name-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/journey-client.ts (6)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/sdk-effects/logger/src/lib/logger.types.ts (1)
  • LogLevel (16-16)
packages/journey-client/src/lib/journey.store.ts (1)
  • createJourneyStore (21-44)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • createStorage (34-139)
packages/journey-client/src/lib/journey.api.ts (1)
  • journeyApi (57-131)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/callbacks/recaptcha-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/text-input-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • PolicyRequirement (107-110)
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (2)
packages/journey-client/src/lib/fr-qrcode/fr-qr-code.mock.data.ts (2)
  • otpQRCodeStep (13-101)
  • pushQRCodeStep (103-184)
packages/journey-client/src/lib/fr-webauthn/fr-webauthn.mock.data.ts (1)
  • webAuthnRegJSCallback70 (89-125)
packages/journey-client/src/lib/fr-webauthn/index.ts (5)
packages/journey-client/src/lib/fr-webauthn/interfaces.ts (2)
  • AttestationType (125-125)
  • WebAuthnTextOutputRegistration (123-123)
packages/journey-client/src/lib/fr-webauthn/script-parser.ts (2)
  • parseWebAuthnAuthenticateText (192-192)
  • parseWebAuthnRegisterText (192-192)
packages/journey-client/src/lib/fr-webauthn/enums.ts (1)
  • WebAuthnOutcomeType (36-36)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/fr-webauthn/helpers.ts (4)
  • arrayBufferToString (117-117)
  • parseRelyingPartyId (123-123)
  • parseCredentials (119-119)
  • parsePubKeyArray (122-122)
packages/journey-client/src/lib/callbacks/device-profile-callback.ts (2)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/fr-device/interfaces.ts (1)
  • DeviceProfileData (64-64)
packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • Callback (46-51)
  • PolicyRequirement (107-110)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: pr
🔇 Additional comments (35)
packages/journey-client/src/lib/fr-qrcode/fr-qrcode.test.ts (1)

17-40: LGTM - Well-structured test cases.

The test structure follows Vitest best practices with clear describe and it blocks to organize tests logically and make them easier to read. The tests properly validate both positive and negative cases for QR code detection.

packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (1)

15-38: Nice sanity coverage for JourneyCallback.
Confirms the base wrapper can round-trip type, prompt, and input updates. 👍

packages/journey-client/src/lib/callbacks/index.ts (1)

32-34: Allow RegExp selectors in getInputValue.
The helper already supports RegExp, but the public signature rejects it, breaking existing RegExp-based callers.

Apply this diff to restore parity with setInputValue/getArrayElement:

-  public getInputValue(selector: number | string = 0): unknown {
+  public getInputValue(selector: number | string | RegExp = 0): unknown {
     return this.getArrayElement(this.payload.input, selector).value;
   }
packages/journey-client/src/lib/callbacks/confirmation-callback.ts (1)

59-63: Handle callbacks with more than two options.
Many confirmation callbacks ship with 3+ buttons. Hardcoding to {0,1} throws even when the payload is valid.

Please validate against the available options:

   public setOptionIndex(index: number): void {
-    if (index !== 0 && index !== 1) {
-      throw new Error(`"${index}" is not a valid choice`);
-    }
-    this.setInputValue(index);
+    const options = this.getOptions();
+    if (!Number.isInteger(index) || index < 0 || index >= options.length) {
+      throw new Error(`"${index}" is not a valid choice (0..${Math.max(0, options.length - 1)})`);
+    }
+    this.setInputValue(index);
   }
packages/journey-client/src/lib/callbacks/ping-protect-evaluation-callback.ts (3)

1-49: LGTM!

The PingOneProtectEvaluationCallback follows the established pattern for callback wrappers in this codebase and correctly implements the interface for gathering device and behavioral data from the PingOne Signals (Protect) SDK. The class properly extends JourneyCallback and provides appropriate typed accessors and mutators for the callback's specific requirements.


43-45: Verify the regex pattern matches the expected input name.

Similar to the signals input, the /clientError/ pattern should be verified against the actual callback structure from OpenAM.


34-36: Regex /signals/ correctly matches IDToken1signals. Existing tests in ping-protect-evaluation-callback verify that setData applies to the IDToken1signals field.

Likely an incorrect or invalid review comment.

packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1)

1-51: LGTM!

The DeviceProfileCallback implementation follows the established patterns and correctly handles device profile data collection. The JSON stringification in setProfile is appropriate for serializing the structured data for transmission.

packages/journey-client/src/lib/callbacks/recaptcha-callback.ts (1)

1-36: LGTM!

The ReCaptchaCallback implementation is clean and follows the established callback wrapper pattern. The methods provide appropriate typed access to the reCAPTCHA site key and result setting functionality.

packages/journey-client/src/lib/callbacks/terms-and-conditions-callback.ts (1)

1-51: LGTM!

The TermsAndConditionsCallback implementation correctly handles terms and conditions data with appropriate type conversions and default values.

packages/journey-client/src/lib/callbacks/factory.ts (3)

8-32: Import organization is well-structured.

The imports are logically organized and include all necessary callback types and the base JourneyCallback class.


39-92: Robust factory implementation with comprehensive callback support.

The factory correctly maps all callback types from the SDK to their corresponding wrapper classes and provides a sensible fallback to the base JourneyCallback class. The switch statement covers all defined callback types from the imported callbackType constants.


41-42: AttributeInputCallback generics supported
Declared as class AttributeInputCallback<T extends string | number | boolean>, so instantiations with <boolean>, <number>, and <string> are valid.

packages/journey-client/src/lib/journey-step.ts (4)

8-17: Clean import organization.

The imports are well-organized, importing necessary types and the callback factory functionality. The StepType import is appropriately separated from the other SDK type imports.


22-44: Well-designed constructor with flexible factory support.

The constructor properly handles the optional callback factory parameter and delegates callback conversion to a private method. The design allows for dependency injection of custom callback factories while providing a sensible default.


51-57: Good error handling in getCallbackOfType.

The method properly validates that exactly one callback of the specified type exists and provides clear error messaging with the actual count found.


74-80: Consistent error handling pattern.

The setCallbackValue method follows the same validation pattern as getCallbackOfType, ensuring consistency in error handling across the API.

packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (1)

1-76: LGTM!

The ValidatedCreateUsernameCallback implementation follows established patterns and correctly handles policy validation requirements with proper error handling for JSON parsing failures.

packages/journey-client/src/lib/callbacks/validated-create-password-callback.ts (1)

1-76: LGTM!

The ValidatedCreatePasswordCallback implementation is consistent with the username callback and properly handles policy validation with appropriate error handling.

packages/journey-client/src/lib/journey-client.ts (2)

42-44: Consider using .unwrap() and waiting for state updates.

The start() method currently dispatches and immediately returns, which can lead to race conditions. The dispatch resolves before the network request completes or state updates.


54-56: Consider using .unwrap() and waiting for state updates.

The next() method has the same race condition issue as start().

packages/journey-client/src/lib/journey-client.test.ts (1)

122-126: Configure Vitest environment for DOM globals.

The test uses window.location which requires a DOM environment. Based on the repository configuration, Vitest is set to use Node environment by default, which will cause this test to fail.

#!/bin/bash
# Check current Vitest environment configuration
echo "=== Checking Vitest configuration ==="
fd -e ts -e js -e json 'vitest.config' --exec cat {} \;

# Check if this test file has any environment override
echo -e "\n=== Checking for test-specific environment config ==="
head -50 packages/journey-client/src/lib/journey-client.test.ts | grep -E '@vitest-environment|environment:' || echo "No environment override found"

# Check package.json for Vitest configuration
echo -e "\n=== Checking package.json for Vitest config ==="
cat packages/journey-client/package.json 2>/dev/null | jq '.vitest' || echo "No vitest config in package.json"
packages/journey-client/src/lib/fr-webauthn/index.ts (9)

86-96: Fix type-unsafe string method call on union type.

The message variable is typed as string | WebAuthnTextOutputRegistration but .includes() is called directly, which will fail at runtime if message is an object.


121-128: Convert base64url strings to ArrayBuffers for WebAuthn API.

The text-output authentication path passes raw base64url strings which must be converted to ArrayBuffers.


182-189: Use correct type for registration public key.

The variable should be typed as PublicKeyCredentialCreationOptions for registration, not PublicKeyCredentialRequestOptions.


191-196: Convert base64url strings to ArrayBuffers for registration.

Similar to authentication, registration needs to convert challenge and credential IDs to ArrayBuffers.


211-213: Fix incorrect type cast in registration JSON branch.

Registration metadata is incorrectly cast to authentication metadata type.


311-319: Add SSR/Node-safe guards for browser APIs.

Direct access to window.PublicKeyCredential and navigator.credentials will throw in SSR/Node environments.

Also applies to: 385-395


415-421: Fix return type and remove invalid AttestationType.Direct.

The function returns a plain string but should return the declared template literal type. Also, AttestationType.Direct appears invalid.


447-455: Use base64url decoder for challenge field.

WebAuthn uses base64url encoding, but atob() expects standard base64.


493-506: Fix challenge decoding and user.name field.

Use base64url decoder and set user.name to the login identifier per WebAuthn spec.

packages/journey-client/src/lib/callbacks/name-callback.ts (2)

24-26: LGTM: sensible default for missing prompt


31-33: LGTM: setter delegates to base input handling

packages/journey-client/src/lib/callbacks/text-input-callback.ts (2)

24-26: LGTM: prompt extraction with default


31-33: LGTM: input setter uses base helper

Comment on lines +7 to +8
import JourneyCallback from './index.js';
import type { Callback } from '@forgerock/sdk-types';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Critical: circular import via ./index.js can break class extension at runtime

Importing the base from the barrel risks a cycle (index re-exports this class), leading to JourneyCallback being undefined during class evaluation. Import the base directly.

Apply this diff:

-import JourneyCallback from './index.js';
+import JourneyCallback from './journey-callback.js';

Scan for similar imports to fix across the callbacks package:


🏁 Script executed:

#!/bin/bash
rg -n -C0 -P "^import\\s+JourneyCallback\\s+from\\s+'\\./index\\.js';"

Length of output: 2676


Critical: avoid circular import in callbacks
Replace every

-import JourneyCallback from './index.js';
+import JourneyCallback from './journey-callback.js';

in packages/journey-client/src/lib/callbacks (use rg -lP "^import JourneyCallback from './index\\.js';" to locate all occurrences).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import JourneyCallback from './index.js';
import type { Callback } from '@forgerock/sdk-types';
import JourneyCallback from './journey-callback.js';
import type { Callback } from '@forgerock/sdk-types';

Comment on lines +7 to +8
import JourneyCallback from './index.js';
import type { Callback } from '@forgerock/sdk-types';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Critical: circular import via ./index.js can break class extension at runtime

Same issue as NameCallback: import the base directly to avoid cycles.

Apply this diff:

-import JourneyCallback from './index.js';
+import JourneyCallback from './journey-callback.js';

Scan for similar imports:


🏁 Script executed:

#!/bin/bash
rg -n -C0 -P "^import\\s+JourneyCallback\\s+from\\s+'\\./index\\.js';"

Length of output: 2676


Critical: avoid circular import via ./index.js in all callbacks
Replace every

-import JourneyCallback from './index.js';
+import JourneyCallback from './journey-callback.js';

across all callback implementation and test files (e.g. in packages/journey-client/src/lib/callbacks).
Scan with:

rg -n -P "^import JourneyCallback from '\\./index\\.js';" packages/journey-client/src/lib/callbacks
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/callbacks/text-input-callback.ts around lines
7 to 8, remove the circular default import "import JourneyCallback from
'./index.js';" and instead import the concrete base callback implementation
directly (e.g. import JourneyCallback from './journey-callback.js' or the actual
module file that defines the JourneyCallback class) so callbacks depend on the
class implementation file rather than the directory index; apply the same
replacement across all files in packages/journey-client/src/lib/callbacks (scan
with the provided ripgrep command) to eliminate the circular import.

Comment on lines +17 to +20
message: string;
use: string;
uri: string;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix QRCodeData type definition.

The use property type should be more specific to match the actual implementation rather than a generic string.

 export type QRCodeData = {
   message: string;
-  use: string;
+  use: 'otp' | 'push';
   uri: string;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
message: string;
use: string;
uri: string;
};
export type QRCodeData = {
message: string;
use: 'otp' | 'push';
uri: string;
};
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/fr-qrcode/fr-qrcode.ts around lines 17 to 20,
the QRCodeData type uses a generic string for the `use` property; replace it
with a specific union type or enum that matches the actual allowed values in the
codebase (e.g. define type QRCodeUse = 'proofRequest' | 'didComm' | 'auth' or
import an existing enum/type) and change `use: string` to `use: QRCodeUse`;
update any places that construct or check QRCodeData to use the new type and fix
imports/types as needed.

Comment on lines 63 to 508
abstract class FRWebAuthn {
/**
* Determines if the given step is a WebAuthn step.
*
* @param step The step to evaluate
* @return A WebAuthnStepType value
*/
public static getWebAuthnStepType(step: JourneyStep): WebAuthnStepType {
const outcomeCallback = this.getOutcomeCallback(step);
const metadataCallback = this.getMetadataCallback(step);
const textOutputCallback = this.getTextOutputCallback(step);

if (outcomeCallback && metadataCallback) {
const metadata = metadataCallback.getOutputValue('data') as {
pubKeyCredParams?: [];
};
if (metadata?.pubKeyCredParams) {
return WebAuthnStepType.Registration;
}

return WebAuthnStepType.Authentication;
}

if (outcomeCallback && textOutputCallback) {
const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration;

if (message.includes('pubKeyCredParams')) {
return WebAuthnStepType.Registration;
}

return WebAuthnStepType.Authentication;
} else {
return WebAuthnStepType.None;
}
}

/**
* Populates the step with the necessary authentication outcome.
*
* @param step The step that contains WebAuthn authentication data
* @return The populated step
*/
public static async authenticate(step: JourneyStep): Promise<JourneyStep> {
const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step);
if (hiddenCallback && (metadataCallback || textOutputCallback)) {
let outcome: ReturnType<typeof this.getAuthenticationOutcome>;
let credential: PublicKeyCredential | null = null;

try {
let publicKey: PublicKeyCredentialRequestOptions;
if (metadataCallback) {
const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata;
publicKey = this.createAuthenticationPublicKey(meta);

credential = await this.getAuthenticationCredential(
publicKey as PublicKeyCredentialRequestOptions,
);
outcome = this.getAuthenticationOutcome(credential);
} else if (textOutputCallback) {
publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());

credential = await this.getAuthenticationCredential(
publicKey as PublicKeyCredentialRequestOptions,
);
outcome = this.getAuthenticationOutcome(credential);
} else {
throw new Error('No Credential found from Public Key');
}
} catch (error) {
if (!(error instanceof Error)) throw error;
// NotSupportedError is a special case
if (error.name === WebAuthnOutcomeType.NotSupportedError) {
hiddenCallback.setInputValue(WebAuthnOutcome.Unsupported);
throw error;
}
hiddenCallback.setInputValue(`${WebAuthnOutcome.Error}::${error.name}:${error.message}`);
throw error;
}

if (metadataCallback) {
const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata;
if (meta?.supportsJsonResponse && credential && 'authenticatorAttachment' in credential) {
hiddenCallback.setInputValue(
JSON.stringify({
authenticatorAttachment: credential.authenticatorAttachment,
legacyData: outcome,
}),
);
return step;
}
}
hiddenCallback.setInputValue(outcome);
return step;
} else {
const e = new Error('Incorrect callbacks for WebAuthn authentication');
e.name = WebAuthnOutcomeType.DataError;
hiddenCallback?.setInputValue(`${WebAuthnOutcome.Error}::${e.name}:${e.message}`);
throw e;
}
}
/**
* Populates the step with the necessary registration outcome.
*
* @param step The step that contains WebAuthn registration data
* @return The populated step
*/
// Can make this generic const in Typescript 5.0 > and the name itself will
// be inferred from the type so `typeof deviceName` will not just return string
// but the actual name of the deviceName passed in as a generic.
public static async register<T extends string = ''>(
step: JourneyStep,
deviceName?: T,
): Promise<JourneyStep> {
const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step);
if (hiddenCallback && (metadataCallback || textOutputCallback)) {
let outcome: OutcomeWithName<string, AttestationType, PublicKeyCredential>;
let credential: PublicKeyCredential | null = null;

try {
let publicKey: PublicKeyCredentialRequestOptions;
if (metadataCallback) {
const meta = metadataCallback.getOutputValue('data') as WebAuthnRegistrationMetadata;
publicKey = this.createRegistrationPublicKey(meta);
credential = await this.getRegistrationCredential(
publicKey as PublicKeyCredentialCreationOptions,
);
outcome = this.getRegistrationOutcome(credential);
} else if (textOutputCallback) {
publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage());
credential = await this.getRegistrationCredential(
publicKey as PublicKeyCredentialCreationOptions,
);
outcome = this.getRegistrationOutcome(credential);
} else {
throw new Error('No Credential found from Public Key');
}
} catch (error) {
if (!(error instanceof Error)) throw error;
// NotSupportedError is a special case
if (error.name === WebAuthnOutcomeType.NotSupportedError) {
hiddenCallback.setInputValue(WebAuthnOutcome.Unsupported);
throw error;
}
hiddenCallback.setInputValue(`${WebAuthnOutcome.Error}::${error.name}:${error.message}`);
throw error;
}

if (metadataCallback) {
const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata;
if (meta?.supportsJsonResponse && credential && 'authenticatorAttachment' in credential) {
hiddenCallback.setInputValue(
JSON.stringify({
authenticatorAttachment: credential.authenticatorAttachment,
legacyData:
deviceName && deviceName.length > 0 ? `${outcome}::${deviceName}` : outcome,
}),
);
return step;
}
}

hiddenCallback.setInputValue(
deviceName && deviceName.length > 0 ? `${outcome}::${deviceName}` : outcome,
);
return step;
} else {
const e = new Error('Incorrect callbacks for WebAuthn registration');
e.name = WebAuthnOutcomeType.DataError;
hiddenCallback?.setInputValue(`${WebAuthnOutcome.Error}::${e.name}:${e.message}`);
throw e;
}
}

/**
* Returns an object containing the two WebAuthn callbacks.
*
* @param step The step that contains WebAuthn callbacks
* @return The WebAuthn callbacks
*/
public static getCallbacks(step: JourneyStep): WebAuthnCallbacks {
const hiddenCallback = this.getOutcomeCallback(step);
const metadataCallback = this.getMetadataCallback(step);
const textOutputCallback = this.getTextOutputCallback(step);

const returnObj: WebAuthnCallbacks = {
hiddenCallback,
};

if (metadataCallback) {
returnObj.metadataCallback = metadataCallback;
} else if (textOutputCallback) {
returnObj.textOutputCallback = textOutputCallback;
}
return returnObj;
}

/**
* Returns the WebAuthn metadata callback containing data to pass to the browser
* Web Authentication API.
*
* @param step The step that contains WebAuthn callbacks
* @return The metadata callback
*/
public static getMetadataCallback(step: JourneyStep): MetadataCallback | undefined {
return step.getCallbacksOfType<MetadataCallback>(callbackType.MetadataCallback).find((x) => {
const cb = x.getOutputByName<WebAuthnMetadata | undefined>('data', undefined);
// eslint-disable-next-line no-prototype-builtins
return cb && cb.hasOwnProperty('relyingPartyId');
});
}

/**
* Returns the WebAuthn hidden value callback where the outcome should be populated.
*
* @param step The step that contains WebAuthn callbacks
* @return The hidden value callback
*/
public static getOutcomeCallback(step: JourneyStep): HiddenValueCallback | undefined {
return step
.getCallbacksOfType<HiddenValueCallback>(callbackType.HiddenValueCallback)
.find((x) => x.getOutputByName<string>('id', '') === 'webAuthnOutcome');
}

/**
* Returns the WebAuthn metadata callback containing data to pass to the browser
* Web Authentication API.
*
* @param step The step that contains WebAuthn callbacks
* @return The metadata callback
*/
public static getTextOutputCallback(step: JourneyStep): TextOutputCallback | undefined {
return step
.getCallbacksOfType<TextOutputCallback>(callbackType.TextOutputCallback)
.find((x) => {
const cb = x.getOutputByName<string | undefined>('message', undefined);
return cb && cb.includes('webAuthnOutcome');
});
}

/**
* Retrieves the credential from the browser Web Authentication API.
*
* @param options The public key options associated with the request
* @return The credential
*/
public static async getAuthenticationCredential(
options: PublicKeyCredentialRequestOptions,
): Promise<PublicKeyCredential | null> {
// Feature check before we attempt registering a device
if (!window.PublicKeyCredential) {
const e = new Error('PublicKeyCredential not supported by this browser');
e.name = WebAuthnOutcomeType.NotSupportedError;
throw e;
}
const credential = await navigator.credentials.get({ publicKey: options });
return credential as PublicKeyCredential;
}

/**
* Converts an authentication credential into the outcome expected by OpenAM.
*
* @param credential The credential to convert
* @return The outcome string
*/
public static getAuthenticationOutcome(
credential: PublicKeyCredential | null,
):
| OutcomeWithName<string, AttestationType, PublicKeyCredential>
| OutcomeWithName<string, AttestationType, PublicKeyCredential, string> {
if (credential === null) {
const e = new Error('No credential generated from authentication');
e.name = WebAuthnOutcomeType.UnknownError;
throw e;
}

try {
const clientDataJSON = arrayBufferToString(credential.response.clientDataJSON);
const assertionResponse = credential.response as AuthenticatorAssertionResponse;
const authenticatorData = new Int8Array(
assertionResponse.authenticatorData,
).toString() as AttestationType;
const signature = new Int8Array(assertionResponse.signature).toString();

// Current native typing for PublicKeyCredential does not include `userHandle`
// eslint-disable-next-line
// @ts-ignore
const userHandle = arrayBufferToString(credential.response.userHandle);

let stringOutput =
`${clientDataJSON}::${authenticatorData}::${signature}::${credential.id}` as OutcomeWithName<
string,
AttestationType,
PublicKeyCredential
>;
// Check if Username is stored on device
if (userHandle) {
stringOutput = `${stringOutput}::${userHandle}`;
return stringOutput as OutcomeWithName<
string,
AttestationType,
PublicKeyCredential,
string
>;
}

return stringOutput;
} catch {
const e = new Error('Transforming credential object to string failed');
e.name = WebAuthnOutcomeType.EncodingError;
throw e;
}
}

/**
* Retrieves the credential from the browser Web Authentication API.
*
* @param options The public key options associated with the request
* @return The credential
*/
public static async getRegistrationCredential(
options: PublicKeyCredentialCreationOptions,
): Promise<PublicKeyCredential | null> {
// Feature check before we attempt registering a device
if (!window.PublicKeyCredential) {
const e = new Error('PublicKeyCredential not supported by this browser');
e.name = WebAuthnOutcomeType.NotSupportedError;
throw e;
}
const credential = await navigator.credentials.create({
publicKey: options,
});
return credential as PublicKeyCredential;
}

/**
* Converts a registration credential into the outcome expected by OpenAM.
*
* @param credential The credential to convert
* @return The outcome string
*/
public static getRegistrationOutcome(
credential: PublicKeyCredential | null,
): OutcomeWithName<string, AttestationType, PublicKeyCredential> {
if (credential === null) {
const e = new Error('No credential generated from registration');
e.name = WebAuthnOutcomeType.UnknownError;
throw e;
}

try {
const clientDataJSON = arrayBufferToString(credential.response.clientDataJSON);
const attestationResponse = credential.response as AuthenticatorAttestationResponse;
const attestationObject = new Int8Array(
attestationResponse.attestationObject,
).toString() as AttestationType.Direct;
return `${clientDataJSON}::${attestationObject}::${credential.id}`;
} catch {
const e = new Error('Transforming credential object to string failed');
e.name = WebAuthnOutcomeType.EncodingError;
throw e;
}
}

/**
* Converts authentication tree metadata into options required by the browser
* Web Authentication API.
*
* @param metadata The metadata provided in the authentication tree MetadataCallback
* @return The Web Authentication API request options
*/
public static createAuthenticationPublicKey(
metadata: WebAuthnAuthenticationMetadata,
): PublicKeyCredentialRequestOptions {
const {
acceptableCredentials,
allowCredentials,
challenge,
relyingPartyId,
timeout,
userVerification,
} = metadata;
const rpId = parseRelyingPartyId(relyingPartyId);
const allowCredentialsValue = parseCredentials(allowCredentials || acceptableCredentials || '');

return {
challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer,
timeout,
// only add key-value pair if proper value is provided
...(allowCredentialsValue && { allowCredentials: allowCredentialsValue }),
...(userVerification && { userVerification }),
...(rpId && { rpId }),
};
}

/**
* Converts authentication tree metadata into options required by the browser
* Web Authentication API.
*
* @param metadata The metadata provided in the authentication tree MetadataCallback
* @return The Web Authentication API request options
*/
public static createRegistrationPublicKey(
metadata: WebAuthnRegistrationMetadata,
): PublicKeyCredentialCreationOptions {
const { pubKeyCredParams: pubKeyCredParamsString } = metadata;
const pubKeyCredParams = parsePubKeyArray(pubKeyCredParamsString);
if (!pubKeyCredParams) {
const e = new Error('Missing pubKeyCredParams property from registration options');
e.name = WebAuthnOutcomeType.DataError;
throw e;
}
const excludeCredentials = parseCredentials(metadata.excludeCredentials);

const {
attestationPreference,
authenticatorSelection,
challenge,
relyingPartyId,
relyingPartyName,
timeout,
userId,
userName,
displayName,
} = metadata;
const rpId = parseRelyingPartyId(relyingPartyId);
const rp: RelyingParty = {
name: relyingPartyName,
...(rpId && { id: rpId }),
};

return {
attestation: attestationPreference,
authenticatorSelection: JSON.parse(authenticatorSelection),
challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer,
...(excludeCredentials.length && { excludeCredentials }),
pubKeyCredParams,
rp,
timeout,
user: {
displayName: displayName || userName,
id: Int8Array.from(userId.split('').map((c: string) => c.charCodeAt(0))),
name: displayName || userName,
},
};
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add base64url conversion helper.

Multiple methods need base64url to ArrayBuffer conversion. Consider adding a utility method.

Add this helper method to the FRWebAuthn class:

/**
 * Converts a base64url string to an ArrayBuffer.
 * @param base64url The base64url encoded string
 * @return The decoded ArrayBuffer
 */
private static fromBase64Url(base64url: string): ArrayBuffer {
  // Replace URL-safe characters with standard base64
  const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
  // Add padding if necessary
  const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, '=');
  // Decode and convert to ArrayBuffer
  const binary = atob(padded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

Then update the challenge decoding in createAuthenticationPublicKey (line 448) and createRegistrationPublicKey (line 496) to use:

challenge: FRWebAuthn.fromBase64Url(challenge),
🤖 Prompt for AI Agents
packages/journey-client/src/lib/fr-webauthn/index.ts lines 63-508: add a private
static helper method fromBase64Url(base64url: string): ArrayBuffer to FRWebAuthn
that normalizes base64url (replace -/_ with +/ and pad to multiple of 4),
decodes with atob into a Uint8Array and returns its buffer, then replace the
existing challenge decoding in createAuthenticationPublicKey (around line 448)
and createRegistrationPublicKey (around line 496) to use
FRWebAuthn.fromBase64Url(challenge) instead of Uint8Array.from(atob(challenge),
...).buffer so all base64url-to-ArrayBuffer conversions are centralized and
handle padding/URL-safe chars correctly.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (44)
packages/journey-client/src/lib/journey-step.test.ts (1)

80-82: Avoid mutating shared fixture data

stepPayload is reused across tests; reusing the exact callback object here means setCallbackValue mutates the shared fixture, which can leak state into later tests. Clone the callback before passing it to createJourneyStep so each test works with isolated data.

-    const singleCallbackPayload: Step = { ...stepPayload, callbacks: [stepPayload.callbacks![1]] };
+    const passwordCallback = stepPayload.callbacks![1];
+    const singleCallbackPayload: Step = {
+      ...stepPayload,
+      callbacks: [
+        {
+          ...passwordCallback,
+          input: passwordCallback.input?.map((input) => ({ ...input })),
+          output: passwordCallback.output?.map((output) => ({ ...output })),
+        },
+      ],
+    };
packages/sdk-types/src/lib/legacy-config.types.ts (2)

14-14: Use import type for type-only symbol to avoid runtime import.
CustomStorageObject is a type; bring it in with a type‑only import to prevent accidental runtime/bundle impact.

-import { CustomStorageObject } from './tokens.types.js';
+import type { CustomStorageObject } from './tokens.types.js';

38-41: Remove or relocate stale doc block.
The “Optional configuration for custom paths for actions” block is now orphaned after moving CustomPathConfig. Clean it up or relocate near ServerConfig.paths.

-/**
- * Optional configuration for custom paths for actions
- */
+// `paths` customization moved to `CustomPathConfig` in am-callback.types.ts
packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1)

46-48: Accept pre‑serialized profiles to avoid double stringify.
Allow callers to pass a string or an object; only stringify when needed.

-  public setProfile(profile: DeviceProfileData): void {
-    this.setInputValue(JSON.stringify(profile));
-  }
+  public setProfile(profile: DeviceProfileData | string): void {
+    const value = typeof profile === 'string' ? profile : JSON.stringify(profile);
+    this.setInputValue(value);
+  }
packages/journey-client/src/lib/journey-device/device-profile.mock.data.ts (2)

1-12: Type the mocks for compile‑time safety.
Import the shape and assert the literals satisfy it without widening.

+import type { DeviceProfileData } from './interfaces.js';
 
-const expectedJsdom = {
+const expectedJsdom = {
   identifier: '',
   metadata: {
     hardware: {
@@
-};
+} as const satisfies DeviceProfileData;

Apply similarly to expectedJsdomWithoutDisplay and expectedJsdomWithNarrowedBrowserProps.


27-50: Reduce brittleness in tests that consume these mocks.
Fields like userAgent, vendor, appVersion, and timezone vary with JSDOM/Node/host OS. Prefer expect.objectContaining on stable keys (e.g., presence/shape) over strict equality on full objects, or compute timezone via new Date().getTimezoneOffset().

If any tests compare these objects wholesale, consider relaxing assertions for the properties above.

Also applies to: 65-77, 106-119

packages/journey-client/src/lib/journey-step.utils.ts (2)

7-17: Consolidate imports from @forgerock/sdk-types.
Minor cleanup to a single import for types and StepType.

-import {
-  type CallbackType,
-  type Callback,
-  type Step,
-  type AuthResponse,
-} from '@forgerock/sdk-types';
+import { StepType, type CallbackType, type Callback, type Step, type AuthResponse } from '@forgerock/sdk-types';
@@
-import { StepType } from '@forgerock/sdk-types';

29-45: Generic return type is unchecked; consider a safer contract.
getCallbackOfType<T>() returns T based only on the string type, which doesn’t enforce that T matches the actual subclass. Either:

  • Drop the generic and return JourneyCallback, letting callers cast explicitly; or
  • Provide a typed map of CallbackType -> subclass and use a user‑defined type guard.
-  getCallbackOfType: <T extends JourneyCallback>(type: CallbackType) => T;
-  getCallbacksOfType: <T extends JourneyCallback>(type: CallbackType) => T[];
+  getCallbackOfType: (type: CallbackType) => JourneyCallback;
+  getCallbacksOfType: (type: CallbackType) => JourneyCallback[];

(If you already maintain a factory map, we can wire a discriminated map to keep strong typing.)

packages/journey-client/src/lib/journey-login-success.utils.test.ts (1)

18-21: Avoid @ts-ignore to keep tests lint‑clean.
Cast the local step to the expected input type (or unknown→that type) instead of suppressing TS.

-    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
-    // @ts-ignore
-    const journeyLoginSuccess = createJourneyLoginSuccess(step);
+    const journeyLoginSuccess = createJourneyLoginSuccess(step as any);

As per coding guidelines, using Vitest here is aligned. Consider introducing a local LoginSuccessPayload test type to keep this strongly typed.

packages/journey-client/src/lib/journey-policy/journey-policy.test.ts (1)

56-73: Avoid unnecessary as cast; type the custom map explicitly.

Declare customMessage as MessageCreator to drop the assertion.

-  const test = {
-    customMessage: {
-      CUSTOM_POLICY: (property: string, params?: { [key: string]: unknown }): string =>
-        `this is a custom message for "${params?.policyRequirement}" policy of ${property}`,
-    },
-    expectedString: `this is a custom message for "CUSTOM_POLICY" policy of ${property}`,
-    policy: {
-      policyRequirement: 'CUSTOM_POLICY',
-    },
-  };
-  const message = JourneyPolicy.parsePolicyRequirement(
-    property,
-    test.policy,
-    test.customMessage as MessageCreator,
-  );
+  const customMessage: MessageCreator = {
+    CUSTOM_POLICY: (property: string, params?: { [key: string]: unknown }): string =>
+      `this is a custom message for "${params?.policyRequirement}" policy of ${property}`,
+  };
+  const test = {
+    customMessage,
+    expectedString: `this is a custom message for "CUSTOM_POLICY" policy of ${property}`,
+    policy: { policyRequirement: 'CUSTOM_POLICY' },
+  };
+  const message = JourneyPolicy.parsePolicyRequirement(property, test.policy, customMessage);
packages/journey-client/src/lib/journey-device/interfaces.ts (1)

25-27: Replace any with unknown and narrow later.

Using any defeats type safety. Prefer unknown (or a union of known hardware blocks) and narrow at use sites.

-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      [key: string]: any;
+      [key: string]: unknown;
packages/journey-client/src/lib/journey-device/device-profile.test.ts (3)

1-12: Set jsdom test environment (file relies on window/navigator).

This test uses window, navigator, Storage, etc. Ensure jsdom is used even if global Vitest config defaults to node by adding the per-file directive at the very top.

+/* @vitest-environment jsdom */
 /*
  * @forgerock/javascript-sdk
  *
  * device-profile.test.ts

10-10: Avoid leaking the crypto stub across tests; restore after tests.

You redefine window.crypto but never restore it. Save and restore to prevent cross-test bleed.

-import { vi, expect, describe, it, afterEach, beforeEach, SpyInstance } from 'vitest';
+import { vi, expect, describe, it, afterEach, beforeEach, afterAll, SpyInstance } from 'vitest';
+const originalCrypto = window.crypto;

 // Patch window.crypto.getRandomValues to return Uint32Array for compatibility
 Object.defineProperty(window, 'crypto', {
   writable: true,
   value: {
     getRandomValues: vi.fn().mockImplementation((arr: Uint32Array) => {
       arr[0] = 714524572;
       arr[1] = 2799534390;
       arr[2] = 3707617532;
       return arr;
     }),
   },
 });

+afterAll(() => {
+  // Restore original crypto
+  // @ts-expect-error allow assignment for test restore
+  window.crypto = originalCrypto;
+});

Also applies to: 13-24, 127-127


41-43: Relax brittle navigator assertions to avoid env/version flakiness.

Hard-equality on appName/appVersion/vendor tends to break across jsdom/node versions. Prefer presence or pattern checks.

-    expect(appName).toBe('Netscape');
-    expect(appVersion).toBe('4.0');
-    expect(vendor).toBe('Apple Computer, Inc.');
+    expect(appName).toBeTruthy();
+    expect(appVersion).toBeTruthy();
+    expect(vendor).toBeTruthy();
@@
-    expect(appName).toBeFalsy();
-    expect(appVersion).toBeFalsy();
-    expect(vendor).toBeFalsy();
+    expect(appName ?? '').toBeFalsy();
+    expect(appVersion ?? '').toBeFalsy();
+    expect(vendor ?? '').toBeFalsy();

Please confirm CI runs jsdom for this suite; if not, keep strict checks and add the file-level env directive.

Also applies to: 78-81

packages/journey-client/src/lib/journey-policy/index.ts (3)

44-60: Use functional mapping instead of .map for side effects.

Current code uses .map only for side effects. Return the mapped array directly for clarity.

-  public static parseErrors(
-    err: Partial<Step>,
-    messageCreator?: MessageCreator,
-  ): ProcessedPropertyError[] {
-    const errors: ProcessedPropertyError[] = [];
-    if (err.detail && err.detail.failedPolicyRequirements) {
-      err.detail.failedPolicyRequirements.map((x: FailedPolicyRequirement) => {
-        errors.push.apply(errors, [
-          {
-            detail: x,
-            messages: this.parseFailedPolicyRequirement(x, messageCreator),
-          },
-        ]);
-      });
-    }
-    return errors;
-  }
+  public static parseErrors(
+    err: Partial<Step>,
+    messageCreator?: MessageCreator,
+  ): ProcessedPropertyError[] {
+    const list = err.detail?.failedPolicyRequirements ?? [];
+    return list.map((x: FailedPolicyRequirement) => ({
+      detail: x,
+      messages: this.parseFailedPolicyRequirement(x, messageCreator),
+    }));
+  }

70-81: Simplify to direct mapping when building string[] of messages.

-  public static parseFailedPolicyRequirement(
-    failedPolicy: FailedPolicyRequirement,
-    messageCreator?: MessageCreator,
-  ): string[] {
-    const errors: string[] = [];
-    failedPolicy.policyRequirements.map((policyRequirement: PolicyRequirement) => {
-      errors.push(
-        this.parsePolicyRequirement(failedPolicy.property, policyRequirement, messageCreator),
-      );
-    });
-    return errors;
-  }
+  public static parseFailedPolicyRequirement(
+    failedPolicy: FailedPolicyRequirement,
+    messageCreator?: MessageCreator,
+  ): string[] {
+    return failedPolicy.policyRequirements.map((policyRequirement: PolicyRequirement) =>
+      this.parsePolicyRequirement(failedPolicy.property, policyRequirement, messageCreator),
+    );
+  }

92-115: Accept string policy inputs and guard JSON.parse for robustness.

AM may send policy requirement as a JSON string. Reflect that in the type and handle malformed JSON without throwing.

-  public static parsePolicyRequirement(
-    property: string,
-    policy: PolicyRequirement,
-    messageCreator: MessageCreator = {},
-  ): string {
-    // AM is returning policy requirement failures as JSON strings
-    const policyObject = typeof policy === 'string' ? JSON.parse(policy) : { ...policy };
-
-    const policyRequirement = policyObject.policyRequirement;
+  public static parsePolicyRequirement(
+    property: string,
+    policy: PolicyRequirement | string,
+    messageCreator: MessageCreator = {},
+  ): string {
+    // AM can return policy requirement failures as JSON strings
+    let policyObject: Partial<PolicyRequirement> = {};
+    if (typeof policy === 'string') {
+      try {
+        policyObject = JSON.parse(policy);
+      } catch {
+        policyObject = { policyRequirement: PolicyKey.UnknownPolicy };
+      }
+    } else {
+      policyObject = { ...policy };
+    }
+
+    const policyRequirement =
+      typeof policyObject.policyRequirement === 'string'
+        ? policyObject.policyRequirement
+        : PolicyKey.UnknownPolicy;
-    const params = policyObject.params
+    const params = policyObject.params
       ? { ...policyObject.params, policyRequirement }
       : { policyRequirement };

Please confirm sdk-types’ PolicyRequirement doesn’t already include a string union; if it does, we can drop the explicit union here. As per coding guidelines.

packages/journey-client/src/lib/journey-client.test.ts (1)

32-35: Restore global.fetch after tests to avoid cross-file side effects.

Mocking fetch at module scope leaks into other suites. Save/restore it.

-import { afterEach, describe, expect, test, vi } from 'vitest';
+import { afterEach, afterAll, describe, expect, test, vi } from 'vitest';
@@
-const mockFetch = vi.fn();
-global.fetch = mockFetch;
+const mockFetch = vi.fn();
+const originalFetch = global.fetch;
+global.fetch = mockFetch;
@@
   afterEach(() => {
     vi.clearAllMocks();
   });
+  afterAll(() => {
+    global.fetch = originalFetch;
+  });

Also applies to: 43-46

packages/journey-client/src/lib/journey-client.ts (2)

20-36: Factory needn’t be async

The function doesn’t await anything before returning. Drop async to avoid unnecessary Promise wrapping.

-export async function journey({
+export function journey({

94-101: Comment/type guard mismatch

The comment says “{ step: JourneyStep }” but the guard checks for { step: Step }. Align the comment to avoid confusion.

packages/journey-client/src/lib/journey-device/index.ts (2)

142-147: Log message refers to OS while collecting hardware

Minor wording fix for clarity.

-    if (typeof navigator === 'undefined') {
-      this.logger.warn('Cannot collect OS metadata. Navigator is not defined.');
+    if (typeof navigator === 'undefined') {
+      this.logger.warn('Cannot collect hardware metadata. navigator is not defined.');

175-206: Optional: clean up canvas element

After measuring fonts, remove the canvas to avoid leaving detached nodes in long‑lived pages.

   public getInstalledFonts(): string {
@@
-    return installedFonts;
+    const result = installedFonts;
+    try { canvas.remove(); } catch { /* noop */ }
+    return result;
   }
packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts (1)

38-41: Rename test to reflect the API name.

Use getDeviceName in the test title for clarity.

-it('should return a display name from the getDisplayName method', () => {
+it('should return a display name from getDeviceName()', () => {
packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts (1)

79-90: Make expected codes fixture readonly.

Prevents accidental mutation and tightens types in tests.

-const expectedRecoveryCodes = [
+const expectedRecoveryCodes = [
   'iZmEtxvQ00',
   'Eqw3GFVamY',
   'nNPqIEtIpS',
   'vGhNQpDjP8',
   'ItA4W3iBaA',
   'JmLQP6XyIo',
   'G2e6foNKke',
   'h2SqAqvT21',
   'q6VX1ojNbI',
   'IZKIQXAfY2',
-];
+ ] as const;
packages/journey-client/src/lib/recovery-codes/script-parser.test.ts (1)

24-33: Add edge-case tests (CRLF and attributes).

Covers Windows-style escaped line breaks and with attributes.

   it('should parse the display name from recovery codes script', () => {
     const text = securityKeyResponse;
     const result = parseDeviceNameText(text);
     expect(result).toStrictEqual('New Security Key');
   });
   it('should parse a custom name out of the recovery text', () => {
     const text = securityKeyCustomNameResponse;
     const result = parseDeviceNameText(text);
     expect(result).toStrictEqual('My Custom Device Name');
   });
+  it('should parse codes when CRLF escaped newlines are present', () => {
+    // Replace all "\n" with "\r\n" in the snippet
+    const windowsStyle = displayRecoveryCodes.replace(/\\n/g, '\\r\\n');
+    const result = parseDisplayRecoveryCodesText(windowsStyle);
+    expect(result).toStrictEqual(expectedRecoveryCodes);
+  });
+  it('should parse device name when <em> has attributes', () => {
+    const withAttr = securityKeyResponse.replace('<em>', '<em class="label">');
+    const result = parseDeviceNameText(withAttr);
+    expect(result).toStrictEqual('New Security Key');
+  });
packages/journey-client/src/lib/recovery-codes/script-parser.ts (1)

44-49: Robust parsing; avoid greedy match and strip tags via capture.

Handles attributes and spacing; returns inner text only, with safe fallback.

-  const displayName =
-    text
-      ?.match(/<em\s*.*>\s*.*<\/em>/g)?.[0]
-      ?.replace('<em>', '')
-      ?.replace('</em>', '') ?? 'New Security Key';
-  return displayName;
+  const m = text.match(/<em[^>]*>\s*([^<]+)\s*<\/em>/i);
+  return m ? m[1] : 'New Security Key';
packages/journey-client/src/lib/recovery-codes/index.ts (2)

30-30: Remove unnecessary abstract.

The class is static-only; abstract adds no benefit.

-abstract class JourneyRecoveryCodes {
+class JourneyRecoveryCodes {

62-69: Use case-insensitive test for step detection.

Simplifies logic and avoids double includes.

-      .find((x) => {
-        const cb = x.getOutputByName<string | undefined>('message', undefined);
-        return cb && (cb.includes('Recovery Codes') || cb.includes('recovery codes'));
-      });
+      .find((x) => {
+        const cb = x.getOutputByName<string | undefined>('message', undefined);
+        return typeof cb === 'string' && /recovery codes/i.test(cb);
+      });
packages/journey-client/src/lib/journey-webauthn/enums.ts (1)

11-36: Consider literal-object + union types instead of runtime enums.

String enums emit runtime objects; for libraries this adds bytes and can hinder tree‑shaking. Prefer const objects + string unions.

Example (illustrative):

export const WebAuthnOutcome = {
  Error: 'ERROR',
  Unsupported: 'unsupported',
} as const;
export type WebAuthnOutcome = typeof WebAuthnOutcome[keyof typeof WebAuthnOutcome];
packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts (2)

55-55: Remove debug log from tests.

Console noise hides failures in CI.

-    console.log('the step type', stepType, WebAuthnStepType.Authentication);

27-27: Align describe text with class name.

Update “FRWebAuthn” → “JourneyWebAuthn” for clarity in test output.

-describe('Test FRWebAuthn class with 6.5.3 "Passwordless"', () => {
+describe('Test JourneyWebAuthn class with 6.5.3 "Passwordless"', () => {
...
-describe('Test FRWebAuthn class with 7.0 "Passwordless"', () => {
+describe('Test JourneyWebAuthn class with 7.0 "Passwordless"', () => {
...
-describe('Test FRWebAuthn class with 7.0 "Usernameless"', () => {
+describe('Test JourneyWebAuthn class with 7.0 "Usernameless"', () => {

Also applies to: 43-43, 74-74

packages/journey-client/src/lib/journey-webauthn/helpers.test.ts (2)

22-25: Use instance checks for ArrayBuffer.

String coercion is brittle. Prefer toBeInstanceOf(ArrayBuffer).

-    expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
...
-    expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
...
-    expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[1].id).toBeInstanceOf(ArrayBuffer);
...
-    expect(credentials[0].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[0].id).toBeInstanceOf(ArrayBuffer);
...
-    expect(credentials[1].id.toString()).toBe('[object ArrayBuffer]');
+    expect(credentials[1].id).toBeInstanceOf(ArrayBuffer);

Also applies to: 28-33, 36-39, 42-47


11-18: Make imports extensionless.

Consistent with TS/Vitest resolver.

-import { parseCredentials, parsePubKeyArray } from './helpers.js';
+import { parseCredentials, parsePubKeyArray } from './helpers';
...
-} from './helpers.mock.data.js';
+} from './helpers.mock.data';
packages/journey-client/src/lib/journey-webauthn/helpers.ts (4)

59-65: Make the Int8Array extractor robust and non-greedy; support Uint8Array too.

Current regex is greedy and only matches Int8Array. Use a lazy, cross‑newline pattern and accept both Int/Uint to avoid partial or failed matches.

-function parseNumberArray(value: string): number[] {
-  const matches = /new Int8Array\((.+)\)/.exec(value);
+function parseNumberArray(value: string): number[] {
+  // Match new Int8Array([...]) or new Uint8Array([...]), across newlines, lazily
+  const matches = /new (?:Int|Uint)8Array\(([\s\S]*?)\)/.exec(value);
   if (matches === null || matches.length < 2) {
     return [];
   }
   return JSON.parse(matches[1]);
 }

67-79: Harden JSON parsing and error semantics for pubKeyCredParams.

Wrap JSON.parse in try/catch and surface a typed error (DataError/EncodingError) for consistency with parseCredentials.

-function parsePubKeyArray(value: string | unknown[]): PublicKeyCredentialParameters[] | undefined {
+function parsePubKeyArray(value: string | unknown[]): PublicKeyCredentialParameters[] | undefined {
   if (Array.isArray(value)) {
     return value as PublicKeyCredentialParameters[];
   }
   if (typeof value !== 'string') {
     return undefined;
   }
-  if (value && value[0] === '[') {
-    return JSON.parse(value);
-  }
-  value = value.replace(/(\w+):/g, '"$1":');
-  return JSON.parse(`[${value}]`);
+  try {
+    if (value && value[0] === '[') {
+      return JSON.parse(value);
+    }
+    const normalized = value.replace(/(\w+):/g, '"$1":');
+    return JSON.parse(`[${normalized}]`);
+  } catch {
+    const e = new Error('Failed to parse pubKeyCredParams');
+    e.name = WebAuthnOutcomeType.EncodingError;
+    throw e;
+  }
 }

81-98: Align allowCredentials parsing error handling with pubKey parsing.

Same rationale: add try/catch and emit a typed error to avoid uncaught SyntaxError from JSON.parse.

-function parseAllowCredentialsArray(
+function parseAllowCredentialsArray(
   value: string | unknown[],
 ): PublicKeyCredentialDescriptor[] | undefined {
   if (!value) {
     return undefined;
   }
   if (Array.isArray(value)) {
     return value as PublicKeyCredentialDescriptor[];
   }
   if (typeof value !== 'string') {
     return undefined;
   }
-  if (value && value[0] === '[') {
-    return JSON.parse(value);
-  }
-  value = value.replace(/(\w+):/g, '"$1":');
-  return JSON.parse(`[${value}]`);
+  try {
+    if (value && value[0] === '[') {
+      return JSON.parse(value);
+    }
+    const normalized = value.replace(/(\w+):/g, '"$1":');
+    return JSON.parse(`[${normalized}]`);
+  } catch {
+    const e = new Error('Failed to parse allowCredentials array');
+    e.name = WebAuthnOutcomeType.EncodingError;
+    throw e;
+  }
 }

107-113: Tighten RP ID extraction to avoid overmatch and trailing comma issues.

Use a character‑class instead of .+ and make the trailing comma optional.

-function parseRelyingPartyId(relyingPartyId: string): string {
-  if (relyingPartyId.includes('rpId')) {
-    return relyingPartyId.replace(/rpId: "(.+)",/, '$1');
-  } else {
-    return relyingPartyId.replace(/id: "(.+)",/, '$1');
-  }
-}
+function parseRelyingPartyId(relyingPartyId: string): string {
+  if (relyingPartyId.includes('rpId')) {
+    return relyingPartyId.replace(/rpId:\s*"([^"]+)"(?:,)?/, '$1');
+  }
+  return relyingPartyId.replace(/id:\s*"([^"]+)"(?:,)?/, '$1');
+}
packages/journey-client/src/lib/journey-webauthn/interfaces.ts (2)

64-77: Use the AttestationType enum for consistency.

You define AttestationType but use a string union for attestationPreference. Aligning types reduces drift and improves autocomplete.

-interface WebAuthnRegistrationMetadata {
-  attestationPreference: 'none' | 'indirect' | 'direct';
+interface WebAuthnRegistrationMetadata {
+  attestationPreference: AttestationType;
   authenticatorSelection: string;
   challenge: string;
   excludeCredentials: string;
   pubKeyCredParams: string;
   relyingPartyId: string;
   relyingPartyName: string;
   timeout: number;

97-112: Confirm ParsedCredential.type matches consumers (PublicKeyCredentialType).

Current literal 'public-key' is fine; if other consumers use PublicKeyCredentialType, consider widening to that alias to avoid future friction.

packages/journey-client/src/lib/journey-webauthn/script-parser.ts (3)

180-181: Simplify rpId matching; avoid brittle {0,} and stray escapes.

Use clearer, equivalent patterns with * and optional backslash before the quote.

-  const rpId = getIndexOne(text.match(/rpId"{0,}:\s{0,}\\{0,}"([^"\\]*)/));
+  const rpId = getIndexOne(text.match(/rpId"*:\s*\\*"([^"\\]*)/));

94-120: Consider validating required fields (rp.name, user.name, timeout).

If rp or user blocks fail to match, empty strings propagate silently. Prefer explicit DataError with a short message.

Would you like a small helper like assertPresent(value, fieldName) to centralize these checks?


19-37: Replace {0,} with * in regex quantifiers
Update every regex in packages/journey-client/src/lib/journey-webauthn/script-parser.ts to use * instead of {0,} for zero-or-more, improving readability and aligning with lint rules.

packages/journey-client/src/lib/journey-webauthn/index.ts (1)

266-272: Avoid hasOwnProperty on possibly untrusted objects.

Prefer 'relyingPartyId' in cb to avoid prototype pitfalls.

-    return step.getCallbacksOfType<MetadataCallback>(callbackType.MetadataCallback).find((x) => {
-      const cb = x.getOutputByName<WebAuthnMetadata | undefined>('data', undefined);
-      // eslint-disable-next-line no-prototype-builtins
-      return cb && cb.hasOwnProperty('relyingPartyId');
-    });
+    return step.getCallbacksOfType<MetadataCallback>(callbackType.MetadataCallback).find((x) => {
+      const cb = x.getOutputByName<WebAuthnMetadata | undefined>('data', undefined);
+      return !!cb && typeof cb === 'object' && 'relyingPartyId' in cb;
+    });
packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts (1)

333-335: Use callbackType.* constants instead of string literals for type.

Improves consistency and reduces typos across fixtures.

-      type: 'MetadataCallback',
+      type: callbackType.MetadataCallback,
-      type: 'HiddenValueCallback',
+      type: callbackType.HiddenValueCallback,

Also applies to: 353-354

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 236f0d4 and 14ef786.

📒 Files selected for processing (43)
  • eslint.config.mjs (1 hunks)
  • packages/journey-client/src/lib/callbacks/device-profile-callback.ts (1 hunks)
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-client.ts (1 hunks)
  • packages/journey-client/src/lib/journey-device/defaults.ts (1 hunks)
  • packages/journey-client/src/lib/journey-device/device-profile.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/journey-device/device-profile.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-device/index.ts (1 hunks)
  • packages/journey-client/src/lib/journey-device/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/journey-device/sample-profile.json (1 hunks)
  • packages/journey-client/src/lib/journey-login-failure.utils.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-login-failure.utils.ts (1 hunks)
  • packages/journey-client/src/lib/journey-login-success.utils.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-login-success.utils.ts (1 hunks)
  • packages/journey-client/src/lib/journey-policy/index.ts (1 hunks)
  • packages/journey-client/src/lib/journey-policy/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/journey-policy/journey-policy.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-policy/message-creator.ts (1 hunks)
  • packages/journey-client/src/lib/journey-qrcode/journey-qr-code.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts (1 hunks)
  • packages/journey-client/src/lib/journey-step.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-step.utils.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/enums.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/helpers.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/helpers.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/helpers.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/index.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/interfaces.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/script-parser.test.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/script-parser.ts (1 hunks)
  • packages/journey-client/src/lib/journey-webauthn/script-text.mock.data.ts (1 hunks)
  • packages/journey-client/src/lib/journey.utils.ts (1 hunks)
  • packages/journey-client/src/lib/recovery-codes/index.ts (1 hunks)
  • packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts (1 hunks)
  • packages/journey-client/src/lib/recovery-codes/script-parser.test.ts (1 hunks)
  • packages/journey-client/src/lib/recovery-codes/script-parser.ts (1 hunks)
  • packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts (1 hunks)
  • packages/oidc-client/tsconfig.json (0 hunks)
  • packages/sdk-types/src/lib/legacy-config.types.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • packages/oidc-client/tsconfig.json
✅ Files skipped from review due to trivial changes (2)
  • packages/journey-client/src/lib/journey-device/sample-profile.json
  • eslint.config.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/journey-client/src/lib/callbacks/validated-create-username-callback.ts
🧰 Additional context used
📓 Path-based instructions (1)
packages/**/?(*.)test.@(ts|tsx)

📄 CodeRabbit inference engine (CLAUDE.md)

Write unit tests with Vitest for package code

Files:

  • packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts
  • packages/journey-client/src/lib/journey-webauthn/script-parser.test.ts
  • packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts
  • packages/journey-client/src/lib/journey-login-success.utils.test.ts
  • packages/journey-client/src/lib/recovery-codes/script-parser.test.ts
  • packages/journey-client/src/lib/journey-webauthn/helpers.test.ts
  • packages/journey-client/src/lib/journey-client.test.ts
  • packages/journey-client/src/lib/journey-policy/journey-policy.test.ts
  • packages/journey-client/src/lib/journey-login-failure.utils.test.ts
  • packages/journey-client/src/lib/journey-step.test.ts
  • packages/journey-client/src/lib/journey-device/device-profile.test.ts
  • packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
PR: ForgeRock/ping-javascript-sdk#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-23T21:39:19.092Z
Learning: Applies to packages/davinci-client/** : Use Redux Toolkit for state management within the davinci-client package
📚 Learning: 2025-09-23T21:39:19.092Z
Learnt from: CR
PR: ForgeRock/ping-javascript-sdk#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-23T21:39:19.092Z
Learning: Applies to packages/**/?(*.)test.@(ts|tsx) : Write unit tests with Vitest for package code

Applied to files:

  • packages/journey-client/src/lib/journey-login-success.utils.test.ts
  • packages/journey-client/src/lib/journey-client.test.ts
🧬 Code graph analysis (27)
packages/journey-client/src/lib/journey-qrcode/journey-qr-code.mock.data.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-webauthn/enums.ts (1)
packages/journey-client/src/lib/journey-webauthn/index.ts (2)
  • WebAuthnOutcome (517-517)
  • WebAuthnStepType (517-517)
packages/journey-client/src/lib/journey-webauthn/journey-webauthn.test.ts (3)
packages/journey-client/src/lib/journey-step.utils.ts (1)
  • createJourneyStep (102-102)
packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts (10)
  • webAuthnRegJSCallback653 (13-47)
  • webAuthnAuthJSCallback653 (49-83)
  • webAuthnRegJSCallback70 (85-119)
  • webAuthnAuthJSCallback70 (121-155)
  • webAuthnRegMetaCallback70 (193-228)
  • webAuthnAuthMetaCallback70 (230-259)
  • webAuthnRegJSCallback70StoredUsername (157-191)
  • webAuthnAuthJSCallback70StoredUsername (444-478)
  • webAuthnRegMetaCallback70StoredUsername (261-296)
  • webAuthnAuthMetaCallback70StoredUsername (298-326)
packages/journey-client/src/lib/journey-webauthn/enums.ts (1)
  • WebAuthnStepType (36-36)
packages/journey-client/src/lib/journey-webauthn/script-parser.test.ts (2)
packages/journey-client/src/lib/journey-webauthn/script-parser.ts (1)
  • parseWebAuthnAuthenticateText (192-192)
packages/journey-client/src/lib/journey-webauthn/script-text.mock.data.ts (10)
  • authenticateInputWithRpidAndAllowCredentials (11-43)
  • authenticateInputWithRpidAllowCredentialsAndQuotes (45-77)
  • authenticateInputWithAcceptableCredentialsWithoutRpid (112-148)
  • authenticateInputWithoutRpidAndAllowCredentials (79-109)
  • registerInputWithRpid (150-206)
  • registerOutputWithRpid (253-275)
  • registerInputWithRpidAndQuotes (208-251)
  • registerInputWithoutRpid (277-319)
  • registerOutputWithoutRpid (321-340)
  • registerInputWithExcludeCreds (342-384)
packages/journey-client/src/lib/journey-login-failure.utils.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • AuthResponse (56-58)
  • FailureDetail (63-65)
packages/journey-client/src/lib/recovery-codes/recovery-codes.test.ts (3)
packages/journey-client/src/lib/journey-step.utils.ts (1)
  • createJourneyStep (102-102)
packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts (4)
  • displayRecoveryCodesResponse (114-114)
  • otherResponse (117-117)
  • expectedRecoveryCodes (116-116)
  • expectedDeviceName (115-115)
packages/journey-client/src/lib/recovery-codes/index.ts (1)
  • isDisplayStep (52-54)
packages/journey-client/src/lib/journey-policy/interfaces.ts (2)
packages/journey-client/src/lib/journey-policy/index.ts (2)
  • MessageCreator (119-119)
  • ProcessedPropertyError (119-119)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • FailedPolicyRequirement (99-102)
packages/journey-client/src/lib/journey-step.utils.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (3)
  • AuthResponse (56-58)
  • CallbackType (37-37)
  • Callback (46-51)
packages/journey-client/src/lib/journey-client.ts (7)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/sdk-effects/logger/src/lib/logger.types.ts (1)
  • LogLevel (16-16)
packages/sdk-effects/logger/src/lib/logger.effects.ts (1)
  • logger (10-61)
packages/journey-client/src/lib/journey.store.ts (1)
  • createJourneyStore (21-44)
packages/sdk-effects/storage/src/lib/storage.effects.ts (1)
  • createStorage (34-139)
packages/journey-client/src/lib/journey.api.ts (1)
  • journeyApi (57-131)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/recovery-codes/script-parser.test.ts (2)
packages/journey-client/src/lib/recovery-codes/script-parser.ts (2)
  • parseDisplayRecoveryCodesText (51-51)
  • parseDeviceNameText (51-51)
packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts (4)
  • displayRecoveryCodes (113-113)
  • expectedRecoveryCodes (116-116)
  • securityKeyResponse (119-119)
  • securityKeyCustomNameResponse (118-118)
packages/journey-client/src/lib/callbacks/device-profile-callback.ts (2)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/journey-device/interfaces.ts (1)
  • DeviceProfileData (64-64)
packages/journey-client/src/lib/journey-webauthn/helpers.ts (2)
packages/journey-client/src/lib/journey-webauthn/interfaces.ts (1)
  • ParsedCredential (116-116)
packages/journey-client/src/lib/journey-webauthn/enums.ts (1)
  • WebAuthnOutcomeType (36-36)
packages/journey-client/src/lib/journey-webauthn/helpers.test.ts (1)
packages/journey-client/src/lib/journey-webauthn/helpers.mock.data.ts (5)
  • allowCredentials70 (11-12)
  • allowMultipleCredentials70 (14-15)
  • acceptableCredentials653 (17-18)
  • acceptableMultipleCredentials653 (20-21)
  • pubKeyCredParamsStr (23-24)
packages/journey-client/src/lib/journey-login-success.utils.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • AuthResponse (56-58)
packages/journey-client/src/lib/journey-client.test.ts (3)
packages/journey-client/src/lib/config.types.ts (1)
  • JourneyClientConfig (10-13)
packages/journey-client/src/lib/journey-client.ts (1)
  • journey (20-144)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-policy/message-creator.ts (1)
packages/sdk-utilities/src/lib/strings/strings.utils.ts (1)
  • plural (16-21)
packages/journey-client/src/lib/journey-webauthn/interfaces.ts (1)
packages/journey-client/src/lib/journey-webauthn/index.ts (4)
  • RelyingParty (512-512)
  • WebAuthnRegistrationMetadata (515-515)
  • WebAuthnAuthenticationMetadata (513-513)
  • WebAuthnCallbacks (514-514)
packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/recovery-codes/script-text.mock.data.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/sdk-types/src/lib/legacy-config.types.ts (2)
packages/journey-client/src/lib/callbacks/factory.ts (1)
  • JourneyCallbackFactory (95-95)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • Callback (46-51)
packages/journey-client/src/lib/journey-step.test.ts (2)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-step.utils.ts (1)
  • createJourneyStep (102-102)
packages/journey-client/src/lib/recovery-codes/index.ts (3)
packages/journey-client/src/lib/journey-step.utils.ts (1)
  • JourneyStep (102-102)
packages/journey-client/src/lib/recovery-codes/script-parser.ts (2)
  • parseDeviceNameText (51-51)
  • parseDisplayRecoveryCodesText (51-51)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-webauthn/index.ts (6)
packages/journey-client/src/lib/journey-webauthn/interfaces.ts (6)
  • AttestationType (125-125)
  • WebAuthnAuthenticationMetadata (121-121)
  • WebAuthnRegistrationMetadata (122-122)
  • WebAuthnTextOutputRegistration (123-123)
  • WebAuthnCallbacks (120-120)
  • RelyingParty (117-117)
packages/journey-client/src/lib/journey-step.utils.ts (1)
  • JourneyStep (102-102)
packages/journey-client/src/lib/journey-webauthn/enums.ts (3)
  • WebAuthnStepType (36-36)
  • WebAuthnOutcomeType (36-36)
  • WebAuthnOutcome (36-36)
packages/journey-client/src/lib/journey-webauthn/script-parser.ts (2)
  • parseWebAuthnAuthenticateText (192-192)
  • parseWebAuthnRegisterText (192-192)
packages/sdk-types/src/lib/am-callback.types.ts (1)
  • callbackType (10-35)
packages/journey-client/src/lib/journey-webauthn/helpers.ts (4)
  • arrayBufferToString (117-117)
  • parseRelyingPartyId (123-123)
  • parseCredentials (119-119)
  • parsePubKeyArray (122-122)
packages/journey-client/src/lib/journey-policy/index.ts (1)
packages/sdk-types/src/lib/am-callback.types.ts (2)
  • FailedPolicyRequirement (99-102)
  • PolicyRequirement (107-110)
packages/journey-client/src/lib/journey-device/index.ts (1)
packages/sdk-effects/logger/src/lib/logger.types.ts (1)
  • LogLevel (16-16)
packages/journey-client/src/lib/journey-qrcode/journey-qrcode.test.ts (2)
packages/journey-client/src/lib/journey-qrcode/journey-qr-code.mock.data.ts (2)
  • otpQRCodeStep (13-101)
  • pushQRCodeStep (103-184)
packages/journey-client/src/lib/journey-webauthn/journey-webauthn.mock.data.ts (1)
  • webAuthnRegJSCallback70 (85-119)
🪛 Biome (2.1.2)
packages/journey-client/src/lib/journey-webauthn/script-parser.ts

[error] 49-49: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 65-65: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 79-79: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 131-131: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)


[error] 139-139: The regular expression includes this negated empty character class.

Negated empty character classes match anything.
If you want to match against [, escape it [.
Otherwise, remove the character class or fill it.

(lint/correctness/noEmptyCharacterClassInRegex)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: pr
  • GitHub Check: Mend Security Check

Comment on lines +42 to +46
const self = {
start: async (options?: StepOptions) => {
const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
return data ? createJourneyObject(data) : undefined;
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Propagate RTK Query errors; avoid silently returning undefined

Destructuring { data } from the dispatched mutation swallows errors and conflates “end of journey” with failure. Use .unwrap() so errors reject and success always yields a Step.

-    start: async (options?: StepOptions) => {
-      const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
-      return data ? createJourneyObject(data) : undefined;
-    },
+    start: async (options?: StepOptions) => {
+      const action = store.dispatch(journeyApi.endpoints.start.initiate(options));
+      const data = await action.unwrap();
+      return createJourneyObject(data);
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const self = {
start: async (options?: StepOptions) => {
const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
return data ? createJourneyObject(data) : undefined;
},
const self = {
start: async (options?: StepOptions) => {
const action = store.dispatch(journeyApi.endpoints.start.initiate(options));
const data = await action.unwrap();
return createJourneyObject(data);
},
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-client.ts around lines 42-46, the
current destructuring of `{ data } = await store.dispatch(...)` swallows RTK
Query errors and returns undefined on failure; replace the dispatch +
destructure with an awaited .unwrap() call on the initiated mutation (e.g. const
result = await
store.dispatch(journeyApi.endpoints.start.initiate(options)).unwrap()) and then
pass the unwrapped result into createJourneyObject so that errors are thrown
(not silently converted to undefined) and successful responses always produce a
Step.

Comment on lines +153 to +173
public getIdentifier(): string {
const storageKey = `${this.prefix}-DeviceID`;

if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
this.logger.warn(
'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.',
);
return '';
}
if (!localStorage) {
this.logger.warn('Cannot store profile ID. localStorage is not supported.');
return '';
}
let id = localStorage.getItem(storageKey);
if (!id) {
// generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532"
id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
localStorage.setItem(storageKey, id);
}
return id;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unsafe localStorage access may throw in SSR/private modes

Referencing localStorage directly can throw (ReferenceError in SSR; SecurityError in some browsers). Guard with typeof and wrap get/set in try/catch. Return empty ID or log-and-continue on failures.

   public getIdentifier(): string {
-    const storageKey = `${this.prefix}-DeviceID`;
-
-    if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
+    const storageKey = `${this.prefix}-DeviceID`;
+    if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
       this.logger.warn(
         'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.',
       );
       return '';
     }
-    if (!localStorage) {
-      this.logger.warn('Cannot store profile ID. localStorage is not supported.');
-      return '';
-    }
-    let id = localStorage.getItem(storageKey);
-    if (!id) {
-      // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532"
-      id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
-      localStorage.setItem(storageKey, id);
-    }
+    if (!(typeof window !== 'undefined' && typeof window.localStorage !== 'undefined')) {
+      this.logger.warn('Cannot store profile ID. localStorage is not supported.');
+      return '';
+    }
+    let id = '';
+    try {
+      id = window.localStorage.getItem(storageKey) || '';
+      if (!id) {
+        id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
+        window.localStorage.setItem(storageKey, id);
+      }
+    } catch {
+      this.logger.warn('Cannot access localStorage. Storage access is restricted.');
+      return '';
+    }
     return id;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public getIdentifier(): string {
const storageKey = `${this.prefix}-DeviceID`;
if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
this.logger.warn(
'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.',
);
return '';
}
if (!localStorage) {
this.logger.warn('Cannot store profile ID. localStorage is not supported.');
return '';
}
let id = localStorage.getItem(storageKey);
if (!id) {
// generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532"
id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
localStorage.setItem(storageKey, id);
}
return id;
}
public getIdentifier(): string {
const storageKey = `${this.prefix}-DeviceID`;
if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) {
this.logger.warn(
'Cannot generate profile ID. Crypto and/or getRandomValues is not supported.',
);
return '';
}
if (!(typeof window !== 'undefined' && typeof window.localStorage !== 'undefined')) {
this.logger.warn('Cannot store profile ID. localStorage is not supported.');
return '';
}
let id = '';
try {
id = window.localStorage.getItem(storageKey) || '';
if (!id) {
id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-');
window.localStorage.setItem(storageKey, id);
}
} catch {
this.logger.warn('Cannot access localStorage. Storage access is restricted.');
return '';
}
return id;
}
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-device/index.ts around lines 153 to
173, direct access to localStorage can throw in SSR or restricted/private modes;
wrap all localStorage interactions in a typeof check (e.g., typeof window !==
'undefined' && typeof window.localStorage !== 'undefined') and surround
getItem/setItem with try/catch to handle ReferenceError/SecurityError, logging
the failure and returning an empty ID when storage is unavailable or operations
fail; keep the crypto check, generate the ID only when safe, and avoid any
unguarded global localStorage references.

};
}

export { createJourneyLoginFailure, JourneyLoginFailure };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the type-only export to avoid runtime export errors.

JourneyLoginFailure is a type alias without a runtime value, so export { … } makes the module emit an undefined export and breaks under tsc --isolatedModules. Export it as a type instead.

Apply this diff:

-export { createJourneyLoginFailure, JourneyLoginFailure };
+export { createJourneyLoginFailure };
+export type { JourneyLoginFailure };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export { createJourneyLoginFailure, JourneyLoginFailure };
export { createJourneyLoginFailure };
export type { JourneyLoginFailure };
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-login-failure.utils.ts around line
56, the file exports a type-only symbol JourneyLoginFailure using a value export
which produces an undefined runtime export under tsc --isolatedModules; change
the export so the runtime value createJourneyLoginFailure is exported normally
and the type alias JourneyLoginFailure is exported using a type-only export
(i.e., split into a plain export for the function and an export type for the
type).

/*
* @forgerock/javascript-sdk
*
* fr-policy.test.ts
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Header filename mismatch.

Comment header references “fr-policy.test.ts”; update to the actual filename for consistency.

- * fr-policy.test.ts
+ * journey-policy.test.ts
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* fr-policy.test.ts
* journey-policy.test.ts
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-policy/journey-policy.test.ts around
line 4, the file header comment incorrectly references "fr-policy.test.ts";
update the header to match the actual filename "journey-policy.test.ts" (or
remove the stale header) so the comment reflects the correct file name and
maintain consistency across tests.

Comment on lines +20 to +23
[PolicyKey.CannotContainDuplicates]: (property: string, params?: { duplicateValue?: string }) => {
const duplicateValue = getProp<string>(params, 'duplicateValue', '');
return `${property} must not contain duplicates: "${duplicateValue}"`;
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the extra space in the duplicate policy message.

There’s an unintended double space before “must” that will surface in user-facing policy errors. Tighten it up to keep the copy clean.

-    return `${property}  must not contain duplicates: "${duplicateValue}"`;
+    return `${property} must not contain duplicates: "${duplicateValue}"`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[PolicyKey.CannotContainDuplicates]: (property: string, params?: { duplicateValue?: string }) => {
const duplicateValue = getProp<string>(params, 'duplicateValue', '');
return `${property} must not contain duplicates: "${duplicateValue}"`;
},
[PolicyKey.CannotContainDuplicates]: (property: string, params?: { duplicateValue?: string }) => {
const duplicateValue = getProp<string>(params, 'duplicateValue', '');
return `${property} must not contain duplicates: "${duplicateValue}"`;
},
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-policy/message-creator.ts around
lines 20 to 23, the template literal for the duplicate policy message contains
an extra space between the property and "must" resulting in a double-space
visible to users; fix it by removing the extra space so the string reads
`${property} must not contain duplicates: "${duplicateValue}"` (ensure there's
exactly one space after the property and preserve the duplicateValue fallback
logic).

/*
* @forgerock/javascript-sdk
*
* index.ts
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Header filename mismatch.

Comment header says “index.ts”; update to “journey-qrcode.ts”.

- * index.ts
+ * journey-qrcode.ts
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* index.ts
* journey-qrcode.ts
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-qrcode/journey-qrcode.ts around line
4, the file header comment incorrectly names the file as "index.ts"; update that
header to "journey-qrcode.ts" so the comment matches the actual filename,
preserving surrounding comment formatting and punctuation.

Comment on lines +86 to +93
const challengeArr: string[] = ensureArray(
text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/),
);
// e.g. `[87, -95, 18, ... -3, 49, 12, 81]`
const challengeJSON = JSON.parse(challengeArr[2]);
// e.g. [87, -95, 18, ... -3, 49, 12, 81]
const challenge = new Int8Array(challengeJSON).buffer;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against missing challenge match; throw DataError instead of TypeError.

Accessing index 2 without verifying match length can throw. Add a check and emit a typed error.

-  const challengeArr: string[] = ensureArray(
-    text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/),
-  );
-  const challengeJSON = JSON.parse(challengeArr[2]);
+  const challengeArr: string[] = ensureArray(
+    text.match(/challenge"*:\s*new\s*(Uint|Int)8Array\(([\s\S]*?)\)/),
+  );
+  if (challengeArr.length < 3) {
+    const e = new Error('Missing challenge in registration options');
+    e.name = WebAuthnOutcomeType.DataError;
+    throw e;
+  }
+  const challengeJSON = JSON.parse(challengeArr[2] as string);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const challengeArr: string[] = ensureArray(
text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/),
);
// e.g. `[87, -95, 18, ... -3, 49, 12, 81]`
const challengeJSON = JSON.parse(challengeArr[2]);
// e.g. [87, -95, 18, ... -3, 49, 12, 81]
const challenge = new Int8Array(challengeJSON).buffer;
const challengeArr: string[] = ensureArray(
text.match(/challenge"*:\s*new\s*(Uint|Int)8Array\(([\s\S]*?)\)/),
);
// e.g. `[87, -95, 18, ... -3, 49, 12, 81]`
if (challengeArr.length < 3) {
const e = new Error('Missing challenge in registration options');
e.name = WebAuthnOutcomeType.DataError;
throw e;
}
const challengeJSON = JSON.parse(challengeArr[2] as string);
// e.g. [87, -95, 18, ... -3, 49, 12, 81]
const challenge = new Int8Array(challengeJSON).buffer;
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-webauthn/script-parser.ts around
lines 86 to 93, the code assumes the regex match succeeded and accesses match[2]
directly which can cause an unhandled TypeError; update the code to first verify
the match is non-null and has at least 3 capture groups, and if not throw a
DataError with a clear message (e.g., "missing challenge in script"); then
proceed to JSON.parse the capture. Also wrap the JSON.parse in a try/catch and
rethrow a DataError with the parse error as the cause so malformed challenge
data yields a typed DataError instead of a generic exception.

Comment on lines +151 to +160
allowCredentials = allowCredentialArr.map((str) => {
// e.g. `{ \"type\": \"public-key\",
const type = getIndexOne(str.match(/type"{0,}:\s{0,}"([\w-]+)"/)) as 'public-key';
// e.g. \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer
const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/));
// e.g. `[-107, 93, 68, -67, ... -19, 7, 4]`
const idJSON = JSON.parse(idArr[2]);
// e.g. [-107, 93, 68, -67, ... -19, 7, 4]
const id = new Int8Array(idJSON).buffer;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard id match and normalize Int/Uint handling.

Add the same safety check used for challenge; use a unified lazy pattern.

-      const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/));
-      const idJSON = JSON.parse(idArr[2]);
+      const idArr = ensureArray(
+        str.match(/id"*:\s*new\s*(Uint|Int)8Array\(([\s\S]*?)\)/),
+      );
+      if (idArr.length < 3) {
+        const e = new Error('Missing credential id in allowCredentials');
+        e.name = WebAuthnOutcomeType.DataError;
+        throw e;
+      }
+      const idJSON = JSON.parse(idArr[2] as string);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
allowCredentials = allowCredentialArr.map((str) => {
// e.g. `{ \"type\": \"public-key\",
const type = getIndexOne(str.match(/type"{0,}:\s{0,}"([\w-]+)"/)) as 'public-key';
// e.g. \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer
const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/));
// e.g. `[-107, 93, 68, -67, ... -19, 7, 4]`
const idJSON = JSON.parse(idArr[2]);
// e.g. [-107, 93, 68, -67, ... -19, 7, 4]
const id = new Int8Array(idJSON).buffer;
allowCredentials = allowCredentialArr.map((str) => {
// e.g. `{ \"type\": \"public-key\",
const type = getIndexOne(str.match(/type"{0,}:\s{0,}"([\w-]+)"/)) as 'public-key';
// e.g. \"id\": new Int8Array([-107, 93, 68, -67, ... -19, 7, 4]).buffer
- const idArr = ensureArray(str.match(/id"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/));
const idArr = ensureArray(
str.match(/id"*:\s*new\s*(Uint|Int)8Array\(([\s\S]*?)\)/),
);
if (idArr.length < 3) {
const e = new Error('Missing credential id in allowCredentials');
e.name = WebAuthnOutcomeType.DataError;
throw e;
}
// e.g. `[-107, 93, 68, -67, ... -19, 7, 4]`
const idJSON = JSON.parse(idArr[2] as string);
// e.g. [-107, 93, 68, -67, ... -19, 7, 4]
const id = new Int8Array(idJSON).buffer;
});
🤖 Prompt for AI Agents
In packages/journey-client/src/lib/journey-webauthn/script-parser.ts around
lines 151-160, the code directly assumes idArr match and uses Int8Array which
can fail; add the same safety guard used for challenge (verify idArr exists and
idArr[2] is present before parsing), parse id JSON inside a try/catch, normalize
numeric values into a plain number[] and always create a Uint8Array from those
numbers (so Int8 vs Uint8 differences are normalized) and then use its .buffer;
implement this as a lazy/guarded pattern (early return or skip this credential
if the match or parse fails) rather than assuming successful matches.

Comment on lines +171 to +179
// e.g. `challenge: new Int8Array([87, -95, 18, ... -3, 49, 12, 81]).buffer,`
const challengeArr: string[] = ensureArray(
text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/),
);
// e.g. `[87, -95, 18, ... -3, 49, 12, 81]`
const challengeJSON = JSON.parse(challengeArr[2]);
// e.g. [87, -95, 18, ... -3, 49, 12, 81]
const challenge = new Int8Array(challengeJSON).buffer;
// e.g. `rpId: \"example.com\"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Apply the same challenge guard for authentication.

-  const challengeArr: string[] = ensureArray(
-    text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/),
-  );
-  const challengeJSON = JSON.parse(challengeArr[2]);
+  const challengeArr: string[] = ensureArray(
+    text.match(/challenge"*:\s*new\s*(Uint|Int)8Array\(([\s\S]*?)\)/),
+  );
+  if (challengeArr.length < 3) {
+    const e = new Error('Missing challenge in authentication options');
+    e.name = WebAuthnOutcomeType.DataError;
+    throw e;
+  }
+  const challengeJSON = JSON.parse(challengeArr[2] as string);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// e.g. `challenge: new Int8Array([87, -95, 18, ... -3, 49, 12, 81]).buffer,`
const challengeArr: string[] = ensureArray(
text.match(/challenge"{0,}:\s{0,}new\s{0,}(Uint|Int)8Array\(([^\)]+)/),
);
// e.g. `[87, -95, 18, ... -3, 49, 12, 81]`
const challengeJSON = JSON.parse(challengeArr[2]);
// e.g. [87, -95, 18, ... -3, 49, 12, 81]
const challenge = new Int8Array(challengeJSON).buffer;
// e.g. `rpId: \"example.com\"`
// e.g. `challenge: new Int8Array([87, -95, 18, ... -3, 49, 12, 81]).buffer,`
const challengeArr: string[] = ensureArray(
text.match(/challenge"*:\s*new\s*(Uint|Int)8Array\(([\s\S]*?)\)/),
);
if (challengeArr.length < 3) {
const e = new Error('Missing challenge in authentication options');
e.name = WebAuthnOutcomeType.DataError;
throw e;
}
// e.g. `[87, -95, 18, ... -3, 49, 12, 81]`
const challengeJSON = JSON.parse(challengeArr[2] as string);
// e.g. [87, -95, 18, ... -3, 49, 12, 81]
const challenge = new Int8Array(challengeJSON).buffer;
// e.g. `rpId: \"example.com\"`

Comment on lines +89 to 93
type JourneyCallbackFactory = (callback: Callback) => any;

export interface LegacyConfigOptions {
callbackFactory?: FRCallbackFactory;
callbackFactory?: JourneyCallbackFactory;
clientId?: string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Export the alias and return unknown to stabilize public typings.
JourneyCallbackFactory is referenced by the exported LegacyConfigOptions. Not exporting it can break d.ts generation (“has or is using private name …”). Also avoid any.

-type JourneyCallbackFactory = (callback: Callback) => any;
+export type JourneyCallbackFactory = (callback: Callback) => unknown;

Run to confirm the shared type is defined and exported where referenced:


🏁 Script executed:

#!/bin/bash
# Check that CustomPathConfig is exported from am-callback.types.ts
rg -nP 'export\s+(interface|type)\s+CustomPathConfig' packages/sdk-types/src/lib/am-callback.types.ts -C2

Length of output: 161


Export JourneyCallbackFactory and use unknown instead of any.
The alias is referenced by the exported LegacyConfigOptions, so it must be public for correct .d.ts generation. Replace any with unknown to tighten typing.

-type JourneyCallbackFactory = (callback: Callback) => any;
+export type JourneyCallbackFactory = (callback: Callback) => unknown;

Run to confirm the alias is exported:

rg -nP 'export\s+type\s+JourneyCallbackFactory' -C2 packages/sdk-types/src/lib/legacy-config.types.ts
🤖 Prompt for AI Agents
In packages/sdk-types/src/lib/legacy-config.types.ts around lines 89 to 93, the
JourneyCallbackFactory type alias is currently non-exported and returns any;
export the alias (export type JourneyCallbackFactory = ...) and change its
return type from any to unknown, then ensure the exported LegacyConfigOptions
continues to reference the now-exported JourneyCallbackFactory so .d.ts
generation and stricter typing work correctly.

refactor(journey-client): rename fr-webauthn to journey-webauthn and …
@ryanbas21 ryanbas21 force-pushed the create-journey-package branch from 14ef786 to 4a3e393 Compare September 29, 2025 16:36
@ryanbas21 ryanbas21 merged commit 2c00751 into main Sep 29, 2025
4 checks passed
@ryanbas21 ryanbas21 deleted the create-journey-package branch September 29, 2025 23:26
@ryanbas21 ryanbas21 mentioned this pull request Sep 29, 2025
This was referenced Nov 13, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants