diff --git a/.changeset/sdks-5067-unified-config.md b/.changeset/sdks-5067-unified-config.md new file mode 100644 index 0000000000..42a66492aa --- /dev/null +++ b/.changeset/sdks-5067-unified-config.md @@ -0,0 +1,59 @@ +--- +'@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` +- `isUnifiedSdkConfig` discriminator +- `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/e2e/journey-suites/src/login.test.ts b/e2e/journey-suites/src/login.test.ts index 63f83d988f..c3f780bacc 100644 --- a/e2e/journey-suites/src/login.test.ts +++ b/e2e/journey-suites/src/login.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. diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index d277642c94..4ba574e878 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'; @@ -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..16f628e01a 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'; @@ -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..35a4a51dbe 100644 --- a/packages/journey-client/api-report/journey-client.api.md +++ b/packages/journey-client/api-report/journey-client.api.md @@ -5,7 +5,6 @@ ```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'; @@ -16,6 +15,8 @@ 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'; @@ -203,11 +204,7 @@ export interface JourneyClient { }) => Promise; } -// @public -export interface JourneyClientConfig extends AsyncLegacyConfigOptions { - // (undocumented) - serverConfig: JourneyServerConfig; -} +export { JourneyClientConfig } // @public (undocumented) export type JourneyLoginFailure = AuthResponse & { @@ -232,11 +229,7 @@ export type JourneyLoginSuccess = AuthResponse & { // @public (undocumented) export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError; -// @public -export interface JourneyServerConfig { - timeout?: number; - wellknown: string; -} +export { JourneyServerConfig } // @public (undocumented) export type JourneyStep = AuthResponse & { 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..d9219a9710 100644 --- a/packages/journey-client/api-report/journey-client.types.api.md +++ b/packages/journey-client/api-report/journey-client.types.api.md @@ -5,7 +5,6 @@ ```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'; @@ -15,6 +14,8 @@ 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'; @@ -190,11 +191,7 @@ export interface JourneyClient { }) => Promise; } -// @public -export interface JourneyClientConfig extends AsyncLegacyConfigOptions { - // (undocumented) - serverConfig: JourneyServerConfig; -} +export { JourneyClientConfig } // @public (undocumented) export type JourneyLoginFailure = AuthResponse & { @@ -219,11 +216,7 @@ export type JourneyLoginSuccess = AuthResponse & { // @public (undocumented) export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError; -// @public -export interface JourneyServerConfig { - timeout?: number; - wellknown: string; -} +export { JourneyServerConfig } // @public (undocumented) export type JourneyStep = AuthResponse & { 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/journey-client/src/lib/journey.utils.ts b/packages/journey-client/src/lib/journey.utils.ts index 198721e43e..44d82e65fb 100644 --- a/packages/journey-client/src/lib/journey.utils.ts +++ b/packages/journey-client/src/lib/journey.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. diff --git a/packages/journey-client/src/types.ts b/packages/journey-client/src/types.ts index e4802c9db3..d4ba5a025c 100644 --- a/packages/journey-client/src/types.ts +++ b/packages/journey-client/src/types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2020 - 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. diff --git a/packages/oidc-client/api-report/oidc-client.api.md b/packages/oidc-client/api-report/oidc-client.api.md index e934c022e6..2fc6229a58 100644 --- a/packages/oidc-client/api-report/oidc-client.api.md +++ b/packages/oidc-client/api-report/oidc-client.api.md @@ -5,7 +5,6 @@ ```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'; @@ -19,6 +18,7 @@ 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'; @@ -134,6 +134,7 @@ url: string; endSession: MutationDefinition< { idToken: string; endpoint: string; +signOutRedirectUri?: string; }, BaseQueryFn, never, null, "oidc", unknown>; exchange: MutationDefinition< { code: string; @@ -170,6 +171,7 @@ url: string; endSession: MutationDefinition< { idToken: string; endpoint: string; +signOutRedirectUri?: string; }, BaseQueryFn, never, null, "oidc", unknown>; exchange: MutationDefinition< { code: string; @@ -287,24 +289,7 @@ export function oidc(input: { // @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; - }; -} +export { OidcConfig } // @public (undocumented) export type OptionalAuthorizeOptions = Partial; 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..2fc6229a58 100644 --- a/packages/oidc-client/api-report/oidc-client.types.api.md +++ b/packages/oidc-client/api-report/oidc-client.types.api.md @@ -5,7 +5,6 @@ ```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'; @@ -19,6 +18,7 @@ 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'; @@ -134,6 +134,7 @@ url: string; endSession: MutationDefinition< { idToken: string; endpoint: string; +signOutRedirectUri?: string; }, BaseQueryFn, never, null, "oidc", unknown>; exchange: MutationDefinition< { code: string; @@ -170,6 +171,7 @@ url: string; endSession: MutationDefinition< { idToken: string; endpoint: string; +signOutRedirectUri?: string; }, BaseQueryFn, never, null, "oidc", unknown>; exchange: MutationDefinition< { code: string; @@ -287,24 +289,7 @@ export function oidc(input: { // @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; - }; -} +export { OidcConfig } // @public (undocumented) export type OptionalAuthorizeOptions = Partial; diff --git a/packages/oidc-client/src/lib/authorize.request.micros.test.ts b/packages/oidc-client/src/lib/authorize.request.micros.test.ts index 289617eae9..4c9e0648be 100644 --- a/packages/oidc-client/src/lib/authorize.request.micros.test.ts +++ b/packages/oidc-client/src/lib/authorize.request.micros.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 * as sdkUtilities from '@forgerock/sdk-utilities'; 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..3a3639deb2 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. @@ -7,6 +7,8 @@ import type { LegacyConfigOptions } from './legacy-config.types.js'; export type ResponseType = 'code' | 'token'; +export type AuthDisplayValue = 'page' | 'popup' | 'touch' | 'wap'; +export type AuthPromptValue = 'none' | 'login' | 'consent' | 'select_account'; /** * Options for the authorization URL @@ -29,7 +31,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..dd3c475f2e 100644 --- a/packages/sdk-types/src/lib/config.types.ts +++ b/packages/sdk-types/src/lib/config.types.ts @@ -1,10 +1,61 @@ /* - * 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 type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'none'; + +/** 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.test.ts b/packages/sdk-utilities/src/lib/config/config.test.ts new file mode 100644 index 0000000000..69106f4bd5 --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.test.ts @@ -0,0 +1,583 @@ +/* + * 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 { Either } from 'effect'; +import { + validateUnifiedSdkConfig, + validateUnifiedOidcConfig, + unifiedToOidcConfig, + unifiedToJourneyConfig, + unifiedToDavinciConfig, + isUnifiedSdkConfig, + makeOidcConfig, + makeJourneyConfig, + makeDavinciConfig, +} from './config.utils.js'; +import type { UnifiedSdkConfig } from './config.types.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: UnifiedSdkConfig = { + 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: UnifiedSdkConfig = { + journey: { + serverUrl: 'https://example.com/am', + realm: 'alpha', + }, + oidc: { + discoveryEndpoint: 'https://example.com/.well-known/openid-configuration', + }, +}; + +describe('validateUnifiedOidcConfig', () => { + it('validateUnifiedOidcConfig_ValidMinimalInput_ReturnsSuccess', () => { + const result = validateUnifiedOidcConfig(minimalOidc); + expect(Either.getOrThrow(result).clientId).toBe('my-client'); + }); + + it('validateUnifiedOidcConfig_StrictMode_MissingClientId_ReturnsError', () => { + const { clientId: _removed, ...input } = minimalOidc; + const errors = Either.getOrThrow(Either.flip(validateUnifiedOidcConfig(input, 'oidc', true))); + expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); + }); + + it('validateUnifiedOidcConfig_MissingDiscoveryEndpoint_ReturnsError', () => { + const { discoveryEndpoint: _removed, ...input } = minimalOidc; + const errors = Either.getOrThrow(Either.flip(validateUnifiedOidcConfig(input))); + expect(errors.some((e) => e.field === 'oidc.discoveryEndpoint')).toBe(true); + }); + + it('validateUnifiedOidcConfig_StrictMode_MissingScopes_ReturnsError', () => { + const { scopes: _removed, ...input } = minimalOidc; + const errors = Either.getOrThrow(Either.flip(validateUnifiedOidcConfig(input, 'oidc', true))); + expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); + }); + + it('validateUnifiedOidcConfig_StrictMode_MissingRedirectUri_ReturnsError', () => { + const { redirectUri: _removed, ...input } = minimalOidc; + const errors = Either.getOrThrow(Either.flip(validateUnifiedOidcConfig(input, 'oidc', true))); + expect(errors.some((e) => e.field === 'oidc.redirectUri')).toBe(true); + }); + + it('validateUnifiedOidcConfig_ScopesNotArray_ReturnsTypeError', () => { + const errors = Either.getOrThrow( + Either.flip(validateUnifiedOidcConfig({ ...minimalOidc, scopes: 'openid profile' })), + ); + expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); + }); + + it('validateUnifiedOidcConfig_ClientIdNotString_ReturnsTypeError', () => { + const errors = Either.getOrThrow( + Either.flip(validateUnifiedOidcConfig({ ...minimalOidc, clientId: 123 })), + ); + expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); + }); + + it('validateUnifiedOidcConfig_RefreshThresholdNotNumber_ReturnsTypeError', () => { + const errors = Either.getOrThrow( + Either.flip(validateUnifiedOidcConfig({ ...minimalOidc, refreshThreshold: 'sixty' })), + ); + expect(errors.some((e) => e.field === 'oidc.refreshThreshold')).toBe(true); + }); + + it('validateUnifiedOidcConfig_AdditionalParametersIsArray_ReturnsTypeError', () => { + const errors = Either.getOrThrow( + Either.flip(validateUnifiedOidcConfig({ ...minimalOidc, additionalParameters: ['foo'] })), + ); + expect(errors.some((e) => e.field === 'oidc.additionalParameters')).toBe(true); + }); + + it('validateUnifiedOidcConfig_UnknownFieldPresent_Ignored', () => { + expect( + Either.isRight(validateUnifiedOidcConfig({ ...minimalOidc, unknownField: 'surprise' })), + ).toBe(true); + }); + + it('validateUnifiedOidcConfig_NullInput_ReturnsError', () => { + expect(Either.isLeft(validateUnifiedOidcConfig(null))).toBe(true); + }); + + it('validateUnifiedOidcConfig_StrictMode_MultipleErrors_AllAccumulated', () => { + const errors = Either.getOrThrow( + Either.flip( + validateUnifiedOidcConfig( + { discoveryEndpoint: 'https://example.com/.well-known', scopes: 'not-an-array' }, + 'oidc', + true, + ), + ), + ); + expect(errors.length).toBeGreaterThanOrEqual(2); + expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); + expect(errors.some((e) => e.field === 'oidc.scopes')).toBe(true); + }); +}); + +describe('validateUnifiedSdkConfig', () => { + it('validateUnifiedSdkConfig_ValidFullConfig_ReturnsSuccess', () => { + expect(Either.isRight(validateUnifiedSdkConfig(fullConfig))).toBe(true); + }); + + it('validateUnifiedSdkConfig_JourneyOnlyConfig_ReturnsSuccess', () => { + expect(Either.isRight(validateUnifiedSdkConfig(journeyOnlyConfig))).toBe(true); + }); + + it('validateUnifiedSdkConfig_NoOidcOrJourneySection_ReturnsSuccess', () => { + expect(Either.isRight(validateUnifiedSdkConfig({ timeout: 5000 }))).toBe(true); + }); + + it('validateUnifiedSdkConfig_TimeoutNotNumber_ReturnsTypeError', () => { + const errors = Either.getOrThrow( + Either.flip(validateUnifiedSdkConfig({ ...fullConfig, timeout: 'thirty' })), + ); + expect(errors.some((e) => e.field === 'timeout')).toBe(true); + }); + + it('validateUnifiedSdkConfig_JourneyMissingServerUrl_ReturnsError', () => { + const errors = Either.getOrThrow( + Either.flip( + validateUnifiedSdkConfig({ + journey: { realm: 'alpha' }, + oidc: { discoveryEndpoint: 'https://example.com/.well-known/openid-configuration' }, + }), + ), + ); + expect(errors.some((e) => e.field === 'journey.serverUrl')).toBe(true); + }); + + it('validateUnifiedSdkConfig_JourneyServerUrlWrongType_ReturnsError', () => { + const errors = Either.getOrThrow( + Either.flip(validateUnifiedSdkConfig({ journey: { serverUrl: 123 } })), + ); + expect(errors.some((e) => e.field === 'journey.serverUrl')).toBe(true); + }); + + it('validateUnifiedSdkConfig_StrictMode_MissingOidcClientId_ReturnsError', () => { + const errors = Either.getOrThrow( + Either.flip( + validateUnifiedSdkConfig( + { + oidc: { + discoveryEndpoint: 'https://example.com/.well-known/openid-configuration', + scopes: ['openid'], + redirectUri: 'https://example.com/cb', + }, + }, + true, + ), + ), + ); + expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); + }); + + it('validateUnifiedSdkConfig_InvalidOidcNested_PropagatesErrors', () => { + const errors = Either.getOrThrow( + Either.flip( + validateUnifiedSdkConfig({ ...fullConfig, oidc: { ...minimalOidc, clientId: 42 } }), + ), + ); + expect(errors.some((e) => e.field === 'oidc.clientId')).toBe(true); + }); + + it('validateUnifiedSdkConfig_NullInput_ReturnsError', () => { + expect(Either.isLeft(validateUnifiedSdkConfig(null))).toBe(true); + }); +}); + +describe('unifiedToOidcConfig', () => { + it('unifiedToOidcConfig_NoOidcBlock_ReturnsFailure', () => { + const error = Either.getOrThrow( + Either.flip(unifiedToOidcConfig({ journey: { serverUrl: 'https://example.com/am' } })), + ); + expect(error.field).toBe('oidc'); + }); + + it('unifiedToOidcConfig_MinimalConfig_MapsRequiredFields', () => { + const data = Either.getOrThrow(unifiedToOidcConfig({ 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('unifiedToOidcConfig_ScopesJoinedWithSpace', () => { + const data = Either.getOrThrow( + unifiedToOidcConfig({ oidc: { ...minimalOidc, scopes: ['openid', 'email'] } }), + ); + expect(data.scope).toBe('openid email'); + }); + + it('unifiedToOidcConfig_RefreshThresholdConvertedToMs', () => { + const data = Either.getOrThrow( + unifiedToOidcConfig({ oidc: { ...minimalOidc, refreshThreshold: 60 } }), + ); + expect(data.oauthThreshold).toBe(60000); + }); + + it('unifiedToOidcConfig_NoRefreshThreshold_OauthThresholdAbsent', () => { + expect( + Either.getOrThrow(unifiedToOidcConfig({ oidc: minimalOidc })).oauthThreshold, + ).toBeUndefined(); + }); + + it('unifiedToOidcConfig_RealmMappedToRealmPath', () => { + const data = Either.getOrThrow( + unifiedToOidcConfig({ + journey: { serverUrl: 'https://example.com/am', realm: 'alpha' }, + oidc: minimalOidc, + }), + ); + expect(data.realmPath).toBe('alpha'); + }); + + it('unifiedToOidcConfig_NoRealm_RealmPathAbsent', () => { + expect(Either.getOrThrow(unifiedToOidcConfig({ oidc: minimalOidc })).realmPath).toBeUndefined(); + }); + + it('unifiedToOidcConfig_TimeoutPassedToServerConfig', () => { + const data = Either.getOrThrow(unifiedToOidcConfig({ timeout: 5000, oidc: minimalOidc })); + expect(data.serverConfig.timeout).toBe(5000); + }); + + it('unifiedToOidcConfig_NoTimeout_TimeoutAbsentInServerConfig', () => { + expect( + Either.getOrThrow(unifiedToOidcConfig({ oidc: minimalOidc })).serverConfig.timeout, + ).toBeUndefined(); + }); + + it('unifiedToOidcConfig_AuthorizeParamsMapped', () => { + const data = Either.getOrThrow( + unifiedToOidcConfig({ + 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('unifiedToOidcConfig_NoAuthorizeParams_AllAbsent', () => { + const data = Either.getOrThrow(unifiedToOidcConfig({ oidc: minimalOidc })); + expect(data.loginHint).toBeUndefined(); + expect(data.nonce).toBeUndefined(); + expect(data.query).toBeUndefined(); + }); +}); + +describe('unifiedToJourneyConfig', () => { + it('unifiedToJourneyConfig_NoOidcBlock_ReturnsFailure', () => { + const error = Either.getOrThrow( + Either.flip(unifiedToJourneyConfig({ journey: { serverUrl: 'https://example.com/am' } })), + ); + expect(error.field).toBe('oidc'); + }); + + it('unifiedToJourneyConfig_MinimalConfig_MapsWellknown', () => { + const data = Either.getOrThrow(unifiedToJourneyConfig({ oidc: minimalOidc })); + expect(data.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('unifiedToJourneyConfig_JourneyOnlyConfig_MapsWellknown', () => { + const data = Either.getOrThrow(unifiedToJourneyConfig(journeyOnlyConfig)); + expect(data.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + }); + + it('unifiedToJourneyConfig_RealmMappedToRealmPath', () => { + const data = Either.getOrThrow( + unifiedToJourneyConfig({ + journey: { serverUrl: 'https://example.com/am', realm: 'beta' }, + oidc: minimalOidc, + }), + ); + expect(data.realmPath).toBe('beta'); + }); + + it('unifiedToJourneyConfig_NoRealm_RealmPathAbsent', () => { + expect( + Either.getOrThrow(unifiedToJourneyConfig({ oidc: minimalOidc })).realmPath, + ).toBeUndefined(); + }); + + it('unifiedToJourneyConfig_TimeoutPassedToServerConfig', () => { + const data = Either.getOrThrow(unifiedToJourneyConfig({ timeout: 10000, oidc: minimalOidc })); + expect(data.serverConfig.timeout).toBe(10000); + }); + + it('unifiedToJourneyConfig_OidcFieldsNotLeakedToResult', () => { + const data = Either.getOrThrow(unifiedToJourneyConfig(fullConfig)) as unknown as Record< + string, + unknown + >; + expect(data['clientId']).toBeUndefined(); + expect(data['scope']).toBeUndefined(); + expect(data['redirectUri']).toBeUndefined(); + }); +}); + +describe('unifiedToDavinciConfig', () => { + it('unifiedToDavinciConfig_NoOidcBlock_ReturnsFailure', () => { + const error = Either.getOrThrow( + Either.flip(unifiedToDavinciConfig({ journey: { serverUrl: 'https://example.com/am' } })), + ); + expect(error.field).toBe('oidc'); + }); + + it('unifiedToDavinciConfig_MinimalConfig_MapsRequiredFields', () => { + const data = Either.getOrThrow(unifiedToDavinciConfig({ 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('unifiedToDavinciConfig_ScopesJoinedWithSpace', () => { + const data = Either.getOrThrow( + unifiedToDavinciConfig({ oidc: { ...minimalOidc, scopes: ['openid', 'email'] } }), + ); + expect(data.scope).toBe('openid email'); + }); + + it('unifiedToDavinciConfig_RefreshThresholdConvertedToMs', () => { + const data = Either.getOrThrow( + unifiedToDavinciConfig({ oidc: { ...minimalOidc, refreshThreshold: 30 } }), + ); + expect(data.oauthThreshold).toBe(30000); + }); + + it('unifiedToDavinciConfig_RealmMappedToRealmPath', () => { + const data = Either.getOrThrow( + unifiedToDavinciConfig({ + journey: { serverUrl: 'https://example.com/am', realm: 'alpha' }, + oidc: minimalOidc, + }), + ); + expect(data.realmPath).toBe('alpha'); + }); + + it('unifiedToDavinciConfig_TimeoutPassedToServerConfig', () => { + const data = Either.getOrThrow(unifiedToDavinciConfig({ timeout: 7000, oidc: minimalOidc })); + expect(data.serverConfig.timeout).toBe(7000); + }); +}); + +describe('unifiedToOidcConfig log mapping', () => { + it('unifiedToOidcConfig_LogFieldMapped_ToLogLevel', () => { + expect(Either.getOrThrow(unifiedToOidcConfig({ log: 'DEBUG', oidc: minimalOidc })).log).toBe( + 'debug', + ); + }); + + it('unifiedToOidcConfig_NoLogField_LogLevelAbsent', () => { + expect(Either.getOrThrow(unifiedToOidcConfig({ oidc: minimalOidc })).log).toBeUndefined(); + }); + + it('unifiedToOidcConfig_CookieName_NotMappedToResult', () => { + const data = Either.getOrThrow( + unifiedToOidcConfig({ + journey: { serverUrl: 'https://example.com/am', cookieName: 'iPlanetDirectoryPro' }, + oidc: minimalOidc, + }), + ) as unknown as Record; + expect(data['cookieName']).toBeUndefined(); + }); +}); + +describe('unifiedToJourneyConfig log mapping', () => { + it('unifiedToJourneyConfig_LogFieldMapped_ToLogLevel', () => { + expect(Either.getOrThrow(unifiedToJourneyConfig({ log: 'WARN', oidc: minimalOidc })).log).toBe( + 'warn', + ); + }); + + it('unifiedToJourneyConfig_NoLogField_LogLevelAbsent', () => { + expect(Either.getOrThrow(unifiedToJourneyConfig({ oidc: minimalOidc })).log).toBeUndefined(); + }); + + it('unifiedToJourneyConfig_CookieName_NotMappedToResult', () => { + const data = Either.getOrThrow( + unifiedToJourneyConfig({ + journey: { serverUrl: 'https://example.com/am', cookieName: 'iPlanetDirectoryPro' }, + oidc: minimalOidc, + }), + ) as unknown as Record; + expect(data['cookieName']).toBeUndefined(); + }); +}); + +describe('unifiedToDavinciConfig log mapping', () => { + it('unifiedToDavinciConfig_LogFieldMapped_ToLogLevel', () => { + expect(Either.getOrThrow(unifiedToDavinciConfig({ log: 'ERROR', oidc: minimalOidc })).log).toBe( + 'error', + ); + }); + + it('unifiedToDavinciConfig_NoLogField_LogLevelAbsent', () => { + expect(Either.getOrThrow(unifiedToDavinciConfig({ oidc: minimalOidc })).log).toBeUndefined(); + }); +}); + +describe('isUnifiedSdkConfig', () => { + it('isUnifiedSdkConfig_FullConfig_ReturnsTrue', () => { + expect(isUnifiedSdkConfig(fullConfig)).toBe(true); + }); + + it('isUnifiedSdkConfig_OidcBlockPresent_ReturnsTrue', () => { + expect( + isUnifiedSdkConfig({ oidc: { discoveryEndpoint: 'https://example.com/.well-known' } }), + ).toBe(true); + }); + + it('isUnifiedSdkConfig_JourneyBlockPresent_ReturnsTrue', () => { + expect(isUnifiedSdkConfig({ journey: { serverUrl: 'https://example.com/am' } })).toBe(true); + }); + + it('isUnifiedSdkConfig_JourneyOnlyConfig_ReturnsTrue', () => { + expect(isUnifiedSdkConfig(journeyOnlyConfig)).toBe(true); + }); + + it('isUnifiedSdkConfig_LegacyInternalConfig_ReturnsFalse', () => { + expect(isUnifiedSdkConfig({ clientId: 'x', serverConfig: { wellknown: 'x' } })).toBe(false); + }); + + it('isUnifiedSdkConfig_NullInput_ReturnsFalse', () => { + expect(isUnifiedSdkConfig(null)).toBe(false); + }); + + it('isUnifiedSdkConfig_StringInput_ReturnsFalse', () => { + expect(isUnifiedSdkConfig('not an object')).toBe(false); + }); + + it('isUnifiedSdkConfig_OidcNull_ReturnsFalse', () => { + expect(isUnifiedSdkConfig({ oidc: null })).toBe(false); + }); + + it('isUnifiedSdkConfig_JourneyPrimitive_ReturnsFalse', () => { + expect(isUnifiedSdkConfig({ journey: 42 })).toBe(false); + }); +}); + +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_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'); + }); +}); 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..d18f1d73a1 --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.types.ts @@ -0,0 +1,50 @@ +/* + * 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 { 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; +}; + +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..9ccaa0693e --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/config.utils.ts @@ -0,0 +1,290 @@ +/* + * 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 { Either } from 'effect'; + +import type { + UnifiedSdkConfig, + UnifiedOidcConfig, + OidcConfig, + JourneyClientConfig, + DaVinciConfig, + LogLevel, + ConfigValidationError, +} from './config.types.js'; + +function toMappedLogLevel(level: Uppercase): LogLevel { + return level.toLowerCase() as LogLevel; +} + +const REQUIRED_OIDC_FIELDS_STRICT = [ + 'clientId', + 'discoveryEndpoint', + 'scopes', + 'redirectUri', +] as const satisfies ReadonlyArray; + +const REQUIRED_OIDC_FIELDS_JOURNEY = ['discoveryEndpoint'] as const satisfies ReadonlyArray< + keyof UnifiedOidcConfig +>; + +function checkType(value: unknown, expected: string, field: string): ConfigValidationError | null { + if (expected === 'array') { + return Array.isArray(value) ? null : { field, message: `Expected array, got ${typeof value}` }; + } + return typeof value === expected + ? null + : { field, message: `Expected ${expected}, got ${typeof value}` }; +} + +export function validateUnifiedOidcConfig( + input: unknown, + prefix = 'oidc', + strict = false, +): Either.Either { + if (typeof input !== 'object' || input === null) { + return Either.left([{ field: prefix, message: `Expected object, got ${typeof input}` }]); + } + + const raw = input as Record; + const requiredFields = strict ? REQUIRED_OIDC_FIELDS_STRICT : REQUIRED_OIDC_FIELDS_JOURNEY; + + const requiredErrors = requiredFields.flatMap((field): ConfigValidationError[] => { + if (raw[field] === undefined || raw[field] === null) { + return [{ field: `${prefix}.${field}`, message: 'Required field is missing' }]; + } + const error = checkType( + raw[field], + field === 'scopes' ? 'array' : 'string', + `${prefix}.${field}`, + ); + return error ? [error] : []; + }); + + const nonStrictOptionalErrors = !strict + ? (['clientId', 'scopes', 'redirectUri'] as const).flatMap((field): ConfigValidationError[] => { + if (raw[field] === undefined || raw[field] === null) return []; + const error = checkType( + raw[field], + field === 'scopes' ? 'array' : 'string', + `${prefix}.${field}`, + ); + return error ? [error] : []; + }) + : []; + + const optionalStringFields = [ + 'signOutRedirectUri', + 'loginHint', + 'nonce', + 'display', + 'prompt', + 'uiLocales', + 'acrValues', + ] as const satisfies ReadonlyArray; + + const optionalStringErrors = optionalStringFields.flatMap((field): ConfigValidationError[] => { + if (raw[field] === undefined || raw[field] === null) return []; + const error = checkType(raw[field], 'string', `${prefix}.${field}`); + return error ? [error] : []; + }); + + const refreshThresholdError = + raw['refreshThreshold'] !== undefined && raw['refreshThreshold'] !== null + ? checkType(raw['refreshThreshold'], 'number', `${prefix}.refreshThreshold`) + : null; + + const additionalParametersError = + raw['additionalParameters'] !== undefined && raw['additionalParameters'] !== null + ? typeof raw['additionalParameters'] !== 'object' || + Array.isArray(raw['additionalParameters']) + ? ({ + field: `${prefix}.additionalParameters`, + message: `Expected object, got ${Array.isArray(raw['additionalParameters']) ? 'array' : typeof raw['additionalParameters']}`, + } satisfies ConfigValidationError) + : null + : null; + + const errors: ConfigValidationError[] = [ + ...requiredErrors, + ...nonStrictOptionalErrors, + ...optionalStringErrors, + ...(refreshThresholdError ? [refreshThresholdError] : []), + ...(additionalParametersError ? [additionalParametersError] : []), + ]; + + return errors.length > 0 + ? Either.left(errors) + : Either.right(raw as unknown as UnifiedOidcConfig); +} + +export function validateUnifiedSdkConfig( + input: unknown, + strictOidc = false, +): Either.Either { + if (typeof input !== 'object' || input === null) { + return Either.left([{ field: 'config', message: `Expected object, got ${typeof input}` }]); + } + + const raw = input as Record; + + const logError = + raw['log'] !== undefined && raw['log'] !== null ? checkType(raw['log'], 'string', 'log') : null; + + const timeoutError = + raw['timeout'] !== undefined && raw['timeout'] !== null + ? checkType(raw['timeout'], 'number', 'timeout') + : null; + + const journeyErrors: ConfigValidationError[] = (() => { + if (raw['journey'] === undefined || raw['journey'] === null) return []; + if (typeof raw['journey'] !== 'object' || Array.isArray(raw['journey'])) { + return [{ field: 'journey', message: `Expected object, got ${typeof raw['journey']}` }]; + } + const journey = raw['journey'] as Record; + const serverUrlError = + journey['serverUrl'] === undefined || journey['serverUrl'] === null + ? { field: 'journey.serverUrl', message: 'Required field is missing' } + : checkType(journey['serverUrl'], 'string', 'journey.serverUrl'); + + const optionalJourneyErrors = (['realm', 'cookieName'] as const).flatMap( + (field): ConfigValidationError[] => { + if (journey[field] === undefined || journey[field] === null) return []; + const error = checkType(journey[field], 'string', `journey.${field}`); + return error ? [error] : []; + }, + ); + + return [...(serverUrlError ? [serverUrlError] : []), ...optionalJourneyErrors]; + })(); + + const oidcErrors: ConfigValidationError[] = (() => { + if (raw['oidc'] === undefined || raw['oidc'] === null) return []; + const result = validateUnifiedOidcConfig(raw['oidc'], 'oidc', strictOidc); + return Either.isLeft(result) ? result.left : []; + })(); + + const errors: ConfigValidationError[] = [ + ...(logError ? [logError] : []), + ...(timeoutError ? [timeoutError] : []), + ...journeyErrors, + ...oidcErrors, + ]; + + return errors.length > 0 ? Either.left(errors) : Either.right(raw as unknown as UnifiedSdkConfig); +} + +export function unifiedToOidcConfig( + config: UnifiedSdkConfig, +): Either.Either { + if (!config.oidc) { + return Either.left({ field: 'oidc', message: 'Required block is missing' }); + } + const oidc = config.oidc; + const { clientId, redirectUri, scopes } = oidc; + if (!clientId || !redirectUri || !scopes) { + return Either.left({ + field: 'oidc', + message: 'clientId, redirectUri, and scopes are required', + }); + } + return Either.right({ + clientId, + redirectUri, + scope: 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) }), + }); +} + +export function unifiedToJourneyConfig( + config: UnifiedSdkConfig, +): Either.Either { + if (!config.oidc) { + return Either.left({ field: 'oidc', message: 'Required block is missing' }); + } + const oidc = config.oidc; + return Either.right({ + 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) }), + }); +} + +export function unifiedToDavinciConfig( + config: UnifiedSdkConfig, +): Either.Either { + if (!config.oidc) { + return Either.left({ field: 'oidc', message: 'Required block is missing' }); + } + const oidc = config.oidc; + return Either.right({ + 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) }), + }); +} + +export function isUnifiedSdkConfig(input: unknown): input is UnifiedSdkConfig { + if (typeof input !== 'object' || input === null) return false; + const raw = input as Record; + const oidcIsObject = 'oidc' in raw && typeof raw['oidc'] === 'object' && raw['oidc'] !== null; + const journeyIsObject = + 'journey' in raw && typeof raw['journey'] === 'object' && raw['journey'] !== null; + return oidcIsObject || journeyIsObject; +} + +function fromJson( + json: unknown, + strictOidc: boolean, + mapper: (config: UnifiedSdkConfig) => Either.Either, +): T { + const validation = validateUnifiedSdkConfig(json, strictOidc); + if (Either.isLeft(validation)) { + const messages = validation.left.map((e) => `${e.field}: ${e.message}`).join(', '); + throw new Error(`Invalid unified SDK config: ${messages}`); + } + const mapped = mapper(validation.right); + if (Either.isLeft(mapped)) { + throw new Error(`Invalid unified SDK config: ${mapped.left.field}: ${mapped.left.message}`); + } + return mapped.right; +} + +export const makeOidcConfig = (json: unknown): OidcConfig => + fromJson(json, true, unifiedToOidcConfig); + +export const makeJourneyConfig = (json: unknown): JourneyClientConfig => + fromJson(json, false, unifiedToJourneyConfig); + +export const makeDavinciConfig = (json: unknown): DaVinciConfig => + fromJson(json, true, unifiedToDavinciConfig); 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..b4f771d760 --- /dev/null +++ b/packages/sdk-utilities/src/lib/config/index.ts @@ -0,0 +1,9 @@ +/* + * 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'; 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: