diff --git a/.changeset/sdks-5067-unified-config.md b/.changeset/sdks-5067-unified-config.md new file mode 100644 index 0000000000..6e4d6fbff9 --- /dev/null +++ b/.changeset/sdks-5067-unified-config.md @@ -0,0 +1,58 @@ +--- +'@forgerock/sdk-utilities': minor +'@forgerock/sdk-types': minor +'@forgerock/sdk-logger': patch +'@forgerock/sdk-oidc': minor +'@forgerock/oidc-client': minor +'@forgerock/journey-client': minor +'@forgerock/davinci-client': minor +--- + +Add unified cross-platform SDK configuration support + +New utility functions in `@forgerock/sdk-utilities` convert the cross-platform unified JSON config schema into each client's native config shape. Validation and mapping are owned entirely by the utilities layer — client factories remain typed to their existing config interfaces. + +**New in `@forgerock/sdk-utilities`:** + +- `makeOidcConfig(json)` — validates and maps unified JSON → `OidcConfig`; throws on invalid input +- `makeJourneyConfig(json)` — validates and maps unified JSON → `JourneyClientConfig`; throws on invalid input +- `makeDavinciConfig(json)` — validates and maps unified JSON → `DaVinciConfig`; throws on invalid input +- `UnifiedSdkConfig`, `UnifiedOidcConfig`, `UnifiedJourneyConfig` types +- `validateUnifiedSdkConfig` / `validateUnifiedOidcConfig` — pure validation returning `Either` +- `unifiedToOidcConfig`, `unifiedToJourneyConfig`, `unifiedToDavinciConfig` — pure mappers returning `Either` +- `AuthDisplayValue`, `AuthPromptValue` types (canonical source — shared between `OidcConfig` and `GetAuthorizationUrlOptions`) + +**Usage:** + +```ts +import { makeDavinciConfig } from '@forgerock/sdk-utilities'; + +const client = await davinci({ config: makeDavinciConfig(unifiedJsonConfig) }); +``` + +**New in `@forgerock/sdk-types`:** + +- `OidcConfig`, `JourneyClientConfig`, `DaVinciConfig` moved here as canonical types (previously mirrored in `sdk-utilities` as `Mapped*` types) +- `AuthDisplayValue`, `AuthPromptValue` types added (renamed from `OidcDisplayValue`/`OidcPromptValue`) +- `GetAuthorizationUrlOptions` extended with `loginHint`, `nonce`, `display`, `uiLocales`, `acrValues`; `prompt` widened to include `'select_account'` + +**Updated in `@forgerock/sdk-logger`:** + +- `LogLevel` now re-exported from `@forgerock/sdk-types` (single source of truth); runtime behaviour unchanged + +**New in `@forgerock/sdk-oidc`:** + +- `buildAuthorizeParams` forwards all new OIDC authorize params into the URL + +**New in `@forgerock/oidc-client`:** + +- `endSession` appends `post_logout_redirect_uri` when `signOutRedirectUri` is set on config +- Authorize URL construction forwards `loginHint`, `state`, `nonce`, `display`, `prompt`, `uiLocales`, `acrValues`, `additionalParameters` from config + +**New in `@forgerock/journey-client`:** + +- No API change — consume `makeJourneyConfig` at call-site to use unified JSON config + +**New in `@forgerock/davinci-client`:** + +- No API change — consume `makeDavinciConfig` at call-site to use unified JSON config diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index d277642c94..ed214a2a64 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -6,8 +6,8 @@ import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; import { CustomLogger } from '@forgerock/sdk-logger'; +import type { DaVinciConfig } from '@forgerock/sdk-types'; import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; import { GenericError } from '@forgerock/sdk-types'; @@ -289,11 +289,13 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; pollStatus: (collector: PollingCollector) => Poller; getClient: () => { + status: "start"; + } | { action: string; collectors: Collectors[]; description?: string; @@ -305,21 +307,19 @@ export function davinci(input: { description?: string; name?: string; status: "error"; - } | { - status: "failure"; - } | { - status: "start"; } | { authorization?: { code?: string; state?: string; }; status: "success"; + } | { + status: "failure"; } | null; getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; getServer: () => { _links?: Links; id?: string; @@ -328,6 +328,8 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; @@ -338,22 +340,20 @@ export function davinci(input: { } | { _links?: Links; eventName?: string; - href?: string; id?: string; interactionId?: string; interactionToken?: string; - status: "failure"; - } | { - status: "start"; + 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: () => ({ @@ -506,11 +506,7 @@ export type DaVinciCacheEntry = { // @public (undocumented) export type DavinciClient = Awaited>; -// @public (undocumented) -export interface DaVinciConfig extends AsyncLegacyConfigOptions { - // (undocumented) - responseType?: string; -} +export { DaVinciConfig } // @public (undocumented) export interface DaVinciError extends Omit { diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index b88390b3c1..bcc005139b 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -6,8 +6,8 @@ import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; import { CustomLogger } from '@forgerock/sdk-logger'; +import type { DaVinciConfig } from '@forgerock/sdk-types'; import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; import { GenericError } from '@forgerock/sdk-types'; @@ -289,11 +289,13 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; pollStatus: (collector: PollingCollector) => Poller; getClient: () => { + status: "start"; + } | { action: string; collectors: Collectors[]; description?: string; @@ -305,21 +307,19 @@ export function davinci(input: { description?: string; name?: string; status: "error"; - } | { - status: "failure"; - } | { - status: "start"; } | { authorization?: { code?: string; state?: string; }; status: "success"; + } | { + status: "failure"; } | null; getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; getServer: () => { _links?: Links; id?: string; @@ -328,6 +328,8 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; @@ -338,22 +340,20 @@ export function davinci(input: { } | { _links?: Links; eventName?: string; - href?: string; id?: string; interactionId?: string; interactionToken?: string; - status: "failure"; - } | { - status: "start"; + 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: () => ({ @@ -506,11 +506,7 @@ export type DaVinciCacheEntry = { // @public (undocumented) export type DavinciClient = Awaited>; -// @public (undocumented) -export interface DaVinciConfig extends AsyncLegacyConfigOptions { - // (undocumented) - responseType?: string; -} +export { DaVinciConfig } // @public (undocumented) export interface DaVinciError extends Omit { diff --git a/packages/davinci-client/src/lib/client.store.test.ts b/packages/davinci-client/src/lib/client.store.test.ts index 8d4e647b1f..ef6c2a9623 100644 --- a/packages/davinci-client/src/lib/client.store.test.ts +++ b/packages/davinci-client/src/lib/client.store.test.ts @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { davinci } from './client.store.js'; +import { makeDavinciConfig } from '@forgerock/sdk-utilities'; import type { DaVinciConfig } from './config.types.js'; // --------------------------------------------------------------------------- @@ -181,3 +182,59 @@ describe('davinci client — cache', () => { }); }); }); + +// --------------------------------------------------------------------------- + +describe('unified JSON config entry', () => { + beforeEach(() => { + vi.stubGlobal('localStorage', makeStorageStub()); + vi.stubGlobal('sessionStorage', makeStorageStub()); + mockFetchImplementation(); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + }); + + it('accepts unified JSON config and initializes successfully', async () => { + const unifiedConfig = { + oidc: { + clientId: '123456789', + discoveryEndpoint: TEST_WELLKNOWN_URL, + scopes: ['openid', 'profile'], + redirectUri: 'https://example.com/callback', + }, + }; + + const client = await davinci({ config: makeDavinciConfig(unifiedConfig) }); + expect(client).toHaveProperty('flow'); + expect(client).toHaveProperty('subscribe'); + }); + + it('throws when unified JSON config has missing required field', async () => { + const invalidConfig = { + oidc: { + // clientId missing + discoveryEndpoint: TEST_WELLKNOWN_URL, + scopes: ['openid'], + redirectUri: 'https://example.com/callback', + }, + }; + + expect(() => makeDavinciConfig(invalidConfig)).toThrow(/Invalid unified SDK config/); + }); + + it('throws when unified JSON config has wrong field type', async () => { + const invalidConfig = { + oidc: { + clientId: '123', + discoveryEndpoint: TEST_WELLKNOWN_URL, + scopes: 'openid', // should be array + redirectUri: 'https://example.com/callback', + }, + }; + + expect(() => makeDavinciConfig(invalidConfig)).toThrow(/Invalid unified SDK config/); + }); +}); diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index dc78d00ae0..a95401dd91 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -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. @@ -77,7 +77,10 @@ export async function davinci({ custom?: CustomLogger; }; }) { - const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); + const log = loggerFn({ + level: logger?.level ?? config.log ?? 'error', + custom: logger?.custom, + }); const store = createClientStore({ requestMiddleware, logger: log }); const serverInfo = createStorage({ type: 'localStorage', diff --git a/packages/davinci-client/src/lib/config.types.ts b/packages/davinci-client/src/lib/config.types.ts index 9a16e5940b..c4ecb63a5d 100644 --- a/packages/davinci-client/src/lib/config.types.ts +++ b/packages/davinci-client/src/lib/config.types.ts @@ -1,15 +1,14 @@ /* - * 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. */ -import type { AsyncLegacyConfigOptions, WellknownResponse } from '@forgerock/sdk-types'; +import type { WellknownResponse } from '@forgerock/sdk-types'; +import type { DaVinciConfig } from '@forgerock/sdk-types'; -export interface DaVinciConfig extends AsyncLegacyConfigOptions { - responseType?: string; -} +export type { DaVinciConfig }; export interface InternalDaVinciConfig extends DaVinciConfig { wellknownResponse: WellknownResponse; diff --git a/packages/journey-client/api-report/journey-client.api.md b/packages/journey-client/api-report/journey-client.api.md index d0dce2aae6..8c06616ab9 100644 --- a/packages/journey-client/api-report/journey-client.api.md +++ b/packages/journey-client/api-report/journey-client.api.md @@ -1,507 +1,501 @@ -## API Report File for "@forgerock/journey-client" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; -import { AuthResponse } from '@forgerock/sdk-types'; -import { Callback } from '@forgerock/sdk-types'; -import { CallbackType } from '@forgerock/sdk-types'; -import { callbackType } from '@forgerock/sdk-types'; -import { createWellknownError } from '@forgerock/sdk-utilities'; -import { CustomLogger } from '@forgerock/sdk-logger'; -import { FailedPolicyRequirement } from '@forgerock/sdk-types'; -import { FailureDetail } from '@forgerock/sdk-types'; -import { GenericError } from '@forgerock/sdk-types'; -import { isValidWellknownUrl } from '@forgerock/sdk-utilities'; -import { LogLevel } from '@forgerock/sdk-logger'; -import { NameValue } from '@forgerock/sdk-types'; -import { PolicyKey } from '@forgerock/sdk-types'; -import { PolicyParams } from '@forgerock/sdk-types'; -import { PolicyRequirement } from '@forgerock/sdk-types'; -import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import { Step } from '@forgerock/sdk-types'; -import { StepDetail } from '@forgerock/sdk-types'; -import { StepType } from '@forgerock/sdk-types'; -import { WellknownResponse } from '@forgerock/sdk-types'; - -export { ActionTypes } - -// @public -export class AttributeInputCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getName(): string; - getPolicies(): Record; - getPrompt(): string; - isRequired(): boolean; - // (undocumented) - payload: Callback; - setValidateOnly(value: boolean): void; - setValue(value: T): void; -} - -export { AuthResponse } - -// @public -export class BaseCallback { - constructor(payload: Callback); - getInputValue(selector?: number | string): unknown; - getOutputByName(name: string, defaultValue: T): T; - getOutputValue(selector?: number | string): unknown; - getType(): CallbackType; - // (undocumented) - payload: Callback; - setInputValue(value: unknown, selector?: number | string | RegExp): void; -} - -export { Callback } - -// @public (undocumented) -export type CallbackFactory = (callback: Callback) => BaseCallback; - -export { CallbackType } - -export { callbackType } - -// @public -export class ChoiceCallback extends BaseCallback { - constructor(payload: Callback); - getChoices(): string[]; - getDefaultChoice(): number; - getPrompt(): string; - // (undocumented) - payload: Callback; - setChoiceIndex(index: number): void; - setChoiceValue(value: string): void; -} - -// @public -export class ConfirmationCallback extends BaseCallback { - constructor(payload: Callback); - getDefaultOption(): number; - getMessageType(): number; - getOptions(): string[]; - getOptionType(): number; - getPrompt(): string; - // (undocumented) - payload: Callback; - setOptionIndex(index: number): void; - setOptionValue(value: string): void; -} - -// @public (undocumented) -export function createCallback(callback: Callback): BaseCallback; - -export { createWellknownError } - -export { CustomLogger } - -// @public -export class DeviceProfileCallback extends BaseCallback { - constructor(payload: Callback); - getMessage(): string; - isLocationRequired(): boolean; - isMetadataRequired(): boolean; - // (undocumented) - payload: Callback; - setProfile(profile: DeviceProfileData): void; -} - -// @public (undocumented) -export interface DeviceProfileData { - // (undocumented) - identifier: string; - // (undocumented) - location?: Geolocation_2 | Record; - // (undocumented) - metadata?: { - hardware: { - display: { - [key: string]: string | number | null; - }; - [key: string]: any; - }; - browser: { - [key: string]: string | number | null; - }; - platform: { - [key: string]: string | number | null; - }; - }; -} - -export { FailedPolicyRequirement } - -export { FailureDetail } - -export { GenericError } - -// @public (undocumented) -interface Geolocation_2 { - // (undocumented) - latitude: number; - // (undocumented) - longitude: number; -} -export { Geolocation_2 as Geolocation } - -// @public -export class HiddenValueCallback extends BaseCallback { - constructor(payload: Callback); - // (undocumented) - payload: Callback; -} - -// @public (undocumented) -export interface IdPValue { - // (undocumented) - provider: string; - // (undocumented) - uiConfig: { - [key: string]: string; - }; -} - -// @internal -export interface InternalJourneyClientConfig { - // (undocumented) - error?: GenericError; - // (undocumented) - serverConfig: ResolvedServerConfig; -} - -export { isValidWellknownUrl } - -// @public -export function journey(input: { - config: JourneyClientConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; -}): Promise; - -// @public -export interface JourneyClient { - // (undocumented) - next: (step: JourneyStep, options?: NextOptions) => Promise; - // (undocumented) - redirect: (step: JourneyStep) => Promise; - // (undocumented) - resume: (url: string, options?: ResumeOptions) => Promise; - // (undocumented) - start: (options?: StartParam) => Promise; - // (undocumented) - subscribe: (listener: () => void) => () => void; - // (undocumented) - terminate: (options?: { - query?: Record; - }) => Promise; -} - -// @public -export interface JourneyClientConfig extends AsyncLegacyConfigOptions { - // (undocumented) - serverConfig: JourneyServerConfig; -} - -// @public (undocumented) -export type JourneyLoginFailure = AuthResponse & { - type: StepType.LoginFailure; - payload: Step; - getCode: () => number; - getDetail: () => FailureDetail | undefined; - getMessage: () => string | undefined; - getProcessedMessage: (messageCreator?: MessageCreator) => ProcessedPropertyError[]; - getReason: () => string | undefined; -}; - -// @public (undocumented) -export type JourneyLoginSuccess = AuthResponse & { - type: StepType.LoginSuccess; - payload: Step; - getRealm: () => string | undefined; - getSessionToken: () => string | undefined; - getSuccessUrl: () => string | undefined; -}; - -// @public (undocumented) -export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError; - -// @public -export interface JourneyServerConfig { - timeout?: number; - wellknown: string; -} - -// @public (undocumented) -export type JourneyStep = AuthResponse & { - type: StepType.Step; - payload: Step; - callbacks: BaseCallback[]; - getCallbackOfType: (type: CallbackType) => T; - getCallbacksOfType: (type: CallbackType) => T[]; - setCallbackValue: (type: CallbackType, value: unknown) => void; - getDescription: () => string | undefined; - getHeader: () => string | undefined; - getStage: () => string | undefined; -}; - -// @public -export class KbaCreateCallback extends BaseCallback { - constructor(payload: Callback); - getPredefinedQuestions(): string[]; - getPrompt(): string; - isAllowedUserDefinedQuestions(): boolean; - // (undocumented) - payload: Callback; - setAnswer(answer: string): void; - setQuestion(question: string): void; -} - -export { LogLevel } - -// @public (undocumented) -export interface MessageCreator { - // (undocumented) - [key: string]: (propertyName: string, params?: { - [key: string]: unknown; - }) => string; -} - -// @public -export class MetadataCallback extends BaseCallback { - constructor(payload: Callback); - getData(): T; - // (undocumented) - payload: Callback; -} - -// @public -export class NameCallback extends BaseCallback { - constructor(payload: Callback); - getPrompt(): string; - // (undocumented) - payload: Callback; - setName(name: string): void; -} - -export { NameValue } - -// @public (undocumented) -export interface NextOptions { - // (undocumented) - query?: Record; -} - -// @public -export class PasswordCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getPolicies(): string[]; - getPrompt(): string; - // (undocumented) - payload: Callback; - setPassword(password: string): void; -} - -// @public -export class PingOneProtectEvaluationCallback extends BaseCallback { - constructor(payload: Callback); - getPauseBehavioralData(): boolean; - // (undocumented) - payload: Callback; - setClientError(errorMessage: string): void; - setData(data: string): void; -} - -// @public -export class PingOneProtectInitializeCallback extends BaseCallback { - constructor(payload: Callback); - getConfig(): Record | { - envId: string; - consoleLogEnabled: boolean; - deviceAttributesToIgnore: string[]; - customHost: string; - lazyMetadata: boolean; - behavioralDataCollection: boolean; - deviceKeyRsyncIntervals: number; - enableTrust: boolean; - disableTags: boolean; - disableHub: boolean; - }; - // (undocumented) - payload: Callback; - // (undocumented) - setClientError(errorMessage: string): void; -} - -export { PolicyKey } - -export { PolicyParams } - -export { PolicyRequirement } - -// @public -export class PollingWaitCallback extends BaseCallback { - constructor(payload: Callback); - getMessage(): string; - getWaitTime(): number; - // (undocumented) - payload: Callback; -} - -// @public (undocumented) -export interface ProcessedPropertyError { - // (undocumented) - detail: FailedPolicyRequirement; - // (undocumented) - messages: string[]; -} - -// @public -export class ReCaptchaCallback extends BaseCallback { - constructor(payload: Callback); - getSiteKey(): string; - // (undocumented) - payload: Callback; - setResult(result: string): void; -} - -// @public -export class ReCaptchaEnterpriseCallback extends BaseCallback { - constructor(payload: Callback); - getApiUrl(): string; - getElementClass(): string; - getSiteKey(): string; - // (undocumented) - payload: Callback; - setAction(action: string): void; - setClientError(error: string): void; - setPayload(payload: unknown): void; - setResult(result: string): void; -} - -// @public -export class RedirectCallback extends BaseCallback { - constructor(payload: Callback); - getRedirectUrl(): string; - // (undocumented) - payload: Callback; -} - -export { RequestMiddleware } - -// @public -export interface ResolvedServerConfig { - // (undocumented) - baseUrl: string; - // (undocumented) - paths: { - authenticate: string; - sessions: string; - }; -} - -// @public (undocumented) -export interface ResumeOptions { - // (undocumented) - journey?: string; - // (undocumented) - query?: Record; -} - -// @public -export class SelectIdPCallback extends BaseCallback { - constructor(payload: Callback); - getProviders(): IdPValue[]; - // (undocumented) - payload: Callback; - setProvider(value: string): void; -} - -// @public (undocumented) -export interface StartParam { - // (undocumented) - journey: string; - // (undocumented) - query?: Record; -} - -export { Step } - -export { StepDetail } - -export { StepType } - -// @public -export class SuspendedTextOutputCallback extends TextOutputCallback { - constructor(payload: Callback); - // (undocumented) - payload: Callback; -} - -// @public -export class TermsAndConditionsCallback extends BaseCallback { - constructor(payload: Callback); - getCreateDate(): Date | null; - getTerms(): string; - getVersion(): string; - // (undocumented) - payload: Callback; - setAccepted(accepted?: boolean): void; -} - -// @public -export class TextInputCallback extends BaseCallback { - constructor(payload: Callback); - getPrompt(): string; - // (undocumented) - payload: Callback; - setInput(input: string): void; -} - -// @public -export class TextOutputCallback extends BaseCallback { - constructor(payload: Callback); - getMessage(): string; - getMessageType(): string; - // (undocumented) - payload: Callback; -} - -// @public -export class ValidatedCreatePasswordCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getPolicies(): Record; - getPrompt(): string; - isRequired(): boolean; - // (undocumented) - payload: Callback; - setPassword(password: string): void; - setValidateOnly(value: boolean): void; -} - -// @public -export class ValidatedCreateUsernameCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getPolicies(): Record; - getPrompt(): string; - isRequired(): boolean; - // (undocumented) - payload: Callback; - setName(name: string): void; - setValidateOnly(value: boolean): void; -} - -export { WellknownResponse } - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@forgerock/journey-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionTypes } from '@forgerock/sdk-request-middleware'; +import { AuthResponse } from '@forgerock/sdk-types'; +import { Callback } from '@forgerock/sdk-types'; +import { CallbackType } from '@forgerock/sdk-types'; +import { callbackType } from '@forgerock/sdk-types'; +import { createWellknownError } from '@forgerock/sdk-utilities'; +import { CustomLogger } from '@forgerock/sdk-logger'; +import { FailedPolicyRequirement } from '@forgerock/sdk-types'; +import { FailureDetail } from '@forgerock/sdk-types'; +import { GenericError } from '@forgerock/sdk-types'; +import { isValidWellknownUrl } from '@forgerock/sdk-utilities'; +import { JourneyClientConfig } from '@forgerock/sdk-types'; +import { JourneyServerConfig } from '@forgerock/sdk-types'; +import { LogLevel } from '@forgerock/sdk-logger'; +import { NameValue } from '@forgerock/sdk-types'; +import { PolicyKey } from '@forgerock/sdk-types'; +import { PolicyParams } from '@forgerock/sdk-types'; +import { PolicyRequirement } from '@forgerock/sdk-types'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import { Step } from '@forgerock/sdk-types'; +import { StepDetail } from '@forgerock/sdk-types'; +import { StepType } from '@forgerock/sdk-types'; +import { WellknownResponse } from '@forgerock/sdk-types'; + +export { ActionTypes }; + +// @public +export class AttributeInputCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getName(): string; + getPolicies(): Record; + getPrompt(): string; + isRequired(): boolean; + // (undocumented) + payload: Callback; + setValidateOnly(value: boolean): void; + setValue(value: T): void; +} + +export { AuthResponse }; + +// @public +export class BaseCallback { + constructor(payload: Callback); + getInputValue(selector?: number | string): unknown; + getOutputByName(name: string, defaultValue: T): T; + getOutputValue(selector?: number | string): unknown; + getType(): CallbackType; + // (undocumented) + payload: Callback; + setInputValue(value: unknown, selector?: number | string | RegExp): void; +} + +export { Callback }; + +// @public (undocumented) +export type CallbackFactory = (callback: Callback) => BaseCallback; + +export { CallbackType }; + +export { callbackType }; + +// @public +export class ChoiceCallback extends BaseCallback { + constructor(payload: Callback); + getChoices(): string[]; + getDefaultChoice(): number; + getPrompt(): string; + // (undocumented) + payload: Callback; + setChoiceIndex(index: number): void; + setChoiceValue(value: string): void; +} + +// @public +export class ConfirmationCallback extends BaseCallback { + constructor(payload: Callback); + getDefaultOption(): number; + getMessageType(): number; + getOptions(): string[]; + getOptionType(): number; + getPrompt(): string; + // (undocumented) + payload: Callback; + setOptionIndex(index: number): void; + setOptionValue(value: string): void; +} + +// @public (undocumented) +export function createCallback(callback: Callback): BaseCallback; + +export { createWellknownError }; + +export { CustomLogger }; + +// @public +export class DeviceProfileCallback extends BaseCallback { + constructor(payload: Callback); + getMessage(): string; + isLocationRequired(): boolean; + isMetadataRequired(): boolean; + // (undocumented) + payload: Callback; + setProfile(profile: DeviceProfileData): void; +} + +// @public (undocumented) +export interface DeviceProfileData { + // (undocumented) + identifier: string; + // (undocumented) + location?: Geolocation_2 | Record; + // (undocumented) + metadata?: { + hardware: { + display: { + [key: string]: string | number | null; + }; + [key: string]: any; + }; + browser: { + [key: string]: string | number | null; + }; + platform: { + [key: string]: string | number | null; + }; + }; +} + +export { FailedPolicyRequirement }; + +export { FailureDetail }; + +export { GenericError }; + +// @public (undocumented) +interface Geolocation_2 { + // (undocumented) + latitude: number; + // (undocumented) + longitude: number; +} +export { Geolocation_2 as Geolocation }; + +// @public +export class HiddenValueCallback extends BaseCallback { + constructor(payload: Callback); + // (undocumented) + payload: Callback; +} + +// @public (undocumented) +export interface IdPValue { + // (undocumented) + provider: string; + // (undocumented) + uiConfig: { + [key: string]: string; + }; +} + +// @internal +export interface InternalJourneyClientConfig { + // (undocumented) + error?: GenericError; + // (undocumented) + serverConfig: ResolvedServerConfig; +} + +export { isValidWellknownUrl }; + +// @public +export function journey(input: { + config: JourneyClientConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; +}): Promise; + +// @public +export interface JourneyClient { + // (undocumented) + next: (step: JourneyStep, options?: NextOptions) => Promise; + // (undocumented) + redirect: (step: JourneyStep) => Promise; + // (undocumented) + resume: (url: string, options?: ResumeOptions) => Promise; + // (undocumented) + start: (options?: StartParam) => Promise; + // (undocumented) + subscribe: (listener: () => void) => () => void; + // (undocumented) + terminate: (options?: { query?: Record }) => Promise; +} + +export { JourneyClientConfig }; + +// @public (undocumented) +export type JourneyLoginFailure = AuthResponse & { + type: StepType.LoginFailure; + payload: Step; + getCode: () => number; + getDetail: () => FailureDetail | undefined; + getMessage: () => string | undefined; + getProcessedMessage: (messageCreator?: MessageCreator) => ProcessedPropertyError[]; + getReason: () => string | undefined; +}; + +// @public (undocumented) +export type JourneyLoginSuccess = AuthResponse & { + type: StepType.LoginSuccess; + payload: Step; + getRealm: () => string | undefined; + getSessionToken: () => string | undefined; + getSuccessUrl: () => string | undefined; +}; + +// @public (undocumented) +export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError; + +export { JourneyServerConfig }; + +// @public (undocumented) +export type JourneyStep = AuthResponse & { + type: StepType.Step; + payload: Step; + callbacks: BaseCallback[]; + getCallbackOfType: (type: CallbackType) => T; + getCallbacksOfType: (type: CallbackType) => T[]; + setCallbackValue: (type: CallbackType, value: unknown) => void; + getDescription: () => string | undefined; + getHeader: () => string | undefined; + getStage: () => string | undefined; +}; + +// @public +export class KbaCreateCallback extends BaseCallback { + constructor(payload: Callback); + getPredefinedQuestions(): string[]; + getPrompt(): string; + isAllowedUserDefinedQuestions(): boolean; + // (undocumented) + payload: Callback; + setAnswer(answer: string): void; + setQuestion(question: string): void; +} + +export { LogLevel }; + +// @public (undocumented) +export interface MessageCreator { + // (undocumented) + [key: string]: ( + propertyName: string, + params?: { + [key: string]: unknown; + }, + ) => string; +} + +// @public +export class MetadataCallback extends BaseCallback { + constructor(payload: Callback); + getData(): T; + // (undocumented) + payload: Callback; +} + +// @public +export class NameCallback extends BaseCallback { + constructor(payload: Callback); + getPrompt(): string; + // (undocumented) + payload: Callback; + setName(name: string): void; +} + +export { NameValue }; + +// @public (undocumented) +export interface NextOptions { + // (undocumented) + query?: Record; +} + +// @public +export class PasswordCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getPolicies(): string[]; + getPrompt(): string; + // (undocumented) + payload: Callback; + setPassword(password: string): void; +} + +// @public +export class PingOneProtectEvaluationCallback extends BaseCallback { + constructor(payload: Callback); + getPauseBehavioralData(): boolean; + // (undocumented) + payload: Callback; + setClientError(errorMessage: string): void; + setData(data: string): void; +} + +// @public +export class PingOneProtectInitializeCallback extends BaseCallback { + constructor(payload: Callback); + getConfig(): + | Record + | { + envId: string; + consoleLogEnabled: boolean; + deviceAttributesToIgnore: string[]; + customHost: string; + lazyMetadata: boolean; + behavioralDataCollection: boolean; + deviceKeyRsyncIntervals: number; + enableTrust: boolean; + disableTags: boolean; + disableHub: boolean; + }; + // (undocumented) + payload: Callback; + // (undocumented) + setClientError(errorMessage: string): void; +} + +export { PolicyKey }; + +export { PolicyParams }; + +export { PolicyRequirement }; + +// @public +export class PollingWaitCallback extends BaseCallback { + constructor(payload: Callback); + getMessage(): string; + getWaitTime(): number; + // (undocumented) + payload: Callback; +} + +// @public (undocumented) +export interface ProcessedPropertyError { + // (undocumented) + detail: FailedPolicyRequirement; + // (undocumented) + messages: string[]; +} + +// @public +export class ReCaptchaCallback extends BaseCallback { + constructor(payload: Callback); + getSiteKey(): string; + // (undocumented) + payload: Callback; + setResult(result: string): void; +} + +// @public +export class ReCaptchaEnterpriseCallback extends BaseCallback { + constructor(payload: Callback); + getApiUrl(): string; + getElementClass(): string; + getSiteKey(): string; + // (undocumented) + payload: Callback; + setAction(action: string): void; + setClientError(error: string): void; + setPayload(payload: unknown): void; + setResult(result: string): void; +} + +// @public +export class RedirectCallback extends BaseCallback { + constructor(payload: Callback); + getRedirectUrl(): string; + // (undocumented) + payload: Callback; +} + +export { RequestMiddleware }; + +// @public +export interface ResolvedServerConfig { + // (undocumented) + baseUrl: string; + // (undocumented) + paths: { + authenticate: string; + sessions: string; + }; +} + +// @public (undocumented) +export interface ResumeOptions { + // (undocumented) + journey?: string; + // (undocumented) + query?: Record; +} + +// @public +export class SelectIdPCallback extends BaseCallback { + constructor(payload: Callback); + getProviders(): IdPValue[]; + // (undocumented) + payload: Callback; + setProvider(value: string): void; +} + +// @public (undocumented) +export interface StartParam { + // (undocumented) + journey: string; + // (undocumented) + query?: Record; +} + +export { Step }; + +export { StepDetail }; + +export { StepType }; + +// @public +export class SuspendedTextOutputCallback extends TextOutputCallback { + constructor(payload: Callback); + // (undocumented) + payload: Callback; +} + +// @public +export class TermsAndConditionsCallback extends BaseCallback { + constructor(payload: Callback); + getCreateDate(): Date | null; + getTerms(): string; + getVersion(): string; + // (undocumented) + payload: Callback; + setAccepted(accepted?: boolean): void; +} + +// @public +export class TextInputCallback extends BaseCallback { + constructor(payload: Callback); + getPrompt(): string; + // (undocumented) + payload: Callback; + setInput(input: string): void; +} + +// @public +export class TextOutputCallback extends BaseCallback { + constructor(payload: Callback); + getMessage(): string; + getMessageType(): string; + // (undocumented) + payload: Callback; +} + +// @public +export class ValidatedCreatePasswordCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getPolicies(): Record; + getPrompt(): string; + isRequired(): boolean; + // (undocumented) + payload: Callback; + setPassword(password: string): void; + setValidateOnly(value: boolean): void; +} + +// @public +export class ValidatedCreateUsernameCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getPolicies(): Record; + getPrompt(): string; + isRequired(): boolean; + // (undocumented) + payload: Callback; + setName(name: string): void; + setValidateOnly(value: boolean): void; +} + +export { WellknownResponse }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/journey-client/api-report/journey-client.types.api.md b/packages/journey-client/api-report/journey-client.types.api.md index 2c6c1c8791..4d4fc66054 100644 --- a/packages/journey-client/api-report/journey-client.types.api.md +++ b/packages/journey-client/api-report/journey-client.types.api.md @@ -1,494 +1,488 @@ -## API Report File for "@forgerock/journey-client" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; -import { AuthResponse } from '@forgerock/sdk-types'; -import { Callback } from '@forgerock/sdk-types'; -import { CallbackType } from '@forgerock/sdk-types'; -import { createWellknownError } from '@forgerock/sdk-utilities'; -import { CustomLogger } from '@forgerock/sdk-logger'; -import { FailedPolicyRequirement } from '@forgerock/sdk-types'; -import { FailureDetail } from '@forgerock/sdk-types'; -import { GenericError } from '@forgerock/sdk-types'; -import { isValidWellknownUrl } from '@forgerock/sdk-utilities'; -import { LogLevel } from '@forgerock/sdk-logger'; -import { NameValue } from '@forgerock/sdk-types'; -import { PolicyKey } from '@forgerock/sdk-types'; -import { PolicyParams } from '@forgerock/sdk-types'; -import { PolicyRequirement } from '@forgerock/sdk-types'; -import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import { Step } from '@forgerock/sdk-types'; -import { StepDetail } from '@forgerock/sdk-types'; -import { StepType } from '@forgerock/sdk-types'; -import { WellknownResponse } from '@forgerock/sdk-types'; - -export { ActionTypes } - -// @public -export class AttributeInputCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getName(): string; - getPolicies(): Record; - getPrompt(): string; - isRequired(): boolean; - // (undocumented) - payload: Callback; - setValidateOnly(value: boolean): void; - setValue(value: T): void; -} - -export { AuthResponse } - -// @public -export class BaseCallback { - constructor(payload: Callback); - getInputValue(selector?: number | string): unknown; - getOutputByName(name: string, defaultValue: T): T; - getOutputValue(selector?: number | string): unknown; - getType(): CallbackType; - // (undocumented) - payload: Callback; - setInputValue(value: unknown, selector?: number | string | RegExp): void; -} - -export { Callback } - -// @public (undocumented) -export type CallbackFactory = (callback: Callback) => BaseCallback; - -export { CallbackType } - -// @public -export class ChoiceCallback extends BaseCallback { - constructor(payload: Callback); - getChoices(): string[]; - getDefaultChoice(): number; - getPrompt(): string; - // (undocumented) - payload: Callback; - setChoiceIndex(index: number): void; - setChoiceValue(value: string): void; -} - -// @public -export class ConfirmationCallback extends BaseCallback { - constructor(payload: Callback); - getDefaultOption(): number; - getMessageType(): number; - getOptions(): string[]; - getOptionType(): number; - getPrompt(): string; - // (undocumented) - payload: Callback; - setOptionIndex(index: number): void; - setOptionValue(value: string): void; -} - -// @public (undocumented) -export function createCallback(callback: Callback): BaseCallback; - -export { createWellknownError } - -export { CustomLogger } - -// @public -export class DeviceProfileCallback extends BaseCallback { - constructor(payload: Callback); - getMessage(): string; - isLocationRequired(): boolean; - isMetadataRequired(): boolean; - // (undocumented) - payload: Callback; - setProfile(profile: DeviceProfileData): void; -} - -// @public (undocumented) -export interface DeviceProfileData { - // (undocumented) - identifier: string; - // (undocumented) - location?: Geolocation_2 | Record; - // (undocumented) - metadata?: { - hardware: { - display: { - [key: string]: string | number | null; - }; - [key: string]: any; - }; - browser: { - [key: string]: string | number | null; - }; - platform: { - [key: string]: string | number | null; - }; - }; -} - -export { FailedPolicyRequirement } - -export { FailureDetail } - -export { GenericError } - -// @public (undocumented) -interface Geolocation_2 { - // (undocumented) - latitude: number; - // (undocumented) - longitude: number; -} -export { Geolocation_2 as Geolocation } - -// @public -export class HiddenValueCallback extends BaseCallback { - constructor(payload: Callback); - // (undocumented) - payload: Callback; -} - -// @public (undocumented) -export interface IdPValue { - // (undocumented) - provider: string; - // (undocumented) - uiConfig: { - [key: string]: string; - }; -} - -// @internal -export interface InternalJourneyClientConfig { - // (undocumented) - error?: GenericError; - // (undocumented) - serverConfig: ResolvedServerConfig; -} - -export { isValidWellknownUrl } - -// @public -export interface JourneyClient { - // (undocumented) - next: (step: JourneyStep, options?: NextOptions) => Promise; - // (undocumented) - redirect: (step: JourneyStep) => Promise; - // (undocumented) - resume: (url: string, options?: ResumeOptions) => Promise; - // (undocumented) - start: (options?: StartParam) => Promise; - // (undocumented) - subscribe: (listener: () => void) => () => void; - // (undocumented) - terminate: (options?: { - query?: Record; - }) => Promise; -} - -// @public -export interface JourneyClientConfig extends AsyncLegacyConfigOptions { - // (undocumented) - serverConfig: JourneyServerConfig; -} - -// @public (undocumented) -export type JourneyLoginFailure = AuthResponse & { - type: StepType.LoginFailure; - payload: Step; - getCode: () => number; - getDetail: () => FailureDetail | undefined; - getMessage: () => string | undefined; - getProcessedMessage: (messageCreator?: MessageCreator) => ProcessedPropertyError[]; - getReason: () => string | undefined; -}; - -// @public (undocumented) -export type JourneyLoginSuccess = AuthResponse & { - type: StepType.LoginSuccess; - payload: Step; - getRealm: () => string | undefined; - getSessionToken: () => string | undefined; - getSuccessUrl: () => string | undefined; -}; - -// @public (undocumented) -export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError; - -// @public -export interface JourneyServerConfig { - timeout?: number; - wellknown: string; -} - -// @public (undocumented) -export type JourneyStep = AuthResponse & { - type: StepType.Step; - payload: Step; - callbacks: BaseCallback[]; - getCallbackOfType: (type: CallbackType) => T; - getCallbacksOfType: (type: CallbackType) => T[]; - setCallbackValue: (type: CallbackType, value: unknown) => void; - getDescription: () => string | undefined; - getHeader: () => string | undefined; - getStage: () => string | undefined; -}; - -// @public -export class KbaCreateCallback extends BaseCallback { - constructor(payload: Callback); - getPredefinedQuestions(): string[]; - getPrompt(): string; - isAllowedUserDefinedQuestions(): boolean; - // (undocumented) - payload: Callback; - setAnswer(answer: string): void; - setQuestion(question: string): void; -} - -export { LogLevel } - -// @public (undocumented) -export interface MessageCreator { - // (undocumented) - [key: string]: (propertyName: string, params?: { - [key: string]: unknown; - }) => string; -} - -// @public -export class MetadataCallback extends BaseCallback { - constructor(payload: Callback); - getData(): T; - // (undocumented) - payload: Callback; -} - -// @public -export class NameCallback extends BaseCallback { - constructor(payload: Callback); - getPrompt(): string; - // (undocumented) - payload: Callback; - setName(name: string): void; -} - -export { NameValue } - -// @public (undocumented) -export interface NextOptions { - // (undocumented) - query?: Record; -} - -// @public -export class PasswordCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getPolicies(): string[]; - getPrompt(): string; - // (undocumented) - payload: Callback; - setPassword(password: string): void; -} - -// @public -export class PingOneProtectEvaluationCallback extends BaseCallback { - constructor(payload: Callback); - getPauseBehavioralData(): boolean; - // (undocumented) - payload: Callback; - setClientError(errorMessage: string): void; - setData(data: string): void; -} - -// @public -export class PingOneProtectInitializeCallback extends BaseCallback { - constructor(payload: Callback); - getConfig(): Record | { - envId: string; - consoleLogEnabled: boolean; - deviceAttributesToIgnore: string[]; - customHost: string; - lazyMetadata: boolean; - behavioralDataCollection: boolean; - deviceKeyRsyncIntervals: number; - enableTrust: boolean; - disableTags: boolean; - disableHub: boolean; - }; - // (undocumented) - payload: Callback; - // (undocumented) - setClientError(errorMessage: string): void; -} - -export { PolicyKey } - -export { PolicyParams } - -export { PolicyRequirement } - -// @public -export class PollingWaitCallback extends BaseCallback { - constructor(payload: Callback); - getMessage(): string; - getWaitTime(): number; - // (undocumented) - payload: Callback; -} - -// @public (undocumented) -export interface ProcessedPropertyError { - // (undocumented) - detail: FailedPolicyRequirement; - // (undocumented) - messages: string[]; -} - -// @public -export class ReCaptchaCallback extends BaseCallback { - constructor(payload: Callback); - getSiteKey(): string; - // (undocumented) - payload: Callback; - setResult(result: string): void; -} - -// @public -export class ReCaptchaEnterpriseCallback extends BaseCallback { - constructor(payload: Callback); - getApiUrl(): string; - getElementClass(): string; - getSiteKey(): string; - // (undocumented) - payload: Callback; - setAction(action: string): void; - setClientError(error: string): void; - setPayload(payload: unknown): void; - setResult(result: string): void; -} - -// @public -export class RedirectCallback extends BaseCallback { - constructor(payload: Callback); - getRedirectUrl(): string; - // (undocumented) - payload: Callback; -} - -export { RequestMiddleware } - -// @public -export interface ResolvedServerConfig { - // (undocumented) - baseUrl: string; - // (undocumented) - paths: { - authenticate: string; - sessions: string; - }; -} - -// @public (undocumented) -export interface ResumeOptions { - // (undocumented) - journey?: string; - // (undocumented) - query?: Record; -} - -// @public -export class SelectIdPCallback extends BaseCallback { - constructor(payload: Callback); - getProviders(): IdPValue[]; - // (undocumented) - payload: Callback; - setProvider(value: string): void; -} - -// @public (undocumented) -export interface StartParam { - // (undocumented) - journey: string; - // (undocumented) - query?: Record; -} - -export { Step } - -export { StepDetail } - -export { StepType } - -// @public -export class SuspendedTextOutputCallback extends TextOutputCallback { - constructor(payload: Callback); - // (undocumented) - payload: Callback; -} - -// @public -export class TermsAndConditionsCallback extends BaseCallback { - constructor(payload: Callback); - getCreateDate(): Date | null; - getTerms(): string; - getVersion(): string; - // (undocumented) - payload: Callback; - setAccepted(accepted?: boolean): void; -} - -// @public -export class TextInputCallback extends BaseCallback { - constructor(payload: Callback); - getPrompt(): string; - // (undocumented) - payload: Callback; - setInput(input: string): void; -} - -// @public -export class TextOutputCallback extends BaseCallback { - constructor(payload: Callback); - getMessage(): string; - getMessageType(): string; - // (undocumented) - payload: Callback; -} - -// @public -export class ValidatedCreatePasswordCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getPolicies(): Record; - getPrompt(): string; - isRequired(): boolean; - // (undocumented) - payload: Callback; - setPassword(password: string): void; - setValidateOnly(value: boolean): void; -} - -// @public -export class ValidatedCreateUsernameCallback extends BaseCallback { - constructor(payload: Callback); - getFailedPolicies(): PolicyRequirement[]; - getPolicies(): Record; - getPrompt(): string; - isRequired(): boolean; - // (undocumented) - payload: Callback; - setName(name: string): void; - setValidateOnly(value: boolean): void; -} - -export { WellknownResponse } - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@forgerock/journey-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionTypes } from '@forgerock/sdk-request-middleware'; +import { AuthResponse } from '@forgerock/sdk-types'; +import { Callback } from '@forgerock/sdk-types'; +import { CallbackType } from '@forgerock/sdk-types'; +import { createWellknownError } from '@forgerock/sdk-utilities'; +import { CustomLogger } from '@forgerock/sdk-logger'; +import { FailedPolicyRequirement } from '@forgerock/sdk-types'; +import { FailureDetail } from '@forgerock/sdk-types'; +import { GenericError } from '@forgerock/sdk-types'; +import { isValidWellknownUrl } from '@forgerock/sdk-utilities'; +import { JourneyClientConfig } from '@forgerock/sdk-types'; +import { JourneyServerConfig } from '@forgerock/sdk-types'; +import { LogLevel } from '@forgerock/sdk-logger'; +import { NameValue } from '@forgerock/sdk-types'; +import { PolicyKey } from '@forgerock/sdk-types'; +import { PolicyParams } from '@forgerock/sdk-types'; +import { PolicyRequirement } from '@forgerock/sdk-types'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import { Step } from '@forgerock/sdk-types'; +import { StepDetail } from '@forgerock/sdk-types'; +import { StepType } from '@forgerock/sdk-types'; +import { WellknownResponse } from '@forgerock/sdk-types'; + +export { ActionTypes }; + +// @public +export class AttributeInputCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getName(): string; + getPolicies(): Record; + getPrompt(): string; + isRequired(): boolean; + // (undocumented) + payload: Callback; + setValidateOnly(value: boolean): void; + setValue(value: T): void; +} + +export { AuthResponse }; + +// @public +export class BaseCallback { + constructor(payload: Callback); + getInputValue(selector?: number | string): unknown; + getOutputByName(name: string, defaultValue: T): T; + getOutputValue(selector?: number | string): unknown; + getType(): CallbackType; + // (undocumented) + payload: Callback; + setInputValue(value: unknown, selector?: number | string | RegExp): void; +} + +export { Callback }; + +// @public (undocumented) +export type CallbackFactory = (callback: Callback) => BaseCallback; + +export { CallbackType }; + +// @public +export class ChoiceCallback extends BaseCallback { + constructor(payload: Callback); + getChoices(): string[]; + getDefaultChoice(): number; + getPrompt(): string; + // (undocumented) + payload: Callback; + setChoiceIndex(index: number): void; + setChoiceValue(value: string): void; +} + +// @public +export class ConfirmationCallback extends BaseCallback { + constructor(payload: Callback); + getDefaultOption(): number; + getMessageType(): number; + getOptions(): string[]; + getOptionType(): number; + getPrompt(): string; + // (undocumented) + payload: Callback; + setOptionIndex(index: number): void; + setOptionValue(value: string): void; +} + +// @public (undocumented) +export function createCallback(callback: Callback): BaseCallback; + +export { createWellknownError }; + +export { CustomLogger }; + +// @public +export class DeviceProfileCallback extends BaseCallback { + constructor(payload: Callback); + getMessage(): string; + isLocationRequired(): boolean; + isMetadataRequired(): boolean; + // (undocumented) + payload: Callback; + setProfile(profile: DeviceProfileData): void; +} + +// @public (undocumented) +export interface DeviceProfileData { + // (undocumented) + identifier: string; + // (undocumented) + location?: Geolocation_2 | Record; + // (undocumented) + metadata?: { + hardware: { + display: { + [key: string]: string | number | null; + }; + [key: string]: any; + }; + browser: { + [key: string]: string | number | null; + }; + platform: { + [key: string]: string | number | null; + }; + }; +} + +export { FailedPolicyRequirement }; + +export { FailureDetail }; + +export { GenericError }; + +// @public (undocumented) +interface Geolocation_2 { + // (undocumented) + latitude: number; + // (undocumented) + longitude: number; +} +export { Geolocation_2 as Geolocation }; + +// @public +export class HiddenValueCallback extends BaseCallback { + constructor(payload: Callback); + // (undocumented) + payload: Callback; +} + +// @public (undocumented) +export interface IdPValue { + // (undocumented) + provider: string; + // (undocumented) + uiConfig: { + [key: string]: string; + }; +} + +// @internal +export interface InternalJourneyClientConfig { + // (undocumented) + error?: GenericError; + // (undocumented) + serverConfig: ResolvedServerConfig; +} + +export { isValidWellknownUrl }; + +// @public +export interface JourneyClient { + // (undocumented) + next: (step: JourneyStep, options?: NextOptions) => Promise; + // (undocumented) + redirect: (step: JourneyStep) => Promise; + // (undocumented) + resume: (url: string, options?: ResumeOptions) => Promise; + // (undocumented) + start: (options?: StartParam) => Promise; + // (undocumented) + subscribe: (listener: () => void) => () => void; + // (undocumented) + terminate: (options?: { query?: Record }) => Promise; +} + +export { JourneyClientConfig }; + +// @public (undocumented) +export type JourneyLoginFailure = AuthResponse & { + type: StepType.LoginFailure; + payload: Step; + getCode: () => number; + getDetail: () => FailureDetail | undefined; + getMessage: () => string | undefined; + getProcessedMessage: (messageCreator?: MessageCreator) => ProcessedPropertyError[]; + getReason: () => string | undefined; +}; + +// @public (undocumented) +export type JourneyLoginSuccess = AuthResponse & { + type: StepType.LoginSuccess; + payload: Step; + getRealm: () => string | undefined; + getSessionToken: () => string | undefined; + getSuccessUrl: () => string | undefined; +}; + +// @public (undocumented) +export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError; + +export { JourneyServerConfig }; + +// @public (undocumented) +export type JourneyStep = AuthResponse & { + type: StepType.Step; + payload: Step; + callbacks: BaseCallback[]; + getCallbackOfType: (type: CallbackType) => T; + getCallbacksOfType: (type: CallbackType) => T[]; + setCallbackValue: (type: CallbackType, value: unknown) => void; + getDescription: () => string | undefined; + getHeader: () => string | undefined; + getStage: () => string | undefined; +}; + +// @public +export class KbaCreateCallback extends BaseCallback { + constructor(payload: Callback); + getPredefinedQuestions(): string[]; + getPrompt(): string; + isAllowedUserDefinedQuestions(): boolean; + // (undocumented) + payload: Callback; + setAnswer(answer: string): void; + setQuestion(question: string): void; +} + +export { LogLevel }; + +// @public (undocumented) +export interface MessageCreator { + // (undocumented) + [key: string]: ( + propertyName: string, + params?: { + [key: string]: unknown; + }, + ) => string; +} + +// @public +export class MetadataCallback extends BaseCallback { + constructor(payload: Callback); + getData(): T; + // (undocumented) + payload: Callback; +} + +// @public +export class NameCallback extends BaseCallback { + constructor(payload: Callback); + getPrompt(): string; + // (undocumented) + payload: Callback; + setName(name: string): void; +} + +export { NameValue }; + +// @public (undocumented) +export interface NextOptions { + // (undocumented) + query?: Record; +} + +// @public +export class PasswordCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getPolicies(): string[]; + getPrompt(): string; + // (undocumented) + payload: Callback; + setPassword(password: string): void; +} + +// @public +export class PingOneProtectEvaluationCallback extends BaseCallback { + constructor(payload: Callback); + getPauseBehavioralData(): boolean; + // (undocumented) + payload: Callback; + setClientError(errorMessage: string): void; + setData(data: string): void; +} + +// @public +export class PingOneProtectInitializeCallback extends BaseCallback { + constructor(payload: Callback); + getConfig(): + | Record + | { + envId: string; + consoleLogEnabled: boolean; + deviceAttributesToIgnore: string[]; + customHost: string; + lazyMetadata: boolean; + behavioralDataCollection: boolean; + deviceKeyRsyncIntervals: number; + enableTrust: boolean; + disableTags: boolean; + disableHub: boolean; + }; + // (undocumented) + payload: Callback; + // (undocumented) + setClientError(errorMessage: string): void; +} + +export { PolicyKey }; + +export { PolicyParams }; + +export { PolicyRequirement }; + +// @public +export class PollingWaitCallback extends BaseCallback { + constructor(payload: Callback); + getMessage(): string; + getWaitTime(): number; + // (undocumented) + payload: Callback; +} + +// @public (undocumented) +export interface ProcessedPropertyError { + // (undocumented) + detail: FailedPolicyRequirement; + // (undocumented) + messages: string[]; +} + +// @public +export class ReCaptchaCallback extends BaseCallback { + constructor(payload: Callback); + getSiteKey(): string; + // (undocumented) + payload: Callback; + setResult(result: string): void; +} + +// @public +export class ReCaptchaEnterpriseCallback extends BaseCallback { + constructor(payload: Callback); + getApiUrl(): string; + getElementClass(): string; + getSiteKey(): string; + // (undocumented) + payload: Callback; + setAction(action: string): void; + setClientError(error: string): void; + setPayload(payload: unknown): void; + setResult(result: string): void; +} + +// @public +export class RedirectCallback extends BaseCallback { + constructor(payload: Callback); + getRedirectUrl(): string; + // (undocumented) + payload: Callback; +} + +export { RequestMiddleware }; + +// @public +export interface ResolvedServerConfig { + // (undocumented) + baseUrl: string; + // (undocumented) + paths: { + authenticate: string; + sessions: string; + }; +} + +// @public (undocumented) +export interface ResumeOptions { + // (undocumented) + journey?: string; + // (undocumented) + query?: Record; +} + +// @public +export class SelectIdPCallback extends BaseCallback { + constructor(payload: Callback); + getProviders(): IdPValue[]; + // (undocumented) + payload: Callback; + setProvider(value: string): void; +} + +// @public (undocumented) +export interface StartParam { + // (undocumented) + journey: string; + // (undocumented) + query?: Record; +} + +export { Step }; + +export { StepDetail }; + +export { StepType }; + +// @public +export class SuspendedTextOutputCallback extends TextOutputCallback { + constructor(payload: Callback); + // (undocumented) + payload: Callback; +} + +// @public +export class TermsAndConditionsCallback extends BaseCallback { + constructor(payload: Callback); + getCreateDate(): Date | null; + getTerms(): string; + getVersion(): string; + // (undocumented) + payload: Callback; + setAccepted(accepted?: boolean): void; +} + +// @public +export class TextInputCallback extends BaseCallback { + constructor(payload: Callback); + getPrompt(): string; + // (undocumented) + payload: Callback; + setInput(input: string): void; +} + +// @public +export class TextOutputCallback extends BaseCallback { + constructor(payload: Callback); + getMessage(): string; + getMessageType(): string; + // (undocumented) + payload: Callback; +} + +// @public +export class ValidatedCreatePasswordCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getPolicies(): Record; + getPrompt(): string; + isRequired(): boolean; + // (undocumented) + payload: Callback; + setPassword(password: string): void; + setValidateOnly(value: boolean): void; +} + +// @public +export class ValidatedCreateUsernameCallback extends BaseCallback { + constructor(payload: Callback); + getFailedPolicies(): PolicyRequirement[]; + getPolicies(): Record; + getPrompt(): string; + isRequired(): boolean; + // (undocumented) + payload: Callback; + setName(name: string): void; + setValidateOnly(value: boolean): void; +} + +export { WellknownResponse }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/journey-client/src/lib/client.store.test.ts b/packages/journey-client/src/lib/client.store.test.ts index 3d150328bf..7d0ddf9345 100644 --- a/packages/journey-client/src/lib/client.store.test.ts +++ b/packages/journey-client/src/lib/client.store.test.ts @@ -1,6 +1,6 @@ // @vitest-environment node /* - * Copyright (c) 2025-2026 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. @@ -9,6 +9,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { journey } from './client.store.js'; +import { makeJourneyConfig } from '@forgerock/sdk-utilities'; import { createJourneyStep } from './step.utils.js'; import { callbackType, type GenericError, type Step, type WellknownResponse } from '../index.js'; @@ -559,4 +560,46 @@ describe('journey-client', () => { expect(request.url).toBe('https://test.com/am/json/realms/root/realms/alpha/authenticate'); }); }); + + describe('unified JSON config entry', () => { + test('accepts unified JSON config and initializes successfully', async () => { + setupMockFetch(); + + const unifiedConfig = { + oidc: { + clientId: 'ignored-by-journey', + discoveryEndpoint: mockWellknownUrl, + scopes: ['openid'], + redirectUri: 'https://example.com/callback', + }, + }; + + const client = await journey({ config: makeJourneyConfig(unifiedConfig) }); + expect(client).toHaveProperty('start'); + expect(client).toHaveProperty('next'); + }); + + test('throws when unified JSON config has missing required field', async () => { + const invalidConfig = { + oidc: { + // discoveryEndpoint missing — required even for journey + }, + }; + + expect(() => makeJourneyConfig(invalidConfig)).toThrow(/Invalid unified SDK config/); + }); + + test('throws when unified JSON config has wrong field type', async () => { + const invalidConfig = { + oidc: { + clientId: '123', + discoveryEndpoint: mockWellknownUrl, + scopes: 'openid', // should be array + redirectUri: 'https://example.com/callback', + }, + }; + + expect(() => makeJourneyConfig(invalidConfig)).toThrow(/Invalid unified SDK config/); + }); + }); }); diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 19d1fabe70..32c2689a64 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025-2026 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. @@ -12,7 +12,6 @@ import { isValidWellknownUrl, createWellknownError, } from '@forgerock/sdk-utilities'; - import type { GenericError } from '@forgerock/sdk-types'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { Step } from '@forgerock/sdk-types'; @@ -82,7 +81,10 @@ export async function journey({ custom?: CustomLogger; }; }): Promise { - const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); + const log = loggerFn({ + level: logger?.level ?? config.log ?? 'error', + custom: logger?.custom, + }); const ignoredProperties = [ 'callbackFactory', diff --git a/packages/journey-client/src/lib/config.types.ts b/packages/journey-client/src/lib/config.types.ts index 25eaeaa888..34dc7ec0e4 100644 --- a/packages/journey-client/src/lib/config.types.ts +++ b/packages/journey-client/src/lib/config.types.ts @@ -1,46 +1,14 @@ /* - * 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. */ -import type { AsyncLegacyConfigOptions, GenericError } from '@forgerock/sdk-types'; +import type { GenericError } from '@forgerock/sdk-types'; import type { ResolvedServerConfig } from './wellknown.utils.js'; -/** - * Server configuration for journey-client. - * - * Only the OIDC discovery endpoint URL is required. All other configuration - * (baseUrl, paths) is automatically derived from the well-known response. - */ -export interface JourneyServerConfig { - /** Required OIDC discovery endpoint URL */ - wellknown: string; - /** Optional request timeout in milliseconds. Included for config-sharing compatibility with other clients. */ - timeout?: number; -} - -/** - * Configuration for creating a journey client instance. - * - * Extends {@link AsyncLegacyConfigOptions} so that the same config object can - * be shared across journey-client, davinci-client, and oidc-client. Properties - * like `clientId`, `scope`, and `redirectUri` are accepted but not used by - * journey-client — a warning is logged when they are provided. - * - * @example - * ```typescript - * const config: JourneyClientConfig = { - * serverConfig: { - * wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration', - * }, - * }; - * ``` - */ -export interface JourneyClientConfig extends AsyncLegacyConfigOptions { - serverConfig: JourneyServerConfig; -} +export type { JourneyServerConfig, JourneyClientConfig } from '@forgerock/sdk-types'; /** * Internal configuration after wellknown discovery and path resolution. diff --git a/packages/oidc-client/api-report/oidc-client.api.md b/packages/oidc-client/api-report/oidc-client.api.md index e934c022e6..8ed36ece40 100644 --- a/packages/oidc-client/api-report/oidc-client.api.md +++ b/packages/oidc-client/api-report/oidc-client.api.md @@ -1,390 +1,575 @@ -## API Report File for "@forgerock/oidc-client" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; -import { BaseQueryFn } from '@reduxjs/toolkit/query'; -import { CombinedState } from '@reduxjs/toolkit/query'; -import { CustomLogger } from '@forgerock/sdk-logger'; -import { EnhancedStore } from '@reduxjs/toolkit'; -import { FetchArgs } from '@reduxjs/toolkit/query'; -import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; -import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; -import { GenericError } from '@forgerock/sdk-types'; -import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; -import { logger } from '@forgerock/sdk-logger'; -import { LogLevel } from '@forgerock/sdk-logger'; -import { LogMessage } from '@forgerock/sdk-logger'; -import { MutationDefinition } from '@reduxjs/toolkit/query'; -import { QueryDefinition } from '@reduxjs/toolkit/query'; -import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; -import { StorageConfig } from '@forgerock/storage'; -import { StoreEnhancer } from '@reduxjs/toolkit'; -import { ThunkDispatch } from '@reduxjs/toolkit'; -import { Tuple } from '@reduxjs/toolkit'; -import { UnknownAction } from '@reduxjs/toolkit'; -import { Unsubscribe } from '@reduxjs/toolkit'; -import { WellknownResponse } from '@forgerock/sdk-types'; - -export { ActionTypes } - -// @public (undocumented) -export type AppDispatch = ReturnType; - -// @public (undocumented) -export interface AuthorizationError { - // (undocumented) - error: string; - // (undocumented) - error_description: string; - // (undocumented) - redirectUrl?: string; - // (undocumented) - type: 'auth_error' | 'argument_error' | 'network_error' | 'unknown_error' | 'wellknown_error'; -} - -// @public (undocumented) -export interface AuthorizationSuccess { - // (undocumented) - code: string; - // (undocumented) - state: string; -} - -// @public (undocumented) -export interface AuthorizeErrorResponse { - // (undocumented) - code?: string; - // (undocumented) - details?: [ - { - code: string; - message: string; - } - ]; - // (undocumented) - id?: string; - // (undocumented) - message?: string; -} - -// @public (undocumented) -export interface AuthorizeSuccessResponse { - // (undocumented) - authorizeResponse?: { - code: string; - state: string; - }; - // (undocumented) - createdAt?: string; - // (undocumented) - _embedded?: { - [key: string]: unknown; - }; - // (undocumented) - environment?: { - id: string; - }; - // (undocumented) - expiresAt?: string; - // (undocumented) - id?: string; - // (undocumented) - _links?: { - [key: string]: { - href: string; - }; - }; - // (undocumented) - resumeUrl?: string; - // (undocumented) - session?: { - id: string; - }; - // (undocumented) - status?: string; -} - -// @public (undocumented) -export type BuildAuthorizationData = [string, GetAuthorizationUrlOptions]; - -// @public (undocumented) -export type ClientStore = ReturnType; - -// @public -export function createClientStore(input: { - requestMiddleware?: RequestMiddleware[]; - logger?: ReturnType; -}): EnhancedStore< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, UnknownAction, Tuple<[StoreEnhancer< { -dispatch: ThunkDispatch< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, { -requestMiddleware: RequestMiddleware[] | undefined; -logger: { -changeLevel: (level: LogLevel) => void; -error: (...args: LogMessage[]) => void; -warn: (...args: LogMessage[]) => void; -info: (...args: LogMessage[]) => void; -debug: (...args: LogMessage[]) => void; -} | undefined; -}, UnknownAction>; -}>, StoreEnhancer]>>; - -export { CustomLogger } - -export { GenericError } - -export { GetAuthorizationUrlOptions } - -// @public (undocumented) -export interface GetTokensOptions { - // (undocumented) - authorizeOptions?: GetAuthorizationUrlOptions; - // (undocumented) - backgroundRenew?: boolean; - // (undocumented) - forceRenew?: boolean; - // (undocumented) - storageOptions?: Partial; -} - -export { LogLevel } - -// @public (undocumented) -export type LogoutErrorResult = { - error: string; - sessionResponse: GenericError | null; - revokeResponse: GenericError | null; - deleteResponse: GenericError | null; -}; - -// @public (undocumented) -export type LogoutSuccessResult = RevokeSuccessResult & { - sessionResponse: null; -}; - -// @public (undocumented) -export interface OauthTokens { - // (undocumented) - accessToken: string; - // (undocumented) - expiresAt?: number; - // (undocumented) - expiryTimestamp?: number; - // (undocumented) - idToken: string; - // (undocumented) - refreshToken?: string; -} - -// @public -export function oidc(input: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - storage?: Partial; -}): Promise<{ - error: string; - type: string; - subscribe?: undefined; - authorize?: undefined; - token?: undefined; - user?: undefined; -} | { - subscribe: (listener: () => void) => Unsubscribe; - authorize: { - url: (options?: GetAuthorizationUrlOptions) => Promise; - background: (options?: GetAuthorizationUrlOptions) => Promise; - }; - token: { - exchange: (code: string, state: string, options?: Partial) => Promise; - get: (options?: GetTokensOptions) => Promise; - revoke: () => Promise; - }; - user: { - info: () => Promise; - logout: () => Promise; - }; - error?: undefined; - type?: undefined; -}>; - -// @public (undocumented) -export type OidcClient = Awaited>; - -// @public (undocumented) -export interface OidcConfig extends AsyncLegacyConfigOptions { - // (undocumented) - clientId: string; - // (undocumented) - par?: boolean; - // (undocumented) - redirectUri: string; - // (undocumented) - responseType?: ResponseType_2; - // (undocumented) - scope: string; - // (undocumented) - serverConfig: { - wellknown: string; - timeout?: number; - }; -} - -// @public (undocumented) -export type OptionalAuthorizeOptions = Partial; - -// @public (undocumented) -export interface PushAuthorizationResponse { - // (undocumented) - expires_in: number; - // (undocumented) - request_uri: string; -} - -export { RequestMiddleware } - -export { ResponseType_2 as ResponseType } - -// @public (undocumented) -export type RevokeErrorResult = { - error: string; - revokeResponse: GenericError | null; - deleteResponse: GenericError | null; -}; - -// @public (undocumented) -export type RevokeSuccessResult = { - revokeResponse: null; - deleteResponse: null; -}; - -// @public (undocumented) -export type RootState = ReturnType; - -export { StorageConfig } - -// @public (undocumented) -export interface TokenExchangeErrorResponse { - // (undocumented) - error: string; - // (undocumented) - message: string; - // (undocumented) - type: 'exchange_error' | 'network_error' | 'state_error' | 'unknown_error'; -} - -// @public (undocumented) -export interface TokenExchangeResponse { - // (undocumented) - access_token: string; - // (undocumented) - expires_in?: number; - // (undocumented) - id_token: string; - // (undocumented) - refresh_token?: string; - // (undocumented) - scope?: string; - // (undocumented) - token_type?: string; -} - -// @public (undocumented) -export interface TokenRequestOptions { - // (undocumented) - code: string; - // (undocumented) - config: OidcConfig; - // (undocumented) - endpoint: string; - // (undocumented) - verifier?: string; -} - -// @public (undocumented) -export type UserInfoResponse = { - sub: string; - [key: string]: unknown; -}; - -export { WellknownResponse } - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@forgerock/oidc-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionTypes } from '@forgerock/sdk-request-middleware'; +import { BaseQueryFn } from '@reduxjs/toolkit/query'; +import { CombinedState } from '@reduxjs/toolkit/query'; +import { CustomLogger } from '@forgerock/sdk-logger'; +import { EnhancedStore } from '@reduxjs/toolkit'; +import { FetchArgs } from '@reduxjs/toolkit/query'; +import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; +import { GenericError } from '@forgerock/sdk-types'; +import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +import { logger } from '@forgerock/sdk-logger'; +import { LogLevel } from '@forgerock/sdk-logger'; +import { LogMessage } from '@forgerock/sdk-logger'; +import { MutationDefinition } from '@reduxjs/toolkit/query'; +import { OidcConfig } from '@forgerock/sdk-types'; +import { QueryDefinition } from '@reduxjs/toolkit/query'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; +import { StorageConfig } from '@forgerock/storage'; +import { StoreEnhancer } from '@reduxjs/toolkit'; +import { ThunkDispatch } from '@reduxjs/toolkit'; +import { Tuple } from '@reduxjs/toolkit'; +import { UnknownAction } from '@reduxjs/toolkit'; +import { Unsubscribe } from '@reduxjs/toolkit'; +import { WellknownResponse } from '@forgerock/sdk-types'; + +export { ActionTypes }; + +// @public (undocumented) +export type AppDispatch = ReturnType; + +// @public (undocumented) +export interface AuthorizationError { + // (undocumented) + error: string; + // (undocumented) + error_description: string; + // (undocumented) + redirectUrl?: string; + // (undocumented) + type: 'auth_error' | 'argument_error' | 'network_error' | 'unknown_error' | 'wellknown_error'; +} + +// @public (undocumented) +export interface AuthorizationSuccess { + // (undocumented) + code: string; + // (undocumented) + state: string; +} + +// @public (undocumented) +export interface AuthorizeErrorResponse { + // (undocumented) + code?: string; + // (undocumented) + details?: [ + { + code: string; + message: string; + }, + ]; + // (undocumented) + id?: string; + // (undocumented) + message?: string; +} + +// @public (undocumented) +export interface AuthorizeSuccessResponse { + // (undocumented) + authorizeResponse?: { + code: string; + state: string; + }; + // (undocumented) + createdAt?: string; + // (undocumented) + _embedded?: { + [key: string]: unknown; + }; + // (undocumented) + environment?: { + id: string; + }; + // (undocumented) + expiresAt?: string; + // (undocumented) + id?: string; + // (undocumented) + _links?: { + [key: string]: { + href: string; + }; + }; + // (undocumented) + resumeUrl?: string; + // (undocumented) + session?: { + id: string; + }; + // (undocumented) + status?: string; +} + +// @public (undocumented) +export type BuildAuthorizationData = [string, GetAuthorizationUrlOptions]; + +// @public (undocumented) +export type ClientStore = ReturnType; + +// @public +export function createClientStore(input: { + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; +}): EnhancedStore< + { + oidc: CombinedState< + { + authorizeFetch: MutationDefinition< + { + url: string; + }, + BaseQueryFn, + never, + AuthorizeSuccessResponse, + 'oidc', + unknown + >; + par: MutationDefinition< + { + endpoint: string; + body: URLSearchParams; + }, + BaseQueryFn, + never, + PushAuthorizationResponse, + 'oidc', + unknown + >; + authorizeIframe: MutationDefinition< + { + url: string; + }, + BaseQueryFn, + never, + AuthorizationSuccess, + 'oidc', + unknown + >; + endSession: MutationDefinition< + { + idToken: string; + endpoint: string; + signOutRedirectUri?: string; + }, + BaseQueryFn, + never, + null, + 'oidc', + unknown + >; + exchange: MutationDefinition< + { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + }, + BaseQueryFn, + never, + TokenExchangeResponse, + 'oidc', + unknown + >; + revoke: MutationDefinition< + { + accessToken: string; + clientId?: string; + endpoint: string; + }, + BaseQueryFn, + never, + object, + 'oidc', + unknown + >; + userInfo: MutationDefinition< + { + accessToken: string; + endpoint: string; + }, + BaseQueryFn, + never, + UserInfoResponse, + 'oidc', + unknown + >; + }, + never, + 'oidc' + >; + wellknown: CombinedState< + { + configuration: QueryDefinition< + string, + BaseQueryFn, + never, + WellknownResponse, + 'wellknown', + unknown + >; + }, + never, + 'wellknown' + >; + }, + UnknownAction, + Tuple< + [ + StoreEnhancer<{ + dispatch: ThunkDispatch< + { + oidc: CombinedState< + { + authorizeFetch: MutationDefinition< + { + url: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + AuthorizeSuccessResponse, + 'oidc', + unknown + >; + par: MutationDefinition< + { + endpoint: string; + body: URLSearchParams; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + PushAuthorizationResponse, + 'oidc', + unknown + >; + authorizeIframe: MutationDefinition< + { + url: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + AuthorizationSuccess, + 'oidc', + unknown + >; + endSession: MutationDefinition< + { + idToken: string; + endpoint: string; + signOutRedirectUri?: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + null, + 'oidc', + unknown + >; + exchange: MutationDefinition< + { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + TokenExchangeResponse, + 'oidc', + unknown + >; + revoke: MutationDefinition< + { + accessToken: string; + clientId?: string; + endpoint: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + object, + 'oidc', + unknown + >; + userInfo: MutationDefinition< + { + accessToken: string; + endpoint: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + UserInfoResponse, + 'oidc', + unknown + >; + }, + never, + 'oidc' + >; + wellknown: CombinedState< + { + configuration: QueryDefinition< + string, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + WellknownResponse, + 'wellknown', + unknown + >; + }, + never, + 'wellknown' + >; + }, + { + requestMiddleware: RequestMiddleware[] | undefined; + logger: + | { + changeLevel: (level: LogLevel) => void; + error: (...args: LogMessage[]) => void; + warn: (...args: LogMessage[]) => void; + info: (...args: LogMessage[]) => void; + debug: (...args: LogMessage[]) => void; + } + | undefined; + }, + UnknownAction + >; + }>, + StoreEnhancer, + ] + > +>; + +export { CustomLogger }; + +export { GenericError }; + +export { GetAuthorizationUrlOptions }; + +// @public (undocumented) +export interface GetTokensOptions { + // (undocumented) + authorizeOptions?: GetAuthorizationUrlOptions; + // (undocumented) + backgroundRenew?: boolean; + // (undocumented) + forceRenew?: boolean; + // (undocumented) + storageOptions?: Partial; +} + +export { LogLevel }; + +// @public (undocumented) +export type LogoutErrorResult = { + error: string; + sessionResponse: GenericError | null; + revokeResponse: GenericError | null; + deleteResponse: GenericError | null; +}; + +// @public (undocumented) +export type LogoutSuccessResult = RevokeSuccessResult & { + sessionResponse: null; +}; + +// @public (undocumented) +export interface OauthTokens { + // (undocumented) + accessToken: string; + // (undocumented) + expiresAt?: number; + // (undocumented) + expiryTimestamp?: number; + // (undocumented) + idToken: string; + // (undocumented) + refreshToken?: string; +} + +// @public +export function oidc(input: { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + storage?: Partial; +}): Promise< + | { + error: string; + type: string; + subscribe?: undefined; + authorize?: undefined; + token?: undefined; + user?: undefined; + } + | { + subscribe: (listener: () => void) => Unsubscribe; + authorize: { + url: (options?: GetAuthorizationUrlOptions) => Promise; + background: ( + options?: GetAuthorizationUrlOptions, + ) => Promise; + }; + token: { + exchange: ( + code: string, + state: string, + options?: Partial, + ) => Promise; + get: ( + options?: GetTokensOptions, + ) => Promise; + revoke: () => Promise; + }; + user: { + info: () => Promise; + logout: () => Promise; + }; + error?: undefined; + type?: undefined; + } +>; + +// @public (undocumented) +export type OidcClient = Awaited>; + +export { OidcConfig }; + +// @public (undocumented) +export type OptionalAuthorizeOptions = Partial; + +// @public (undocumented) +export interface PushAuthorizationResponse { + // (undocumented) + expires_in: number; + // (undocumented) + request_uri: string; +} + +export { RequestMiddleware }; + +export { ResponseType_2 as ResponseType }; + +// @public (undocumented) +export type RevokeErrorResult = { + error: string; + revokeResponse: GenericError | null; + deleteResponse: GenericError | null; +}; + +// @public (undocumented) +export type RevokeSuccessResult = { + revokeResponse: null; + deleteResponse: null; +}; + +// @public (undocumented) +export type RootState = ReturnType; + +export { StorageConfig }; + +// @public (undocumented) +export interface TokenExchangeErrorResponse { + // (undocumented) + error: string; + // (undocumented) + message: string; + // (undocumented) + type: 'exchange_error' | 'network_error' | 'state_error' | 'unknown_error'; +} + +// @public (undocumented) +export interface TokenExchangeResponse { + // (undocumented) + access_token: string; + // (undocumented) + expires_in?: number; + // (undocumented) + id_token: string; + // (undocumented) + refresh_token?: string; + // (undocumented) + scope?: string; + // (undocumented) + token_type?: string; +} + +// @public (undocumented) +export interface TokenRequestOptions { + // (undocumented) + code: string; + // (undocumented) + config: OidcConfig; + // (undocumented) + endpoint: string; + // (undocumented) + verifier?: string; +} + +// @public (undocumented) +export type UserInfoResponse = { + sub: string; + [key: string]: unknown; +}; + +export { WellknownResponse }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/oidc-client/api-report/oidc-client.types.api.md b/packages/oidc-client/api-report/oidc-client.types.api.md index e934c022e6..8ed36ece40 100644 --- a/packages/oidc-client/api-report/oidc-client.types.api.md +++ b/packages/oidc-client/api-report/oidc-client.types.api.md @@ -1,390 +1,575 @@ -## API Report File for "@forgerock/oidc-client" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; -import { BaseQueryFn } from '@reduxjs/toolkit/query'; -import { CombinedState } from '@reduxjs/toolkit/query'; -import { CustomLogger } from '@forgerock/sdk-logger'; -import { EnhancedStore } from '@reduxjs/toolkit'; -import { FetchArgs } from '@reduxjs/toolkit/query'; -import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; -import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; -import { GenericError } from '@forgerock/sdk-types'; -import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; -import { logger } from '@forgerock/sdk-logger'; -import { LogLevel } from '@forgerock/sdk-logger'; -import { LogMessage } from '@forgerock/sdk-logger'; -import { MutationDefinition } from '@reduxjs/toolkit/query'; -import { QueryDefinition } from '@reduxjs/toolkit/query'; -import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; -import { StorageConfig } from '@forgerock/storage'; -import { StoreEnhancer } from '@reduxjs/toolkit'; -import { ThunkDispatch } from '@reduxjs/toolkit'; -import { Tuple } from '@reduxjs/toolkit'; -import { UnknownAction } from '@reduxjs/toolkit'; -import { Unsubscribe } from '@reduxjs/toolkit'; -import { WellknownResponse } from '@forgerock/sdk-types'; - -export { ActionTypes } - -// @public (undocumented) -export type AppDispatch = ReturnType; - -// @public (undocumented) -export interface AuthorizationError { - // (undocumented) - error: string; - // (undocumented) - error_description: string; - // (undocumented) - redirectUrl?: string; - // (undocumented) - type: 'auth_error' | 'argument_error' | 'network_error' | 'unknown_error' | 'wellknown_error'; -} - -// @public (undocumented) -export interface AuthorizationSuccess { - // (undocumented) - code: string; - // (undocumented) - state: string; -} - -// @public (undocumented) -export interface AuthorizeErrorResponse { - // (undocumented) - code?: string; - // (undocumented) - details?: [ - { - code: string; - message: string; - } - ]; - // (undocumented) - id?: string; - // (undocumented) - message?: string; -} - -// @public (undocumented) -export interface AuthorizeSuccessResponse { - // (undocumented) - authorizeResponse?: { - code: string; - state: string; - }; - // (undocumented) - createdAt?: string; - // (undocumented) - _embedded?: { - [key: string]: unknown; - }; - // (undocumented) - environment?: { - id: string; - }; - // (undocumented) - expiresAt?: string; - // (undocumented) - id?: string; - // (undocumented) - _links?: { - [key: string]: { - href: string; - }; - }; - // (undocumented) - resumeUrl?: string; - // (undocumented) - session?: { - id: string; - }; - // (undocumented) - status?: string; -} - -// @public (undocumented) -export type BuildAuthorizationData = [string, GetAuthorizationUrlOptions]; - -// @public (undocumented) -export type ClientStore = ReturnType; - -// @public -export function createClientStore(input: { - requestMiddleware?: RequestMiddleware[]; - logger?: ReturnType; -}): EnhancedStore< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, UnknownAction, Tuple<[StoreEnhancer< { -dispatch: ThunkDispatch< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, { -requestMiddleware: RequestMiddleware[] | undefined; -logger: { -changeLevel: (level: LogLevel) => void; -error: (...args: LogMessage[]) => void; -warn: (...args: LogMessage[]) => void; -info: (...args: LogMessage[]) => void; -debug: (...args: LogMessage[]) => void; -} | undefined; -}, UnknownAction>; -}>, StoreEnhancer]>>; - -export { CustomLogger } - -export { GenericError } - -export { GetAuthorizationUrlOptions } - -// @public (undocumented) -export interface GetTokensOptions { - // (undocumented) - authorizeOptions?: GetAuthorizationUrlOptions; - // (undocumented) - backgroundRenew?: boolean; - // (undocumented) - forceRenew?: boolean; - // (undocumented) - storageOptions?: Partial; -} - -export { LogLevel } - -// @public (undocumented) -export type LogoutErrorResult = { - error: string; - sessionResponse: GenericError | null; - revokeResponse: GenericError | null; - deleteResponse: GenericError | null; -}; - -// @public (undocumented) -export type LogoutSuccessResult = RevokeSuccessResult & { - sessionResponse: null; -}; - -// @public (undocumented) -export interface OauthTokens { - // (undocumented) - accessToken: string; - // (undocumented) - expiresAt?: number; - // (undocumented) - expiryTimestamp?: number; - // (undocumented) - idToken: string; - // (undocumented) - refreshToken?: string; -} - -// @public -export function oidc(input: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - storage?: Partial; -}): Promise<{ - error: string; - type: string; - subscribe?: undefined; - authorize?: undefined; - token?: undefined; - user?: undefined; -} | { - subscribe: (listener: () => void) => Unsubscribe; - authorize: { - url: (options?: GetAuthorizationUrlOptions) => Promise; - background: (options?: GetAuthorizationUrlOptions) => Promise; - }; - token: { - exchange: (code: string, state: string, options?: Partial) => Promise; - get: (options?: GetTokensOptions) => Promise; - revoke: () => Promise; - }; - user: { - info: () => Promise; - logout: () => Promise; - }; - error?: undefined; - type?: undefined; -}>; - -// @public (undocumented) -export type OidcClient = Awaited>; - -// @public (undocumented) -export interface OidcConfig extends AsyncLegacyConfigOptions { - // (undocumented) - clientId: string; - // (undocumented) - par?: boolean; - // (undocumented) - redirectUri: string; - // (undocumented) - responseType?: ResponseType_2; - // (undocumented) - scope: string; - // (undocumented) - serverConfig: { - wellknown: string; - timeout?: number; - }; -} - -// @public (undocumented) -export type OptionalAuthorizeOptions = Partial; - -// @public (undocumented) -export interface PushAuthorizationResponse { - // (undocumented) - expires_in: number; - // (undocumented) - request_uri: string; -} - -export { RequestMiddleware } - -export { ResponseType_2 as ResponseType } - -// @public (undocumented) -export type RevokeErrorResult = { - error: string; - revokeResponse: GenericError | null; - deleteResponse: GenericError | null; -}; - -// @public (undocumented) -export type RevokeSuccessResult = { - revokeResponse: null; - deleteResponse: null; -}; - -// @public (undocumented) -export type RootState = ReturnType; - -export { StorageConfig } - -// @public (undocumented) -export interface TokenExchangeErrorResponse { - // (undocumented) - error: string; - // (undocumented) - message: string; - // (undocumented) - type: 'exchange_error' | 'network_error' | 'state_error' | 'unknown_error'; -} - -// @public (undocumented) -export interface TokenExchangeResponse { - // (undocumented) - access_token: string; - // (undocumented) - expires_in?: number; - // (undocumented) - id_token: string; - // (undocumented) - refresh_token?: string; - // (undocumented) - scope?: string; - // (undocumented) - token_type?: string; -} - -// @public (undocumented) -export interface TokenRequestOptions { - // (undocumented) - code: string; - // (undocumented) - config: OidcConfig; - // (undocumented) - endpoint: string; - // (undocumented) - verifier?: string; -} - -// @public (undocumented) -export type UserInfoResponse = { - sub: string; - [key: string]: unknown; -}; - -export { WellknownResponse } - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@forgerock/oidc-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionTypes } from '@forgerock/sdk-request-middleware'; +import { BaseQueryFn } from '@reduxjs/toolkit/query'; +import { CombinedState } from '@reduxjs/toolkit/query'; +import { CustomLogger } from '@forgerock/sdk-logger'; +import { EnhancedStore } from '@reduxjs/toolkit'; +import { FetchArgs } from '@reduxjs/toolkit/query'; +import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; +import { GenericError } from '@forgerock/sdk-types'; +import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +import { logger } from '@forgerock/sdk-logger'; +import { LogLevel } from '@forgerock/sdk-logger'; +import { LogMessage } from '@forgerock/sdk-logger'; +import { MutationDefinition } from '@reduxjs/toolkit/query'; +import { OidcConfig } from '@forgerock/sdk-types'; +import { QueryDefinition } from '@reduxjs/toolkit/query'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; +import { StorageConfig } from '@forgerock/storage'; +import { StoreEnhancer } from '@reduxjs/toolkit'; +import { ThunkDispatch } from '@reduxjs/toolkit'; +import { Tuple } from '@reduxjs/toolkit'; +import { UnknownAction } from '@reduxjs/toolkit'; +import { Unsubscribe } from '@reduxjs/toolkit'; +import { WellknownResponse } from '@forgerock/sdk-types'; + +export { ActionTypes }; + +// @public (undocumented) +export type AppDispatch = ReturnType; + +// @public (undocumented) +export interface AuthorizationError { + // (undocumented) + error: string; + // (undocumented) + error_description: string; + // (undocumented) + redirectUrl?: string; + // (undocumented) + type: 'auth_error' | 'argument_error' | 'network_error' | 'unknown_error' | 'wellknown_error'; +} + +// @public (undocumented) +export interface AuthorizationSuccess { + // (undocumented) + code: string; + // (undocumented) + state: string; +} + +// @public (undocumented) +export interface AuthorizeErrorResponse { + // (undocumented) + code?: string; + // (undocumented) + details?: [ + { + code: string; + message: string; + }, + ]; + // (undocumented) + id?: string; + // (undocumented) + message?: string; +} + +// @public (undocumented) +export interface AuthorizeSuccessResponse { + // (undocumented) + authorizeResponse?: { + code: string; + state: string; + }; + // (undocumented) + createdAt?: string; + // (undocumented) + _embedded?: { + [key: string]: unknown; + }; + // (undocumented) + environment?: { + id: string; + }; + // (undocumented) + expiresAt?: string; + // (undocumented) + id?: string; + // (undocumented) + _links?: { + [key: string]: { + href: string; + }; + }; + // (undocumented) + resumeUrl?: string; + // (undocumented) + session?: { + id: string; + }; + // (undocumented) + status?: string; +} + +// @public (undocumented) +export type BuildAuthorizationData = [string, GetAuthorizationUrlOptions]; + +// @public (undocumented) +export type ClientStore = ReturnType; + +// @public +export function createClientStore(input: { + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; +}): EnhancedStore< + { + oidc: CombinedState< + { + authorizeFetch: MutationDefinition< + { + url: string; + }, + BaseQueryFn, + never, + AuthorizeSuccessResponse, + 'oidc', + unknown + >; + par: MutationDefinition< + { + endpoint: string; + body: URLSearchParams; + }, + BaseQueryFn, + never, + PushAuthorizationResponse, + 'oidc', + unknown + >; + authorizeIframe: MutationDefinition< + { + url: string; + }, + BaseQueryFn, + never, + AuthorizationSuccess, + 'oidc', + unknown + >; + endSession: MutationDefinition< + { + idToken: string; + endpoint: string; + signOutRedirectUri?: string; + }, + BaseQueryFn, + never, + null, + 'oidc', + unknown + >; + exchange: MutationDefinition< + { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + }, + BaseQueryFn, + never, + TokenExchangeResponse, + 'oidc', + unknown + >; + revoke: MutationDefinition< + { + accessToken: string; + clientId?: string; + endpoint: string; + }, + BaseQueryFn, + never, + object, + 'oidc', + unknown + >; + userInfo: MutationDefinition< + { + accessToken: string; + endpoint: string; + }, + BaseQueryFn, + never, + UserInfoResponse, + 'oidc', + unknown + >; + }, + never, + 'oidc' + >; + wellknown: CombinedState< + { + configuration: QueryDefinition< + string, + BaseQueryFn, + never, + WellknownResponse, + 'wellknown', + unknown + >; + }, + never, + 'wellknown' + >; + }, + UnknownAction, + Tuple< + [ + StoreEnhancer<{ + dispatch: ThunkDispatch< + { + oidc: CombinedState< + { + authorizeFetch: MutationDefinition< + { + url: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + AuthorizeSuccessResponse, + 'oidc', + unknown + >; + par: MutationDefinition< + { + endpoint: string; + body: URLSearchParams; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + PushAuthorizationResponse, + 'oidc', + unknown + >; + authorizeIframe: MutationDefinition< + { + url: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + AuthorizationSuccess, + 'oidc', + unknown + >; + endSession: MutationDefinition< + { + idToken: string; + endpoint: string; + signOutRedirectUri?: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + null, + 'oidc', + unknown + >; + exchange: MutationDefinition< + { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + TokenExchangeResponse, + 'oidc', + unknown + >; + revoke: MutationDefinition< + { + accessToken: string; + clientId?: string; + endpoint: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + object, + 'oidc', + unknown + >; + userInfo: MutationDefinition< + { + accessToken: string; + endpoint: string; + }, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + UserInfoResponse, + 'oidc', + unknown + >; + }, + never, + 'oidc' + >; + wellknown: CombinedState< + { + configuration: QueryDefinition< + string, + BaseQueryFn< + string | FetchArgs, + unknown, + FetchBaseQueryError, + {}, + FetchBaseQueryMeta + >, + never, + WellknownResponse, + 'wellknown', + unknown + >; + }, + never, + 'wellknown' + >; + }, + { + requestMiddleware: RequestMiddleware[] | undefined; + logger: + | { + changeLevel: (level: LogLevel) => void; + error: (...args: LogMessage[]) => void; + warn: (...args: LogMessage[]) => void; + info: (...args: LogMessage[]) => void; + debug: (...args: LogMessage[]) => void; + } + | undefined; + }, + UnknownAction + >; + }>, + StoreEnhancer, + ] + > +>; + +export { CustomLogger }; + +export { GenericError }; + +export { GetAuthorizationUrlOptions }; + +// @public (undocumented) +export interface GetTokensOptions { + // (undocumented) + authorizeOptions?: GetAuthorizationUrlOptions; + // (undocumented) + backgroundRenew?: boolean; + // (undocumented) + forceRenew?: boolean; + // (undocumented) + storageOptions?: Partial; +} + +export { LogLevel }; + +// @public (undocumented) +export type LogoutErrorResult = { + error: string; + sessionResponse: GenericError | null; + revokeResponse: GenericError | null; + deleteResponse: GenericError | null; +}; + +// @public (undocumented) +export type LogoutSuccessResult = RevokeSuccessResult & { + sessionResponse: null; +}; + +// @public (undocumented) +export interface OauthTokens { + // (undocumented) + accessToken: string; + // (undocumented) + expiresAt?: number; + // (undocumented) + expiryTimestamp?: number; + // (undocumented) + idToken: string; + // (undocumented) + refreshToken?: string; +} + +// @public +export function oidc(input: { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + storage?: Partial; +}): Promise< + | { + error: string; + type: string; + subscribe?: undefined; + authorize?: undefined; + token?: undefined; + user?: undefined; + } + | { + subscribe: (listener: () => void) => Unsubscribe; + authorize: { + url: (options?: GetAuthorizationUrlOptions) => Promise; + background: ( + options?: GetAuthorizationUrlOptions, + ) => Promise; + }; + token: { + exchange: ( + code: string, + state: string, + options?: Partial, + ) => Promise; + get: ( + options?: GetTokensOptions, + ) => Promise; + revoke: () => Promise; + }; + user: { + info: () => Promise; + logout: () => Promise; + }; + error?: undefined; + type?: undefined; + } +>; + +// @public (undocumented) +export type OidcClient = Awaited>; + +export { OidcConfig }; + +// @public (undocumented) +export type OptionalAuthorizeOptions = Partial; + +// @public (undocumented) +export interface PushAuthorizationResponse { + // (undocumented) + expires_in: number; + // (undocumented) + request_uri: string; +} + +export { RequestMiddleware }; + +export { ResponseType_2 as ResponseType }; + +// @public (undocumented) +export type RevokeErrorResult = { + error: string; + revokeResponse: GenericError | null; + deleteResponse: GenericError | null; +}; + +// @public (undocumented) +export type RevokeSuccessResult = { + revokeResponse: null; + deleteResponse: null; +}; + +// @public (undocumented) +export type RootState = ReturnType; + +export { StorageConfig }; + +// @public (undocumented) +export interface TokenExchangeErrorResponse { + // (undocumented) + error: string; + // (undocumented) + message: string; + // (undocumented) + type: 'exchange_error' | 'network_error' | 'state_error' | 'unknown_error'; +} + +// @public (undocumented) +export interface TokenExchangeResponse { + // (undocumented) + access_token: string; + // (undocumented) + expires_in?: number; + // (undocumented) + id_token: string; + // (undocumented) + refresh_token?: string; + // (undocumented) + scope?: string; + // (undocumented) + token_type?: string; +} + +// @public (undocumented) +export interface TokenRequestOptions { + // (undocumented) + code: string; + // (undocumented) + config: OidcConfig; + // (undocumented) + endpoint: string; + // (undocumented) + verifier?: string; +} + +// @public (undocumented) +export type UserInfoResponse = { + sub: string; + [key: string]: unknown; +}; + +export { WellknownResponse }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/oidc-client/src/lib/authorize.request.micros.ts b/packages/oidc-client/src/lib/authorize.request.micros.ts index 8c31bd2cc9..43db85a0cf 100644 --- a/packages/oidc-client/src/lib/authorize.request.micros.ts +++ b/packages/oidc-client/src/lib/authorize.request.micros.ts @@ -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. @@ -17,8 +17,8 @@ import { hasPushRequestUri, isFetchBaseQueryError, toDispatchError, - type PromptValue, } from './authorize.request.utils.js'; +import type { AuthPromptValue } from '@forgerock/sdk-utilities'; import { oidcApi } from './oidc.api.js'; @@ -91,7 +91,7 @@ export const buildParBodyµ = ( parBodyOptions: OptionalAuthorizeOptions, challenge: string, state: string, - prompt?: PromptValue, + prompt?: AuthPromptValue, ): Micro.Micro => { return Micro.try({ try: () => @@ -216,7 +216,7 @@ export const buildParSlimUrlµ = ( authorizationEndpoint: string, clientId: string, requestUri: string, - prompt?: PromptValue, + prompt?: AuthPromptValue, ): Micro.Micro => { return Micro.try({ try: () => buildParAuthorizeUrl({ authorizationEndpoint, clientId, requestUri, prompt }), diff --git a/packages/oidc-client/src/lib/authorize.request.utils.test.ts b/packages/oidc-client/src/lib/authorize.request.utils.test.ts index f850406461..d267eb593f 100644 --- a/packages/oidc-client/src/lib/authorize.request.utils.test.ts +++ b/packages/oidc-client/src/lib/authorize.request.utils.test.ts @@ -1,12 +1,12 @@ /* - * 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. */ -import { it, expect } from '@effect/vitest'; +import { it } from '@effect/vitest'; import { Micro } from 'effect'; -import { vi, afterEach } from 'vitest'; +import { vi, afterEach, expect } from 'vitest'; import * as sdkOidc from '@forgerock/sdk-oidc'; import { createParAuthorizeUrlµ, authorizeµ } from './authorize.request.js'; import { diff --git a/packages/oidc-client/src/lib/authorize.request.utils.ts b/packages/oidc-client/src/lib/authorize.request.utils.ts index ae70d9874f..69e23383e3 100644 --- a/packages/oidc-client/src/lib/authorize.request.utils.ts +++ b/packages/oidc-client/src/lib/authorize.request.utils.ts @@ -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. @@ -7,16 +7,15 @@ import type { SerializedError } from '@reduxjs/toolkit'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import type { WellknownResponse, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +import type { AuthPromptValue } from '@forgerock/sdk-utilities'; import type { AuthorizationError, OptionalAuthorizeOptions } from './authorize.request.types.js'; import type { OidcConfig } from './config.types.js'; -export type PromptValue = 'none' | 'login' | 'consent'; - export type ParUrlParams = { authorizationEndpoint: string; clientId: string; requestUri: string; - prompt?: PromptValue; + prompt?: AuthPromptValue; }; export function isStringRecord(value: unknown): value is Record { diff --git a/packages/oidc-client/src/lib/client.store.test.ts b/packages/oidc-client/src/lib/client.store.test.ts index d15f8352ba..083b2e49f2 100644 --- a/packages/oidc-client/src/lib/client.store.test.ts +++ b/packages/oidc-client/src/lib/client.store.test.ts @@ -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. @@ -10,6 +10,7 @@ import { setupServer } from 'msw/node'; import { it, expect, describe, vi, beforeEach, afterEach, afterAll, beforeAll } from 'vitest'; import { oidc } from './client.store.js'; +import { makeOidcConfig } from '@forgerock/sdk-utilities'; import type { OidcConfig } from './config.types.js'; @@ -718,3 +719,82 @@ describe('authorize.url() with PAR enabled on non-pi.flow server', async () => { expect(parsed.searchParams.has('redirect_uri')).toBe(false); }); }); + +describe('unified JSON config entry', () => { + it('accepts unified JSON config and initializes successfully', async () => { + const unifiedConfig = { + oidc: { + clientId: '123456789', + discoveryEndpoint: 'https://api.example.com/wellknown', + scopes: ['openid', 'profile'], + redirectUri: 'https://example.com/callback.html', + }, + }; + + const client = await oidc({ + config: makeOidcConfig(unifiedConfig), + storage: customStorageConfig, + }); + expect(client).not.toHaveProperty('error'); + expect(client).toHaveProperty('authorize'); + expect(client).toHaveProperty('token'); + }); + + it('rejects Promise when unified JSON config has missing required fields', async () => { + const invalidConfig = { + oidc: { + // clientId missing + discoveryEndpoint: 'https://api.example.com/wellknown', + scopes: ['openid'], + redirectUri: 'https://example.com/callback.html', + }, + }; + + expect(() => makeOidcConfig(invalidConfig)).toThrow(/Invalid unified SDK config/); + }); + + it('rejects Promise when unified JSON config has wrong field type', async () => { + const invalidConfig = { + oidc: { + clientId: '123456789', + discoveryEndpoint: 'https://api.example.com/wellknown', + scopes: 'openid', // deliberately wrong type to test runtime validation + redirectUri: 'https://example.com/callback.html', + }, + }; + + expect(() => makeOidcConfig(invalidConfig)).toThrow(/Invalid unified SDK config/); + }); + + it('surfaces authorize params from unified JSON config in authorize URL', async () => { + const unifiedConfig = { + oidc: { + clientId: '123456789', + discoveryEndpoint: 'https://api.example.com/wellknown', + scopes: ['openid', 'profile'], + redirectUri: 'https://example.com/callback.html', + loginHint: 'user@example.com', + nonce: 'my-nonce', + acrValues: 'Level3', + additionalParameters: { max_age: '3600' }, + }, + }; + + const client = await oidc({ + config: makeOidcConfig(unifiedConfig), + storage: customStorageConfig, + }); + + if ('error' in client) throw new Error('Error creating OIDC client'); + + const url = await client.authorize.url(); + + if (typeof url !== 'string') expect.fail(`Expected string URL, got: ${JSON.stringify(url)}`); + + const parsed = new URL(url); + expect(parsed.searchParams.get('login_hint')).toBe('user@example.com'); + expect(parsed.searchParams.get('nonce')).toBe('my-nonce'); + expect(parsed.searchParams.get('acr_values')).toBe('Level3'); + expect(parsed.searchParams.get('max_age')).toBe('3600'); + }); +}); diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index 5a79f04ace..baec95d41e 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -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. @@ -62,7 +62,10 @@ export async function oidc({ }; storage?: Partial; }) { - const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); + const log = loggerFn({ + level: logger?.level ?? config.log ?? 'error', + custom: logger?.custom, + }); const oauthThreshold = config.oauthThreshold || 30 * 1000; // Default to 30 seconds const storageClient = createStorage({ type: storage?.type || 'localStorage', @@ -169,6 +172,13 @@ export async function oidc({ redirectUri: config.redirectUri, scope: config.scope || 'openid', responseType: config.responseType || 'code', + ...(config.loginHint !== undefined && { loginHint: config.loginHint }), + ...(config.nonce !== undefined && { nonce: config.nonce }), + ...(config.display !== undefined && { display: config.display }), + ...(config.prompt !== undefined && { prompt: config.prompt }), + ...(config.uiLocales !== undefined && { uiLocales: config.uiLocales }), + ...(config.acrValues !== undefined && { acrValues: config.acrValues }), + ...(config.query !== undefined && { query: config.query }), ...options, }; diff --git a/packages/oidc-client/src/lib/config.types.ts b/packages/oidc-client/src/lib/config.types.ts index 3f25a8fa39..c846a29292 100644 --- a/packages/oidc-client/src/lib/config.types.ts +++ b/packages/oidc-client/src/lib/config.types.ts @@ -1,23 +1,10 @@ /* - * 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. */ -import type { AsyncLegacyConfigOptions, ResponseType } from '@forgerock/sdk-types'; - -export interface OidcConfig extends AsyncLegacyConfigOptions { - // Redundant properties are redeclared to define as required - clientId: string; - redirectUri: string; - scope: string; - serverConfig: { - wellknown: string; - timeout?: number; - }; - responseType?: ResponseType; - par?: boolean; -} +export type { OidcConfig } from '@forgerock/sdk-types'; export interface OauthTokens { accessToken: string; diff --git a/packages/oidc-client/src/lib/logout.request.test.ts b/packages/oidc-client/src/lib/logout.request.test.ts index 09bc0af911..1beaf712f4 100644 --- a/packages/oidc-client/src/lib/logout.request.test.ts +++ b/packages/oidc-client/src/lib/logout.request.test.ts @@ -1,10 +1,10 @@ /* - * 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. */ -import { it, expect, describe } from '@effect/vitest'; +import { it, expect, describe } from 'vitest'; import { Micro } from 'effect'; import { deepStrictEqual } from 'node:assert'; import { setupServer } from 'msw/node'; @@ -99,41 +99,24 @@ const partialWellknown = { introspection_endpoint: 'https://example.com/introspect', }; -describe('Ping AM', () => { - it.effect('logoutµ succeeds with valid wellknown endpoints', () => - Micro.gen(function* () { - const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; - const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; - - const result = yield* logoutµ({ - tokens, - config, - wellknown: { - ...partialWellknown, - end_session_endpoint, - revocation_endpoint, - }, - store, - storageClient, - }); - - expect(result).toStrictEqual({ - sessionResponse: null, - revokeResponse: null, - deleteResponse: null, - }); - }), - ); - - it.effect('logoutµ fails on bad endSession', () => - Micro.gen(function* () { - const end_session_endpoint = 'https://example.com/am/oauth2/fake-realm/connect/endSession'; - const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; - - const result = yield* Micro.exit( - logoutµ({ +describe('signOutRedirectUri', () => { + it('logoutµ appends post_logout_redirect_uri when signOutRedirectUri is set in config', () => + Micro.runPromise( + Micro.gen(function* () { + const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; + const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; + let capturedUrl = ''; + + server.use( + http.get(end_session_endpoint, ({ request }) => { + capturedUrl = request.url; + return new HttpResponse(null, { status: 204 }); + }), + ); + + yield* logoutµ({ tokens, - config, + config: { ...config, signOutRedirectUri: 'https://example.com/logout' }, wellknown: { ...partialWellknown, end_session_endpoint, @@ -141,33 +124,28 @@ describe('Ping AM', () => { }, store, storageClient, - }), - ); - - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, - revokeResponse: null, - deleteResponse: null, - }), - ); - }), - ); - - it.effect('logoutµ fails on bad revoke', () => - Micro.gen(function* () { - const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; - const revocation_endpoint = 'https://example.com/am/oauth2/fake-realm/token/revoke'; - - const result = yield* Micro.exit( - logoutµ({ + }); + + const url = new URL(capturedUrl); + expect(url.searchParams.get('post_logout_redirect_uri')).toBe('https://example.com/logout'); + }), + )); + + it('logoutµ omits post_logout_redirect_uri when signOutRedirectUri is absent', () => + Micro.runPromise( + Micro.gen(function* () { + const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; + const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; + let capturedUrl = ''; + + server.use( + http.get(end_session_endpoint, ({ request }) => { + capturedUrl = request.url; + return new HttpResponse(null, { status: 204 }); + }), + ); + + yield* logoutµ({ tokens, config, wellknown: { @@ -177,100 +155,126 @@ describe('Ping AM', () => { }, store, storageClient, - }), - ); + }); - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: null, - revokeResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, - deleteResponse: null, - }), - ); - }), - ); + const url = new URL(capturedUrl); + expect(url.searchParams.has('post_logout_redirect_uri')).toBe(false); + }), + )); }); -describe('PingOne', () => { - const fakeEndSessionEndpoint = 'https://example.com/endSession'; +describe('Ping AM', () => { + it('logoutµ succeeds with valid wellknown endpoints', () => + Micro.runPromise( + Micro.gen(function* () { + const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; + const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; - it.effect('logoutµ succeeds with valid wellknown endpoints', () => - Micro.gen(function* () { - const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff'; - const revocation_endpoint = 'https://example.com/as/revoke'; - - const result = yield* logoutµ({ - tokens, - config, - wellknown: { - ...partialWellknown, - ping_end_idp_session_endpoint, - end_session_endpoint: fakeEndSessionEndpoint, - revocation_endpoint, - }, - store, - storageClient, - }); - - expect(result).toStrictEqual({ - sessionResponse: null, - revokeResponse: null, - deleteResponse: null, - }); - }), - ); - - it.effect('logoutµ fails on bad endSession', () => - Micro.gen(function* () { - const ping_end_idp_session_endpoint = 'https://example.com/as/badIdpSignoff'; - const revocation_endpoint = 'https://example.com/as/revoke'; - - const result = yield* Micro.exit( - logoutµ({ + const result = yield* logoutµ({ tokens, config, wellknown: { ...partialWellknown, - ping_end_idp_session_endpoint, - end_session_endpoint: fakeEndSessionEndpoint, + end_session_endpoint, revocation_endpoint, }, store, storageClient, - }), - ); - - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', - sessionResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, + }); + + expect(result).toStrictEqual({ + sessionResponse: null, revokeResponse: null, deleteResponse: null, - }), - ); - }), - ); - - it.effect('logoutµ fails on bad revoke', () => - Micro.gen(function* () { - const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff'; - const revocation_endpoint = 'https://example.com/as/badRevoke'; - - const result = yield* Micro.exit( - logoutµ({ + }); + }), + )); + + it('logoutµ fails on bad endSession', () => + Micro.runPromise( + Micro.gen(function* () { + const end_session_endpoint = 'https://example.com/am/oauth2/fake-realm/connect/endSession'; + const revocation_endpoint = 'https://example.com/am/oauth2/alpha/token/revoke'; + + const result = yield* Micro.exit( + logoutµ({ + tokens, + config, + wellknown: { + ...partialWellknown, + end_session_endpoint, + revocation_endpoint, + }, + store, + storageClient, + }), + ); + + deepStrictEqual( + result, + Micro.exitFail({ + error: 'Inner request error', + sessionResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + revokeResponse: null, + deleteResponse: null, + }), + ); + }), + )); + + it('logoutµ fails on bad revoke', () => + Micro.runPromise( + Micro.gen(function* () { + const end_session_endpoint = 'https://example.com/am/oauth2/alpha/connect/endSession'; + const revocation_endpoint = 'https://example.com/am/oauth2/fake-realm/token/revoke'; + + const result = yield* Micro.exit( + logoutµ({ + tokens, + config, + wellknown: { + ...partialWellknown, + end_session_endpoint, + revocation_endpoint, + }, + store, + storageClient, + }), + ); + + deepStrictEqual( + result, + Micro.exitFail({ + error: 'Inner request error', + sessionResponse: null, + revokeResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + deleteResponse: null, + }), + ); + }), + )); +}); + +describe('PingOne', () => { + const fakeEndSessionEndpoint = 'https://example.com/endSession'; + + it('logoutµ succeeds with valid wellknown endpoints', () => + Micro.runPromise( + Micro.gen(function* () { + const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff'; + const revocation_endpoint = 'https://example.com/as/revoke'; + + const result = yield* logoutµ({ tokens, config, wellknown: { @@ -281,23 +285,89 @@ describe('PingOne', () => { }, store, storageClient, - }), - ); + }); - deepStrictEqual( - result, - Micro.exitFail({ - error: 'Inner request error', + expect(result).toStrictEqual({ sessionResponse: null, - revokeResponse: { - error: 'End Session failure', - message: 'An error occurred while ending the session', - type: 'auth_error', - status: 400, - }, + revokeResponse: null, deleteResponse: null, - }), - ); - }), - ); + }); + }), + )); + + it('logoutµ fails on bad endSession', () => + Micro.runPromise( + Micro.gen(function* () { + const ping_end_idp_session_endpoint = 'https://example.com/as/badIdpSignoff'; + const revocation_endpoint = 'https://example.com/as/revoke'; + + const result = yield* Micro.exit( + logoutµ({ + tokens, + config, + wellknown: { + ...partialWellknown, + ping_end_idp_session_endpoint, + end_session_endpoint: fakeEndSessionEndpoint, + revocation_endpoint, + }, + store, + storageClient, + }), + ); + + deepStrictEqual( + result, + Micro.exitFail({ + error: 'Inner request error', + sessionResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + revokeResponse: null, + deleteResponse: null, + }), + ); + }), + )); + + it('logoutµ fails on bad revoke', () => + Micro.runPromise( + Micro.gen(function* () { + const ping_end_idp_session_endpoint = 'https://example.com/as/idpSignoff'; + const revocation_endpoint = 'https://example.com/as/badRevoke'; + + const result = yield* Micro.exit( + logoutµ({ + tokens, + config, + wellknown: { + ...partialWellknown, + ping_end_idp_session_endpoint, + end_session_endpoint: fakeEndSessionEndpoint, + revocation_endpoint, + }, + store, + storageClient, + }), + ); + + deepStrictEqual( + result, + Micro.exitFail({ + error: 'Inner request error', + sessionResponse: null, + revokeResponse: { + error: 'End Session failure', + message: 'An error occurred while ending the session', + type: 'auth_error', + status: 400, + }, + deleteResponse: null, + }), + ); + }), + )); }); diff --git a/packages/oidc-client/src/lib/logout.request.ts b/packages/oidc-client/src/lib/logout.request.ts index 92ee6869d6..636ab5fdd9 100644 --- a/packages/oidc-client/src/lib/logout.request.ts +++ b/packages/oidc-client/src/lib/logout.request.ts @@ -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. @@ -33,6 +33,7 @@ export function logoutµ({ oidcApi.endpoints.endSession.initiate({ idToken: tokens.idToken, endpoint: wellknown.ping_end_idp_session_endpoint || wellknown.end_session_endpoint, + signOutRedirectUri: config.signOutRedirectUri, }), ), ).pipe(Micro.map(({ data, error }) => createLogoutError(data, error))), diff --git a/packages/oidc-client/src/lib/oidc.api.ts b/packages/oidc-client/src/lib/oidc.api.ts index 1fee7fd373..1bb2f7ca7b 100644 --- a/packages/oidc-client/src/lib/oidc.api.ts +++ b/packages/oidc-client/src/lib/oidc.api.ts @@ -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. @@ -260,12 +260,17 @@ export const oidcApi = createApi({ return { data: response.data } as { data: AuthorizationSuccess }; }, }), - endSession: builder.mutation({ - queryFn: async ({ idToken, endpoint }, api, _, baseQuery) => { + endSession: builder.mutation< + null, + { idToken: string; endpoint: string; signOutRedirectUri?: string } + >({ + queryFn: async ({ idToken, endpoint, signOutRedirectUri }, api, _, baseQuery) => { const { requestMiddleware, logger } = api.extra as Extras; const url = new URL(endpoint); url.searchParams.append('id_token_hint', idToken); + if (signOutRedirectUri) + url.searchParams.append('post_logout_redirect_uri', signOutRedirectUri); const request: FetchArgs = { url: url.toString(), diff --git a/packages/sdk-effects/logger/package.json b/packages/sdk-effects/logger/package.json index 5a0b3d82c5..0d41eae8b1 100644 --- a/packages/sdk-effects/logger/package.json +++ b/packages/sdk-effects/logger/package.json @@ -31,7 +31,9 @@ "test": "pnpm nx nxTest", "test:watch": "pnpm nx nxTest --watch" }, - "dependencies": {}, + "dependencies": { + "@forgerock/sdk-types": "workspace:*" + }, "nx": { "tags": ["scope:sdk-effects"] } diff --git a/packages/sdk-effects/logger/src/lib/logger.types.ts b/packages/sdk-effects/logger/src/lib/logger.types.ts index 8bbc9ee95e..277aa40501 100644 --- a/packages/sdk-effects/logger/src/lib/logger.types.ts +++ b/packages/sdk-effects/logger/src/lib/logger.types.ts @@ -1,10 +1,12 @@ /* - * 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. */ +export type { LogLevel } from '@forgerock/sdk-types'; + export interface CustomLogger { error: (...args: LogMessage[]) => void; warn: (...args: LogMessage[]) => void; @@ -12,8 +14,4 @@ export interface CustomLogger { debug: (...args: LogMessage[]) => void; } -// Define log levels -export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'none'; - -// Define log message type export type LogMessage = string | number | object; diff --git a/packages/sdk-effects/logger/tsconfig.lib.json b/packages/sdk-effects/logger/tsconfig.lib.json index c0377d19ce..79c8dd7977 100644 --- a/packages/sdk-effects/logger/tsconfig.lib.json +++ b/packages/sdk-effects/logger/tsconfig.lib.json @@ -16,7 +16,7 @@ "types": ["node"] }, "include": ["src/**/*.ts"], - "references": [], + "references": [{ "path": "../../sdk-types/tsconfig.lib.json" }], "exclude": [ "vite.config.ts", "vite.config.mts", diff --git a/packages/sdk-effects/oidc/src/lib/authorize.test.ts b/packages/sdk-effects/oidc/src/lib/authorize.test.ts index 484e8bed25..437dc1f9af 100644 --- a/packages/sdk-effects/oidc/src/lib/authorize.test.ts +++ b/packages/sdk-effects/oidc/src/lib/authorize.test.ts @@ -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. @@ -184,5 +184,102 @@ describe('buildAuthorizeParams', () => { expect(params.has('response_mode')).toBe(false); expect(params.has('prompt')).toBe(false); + expect(params.has('login_hint')).toBe(false); + expect(params.has('nonce')).toBe(false); + expect(params.has('display')).toBe(false); + expect(params.has('ui_locales')).toBe(false); + expect(params.has('acr_values')).toBe(false); + }); + + it('includes login_hint when provided', () => { + const params = buildAuthorizeParams({ + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: 'openid', + responseType: 'code', + challenge: 'abc123', + state: 'state1', + loginHint: 'user@example.com', + }); + + expect(params.get('login_hint')).toBe('user@example.com'); + }); + + it('includes nonce when provided', () => { + const params = buildAuthorizeParams({ + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: 'openid', + responseType: 'code', + challenge: 'abc123', + state: 'state1', + nonce: 'custom-nonce-value', + }); + + expect(params.get('nonce')).toBe('custom-nonce-value'); + }); + + it('includes display when provided', () => { + const params = buildAuthorizeParams({ + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: 'openid', + responseType: 'code', + challenge: 'abc123', + state: 'state1', + display: 'popup', + }); + + expect(params.get('display')).toBe('popup'); + }); + + it('includes ui_locales when provided', () => { + const params = buildAuthorizeParams({ + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: 'openid', + responseType: 'code', + challenge: 'abc123', + state: 'state1', + uiLocales: 'en-US', + }); + + expect(params.get('ui_locales')).toBe('en-US'); + }); + + it('includes acr_values when provided', () => { + const params = buildAuthorizeParams({ + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: 'openid', + responseType: 'code', + challenge: 'abc123', + state: 'state1', + acrValues: 'Level3', + }); + + expect(params.get('acr_values')).toBe('Level3'); + }); + + it('includes all new OIDC params together', () => { + const params = buildAuthorizeParams({ + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: 'openid', + responseType: 'code', + challenge: 'abc123', + state: 'state1', + loginHint: 'user@example.com', + nonce: 'my-nonce', + display: 'page', + uiLocales: 'fr-FR', + acrValues: 'Level2', + }); + + expect(params.get('login_hint')).toBe('user@example.com'); + expect(params.get('nonce')).toBe('my-nonce'); + expect(params.get('display')).toBe('page'); + expect(params.get('ui_locales')).toBe('fr-FR'); + expect(params.get('acr_values')).toBe('Level2'); }); }); diff --git a/packages/sdk-effects/oidc/src/lib/authorize.utils.ts b/packages/sdk-effects/oidc/src/lib/authorize.utils.ts index 27e41bf14d..204749334d 100644 --- a/packages/sdk-effects/oidc/src/lib/authorize.utils.ts +++ b/packages/sdk-effects/oidc/src/lib/authorize.utils.ts @@ -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. @@ -29,6 +29,11 @@ export function buildAuthorizeParams( if (options.responseMode) params.set('response_mode', options.responseMode); if (options.prompt) params.set('prompt', options.prompt); + if (options.loginHint) params.set('login_hint', options.loginHint); + if (options.nonce) params.set('nonce', options.nonce); + if (options.display) params.set('display', options.display); + if (options.uiLocales) params.set('ui_locales', options.uiLocales); + if (options.acrValues) params.set('acr_values', options.acrValues); return params; } diff --git a/packages/sdk-types/src/lib/authorize.types.ts b/packages/sdk-types/src/lib/authorize.types.ts index f4815d2e73..014273d67f 100644 --- a/packages/sdk-types/src/lib/authorize.types.ts +++ b/packages/sdk-types/src/lib/authorize.types.ts @@ -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. @@ -8,6 +8,15 @@ import type { LegacyConfigOptions } from './legacy-config.types.js'; export type ResponseType = 'code' | 'token'; +// Canonical runtime lists of the OIDC `display`/`prompt` values; the union types are +// derived from them so the allowed values live in one place — runtime validators can +// iterate the arrays while the types stay in sync. Colocated as in `am-callback.types.ts`. +export const AUTH_DISPLAY_VALUES = ['page', 'popup', 'touch', 'wap'] as const; +export const AUTH_PROMPT_VALUES = ['none', 'login', 'consent', 'select_account'] as const; + +export type AuthDisplayValue = (typeof AUTH_DISPLAY_VALUES)[number]; +export type AuthPromptValue = (typeof AUTH_PROMPT_VALUES)[number]; + /** * Options for the authorization URL * @param clientId The client ID of the application @@ -29,7 +38,12 @@ export interface GetAuthorizationUrlOptions extends LegacyConfigOptions { state?: string; verifier?: string; query?: Record; - prompt?: 'none' | 'login' | 'consent'; + prompt?: AuthPromptValue; + loginHint?: string; + nonce?: string; + display?: AuthDisplayValue; + uiLocales?: string; + acrValues?: string; successParams?: string[]; errorParams?: string[]; } diff --git a/packages/sdk-types/src/lib/config.types.ts b/packages/sdk-types/src/lib/config.types.ts index 4a4a3f63cf..3752b970b4 100644 --- a/packages/sdk-types/src/lib/config.types.ts +++ b/packages/sdk-types/src/lib/config.types.ts @@ -1,10 +1,66 @@ /* - * 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. */ import { CustomStorageObject } from './tokens.types.js'; +import type { AsyncLegacyConfigOptions } from './legacy-config.types.js'; +import type { ResponseType, AuthDisplayValue, AuthPromptValue } from './authorize.types.js'; + +export const LOG_LEVEL_VALUES = ['none', 'error', 'warn', 'info', 'debug'] as const; +export type LogLevel = (typeof LOG_LEVEL_VALUES)[number]; + +export const LOG_LEVEL_UPPERCASE_VALUES = LOG_LEVEL_VALUES.map((v) => + v.toUpperCase(), +) as readonly Uppercase[]; + +/** Configuration for creating an OIDC client instance. */ +export interface OidcConfig extends AsyncLegacyConfigOptions { + clientId: string; + redirectUri: string; + scope: string; + serverConfig: { + wellknown: string; + timeout?: number; + }; + responseType?: ResponseType; + /** Use Pushed Authorization Requests (PAR) for the authorization flow. */ + par?: boolean; + /** URI to redirect to after logout; maps to `post_logout_redirect_uri` in the end-session request. */ + signOutRedirectUri?: string; + loginHint?: string; + nonce?: string; + display?: AuthDisplayValue; + prompt?: AuthPromptValue; + uiLocales?: string; + acrValues?: string; + query?: Record; + log?: LogLevel; +} + +export interface JourneyServerConfig { + wellknown: string; + timeout?: number; +} + +/** + * Configuration for creating a journey client instance. + * + * Extends {@link AsyncLegacyConfigOptions} so that the same config object can + * be shared across journey-client, davinci-client, and oidc-client. Properties + * like `clientId`, `scope`, and `redirectUri` are accepted but not used by + * journey-client — a warning is logged when they are provided. + */ +export interface JourneyClientConfig extends AsyncLegacyConfigOptions { + serverConfig: JourneyServerConfig; + log?: LogLevel; +} + +export interface DaVinciConfig extends AsyncLegacyConfigOptions { + responseType?: string; + log?: LogLevel; +} /** * Union of possible OAuth Configs diff --git a/packages/sdk-utilities/package.json b/packages/sdk-utilities/package.json index 0f37be6be9..d9f98ddc52 100644 --- a/packages/sdk-utilities/package.json +++ b/packages/sdk-utilities/package.json @@ -40,7 +40,8 @@ "test:watch": "pnpm nx nxTest --watch" }, "dependencies": { - "@forgerock/sdk-types": "workspace:*" + "@forgerock/sdk-types": "workspace:*", + "effect": "catalog:effect" }, "nx": { "tags": ["scope:sdk-utilities"] diff --git a/packages/sdk-utilities/src/index.ts b/packages/sdk-utilities/src/index.ts index f01a5f77b8..ed3a08e0a1 100644 --- a/packages/sdk-utilities/src/index.ts +++ b/packages/sdk-utilities/src/index.ts @@ -1,6 +1,6 @@ /* * - * Copyright © 2025 Ping Identity Corporation. All right reserved. + * Copyright © 2025 - 2026 Ping Identity Corporation. All right reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -14,3 +14,4 @@ export * from './lib/url/index.js'; export * from './lib/wellknown/index.js'; export * from './lib/object.utils.js'; export * from './lib/constants/index.js'; +export * from './lib/config/index.js'; diff --git a/packages/sdk-utilities/src/lib/config/config.effects.ts b/packages/sdk-utilities/src/lib/config/config.effects.ts new file mode 100644 index 0000000000..76defcf49a --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.effects.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 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 * as Either from 'effect/Either'; + +import { parseToOidcConfig, parseToJourneyConfig, parseToDavinciConfig } from './config.utils.js'; +import type { OidcConfig, JourneyClientConfig, DaVinciConfig } from './config.types.js'; + +function throwOnLeft(result: Either.Either): T { + if (Either.isLeft(result)) { + const messages = result.left.map((e) => `${e.field}: ${e.message}`).join(', '); + throw new Error(`Invalid unified SDK config: ${messages}`); + } + return result.right; +} + +export const makeOidcConfig = (json: unknown): OidcConfig => throwOnLeft(parseToOidcConfig(json)); + +export const makeJourneyConfig = (json: unknown): JourneyClientConfig => + throwOnLeft(parseToJourneyConfig(json)); + +export const makeDavinciConfig = (json: unknown): DaVinciConfig => + throwOnLeft(parseToDavinciConfig(json)); diff --git a/packages/sdk-utilities/src/lib/config/config.test.ts b/packages/sdk-utilities/src/lib/config/config.test.ts new file mode 100644 index 0000000000..158ffdd70c --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.test.ts @@ -0,0 +1,717 @@ +/* + * Copyright (c) 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 { describe, it, expect } from 'vitest'; +import * as Either from 'effect/Either'; +import { + parseToOidcConfig, + parseToJourneyConfig, + parseToDavinciConfig, + parseRefreshThreshold, + parseDisplay, + parsePrompt, + parseLog, + parseScopes, + parseDiscoveryEndpoint, + parseServerUrl, + parseTimeout, + collectErrors, + parseOidcSection, + parseUnifiedSdkConfig, +} from './config.utils.js'; +import { makeOidcConfig, makeJourneyConfig, makeDavinciConfig } from './config.effects.js'; + +const minimalOidc = { + clientId: 'my-client', + discoveryEndpoint: 'https://example.com/.well-known/openid-configuration', + scopes: ['openid', 'profile'], + redirectUri: 'https://app.example.com/callback', +}; + +const fullConfig = { + timeout: 30000, + log: 'DEBUG', + journey: { + serverUrl: 'https://example.com/am', + realm: 'alpha', + cookieName: 'iPlanetDirectoryPro', + }, + oidc: { + ...minimalOidc, + signOutRedirectUri: 'https://app.example.com/logout', + refreshThreshold: 60, + loginHint: 'user@example.com', + nonce: 'custom-nonce', + display: 'page', + prompt: 'login', + uiLocales: 'en-US', + acrValues: 'Level3', + additionalParameters: { max_age: '3600' }, + openId: { deviceAuthorizationEndpoint: 'https://example.com/device/code' }, + }, +}; + +const journeyOnlyConfig = { + journey: { + serverUrl: 'https://example.com/am', + realm: 'alpha', + }, + oidc: { + discoveryEndpoint: 'https://example.com/.well-known/openid-configuration', + }, +}; + +describe('parseUnifiedSdkConfig', () => { + it('parseUnifiedSdkConfig_ValidFullConfig_ReturnsSuccess', () => { + expect(Either.isRight(parseUnifiedSdkConfig(fullConfig))).toBe(true); + }); + + it('parseUnifiedSdkConfig_JourneyOnlyConfig_ReturnsSuccess', () => { + expect(Either.isRight(parseUnifiedSdkConfig(journeyOnlyConfig))).toBe(true); + }); + + it('parseUnifiedSdkConfig_NoOidcOrJourneySection_ReturnsSuccess', () => { + expect(Either.isRight(parseUnifiedSdkConfig({ timeout: 5000 }))).toBe(true); + }); + + it('parseUnifiedSdkConfig_UnknownTopLevelField_Ignored', () => { + expect(Either.isRight(parseUnifiedSdkConfig({ timeout: 5000, surprise: 'kept' }))).toBe(true); + }); + + it('parseUnifiedSdkConfig_TimeoutNotNumber_ReturnsTypeError', () => { + const errors = Either.getOrThrow( + Either.flip(parseUnifiedSdkConfig({ ...fullConfig, timeout: 'thirty' })), + ); + expect(errors.some((e) => e.field === 'timeout')).toBe(true); + }); + + it('parseUnifiedSdkConfig_JourneyMissingServerUrl_ReturnsError', () => { + const errors = Either.getOrThrow( + Either.flip( + parseUnifiedSdkConfig({ + journey: { realm: 'alpha' }, + oidc: { discoveryEndpoint: 'https://example.com/.well-known/openid-configuration' }, + }), + ), + ); + expect(errors.some((e) => e.field === 'journey.serverUrl')).toBe(true); + }); + + it('parseUnifiedSdkConfig_InvalidOidcNested_PropagatesErrors', () => { + const errors = Either.getOrThrow( + Either.flip(parseUnifiedSdkConfig({ ...fullConfig, oidc: { ...minimalOidc, clientId: 42 } })), + ); + expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); + }); + + it('parseUnifiedSdkConfig_MultipleErrors_AllAccumulated', () => { + const errors = Either.getOrThrow( + Either.flip(parseUnifiedSdkConfig({ timeout: 'thirty', oidc: { scopes: 'not-an-array' } })), + ); + expect(errors.length).toBeGreaterThanOrEqual(2); + expect(errors.some((e) => e.field === 'timeout')).toBe(true); + expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); + }); +}); + +describe('parseServerUrl', () => { + it('parseServerUrl_String_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseServerUrl('https://example.com/am'))).toBe( + 'https://example.com/am', + ); + }); + + it('parseServerUrl_Absent_ReturnsRequiredError', () => { + const errors = Either.getOrThrow(Either.flip(parseServerUrl(undefined))); + expect(errors[0]?.field).toBe('journey.serverUrl'); + }); + + it('parseServerUrl_WrongType_ReturnsTypeError', () => { + const errors = Either.getOrThrow(Either.flip(parseServerUrl(123))); + expect(errors[0]?.field).toBe('journey.serverUrl'); + }); +}); + +describe('parseTimeout', () => { + it('parseTimeout_FiniteNonNegative_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseTimeout(5000))).toBe(5000); + }); + + it('parseTimeout_Absent_ReturnsUndefined', () => { + expect(Either.getOrThrow(parseTimeout(undefined))).toBeUndefined(); + }); + + it('parseTimeout_Negative_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseTimeout(-5))); + expect(errors[0]?.field).toBe('timeout'); + }); + + it('parseTimeout_NotNumber_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseTimeout('thirty'))); + expect(errors[0]?.field).toBe('timeout'); + }); +}); + +describe('parseToOidcConfig', () => { + it('parseToOidcConfig_NoOidcBlock_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip(parseToOidcConfig({ journey: { serverUrl: 'https://example.com/am' } })), + ); + expect(errors.some((e) => e.field === 'oidc')).toBe(true); + }); + + it('parseToOidcConfig_MinimalConfig_MapsRequiredFields', () => { + const data = Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })); + expect(data.clientId).toBe('my-client'); + expect(data.redirectUri).toBe('https://app.example.com/callback'); + expect(data.scope).toBe('openid profile'); + expect(data.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('parseToOidcConfig_ScopesJoinedWithSpace', () => { + const data = Either.getOrThrow( + parseToOidcConfig({ oidc: { ...minimalOidc, scopes: ['openid', 'email'] } }), + ); + expect(data.scope).toBe('openid email'); + }); + + it('parseToOidcConfig_RefreshThresholdConvertedToMs', () => { + const data = Either.getOrThrow( + parseToOidcConfig({ oidc: { ...minimalOidc, refreshThreshold: 60 } }), + ); + expect(data.oauthThreshold).toBe(60000); + }); + + it('parseToOidcConfig_NoRefreshThreshold_OauthThresholdAbsent', () => { + expect( + Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).oauthThreshold, + ).toBeUndefined(); + }); + + it('parseToOidcConfig_RealmMappedToRealmPath', () => { + const data = Either.getOrThrow( + parseToOidcConfig({ + journey: { serverUrl: 'https://example.com/am', realm: 'alpha' }, + oidc: minimalOidc, + }), + ); + expect(data.realmPath).toBe('alpha'); + }); + + it('parseToOidcConfig_NoRealm_RealmPathAbsent', () => { + expect(Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).realmPath).toBeUndefined(); + }); + + it('parseToOidcConfig_TimeoutPassedToServerConfig', () => { + const data = Either.getOrThrow(parseToOidcConfig({ timeout: 5000, oidc: minimalOidc })); + expect(data.serverConfig.timeout).toBe(5000); + }); + + it('parseToOidcConfig_NoTimeout_TimeoutAbsentInServerConfig', () => { + expect( + Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).serverConfig.timeout, + ).toBeUndefined(); + }); + + it('parseToOidcConfig_AuthorizeParamsMapped', () => { + const data = Either.getOrThrow( + parseToOidcConfig({ + oidc: { + ...minimalOidc, + loginHint: 'user@example.com', + nonce: 'custom-nonce', + display: 'page', + prompt: 'login', + uiLocales: 'en-US', + acrValues: 'Level3', + additionalParameters: { max_age: '3600' }, + }, + }), + ); + expect(data.loginHint).toBe('user@example.com'); + expect(data.nonce).toBe('custom-nonce'); + expect(data.display).toBe('page'); + expect(data.prompt).toBe('login'); + expect(data.uiLocales).toBe('en-US'); + expect(data.acrValues).toBe('Level3'); + expect(data.query).toEqual({ max_age: '3600' }); + }); + + it('parseToOidcConfig_EmptyScopes_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip(parseToOidcConfig({ oidc: { ...minimalOidc, scopes: [] } })), + ); + expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); + }); + + it('parseToOidcConfig_NoAuthorizeParams_AllAbsent', () => { + const data = Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })); + expect(data.loginHint).toBeUndefined(); + expect(data.nonce).toBeUndefined(); + expect(data.query).toBeUndefined(); + }); + + it('parseToOidcConfig_OidcMissingDiscoveryEndpoint_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip( + parseToOidcConfig({ oidc: { clientId: 'x', redirectUri: 'x', scopes: ['openid'] } }), + ), + ); + expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); + }); + + it('parseToOidcConfig_NullInput_ReturnsFailure', () => { + expect(Either.isLeft(parseToOidcConfig(null))).toBe(true); + }); +}); + +describe('parseToJourneyConfig', () => { + it('parseToJourneyConfig_NoOidcBlock_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip(parseToJourneyConfig({ journey: { serverUrl: 'https://example.com/am' } })), + ); + expect(errors.some((e) => e.field === 'oidc')).toBe(true); + }); + + it('parseToJourneyConfig_MinimalConfig_MapsWellknown', () => { + const data = Either.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })); + expect(data.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('parseToJourneyConfig_JourneyOnlyConfig_MapsWellknown', () => { + const data = Either.getOrThrow(parseToJourneyConfig(journeyOnlyConfig)); + expect(data.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('parseToJourneyConfig_RealmMappedToRealmPath', () => { + const data = Either.getOrThrow( + parseToJourneyConfig({ + journey: { serverUrl: 'https://example.com/am', realm: 'beta' }, + oidc: minimalOidc, + }), + ); + expect(data.realmPath).toBe('beta'); + }); + + it('parseToJourneyConfig_NoRealm_RealmPathAbsent', () => { + expect( + Either.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })).realmPath, + ).toBeUndefined(); + }); + + it('parseToJourneyConfig_TimeoutPassedToServerConfig', () => { + const data = Either.getOrThrow(parseToJourneyConfig({ timeout: 10000, oidc: minimalOidc })); + expect(data.serverConfig.timeout).toBe(10000); + }); + + it('parseToJourneyConfig_OidcFieldsNotLeakedToResult', () => { + const data = Either.getOrThrow(parseToJourneyConfig(fullConfig)) as unknown as Record< + string, + unknown + >; + expect(data['clientId']).toBeUndefined(); + expect(data['scope']).toBeUndefined(); + expect(data['redirectUri']).toBeUndefined(); + }); + + it('parseToJourneyConfig_OidcMissingDiscoveryEndpoint_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip(parseToJourneyConfig({ oidc: { realm: 'alpha' } })), + ); + expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); + }); + + it('parseToJourneyConfig_NullInput_ReturnsFailure', () => { + expect(Either.isLeft(parseToJourneyConfig(null))).toBe(true); + }); +}); + +describe('parseToDavinciConfig', () => { + it('parseToDavinciConfig_NoOidcBlock_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip(parseToDavinciConfig({ journey: { serverUrl: 'https://example.com/am' } })), + ); + expect(errors.some((e) => e.field === 'oidc')).toBe(true); + }); + + it('parseToDavinciConfig_MinimalConfig_MapsRequiredFields', () => { + const data = Either.getOrThrow(parseToDavinciConfig({ oidc: minimalOidc })); + expect(data.clientId).toBe('my-client'); + expect(data.redirectUri).toBe('https://app.example.com/callback'); + expect(data.scope).toBe('openid profile'); + expect(data.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('parseToDavinciConfig_ScopesJoinedWithSpace', () => { + const data = Either.getOrThrow( + parseToDavinciConfig({ oidc: { ...minimalOidc, scopes: ['openid', 'email'] } }), + ); + expect(data.scope).toBe('openid email'); + }); + + it('parseToDavinciConfig_RefreshThresholdConvertedToMs', () => { + const data = Either.getOrThrow( + parseToDavinciConfig({ oidc: { ...minimalOidc, refreshThreshold: 30 } }), + ); + expect(data.oauthThreshold).toBe(30000); + }); + + it('parseToDavinciConfig_RealmMappedToRealmPath', () => { + const data = Either.getOrThrow( + parseToDavinciConfig({ + journey: { serverUrl: 'https://example.com/am', realm: 'alpha' }, + oidc: minimalOidc, + }), + ); + expect(data.realmPath).toBe('alpha'); + }); + + it('parseToDavinciConfig_EmptyScopes_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip(parseToDavinciConfig({ oidc: { ...minimalOidc, scopes: [] } })), + ); + expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); + }); + + it('parseToDavinciConfig_TimeoutPassedToServerConfig', () => { + const data = Either.getOrThrow(parseToDavinciConfig({ timeout: 7000, oidc: minimalOidc })); + expect(data.serverConfig.timeout).toBe(7000); + }); + + it('parseToDavinciConfig_OidcMissingDiscoveryEndpoint_ReturnsFailure', () => { + const errors = Either.getOrThrow( + Either.flip( + parseToDavinciConfig({ oidc: { clientId: 'x', redirectUri: 'x', scopes: ['openid'] } }), + ), + ); + expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); + }); + + it('parseToDavinciConfig_NullInput_ReturnsFailure', () => { + expect(Either.isLeft(parseToDavinciConfig(null))).toBe(true); + }); +}); + +describe('parseToOidcConfig log mapping', () => { + it('parseToOidcConfig_LogFieldMapped_ToLogLevel', () => { + expect(Either.getOrThrow(parseToOidcConfig({ log: 'DEBUG', oidc: minimalOidc })).log).toBe( + 'debug', + ); + }); + + it('parseToOidcConfig_NoLogField_LogLevelAbsent', () => { + expect(Either.getOrThrow(parseToOidcConfig({ oidc: minimalOidc })).log).toBeUndefined(); + }); + + it('parseToOidcConfig_CookieName_NotMappedToResult', () => { + const data = Either.getOrThrow( + parseToOidcConfig({ + journey: { serverUrl: 'https://example.com/am', cookieName: 'iPlanetDirectoryPro' }, + oidc: minimalOidc, + }), + ) as unknown as Record; + expect(data['cookieName']).toBeUndefined(); + }); +}); + +describe('parseToJourneyConfig log mapping', () => { + it('parseToJourneyConfig_LogFieldMapped_ToLogLevel', () => { + expect(Either.getOrThrow(parseToJourneyConfig({ log: 'WARN', oidc: minimalOidc })).log).toBe( + 'warn', + ); + }); + + it('parseToJourneyConfig_NoLogField_LogLevelAbsent', () => { + expect(Either.getOrThrow(parseToJourneyConfig({ oidc: minimalOidc })).log).toBeUndefined(); + }); + + it('parseToJourneyConfig_CookieName_NotMappedToResult', () => { + const data = Either.getOrThrow( + parseToJourneyConfig({ + journey: { serverUrl: 'https://example.com/am', cookieName: 'iPlanetDirectoryPro' }, + oidc: minimalOidc, + }), + ) as unknown as Record; + expect(data['cookieName']).toBeUndefined(); + }); +}); + +describe('parseToDavinciConfig log mapping', () => { + it('parseToDavinciConfig_LogFieldMapped_ToLogLevel', () => { + expect(Either.getOrThrow(parseToDavinciConfig({ log: 'ERROR', oidc: minimalOidc })).log).toBe( + 'error', + ); + }); + + it('parseToDavinciConfig_NoLogField_LogLevelAbsent', () => { + expect(Either.getOrThrow(parseToDavinciConfig({ oidc: minimalOidc })).log).toBeUndefined(); + }); +}); + +describe('makeOidcConfig', () => { + it('makeOidcConfig_ValidFullConfig_ReturnsMappedOidcConfig', () => { + const result = makeOidcConfig(fullConfig); + expect(result.clientId).toBe('my-client'); + expect(result.redirectUri).toBe('https://app.example.com/callback'); + expect(result.scope).toBe('openid profile'); + expect(result.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('makeOidcConfig_NullInput_Throws', () => { + expect(() => makeOidcConfig(null)).toThrow('Invalid unified SDK config'); + }); + + it('makeOidcConfig_MissingDiscoveryEndpoint_Throws', () => { + expect(() => + makeOidcConfig({ oidc: { clientId: 'x', scopes: ['openid'], redirectUri: 'x' } }), + ).toThrow('Invalid unified SDK config'); + }); + + it('makeOidcConfig_EmptyDiscoveryEndpoint_Throws', () => { + expect(() => makeOidcConfig({ oidc: { ...minimalOidc, discoveryEndpoint: '' } })).toThrow( + 'Invalid unified SDK config', + ); + }); + + it('makeOidcConfig_EmptyScopes_Throws', () => { + expect(() => makeOidcConfig({ oidc: { ...minimalOidc, scopes: [] } })).toThrow( + 'Invalid unified SDK config', + ); + }); + + it('makeOidcConfig_MissingClientId_Throws', () => { + expect(() => + makeOidcConfig({ + oidc: { + discoveryEndpoint: 'https://example.com/.well-known', + scopes: ['openid'], + redirectUri: 'x', + }, + }), + ).toThrow('Invalid unified SDK config'); + }); +}); + +describe('makeJourneyConfig', () => { + it('makeJourneyConfig_ValidConfig_ReturnsMappedJourneyConfig', () => { + const result = makeJourneyConfig(fullConfig); + expect(result.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + expect(result.realmPath).toBe('alpha'); + }); + + it('makeJourneyConfig_NullInput_Throws', () => { + expect(() => makeJourneyConfig(null)).toThrow('Invalid unified SDK config'); + }); + + it('makeJourneyConfig_MissingOidcBlock_Throws', () => { + expect(() => makeJourneyConfig({ journey: { serverUrl: 'https://example.com/am' } })).toThrow( + 'Invalid unified SDK config', + ); + }); +}); + +describe('makeDavinciConfig', () => { + it('makeDavinciConfig_ValidFullConfig_ReturnsMappedDavinciConfig', () => { + const result = makeDavinciConfig(fullConfig); + expect(result.clientId).toBe('my-client'); + expect(result.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('makeDavinciConfig_NullInput_Throws', () => { + expect(() => makeDavinciConfig(null)).toThrow('Invalid unified SDK config'); + }); + + it('makeDavinciConfig_MissingDiscoveryEndpoint_Throws', () => { + expect(() => + makeDavinciConfig({ oidc: { clientId: 'x', scopes: ['openid'], redirectUri: 'x' } }), + ).toThrow('Invalid unified SDK config'); + }); + + it('makeDavinciConfig_EmptyScopes_Throws', () => { + expect(() => makeDavinciConfig({ oidc: { ...minimalOidc, scopes: [] } })).toThrow( + 'Invalid unified SDK config', + ); + }); + + it('makeDavinciConfig_MissingClientId_Throws', () => { + expect(() => + makeDavinciConfig({ + oidc: { + discoveryEndpoint: 'https://example.com/.well-known', + scopes: ['openid'], + redirectUri: 'x', + }, + }), + ).toThrow('Invalid unified SDK config'); + }); +}); + +describe('parseRefreshThreshold', () => { + it('parseRefreshThreshold_FiniteNonNegative_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseRefreshThreshold(60))).toBe(60); + }); + + it('parseRefreshThreshold_Zero_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseRefreshThreshold(0))).toBe(0); + }); + + it('parseRefreshThreshold_Absent_ReturnsUndefined', () => { + expect(Either.getOrThrow(parseRefreshThreshold(undefined))).toBeUndefined(); + }); + + it('parseRefreshThreshold_NaN_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseRefreshThreshold(NaN))); + expect(errors[0]?.field).toBe('oidc.refreshThreshold'); + }); + + it('parseRefreshThreshold_Infinity_ReturnsError', () => { + expect(Either.isLeft(parseRefreshThreshold(Infinity))).toBe(true); + }); + + it('parseRefreshThreshold_Negative_ReturnsError', () => { + expect(Either.isLeft(parseRefreshThreshold(-1))).toBe(true); + }); + + it('parseRefreshThreshold_NotNumber_ReturnsError', () => { + expect(Either.isLeft(parseRefreshThreshold('sixty'))).toBe(true); + }); +}); + +describe('parseDisplay', () => { + it('parseDisplay_ValidMember_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseDisplay('page'))).toBe('page'); + }); + + it('parseDisplay_Absent_ReturnsUndefined', () => { + expect(Either.getOrThrow(parseDisplay(undefined))).toBeUndefined(); + }); + + it('parseDisplay_InvalidMember_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseDisplay('fullscreen'))); + expect(errors[0]?.field).toBe('oidc.display'); + }); +}); + +describe('parsePrompt', () => { + it('parsePrompt_ValidMember_ReturnsSuccess', () => { + expect(Either.getOrThrow(parsePrompt('login'))).toBe('login'); + }); + + it('parsePrompt_InvalidMember_ReturnsError', () => { + expect(Either.isLeft(parsePrompt('always'))).toBe(true); + }); +}); + +describe('parseLog', () => { + it('parseLog_ValidUppercase_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseLog('DEBUG'))).toBe('DEBUG'); + }); + + it('parseLog_Lowercase_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseLog('debug'))); + expect(errors[0]?.field).toBe('log'); + }); + + it('parseLog_UnknownValue_ReturnsError', () => { + expect(Either.isLeft(parseLog('VERBOSE'))).toBe(true); + }); +}); + +describe('parseScopes', () => { + it('parseScopes_AllStrings_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseScopes(['openid', 'profile']))).toEqual(['openid', 'profile']); + }); + + it('parseScopes_NonArray_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseScopes('openid profile'))); + expect(errors[0]?.field).toBe('oidc.scopes'); + }); + + it('parseScopes_ContainsNonString_ReturnsIndexedError', () => { + const errors = Either.getOrThrow(Either.flip(parseScopes(['openid', 42]))); + expect(errors.some((e) => e.field === 'oidc.scopes[1]')).toBe(true); + }); +}); + +describe('parseDiscoveryEndpoint', () => { + it('parseDiscoveryEndpoint_String_ReturnsSuccess', () => { + expect(Either.getOrThrow(parseDiscoveryEndpoint('https://example.com'))).toBe( + 'https://example.com', + ); + }); + + it('parseDiscoveryEndpoint_Absent_ReturnsRequiredError', () => { + const errors = Either.getOrThrow(Either.flip(parseDiscoveryEndpoint(undefined))); + expect(errors[0]?.field).toBe('oidc.discoveryEndpoint'); + }); + + it('parseDiscoveryEndpoint_EmptyString_ReturnsRequiredError', () => { + const errors = Either.getOrThrow(Either.flip(parseDiscoveryEndpoint(''))); + expect(errors[0]?.field).toBe('oidc.discoveryEndpoint'); + }); +}); + +describe('collectErrors', () => { + it('collectErrors_AllRight_ReturnsEmpty', () => { + expect(collectErrors([Either.right(1), Either.right('a')])).toEqual([]); + }); + + it('collectErrors_MultipleLeft_AccumulatesAllErrors', () => { + const errors = collectErrors([ + Either.right(1), + Either.left([{ field: 'a', message: 'bad a' }]), + Either.left([{ field: 'b', message: 'bad b' }]), + ]); + expect(errors.map((e) => e.field)).toEqual(['a', 'b']); + }); + + it('collectErrors_DoesNotShortCircuit', () => { + const errors = collectErrors([ + Either.left([{ field: 'first', message: 'x' }]), + Either.left([{ field: 'second', message: 'y' }]), + ]); + expect(errors).toHaveLength(2); + }); +}); + +describe('parseOidcSection', () => { + it('parseOidcSection_ValidInput_ReturnsParsedConfig', () => { + const result = parseOidcSection({ + discoveryEndpoint: 'https://example.com/.well-known', + clientId: 'my-client', + scopes: ['openid'], + }); + expect(Either.getOrThrow(result).clientId).toBe('my-client'); + }); + + it('parseOidcSection_UnknownField_Ignored', () => { + const result = parseOidcSection({ + discoveryEndpoint: 'https://example.com/.well-known', + unknownField: 'kept', + }); + expect(Either.isRight(result)).toBe(true); + }); + + it('parseOidcSection_MissingDiscoveryEndpoint_ReturnsError', () => { + const errors = Either.getOrThrow(Either.flip(parseOidcSection({ clientId: 'my-client' }))); + expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); + }); +}); diff --git a/packages/sdk-utilities/src/lib/config/config.types.ts b/packages/sdk-utilities/src/lib/config/config.types.ts new file mode 100644 index 0000000000..1bf52724cd --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.types.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 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 type * as Either from 'effect/Either'; + +import type { LogLevel, AuthDisplayValue, AuthPromptValue } from '@forgerock/sdk-types'; + +export type { OidcConfig } from '@forgerock/sdk-types'; +export type { AuthDisplayValue, AuthPromptValue }; + +export interface UnifiedOidcConfig { + clientId?: string; + discoveryEndpoint: string; + scopes?: string[]; + redirectUri?: string; + signOutRedirectUri?: string; + refreshThreshold?: number; + loginHint?: string; + nonce?: string; + display?: AuthDisplayValue; + prompt?: AuthPromptValue; + uiLocales?: string; + acrValues?: string; + additionalParameters?: Record; + openId?: { + deviceAuthorizationEndpoint?: string; + }; +} + +export interface UnifiedJourneyConfig { + serverUrl: string; + realm?: string; + cookieName?: string; +} + +export interface UnifiedSdkConfig { + timeout?: number; + log?: Uppercase; + journey?: UnifiedJourneyConfig; + oidc?: UnifiedOidcConfig; +} + +export type ConfigValidationError = { + field: string; + message: string; +}; + +/** + * A parsed result over the accumulating-error channel. Effect's `Either` is + * `Either`, so the SECOND type parameter is the error channel. + */ +export type ParseResult = Either.Either; + +/** Parses a record of unknown values into `A`. Unknown fields are silently ignored. */ +export type Parser = (input: Readonly>) => ParseResult; + +/** Parses a single field value (already extracted from its parent record) into `A`. */ +export type FieldParser = (value: unknown, fieldPath: string) => ParseResult; + +export type { LogLevel, JourneyClientConfig, DaVinciConfig } from '@forgerock/sdk-types'; diff --git a/packages/sdk-utilities/src/lib/config/config.utils.ts b/packages/sdk-utilities/src/lib/config/config.utils.ts new file mode 100644 index 0000000000..2865d73739 --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.utils.ts @@ -0,0 +1,536 @@ +/* + * Copyright (c) 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 { pipe } from 'effect'; +import * as Either from 'effect/Either'; + +import { + AUTH_DISPLAY_VALUES, + AUTH_PROMPT_VALUES, + LOG_LEVEL_UPPERCASE_VALUES, +} from '@forgerock/sdk-types'; +import type { LogLevel, AuthDisplayValue, AuthPromptValue } from '@forgerock/sdk-types'; + +import type { + UnifiedSdkConfig, + UnifiedOidcConfig, + UnifiedJourneyConfig, + OidcConfig, + JourneyClientConfig, + DaVinciConfig, + ConfigValidationError, + FieldParser, + Parser, + ParseResult, +} from './config.types.js'; + +/* ------------------------------------------------------------------ * + * Internal narrowed types — proof types for pure transforms, not public API + * ------------------------------------------------------------------ */ + +interface ClientOidcBlock extends UnifiedOidcConfig { + clientId: string; + redirectUri: string; + scopes: string[]; +} + +interface ClientSdkConfig extends UnifiedSdkConfig { + oidc: ClientOidcBlock; +} + +interface JourneySdkConfig extends UnifiedSdkConfig { + oidc: UnifiedOidcConfig; +} + +/** Partial record with exactly one optional key — the return type of `parsedProp`. */ +type ParsedProp = { [P in K]?: V }; + +/* ------------------------------------------------------------------ * + * Shared helpers + * ------------------------------------------------------------------ */ + +/** Human-readable type name for error messages — distinguishes array and null from object. */ +function typeName(value: unknown): string { + if (Array.isArray(value)) return 'array'; + if (value === null) return 'null'; + return typeof value; +} + +/** An absent optional field is `undefined` or `null`. */ +function isAbsent(value: unknown): value is undefined | null { + return value === undefined || value === null; +} + +/** + * Gather every error from a list of parsed results without short-circuiting (unlike + * `Either.all`, which stops at the first `Left`). Returns all accumulated errors so a + * section parser can report every invalid field in one pass. + */ +export function collectErrors( + results: ReadonlyArray>, +): ConfigValidationError[] { + return results.flatMap((result) => (Either.isLeft(result) ? result.left : [])); +} + +/** + * Unwraps a `ParseResult`, then returns `{ [key]: value }` when the value is defined, `{}` otherwise. + * Spread into an object literal to conditionally include a property without an inline ternary. + * Safe to call after `collectErrors` — every result is guaranteed `Right` past the error guard. + */ +function parsedProp(key: K, result: ParseResult): ParsedProp { + const value = Either.getOrThrow(result); + return (value !== undefined ? { [key]: value } : {}) as ParsedProp; +} + +/* ------------------------------------------------------------------ * + * Generic field parsers (reusable constraints) + * ------------------------------------------------------------------ */ + +const requiredString: FieldParser = (value, fieldPath) => { + if (isAbsent(value)) { + return Either.left([{ field: fieldPath, message: 'Required field is missing' }]); + } + return typeof value === 'string' + ? Either.right(value) + : Either.left([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); +}; + +/** Required, non-empty string — treats `''` as missing (a blank value can't satisfy a requirement). */ +const requiredNonEmptyString: FieldParser = (value, fieldPath) => { + if (isAbsent(value) || value === '') { + return Either.left([{ field: fieldPath, message: 'Required field is missing' }]); + } + return typeof value === 'string' + ? Either.right(value) + : Either.left([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); +}; + +/** Required, non-empty array of strings — treats absent or `[]` as missing. */ +const requiredNonEmptyStringArray: FieldParser = (value, fieldPath) => { + if (isAbsent(value) || (Array.isArray(value) && value.length === 0)) { + return Either.left([{ field: fieldPath, message: 'Required field is missing' }]); + } + if (!Array.isArray(value)) { + return Either.left([{ field: fieldPath, message: `Expected array, got ${typeName(value)}` }]); + } + const parsed: string[] = []; + const errors: ConfigValidationError[] = []; + value.forEach((item, index) => { + if (typeof item === 'string') { + parsed.push(item); + } else { + errors.push({ + field: `${fieldPath}[${index}]`, + message: `Expected string, got ${typeName(item)}`, + }); + } + }); + return errors.length > 0 ? Either.left(errors) : Either.right(parsed); +}; + +const optionalString: FieldParser = (value, fieldPath) => { + if (isAbsent(value)) return Either.right(undefined); + return typeof value === 'string' + ? Either.right(value) + : Either.left([{ field: fieldPath, message: `Expected string, got ${typeName(value)}` }]); +}; + +/** Finite, non-negative number (rejects NaN, Infinity, negatives). Optional-aware. */ +const finiteNonNegativeNumber: FieldParser = (value, fieldPath) => { + if (isAbsent(value)) return Either.right(undefined); + if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { + return Either.left([ + { + field: fieldPath, + message: `Expected a finite, non-negative number, got ${typeName(value)}`, + }, + ]); + } + return Either.right(value); +}; + +/** Optional array of strings, accumulating one error per non-string element. */ +const optionalStringArray: FieldParser = (value, fieldPath) => { + if (isAbsent(value)) return Either.right(undefined); + if (!Array.isArray(value)) { + return Either.left([{ field: fieldPath, message: `Expected array, got ${typeName(value)}` }]); + } + const parsed: string[] = []; + const errors: ConfigValidationError[] = []; + value.forEach((item, index) => { + if (typeof item === 'string') { + parsed.push(item); + } else { + errors.push({ + field: `${fieldPath}[${index}]`, + message: `Expected string, got ${typeName(item)}`, + }); + } + }); + return errors.length > 0 ? Either.left(errors) : Either.right(parsed); +}; + +/** Optional `Record` — builds a fresh record, erroring on non-string values. */ +const optionalStringRecord: FieldParser | undefined> = ( + value, + fieldPath, +) => { + if (isAbsent(value)) return Either.right(undefined); + if (typeof value !== 'object' || Array.isArray(value)) { + return Either.left([{ field: fieldPath, message: `Expected object, got ${typeName(value)}` }]); + } + const parsed: Record = {}; + const errors: ConfigValidationError[] = []; + for (const [key, entryValue] of Object.entries(value)) { + if (typeof entryValue === 'string') { + parsed[key] = entryValue; + } else { + errors.push({ + field: `${fieldPath}.${key}`, + message: `Expected string, got ${typeName(entryValue)}`, + }); + } + } + return errors.length > 0 ? Either.left(errors) : Either.right(parsed); +}; + +/** + * Optional value restricted to a closed set of string literals. Narrows via a typed + * predicate `find` so the parsed value carries the literal-union type without a cast. + */ +function optionalLiteralUnion( + members: Members, +): FieldParser { + return (value, fieldPath) => { + if (isAbsent(value)) return Either.right(undefined); + if (typeof value !== 'string') { + return Either.left([ + { field: fieldPath, message: `Expected string, got ${typeName(value)}` }, + ]); + } + const matched = members.find((member): member is Members[number] => member === value); + return matched !== undefined + ? Either.right(matched) + : Either.left([ + { field: fieldPath, message: `Expected one of ${members.join(', ')}, got ${value}` }, + ]); + }; +} + +/* ------------------------------------------------------------------ * + * Per-property parsers (each owns its field path + constraint) + * ------------------------------------------------------------------ */ + +export const parseDiscoveryEndpoint = (value: unknown): ParseResult => + requiredNonEmptyString(value, 'oidc.discoveryEndpoint'); + +export const parseClientId = (value: unknown): ParseResult => + optionalString(value, 'oidc.clientId'); + +export const parseRedirectUri = (value: unknown): ParseResult => + optionalString(value, 'oidc.redirectUri'); + +export const parseScopes = (value: unknown): ParseResult => + optionalStringArray(value, 'oidc.scopes'); + +export const parseSignOutRedirectUri = (value: unknown): ParseResult => + optionalString(value, 'oidc.signOutRedirectUri'); + +export const parseRefreshThreshold = (value: unknown): ParseResult => + finiteNonNegativeNumber(value, 'oidc.refreshThreshold'); + +export const parseLoginHint = (value: unknown): ParseResult => + optionalString(value, 'oidc.loginHint'); + +export const parseNonce = (value: unknown): ParseResult => + optionalString(value, 'oidc.nonce'); + +export const parseDisplay = (value: unknown): ParseResult => + optionalLiteralUnion(AUTH_DISPLAY_VALUES)(value, 'oidc.display'); + +export const parsePrompt = (value: unknown): ParseResult => + optionalLiteralUnion(AUTH_PROMPT_VALUES)(value, 'oidc.prompt'); + +export const parseUiLocales = (value: unknown): ParseResult => + optionalString(value, 'oidc.uiLocales'); + +export const parseAcrValues = (value: unknown): ParseResult => + optionalString(value, 'oidc.acrValues'); + +export const parseAdditionalParameters = ( + value: unknown, +): ParseResult | undefined> => + optionalStringRecord(value, 'oidc.additionalParameters'); + +export const parseTimeout = (value: unknown): ParseResult => + finiteNonNegativeNumber(value, 'timeout'); + +export const parseLog = (value: unknown): ParseResult | undefined> => + optionalLiteralUnion(LOG_LEVEL_UPPERCASE_VALUES)(value, 'log'); + +export const parseServerUrl = (value: unknown): ParseResult => + requiredString(value, 'journey.serverUrl'); + +export const parseRealm = (value: unknown): ParseResult => + optionalString(value, 'journey.realm'); + +export const parseCookieName = (value: unknown): ParseResult => + optionalString(value, 'journey.cookieName'); + +/* ------------------------------------------------------------------ * + * Section parsers — compose property parsers, build typed objects cast-free + * ------------------------------------------------------------------ */ + +/** Parse the `oidc` block. Each property parser carries its own `oidc.`-prefixed path. */ +export const parseOidcSection: Parser = (input) => { + const discoveryEndpoint = parseDiscoveryEndpoint(input['discoveryEndpoint']); + const clientId = parseClientId(input['clientId']); + const redirectUri = parseRedirectUri(input['redirectUri']); + const scopes = parseScopes(input['scopes']); + const signOutRedirectUri = parseSignOutRedirectUri(input['signOutRedirectUri']); + const refreshThreshold = parseRefreshThreshold(input['refreshThreshold']); + const loginHint = parseLoginHint(input['loginHint']); + const nonce = parseNonce(input['nonce']); + const display = parseDisplay(input['display']); + const prompt = parsePrompt(input['prompt']); + const uiLocales = parseUiLocales(input['uiLocales']); + const acrValues = parseAcrValues(input['acrValues']); + const additionalParameters = parseAdditionalParameters(input['additionalParameters']); + + const errors = collectErrors([ + discoveryEndpoint, + clientId, + redirectUri, + scopes, + signOutRedirectUri, + refreshThreshold, + loginHint, + nonce, + display, + prompt, + uiLocales, + acrValues, + additionalParameters, + ]); + if (errors.length > 0) return Either.left(errors); + + const oidc: UnifiedOidcConfig = { + discoveryEndpoint: Either.getOrThrow(discoveryEndpoint), + ...parsedProp('clientId', clientId), + ...parsedProp('redirectUri', redirectUri), + ...parsedProp('scopes', scopes), + ...parsedProp('signOutRedirectUri', signOutRedirectUri), + ...parsedProp('refreshThreshold', refreshThreshold), + ...parsedProp('loginHint', loginHint), + ...parsedProp('nonce', nonce), + ...parsedProp('display', display), + ...parsedProp('prompt', prompt), + ...parsedProp('uiLocales', uiLocales), + ...parsedProp('acrValues', acrValues), + ...parsedProp('additionalParameters', additionalParameters), + }; + return Either.right(oidc); +}; + +/** Parse the `journey` block. `serverUrl` required; `realm`/`cookieName` optional. */ +export const parseJourneySection: Parser = (input) => { + const serverUrl = parseServerUrl(input['serverUrl']); + const realm = parseRealm(input['realm']); + const cookieName = parseCookieName(input['cookieName']); + + const errors = collectErrors([serverUrl, realm, cookieName]); + if (errors.length > 0) return Either.left(errors); + + const journey: UnifiedJourneyConfig = { + serverUrl: Either.getOrThrow(serverUrl), + ...parsedProp('realm', realm), + ...parsedProp('cookieName', cookieName), + }; + return Either.right(journey); +}; + +/** + * Run a section parser against an optional nested object: absent → `Right(undefined)`; + * present-but-not-an-object → `Left`; present object → delegate to `parser`. + */ +function parseOptionalSection( + value: unknown, + prefix: string, + parser: Parser, +): ParseResult { + if (isAbsent(value)) return Either.right(undefined); + if (typeof value !== 'object' || Array.isArray(value)) { + return Either.left([{ field: prefix, message: `Expected object, got ${typeName(value)}` }]); + } + return parser({ ...value }); +} + +/** Parse a full unified SDK config from an already-typed record. Unknown fields are ignored. */ +export const parseUnifiedSdkConfig: Parser = (input) => { + const timeout = parseTimeout(input['timeout']); + const log = parseLog(input['log']); + const journey = parseOptionalSection(input['journey'], 'journey', parseJourneySection); + const oidc = parseOptionalSection(input['oidc'], 'oidc', parseOidcSection); + + const errors = collectErrors([timeout, log, journey, oidc]); + if (errors.length > 0) return Either.left(errors); + + const config: UnifiedSdkConfig = { + ...parsedProp('timeout', timeout), + ...parsedProp('log', log), + ...parsedProp('journey', journey), + ...parsedProp('oidc', oidc), + }; + return Either.right(config); +}; + +/* ------------------------------------------------------------------ * + * Strict config parsers — require client fields, build the narrowed type cast-free + * ------------------------------------------------------------------ */ + +/** + * Require the client-flow fields (`discoveryEndpoint`, `clientId`, `redirectUri`, + * `scopes`) and return a `ClientSdkConfig` whose `oidc` block has them guaranteed + * present — so downstream transforms read them without any runtime guard. Errors + * accumulate per field. + */ +function parseClientSdkConfig(config: UnifiedSdkConfig): ParseResult { + if (!config.oidc) { + return Either.left([{ field: 'oidc', message: 'Required block is missing' }]); + } + const oidc = config.oidc; + // All four are required and non-optional, so `Either.all` (first-error) is enough — the + // struct form keeps field/value paired by key. Section parsers collect errors across many + // optional fields instead, so they accumulate via `collectErrors`. + return Either.map( + Either.all({ + discoveryEndpoint: requiredNonEmptyString(oidc.discoveryEndpoint, 'oidc.discoveryEndpoint'), + clientId: requiredNonEmptyString(oidc.clientId, 'oidc.clientId'), + redirectUri: requiredNonEmptyString(oidc.redirectUri, 'oidc.redirectUri'), + scopes: requiredNonEmptyStringArray(oidc.scopes, 'oidc.scopes'), + }), + (validated): ClientSdkConfig => ({ ...config, oidc: { ...oidc, ...validated } }), + ); +} + +/** + * Require only `discoveryEndpoint` (Journey derives all server config from it) and + * return a `JourneySdkConfig` whose `oidc` block is guaranteed present. + */ +function parseJourneySdkConfig(config: UnifiedSdkConfig): ParseResult { + if (!config.oidc) { + return Either.left([{ field: 'oidc', message: 'Required block is missing' }]); + } + const oidc = config.oidc; + return Either.map( + requiredNonEmptyString(oidc.discoveryEndpoint, 'oidc.discoveryEndpoint'), + (discoveryEndpoint): JourneySdkConfig => ({ + ...config, + oidc: { ...oidc, discoveryEndpoint }, + }), + ); +} + +/* ------------------------------------------------------------------ * + * Pure transforms — validated config → native client config (no guards) + * ------------------------------------------------------------------ */ + +function toMappedLogLevel(level: Uppercase): LogLevel { + return level.toLowerCase() as LogLevel; +} + +function buildOidcConfig(config: ClientSdkConfig): OidcConfig { + const { oidc } = config; + return { + clientId: oidc.clientId, + redirectUri: oidc.redirectUri, + scope: oidc.scopes.join(' '), + serverConfig: { + wellknown: oidc.discoveryEndpoint, + ...(config.timeout !== undefined && { timeout: config.timeout }), + }, + ...(oidc.refreshThreshold !== undefined && { oauthThreshold: oidc.refreshThreshold * 1000 }), + ...(config.journey?.realm !== undefined && { realmPath: config.journey.realm }), + ...(oidc.signOutRedirectUri !== undefined && { signOutRedirectUri: oidc.signOutRedirectUri }), + ...(oidc.loginHint !== undefined && { loginHint: oidc.loginHint }), + ...(oidc.nonce !== undefined && { nonce: oidc.nonce }), + ...(oidc.display !== undefined && { display: oidc.display }), + ...(oidc.prompt !== undefined && { prompt: oidc.prompt }), + ...(oidc.uiLocales !== undefined && { uiLocales: oidc.uiLocales }), + ...(oidc.acrValues !== undefined && { acrValues: oidc.acrValues }), + ...(oidc.additionalParameters !== undefined && { query: oidc.additionalParameters }), + ...(config.log !== undefined && { log: toMappedLogLevel(config.log) }), + }; +} + +function buildJourneyConfig(config: JourneySdkConfig): JourneyClientConfig { + const { oidc } = config; + return { + serverConfig: { + wellknown: oidc.discoveryEndpoint, + ...(config.timeout !== undefined && { timeout: config.timeout }), + }, + ...(config.journey?.realm !== undefined && { realmPath: config.journey.realm }), + // journey.cookieName is not used by JS — all session handling is cookie-free via tokens. + // Accepted in unified schema for cross-platform parity (Android/iOS use it) but not mapped. + ...(config.log !== undefined && { log: toMappedLogLevel(config.log) }), + }; +} + +function buildDavinciConfig(config: ClientSdkConfig): DaVinciConfig { + const { oidc } = config; + return { + clientId: oidc.clientId, + redirectUri: oidc.redirectUri, + scope: oidc.scopes.join(' '), + serverConfig: { + wellknown: oidc.discoveryEndpoint, + ...(config.timeout !== undefined && { timeout: config.timeout }), + }, + ...(oidc.refreshThreshold !== undefined && { oauthThreshold: oidc.refreshThreshold * 1000 }), + ...(config.journey?.realm !== undefined && { realmPath: config.journey.realm }), + ...(config.log !== undefined && { log: toMappedLogLevel(config.log) }), + }; +} + +/* ------------------------------------------------------------------ * + * Single-pass public parsers — the PDV entry points + * Parse from `unknown` directly to the native config type in one pass. + * ------------------------------------------------------------------ */ + +function assertObject( + input: unknown, +): Either.Either>, ConfigValidationError[]> { + if (typeof input !== 'object' || input === null || Array.isArray(input)) { + return Either.left([{ field: 'config', message: `Expected object, got ${typeName(input)}` }]); + } + return Either.right(input as Readonly>); +} + +export const parseToOidcConfig = (input: unknown): ParseResult => + pipe( + assertObject(input), + Either.flatMap(parseUnifiedSdkConfig), + Either.flatMap(parseClientSdkConfig), + Either.map(buildOidcConfig), + ); + +export const parseToJourneyConfig = (input: unknown): ParseResult => + pipe( + assertObject(input), + Either.flatMap(parseUnifiedSdkConfig), + Either.flatMap(parseJourneySdkConfig), + Either.map(buildJourneyConfig), + ); + +export const parseToDavinciConfig = (input: unknown): ParseResult => + pipe( + assertObject(input), + Either.flatMap(parseUnifiedSdkConfig), + Either.flatMap(parseClientSdkConfig), + Either.map(buildDavinciConfig), + ); diff --git a/packages/sdk-utilities/src/lib/config/index.ts b/packages/sdk-utilities/src/lib/config/index.ts new file mode 100644 index 0000000000..7d538e1d3f --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright (c) 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. + */ + +export * from './config.types.js'; +export * from './config.utils.js'; +export * from './config.effects.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddf957384f..bc52cb8056 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -559,7 +559,11 @@ importers: packages/sdk-effects/iframe-manager: {} - packages/sdk-effects/logger: {} + packages/sdk-effects/logger: + dependencies: + '@forgerock/sdk-types': + specifier: workspace:* + version: link:../../sdk-types packages/sdk-effects/oidc: dependencies: @@ -589,6 +593,9 @@ importers: '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types + effect: + specifier: catalog:effect + version: 3.21.0 scratchpad: dependencies: @@ -11733,7 +11740,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -11792,7 +11799,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -17227,7 +17234,7 @@ snapshots: dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): dependencies: