Skip to content
Merged
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/dull-rockets-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Implemented Ping Protect collector
3 changes: 1 addition & 2 deletions e2e/davinci-app/components/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
Updater,
} from '@forgerock/davinci-client/types';

export default function (
export default function protectComponent(
formEl: HTMLFormElement,
collector: TextCollector | ValidatedTextCollector,
updater: Updater,
Expand All @@ -20,7 +20,6 @@ export default function (

p.innerText = collector.output.label;
formEl?.appendChild(p);

const error = updater('fakeprofile');
if (error && 'error' in error) {
console.error(error.error.message);
Expand Down
36 changes: 32 additions & 4 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import type {
DaVinciConfig,
DavinciClient,
GetClient,
ProtectCollector,
RequestMiddleware,
} from '@forgerock/davinci-client/types';
import { protect } from '@pingidentity/protect';

import textComponent from './components/text.js';
import passwordComponent from './components/password.js';
import submitButtonComponent from './components/submit-button.js';
import protect from './components/protect.js';
import protectComponent from './components/protect.js';
import flowLinkComponent from './components/flow-link.js';
import socialLoginButtonComponent from './components/social-login-button.js';
import { serverConfigs } from './server-configs.js';
Expand Down Expand Up @@ -77,10 +79,14 @@ const urlParams = new URLSearchParams(window.location.search);

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

// Initialize Protect
await protectAPI.start();

if (continueToken) {
resumed = await davinciClient.resume({ continueToken });
} else {
Expand Down Expand Up @@ -186,11 +192,12 @@ const urlParams = new URLSearchParams(window.location.search);
}

const collectors = davinciClient.getCollectors();

collectors.forEach((collector) => {
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
collector;
protect(
protectComponent(
formEl, // You can ignore this; it's just for rendering
collector, // This is the plain object of the collector
davinciClient.update(collector), // Returns an update function for this collector
Expand Down Expand Up @@ -238,7 +245,6 @@ const urlParams = new URLSearchParams(window.location.search);
submitForm,
);
} else if (collector.type === 'IdpCollector') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
socialLoginButtonComponent(formEl, collector, davinciClient.externalIdp());
} else if (collector.type === 'FlowCollector') {
flowLinkComponent(
Expand All @@ -257,11 +263,24 @@ const urlParams = new URLSearchParams(window.location.search);
}
});

if (davinciClient.getCollectors().find((collector) => collector.name === 'protectsdk')) {
if (
davinciClient
.getCollectors()
.find((collector) => collector.type === 'TextCollector' && collector.name === 'protectsdk')
) {
submitForm();
}
}

async function updateProtectCollector(protectCollector: ProtectCollector) {
const data = await protectAPI.getData();
const updater = davinciClient.update(protectCollector);
const error = updater(data);
if (error && 'error' in error) {
console.error(error.error.message);
}
}

async function submitForm() {
const newNode = await davinciClient.next();

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

formEl.addEventListener('submit', async (event) => {
event.preventDefault();

// Evaluate Protect data
const protectCollector = davinciClient
.getCollectors()
.find((collector) => collector.type === 'ProtectCollector');
if (protectCollector) {
await updateProtectCollector(protectCollector);
}

/**
* We can just call `next` here and not worry about passing any arguments
*/
Expand Down
3 changes: 2 additions & 1 deletion e2e/davinci-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"dependencies": {
"@forgerock/davinci-client": "workspace:*",
"@forgerock/javascript-sdk": "4.7.0",
"@forgerock/sdk-logger": "workspace:*"
"@forgerock/sdk-logger": "workspace:*",
"@pingidentity/protect": "workspace:*"
},
"devDependencies": {}
}
3 changes: 3 additions & 0 deletions e2e/davinci-app/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"components/**/*.ts"
],
"references": [
{
"path": "../../packages/protect/tsconfig.lib.json"
},
{
"path": "../../packages/sdk-effects/logger/tsconfig.lib.json"
},
Expand Down
3 changes: 3 additions & 0 deletions e2e/davinci-app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"skipLibCheck": true
},
"references": [
{
"path": "../../packages/protect"
},
{
"path": "../../packages/sdk-effects/logger"
},
Expand Down
14 changes: 10 additions & 4 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
MultiSelectCollector,
ObjectValueCollectors,
PhoneNumberInputValue,
AutoCollectors,
} from './collector.types.js';
import type {
InitFlow,
Expand Down Expand Up @@ -227,11 +228,15 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({

/**
* @method update - Exclusive method for updating the current node with user provided values
* @param {SingleValueCollector} collector - the collector to update
* @param {SingleValueCollector | MultiSelectCollector | ObjectValueCollectors | AutoCollectors} collector - the collector to update
* @returns {function} - a function to call for updating collector value
*/
update: (
collector: SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors,
collector:
| SingleValueCollectors
| MultiSelectCollector
| ObjectValueCollectors
| AutoCollectors,
): Updater => {
if (!collector.id) {
return handleUpdateValidateError(
Expand Down Expand Up @@ -259,10 +264,11 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
collectorToUpdate.category !== 'MultiValueCollector' &&
collectorToUpdate.category !== 'SingleValueCollector' &&
collectorToUpdate.category !== 'ValidatedSingleValueCollector' &&
collectorToUpdate.category !== 'ObjectValueCollector'
collectorToUpdate.category !== 'ObjectValueCollector' &&
collectorToUpdate.category !== 'SingleValueAutoCollector'
) {
return handleUpdateValidateError(
'Collector is not a MultiValueCollector, SingleValueCollector or ValidatedSingleValueCollector and cannot be updated',
'Collector is not a MultiValueCollector, SingleValueCollector, ValidatedSingleValueCollector, ObjectValueCollector, or SingleValueAutoCollector and cannot be updated',
'state_error',
log.error,
);
Expand Down
67 changes: 64 additions & 3 deletions packages/davinci-client/src/lib/collector.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'Tex
*/

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

Expand Down Expand Up @@ -468,7 +468,7 @@ export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>;
*/

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

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

/**
* Type to help infer the collector based on the collector type
* Used specifically in the returnMultiValueCollector wrapper function.
* Used specifically in the returnNoValueCollector wrapper function.
* When given a type, it can narrow which type it is returning
*
* Note: You can see this type in action in the test file or in the collector.utils file.
Expand Down Expand Up @@ -517,3 +517,64 @@ export type UnknownCollector = {
type: string;
};
};

/** *********************************************************************
* AUTOMATED COLLECTORS
*/

/**
* @interface AutoCollector - Represents a collector that collects a value programmatically without user intervention.
*/

export type AutoCollectorCategories = 'SingleValueAutoCollector';
export type AutoCollectorTypes = AutoCollectorCategories | 'ProtectCollector';

export interface AutoCollector<
C extends AutoCollectorCategories,
T extends AutoCollectorTypes,
V = string,
> {
category: C;
error: string | null;
type: T;
id: string;
name: string;
input: {
key: string;
value: V;
type: string;
};
output: {
key: string;
type: string;
config: Record<string, unknown>;
};
}

export type ProtectCollector = AutoCollector<
'SingleValueAutoCollector',
'ProtectCollector',
string
>;
export type SingleValueAutoCollector = AutoCollector<
'SingleValueAutoCollector',
'SingleValueAutoCollector',
string
>;

export type AutoCollectors = ProtectCollector | SingleValueAutoCollector;

/**
* Type to help infer the collector based on the collector type
* Used specifically in the returnAutoCollector wrapper function.
* When given a type, it can narrow which type it is returning
*
* Note: You can see this type in action in the test file or in the collector.utils file.
*/
export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'ProtectCollector'
? ProtectCollector
: /**
* At this point, we have not passed in a collector type
* so we can return a SingleValueAutoCollector
**/
SingleValueAutoCollector;
74 changes: 74 additions & 0 deletions packages/davinci-client/src/lib/collector.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import type {
ValidatedTextCollector,
InferValueObjectCollectorType,
ObjectValueCollectorTypes,
AutoCollectorTypes,
UnknownCollector,
InferAutoCollectorType,
} from './collector.types.js';
import type {
DeviceAuthenticationField,
DeviceRegistrationField,
MultiSelectField,
PhoneNumberField,
ProtectField,
ReadOnlyField,
RedirectField,
SingleSelectField,
Expand Down Expand Up @@ -253,6 +256,66 @@ export function returnSingleValueCollector<
}
}

/**
* @function returnAutoCollector - Creates an AutoCollector object based on the provided field, index, and optional collector type.
* @param {DaVinciField} field - The field object containing key, label, type, and links.
* @param {number} idx - The index to be used in the id of the AutoCollector.
* @param {AutoCollectorTypes} [collectorType] - Optional type of the AutoCollector.
* @returns {AutoCollector} The constructed AutoCollector object.
*/
export function returnAutoCollector<
Field extends ProtectField,
CollectorType extends AutoCollectorTypes = 'SingleValueAutoCollector',
>(field: Field, idx: number, collectorType: CollectorType, data?: string) {
let error = '';
if (!('key' in field)) {
error = `${error}Key is not found in the field object. `;
}
if (!('type' in field)) {
error = `${error}Type is not found in the field object. `;
}

if (collectorType === 'ProtectCollector') {
return {
category: 'SingleValueAutoCollector',
error: error || null,
type: collectorType,
id: `${field?.key}-${idx}`,
name: field.key,
input: {
key: field.key,
value: data || '',
type: field.type,
},
output: {
key: field.key,
type: field.type,
config: {
behavioralDataCollection: field.behavioralDataCollection,
universalDeviceIdentification: field.universalDeviceIdentification,
},
},
} as InferAutoCollectorType<'ProtectCollector'>;
} else {
return {
category: 'SingleValueAutoCollector',
error: error || null,
type: collectorType || 'SingleValueAutoCollector',
id: `${field.key}-${idx}`,
name: field.key,
input: {
key: field.key,
value: data || '',
type: field.type,
},
output: {
key: field.key,
type: field.type,
},
} as InferAutoCollectorType<CollectorType>;
}
}

/**
* @function returnPasswordCollector - Creates a PasswordCollector object based on the provided field and index.
* @param {DaVinciField} field - The field object containing key, label, type, and links.
Expand All @@ -272,6 +335,7 @@ export function returnPasswordCollector(field: StandardField, idx: number) {
export function returnTextCollector(field: StandardField, idx: number, data: string) {
return returnSingleValueCollector(field, idx, 'TextCollector', data);
}

/**
* @function returnSingleSelectCollector - Creates a SingleCollector object based on the provided field and index.
* @param {DaVinciField} field - The field object containing key, label, type, and links.
Expand All @@ -282,6 +346,16 @@ export function returnSingleSelectCollector(field: SingleSelectField, idx: numbe
return returnSingleValueCollector(field, idx, 'SingleSelectCollector', data);
}

/**
* @function returnProtectCollector - Creates a ProtectCollector object based on the provided field and index.
* @param {DaVinciField} field - The field object containing key, label, type, and links.
* @param {number} idx - The index to be used in the id of the ProtectCollector.
* @returns {ProtectCollector} The constructed ProtectCollector object.
*/
export function returnProtectCollector(field: ProtectField, idx: number, data: string) {
return returnAutoCollector(field, idx, 'ProtectCollector', data);
}

/**
* @function returnMultiValueCollector - Creates a MultiValueCollector object based on the provided field, index, and optional collector type.
* @param {DaVinciField} field - The field object containing key, label, type, and links.
Expand Down
Loading
Loading