Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/embed-password-policy-in-component.md
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 40 additions & 3 deletions e2e/davinci-app/components/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PasswordCollector>,
collector: PasswordCollector | PasswordVerifyCollector,
updater: Updater<PasswordCollector | PasswordVerifyCollector>,
) {
const label = document.createElement('label');
const input = document.createElement('input');
Expand All @@ -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);
}
Comment on lines +37 to +41
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle optional length.min / length.max independently.

PasswordPolicy.length has both min and max as optional. When only one is set, the current template renders e.g. "undefined–255 characters" or "8–undefined characters". Consider branching on each side (or guarding the whole block on both being present).

🛠️ Suggested fix
-    if (policy.length) {
-      const li = document.createElement('li');
-      li.textContent = `${policy.length.min}–${policy.length.max} characters`;
-      requirementsList.appendChild(li);
-    }
+    if (policy.length && (policy.length.min != null || policy.length.max != null)) {
+      const li = document.createElement('li');
+      const { min, max } = policy.length;
+      if (min != null && max != null) {
+        li.textContent = `${min}–${max} characters`;
+      } else if (min != null) {
+        li.textContent = `At least ${min} characters`;
+      } else {
+        li.textContent = `At most ${max} characters`;
+      }
+      requirementsList.appendChild(li);
+    }
📝 Committable suggestion

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

Suggested change
if (policy.length) {
const li = document.createElement('li');
li.textContent = `${policy.length.min}${policy.length.max} characters`;
requirementsList.appendChild(li);
}
if (policy.length && (policy.length.min != null || policy.length.max != null)) {
const li = document.createElement('li');
const { min, max } = policy.length;
if (min != null && max != null) {
li.textContent = `${min}${max} characters`;
} else if (min != null) {
li.textContent = `At least ${min} characters`;
} else {
li.textContent = `At most ${max} characters`;
}
requirementsList.appendChild(li);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/davinci-app/components/password.ts` around lines 37 - 41, The current
rendering uses policy.length.min and policy.length.max directly, which produces
"undefined" when one side is missing; update the logic around policy.length to
check length.min and length.max independently (e.g., if both present render
"min–max characters", if only min render "at least {min} characters", if only
max render "up to {max} characters"), only create/append the li element to
requirementsList when at least one of length.min or length.max exists, and use
the existing li variable for the text content assignment.


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) => {
Expand Down
7 changes: 4 additions & 3 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 98 additions & 8 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T extends {
type: 'PasswordCollector';
} ? string : T extends {
type: 'PasswordVerifyCollector';
} ? string : T extends {
type: 'TextCollector';
category: 'SingleValueCollector';
Expand Down Expand Up @@ -649,6 +651,8 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
};
// (undocumented)
_links?: Links;
// (undocumented)
passwordPolicy?: PasswordPolicy;
}

// @public
Expand Down Expand Up @@ -1001,7 +1005,7 @@ export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>;

// @public
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = 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 SingleValueCollectorTypes> = 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 ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;
Expand Down Expand Up @@ -1136,11 +1140,12 @@ fields: DaVinciField[];
formData: {
value: Record<string, unknown>;
};
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)
Expand Down Expand Up @@ -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<string, number>;
// (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>;

Expand Down Expand Up @@ -1557,10 +1647,10 @@ export interface SingleValueCollectorNoValue<T extends SingleValueCollectorTypes
}

// @public (undocumented)
export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | 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<T extends SingleValueCollectorTypes> {
Expand Down Expand Up @@ -1590,11 +1680,11 @@ export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTyp
}

// @public (undocumented)
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;
export type SingleValueFields = StandardField | PasswordVerifyField | ValidatedField | SingleSelectField | ProtectField;

// @public (undocumented)
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;
required?: boolean;
Expand Down
Loading
Loading