From ffcf876216bbc80f226cc9683fe3d98b5650134a Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 13 Nov 2025 11:02:29 -0700 Subject: [PATCH 1/2] chore: type-exports --- packages/davinci-client/src/types.ts | 5 ++--- packages/device-client/package.json | 2 +- packages/device-client/src/lib/types/index.ts | 5 +++++ packages/journey-client/src/types.ts | 15 +++++++++++++++ packages/oidc-client/src/types.ts | 8 +++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/davinci-client/src/types.ts b/packages/davinci-client/src/types.ts index e6e7ec90c5..1ee3cb3242 100644 --- a/packages/davinci-client/src/types.ts +++ b/packages/davinci-client/src/types.ts @@ -5,7 +5,6 @@ */ import 'immer'; // Side-effect needed only for getting types in workspace -import type { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { FidoClient } from './lib/fido/fido.js'; import type * as collectors from './lib/collector.types.js'; import type * as config from './lib/config.types.js'; @@ -14,7 +13,7 @@ import type * as client from './lib/client.types.js'; import { davinci } from './lib/client.store.js'; import { nodeSlice } from './lib/node.slice.js'; -export type { CustomLogger } from '@forgerock/sdk-logger/types'; +export type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; export type DaVinciConfig = config.DaVinciConfig; @@ -54,5 +53,5 @@ export type FidoRegistrationCollector = collectors.FidoRegistrationCollector; export type FidoAuthenticationCollector = collectors.FidoAuthenticationCollector; export type InternalErrorResponse = client.InternalErrorResponse; -export type { RequestMiddleware }; +export type { RequestMiddleware, ActionTypes } from '@forgerock/sdk-request-middleware'; export type { FidoClient }; diff --git a/packages/device-client/package.json b/packages/device-client/package.json index bec0adc915..2c6ff4a093 100644 --- a/packages/device-client/package.json +++ b/packages/device-client/package.json @@ -13,7 +13,7 @@ "exports": { ".": "./dist/src/index.js", "./package.json": "./package.json", - "./types": "./dist/src/lib/types/index.d.ts" + "./types": "./dist/src/types.d.ts" }, "main": "./dist/src/index.js", "module": "./dist/src/index.js", diff --git a/packages/device-client/src/lib/types/index.ts b/packages/device-client/src/lib/types/index.ts index 33a54ff60d..2571e8cc4e 100644 --- a/packages/device-client/src/lib/types/index.ts +++ b/packages/device-client/src/lib/types/index.ts @@ -4,6 +4,11 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ + +// Re-export types from external dependencies that consumers need +export type { ConfigOptions } from '@forgerock/javascript-sdk'; + +// Re-export local types export * from './oath.types.js'; export * from './webauthn.types.js'; export * from './profile-device.types.js'; diff --git a/packages/journey-client/src/types.ts b/packages/journey-client/src/types.ts index 9c1ef38f22..ed7cade5fd 100644 --- a/packages/journey-client/src/types.ts +++ b/packages/journey-client/src/types.ts @@ -5,6 +5,21 @@ * of the MIT license. See the LICENSE file for details. */ +// Re-export types from internal packages that consumers need +export type { LogLevel, CustomLogger } from '@forgerock/sdk-logger'; +export type { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +export type { + Step, + Callback, + CallbackType, + StepType, + callbackType, + GenericError, + PolicyRequirement, + FailedPolicyRequirement, +} from '@forgerock/sdk-types'; + +// Re-export local types export * from './lib/config.types.js'; export * from './lib/interfaces.js'; export * from './lib/step.types.js'; diff --git a/packages/oidc-client/src/types.ts b/packages/oidc-client/src/types.ts index f409a8a47b..ee7d1a415d 100644 --- a/packages/oidc-client/src/types.ts +++ b/packages/oidc-client/src/types.ts @@ -3,8 +3,14 @@ * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ -export { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +// Re-export types from internal packages that consumers need +export type { LogLevel, CustomLogger } from '@forgerock/sdk-logger'; +export type { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +export type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +export type { StorageConfig } from '@forgerock/storage'; + +// Re-export local types export * from './lib/client.types.js'; export * from './lib/config.types.js'; export * from './lib/authorize.request.types.js'; From b160ecdd6781830a00ebfb7b9b80ee53460692a1 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 13 Nov 2025 12:10:19 -0700 Subject: [PATCH 2/2] chore: update-types-updater-further --- e2e/davinci-app/components/multi-value.ts | 2 +- e2e/davinci-app/components/object-value.ts | 9 +- e2e/davinci-app/components/password.ts | 2 +- e2e/davinci-app/components/protect.ts | 2 +- e2e/davinci-app/components/single-value.ts | 2 +- e2e/davinci-app/components/text.ts | 2 +- .../davinci-client/src/lib/client.store.ts | 8 +- .../davinci-client/src/lib/client.types.ts | 63 +++- .../src/lib/updater-narrowing.types.test-d.ts | 297 ++++++++++++++++++ packages/davinci-client/src/types.ts | 3 +- 10 files changed, 371 insertions(+), 19 deletions(-) create mode 100644 packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts diff --git a/e2e/davinci-app/components/multi-value.ts b/e2e/davinci-app/components/multi-value.ts index 8e2f949af1..415f8c538a 100644 --- a/e2e/davinci-app/components/multi-value.ts +++ b/e2e/davinci-app/components/multi-value.ts @@ -16,7 +16,7 @@ import type { MultiSelectCollector, Updater } from '@forgerock/davinci-client/ty export default function multiValueComponent( formEl: HTMLFormElement, collector: MultiSelectCollector, - updater: Updater, + updater: Updater, ) { // Create a container for the checkboxes const containerDiv = document.createElement('div'); diff --git a/e2e/davinci-app/components/object-value.ts b/e2e/davinci-app/components/object-value.ts index 4c847a6bd6..77e964d765 100644 --- a/e2e/davinci-app/components/object-value.ts +++ b/e2e/davinci-app/components/object-value.ts @@ -20,7 +20,10 @@ import type { export default function objectValueComponent( formEl: HTMLFormElement, collector: DeviceRegistrationCollector | DeviceAuthenticationCollector | PhoneNumberCollector, - updater: Updater, + updater: + | Updater + | Updater + | Updater, submitForm: () => void, ) { if ( @@ -50,7 +53,7 @@ export default function objectValueComponent( console.error('No value found for the selected option'); return; } - updater(selectedValue); + updater(selectedValue as any); submitForm(); }); @@ -84,7 +87,7 @@ export default function objectValueComponent( updater({ phoneNumber: selectedValue, countryCode: collector.output.value?.countryCode || '', - }); + } as any); }); formEl.appendChild(phoneLabel); diff --git a/e2e/davinci-app/components/password.ts b/e2e/davinci-app/components/password.ts index 8288f1a7b9..5b835478f8 100644 --- a/e2e/davinci-app/components/password.ts +++ b/e2e/davinci-app/components/password.ts @@ -10,7 +10,7 @@ import { dotToCamelCase } from '../helper.js'; export default function passwordComponent( formEl: HTMLFormElement, collector: PasswordCollector, - updater: Updater, + updater: Updater, ) { const label = document.createElement('label'); const input = document.createElement('input'); diff --git a/e2e/davinci-app/components/protect.ts b/e2e/davinci-app/components/protect.ts index f6edea6b02..ac2434b6f6 100644 --- a/e2e/davinci-app/components/protect.ts +++ b/e2e/davinci-app/components/protect.ts @@ -13,7 +13,7 @@ import type { export default function protectComponent( formEl: HTMLFormElement, collector: TextCollector | ValidatedTextCollector, - updater: Updater, + updater: Updater, ) { // create paragraph element with text of "Loading ... " const p = document.createElement('p'); diff --git a/e2e/davinci-app/components/single-value.ts b/e2e/davinci-app/components/single-value.ts index c3e4b8fab4..ca3d3df2fe 100644 --- a/e2e/davinci-app/components/single-value.ts +++ b/e2e/davinci-app/components/single-value.ts @@ -15,7 +15,7 @@ import type { SingleSelectCollector, Updater } from '@forgerock/davinci-client/t export default function singleValueComponent( formEl: HTMLFormElement, collector: SingleSelectCollector, - updater: Updater, + updater: Updater, ) { // Create the label element const labelEl = document.createElement('label'); diff --git a/e2e/davinci-app/components/text.ts b/e2e/davinci-app/components/text.ts index 79a608c1d7..5bc6d71c31 100644 --- a/e2e/davinci-app/components/text.ts +++ b/e2e/davinci-app/components/text.ts @@ -15,7 +15,7 @@ import { dotToCamelCase } from '../helper.js'; export default function textComponent( formEl: HTMLFormElement, collector: TextCollector | ValidatedTextCollector, - updater: Updater, + updater: Updater, validator: Validator, ) { const collectorKey = dotToCamelCase(collector.output.key); diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index aeee826647..a95d0079e3 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -234,13 +234,15 @@ export async function davinci({ * @param {SingleValueCollector | MultiSelectCollector | ObjectValueCollectors | AutoCollectors} collector - the collector to update * @returns {function} - a function to call for updating collector value */ - update: ( - collector: + update: < + T extends | SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors, - ): Updater => { + >( + collector: T, + ): Updater => { if (!collector.id) { return handleUpdateValidateError( 'Argument for `collector` has no ID', diff --git a/packages/davinci-client/src/lib/client.types.ts b/packages/davinci-client/src/lib/client.types.ts index 8372b073c2..5d82027918 100644 --- a/packages/davinci-client/src/lib/client.types.ts +++ b/packages/davinci-client/src/lib/client.types.ts @@ -22,13 +22,62 @@ export interface InternalErrorResponse { export type InitFlow = () => Promise; -export type Updater = ( - value: - | string - | string[] - | PhoneNumberInputValue - | FidoRegistrationInputValue - | FidoAuthenticationInputValue, +/** + * Maps collector types to the specific value type they accept. + * This enables type narrowing when using the update method with specific collector types. + * + * @example + * ```typescript + * if (collector.type === "PasswordCollector") { + * const updater = davinciClient.update(collector); + * // updater now only accepts: (value: string, index?: number) => ... + * } + * ``` + */ +export type CollectorValueType = T extends { type: 'PasswordCollector' } + ? string + : T extends { type: 'TextCollector'; category: 'SingleValueCollector' } + ? string + : T extends { type: 'TextCollector'; category: 'ValidatedSingleValueCollector' } + ? string + : T extends { type: 'SingleSelectCollector' } + ? string + : T extends { type: 'MultiSelectCollector' } + ? string[] + : T extends { type: 'DeviceRegistrationCollector' } + ? string + : T extends { type: 'DeviceAuthenticationCollector' } + ? string + : T extends { type: 'PhoneNumberCollector' } + ? PhoneNumberInputValue + : T extends { type: 'FidoRegistrationCollector' } + ? FidoRegistrationInputValue + : T extends { type: 'FidoAuthenticationCollector' } + ? FidoAuthenticationInputValue + : T extends { category: 'SingleValueCollector' } + ? string + : T extends { category: 'ValidatedSingleValueCollector' } + ? string + : T extends { category: 'MultiValueCollector' } + ? string[] + : + | string + | string[] + | PhoneNumberInputValue + | FidoRegistrationInputValue + | FidoAuthenticationInputValue; + +/** + * Generic updater function that accepts values appropriate for the collector type. + * When used with type narrowing, the value parameter will be constrained to the correct type. + * + * @template T The collector type (inferred from the collector passed to update()) + * @param value The value to update the collector with (type depends on T) + * @param index Optional index for multi-value collectors + * @returns null on success, or an InternalErrorResponse on failure + */ +export type Updater = ( + value: CollectorValueType, index?: number, ) => InternalErrorResponse | null; export type Validator = (value: string) => diff --git a/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts b/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts new file mode 100644 index 0000000000..7ba8b20d75 --- /dev/null +++ b/packages/davinci-client/src/lib/updater-narrowing.types.test-d.ts @@ -0,0 +1,297 @@ +/* + * Copyright (c) 2025 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. + */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { describe, expectTypeOf, it } from 'vitest'; + +import type { Updater } from './client.types.js'; +import type { + PasswordCollector, + TextCollector, + ValidatedTextCollector, + SingleSelectCollector, + MultiSelectCollector, + DeviceRegistrationCollector, + DeviceAuthenticationCollector, + PhoneNumberCollector, + FidoRegistrationCollector, + FidoAuthenticationCollector, + PhoneNumberInputValue, + FidoRegistrationInputValue, + FidoAuthenticationInputValue, +} from './collector.types.js'; +import type { Collectors } from './node.types.js'; + +// Mock update function that mimics davinciClient.update signature +type MockUpdate = < + T extends + | PasswordCollector + | TextCollector + | ValidatedTextCollector + | SingleSelectCollector + | MultiSelectCollector + | DeviceRegistrationCollector + | DeviceAuthenticationCollector + | PhoneNumberCollector + | FidoRegistrationCollector + | FidoAuthenticationCollector, +>( + collector: T, +) => Updater; + +const mockUpdate: MockUpdate = (collector) => { + return ((value: unknown) => null) as any; +}; + +describe('Updater Type Narrowing with Real Usage Pattern', () => { + describe('Single Value Collectors - should narrow collector type and updater parameter', () => { + it('PasswordCollector should narrow collector to PasswordCollector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'PasswordCollector') { + // 1. Collector itself should be narrowed to PasswordCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + + it('TextCollector should narrow collector to TextCollector | ValidatedTextCollector', () => { + const collector = {} as Collectors; + + if (collector.type === 'TextCollector') { + // 1. Collector narrows to union of both text collector types + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string (both types accept string) + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + + it('SingleSelectCollector should narrow collector to SingleSelectCollector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'SingleSelectCollector') { + // 1. Collector should be narrowed to SingleSelectCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + + it('DeviceRegistrationCollector should narrow collector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'DeviceRegistrationCollector') { + // 1. Collector should be narrowed to DeviceRegistrationCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + + it('DeviceAuthenticationCollector should narrow collector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'DeviceAuthenticationCollector') { + // 1. Collector should be narrowed to DeviceAuthenticationCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + }); + + describe('Multi Value Collectors - should narrow collector type and updater parameter', () => { + it('MultiSelectCollector should narrow collector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'MultiSelectCollector') { + // 1. Collector should be narrowed to MultiSelectCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept string[] + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + }); + + describe('Object Value Collectors - should narrow collector type and updater parameter', () => { + it('PhoneNumberCollector should narrow collector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'PhoneNumberCollector') { + // 1. Collector should be narrowed to PhoneNumberCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept PhoneNumberInputValue + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + + it('FidoRegistrationCollector should narrow collector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'FidoRegistrationCollector') { + // 1. Collector should be narrowed to FidoRegistrationCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept FidoRegistrationInputValue + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + + it('FidoAuthenticationCollector should narrow collector type', () => { + const collector = {} as Collectors; + + if (collector.type === 'FidoAuthenticationCollector') { + // 1. Collector should be narrowed to FidoAuthenticationCollector + expectTypeOf(collector).toEqualTypeOf(); + + // 2. update() should return Updater + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // 3. The updater parameter should accept FidoAuthenticationInputValue + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + }); + + describe('Real-world usage patterns', () => { + it('should narrow correctly with forEach loop', () => { + const collectors = [] as Collectors[]; + + collectors.forEach((collector) => { + if (collector.type === 'PasswordCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } else if (collector.type === 'PhoneNumberCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } else if (collector.type === 'MultiSelectCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + }); + + it('should narrow correctly with for...of loop', () => { + const collectors = [] as Collectors[]; + + for (const collector of collectors) { + if (collector.type === 'TextCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + } + }); + + it('should work with early return pattern', () => { + const collector = {} as Collectors; + + if (collector.type !== 'PasswordCollector') { + return; + } + + // After early return, collector is narrowed to PasswordCollector + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + }); + }); + + describe('Edge cases', () => { + it('should maintain index parameter optionality after narrowing', () => { + const collector = {} as Collectors; + + if (collector.type === 'PasswordCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + + // Index parameter should be optional (number | undefined) + expectTypeOf(updater).parameter(1).toMatchTypeOf(); + } + }); + + it('should work with complex conditional chains', () => { + const collector = {} as Collectors; + + if (collector.type === 'PasswordCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } else if (collector.type === 'TextCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } else if (collector.type === 'PhoneNumberCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } else if (collector.type === 'MultiSelectCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } else if (collector.type === 'FidoRegistrationCollector') { + expectTypeOf(collector).toEqualTypeOf(); + const updater = mockUpdate(collector); + expectTypeOf(updater).toEqualTypeOf>(); + expectTypeOf(updater).parameter(0).toEqualTypeOf(); + } + }); + }); +}); diff --git a/packages/davinci-client/src/types.ts b/packages/davinci-client/src/types.ts index 1ee3cb3242..691be0679e 100644 --- a/packages/davinci-client/src/types.ts +++ b/packages/davinci-client/src/types.ts @@ -18,7 +18,8 @@ export type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; export type DaVinciConfig = config.DaVinciConfig; export type DavinciClient = Awaited>; -export type Updater = client.Updater; +export type Updater = client.Updater; +export type CollectorValueType = client.CollectorValueType; export type InitFlow = client.InitFlow; export type Validator = client.Validator; export type GetClient = ReturnType;