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
29 changes: 28 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,33 @@ 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: string[] = ignoredProperties.filter((prop) => config[prop] !== undefined);

if (config.serverConfig?.timeout !== undefined) {
providedIgnored.push('serverConfig.timeout');
}

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;
Expand Down
72 changes: 72 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,72 @@
/*
* 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>().toExtend<AsyncLegacyConfigOptions>();
});

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

it('should reject config without wellknown', () => {
// @ts-expect-error - wellknown is required on serverConfig
const config: JourneyClientConfig = { serverConfig: {} };
// This assertion verifies the variable's runtime shape doesn't satisfy the full type.
expectTypeOf(config).not.toMatchObjectType<Required<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).toExtend<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).toExtend<JourneyClientConfig>();
});

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

describe('InternalJourneyClientConfig', () => {
it('should have ResolvedServerConfig', () => {
expectTypeOf<InternalJourneyClientConfig>()
.toHaveProperty('serverConfig')
.toExtend<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