Skip to content

Commit a369259

Browse files
authored
Merge pull request #340 from ForgeRock/SDKS-4146-protect-collector
SDKS-4146: Implement Protect collector in DaVinci client
2 parents 5e51d67 + 0fa522a commit a369259

16 files changed

Lines changed: 233 additions & 29 deletions

.changeset/dull-rockets-give.md

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+
Implemented Ping Protect collector

e2e/davinci-app/components/protect.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
Updater,
1111
} from '@forgerock/davinci-client/types';
1212

13-
export default function (
13+
export default function protectComponent(
1414
formEl: HTMLFormElement,
1515
collector: TextCollector | ValidatedTextCollector,
1616
updater: Updater,
@@ -20,7 +20,6 @@ export default function (
2020

2121
p.innerText = collector.output.label;
2222
formEl?.appendChild(p);
23-
2423
const error = updater('fakeprofile');
2524
if (error && 'error' in error) {
2625
console.error(error.error.message);

e2e/davinci-app/main.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import type {
1313
DaVinciConfig,
1414
DavinciClient,
1515
GetClient,
16+
ProtectCollector,
1617
RequestMiddleware,
1718
} from '@forgerock/davinci-client/types';
19+
import { protect } from '@pingidentity/protect';
1820

1921
import textComponent from './components/text.js';
2022
import passwordComponent from './components/password.js';
2123
import submitButtonComponent from './components/submit-button.js';
22-
import protect from './components/protect.js';
24+
import protectComponent from './components/protect.js';
2325
import flowLinkComponent from './components/flow-link.js';
2426
import socialLoginButtonComponent from './components/social-login-button.js';
2527
import { serverConfigs } from './server-configs.js';
@@ -77,10 +79,14 @@ const urlParams = new URLSearchParams(window.location.search);
7779

7880
(async () => {
7981
const davinciClient: DavinciClient = await davinci({ config, logger, requestMiddleware });
82+
const protectAPI = await protect({ envId: '02fb4743-189a-4bc7-9d6c-a919edfe6447' });
8083
const continueToken = urlParams.get('continueToken');
8184
const formEl = document.getElementById('form') as HTMLFormElement;
8285
let resumed: any;
8386

87+
// Initialize Protect
88+
await protectAPI.start();
89+
8490
if (continueToken) {
8591
resumed = await davinciClient.resume({ continueToken });
8692
} else {
@@ -186,11 +192,12 @@ const urlParams = new URLSearchParams(window.location.search);
186192
}
187193

188194
const collectors = davinciClient.getCollectors();
195+
189196
collectors.forEach((collector) => {
190197
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') {
191198
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
192199
collector;
193-
protect(
200+
protectComponent(
194201
formEl, // You can ignore this; it's just for rendering
195202
collector, // This is the plain object of the collector
196203
davinciClient.update(collector), // Returns an update function for this collector
@@ -238,7 +245,6 @@ const urlParams = new URLSearchParams(window.location.search);
238245
submitForm,
239246
);
240247
} else if (collector.type === 'IdpCollector') {
241-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
242248
socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp());
243249
} else if (collector.type === 'FlowCollector') {
244250
flowLinkComponent(
@@ -257,11 +263,24 @@ const urlParams = new URLSearchParams(window.location.search);
257263
}
258264
});
259265

260-
if (davinciClient.getCollectors().find((collector) => collector.name === 'protectsdk')) {
266+
if (
267+
davinciClient
268+
.getCollectors()
269+
.find((collector) => collector.type === 'TextCollector' && collector.name === 'protectsdk')
270+
) {
261271
submitForm();
262272
}
263273
}
264274

275+
async function updateProtectCollector(protectCollector: ProtectCollector) {
276+
const data = await protectAPI.getData();
277+
const updater = davinciClient.update(protectCollector);
278+
const error = updater(data);
279+
if (error && 'error' in error) {
280+
console.error(error.error.message);
281+
}
282+
}
283+
265284
async function submitForm() {
266285
const newNode = await davinciClient.next();
267286

@@ -311,6 +330,15 @@ const urlParams = new URLSearchParams(window.location.search);
311330

312331
formEl.addEventListener('submit', async (event) => {
313332
event.preventDefault();
333+
334+
// Evaluate Protect data
335+
const protectCollector = davinciClient
336+
.getCollectors()
337+
.find((collector) => collector.type === 'ProtectCollector');
338+
if (protectCollector) {
339+
await updateProtectCollector(protectCollector);
340+
}
341+
314342
/**
315343
* We can just call `next` here and not worry about passing any arguments
316344
*/

e2e/davinci-app/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"dependencies": {
1717
"@forgerock/davinci-client": "workspace:*",
1818
"@forgerock/javascript-sdk": "4.7.0",
19-
"@forgerock/sdk-logger": "workspace:*"
19+
"@forgerock/sdk-logger": "workspace:*",
20+
"@pingidentity/protect": "workspace:*"
2021
},
2122
"devDependencies": {}
2223
}

e2e/davinci-app/tsconfig.app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
"components/**/*.ts"
1313
],
1414
"references": [
15+
{
16+
"path": "../../packages/protect/tsconfig.lib.json"
17+
},
1518
{
1619
"path": "../../packages/sdk-effects/logger/tsconfig.lib.json"
1720
},

e2e/davinci-app/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
"skipLibCheck": true
1515
},
1616
"references": [
17+
{
18+
"path": "../../packages/protect"
19+
},
1720
{
1821
"path": "../../packages/sdk-effects/logger"
1922
},

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import type {
3232
MultiSelectCollector,
3333
ObjectValueCollectors,
3434
PhoneNumberInputValue,
35+
AutoCollectors,
3536
} from './collector.types.js';
3637
import type {
3738
InitFlow,
@@ -227,11 +228,15 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
227228

228229
/**
229230
* @method update - Exclusive method for updating the current node with user provided values
230-
* @param {SingleValueCollector} collector - the collector to update
231+
* @param {SingleValueCollector | MultiSelectCollector | ObjectValueCollectors | AutoCollectors} collector - the collector to update
231232
* @returns {function} - a function to call for updating collector value
232233
*/
233234
update: (
234-
collector: SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors,
235+
collector:
236+
| SingleValueCollectors
237+
| MultiSelectCollector
238+
| ObjectValueCollectors
239+
| AutoCollectors,
235240
): Updater => {
236241
if (!collector.id) {
237242
return handleUpdateValidateError(
@@ -259,10 +264,11 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
259264
collectorToUpdate.category !== 'MultiValueCollector' &&
260265
collectorToUpdate.category !== 'SingleValueCollector' &&
261266
collectorToUpdate.category !== 'ValidatedSingleValueCollector' &&
262-
collectorToUpdate.category !== 'ObjectValueCollector'
267+
collectorToUpdate.category !== 'ObjectValueCollector' &&
268+
collectorToUpdate.category !== 'SingleValueAutoCollector'
263269
) {
264270
return handleUpdateValidateError(
265-
'Collector is not a MultiValueCollector, SingleValueCollector or ValidatedSingleValueCollector and cannot be updated',
271+
'Collector is not a MultiValueCollector, SingleValueCollector, ValidatedSingleValueCollector, ObjectValueCollector, or SingleValueAutoCollector and cannot be updated',
266272
'state_error',
267273
log.error,
268274
);

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

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'Tex
182182
*/
183183

184184
/**
185-
* @interface MultiValueCollector - Represents a request to collect a single value from the user, like email or password.
185+
* @interface MultiValueCollector - Represents a request to collect multiple values from the user.
186186
*/
187187
export type MultiValueCollectorTypes = 'MultiSelectCollector' | 'MultiValueCollector';
188188

@@ -468,7 +468,7 @@ export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>;
468468
*/
469469

470470
/**
471-
* @interface NoValueCollector - Represents a collect that collects no value; text only for display.
471+
* @interface NoValueCollector - Represents a collector that collects no value; text only for display.
472472
*/
473473
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector';
474474

@@ -487,7 +487,7 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
487487

488488
/**
489489
* Type to help infer the collector based on the collector type
490-
* Used specifically in the returnMultiValueCollector wrapper function.
490+
* Used specifically in the returnNoValueCollector wrapper function.
491491
* When given a type, it can narrow which type it is returning
492492
*
493493
* Note: You can see this type in action in the test file or in the collector.utils file.
@@ -517,3 +517,64 @@ export type UnknownCollector = {
517517
type: string;
518518
};
519519
};
520+
521+
/** *********************************************************************
522+
* AUTOMATED COLLECTORS
523+
*/
524+
525+
/**
526+
* @interface AutoCollector - Represents a collector that collects a value programmatically without user intervention.
527+
*/
528+
529+
export type AutoCollectorCategories = 'SingleValueAutoCollector';
530+
export type AutoCollectorTypes = AutoCollectorCategories | 'ProtectCollector';
531+
532+
export interface AutoCollector<
533+
C extends AutoCollectorCategories,
534+
T extends AutoCollectorTypes,
535+
V = string,
536+
> {
537+
category: C;
538+
error: string | null;
539+
type: T;
540+
id: string;
541+
name: string;
542+
input: {
543+
key: string;
544+
value: V;
545+
type: string;
546+
};
547+
output: {
548+
key: string;
549+
type: string;
550+
config: Record<string, unknown>;
551+
};
552+
}
553+
554+
export type ProtectCollector = AutoCollector<
555+
'SingleValueAutoCollector',
556+
'ProtectCollector',
557+
string
558+
>;
559+
export type SingleValueAutoCollector = AutoCollector<
560+
'SingleValueAutoCollector',
561+
'SingleValueAutoCollector',
562+
string
563+
>;
564+
565+
export type AutoCollectors = ProtectCollector | SingleValueAutoCollector;
566+
567+
/**
568+
* Type to help infer the collector based on the collector type
569+
* Used specifically in the returnAutoCollector wrapper function.
570+
* When given a type, it can narrow which type it is returning
571+
*
572+
* Note: You can see this type in action in the test file or in the collector.utils file.
573+
*/
574+
export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'ProtectCollector'
575+
? ProtectCollector
576+
: /**
577+
* At this point, we have not passed in a collector type
578+
* so we can return a SingleValueAutoCollector
579+
**/
580+
SingleValueAutoCollector;

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ import type {
2121
ValidatedTextCollector,
2222
InferValueObjectCollectorType,
2323
ObjectValueCollectorTypes,
24+
AutoCollectorTypes,
2425
UnknownCollector,
26+
InferAutoCollectorType,
2527
} from './collector.types.js';
2628
import type {
2729
DeviceAuthenticationField,
2830
DeviceRegistrationField,
2931
MultiSelectField,
3032
PhoneNumberField,
33+
ProtectField,
3134
ReadOnlyField,
3235
RedirectField,
3336
SingleSelectField,
@@ -253,6 +256,66 @@ export function returnSingleValueCollector<
253256
}
254257
}
255258

259+
/**
260+
* @function returnAutoCollector - Creates an AutoCollector object based on the provided field, index, and optional collector type.
261+
* @param {DaVinciField} field - The field object containing key, label, type, and links.
262+
* @param {number} idx - The index to be used in the id of the AutoCollector.
263+
* @param {AutoCollectorTypes} [collectorType] - Optional type of the AutoCollector.
264+
* @returns {AutoCollector} The constructed AutoCollector object.
265+
*/
266+
export function returnAutoCollector<
267+
Field extends ProtectField,
268+
CollectorType extends AutoCollectorTypes = 'SingleValueAutoCollector',
269+
>(field: Field, idx: number, collectorType: CollectorType, data?: string) {
270+
let error = '';
271+
if (!('key' in field)) {
272+
error = `${error}Key is not found in the field object. `;
273+
}
274+
if (!('type' in field)) {
275+
error = `${error}Type is not found in the field object. `;
276+
}
277+
278+
if (collectorType === 'ProtectCollector') {
279+
return {
280+
category: 'SingleValueAutoCollector',
281+
error: error || null,
282+
type: collectorType,
283+
id: `${field?.key}-${idx}`,
284+
name: field.key,
285+
input: {
286+
key: field.key,
287+
value: data || '',
288+
type: field.type,
289+
},
290+
output: {
291+
key: field.key,
292+
type: field.type,
293+
config: {
294+
behavioralDataCollection: field.behavioralDataCollection,
295+
universalDeviceIdentification: field.universalDeviceIdentification,
296+
},
297+
},
298+
} as InferAutoCollectorType<'ProtectCollector'>;
299+
} else {
300+
return {
301+
category: 'SingleValueAutoCollector',
302+
error: error || null,
303+
type: collectorType || 'SingleValueAutoCollector',
304+
id: `${field.key}-${idx}`,
305+
name: field.key,
306+
input: {
307+
key: field.key,
308+
value: data || '',
309+
type: field.type,
310+
},
311+
output: {
312+
key: field.key,
313+
type: field.type,
314+
},
315+
} as InferAutoCollectorType<CollectorType>;
316+
}
317+
}
318+
256319
/**
257320
* @function returnPasswordCollector - Creates a PasswordCollector object based on the provided field and index.
258321
* @param {DaVinciField} field - The field object containing key, label, type, and links.
@@ -272,6 +335,7 @@ export function returnPasswordCollector(field: StandardField, idx: number) {
272335
export function returnTextCollector(field: StandardField, idx: number, data: string) {
273336
return returnSingleValueCollector(field, idx, 'TextCollector', data);
274337
}
338+
275339
/**
276340
* @function returnSingleSelectCollector - Creates a SingleCollector object based on the provided field and index.
277341
* @param {DaVinciField} field - The field object containing key, label, type, and links.
@@ -282,6 +346,16 @@ export function returnSingleSelectCollector(field: SingleSelectField, idx: numbe
282346
return returnSingleValueCollector(field, idx, 'SingleSelectCollector', data);
283347
}
284348

349+
/**
350+
* @function returnProtectCollector - Creates a ProtectCollector object based on the provided field and index.
351+
* @param {DaVinciField} field - The field object containing key, label, type, and links.
352+
* @param {number} idx - The index to be used in the id of the ProtectCollector.
353+
* @returns {ProtectCollector} The constructed ProtectCollector object.
354+
*/
355+
export function returnProtectCollector(field: ProtectField, idx: number, data: string) {
356+
return returnAutoCollector(field, idx, 'ProtectCollector', data);
357+
}
358+
285359
/**
286360
* @function returnMultiValueCollector - Creates a MultiValueCollector object based on the provided field, index, and optional collector type.
287361
* @param {DaVinciField} field - The field object containing key, label, type, and links.

0 commit comments

Comments
 (0)