Skip to content

Commit dd2a363

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 354a238 commit dd2a363

29 files changed

Lines changed: 2162 additions & 636 deletions

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

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

281281
// @public
282282
export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
283-
config: DaVinciConfig;
283+
config: DaVinciConfig | Record<string, unknown>;
284284
requestMiddleware?: RequestMiddleware<ActionType>[];
285285
logger?: {
286286
level: LogLevel;
@@ -294,11 +294,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
294294
resume: (input: {
295295
continueToken: string;
296296
}) => Promise<InternalErrorResponse | NodeStates>;
297-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
297+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
298298
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
299299
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
300300
pollStatus: (collector: PollingCollector) => Poller;
301301
getClient: () => {
302+
status: "start";
303+
} | {
302304
action: string;
303305
collectors: Collectors[];
304306
description?: string;
@@ -312,8 +314,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
312314
status: "error";
313315
} | {
314316
status: "failure";
315-
} | {
316-
status: "start";
317317
} | {
318318
authorization?: {
319319
code?: string;
@@ -324,7 +324,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
324324
getCollectors: () => Collectors[];
325325
getError: () => DaVinciError | null;
326326
getErrorCollectors: () => CollectorErrors[];
327-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
327+
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
328328
getServer: () => {
329329
_links?: Links;
330330
id?: string;
@@ -333,6 +333,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
333333
href?: string;
334334
eventName?: string;
335335
status: "continue";
336+
} | {
337+
status: "start";
336338
} | {
337339
_links?: Links;
338340
eventName?: string;
@@ -348,8 +350,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
348350
interactionId?: string;
349351
interactionToken?: string;
350352
status: "failure";
351-
} | {
352-
status: "start";
353353
} | {
354354
_links?: Links;
355355
eventName?: string;
@@ -599,6 +599,8 @@ export type DavinciClient = Awaited<ReturnType<typeof davinci>>;
599599

600600
// @public (undocumented)
601601
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
602+
// (undocumented)
603+
log?: LogLevel;
602604
// (undocumented)
603605
responseType?: string;
604606
}

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

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

281281
// @public
282282
export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
283-
config: DaVinciConfig;
283+
config: DaVinciConfig | Record<string, unknown>;
284284
requestMiddleware?: RequestMiddleware<ActionType>[];
285285
logger?: {
286286
level: LogLevel;
@@ -294,11 +294,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
294294
resume: (input: {
295295
continueToken: string;
296296
}) => Promise<InternalErrorResponse | NodeStates>;
297-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
297+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
298298
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
299299
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
300300
pollStatus: (collector: PollingCollector) => Poller;
301301
getClient: () => {
302+
status: "start";
303+
} | {
302304
action: string;
303305
collectors: Collectors[];
304306
description?: string;
@@ -312,8 +314,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
312314
status: "error";
313315
} | {
314316
status: "failure";
315-
} | {
316-
status: "start";
317317
} | {
318318
authorization?: {
319319
code?: string;
@@ -324,7 +324,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
324324
getCollectors: () => Collectors[];
325325
getError: () => DaVinciError | null;
326326
getErrorCollectors: () => CollectorErrors[];
327-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
327+
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
328328
getServer: () => {
329329
_links?: Links;
330330
id?: string;
@@ -333,6 +333,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
333333
href?: string;
334334
eventName?: string;
335335
status: "continue";
336+
} | {
337+
status: "start";
336338
} | {
337339
_links?: Links;
338340
eventName?: string;
@@ -348,8 +350,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
348350
interactionId?: string;
349351
interactionToken?: string;
350352
status: "failure";
351-
} | {
352-
status: "start";
353353
} | {
354354
_links?: Links;
355355
eventName?: string;
@@ -599,6 +599,8 @@ export type DavinciClient = Awaited<ReturnType<typeof davinci>>;
599599

600600
// @public (undocumented)
601601
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
602+
// (undocumented)
603+
log?: LogLevel;
602604
// (undocumented)
603605
responseType?: string;
604606
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// @vitest-environment node
2+
/*
3+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
4+
*
5+
* This software may be modified and distributed under the terms
6+
* of the MIT license. See the LICENSE file for details.
7+
*/
8+
9+
import { describe, expect, it, vi } from 'vitest';
10+
import { davinci } from './client.store.js';
11+
import type { DaVinciConfig } from './config.types.js';
12+
13+
const storageStore: Record<string, string> = {};
14+
const mockStorage = {
15+
getItem: (key: string) => storageStore[key] ?? null,
16+
setItem: (key: string, value: string) => {
17+
storageStore[key] = value;
18+
},
19+
removeItem: (key: string) => {
20+
delete storageStore[key];
21+
},
22+
clear: () => Object.keys(storageStore).forEach((k) => delete storageStore[k]),
23+
length: 0,
24+
key: () => null,
25+
};
26+
27+
Object.defineProperty(global, 'localStorage', { value: mockStorage, writable: true });
28+
Object.defineProperty(global, 'sessionStorage', { value: mockStorage, writable: true });
29+
30+
const mockWellknownUrl = 'https://auth.pingone.com/env-id/as/.well-known/openid-configuration';
31+
32+
const mockWellknownResponse = {
33+
issuer: 'https://auth.pingone.com/env-id/as',
34+
authorization_endpoint: 'https://auth.pingone.com/env-id/as/authorize',
35+
token_endpoint: 'https://auth.pingone.com/env-id/as/token',
36+
userinfo_endpoint: 'https://auth.pingone.com/env-id/as/userinfo',
37+
end_session_endpoint: 'https://auth.pingone.com/env-id/as/signoff',
38+
introspection_endpoint: 'https://auth.pingone.com/env-id/as/introspect',
39+
revocation_endpoint: 'https://auth.pingone.com/env-id/as/revoke',
40+
};
41+
42+
const mockFetch = vi.fn();
43+
global.fetch = mockFetch;
44+
45+
function setupMockFetch() {
46+
mockFetch.mockImplementation((input: RequestInfo | URL) => {
47+
const url =
48+
typeof input === 'string' ? input : input instanceof Request ? input.url : input.toString();
49+
if (url.includes('.well-known/openid-configuration')) {
50+
return Promise.resolve(new Response(JSON.stringify(mockWellknownResponse)));
51+
}
52+
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
53+
});
54+
}
55+
56+
describe('unified JSON config entry', () => {
57+
it('accepts unified JSON config and initializes successfully', async () => {
58+
setupMockFetch();
59+
60+
const unifiedConfig = {
61+
oidc: {
62+
clientId: '123456789',
63+
discoveryEndpoint: mockWellknownUrl,
64+
scopes: ['openid', 'profile'],
65+
redirectUri: 'https://example.com/callback',
66+
},
67+
} as unknown as DaVinciConfig;
68+
69+
const client = await davinci({ config: unifiedConfig });
70+
expect(client).toHaveProperty('flow');
71+
expect(client).toHaveProperty('subscribe');
72+
});
73+
74+
it('throws when unified JSON config has missing required field', async () => {
75+
const invalidConfig = {
76+
oidc: {
77+
// clientId missing
78+
discoveryEndpoint: mockWellknownUrl,
79+
scopes: ['openid'],
80+
redirectUri: 'https://example.com/callback',
81+
},
82+
} as unknown as DaVinciConfig;
83+
84+
await expect(davinci({ config: invalidConfig })).rejects.toThrow(/Invalid unified SDK config/);
85+
});
86+
87+
it('throws when unified JSON config has wrong field type', async () => {
88+
const invalidConfig = {
89+
oidc: {
90+
clientId: '123',
91+
discoveryEndpoint: mockWellknownUrl,
92+
scopes: 'openid', // should be array
93+
redirectUri: 'https://example.com/callback',
94+
},
95+
} as unknown as DaVinciConfig;
96+
97+
await expect(davinci({ config: invalidConfig })).rejects.toThrow(/Invalid unified SDK config/);
98+
});
99+
});

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)