Skip to content

Commit 2a4fee2

Browse files
committed
feat(ping-sdk): standardize sdk configuration
feat(sdk-utilities): add pure unified config validation feat(sdk-utilities): add unified config mapping functions feat(sdk-utilities): export config module from sdk-utilities test(sdk-utilities): add unit tests for config mapping and validation feat(sdk-types): add OIDC authorize params to GetAuthorizationUrlOptions feat(sdk-oidc): wire OIDC authorize params into buildAuthorizeParams feat(oidc-client): add signOutRedirectUri to endSession URL feat(oidc-client): accept unified JSON config in oidc factory feat(journey-client): accept unified JSON config in journey factory feat(davinci-client): accept unified JSON config in davinci factory chore: update api-reports for unified config param widening feat(sdk-utilities): align unified config schema with new journey sub-object refactor(sdk-utilities): mapper functions return ConfigMappingResult with single error fix(sdk-utilities): map log field and use config.log in factories fix(sdk-utilities): tighten isUnifiedSdkConfig discriminant to require object values fix(oidc-client): use throw instead of Promise.reject in unified config error paths refactor(oidc-client): migrate logout.request.test.ts to it + Micro.runPromise pattern docs(oidc-client): add JSDoc to OidcConfig and all new fields
1 parent 48279d3 commit 2a4fee2

30 files changed

Lines changed: 2160 additions & 636 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@forgerock/sdk-utilities': minor
3+
'@forgerock/sdk-types': minor
4+
'@forgerock/sdk-oidc': minor
5+
'@forgerock/oidc-client': minor
6+
'@forgerock/journey-client': minor
7+
'@forgerock/davinci-client': minor
8+
---
9+
10+
Add unified cross-platform SDK configuration support
11+
12+
All three client factories (`oidc`, `journey`, `davinci`) now accept the cross-platform unified JSON config schema alongside the existing internal config shape. Pass a unified config object and the factory maps, validates, and rejects on invalid input.
13+
14+
**New in `@forgerock/sdk-utilities`:**
15+
16+
- `UnifiedSdkConfig`, `UnifiedOidcConfig`, `UnifiedJourneyConfig` types
17+
- `validateUnifiedSdkConfig` / `validateUnifiedOidcConfig` — pure validation returning `ConfigValidationResult<T>` (no throws)
18+
- `unifiedToOidcConfig`, `unifiedToJourneyConfig`, `unifiedToDavinciConfig` — pure mapping functions
19+
- `isUnifiedSdkConfig` discriminator (`'oidc' in input || 'journey' in input`)
20+
21+
**New in `@forgerock/sdk-types`:**
22+
23+
- `GetAuthorizationUrlOptions` extended with `loginHint`, `nonce`, `display`, `uiLocales`, `acrValues`; `prompt` widened to include `'select_account'`
24+
25+
**New in `@forgerock/sdk-oidc`:**
26+
27+
- `buildAuthorizeParams` forwards all new OIDC authorize params into the URL
28+
29+
**New in `@forgerock/oidc-client`:**
30+
31+
- `oidc()` factory accepts unified JSON config; rejects Promise on validation failure
32+
- `endSession` appends `post_logout_redirect_uri` when `signOutRedirectUri` is set
33+
- Authorize URL construction forwards `loginHint`, `state`, `nonce`, `display`, `prompt`, `uiLocales`, `acrValues`, `additionalParameters`
34+
35+
**New in `@forgerock/journey-client`:**
36+
37+
- `journey()` factory accepts unified JSON config; throws on validation failure
38+
39+
**New in `@forgerock/davinci-client`:**
40+
41+
- `davinci()` factory accepts unified JSON config; throws on validation failure

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export type CustomPollingStatus = string & {};
275275

