Skip to content

Commit 89159b3

Browse files
committed
feat(davinci-client): support Solution 3 root-level passwordPolicy fallback
Component-level policy takes precedence; falls back to root-level passwordPolicy for backward compatibility during the migration period. - Add passwordPolicy? to DaVinciNextResponse - Thread root-level policy through node.slice → reducer → factory - returnPasswordVerifyCollector prefers field.passwordPolicy ?? rootPasswordPolicy - Add tests for fallback, precedence, and no-policy cases - Restore root-level passwordPolicy in mock data (Solution 3 shape)
1 parent 9911e8e commit 89159b3

9 files changed

Lines changed: 182 additions & 40 deletions

File tree

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

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -269,16 +269,12 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
269269
resume: (input: {
270270
continueToken: string;
271271
}) => Promise<InternalErrorResponse | NodeStates>;
272-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
272+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<StartNode | ErrorNode | FailureNode | ContinueNode | SuccessNode>;
273273
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
274274
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
275275
poll: (collector: PollingCollector) => Poller;
276276
getClient: () => {
277-
action: string;
278-
collectors: Collectors[];
279-
description?: string;
280-
name?: string;
281-
status: "continue";
277+
status: "start";
282278
} | {
283279
action: string;
284280
collectors: Collectors[];
@@ -288,7 +284,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
288284
} | {
289285
status: "failure";
290286
} | {
291-
status: "start";
287+
action: string;
288+
collectors: Collectors[];
289+
description?: string;
290+
name?: string;
291+
status: "continue";
292292
} | {
293293
authorization?: {
294294
code?: string;
@@ -299,15 +299,9 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
299299
getCollectors: () => Collectors[];
300300
getError: () => DaVinciError | null;
301301
getErrorCollectors: () => CollectorErrors[];
302-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
302+
getNode: () => StartNode | ErrorNode | FailureNode | ContinueNode | SuccessNode;
303303
getServer: () => {
304-
_links?: Links;
305-
id?: string;
306-
interactionId?: string;
307-
interactionToken?: string;
308-
href?: string;
309-
eventName?: string;
310-
status: "continue";
304+
status: "start";
311305
} | {
312306
_links?: Links;
313307
eventName?: string;
@@ -324,7 +318,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
324318
interactionToken?: string;
325319
status: "failure";
326320
} | {
327-
status: "start";
321+
_links?: Links;
322+
id?: string;
323+
interactionId?: string;
324+
interactionToken?: string;
325+
href?: string;
326+
eventName?: string;
327+
status: "continue";
328328
} | {
329329
_links?: Links;
330330
eventName?: string;
@@ -682,6 +682,8 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
682682
};
683683
// (undocumented)
684684
_links?: Links;
685+
// (undocumented)
686+
passwordPolicy?: PasswordPolicy;
685687
}
686688

687689
// @public
@@ -1169,11 +1171,12 @@ fields: DaVinciField[];
11691171
formData: {
11701172
value: Record<string, unknown>;
11711173
};
1174+
passwordPolicy?: PasswordPolicy;
11721175
}, string>;
11731176

11741177
// @public
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">)[];
1178+
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | PasswordCollector | PasswordVerifyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | QrCodeCollectorBase | AgreementCollector | UnknownCollector | IdpCollector | FlowCollector | SubmitCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector"> | MultiSelectCollector | ReadOnlyCollector)[]> & {
1179+
getInitialState: () => (TextCollector | SingleSelectCollector | PasswordCollector | PasswordVerifyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | QrCodeCollectorBase | AgreementCollector | UnknownCollector | IdpCollector | FlowCollector | SubmitCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector"> | MultiSelectCollector | ReadOnlyCollector)[];
11771180
};
11781181

