Conversation
🦋 Changeset detectedLatest commit: 5366be3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
WalkthroughIntroduces 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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ 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)
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. 🧪 Early access (Sonnet 4.5): enabledWe 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:
Comment |
There was a problem hiding this comment.
Actionable comments posted: 25
🧹 Nitpick comments (99)
packages/journey-client/README.md (1)
5-7: Round out README with usage/testing basicsAdd 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 toolingSince 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-propertiesCurrent cast
obj[prop] as Tis unchecked and prototype props may leak in. Make the map typed toT | undefinedand 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: Broadenquerytype 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
StepOptionshere is fine. Also export it frompackages/journey-client/src/index.tsto 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 howqueryis 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.
reduceToObjectreturns''for missing dotted props butnullfor 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
prop2as the key can overwrite prior entries if two inputs share the same second segment (e.g.,a.idandb.id).split('.')only supports exactly two segments;a.b.cwill 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 inreduceToString.
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. LetURLdo 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
stringifyonly 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
resolverefactor 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
stringifywith 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/AtLeastNumbersto mirror valuesAT_LEAST_*, but this is cosmetic.packages/sdk-types/src/lib/am-callback.types.ts (2)
70-85: MakeStepanAuthResponsefor consistency.
AuthResponseintroducestype: StepType;Stepshould 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: ReuseFailureDetailinStepDetail.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 mutationSafer 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 typesReal‑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: Avoidanyin factory return typeGeneric or
unknownimproves type safety without constraining consumers.-type FRCallbackFactory = (callback: Callback) => any; +type FRCallbackFactory<T = unknown> = (callback: Callback) => T;
39-41: Orphaned doc blockThis comment references custom paths but no adjacent type remains after the refactor. Remove or move it next to
CustomPathConfigusage.
47-48: Stray TODOThe “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 typoShould 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-utilitiesThis 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 sourceUpdate comment from "strings.ts" to "strings.utils.ts" to avoid confusion.
- * strings.ts + * strings.utils.tspackages/journey-client/src/lib/callbacks/text-input-callback.test.ts (1)
4-4: Correct the file headerHeader says "attribute-input-callback.test.ts"; update to the actual filename.
- * attribute-input-callback.test.ts + * text-input-callback.test.tspackages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (1)
18-33: Align test payload with typical AM NameCallback input nameReal 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 sourceRename header from "strings.ts" to "strings.utils.ts".
- * strings.ts + * strings.utils.ts
16-21: Consider minimal English inflection or document limitationThe 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 buildsGenerating d.ts for tests slows CI and clutters outputs. Disable declaration here; keep it enabled in lib config.
- "declaration": true + "declaration": falsepackages/journey-client/tsconfig.json (1)
5-27: Remove duplicate cross-package references from packages/journey-client/tsconfig.jsonpackages/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 narrowingThis 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 stringReturning '' 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 testThe shared top‑level
payloadis 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 importUse
beforeEachwith a fresh payload to avoid shared state; importCallbackas 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 commentMinor 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 directlyAsserting 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
Recordaliases 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
datacan be absent, returning{}typed asTcan mask errors. Consider returningT | undefined(orT | null) and letting callers handle absence.Can
FRCallback.getOutputByNameomit the default argument? If so, a safer variant would be:
public getData<T = unknown>(): T | undefinedand delegate without a default; otherwise, passundefined as unknown as T(ornull) 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 theNumber(...)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: ConfirmsetInputValuesupports 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 encodingAllow 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 driftSome 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 readonlySmall 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 calledSpying 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 insteadDirect 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 extensibilityA 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 testsBoth 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 testsUse 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 reusegetArrayElement 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‑awareInputs 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 absentNumber(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 | undefinedandreturn 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‑awareMirror 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 handlingnew Date('') yields an Invalid Date object. If createDate can be absent or non‑ISO, consider returning
Date | undefinedor 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 callfrDevice.getProfilefor each. Aggregate requirements, fetch once, and apply to all callbacks.Apply this diff in both
onQueryStartedblocks:- 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.
_errorcan beFetchBaseQueryErrororSerializedError, not alwaysError. Casting toErrormay 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 parsingReturn 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 stateError 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 explicitlyTiny 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 protectionTyping 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 mutatorsConsistency 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 getPoliciesUse 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 getPoliciesUse 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 prefixUse 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 stepremove() is async; ensure cleanup completes (and test expectations remain deterministic).
- stepStorage.remove(); + await stepStorage.remove();
108-117: URL params should override caller-provided optionsGive 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 inputsnew 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 navigationIf 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 fetchCleaner 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 pathSimulate 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 gracefullyAM 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 effectsUse 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...ofMinor 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 failedPoliciesDon’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
Categoryunion, deriveCategoryfromBaseProfileConfig(excludingdevicePlatforms) 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.
getCallbackOfTypethrows unless exactly one exists. Consider atryGetCallbackOfTypethat returnsT | undefinedto 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
CategoryfromBaseProfileConfigand use it inProfileConfigOptionsto 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.
Geolocationconflicts conceptually with the DOMGeolocationinterface (navigator.geolocation). Rename toCoordinatesto 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 (
getLocationCoordinatesreturn 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
platformliteral to reduce cardinality.- case this.config.devicePlatforms.ios.includes(platform): - return `${platform} (Browser)`; + case this.config.devicePlatforms.ios.includes(platform): + return 'iOS (Browser)';
| public setOptionIndex(index: number): void { | ||
| if (index !== 0 && index !== 1) { | ||
| throw new Error(`"${index}" is not a valid choice`); | ||
| } | ||
| this.setInputValue(index); | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| /** | ||
| * Types of steps returned by the authentication tree. | ||
| */ | ||
| enum StepType { | ||
| LoginFailure = 'LoginFailure', | ||
| LoginSuccess = 'LoginSuccess', | ||
| Step = 'Step', | ||
| } | ||
|
|
||
| export { StepType }; |
There was a problem hiding this comment.
🛠️ 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+")' || trueLength 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.
b70ccef to
11c3bcd
Compare
|
View your CI Pipeline Execution ↗ for commit 5366be3
☁️ Nx Cloud last updated this comment at |
|
Deployed da7b33e to https://ForgeRock.github.io/ping-javascript-sdk/pr-412/da7b33e691298c572458a9f89b4f9b2f16f06f83 branch gh-pages in ForgeRock/ping-javascript-sdk |
📦 Bundle Size Analysis📦 Bundle Size Analysis🆕 New Packages🆕 @forgerock/device-client - 9.2 KB (new) 13 packages analyzed • Baseline from latest Legend🆕 New package ℹ️ How bundle sizes are calculated
🔄 Updated automatically on each push to this PR |
There was a problem hiding this comment.
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 toangle, losing ownership byorientation. Either preserve nesting inreduceToObjector 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
Awaitingdispatch(...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_PLATFORMreads like a header name and doesn’t matchREQUESTED_WITH. Either drop theX_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.tsshared 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:
LeastNumbersreads oddly next toLeastCapitalLetters. ConsiderAtLeastXNumbersfor 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 inreduceToObject.
- Dotted path defaults to
''while top‑level defaults tonull. Pick one for predictability.- Using
prop2as the key can collide across different parents (e.g.,a.idandb.id).Unify defaults to
nulland 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.childsyntax, or return nested objects.
49-54: Guard missing keys and avoid trailing delimiter inreduceToString.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
satisfiesto ensure allPolicyKeymembers 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
DeviceProfileDataever includes circular refs,JSON.stringifywill 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 keysRan 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
TodayrequiresPreviousStepmandates bothcodeandstate. Some servers may returncodewithoutstate. Consider treating presence of any ofcode | form_post_entry | responsekeyas requiring previous step, and optionally validatingstateseparately.
21-32: Why is journey() async?
No awaits inside; returning a Promise may needlessly complicate consumers. If not intentional, dropasync.-export async function journey({ +export function journey({ @@ - return self; + return self;Also applies to: 132-133
128-130: Type the return of getStep()
IfselectStepreturnsFRStep | 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 likeappMinorVersion,buildID,browserLanguage,oscpu,userLanguage,systemLanguageare 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’?
FailureDetailisn’t referenced. If unused, remove to reduce surface; otherwise, link it inStepDetailor docs.Also applies to: 87-95
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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
./constantssubpath and the multi-entry./typesexport 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.LoginSuccesscleanly models the response discriminator.
24-26: Getter is straightforward and safe.Returning
realmas optional aligns withStep.packages/sdk-utilities/tsconfig.lib.json (1)
20-21: Tighten exclude glob for mock data; confirm .mts/.cts usageExclude 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 || truepackages/sdk-utilities/src/lib/object.utils.ts (1)
11-16: LGTM ongetProp.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.
getPromptandsetNamematch 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 acceptunknownand 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' -C2packages/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 exportedcallbackTypeandCallbackTypeunion look good.
| }, | ||
| { | ||
| "plugin": "@nx/js/typescript", | ||
| "include": ["packages/journey-client/**"], | ||
| "options": { | ||
| "typecheck": { | ||
| "targetName": "typecheck" | ||
| }, | ||
| "build": { | ||
| "targetName": "build", | ||
| "configName": "tsconfig.lib.json", | ||
| "options": { | ||
| "format": ["esm"] | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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"
fiLength 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".
| public getSessionToken(): string | undefined { | ||
| return this.payload.tokenId; | ||
| } |
There was a problem hiding this comment.
🛠️ 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 || trueLength 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.
There was a problem hiding this comment.
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
hostnamein[]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 targetRoot @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.
URLSearchParamsis 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.
journeyReducerlikely collides/confuses with the slice reducer name; preferjourneyApi.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
configureRequestset 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
loggerinextrafor 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
📒 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.
URLSearchParamskeeps the last value; duplicates collapse (e.g., ?a=1&a=2 → { a: '2' }). If arrays are needed later, we’ll needgetAll.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).
There was a problem hiding this comment.
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 forresumepayloads.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_PLATFORMexports 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).
verbatimModuleSyntaxgenerally 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 responsesRTK Query’s
fetchBaseQuerytypically parses JSON only when theContent-Typeincludesapplication/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 inputsRTK Query often calls
fetch(url, init)(string URL) rather than aRequestinstance. Current assertions assume aRequest. 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.fetchafter 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 assertionThis test asserts
getis 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 usageTo ensure the client uses the intended storage, consider asserting
createStorageis invoked with{ type: 'sessionStorage', name: 'journey-step' }. This tightens the contract around redirect/resume persistence.I can add an expectation for
createStoragearguments in the initialization test if you’d like.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.
| const assignMock = vi.fn(); | ||
| const locationSpy = vi.spyOn(window, 'location', 'get').mockReturnValue({ | ||
| ...window.location, | ||
| assign: assignMock, | ||
| }); | ||
|
|
There was a problem hiding this comment.
🧩 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
doneLength 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.
850e239 to
6a3a765
Compare
There was a problem hiding this comment.
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 typefor re‑exports and note interaction withverbatimModuleSyntaxto 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=mainmay differ across environments. Recommend explicit remote ref and an alternative usingprint-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
📒 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.mdStandalone "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:
-8Validate with:
npx changeset status --since=origin/main
There was a problem hiding this comment.
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 theissuer, 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
baseQueryconfiguration (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
localStorageunless 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 validatesstate(CSRF) andnonce(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
forceRenewvsbackgroundRenewbehaves, 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_endpointis 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
📒 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.
d5c5700 to
cabd78d
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (37)
packages/journey-client/src/lib/callbacks/text-output-callback.test.ts (1)
13-26: Prefersatisfiesto preserve literal types and stricter checksThis 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 explicitlyImport
TextOutputCallbackso 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: Prefersatisfiesfor payload typingKeeps 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 explicitlyStrengthens 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 testsThe 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 isolatedFollow‑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 nameRename 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: Usesatisfiesfor stronger compile‑time shape checking.This keeps runtime the same while validating the payload conforms to
Callbackwithout 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/setAnswerlikely mutate the payload. Reusing a singlepayloadacrossitblocks 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 testAdd 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 robustnessPlease 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 resilientString‑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 checkReduce 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 betweencallbackTypeand tested casesAdd a test that asserts every
callbackTypevalue is covered bytestCases. 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: Usetest.eachfor clearer per‑case reportingImproves 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 optionalinputhandling
Callback.inputis 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 testsThe 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 testSame 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 testAdd a case for missing or malformed
redirectUrlto 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.fetchafter each test to avoid cross-suite leakage.- Make assertions resilient whether
fetchis called withRequestor(url, init)by normalizing to aRequest.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: PrefercallbackTypeconstant over string literalUse 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
setProfilelikely mutatesinput. Reusing a top‑levelpayloadcan couple tests. Introduce abasePayloadandmakePayload()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 testSwap
payloadformakePayload()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 profileCompare 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 valueCatches 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 viatoISOString()to avoid flakiness across environments.- The single
payloadobject is reused and mutated between tests viasetAccepted, 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 mutatesinputvia setters. Reusing the samepayloadobject 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
📒 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 formessageTypeAM often emits
messageTypeas 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 gettersSolid happy‑path test for
getMessage()andgetMessageType().packages/journey-client/src/lib/callbacks/suspended-text-output-callback.test.ts (1)
22-24: ConfirmmessageTypevalue typeSame note as the sibling test: if the contract is numeric, consider using
0rather than'0'in payload and assertions.packages/journey-client/src/lib/callbacks/recaptcha-callback.test.ts (3)
29-32: LGTM: validates site key retrieval correctlyTest accurately asserts getSiteKey() behavior.
34-38: LGTM: validates result write‑backTest confirms setResult() updates the input value as expected.
10-10: ESM import extension is correct for this packagepackages/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: gettersCovers prompt and predefined questions; assertions are precise.
43-49: LGTM: settersValidates 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 correctThe 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 configCouldn’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 coverageCovers 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 behaviorsHappy path coverage for message/flags looks good.
8-11: DeviceProfileData import already present; .js ESM imports are correctpackages/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 importspackages/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.
| 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, | ||
| }, | ||
| ], | ||
| }; |
There was a problem hiding this comment.
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.
| const payload: Callback = { | ||
| type: callbackType.SelectIdPCallback, | ||
| output: [ | ||
| { | ||
| name: 'providers', | ||
| value: [ | ||
| { provider: 'google', uiConfig: {} }, | ||
| { provider: 'facebook', uiConfig: {} }, | ||
| ], | ||
| }, | ||
| ], | ||
| input: [ | ||
| { | ||
| name: 'IDToken1', | ||
| value: '', | ||
| }, | ||
| ], | ||
| }; | ||
|
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 18
♻️ Duplicate comments (2)
packages/journey-client/src/lib/journey.api.ts (1)
1-3: Resolved: StepOptions import path.Importing
StepOptionsfrom@forgerock/sdk-typesfixes 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. Useunwrap()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 bulletsContent 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 consistencyUse 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: Dropabstract(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 -eadds exclude patterns; using!negates and the comma combines two patterns into one, so neither exclusion works as intended. Exclude each pattern with its own-eand 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.
createJourneyStorepassesrequestMiddleware?; mark it optional inExtrasto 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—
applyMiddlewarehandlesundefined.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 definitionpackages/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;
createSliceinfers 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
selfasJourneyClient.packages/journey-client/src/lib/fr-webauthn/script-text.mock.data.ts (2)
191-195: Missing comma in authenticatorSelection makes the snippet invalid JSBetween 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 snippetThe 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 unionYou 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 testLeftover 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 themCommented 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 assertionMore 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 casesUse 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 incorrectThe banner says index.ts; should reflect fr-qrcode.ts.
- * index.ts + * fr-qrcode.ts
16-20: Narrow use type to a unionConstrain 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 explicitlyCurrent 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 comparisongetMessageType 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 checksOptional 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+):/gcan 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
atobis 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 forchallenge.- return cb && cb.hasOwnProperty('relyingPartyId'); + return !!cb && ('relyingPartyId' in cb || 'challenge' in (cb as Record<string, unknown>));
340-351:userHandlemay be null; ensure safe handling.If
arrayBufferToStringthrows 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:getCallbacksreturns 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: Genericregister<T>:deviceNameempty 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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 correctnessMinor 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
windowfor 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/PasswordCallbackImport 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 IDsThe 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 pathsHappy 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 variantsCovers 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 parsingCovers 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 dataStatic 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 -C1Likely 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 saferipgrep 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: preservelegacyDatawhensupportsJsonResponse.Looks good overall.
Please confirm server-side supports the
authenticatorAttachment+legacyDataJSON envelope for both paths and version-gate it if necessary.
426-456: Auth options builder looks correct (with base64url fix).Parsing
allowCredentialsand 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', |
There was a problem hiding this comment.
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.
| if (outcomeCallback && textOutputCallback) { | ||
| const message = textOutputCallback.getMessage() as string | WebAuthnTextOutputRegistration; | ||
|
|
||
| if (message.includes('pubKeyCredParams')) { | ||
| return WebAuthnStepType.Registration; | ||
| } | ||
|
|
||
| return WebAuthnStepType.Authentication; | ||
| } else { | ||
| return WebAuthnStepType.None; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| } else if (textOutputCallback) { | ||
| publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage()); | ||
|
|
||
| credential = await this.getAuthenticationCredential( | ||
| publicKey as PublicKeyCredentialRequestOptions, | ||
| ); | ||
| outcome = this.getAuthenticationOutcome(credential); | ||
| } else { |
There was a problem hiding this comment.
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.
| } 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.
| /** | ||
| * 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; | ||
| }, |
There was a problem hiding this comment.
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.
| 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({ |
There was a problem hiding this comment.
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.
| 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.
| step = await client.next({ step: step.payload }); // Submit the step | ||
| } |
There was a problem hiding this comment.
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 stepAlso 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.
| "path": "../packages/sdk-types" | ||
| }, | ||
| { | ||
| "path": "../packages/journey-client" | ||
| } |
There was a problem hiding this comment.
🧩 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"; doneLength 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.
efea18e to
b7acf08
Compare
There was a problem hiding this comment.
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 selectorUse 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 | undefinedrisks 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/1Current 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
localStoragewill 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: wrongpublicKeytype; 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 compatibilitypackages/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, andMinimumNumberexpose 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
unknowntype 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 errorpackages/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 payloadRedundant override/reassignment; rely on base class storage.
- constructor(public override payload: Callback) { + constructor(payload: Callback) { super(payload); }
15-16: Nit: modernize wordingPrefer “AM (Access Management)” over “OpenAM” in docs.
- * @param payload The raw payload returned by OpenAM + * @param payload The raw AM (Access Management) callback payloadpackages/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": 360Option 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.
pluginsandfontsare 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
tokenIdat the type level; keepsuccessUrloptional 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
payloadreadonly and assert presence oftokenId(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 thetypeconstant.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 optionsPrevents 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 debuggingImproves 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
reduceToStringis 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
MacIntelplatform; 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 usetoBeInstanceOffor 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.eachto 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.
setPasswordlikely mutatesinput[0].value. Reusingpayloadacross 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
inputis missing or malformed, e.g., noIDToken*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.outputand selectively sets known keys only when present?
13-13: Naming consistency nit: file vs class.Class is
PingOneProtectInitializeCallbackbut filename isping-protect-initialize-callback.ts. Consider aligning toping-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
Stepinstance 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:
StepOptionsis exported in packages/journey-client/src/lib/config.types.ts but NOT re‑exported from packages/journey-client/src/index.ts;NextOptionsis declared in packages/journey-client/src/lib/interfaces.ts but not exported;Stepis 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.,
\\xsplit) 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
messageTypevalues 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.createCodeshape 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 constto 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) withfr-qr-code.mock.data.ts(file). Consider standardizing on one form (fr-qrcodeorfr-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 testReuse 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 assertionsUse 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 nameSame 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 parseablenew 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 clarityIf 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 possibleSlight 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 resultEnsures 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 asyncCurrently 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 suffixRename 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.
| public getInputValue(selector: number | string = 0): unknown { | ||
| return this.getArrayElement(this.payload.input, selector).value; | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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']); | ||
| }); |
There was a problem hiding this comment.
🧩 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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', |
There was a problem hiding this comment.
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.
| 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issue' + | ||
| 'r=ForgeRock&period=30&digits=6&b=032b75', | ||
| }, |
There was a problem hiding this comment.
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.
| 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 || []; | ||
| } |
There was a problem hiding this comment.
🧩 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
fiLength 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 ... ', |
There was a problem hiding this comment.
🧩 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
fiLength 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.
There was a problem hiding this comment.
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 TSresolveJsonModule(if used).- Ignoring
tools/**/*andscripts/**/*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/orscripts/if they’re never executed in production paths.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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: trueandnotify.wait_for_ci: truedon’t delay or suppress the PR comment.
27-31: Verifyrangesyntax; 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.
layoutincludingflagsandrequire_head: trueare sensible defaults. Keepingrequire_changes: falseavoids missing context on no‑diff PRs.
84-86: Confirm Codecov support forbundle_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 wholepackages/tree — narrow it or rely on per-upload flags.File: codecov.yml (lines 66–83). Mapping
package-*topaths: - packages/causes everypackage-...flag to be evaluated against the same fileset (packages/), diluting per-package signals; either remove thepathsentry and rely on flagged uploads, or add explicit per-packagepaths. (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.
4301fa4 to
b0f4368
Compare
There was a problem hiding this comment.
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 pushesIf 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: spreadingundefinedthrows at runtime.Use an empty object when
treeis falsy and narrow toRecord<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
localStoragethrows in SSR/non-DOM and some browsers can throw ongetItem/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
localStoragetouches:#!/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 objectsReturn 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 lengthCurrent 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 placeholdersThese 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
guidelinesunderreviews, which isn’t a defined property. If you intend to enforce team guidelines, add them underknowledge_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.
-12packages/journey-client/src/lib/callbacks/fr-auth-callback.test.ts (2)
36-36: Assert against the source of truth constantUse 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 testConsider 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.parseCredentialsalready 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: 2packages/journey-client/src/lib/callbacks/validated-create-username-callback.test.ts (3)
16-59: Isolate test state: avoid shared mutable payload across testsThe 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 assertionUse the exported constant to avoid drift.
- expect(cb.getType()).toBe('ValidatedCreateUsernameCallback'); + expect(cb.getType()).toBe(callbackType.ValidatedCreateUsernameCallback);
72-73: Make assertions resilient to input orderingFind 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
queryFncan 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(): mergeconfig.querywithoptions?.query.- const query = options?.query || {}; + const { config: cfg } = api.extra as Extras; + const query = { ...(cfg.query ?? {}), ...(options?.query ?? {}) };
51-55: MakerequestMiddlewareoptional to match.applyMiddlewaresignature.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 tonamewhenfilenameis 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 assertionsAdd 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 brittleAsserting 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 testsThe 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 testsIf 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 returnReturning {} 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 typesHelpful 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 oneEither 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 oneAlign 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 conversionPrefer 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
| # 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: |
There was a problem hiding this comment.
🧩 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: trueRun 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.
| config: BaseProfileConfig = { | ||
| fontNames, | ||
| devicePlatforms, | ||
| displayProps, | ||
| browserProps, | ||
| hardwareProps, | ||
| platformProps, | ||
| }; |
There was a problem hiding this comment.
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
| 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...', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }; |
There was a problem hiding this comment.
🧩 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 || trueLength 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'; | |||
There was a problem hiding this comment.
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.
| 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.
ae760ce to
703e47f
Compare
There was a problem hiding this comment.
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 tokenCodecov Action v5 supports tokenless uploads via GitHub OIDC. To switch:
Add workflow/job permission:
permissions:
id-token: writeReplace the action inputs (remove token, add use_oidc):
- uses: codecov/codecov-action@v5
with:
directory: ./packages/
use_oidc: trueAny 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
📒 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-actioncalls or explicitflagsusage — 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 stepQuick 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
There was a problem hiding this comment.
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: trueRefs: 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: trueRefs: 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
📒 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)
| 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: |
There was a problem hiding this comment.
🧩 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: trueIf 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"
fiLength 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
fiLength 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.
… to journeycallback
…other fr prefixes to journey
cerebrl
left a comment
There was a problem hiding this comment.
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.
| "version": "0.0.1", | ||
| "private": true, | ||
| "type": "module", | ||
| "exports": { |
There was a problem hiding this comment.
How are we exporting the new modules? I thought we'd need to add them here, like for QR Code, WebAuthn, etc?
refactor(journey-client): rename frstep to journeystep and frcallback…
There was a problem hiding this comment.
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
booleanunnecessarily and should validate thatgetQRCodeURICbreturns 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
getQRCodeDatais 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 withInvalid Dateas 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
createCallbackshould 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 sinceoptionsis already spread on line 122, this could be simplified.- ...(options && options.query), // Spread options.query first + ...(options?.query), // Spread options.query firstpackages/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
/authenticateinstead 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=startand/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 misuseAssert 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 consistencyUpdate 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 stringAvoid 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 stringSame 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 misuseValidate 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
📒 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.tspackages/journey-client/src/lib/fr-webauthn/fr-webauthn.test.tspackages/journey-client/src/lib/journey-step.test.tspackages/journey-client/src/lib/callbacks/fr-auth-callback.test.tspackages/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 ingetInputValue.
The helper already supportsRegExp, 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 matchesIDToken1signals. Existing tests in ping-protect-evaluation-callback verify thatsetDataapplies to theIDToken1signalsfield.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
setProfileis 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
callbackTypeconstants.
41-42: AttributeInputCallback generics supported
Declared asclass 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
setCallbackValuemethod follows the same validation pattern asgetCallbackOfType, 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 asstart().packages/journey-client/src/lib/journey-client.test.ts (1)
122-126: Configure Vitest environment for DOM globals.The test uses
window.locationwhich 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
messagevariable is typed asstring | WebAuthnTextOutputRegistrationbut.includes()is called directly, which will fail at runtime ifmessageis 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
PublicKeyCredentialCreationOptionsfor registration, notPublicKeyCredentialRequestOptions.
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.PublicKeyCredentialandnavigator.credentialswill 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.Directappears 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.nameto 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 handlingpackages/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
| import JourneyCallback from './index.js'; | ||
| import type { Callback } from '@forgerock/sdk-types'; |
There was a problem hiding this comment.
🧩 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.
| 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'; |
| import JourneyCallback from './index.js'; | ||
| import type { Callback } from '@forgerock/sdk-types'; |
There was a problem hiding this comment.
🧩 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.
| message: string; | ||
| use: string; | ||
| uri: string; | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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, | ||
| }, | ||
| }; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (44)
packages/journey-client/src/lib/journey-step.test.ts (1)
80-82: Avoid mutating shared fixture data
stepPayloadis reused across tests; reusing the exact callback object here meanssetCallbackValuemutates the shared fixture, which can leak state into later tests. Clone the callback before passing it tocreateJourneyStepso 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: Useimport typefor type-only symbol to avoid runtime import.
CustomStorageObjectis 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 movingCustomPathConfig. Clean it up or relocate nearServerConfig.paths.-/** - * Optional configuration for custom paths for actions - */ +// `paths` customization moved to `CustomPathConfig` in am-callback.types.tspackages/journey-client/src/lib/callbacks/device-profile-callback.ts (1)
46-48: Accept pre‑serialized profiles to avoid double stringify.
Allow callers to pass astringor 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
expectedJsdomWithoutDisplayandexpectedJsdomWithNarrowedBrowserProps.
27-50: Reduce brittleness in tests that consume these mocks.
Fields likeuserAgent,vendor,appVersion, andtimezonevary with JSDOM/Node/host OS. Preferexpect.objectContainingon stable keys (e.g., presence/shape) over strict equality on full objects, or computetimezonevianew 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 andStepType.-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>()returnsTbased only on the stringtype, which doesn’t enforce thatTmatches the actual subclass. Either:
- Drop the generic and return
JourneyCallback, letting callers cast explicitly; or- Provide a typed map of
CallbackType -> subclassand 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-ignoreto keep tests lint‑clean.
Cast the localstepto the expected input type (orunknown→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
LoginSuccessPayloadtest type to keep this strongly typed.packages/journey-client/src/lib/journey-policy/journey-policy.test.ts (1)
56-73: Avoid unnecessaryascast; type the custom map explicitly.Declare
customMessageasMessageCreatorto 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: Replaceanywithunknownand narrow later.Using
anydefeats type safety. Preferunknown(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 asyncThe function doesn’t await anything before returning. Drop
asyncto avoid unnecessary Promise wrapping.-export async function journey({ +export function journey({
94-101: Comment/type guard mismatchThe 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 hardwareMinor 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 elementAfter 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 unnecessaryabstract.The class is static-only;
abstractadds 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 usePublicKeyCredentialType, 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
rporuserblocks 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 inpackages/journey-client/src/lib/journey-webauthn/script-parser.tsto 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: AvoidhasOwnPropertyon possibly untrusted objects.Prefer
'relyingPartyId' in cbto 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: UsecallbackType.*constants instead of string literals fortype.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
📒 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.tspackages/journey-client/src/lib/journey-webauthn/script-parser.test.tspackages/journey-client/src/lib/recovery-codes/recovery-codes.test.tspackages/journey-client/src/lib/journey-login-success.utils.test.tspackages/journey-client/src/lib/recovery-codes/script-parser.test.tspackages/journey-client/src/lib/journey-webauthn/helpers.test.tspackages/journey-client/src/lib/journey-client.test.tspackages/journey-client/src/lib/journey-policy/journey-policy.test.tspackages/journey-client/src/lib/journey-login-failure.utils.test.tspackages/journey-client/src/lib/journey-step.test.tspackages/journey-client/src/lib/journey-device/device-profile.test.tspackages/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.tspackages/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
| const self = { | ||
| start: async (options?: StepOptions) => { | ||
| const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options)); | ||
| return data ? createJourneyObject(data) : undefined; | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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 }; |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| * 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.
| [PolicyKey.CannotContainDuplicates]: (property: string, params?: { duplicateValue?: string }) => { | ||
| const duplicateValue = getProp<string>(params, 'duplicateValue', ''); | ||
| return `${property} must not contain duplicates: "${duplicateValue}"`; | ||
| }, |
There was a problem hiding this comment.
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.
| [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 |
There was a problem hiding this comment.
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.
| * 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.
| 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; | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| // 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\"` |
There was a problem hiding this comment.
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.
| // 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\"` |
| type JourneyCallbackFactory = (callback: Callback) => any; | ||
|
|
||
| export interface LegacyConfigOptions { | ||
| callbackFactory?: FRCallbackFactory; | ||
| callbackFactory?: JourneyCallbackFactory; | ||
| clientId?: string; |
There was a problem hiding this comment.
🧩 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 -C2Length 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 …
14ef786 to
4a3e393
Compare
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