diff --git a/.changeset/embed-password-policy-in-component.md b/.changeset/embed-password-policy-in-component.md new file mode 100644 index 0000000000..b2de85aee4 --- /dev/null +++ b/.changeset/embed-password-policy-in-component.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': minor +--- + +Add PasswordVerifyCollector to support password policy embedded in PASSWORD_VERIFY field components. The DaVinci API now returns passwordPolicy inside the PASSWORD_VERIFY field (DV-16053) instead of at the response root. The new PasswordVerifyCollector exposes the policy via output.passwordPolicy, enabling consumers to render password requirements directly from the collector. diff --git a/e2e/davinci-app/components/password.ts b/e2e/davinci-app/components/password.ts index 5b835478f8..05c37f6953 100644 --- a/e2e/davinci-app/components/password.ts +++ b/e2e/davinci-app/components/password.ts @@ -4,13 +4,17 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -import type { PasswordCollector, Updater } from '@forgerock/davinci-client/types'; +import type { + PasswordCollector, + PasswordVerifyCollector, + Updater, +} from '@forgerock/davinci-client/types'; import { dotToCamelCase } from '../helper.js'; export default function passwordComponent( formEl: HTMLFormElement, - collector: PasswordCollector, - updater: Updater, + collector: PasswordCollector | PasswordVerifyCollector, + updater: Updater, ) { const label = document.createElement('label'); const input = document.createElement('input'); @@ -24,6 +28,39 @@ export default function passwordComponent( formEl?.appendChild(label); formEl?.appendChild(input); + // Render password policy requirements if available + if (collector.type === 'PasswordVerifyCollector' && collector.output.passwordPolicy) { + const policy = collector.output.passwordPolicy; + const requirementsList = document.createElement('ul'); + requirementsList.className = 'password-requirements'; + + if (policy.length) { + const li = document.createElement('li'); + li.textContent = `${policy.length.min}–${policy.length.max} characters`; + requirementsList.appendChild(li); + } + + if (policy.minCharacters) { + for (const [charset, count] of Object.entries(policy.minCharacters)) { + const li = document.createElement('li'); + if (charset.match(/^[A-Z]+$/)) { + li.textContent = `At least ${count} uppercase letter(s)`; + } else if (charset.match(/^[a-z]+$/)) { + li.textContent = `At least ${count} lowercase letter(s)`; + } else if (charset.match(/^[0-9]+$/)) { + li.textContent = `At least ${count} number(s)`; + } else { + li.textContent = `At least ${count} special character(s)`; + } + requirementsList.appendChild(li); + } + } + + if (requirementsList.children.length > 0) { + formEl?.appendChild(requirementsList); + } + } + formEl ?.querySelector(`#${dotToCamelCase(collector.output.key)}`) ?.addEventListener('blur', (event: Event) => { diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index 46ef800d5b..72f78a30f5 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -234,9 +234,10 @@ const urlParams = new URLSearchParams(window.location.search); davinciClient.update(collector), // Returns an update function for this collector davinciClient.validate(collector), // Returns a validate function for this collector ); - } else if (collector.type === 'PasswordCollector') { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - collector; + } else if ( + collector.type === 'PasswordCollector' || + collector.type === 'PasswordVerifyCollector' + ) { passwordComponent( formEl, // You can ignore this; it's just for rendering collector, // This is the plain object of the collector diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index cedf484d86..d5d1ca6198 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -147,11 +147,13 @@ export interface CollectorErrors { } // @public (undocumented) -export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector; +export type Collectors = FlowCollector | PasswordCollector | PasswordVerifyCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector; // @public export type CollectorValueType = T extends { type: 'PasswordCollector'; +} ? string : T extends { + type: 'PasswordVerifyCollector'; } ? string : T extends { type: 'TextCollector'; category: 'SingleValueCollector'; @@ -649,6 +651,8 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse { }; // (undocumented) _links?: Links; + // (undocumented) + passwordPolicy?: PasswordPolicy; } // @public @@ -1001,7 +1005,7 @@ export type InferMultiValueCollectorType = T export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>; // @public -export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; +export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'PasswordVerifyCollector' ? PasswordVerifyCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; // @public (undocumented) export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; @@ -1136,11 +1140,12 @@ fields: DaVinciField[]; formData: { value: Record; }; +passwordPolicy?: PasswordPolicy; }, string>; // @public -export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { - getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; +export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { + getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; }; // @public (undocumented) @@ -1294,6 +1299,91 @@ export interface OutgoingQueryParams { // @public (undocumented) export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>; +// @public (undocumented) +export interface PasswordPolicy { + // (undocumented) + createdAt?: string; + // (undocumented) + default?: boolean; + // (undocumented) + description?: string; + // (undocumented) + excludesCommonlyUsed?: boolean; + // (undocumented) + excludesProfileData?: boolean; + // (undocumented) + history?: { + count?: number; + retentionDays?: number; + }; + // (undocumented) + id?: string; + // (undocumented) + length?: { + min?: number; + max?: number; + }; + // (undocumented) + lockout?: { + failureCount?: number; + durationSeconds?: number; + }; + // (undocumented) + maxAgeDays?: number; + // (undocumented) + maxRepeatedCharacters?: number; + // (undocumented) + minAgeDays?: number; + // (undocumented) + minCharacters?: Record; + // (undocumented) + minUniqueCharacters?: number; + // (undocumented) + name?: string; + // (undocumented) + notSimilarToCurrent?: boolean; + // (undocumented) + populationCount?: number; + // (undocumented) + updatedAt?: string; +} + +// @public (undocumented) +export interface PasswordVerifyCollector { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + passwordPolicy?: PasswordPolicy; + }; + // (undocumented) + type: 'PasswordVerifyCollector'; +} + +// @public (undocumented) +export type PasswordVerifyField = { + type: 'PASSWORD_VERIFY'; + key: string; + label: string; + required?: boolean; + passwordPolicy?: PasswordPolicy; +}; + // @public (undocumented) export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>; @@ -1557,10 +1647,10 @@ export interface SingleValueCollectorNoValue | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>; +export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | PasswordVerifyCollector | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>; // @public -export type SingleValueCollectorTypes = 'PasswordCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; +export type SingleValueCollectorTypes = 'PasswordCollector' | 'PasswordVerifyCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; // @public (undocumented) export interface SingleValueCollectorWithValue { @@ -1590,11 +1680,11 @@ export interface SingleValueCollectorWithValue | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector; +export type Collectors = FlowCollector | PasswordCollector | PasswordVerifyCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector; // @public export type CollectorValueType = T extends { type: 'PasswordCollector'; +} ? string : T extends { + type: 'PasswordVerifyCollector'; } ? string : T extends { type: 'TextCollector'; category: 'SingleValueCollector'; @@ -649,6 +651,8 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse { }; // (undocumented) _links?: Links; + // (undocumented) + passwordPolicy?: PasswordPolicy; } // @public @@ -998,7 +1002,7 @@ export type InferMultiValueCollectorType = T export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>; // @public -export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; +export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'PasswordVerifyCollector' ? PasswordVerifyCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; // @public (undocumented) export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; @@ -1133,11 +1137,12 @@ fields: DaVinciField[]; formData: { value: Record; }; +passwordPolicy?: PasswordPolicy; }, string>; // @public -export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { - getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; +export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { + getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; }; // @public (undocumented) @@ -1291,6 +1296,91 @@ export interface OutgoingQueryParams { // @public (undocumented) export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>; +// @public (undocumented) +export interface PasswordPolicy { + // (undocumented) + createdAt?: string; + // (undocumented) + default?: boolean; + // (undocumented) + description?: string; + // (undocumented) + excludesCommonlyUsed?: boolean; + // (undocumented) + excludesProfileData?: boolean; + // (undocumented) + history?: { + count?: number; + retentionDays?: number; + }; + // (undocumented) + id?: string; + // (undocumented) + length?: { + min?: number; + max?: number; + }; + // (undocumented) + lockout?: { + failureCount?: number; + durationSeconds?: number; + }; + // (undocumented) + maxAgeDays?: number; + // (undocumented) + maxRepeatedCharacters?: number; + // (undocumented) + minAgeDays?: number; + // (undocumented) + minCharacters?: Record; + // (undocumented) + minUniqueCharacters?: number; + // (undocumented) + name?: string; + // (undocumented) + notSimilarToCurrent?: boolean; + // (undocumented) + populationCount?: number; + // (undocumented) + updatedAt?: string; +} + +// @public (undocumented) +export interface PasswordVerifyCollector { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + passwordPolicy?: PasswordPolicy; + }; + // (undocumented) + type: 'PasswordVerifyCollector'; +} + +// @public (undocumented) +export type PasswordVerifyField = { + type: 'PASSWORD_VERIFY'; + key: string; + label: string; + required?: boolean; + passwordPolicy?: PasswordPolicy; +}; + // @public (undocumented) export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>; @@ -1554,10 +1644,10 @@ export interface SingleValueCollectorNoValue | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>; +export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | PasswordVerifyCollector | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>; // @public -export type SingleValueCollectorTypes = 'PasswordCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; +export type SingleValueCollectorTypes = 'PasswordCollector' | 'PasswordVerifyCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; // @public (undocumented) export interface SingleValueCollectorWithValue { @@ -1587,11 +1677,11 @@ export interface SingleValueCollectorWithValue Promise; */ export type CollectorValueType = T extends { type: 'PasswordCollector' } ? string - : T extends { type: 'TextCollector'; category: 'SingleValueCollector' } + : T extends { type: 'PasswordVerifyCollector' } ? string - : T extends { type: 'TextCollector'; category: 'ValidatedSingleValueCollector' } + : T extends { type: 'TextCollector'; category: 'SingleValueCollector' } ? string - : T extends { type: 'SingleSelectCollector' } + : T extends { type: 'TextCollector'; category: 'ValidatedSingleValueCollector' } ? string - : T extends { type: 'MultiSelectCollector' } - ? string[] - : T extends { type: 'DeviceRegistrationCollector' } - ? string - : T extends { type: 'DeviceAuthenticationCollector' } + : T extends { type: 'SingleSelectCollector' } + ? string + : T extends { type: 'MultiSelectCollector' } + ? string[] + : T extends { type: 'DeviceRegistrationCollector' } ? string - : T extends { type: 'PhoneNumberCollector' } - ? PhoneNumberInputValue - : T extends { type: 'FidoRegistrationCollector' } - ? FidoRegistrationInputValue - : T extends { type: 'FidoAuthenticationCollector' } - ? FidoAuthenticationInputValue - : T extends { category: 'SingleValueCollector' } - ? string - : T extends { category: 'ValidatedSingleValueCollector' } + : T extends { type: 'DeviceAuthenticationCollector' } + ? string + : T extends { type: 'PhoneNumberCollector' } + ? PhoneNumberInputValue + : T extends { type: 'FidoRegistrationCollector' } + ? FidoRegistrationInputValue + : T extends { type: 'FidoAuthenticationCollector' } + ? FidoAuthenticationInputValue + : T extends { category: 'SingleValueCollector' } ? string - : T extends { category: 'MultiValueCollector' } - ? string[] - : - | string - | string[] - | PhoneNumberInputValue - | FidoRegistrationInputValue - | FidoAuthenticationInputValue; + : T extends { category: 'ValidatedSingleValueCollector' } + ? string + : T extends { category: 'MultiValueCollector' } + ? string[] + : + | string + | string[] + | PhoneNumberInputValue + | FidoRegistrationInputValue + | FidoAuthenticationInputValue; /** * Generic updater function that accepts values appropriate for the collector type. diff --git a/packages/davinci-client/src/lib/collector.types.test-d.ts b/packages/davinci-client/src/lib/collector.types.test-d.ts index deafe24f8c..cc3cf3f7f2 100644 --- a/packages/davinci-client/src/lib/collector.types.test-d.ts +++ b/packages/davinci-client/src/lib/collector.types.test-d.ts @@ -14,6 +14,7 @@ import type { ActionCollectorNoUrl, TextCollector, PasswordCollector, + PasswordVerifyCollector, FlowCollector, IdpCollector, SubmitCollector, @@ -24,6 +25,7 @@ import type { InferMultiValueCollectorType, InferActionCollectorType, } from './collector.types.js'; +import type { PasswordPolicy } from './davinci.types.js'; describe('Collector Types', () => { describe('SingleValueCollector Types', () => { @@ -52,6 +54,23 @@ describe('Collector Types', () => { }>(); }); + it('should validate PasswordVerifyCollector structure', () => { + expectTypeOf() + .toHaveProperty('category') + .toEqualTypeOf<'SingleValueCollector'>(); + expectTypeOf() + .toHaveProperty('type') + .toEqualTypeOf<'PasswordVerifyCollector'>(); + expectTypeOf().toHaveProperty('passwordPolicy'); + expectTypeOf().toEqualTypeOf< + PasswordPolicy | undefined + >(); + }); + + it('should validate PasswordCollector output does NOT have passwordPolicy', () => { + expectTypeOf().not.toHaveProperty('passwordPolicy'); + }); + it('should validate SingleCollector structure', () => { expectTypeOf().toMatchTypeOf< SingleValueCollectorWithValue<'SingleSelectCollector'> @@ -267,6 +286,27 @@ describe('Collector Types', () => { expectTypeOf(tCollector).toMatchTypeOf(); }); + it('should correctly infer PasswordVerifyCollector Type', () => { + const tCollector: InferSingleValueCollectorType<'PasswordVerifyCollector'> = { + category: 'SingleValueCollector', + error: null, + type: 'PasswordVerifyCollector', + id: '', + name: '', + input: { + key: '', + value: '', + type: '', + }, + output: { + key: '', + label: '', + type: '', + }, + }; + + expectTypeOf(tCollector).toMatchTypeOf(); + }); it('should correctly infer SingleValueCollector Type', () => { const tCollector: InferSingleValueCollectorType<'SingleValueCollector'> = { category: 'SingleValueCollector', diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index e6c882a5c4..c98e0ce635 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -5,7 +5,11 @@ * of the MIT license. See the LICENSE file for details. */ -import type { FidoAuthenticationOptions, FidoRegistrationOptions } from './davinci.types.js'; +import type { + FidoAuthenticationOptions, + FidoRegistrationOptions, + PasswordPolicy, +} from './davinci.types.js'; /** ********************************************************************* * SINGLE-VALUE COLLECTORS @@ -16,6 +20,7 @@ import type { FidoAuthenticationOptions, FidoRegistrationOptions } from './davin */ export type SingleValueCollectorTypes = | 'PasswordCollector' + | 'PasswordVerifyCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' @@ -157,14 +162,16 @@ export type InferSingleValueCollectorType = ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector - : /** - * At this point, we have not passed in a collector type - * or we have explicitly passed in 'SingleValueCollector' - * So we can return either a SingleValueCollector with value - * or without a value. - **/ - | SingleValueCollectorWithValue<'SingleValueCollector'> - | SingleValueCollectorNoValue<'SingleValueCollector'>; + : T extends 'PasswordVerifyCollector' + ? PasswordVerifyCollector + : /** + * At this point, we have not passed in a collector type + * or we have explicitly passed in 'SingleValueCollector' + * So we can return either a SingleValueCollector with value + * or without a value. + **/ + | SingleValueCollectorWithValue<'SingleValueCollector'> + | SingleValueCollectorNoValue<'SingleValueCollector'>; /** * SINGLE-VALUE COLLECTOR TYPES @@ -175,12 +182,32 @@ export type SingleValueCollector = export type SingleValueCollectors = | SingleValueCollectorNoValue<'PasswordCollector'> + | PasswordVerifyCollector | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>; export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>; + +export interface PasswordVerifyCollector { + category: 'SingleValueCollector'; + error: string | null; + type: 'PasswordVerifyCollector'; + id: string; + name: string; + input: { + key: string; + value: string | number | boolean; + type: string; + }; + output: { + key: string; + label: string; + type: string; + passwordPolicy?: PasswordPolicy; + }; +} export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>; export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>; export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>; diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index ebc33f3c6b..f4da0341f1 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -12,6 +12,7 @@ import { returnSubmitCollector, returnSingleValueCollector, returnPasswordCollector, + returnPasswordVerifyCollector, returnTextCollector, returnSingleSelectCollector, returnMultiSelectCollector, @@ -28,6 +29,7 @@ import type { DaVinciField, DeviceAuthenticationField, DeviceRegistrationField, + PasswordVerifyField, FidoAuthenticationField, FidoRegistrationField, PhoneNumberField, @@ -1136,3 +1138,129 @@ describe('Return collector validator', () => { ); }); }); + +describe('returnPasswordVerifyCollector', () => { + const mockPasswordPolicy = { + id: '39cad7af-3c2f-4672-9c3f-c47e5169e582', + name: 'Standard', + length: { min: 8, max: 255 }, + minCharacters: { + '~!@#$%^&*()-_=+[]{}|;:,.<>/?': 1, + '0123456789': 1, + ABCDEFGHIJKLMNOPQRSTUVWXYZ: 1, + abcdefghijklmnopqrstuvwxyz: 1, + }, + }; + + it('should create a PasswordVerifyCollector with embedded passwordPolicy', () => { + const field: PasswordVerifyField = { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + required: true, + passwordPolicy: mockPasswordPolicy, + }; + + const result = returnPasswordVerifyCollector(field, 0); + + expect(result).toEqual({ + category: 'SingleValueCollector', + error: null, + type: 'PasswordVerifyCollector', + id: 'user.password-0', + name: 'user.password', + input: { + key: 'user.password', + value: '', + type: 'PASSWORD_VERIFY', + }, + output: { + key: 'user.password', + label: 'Password', + type: 'PASSWORD_VERIFY', + passwordPolicy: mockPasswordPolicy, + }, + }); + }); + + it('should create a PasswordVerifyCollector without passwordPolicy when field has none', () => { + const field: PasswordVerifyField = { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + }; + + const result = returnPasswordVerifyCollector(field, 1); + + expect(result).toEqual({ + category: 'SingleValueCollector', + error: null, + type: 'PasswordVerifyCollector', + id: 'user.password-1', + name: 'user.password', + input: { + key: 'user.password', + value: '', + type: 'PASSWORD_VERIFY', + }, + output: { + key: 'user.password', + label: 'Password', + type: 'PASSWORD_VERIFY', + }, + }); + expect(result.output).not.toHaveProperty('passwordPolicy'); + }); + + it('should fall back to root-level passwordPolicy when field has none', () => { + const field: PasswordVerifyField = { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + }; + const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } }; + + const result = returnPasswordVerifyCollector(field, 0, rootPolicy); + + expect(result.output.passwordPolicy).toEqual(rootPolicy); + }); + + it('should prefer component-level policy over root-level', () => { + const componentPolicy = { + id: 'component-policy', + name: 'Component', + length: { min: 8, max: 255 }, + }; + const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } }; + const field: PasswordVerifyField = { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + passwordPolicy: componentPolicy, + }; + + const result = returnPasswordVerifyCollector(field, 0, rootPolicy); + + expect(result.output.passwordPolicy).toEqual(componentPolicy); + }); + + it('should have no passwordPolicy when neither field nor root provides one', () => { + const field: PasswordVerifyField = { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + }; + + const result = returnPasswordVerifyCollector(field, 0, undefined); + + expect(result.output).not.toHaveProperty('passwordPolicy'); + }); + + it('should record errors when field is missing properties', () => { + const invalidField = {} as PasswordVerifyField; + const result = returnPasswordVerifyCollector(invalidField, 0); + expect(result.error).toContain('Key is not found'); + expect(result.error).toContain('Label is not found'); + expect(result.error).toContain('Type is not found'); + }); +}); diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index bbbdc2fdde..732464b0ab 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -30,6 +30,7 @@ import type { SingleValueAutoCollectorTypes, ObjectValueAutoCollectorTypes, QrCodeCollectorBase, + PasswordVerifyCollector, } from './collector.types.js'; import type { DeviceAuthenticationField, @@ -37,6 +38,8 @@ import type { FidoAuthenticationField, FidoRegistrationField, MultiSelectField, + PasswordPolicy, + PasswordVerifyField, PhoneNumberField, ProtectField, QrCodeField, @@ -439,6 +442,53 @@ export function returnPasswordCollector(field: StandardField, idx: number) { return returnSingleValueCollector(field, idx, 'PasswordCollector'); } +/** + * @function returnPasswordVerifyCollector - Creates a PasswordVerifyCollector with optional password policy. + * Component-level policy takes precedence over root-level (Solution 3: temporary duplication). + * @param {PasswordVerifyField} field - The PASSWORD_VERIFY field, possibly containing passwordPolicy. + * @param {number} idx - The index of the field in the form. + * @param {PasswordPolicy} [rootPasswordPolicy] - Optional root-level password policy for backward compatibility. + * @returns {PasswordVerifyCollector} The constructed PasswordVerifyCollector object. + */ +export function returnPasswordVerifyCollector( + field: PasswordVerifyField, + idx: number, + rootPasswordPolicy?: PasswordPolicy, +): PasswordVerifyCollector { + let error = ''; + if (!('key' in field)) { + error = `${error}Key is not found in the field object. `; + } + if (!('label' in field)) { + error = `${error}Label is not found in the field object. `; + } + if (!('type' in field)) { + error = `${error}Type is not found in the field object. `; + } + + // Component-level policy takes precedence over root-level + const passwordPolicy = field.passwordPolicy ?? rootPasswordPolicy; + + return { + category: 'SingleValueCollector', + error: error || null, + type: 'PasswordVerifyCollector', + id: `${field?.key}-${idx}`, + name: field.key, + input: { + key: field.key, + value: '', + type: field.type, + }, + output: { + key: field.key, + label: field.label, + type: field.type, + ...(passwordPolicy && { passwordPolicy }), + }, + }; +} + /** * @function returnTextCollector - Creates a TextCollector object based on the provided field and index. * @param {DaVinciField} field - The field object containing key, label, type, and links. diff --git a/packages/davinci-client/src/lib/davinci.types.ts b/packages/davinci-client/src/lib/davinci.types.ts index 2f97c3c63b..31b215030c 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -53,14 +53,7 @@ export interface Links { } export type StandardField = { - type: - | 'PASSWORD' - | 'PASSWORD_VERIFY' - | 'TEXT' - | 'SUBMIT_BUTTON' - | 'FLOW_BUTTON' - | 'FLOW_LINK' - | 'BUTTON'; + type: 'PASSWORD' | 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON'; key: string; label: string; @@ -68,6 +61,35 @@ export type StandardField = { required?: boolean; }; +export interface PasswordPolicy { + id?: string; + name?: string; + description?: string; + excludesProfileData?: boolean; + notSimilarToCurrent?: boolean; + excludesCommonlyUsed?: boolean; + maxAgeDays?: number; + minAgeDays?: number; + maxRepeatedCharacters?: number; + minUniqueCharacters?: number; + history?: { count?: number; retentionDays?: number }; + lockout?: { failureCount?: number; durationSeconds?: number }; + length?: { min?: number; max?: number }; + minCharacters?: Record; + populationCount?: number; + createdAt?: string; + updatedAt?: string; + default?: boolean; +} + +export type PasswordVerifyField = { + type: 'PASSWORD_VERIFY'; + key: string; + label: string; + required?: boolean; + passwordPolicy?: PasswordPolicy; +}; + export type ReadOnlyField = { type: 'LABEL'; content: string; @@ -240,7 +262,12 @@ export type ComplexValueFields = export type MultiValueFields = MultiSelectField; export type ReadOnlyFields = ReadOnlyField | QrCodeField; export type RedirectFields = RedirectField; -export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField; +export type SingleValueFields = + | StandardField + | PasswordVerifyField + | ValidatedField + | SingleSelectField + | ProtectField; export type DaVinciField = | ComplexValueFields @@ -269,6 +296,7 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse { fields?: DaVinciField[]; }; }; + passwordPolicy?: PasswordPolicy; } /** diff --git a/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts b/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts index 4962470b45..27f046ebeb 100644 --- a/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts +++ b/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts @@ -42,6 +42,49 @@ export const obj = { label: 'Password', required: true, }, + { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + required: true, + passwordPolicy: { + id: '39cad7af-3c2f-4672-9c3f-c47e5169e582', + environment: { + id: '02fb4743-189a-4bc7-9d6c-a919edfe6447', + }, + name: 'Standard', + description: 'A standard policy that incorporates industry best practices', + excludesProfileData: true, + notSimilarToCurrent: true, + excludesCommonlyUsed: true, + maxAgeDays: 182, + minAgeDays: 1, + maxRepeatedCharacters: 2, + minUniqueCharacters: 5, + history: { + count: 6, + retentionDays: 365, + }, + lockout: { + failureCount: 5, + durationSeconds: 900, + }, + length: { + min: 8, + max: 255, + }, + minCharacters: { + '~!@#$%^&*()-_=+[]{}|;:,.<>/?': 1, + '0123456789': 1, + ABCDEFGHIJKLMNOPQRSTUVWXYZ: 1, + abcdefghijklmnopqrstuvwxyz: 1, + }, + populationCount: 1, + createdAt: '2024-01-03T19:50:39.586Z', + updatedAt: '2024-01-03T19:50:39.586Z', + default: true, + }, + }, { type: 'SUBMIT_BUTTON', label: 'Sign On', @@ -140,6 +183,7 @@ export const obj = { value: { 'user.username': '', password: '', + 'user.password': '', 'dropdown-field': '', 'combobox-field': '', 'radio-field': '', @@ -163,14 +207,6 @@ export const obj = { themeId: 'activeTheme', formId: 'f0cf83ab-f8f4-4f4a-9260-8f7d27061fa7', passwordPolicy: { - _links: { - environment: { - href: 'http://10.76.247.190:4140/directory-api/environments/02fb4743-189a-4bc7-9d6c-a919edfe6447', - }, - self: { - href: 'http://10.76.247.190:4140/directory-api/environments/02fb4743-189a-4bc7-9d6c-a919edfe6447/passwordPolicies/39cad7af-3c2f-4672-9c3f-c47e5169e582', - }, - }, id: '39cad7af-3c2f-4672-9c3f-c47e5169e582', environment: { id: '02fb4743-189a-4bc7-9d6c-a919edfe6447', @@ -213,6 +249,7 @@ export const obj = { 'ERROR_DISPLAY', 'TEXT', 'PASSWORD', + 'PASSWORD_VERIFY', 'RADIO', 'CHECKBOX', 'FLOW_LINK', diff --git a/packages/davinci-client/src/lib/node.reducer.test.ts b/packages/davinci-client/src/lib/node.reducer.test.ts index b30d7d12b3..5a8f077b7b 100644 --- a/packages/davinci-client/src/lib/node.reducer.test.ts +++ b/packages/davinci-client/src/lib/node.reducer.test.ts @@ -13,6 +13,7 @@ import type { FidoAuthenticationCollector, FidoRegistrationCollector, MultiSelectCollector, + PasswordVerifyCollector, PhoneNumberCollector, PollingCollector, ProtectCollector, @@ -1190,6 +1191,143 @@ describe('The node collector reducer with pollCollectorValues', () => { }); }); +describe('PASSWORD_VERIFY with password policy', () => { + const mockPasswordPolicy = { + id: '39cad7af-3c2f-4672-9c3f-c47e5169e582', + name: 'Standard', + description: 'A standard policy that incorporates industry best practices', + length: { min: 8, max: 255 }, + minCharacters: { + '~!@#$%^&*()-_=+[]{}|;:,.<>/?': 1, + '0123456789': 1, + ABCDEFGHIJKLMNOPQRSTUVWXYZ: 1, + abcdefghijklmnopqrstuvwxyz: 1, + }, + }; + + it('should produce PasswordVerifyCollector with embedded passwordPolicy', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + required: true, + passwordPolicy: mockPasswordPolicy, + }, + ], + formData: {}, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect(result).toEqual([ + { + category: 'SingleValueCollector', + error: null, + type: 'PasswordVerifyCollector', + id: 'user.password-0', + name: 'user.password', + input: { + key: 'user.password', + value: '', + type: 'PASSWORD_VERIFY', + }, + output: { + key: 'user.password', + label: 'Password', + type: 'PASSWORD_VERIFY', + passwordPolicy: mockPasswordPolicy, + }, + } satisfies PasswordVerifyCollector, + ]); + }); + + it('should produce PasswordVerifyCollector without policy when field has none', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + }, + ], + formData: {}, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect(result[0].type).toBe('PasswordVerifyCollector'); + expect(result[0].output).not.toHaveProperty('passwordPolicy'); + }); + + it('should fall back to root-level passwordPolicy when field has no policy', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + }, + ], + formData: {}, + passwordPolicy: mockPasswordPolicy, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect(result[0].type).toBe('PasswordVerifyCollector'); + expect((result[0] as PasswordVerifyCollector).output.passwordPolicy).toEqual( + mockPasswordPolicy, + ); + }); + + it('should prefer component-level policy over root-level', () => { + const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } }; + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'PASSWORD_VERIFY', + key: 'user.password', + label: 'Password', + passwordPolicy: mockPasswordPolicy, + }, + ], + formData: {}, + passwordPolicy: rootPolicy, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect((result[0] as PasswordVerifyCollector).output.passwordPolicy).toEqual( + mockPasswordPolicy, + ); + }); + + it('should still produce PasswordCollector for PASSWORD type (no regression)', () => { + const action = { + type: 'node/next', + payload: { + fields: [ + { + type: 'PASSWORD', + key: 'password', + label: 'Password', + }, + ], + formData: {}, + }, + }; + const result = nodeCollectorReducer(undefined, action); + expect(result[0].type).toBe('PasswordCollector'); + expect(result[0].output).not.toHaveProperty('passwordPolicy'); + }); +}); + describe('The node collector reducer with FidoAuthenticationFieldValue', () => { it('should handle collector updates ', () => { // todo: declare inputValue type as FidoAuthenticationInputValue diff --git a/packages/davinci-client/src/lib/node.reducer.ts b/packages/davinci-client/src/lib/node.reducer.ts index e97a0af42e..c0ad6b042e 100644 --- a/packages/davinci-client/src/lib/node.reducer.ts +++ b/packages/davinci-client/src/lib/node.reducer.ts @@ -16,6 +16,7 @@ import { returnActionCollector, returnFlowCollector, returnPasswordCollector, + returnPasswordVerifyCollector, returnIdpCollector, returnSubmitCollector, returnTextCollector, @@ -31,13 +32,14 @@ import { returnFidoAuthenticationCollector, returnQrCodeCollector, } from './collector.utils.js'; -import type { DaVinciField, UnknownField } from './davinci.types.js'; +import type { DaVinciField, PasswordPolicy, UnknownField } from './davinci.types.js'; import type { ActionCollector, MultiSelectCollector, SingleSelectCollector, FlowCollector, PasswordCollector, + PasswordVerifyCollector, SingleValueCollector, IdpCollector, SubmitCollector, @@ -68,6 +70,7 @@ import type { export const nextCollectorValues = createAction<{ fields: DaVinciField[]; formData: { value: Record }; + passwordPolicy?: PasswordPolicy; }>('node/next'); export const updateCollectorValues = createAction<{ id: string; @@ -87,6 +90,7 @@ export const pollCollectorValues = createAction('node/poll'); const initialCollectorValues: ( | FlowCollector | PasswordCollector + | PasswordVerifyCollector | TextCollector | IdpCollector | SubmitCollector @@ -166,11 +170,14 @@ export const nodeCollectorReducer = createReducer(initialCollectorValues, (build // Intentional fall-through return returnObjectSelectCollector(field, idx); } - case 'PASSWORD': - case 'PASSWORD_VERIFY': { + case 'PASSWORD': { // No data to send return returnPasswordCollector(field, idx); } + case 'PASSWORD_VERIFY': { + // No data to send; policy may be embedded in field or at root level + return returnPasswordVerifyCollector(field, idx, action.payload.passwordPolicy); + } case 'PHONE_NUMBER': { const prefillData = data as PhoneNumberOutputValue; return returnObjectValueCollector(field, idx, prefillData); diff --git a/packages/davinci-client/src/lib/node.slice.ts b/packages/davinci-client/src/lib/node.slice.ts index 29d91342bd..8bc05400f0 100644 --- a/packages/davinci-client/src/lib/node.slice.ts +++ b/packages/davinci-client/src/lib/node.slice.ts @@ -185,6 +185,7 @@ export const nodeSlice = createSlice({ payload: { fields: action.payload.data?.form?.components?.fields, formData: action.payload.data?.formData, + passwordPolicy: action.payload.data?.passwordPolicy, }, }); diff --git a/packages/davinci-client/src/lib/node.types.test-d.ts b/packages/davinci-client/src/lib/node.types.test-d.ts index d2733abc0e..14008b87b1 100644 --- a/packages/davinci-client/src/lib/node.types.test-d.ts +++ b/packages/davinci-client/src/lib/node.types.test-d.ts @@ -21,6 +21,7 @@ import { FlowCollector, MultiSelectCollector, PasswordCollector, + PasswordVerifyCollector, ReadOnlyCollector, SingleSelectCollector, SingleValueCollector, @@ -223,6 +224,7 @@ describe('Node Types', () => { expectTypeOf().toMatchTypeOf< | TextCollector | PasswordCollector + | PasswordVerifyCollector | FlowCollector | IdpCollector | SubmitCollector diff --git a/packages/davinci-client/src/lib/node.types.ts b/packages/davinci-client/src/lib/node.types.ts index ecc01c5616..5c2535d45b 100644 --- a/packages/davinci-client/src/lib/node.types.ts +++ b/packages/davinci-client/src/lib/node.types.ts @@ -9,6 +9,7 @@ import { GenericError } from '@forgerock/sdk-types'; import type { FlowCollector, PasswordCollector, + PasswordVerifyCollector, TextCollector, IdpCollector, SubmitCollector, @@ -33,6 +34,7 @@ import type { Links } from './davinci.types.js'; export type Collectors = | FlowCollector | PasswordCollector + | PasswordVerifyCollector | TextCollector | SingleSelectCollector | IdpCollector diff --git a/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts b/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts index 7ba8b20d75..b1d6857921 100644 --- a/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts +++ b/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts @@ -10,6 +10,7 @@ import { describe, expectTypeOf, it } from 'vitest'; import type { Updater } from './client.types.js'; import type { PasswordCollector, + PasswordVerifyCollector, TextCollector, ValidatedTextCollector, SingleSelectCollector, @@ -29,6 +30,7 @@ import type { Collectors } from './node.types.js'; type MockUpdate = < T extends | PasswordCollector + | PasswordVerifyCollector | TextCollector | ValidatedTextCollector | SingleSelectCollector @@ -64,6 +66,22 @@ describe('Updater Type Narrowing with Real Usage Pattern', () => { } }); + it('PasswordVerifyCollector should narrow collector to PasswordVerifyCollector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'PasswordVerifyCollector') { + // 1. Collector itself should be narrowed to PasswordVerifyCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + it('TextCollector should narrow collector to TextCollector | ValidatedTextCollector', () => { const collector = {} as Collectors;