diff --git a/.changeset/journey-client-config-alignment.md b/.changeset/journey-client-config-alignment.md new file mode 100644 index 0000000000..63a7254776 --- /dev/null +++ b/.changeset/journey-client-config-alignment.md @@ -0,0 +1,8 @@ +--- +'@forgerock/journey-client': patch +--- + +Extend `JourneyClientConfig` from `AsyncLegacyConfigOptions` so the same config object can be shared across journey-client, davinci-client, and oidc-client + +- `clientId`, `scope`, `redirectUri`, and other inherited properties are now accepted but ignored — a warning is logged when they are provided +- `serverConfig.wellknown` remains required diff --git a/packages/journey-client/src/lib/client.store.test.ts b/packages/journey-client/src/lib/client.store.test.ts index 5504347a17..f57a1c0f7c 100644 --- a/packages/journey-client/src/lib/client.store.test.ts +++ b/packages/journey-client/src/lib/client.store.test.ts @@ -359,6 +359,71 @@ describe('journey-client', () => { }); }); + describe('config property warnings', () => { + test('journey_ExtraConfigProperties_LogsWarning', async () => { + setupMockFetch(); + const customLogger = { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + }; + + await journey({ + config: { + serverConfig: { wellknown: mockWellknownUrl }, + clientId: 'test-client', + scope: 'openid', + redirectUri: 'https://example.com/callback', + }, + logger: { level: 'warn', custom: customLogger }, + }); + + expect(customLogger.warn).toHaveBeenCalledTimes(1); + expect(customLogger.warn).toHaveBeenCalledWith(expect.stringContaining('clientId')); + expect(customLogger.warn).toHaveBeenCalledWith(expect.stringContaining('scope')); + expect(customLogger.warn).toHaveBeenCalledWith(expect.stringContaining('redirectUri')); + }); + + test('journey_SingleIgnoredProperty_LogsWarning', async () => { + setupMockFetch(); + const customLogger = { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + }; + + await journey({ + config: { + serverConfig: { wellknown: mockWellknownUrl }, + clientId: 'test-client', + }, + logger: { level: 'warn', custom: customLogger }, + }); + + expect(customLogger.warn).toHaveBeenCalledTimes(1); + expect(customLogger.warn).toHaveBeenCalledWith(expect.stringContaining('clientId')); + }); + + test('journey_MinimalConfig_NoWarning', async () => { + setupMockFetch(); + const customLogger = { + error: vi.fn(), + warn: vi.fn(), + info: vi.fn(), + debug: vi.fn(), + }; + + await journey({ + config: { serverConfig: { wellknown: mockWellknownUrl } }, + logger: { level: 'warn', custom: customLogger }, + }); + + expect(customLogger.warn).not.toHaveBeenCalled(); + }); + }); + describe('subrealm inference', () => { test('journey_WellknownWithSubrealm_DerivesCorrectPaths', async () => { const alphaConfig: JourneyClientConfig = { diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 10fad05d89..5dc14ad4f4 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -50,7 +50,7 @@ export interface JourneyClient { * It uses AM-proprietary endpoints for callback-based authentication trees. * * @param options - Configuration options for the journey client - * @param options.config - Server configuration with required wellknown URL + * @param options.config - Configuration options (see {@link JourneyClientConfig}); only `serverConfig.wellknown` is required * @param options.requestMiddleware - Optional middleware for request customization * @param options.logger - Optional logger configuration * @returns A journey client instance @@ -86,6 +86,29 @@ export async function journey({ }): Promise { const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom }); + const ignoredProperties = [ + 'callbackFactory', + 'clientId', + 'middleware', + 'oauthThreshold', + 'platformHeader', + 'prefix', + 'realmPath', + 'redirectUri', + 'scope', + 'tokenStore', + 'tree', + 'type', + ] as const; + + const providedIgnored = ignoredProperties.filter((prop) => config[prop] !== undefined); + + if (providedIgnored.length > 0) { + log.warn( + `The following configuration properties are not used by journey-client and will be ignored: ${providedIgnored.join(', ')}`, + ); + } + const store = createJourneyStore({ requestMiddleware, logger: log }); const { wellknown } = config.serverConfig; diff --git a/packages/journey-client/src/lib/config.types.test-d.ts b/packages/journey-client/src/lib/config.types.test-d.ts new file mode 100644 index 0000000000..3b28813538 --- /dev/null +++ b/packages/journey-client/src/lib/config.types.test-d.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import { describe, expectTypeOf, it } from 'vitest'; +import type { + JourneyClientConfig, + JourneyServerConfig, + InternalJourneyClientConfig, +} from './config.types.js'; +import type { AsyncLegacyConfigOptions } from '@forgerock/sdk-types'; +import type { ResolvedServerConfig } from './wellknown.utils.js'; + +describe('Config Types', () => { + describe('JourneyClientConfig', () => { + it('should extend AsyncLegacyConfigOptions', () => { + expectTypeOf().toMatchTypeOf(); + }); + + it('should narrow serverConfig to JourneyServerConfig', () => { + expectTypeOf().toMatchTypeOf(); + expectTypeOf().toBeString(); + }); + + it('should reject config without wellknown', () => { + // @ts-expect-error - wellknown is required on serverConfig + const config: JourneyClientConfig = { serverConfig: {} }; + expectTypeOf(config).toMatchTypeOf(); + }); + + it('should allow AsyncLegacyConfigOptions properties', () => { + const config: JourneyClientConfig = { + clientId: 'test-client', + scope: 'openid profile', + redirectUri: 'https://app.example.com/callback', + serverConfig: { + wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration', + timeout: 30000, + }, + }; + expectTypeOf(config).toMatchTypeOf(); + }); + + it('should not require inherited properties like clientId', () => { + const config: JourneyClientConfig = { + serverConfig: { + wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration', + }, + }; + expectTypeOf(config).toMatchTypeOf(); + }); + + it('should have optional timeout on serverConfig', () => { + expectTypeOf().toHaveProperty('timeout'); + }); + }); + + describe('InternalJourneyClientConfig', () => { + it('should have ResolvedServerConfig', () => { + expectTypeOf() + .toHaveProperty('serverConfig') + .toMatchTypeOf(); + }); + + it('should have optional error', () => { + expectTypeOf().toHaveProperty('error').toBeNullable(); + }); + }); +}); diff --git a/packages/journey-client/src/lib/config.types.ts b/packages/journey-client/src/lib/config.types.ts index cecb97ea07..25eaeaa888 100644 --- a/packages/journey-client/src/lib/config.types.ts +++ b/packages/journey-client/src/lib/config.types.ts @@ -5,7 +5,7 @@ * of the MIT license. See the LICENSE file for details. */ -import type { GenericError } from '@forgerock/sdk-types'; +import type { AsyncLegacyConfigOptions, GenericError } from '@forgerock/sdk-types'; import type { ResolvedServerConfig } from './wellknown.utils.js'; /** @@ -17,11 +17,18 @@ import type { ResolvedServerConfig } from './wellknown.utils.js'; 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 = { @@ -31,7 +38,7 @@ export interface JourneyServerConfig { * }; * ``` */ -export interface JourneyClientConfig { +export interface JourneyClientConfig extends AsyncLegacyConfigOptions { serverConfig: JourneyServerConfig; }