Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/journey-client-config-alignment.md
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions packages/journey-client/src/lib/client.store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
25 changes: 24 additions & 1 deletion packages/journey-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,6 +86,29 @@ export async function journey<ActionType extends ActionTypes = ActionTypes>({
}): Promise<JourneyClient> {
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(', ')}`,
);
}
Comment on lines +89 to +110
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

serverConfig.timeout is silently ignored without a warning.

JourneyServerConfig now accepts timeout, but this warning path only checks top-level keys. Since only wellknown is read later, timeout is effectively ignored with no signal to callers.

🔧 Proposed fix
   const ignoredProperties = [
     'callbackFactory',
     'clientId',
     'middleware',
     'oauthThreshold',
     'platformHeader',
     'prefix',
     'realmPath',
     'redirectUri',
     'scope',
     'tokenStore',
     'tree',
     'type',
   ] as const;
 
-  const providedIgnored = ignoredProperties.filter((prop) => config[prop] !== undefined);
+  const providedIgnored = ignoredProperties.filter(
+    (prop) => Object.prototype.hasOwnProperty.call(config, prop) && config[prop] !== undefined,
+  );
+
+  if (config.serverConfig?.timeout !== undefined) {
+    providedIgnored.push('serverConfig.timeout' as (typeof ignoredProperties)[number]);
+  }
 
   if (providedIgnored.length > 0) {
     log.warn(
       `The following configuration properties are not used by journey-client and will be ignored: ${providedIgnored.join(', ')}`,
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/journey-client/src/lib/client.store.ts` around lines 89 - 110, The
code currently warns only for top-level ignored keys via
ignoredProperties/providedIgnored, so serverConfig.timeout is silently ignored;
update the check to detect nested serverConfig.timeout (e.g., inspect
config.serverConfig?.timeout) and include it in the warning (or add 'timeout' to
ignoredProperties when appropriate) so that the log.warn call reports when
timeout is provided but not used by journey-client; reference the existing
identifiers ignoredProperties, providedIgnored, config and the
wellknown/serverConfig usage to locate where to add the nested check and include
the key in the joined warning string.


const store = createJourneyStore({ requestMiddleware, logger: log });

const { wellknown } = config.serverConfig;
Expand Down
71 changes: 71 additions & 0 deletions packages/journey-client/src/lib/config.types.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<JourneyClientConfig>().toMatchTypeOf<AsyncLegacyConfigOptions>();
});

it('should narrow serverConfig to JourneyServerConfig', () => {
expectTypeOf<JourneyClientConfig['serverConfig']>().toMatchTypeOf<JourneyServerConfig>();
expectTypeOf<JourneyClientConfig['serverConfig']['wellknown']>().toBeString();
});

it('should reject config without wellknown', () => {
// @ts-expect-error - wellknown is required on serverConfig
const config: JourneyClientConfig = { serverConfig: {} };
expectTypeOf(config).toMatchTypeOf<JourneyClientConfig>();
});

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<JourneyClientConfig>();
});

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<JourneyClientConfig>();
});

it('should have optional timeout on serverConfig', () => {
expectTypeOf<JourneyClientConfig['serverConfig']>().toHaveProperty('timeout');
});
});

describe('InternalJourneyClientConfig', () => {
it('should have ResolvedServerConfig', () => {
expectTypeOf<InternalJourneyClientConfig>()
.toHaveProperty('serverConfig')
.toMatchTypeOf<ResolvedServerConfig>();
});

it('should have optional error', () => {
expectTypeOf<InternalJourneyClientConfig>().toHaveProperty('error').toBeNullable();
});
});
});
11 changes: 9 additions & 2 deletions packages/journey-client/src/lib/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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 = {
Expand All @@ -31,7 +38,7 @@ export interface JourneyServerConfig {
* };
* ```
*/
export interface JourneyClientConfig {
export interface JourneyClientConfig extends AsyncLegacyConfigOptions {
serverConfig: JourneyServerConfig;
}

Expand Down
Loading