Skip to content

Commit 9911e8e

Browse files
committed
feat(davinci-client): embed password policy in PASSWORD_VERIFY collector
DV-16053: The DaVinci API moves passwordPolicy from the response root into the PASSWORD_VERIFY field component. This adds a new PasswordVerifyCollector type that reads the policy from the field and exposes it via output.passwordPolicy. - Add PasswordPolicy interface and PasswordVerifyField type - Add PasswordVerifyCollector with optional passwordPolicy in output - Split PASSWORD/PASSWORD_VERIFY into separate reducer cases - Update CollectorValueType, Collectors union, and type inference - Add unit tests, type tests, and updater narrowing tests - Update sample app to render password requirements from policy - Regenerate API reports
1 parent c82e795 commit 9911e8e

17 files changed

Lines changed: 679 additions & 118 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/davinci-client': minor
3+
---
4+
5+
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.

e2e/davinci-app/components/password.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
import type { PasswordCollector, Updater } from '@forgerock/davinci-client/types';
7+
import type {
8+
PasswordCollector,
9+
PasswordVerifyCollector,
10+
Updater,
11+
} from '@forgerock/davinci-client/types';
812
import { dotToCamelCase } from '../helper.js';
913

1014
export default function passwordComponent(
1115
formEl: HTMLFormElement,
12-
collector: PasswordCollector,
13-
updater: Updater<PasswordCollector>,
16+
collector: PasswordCollector | PasswordVerifyCollector,
17+
updater: Updater<PasswordCollector | PasswordVerifyCollector>,
1418
) {
1519
const label = document.createElement('label');
1620
const input = document.createElement('input');
@@ -24,6 +28,39 @@ export default function passwordComponent(
2428
formEl?.appendChild(label);
2529
formEl?.appendChild(input);
2630

31+
// Render password policy requirements if available
32+
if (collector.type === 'PasswordVerifyCollector' && collector.output.passwordPolicy) {
33+
const policy = collector.output.passwordPolicy;
34+
const requirementsList = document.createElement('ul');
35+
requirementsList.className = 'password-requirements';
36+
37+
if (policy.length) {
38+
const li = document.createElement('li');
39+
li.textContent = `${policy.length.min}${policy.length.max} characters`;
40+
requirementsList.appendChild(li);
41+
}
42+
43+
if (policy.minCharacters) {
44+
for (const [charset, count] of Object.entries(policy.minCharacters)) {
45+
const li = document.createElement('li');
46+
if (charset.match(/^[A-Z]+$/)) {
47+
li.textContent = `At least ${count} uppercase letter(s)`;
48+
} else if (charset.match(/^[a-z]+$/)) {
49+
li.textContent = `At least ${count} lowercase letter(s)`;
50+
} else if (charset.match(/^[0-9]+$/)) {
51+
li.textContent = `At least ${count} number(s)`;
52+
} else {
53+
li.textContent = `At least ${count} special character(s)`;
54+
}
55+
requirementsList.appendChild(li);
56+
}
57+
}
58+
59+
if (requirementsList.children.length > 0) {
60+
formEl?.appendChild(requirementsList);
61+
}
62+
}
63+
2764
formEl
2865
?.querySelector(`#${dotToCamelCase(collector.output.key)}`)
2966
?.addEventListener('blur', (event: Event) => {

e2e/davinci-app/main.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,10 @@ const urlParams = new URLSearchParams(window.location.search);
237237
davinciClient.update(collector), // Returns an update function for this collector
238238
davinciClient.validate(collector), // Returns a validate function for this collector
239239
);
240-
} else if (collector.type === 'PasswordCollector') {
241-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
242-
collector;
240+
} else if (
241+
collector.type === 'PasswordCollector' ||
242+
collector.type === 'PasswordVerifyCollector'
243+
) {
243244
passwordComponent(
244245
formEl, // You can ignore this; it's just for rendering
245246
collector, // This is the plain object of the collector

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,13 @@ export interface CollectorErrors {
178178
}
179179

180180
// @public (undocumented)
181-
export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector;
181+
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 | AgreementCollector | UnknownCollector;
182182

183183
// @public
184184
export type CollectorValueType<T> = T extends {
185185
type: 'PasswordCollector';
186+
} ? string : T extends {
187+
type: 'PasswordVerifyCollector';
186188
} ? string : T extends {
187189
type: 'TextCollector';
188190
category: 'SingleValueCollector';
@@ -267,13 +269,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
267269
resume: (input: {
268270
continueToken: string;
269271
}) => Promise<InternalErrorResponse | NodeStates>;
270-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
272+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
271273
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
272274
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
273275
poll: (collector: PollingCollector) => Poller;
274276
getClient: () => {
275-
status: "start";
276-
} | {
277277
action: string;
278278
collectors: Collectors[];
279279
description?: string;
@@ -287,6 +287,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
287287
status: "error";
288288
} | {
289289
status: "failure";
290+
} | {
291+
status: "start";
290292
} | {
291293
authorization?: {
292294
code?: string;
@@ -297,7 +299,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
297299
getCollectors: () => Collectors[];
298300
getError: () => DaVinciError | null;
299301
getErrorCollectors: () => CollectorErrors[];
300-
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
302+
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
301303
getServer: () => {
302304
_links?: Links;
303305
id?: string;
@@ -306,8 +308,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
306308
href?: string;
307309
eventName?: string;
308310
status: "continue";
309-
} | {
310-
status: "start";
311311
} | {
312312
_links?: Links;
313313
eventName?: string;
@@ -323,6 +323,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
323323
interactionId?: string;
324324
interactionToken?: string;
325325
status: "failure";
326+
} | {
327+
status: "start";
326328
} | {
327329
_links?: Links;
328330
eventName?: string;
@@ -1032,7 +1034,7 @@ export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T
10321034
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : T extends 'AgreementCollector' ? AgreementCollector : NoValueCollectorBase<'NoValueCollector'>;
10331035

10341036
// @public
1035-
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'>;
1037+
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'>;
10361038

10371039
// @public (undocumented)
10381040
export type InferValueObjectCollectorType<T extends ObjectValueCollectorTypes> = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>;
@@ -1170,8 +1172,8 @@ value: Record<string, unknown>;
11701172
}, string>;
11711173

11721174
// @public
1173-
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
1174-
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
1175+
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
1176+
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | PasswordVerifyCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
11751177
};
11761178

11771179
// @public (undocumented)
@@ -1325,6 +1327,91 @@ export interface OutgoingQueryParams {
13251327
// @public (undocumented)
13261328
export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'>;
13271329

1330+
// @public (undocumented)
1331+
export interface PasswordPolicy {
1332+
// (undocumented)
1333+
createdAt?: string;
1334+
// (undocumented)
1335+
default?: boolean;
1336+
// (undocumented)
1337+
description?: string;
1338+
// (undocumented)
1339+
excludesCommonlyUsed?: boolean;
1340+
// (undocumented)
1341+
excludesProfileData?: boolean;
1342+
// (undocumented)
1343+
history?: {
1344+
count?: number;
1345+
retentionDays?: number;
1346+
};
1347+
// (undocumented)
1348+
id?: string;
1349+
// (undocumented)
1350+
length?: {
1351+
min?: number;
1352+
max?: number;
1353+
};
1354+
// (undocumented)
1355+
lockout?: {
1356+
failureCount?: number;
1357+
durationSeconds?: number;
1358+
};
1359+
// (undocumented)
1360+
maxAgeDays?: number;
1361+
// (undocumented)
1362+
maxRepeatedCharacters?: number;
1363+
// (undocumented)
1364+
minAgeDays?: number;
1365+
// (undocumented)
1366+
minCharacters?: Record<string, number>;
1367+
// (undocumented)
1368+
minUniqueCharacters?: number;
1369+
// (undocumented)
1370+
name?: string;
1371+
// (undocumented)
1372+
notSimilarToCurrent?: boolean;
1373+
// (undocumented)
1374+
populationCount?: number;
1375+
// (undocumented)
1376+
updatedAt?: string;
1377+
}
1378+
1379+
// @public (undocumented)
1380+
export interface PasswordVerifyCollector {
1381+
// (undocumented)
1382+
category: 'SingleValueCollector';
1383+
// (undocumented)
1384+
error: string | null;
1385+
// (undocumented)
1386+
id: string;
1387+
// (undocumented)
1388+
input: {
1389+
key: string;
1390+
value: string | number | boolean;
1391+
type: string;
1392+
};
1393+
// (undocumented)
1394+
name: string;
1395+
// (undocumented)
1396+
output: {
1397+
key: string;
1398+
label: string;
1399+
type: string;
1400+
passwordPolicy?: PasswordPolicy;
1401+
};
1402+
// (undocumented)
1403+
type: 'PasswordVerifyCollector';
1404+
}
1405+
1406+
// @public (undocumented)
1407+
export type PasswordVerifyField = {
1408+
type: 'PASSWORD_VERIFY';
1409+
key: string;
1410+
label: string;
1411+
required?: boolean;
1412+
passwordPolicy?: PasswordPolicy;
1413+
};
1414+
13281415
// @public (undocumented)
13291416
export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>;
13301417

@@ -1588,10 +1675,10 @@ export interface SingleValueCollectorNoValue<T extends SingleValueCollectorTypes
15881675
}
15891676

15901677
// @public (undocumented)
1591-
export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>;
1678+
export type SingleValueCollectors = SingleValueCollectorNoValue<'PasswordCollector'> | PasswordVerifyCollector | SingleSelectCollectorWithValue<'SingleSelectCollector'> | SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorWithValue<'TextCollector'> | ValidatedSingleValueCollectorWithValue<'TextCollector'>;
15921679

15931680
// @public
1594-
export type SingleValueCollectorTypes = 'PasswordCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector';
1681+
export type SingleValueCollectorTypes = 'PasswordCollector' | 'PasswordVerifyCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector';
15951682

15961683
// @public (undocumented)
15971684
export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
@@ -1621,11 +1708,11 @@ export interface SingleValueCollectorWithValue<T extends SingleValueCollectorTyp
16211708
}
16221709

16231710
// @public (undocumented)
1624-
export type SingleValueFields = StandardField | ValidatedField | SingleSelectField | ProtectField;
1711+
export type SingleValueFields = StandardField | PasswordVerifyField | ValidatedField | SingleSelectField | ProtectField;
16251712

16261713
// @public (undocumented)
16271714
export type StandardField = {
1628-
type: 'PASSWORD' | 'PASSWORD_VERIFY' | 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON';
1715+
type: 'PASSWORD' | 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON';
16291716
key: string;
16301717
label: string;
16311718
required?: boolean;

0 commit comments

Comments
 (0)