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/thirty-badgers-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Catch FIDO/WebAuthn DOMExeceptions and send them to DaVinci
26 changes: 13 additions & 13 deletions e2e/davinci-app/components/fido.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -32,13 +32,13 @@ export default function fidoComponent(
console.log('fido.register response:', response);
if ('error' in response) {
console.error(response);
}

const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
await submitForm();
}
await submitForm();
}
};
} else if (collector.type === 'FidoAuthenticationCollector') {
Expand All @@ -54,13 +54,13 @@ export default function fidoComponent(
console.log('fido.authenticate response:', response);
if ('error' in response) {
console.error(response);
}

const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
const error = updater(response);
if (error && 'error' in error) {
console.error(error.error.message);
} else {
await submitForm();
}
await submitForm();
}
};
}
Expand Down
24 changes: 22 additions & 2 deletions e2e/davinci-suites/src/fido.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/*
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
import { test, expect, CDPSession } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';

Expand All @@ -10,6 +16,7 @@ test.use({ browserName: 'chromium' }); // ensure CDP/WebAuthn is available

test.beforeEach(async ({ context, page }) => {
cdp = await context.newCDPSession(page);
await expect(cdp).toBeDefined();
await cdp.send('WebAuthn.enable');

// A "platform" authenticator (aka internal) with UV+RK enabled is the usual default for passkeys.
Expand All @@ -27,8 +34,10 @@ test.beforeEach(async ({ context, page }) => {
});

test.afterEach(async () => {
await cdp.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
await cdp.send('WebAuthn.disable');
if (authenticatorId) {
await cdp?.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId });
}
await cdp?.send('WebAuthn.disable');
});

test.describe('FIDO/WebAuthn Tests', () => {
Expand All @@ -48,6 +57,10 @@ test.describe('FIDO/WebAuthn Tests', () => {
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

// Register WebAuthn credential
const { credentials: initialCredentials } = await cdp.send('WebAuthn.getCredentials', {
authenticatorId,
Expand Down Expand Up @@ -103,6 +116,13 @@ test.describe('FIDO/WebAuthn Tests', () => {
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign On' }).click();

await expect(cdp).toBeDefined;
await expect(authenticatorId).toBeDefined();

if (!cdp || !authenticatorId) {
throw new Error('Missing virtual authenticator');
}

// Register WebAuthn credential
const { credentials: initialCredentials } = await cdp.send('WebAuthn.getCredentials', {
authenticatorId,
Expand Down
19 changes: 5 additions & 14 deletions e2e/davinci-suites/src/password-policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ test.describe('ValidatedPasswordCollector — password policy (Example - Registr
try {
await deleteTestUser(page, email);
} catch (err) {
console.error(`[cleanup] Failed to delete test user ${email}:`, err);
throw new Error(`[cleanup] Failed to delete test user ${email}: ${JSON.stringify(err)}`);
}
}
});
Expand Down Expand Up @@ -146,26 +146,17 @@ test.describe('ValidatedPasswordCollector — password policy (Example - Registr
await page.locator('#userPassword').fill(password);

// Submit the form by calling submit() on the form element
await page.locator('form').evaluate((form: HTMLFormElement) => form.submit());
await page.getByRole('button', { name: 'Submit' }).click({ timeout: 10000 });

// Wait for the page to navigate to the next step
// The heading should change from "Example - Registration 1" to something else
await page.waitForFunction(
() => {
const heading = document.querySelector('h2');
return heading && !heading.textContent?.includes('Example - Registration');
},
{ timeout: 10000 },
);

// Verify we've moved to the next step
const heading = page.locator('h2').first();
await expect(heading).toBeVisible();
await expect(page.getByRole('heading', { name: 'Example - Registration' })).not.toBeVisible();

// If the flow shows a "Continue" button, click through to complete it
const continueBtn = page.getByRole('button', { name: 'Continue' });
if (await continueBtn.isVisible()) {
await continueBtn.click();
}

await expect(page.getByRole('heading', { name: 'Complete' })).toBeVisible();
});
});
49 changes: 31 additions & 18 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FetchBaseQueryError } from '@reduxjs/toolkit/query';
import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query';
import { GenericError } from '@forgerock/sdk-types';
import { LogLevel } from '@forgerock/sdk-logger';
import type { LogLevel as LogLevel_2 } from '@forgerock/sdk-types';
import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query';
import { QueryStatus } from '@reduxjs/toolkit/query';
import { Reducer } from '@reduxjs/toolkit';
Expand Down Expand Up @@ -208,9 +209,9 @@ export type CollectorValueType<T> = T extends {
type: 'PhoneNumberExtensionCollector';
} ? PhoneNumberExtensionInputValue : T extends {
type: 'FidoRegistrationCollector';
} ? FidoRegistrationInputValue : T extends {
} ? FidoRegistrationInputValue | GenericError : T extends {
type: 'FidoAuthenticationCollector';
} ? FidoAuthenticationInputValue : T extends {
} ? FidoAuthenticationInputValue | GenericError : T extends {
type: 'MetadataCollector';
} ? Record<string, unknown> | MetadataError : T extends {
category: 'SingleValueCollector';
Expand All @@ -227,7 +228,7 @@ export type CollectorValueType<T> = T extends {
} ? never : CollectorValueTypes;

// @public
export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError;
export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError | GenericError;

// @public (undocumented)
export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField | MetadataField;
Expand Down Expand Up @@ -285,38 +286,38 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
resume: (input: {
continueToken: string;
}) => Promise<InternalErrorResponse | NodeStates>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
pollStatus: (collector: PollingCollector) => Poller;
getMetadataError: (errorDetails: MetadataError) => MetadataError;
getClient: () => {
status: "start";
} | {
action: string;
collectors: Collectors[];
description?: string;
name?: string;
status: "continue";
} | {
status: "start";
} | {
action: string;
collectors: Collectors[];
description?: string;
name?: string;
status: "error";
} | {
status: "failure";
} | {
authorization?: {
code?: string;
state?: string;
};
status: "success";
} | {
status: "failure";
} | null;
getCollectors: () => Collectors[];
getError: () => DaVinciError | null;
getErrorCollectors: () => CollectorErrors[];
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode;
getServer: () => {
_links?: Links;
id?: string;
Expand All @@ -337,20 +338,20 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} | {
_links?: Links;
eventName?: string;
href?: string;
id?: string;
interactionId?: string;
interactionToken?: string;
status: "failure";
href?: string;
session?: string;
status: "success";
} | {
_links?: Links;
eventName?: string;
href?: string;
id?: string;
interactionId?: string;
interactionToken?: string;
href?: string;
session?: string;
status: "success";
status: "failure";
} | null;
cache: {
getLatestResponse: () => ({
Expand Down Expand Up @@ -640,7 +641,7 @@ export interface DaVinciRequest {
}

// @public
export type DaVinciRequestValueTypes = string | number | boolean | (string | number | boolean)[] | DeviceValue | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError;
export type DaVinciRequestValueTypes = string | number | boolean | (string | number | boolean)[] | DeviceValue | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError | GenericError;

// @public (undocumented)
export interface DaVinciSuccessResponse extends DaVinciBaseResponse {
Expand Down Expand Up @@ -836,10 +837,10 @@ export interface FailureNode {
}

// @public
export function fido(): FidoClient;
export function fido(config?: FidoClientConfig): FidoClient;

// @public (undocumented)
export type FidoAuthenticationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoAuthenticationCollector', FidoAuthenticationInputValue, FidoAuthenticationOutputValue>;
export type FidoAuthenticationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoAuthenticationCollector', FidoAuthenticationInputValue | GenericError, FidoAuthenticationOutputValue>;

// @public (undocumented)
export type FidoAuthenticationField = {
Expand Down Expand Up @@ -887,7 +888,19 @@ export interface FidoClient {
}

// @public (undocumented)
export type FidoRegistrationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoRegistrationCollector', FidoRegistrationInputValue, FidoRegistrationOutputValue>;
export interface FidoClientConfig {
// (undocumented)
logger?: {
level?: LogLevel_2;
custom?: CustomLogger;
};
}

// @public
export type FidoErrorCode = 'NotAllowedError' | 'AbortError' | 'InvalidStateError' | 'NotSupportedError' | 'SecurityError' | 'TimeoutError' | 'UnknownError';

// @public (undocumented)
export type FidoRegistrationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoRegistrationCollector', FidoRegistrationInputValue | GenericError, FidoRegistrationOutputValue>;

// @public (undocumented)
export type FidoRegistrationField = {
Expand Down
Loading
Loading