276276
// @public
277277
export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
278-
config: DaVinciConfig;
278+
config: DaVinciConfig | Record<string, unknown>;
279279
requestMiddleware?: RequestMiddleware<ActionType>[];
280280
logger?: {
281281
level: LogLevel;
@@ -289,11 +289,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
289289
resume: (input: {
290290
continueToken: string;
291291
}) => Promise<InternalErrorResponse | NodeStates>;
292-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
292+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
293293
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
294294
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
295295
pollStatus: (collector: PollingCollector) => Poller;
296296
getClient: () => {
297+
status: "start";
298+
} | {
297299
action: string;
298300
collectors: Collectors[];
299301
description?: string;
@@ -307,8 +309,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
307309
status: "error";
308310
} | {
309311
status: "failure";
310-
} | {
311-
status: "start";
312312
} | {
313313
authorization?: {
314314
code?: string;
@@ -319,7 +319,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
319319
getCollectors: () => Collectors[];
320320
getError: () => DaVinciError | null;
321321
getErrorCollectors: () => CollectorErrors[];
322-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
322+
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
323323
getServer: () => {
324324
_links?: Links;
325325
id?: string;
@@ -328,6 +328,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
328328
href?: string;
329329
eventName?: string;
330330
status: "continue";
331+
} | {
332+
status: "start";
331333
} | {
332334
_links?: Links;
333335
eventName?: string;
@@ -343,8 +345,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
343345
interactionId?: string;
344346
interactionToken?: string;
345347
status: "failure";
346-
} | {
347-
status: "start";
348348
} | {
349349
_links?: Links;
350350
eventName?: string;
@@ -508,6 +508,8 @@ export type DavinciClient = Awaited<ReturnType<typeof davinci>>;
508508

509509
// @public (undocumented)
510510
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
511+
// (undocumented)
512+
log?: LogLevel;
511513
// (undocumented)
512514
responseType?: string;
513515
}

packages/davinci-client/api-report/davinci-client.types.api.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export type CustomPollingStatus = string & {};
275275

276276
// @public
277277
export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
278-
config: DaVinciConfig;
278+
config: DaVinciConfig | Record<string, unknown>;
279279
requestMiddleware?: RequestMiddleware<ActionType>[];
280280
logger?: {
281281
level: LogLevel;
@@ -289,11 +289,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
289289
resume: (input: {
290290
continueToken: string;
291291
}) => Promise<InternalErrorResponse | NodeStates>;
292-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
292+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
293293
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
294294
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
295295
pollStatus: (collector: PollingCollector) => Poller;
296296
getClient: () => {
297+
status: "start";
298+
} | {
297299
action: string;
298300
collectors: Collectors[];
299301
description?: string;
@@ -307,8 +309,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
307309
status: "error";
308310
} | {
309311
status: "failure";
310-
} | {
311-
status: "start";
312312
} | {
313313
authorization?: {
314314
code?: string;
@@ -319,7 +319,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
319319
getCollectors: () => Collectors[];
320320
getError: () => DaVinciError | null;
321321
getErrorCollectors: () => CollectorErrors[];
322-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
322+
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
323323
getServer: () => {
324324
_links?: Links;
325325
id?: string;
@@ -328,6 +328,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
328328
href?: string;
329329
eventName?: string;
330330
status: "continue";
331+
} | {
332+
status: "start";
331333
} | {
332334
_links?: Links;
333335
eventName?: string;
@@ -343,8 +345,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
343345
interactionId?: string;
344346
interactionToken?: string;
345347
status: "failure";
346-
} | {
347-
status: "start";
348348
} | {
349349
_links?: Links;
350350
eventName?: string;
@@ -508,6 +508,8 @@ export type DavinciClient = Awaited<ReturnType<typeof davinci>>;
508508

509509
// @public (undocumented)
510510
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
511+
// (undocumented)
512+
log?: LogLevel;
511513
// (undocumented)
512514
responseType?: string;
513515
}

packages/davinci-client/src/lib/client.store.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,59 @@ describe('davinci client — cache', () => {
181181
});
182182
});
183183
});
184+
185+
// ---------------------------------------------------------------------------
186+
187+
describe('unified JSON config entry', () => {
188+
beforeEach(() => {
189+
vi.stubGlobal('localStorage', makeStorageStub());
190+
vi.stubGlobal('sessionStorage', makeStorageStub());
191+
mockFetchImplementation();
192+
});
193+
194+
afterEach(() => {
195+
vi.unstubAllGlobals();
196+
vi.restoreAllMocks();
197+
});
198+
199+
it('accepts unified JSON config and initializes successfully', async () => {
200+
const unifiedConfig = {
201+
oidc: {
202+
clientId: '123456789',
203+
discoveryEndpoint: TEST_WELLKNOWN_URL,
204+
scopes: ['openid', 'profile'],
205+
redirectUri: 'https://example.com/callback',
206+
},
207+
} as unknown as DaVinciConfig;
208+
209+
const client = await davinci({ config: unifiedConfig });
210+
expect(client).toHaveProperty('flow');
211+
expect(client).toHaveProperty('subscribe');
212+
});
213+
214+
it('throws when unified JSON config has missing required field', async () => {
215+
const invalidConfig = {
216+
oidc: {
217+
// clientId missing
218+
discoveryEndpoint: TEST_WELLKNOWN_URL,
219+
scopes: ['openid'],
220+
redirectUri: 'https://example.com/callback',
221+
},
222+
} as unknown as DaVinciConfig;
223+
224+
await expect(davinci({ config: invalidConfig })).rejects.toThrow(/Invalid unified SDK config/);
225+
});
226+
227+
it('throws when unified JSON config has wrong field type', async () => {
228+
const invalidConfig = {
229+
oidc: {
230+
clientId: '123',
231+
discoveryEndpoint: TEST_WELLKNOWN_URL,
232+
scopes: 'openid', // should be array
233+
redirectUri: 'https://example.com/callback',
234+
},
235+
} as unknown as DaVinciConfig;
236+
237+
await expect(davinci({ config: invalidConfig })).rejects.toThrow(/Invalid unified SDK config/);
238+
});
239+
});

packages/davinci-client/src/lib/client.store.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { Micro } from 'effect';
88
import { exitIsFail, exitIsSuccess } from 'effect/Micro';
99
import { type CustomLogger, logger as loggerFn, type LogLevel } from '@forgerock/sdk-logger';
1010
import { createStorage } from '@forgerock/storage';
11-
import { isGenericError, createWellknownError } from '@forgerock/sdk-utilities';
11+
import {
12+
isGenericError,
13+
isUnifiedSdkConfig,
14+
unifiedToDavinciConfig,
15+
validateUnifiedSdkConfig,
16+
createWellknownError,
17+
} from '@forgerock/sdk-utilities';
1218

1319
/**
1420
* Import RTK slices and api
@@ -66,18 +72,38 @@ import type { ContinueNode, StartNode } from './node.types.js';
6672
* @returns {Observable} - an observable client for DaVinci flows
6773
*/
6874
export async function davinci<ActionType extends ActionTypes = ActionTypes>({
69-
config,
75+
config: rawConfig,
7076
requestMiddleware,
7177
logger,
7278
}: {
73-
config: DaVinciConfig;
79+
config: DaVinciConfig | Record<string, unknown>;
7480
requestMiddleware?: RequestMiddleware<ActionType>[];
7581
logger?: {
7682
level: LogLevel;
7783
custom?: CustomLogger;
7884
};
7985
}) {
80-
const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom });
86+
let config: DaVinciConfig;
87+
88+
if (isUnifiedSdkConfig(rawConfig)) {
89+
const validation = validateUnifiedSdkConfig(rawConfig, true);
90+
if (!validation.success) {
91+
const messages = validation.errors.map((e) => `${e.field}: ${e.message}`).join(', ');
92+
throw new Error(`Invalid unified SDK config: ${messages}`);
93+
}
94+
const mapped = unifiedToDavinciConfig(validation.data);
95+
if (!mapped.success) {
96+
throw new Error(`Invalid unified SDK config: ${mapped.error.field}: ${mapped.error.message}`);
97+
}
98+
config = mapped.data as DaVinciConfig;
99+
} else {
100+
config = rawConfig as DaVinciConfig;
101+
}
102+
103+
const log = loggerFn({
104+
level: logger?.level ?? config.log ?? 'error',
105+
custom: logger?.custom,
106+
});
81107
const store = createClientStore({ requestMiddleware, logger: log });
82108
const serverInfo = createStorage<ContinueNode['server']>({
83109
type: 'localStorage',

packages/davinci-client/src/lib/config.types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
*/
77

88
import type { AsyncLegacyConfigOptions, WellknownResponse } from '@forgerock/sdk-types';
9+
import type { LogLevel } from '@forgerock/sdk-logger';
910

1011
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
1112
responseType?: string;
13+
log?: LogLevel;
1214
}
1315

1416
export interface InternalDaVinciConfig extends DaVinciConfig {

packages/journey-client/api-report/journey-client.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export { isValidWellknownUrl }
176176

177177
// @public
178178
export function journey<ActionType extends ActionTypes = ActionTypes>(input: {
179-
config: JourneyClientConfig;
179+
config: JourneyClientConfig | Record<string, unknown>;
180180
requestMiddleware?: RequestMiddleware<ActionType>[];
181181
logger?: {
182182
level: LogLevel;
@@ -204,6 +204,8 @@ export interface JourneyClient {
204204

205205
// @public
206206
export interface JourneyClientConfig extends AsyncLegacyConfigOptions {
207+
// (undocumented)
208+
log?: LogLevel;
207209
// (undocumented)
208210
serverConfig: JourneyServerConfig;
209211
}

packages/journey-client/api-report/journey-client.types.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ export interface JourneyClient {
191191

192192
// @public
193193
export interface JourneyClientConfig extends AsyncLegacyConfigOptions {
194+
// (undocumented)
195+
log?: LogLevel;
194196
// (undocumented)
195197
serverConfig: JourneyServerConfig;
196198
}

packages/journey-client/src/lib/client.store.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,4 +502,50 @@ describe('journey-client', () => {
502502
expect(request.url).toBe('https://test.com/am/json/realms/root/realms/alpha/authenticate');
503503
});
504504
});
505+
506+
describe('unified JSON config entry', () => {
507+
test('accepts unified JSON config and initializes successfully', async () => {
508+
setupMockFetch();
509+
510+
const unifiedConfig = {
511+
oidc: {
512+
clientId: 'ignored-by-journey',
513+
discoveryEndpoint: mockWellknownUrl,
514+
scopes: ['openid'],
515+
redirectUri: 'https://example.com/callback',
516+
},
517+
} as unknown as JourneyClientConfig;
518+
519+
const client = await journey({ config: unifiedConfig });
520+
expect(client).toHaveProperty('start');
521+
expect(client).toHaveProperty('next');
522+
});
523+
524+
test('throws when unified JSON config has missing required field', async () => {
525+
const invalidConfig = {
526+
oidc: {
527+
// discoveryEndpoint missing — required even for journey
528+
},
529+
} as unknown as JourneyClientConfig;
530+
531+
await expect(journey({ config: invalidConfig })).rejects.toThrow(
532+
/Invalid unified SDK config/,
533+
);
534+
});
535+
536+
test('throws when unified JSON config has wrong field type', async () => {
537+
const invalidConfig = {
538+
oidc: {
539+
clientId: '123',
540+
discoveryEndpoint: mockWellknownUrl,
541+
scopes: 'openid', // should be array
542+
redirectUri: 'https://example.com/callback',
543+
},
544+
} as unknown as JourneyClientConfig;
545+
546+
await expect(journey({ config: invalidConfig })).rejects.toThrow(
547+
/Invalid unified SDK config/,
548+
);
549+
});
550+
});
505551
});

0 commit comments

Comments
 (0)