11791182
// @public (undocumented)

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

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -269,16 +269,12 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
269269
resume: (input: {
270270
continueToken: string;
271271
}) => Promise<InternalErrorResponse | NodeStates>;
272-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
272+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<StartNode | ErrorNode | FailureNode | ContinueNode | SuccessNode>;
273273
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
274274
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
275275
poll: (collector: PollingCollector) => Poller;
276276
getClient: () => {
277-
action: string;
278-
collectors: Collectors[];
279-
description?: string;
280-
name?: string;
281-
status: "continue";
277+
status: "start";
282278
} | {
283279
action: string;
284280
collectors: Collectors[];
@@ -288,7 +284,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
288284
} | {
289285
status: "failure";
290286
} | {
291-
status: "start";
287+
action: string;
288+
collectors: Collectors[];
289+
description?: string;
290+
name?: string;
291+
status: "continue";
292292
} | {
293293
authorization?: {
294294
code?: string;
@@ -299,15 +299,9 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
299299
getCollectors: () => Collectors[];
300300
getError: () => DaVinciError | null;
301301
getErrorCollectors: () => CollectorErrors[];
302-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
302+
getNode: () => StartNode | ErrorNode | FailureNode | ContinueNode | SuccessNode;
303303
getServer: () => {
304-
_links?: Links;
305-
id?: string;
306-
interactionId?: string;
307-
interactionToken?: string;
308-
href?: string;
309-
eventName?: string;
310-
status: "continue";
304+
status: "start";
311305
} | {
312306
_links?: Links;
313307
eventName?: string;
@@ -324,7 +318,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
324318
interactionToken?: string;
325319
status: "failure";
326320
} | {
327-
status: "start";
321+
_links?: Links;
322+
id?: string;
323+
interactionId?: string;
324+
interactionToken?: string;
325+
href?: string;
326+
eventName?: string;
327+
status: "continue";
328328
} | {
329329
_links?: Links;
330330
eventName?: string;
@@ -682,6 +682,8 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
682682
};
683683
// (undocumented)
684684
_links?: Links;
685+
// (undocumented)
686+
passwordPolicy?: PasswordPolicy;
685687
}
686688

687689
// @public
@@ -1166,11 +1168,12 @@ fields: DaVinciField[];
11661168
formData: {
11671169
value: Record<string, unknown>;
11681170
};
1171+
passwordPolicy?: PasswordPolicy;
11691172
}, string>;
11701173

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

11761179
// @public (undocumented)

packages/davinci-client/src/lib/collector.utils.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,50 @@ describe('returnPasswordVerifyCollector', () => {
12571257
expect(result.output).not.toHaveProperty('passwordPolicy');
12581258
});
12591259

1260+
it('should fall back to root-level passwordPolicy when field has none', () => {
1261+
const field: PasswordVerifyField = {
1262+
type: 'PASSWORD_VERIFY',
1263+
key: 'user.password',
1264+
label: 'Password',
1265+
};
1266+
const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } };
1267+
1268+
const result = returnPasswordVerifyCollector(field, 0, rootPolicy);
1269+
1270+
expect(result.output.passwordPolicy).toEqual(rootPolicy);
1271+
});
1272+
1273+
it('should prefer component-level policy over root-level', () => {
1274+
const componentPolicy = {
1275+
id: 'component-policy',
1276+
name: 'Component',
1277+
length: { min: 8, max: 255 },
1278+
};
1279+
const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } };
1280+
const field: PasswordVerifyField = {
1281+
type: 'PASSWORD_VERIFY',
1282+
key: 'user.password',
1283+
label: 'Password',
1284+
passwordPolicy: componentPolicy,
1285+
};
1286+
1287+
const result = returnPasswordVerifyCollector(field, 0, rootPolicy);
1288+
1289+
expect(result.output.passwordPolicy).toEqual(componentPolicy);
1290+
});
1291+
1292+
it('should have no passwordPolicy when neither field nor root provides one', () => {
1293+
const field: PasswordVerifyField = {
1294+
type: 'PASSWORD_VERIFY',
1295+
key: 'user.password',
1296+
label: 'Password',
1297+
};
1298+
1299+
const result = returnPasswordVerifyCollector(field, 0, undefined);
1300+
1301+
expect(result.output).not.toHaveProperty('passwordPolicy');
1302+
});
1303+
12601304
it('should record errors when field is missing properties', () => {
12611305
const invalidField = {} as PasswordVerifyField;
12621306
const result = returnPasswordVerifyCollector(invalidField, 0);

packages/davinci-client/src/lib/collector.utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type {
3939
FidoAuthenticationField,
4040
FidoRegistrationField,
4141
MultiSelectField,
42+
PasswordPolicy,
4243
PasswordVerifyField,
4344
PhoneNumberField,
4445
ProtectField,
@@ -446,13 +447,16 @@ export function returnPasswordCollector(field: StandardField, idx: number) {
446447

447448
/**
448449
* @function returnPasswordVerifyCollector - Creates a PasswordVerifyCollector with optional password policy.
450+
* Component-level policy takes precedence over root-level (Solution 3: temporary duplication).
449451
* @param {PasswordVerifyField} field - The PASSWORD_VERIFY field, possibly containing passwordPolicy.
450452
* @param {number} idx - The index of the field in the form.
453+
* @param {PasswordPolicy} [rootPasswordPolicy] - Optional root-level password policy for backward compatibility.
451454
* @returns {PasswordVerifyCollector} The constructed PasswordVerifyCollector object.
452455
*/
453456
export function returnPasswordVerifyCollector(
454457
field: PasswordVerifyField,
455458
idx: number,
459+
rootPasswordPolicy?: PasswordPolicy,
456460
): PasswordVerifyCollector {
457461
let error = '';
458462
if (!('key' in field)) {
@@ -465,6 +469,9 @@ export function returnPasswordVerifyCollector(
465469
error = `${error}Type is not found in the field object. `;
466470
}
467471

472+
// Component-level policy takes precedence over root-level
473+
const passwordPolicy = field.passwordPolicy ?? rootPasswordPolicy;
474+
468475
return {
469476
category: 'SingleValueCollector',
470477
error: error || null,
@@ -480,7 +487,7 @@ export function returnPasswordVerifyCollector(
480487
key: field.key,
481488
label: field.label,
482489
type: field.type,
483-
...(field.passwordPolicy && { passwordPolicy: field.passwordPolicy }),
490+
...(passwordPolicy && { passwordPolicy }),
484491
},
485492
};
486493
}

packages/davinci-client/src/lib/davinci.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ export interface DaVinciNextResponse extends DaVinciBaseResponse {
310310
fields?: DaVinciField[];
311311
};
312312
};
313+
passwordPolicy?: PasswordPolicy;
313314
}
314315

315316
/**

packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,43 @@ export const obj = {
206206
region: 'CA',
207207
themeId: 'activeTheme',
208208
formId: 'f0cf83ab-f8f4-4f4a-9260-8f7d27061fa7',
209+
passwordPolicy: {
210+
id: '39cad7af-3c2f-4672-9c3f-c47e5169e582',
211+
environment: {
212+
id: '02fb4743-189a-4bc7-9d6c-a919edfe6447',
213+
},
214+
name: 'Standard',
215+
description: 'A standard policy that incorporates industry best practices',
216+
excludesProfileData: true,
217+
notSimilarToCurrent: true,
218+
excludesCommonlyUsed: true,
219+
maxAgeDays: 182,
220+
minAgeDays: 1,
221+
maxRepeatedCharacters: 2,
222+
minUniqueCharacters: 5,
223+
history: {
224+
count: 6,
225+
retentionDays: 365,
226+
},
227+
lockout: {
228+
failureCount: 5,
229+
durationSeconds: 900,
230+
},
231+
length: {
232+
min: 8,
233+
max: 255,
234+
},
235+
minCharacters: {
236+
'~!@#$%^&*()-_=+[]{}|;:,.<>/?': 1,
237+
'0123456789': 1,
238+
ABCDEFGHIJKLMNOPQRSTUVWXYZ: 1,
239+
abcdefghijklmnopqrstuvwxyz: 1,
240+
},
241+
populationCount: 1,
242+
createdAt: '2024-01-03T19:50:39.586Z',
243+
updatedAt: '2024-01-03T19:50:39.586Z',
244+
default: true,
245+
},
209246
isResponseCompatibleWithMobileAndWebSdks: true,
210247
fieldTypes: [
211248
'LABEL',

packages/davinci-client/src/lib/node.reducer.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,51 @@ describe('PASSWORD_VERIFY with password policy', () => {
13081308
expect(result[0].output).not.toHaveProperty('passwordPolicy');
13091309
});
13101310

1311+
it('should fall back to root-level passwordPolicy when field has no policy', () => {
1312+
const action = {
1313+
type: 'node/next',
1314+
payload: {
1315+
fields: [
1316+
{
1317+
type: 'PASSWORD_VERIFY',
1318+
key: 'user.password',
1319+
label: 'Password',
1320+
},
1321+
],
1322+
formData: {},
1323+
passwordPolicy: mockPasswordPolicy,
1324+
},
1325+
};
1326+
const result = nodeCollectorReducer(undefined, action);
1327+
expect(result[0].type).toBe('PasswordVerifyCollector');
1328+
expect((result[0] as PasswordVerifyCollector).output.passwordPolicy).toEqual(
1329+
mockPasswordPolicy,
1330+
);
1331+
});
1332+
1333+
it('should prefer component-level policy over root-level', () => {
1334+
const rootPolicy = { id: 'root-policy', name: 'Root', length: { min: 6, max: 128 } };
1335+
const action = {
1336+
type: 'node/next',
1337+
payload: {
1338+
fields: [
1339+
{
1340+
type: 'PASSWORD_VERIFY',
1341+
key: 'user.password',
1342+
label: 'Password',
1343+
passwordPolicy: mockPasswordPolicy,
1344+
},
1345+
],
1346+
formData: {},
1347+
passwordPolicy: rootPolicy,
1348+
},
1349+
};
1350+
const result = nodeCollectorReducer(undefined, action);
1351+
expect((result[0] as PasswordVerifyCollector).output.passwordPolicy).toEqual(
1352+
mockPasswordPolicy,
1353+
);
1354+
});
1355+
13111356
it('should still produce PasswordCollector for PASSWORD type (no regression)', () => {
13121357
const action = {
13131358
type: 'node/next',

0 commit comments

Comments
 (0)