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
41 changes: 41 additions & 0 deletions .changeset/sdks-5067-unified-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@forgerock/sdk-utilities': minor
'@forgerock/sdk-types': minor
'@forgerock/sdk-oidc': minor
'@forgerock/oidc-client': minor
'@forgerock/journey-client': minor
'@forgerock/davinci-client': minor
---

Add unified cross-platform SDK configuration support

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.

**New in `@forgerock/sdk-utilities`:**

- `UnifiedSdkConfig`, `UnifiedOidcConfig`, `UnifiedJourneyConfig` types
- `validateUnifiedSdkConfig` / `validateUnifiedOidcConfig` — pure validation returning `ConfigValidationResult<T>` (no throws)
- `unifiedToOidcConfig`, `unifiedToJourneyConfig`, `unifiedToDavinciConfig` — pure mapping functions
- `isUnifiedSdkConfig` discriminator (`'oidc' in input || 'journey' in input`)

**New in `@forgerock/sdk-types`:**

- `GetAuthorizationUrlOptions` extended with `loginHint`, `nonce`, `display`, `uiLocales`, `acrValues`; `prompt` widened to include `'select_account'`

**New in `@forgerock/sdk-oidc`:**

- `buildAuthorizeParams` forwards all new OIDC authorize params into the URL

**New in `@forgerock/oidc-client`:**

- `oidc()` factory accepts unified JSON config; rejects Promise on validation failure
- `endSession` appends `post_logout_redirect_uri` when `signOutRedirectUri` is set
- Authorize URL construction forwards `loginHint`, `state`, `nonce`, `display`, `prompt`, `uiLocales`, `acrValues`, `additionalParameters`

**New in `@forgerock/journey-client`:**

- `journey()` factory accepts unified JSON config; throws on validation failure

**New in `@forgerock/davinci-client`:**

- `davinci()` factory accepts unified JSON config; throws on validation failure
16 changes: 9 additions & 7 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export type CustomPollingStatus = string & {};

// @public
export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
config: DaVinciConfig;
config: DaVinciConfig | Record<string, unknown>;
requestMiddleware?: RequestMiddleware<ActionType>[];
logger?: {
level: LogLevel;
Expand All @@ -289,11 +289,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
resume: (input: {
continueToken: string;
}) => Promise<InternalErrorResponse | NodeStates>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
pollStatus: (collector: PollingCollector) => Poller;
getClient: () => {
status: "start";
} | {
action: string;
collectors: Collectors[];
description?: string;
Expand All @@ -307,8 +309,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
status: "error";
} | {
status: "failure";
} | {
status: "start";
} | {
authorization?: {
code?: string;
Expand All @@ -319,7 +319,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
getCollectors: () => Collectors[];
getError: () => DaVinciError | null;
getErrorCollectors: () => CollectorErrors[];
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
getServer: () => {
_links?: Links;
id?: string;
Expand All @@ -328,6 +328,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
href?: string;
eventName?: string;
status: "continue";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand All @@ -343,8 +345,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
interactionId?: string;
interactionToken?: string;
status: "failure";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand Down Expand Up @@ -508,6 +508,8 @@ export type DavinciClient = Awaited<ReturnType<typeof davinci>>;

// @public (undocumented)
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
// (undocumented)
log?: LogLevel;
// (undocumented)
responseType?: string;
}
Expand Down
16 changes: 9 additions & 7 deletions packages/davinci-client/api-report/davinci-client.types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export type CustomPollingStatus = string & {};

// @public
export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
config: DaVinciConfig;
config: DaVinciConfig | Record<string, unknown>;
requestMiddleware?: RequestMiddleware<ActionType>[];
logger?: {
level: LogLevel;
Expand All @@ -289,11 +289,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
resume: (input: {
continueToken: string;
}) => Promise<InternalErrorResponse | NodeStates>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
pollStatus: (collector: PollingCollector) => Poller;
getClient: () => {
status: "start";
} | {
action: string;
collectors: Collectors[];
description?: string;
Expand All @@ -307,8 +309,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
status: "error";
} | {
status: "failure";
} | {
status: "start";
} | {
authorization?: {
code?: string;
Expand All @@ -319,7 +319,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
getCollectors: () => Collectors[];
getError: () => DaVinciError | null;
getErrorCollectors: () => CollectorErrors[];
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
getServer: () => {
_links?: Links;
id?: string;
Expand All @@ -328,6 +328,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
href?: string;
eventName?: string;
status: "continue";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand All @@ -343,8 +345,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
interactionId?: string;
interactionToken?: string;
status: "failure";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand Down Expand Up @@ -508,6 +508,8 @@ export type DavinciClient = Awaited<ReturnType<typeof davinci>>;

// @public (undocumented)
export interface DaVinciConfig extends AsyncLegacyConfigOptions {
// (undocumented)
log?: LogLevel;
// (undocumented)
responseType?: string;
}
Expand Down
56 changes: 56 additions & 0 deletions packages/davinci-client/src/lib/client.store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,59 @@ describe('davinci client — cache', () => {
});
});
});

// ---------------------------------------------------------------------------

describe('unified JSON config entry', () => {
beforeEach(() => {
vi.stubGlobal('localStorage', makeStorageStub());
vi.stubGlobal('sessionStorage', makeStorageStub());
mockFetchImplementation();
});

afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

it('accepts unified JSON config and initializes successfully', async () => {
const unifiedConfig = {
oidc: {
clientId: '123456789',
discoveryEndpoint: TEST_WELLKNOWN_URL,
scopes: ['openid', 'profile'],
redirectUri: 'https://example.com/callback',
},
} as unknown as DaVinciConfig;

const client = await davinci({ config: unifiedConfig });
expect(client).toHaveProperty('flow');
expect(client).toHaveProperty('subscribe');
});

it('throws when unified JSON config has missing required field', async () => {
const invalidConfig = {
oidc: {
// clientId missing
discoveryEndpoint: TEST_WELLKNOWN_URL,
scopes: ['openid'],
redirectUri: 'https://example.com/callback',
},
} as unknown as DaVinciConfig;

await expect(davinci({ config: invalidConfig })).rejects.toThrow(/Invalid unified SDK config/);
});

it('throws when unified JSON config has wrong field type', async () => {
const invalidConfig = {
oidc: {
clientId: '123',
discoveryEndpoint: TEST_WELLKNOWN_URL,
scopes: 'openid', // should be array
redirectUri: 'https://example.com/callback',
},
} as unknown as DaVinciConfig;

await expect(davinci({ config: invalidConfig })).rejects.toThrow(/Invalid unified SDK config/);
});
});
34 changes: 30 additions & 4 deletions packages/davinci-client/src/lib/client.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { Micro } from 'effect';
import { exitIsFail, exitIsSuccess } from 'effect/Micro';
import { type CustomLogger, logger as loggerFn, type LogLevel } from '@forgerock/sdk-logger';
import { createStorage } from '@forgerock/storage';
import { isGenericError, createWellknownError } from '@forgerock/sdk-utilities';
import {
isGenericError,
isUnifiedSdkConfig,
unifiedToDavinciConfig,
validateUnifiedSdkConfig,
createWellknownError,
} from '@forgerock/sdk-utilities';

/**
* Import RTK slices and api
Expand Down Expand Up @@ -66,18 +72,38 @@ import type { ContinueNode, StartNode } from './node.types.js';
* @returns {Observable} - an observable client for DaVinci flows
*/
export async function davinci<ActionType extends ActionTypes = ActionTypes>({
config,
config: rawConfig,
requestMiddleware,
logger,
}: {
config: DaVinciConfig;
config: DaVinciConfig | Record<string, unknown>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In Andy's design doc, he suggests initializing the client through a new property called json which contains the json config. Is there a reason why you deviated from this? I think it may be a good idea to distinguish between the standard config and json config. The typing could also be a little more strict than Record<string, unknown>. If we have agreed on a standard JSON config as a team then we should be able to create an interface for it. This also removes the need for a isUnifiedSdkConfig utility.

requestMiddleware?: RequestMiddleware<ActionType>[];
logger?: {
level: LogLevel;
custom?: CustomLogger;
};
}) {
const log = loggerFn({ level: logger?.level || 'error', custom: logger?.custom });
let config: DaVinciConfig;

if (isUnifiedSdkConfig(rawConfig)) {
const validation = validateUnifiedSdkConfig(rawConfig, true);
if (!validation.success) {
const messages = validation.errors.map((e) => `${e.field}: ${e.message}`).join(', ');
throw new Error(`Invalid unified SDK config: ${messages}`);
}
const mapped = unifiedToDavinciConfig(validation.data);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure if "unified" is referring the "Unified SDK" here or the "Unified JSON Config". Can we rename it jsonToDavinciConfig or something? I believe we are trying to move away from calling it the "Unified SDK" and this may even change in the future.

if (!mapped.success) {
throw new Error(`Invalid unified SDK config: ${mapped.error.field}: ${mapped.error.message}`);
}
config = mapped.data as DaVinciConfig;
} else {
config = rawConfig as DaVinciConfig;
}

const log = loggerFn({
level: logger?.level ?? config.log ?? 'error',
custom: logger?.custom,
});
Comment on lines +86 to +106

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i'm not a huge fan of this here. I think we should maybe write a separate validation function if we need to do some validation but i'm concerned about the type of Record<string,unknown> it will break the entire purpose of typing configs.

const store = createClientStore({ requestMiddleware, logger: log });
const serverInfo = createStorage<ContinueNode['server']>({
type: 'localStorage',
Expand Down
2 changes: 2 additions & 0 deletions packages/davinci-client/src/lib/config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
*/

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

export interface DaVinciConfig extends AsyncLegacyConfigOptions {
responseType?: string;
log?: LogLevel;
}

export interface InternalDaVinciConfig extends DaVinciConfig {
Expand Down
4 changes: 3 additions & 1 deletion packages/journey-client/api-report/journey-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export { isValidWellknownUrl }

// @public
export function journey<ActionType extends ActionTypes = ActionTypes>(input: {
config: JourneyClientConfig;
config: JourneyClientConfig | Record<string, unknown>;
requestMiddleware?: RequestMiddleware<ActionType>[];
logger?: {
level: LogLevel;
Expand Down Expand Up @@ -204,6 +204,8 @@ export interface JourneyClient {

// @public
export interface JourneyClientConfig extends AsyncLegacyConfigOptions {
// (undocumented)
log?: LogLevel;
// (undocumented)
serverConfig: JourneyServerConfig;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ export interface JourneyClient {

// @public
export interface JourneyClientConfig extends AsyncLegacyConfigOptions {
// (undocumented)
log?: LogLevel;
// (undocumented)
serverConfig: JourneyServerConfig;
}
Expand Down
46 changes: 46 additions & 0 deletions packages/journey-client/src/lib/client.store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,4 +502,50 @@ describe('journey-client', () => {
expect(request.url).toBe('https://test.com/am/json/realms/root/realms/alpha/authenticate');
});
});

describe('unified JSON config entry', () => {
test('accepts unified JSON config and initializes successfully', async () => {
setupMockFetch();

const unifiedConfig = {
oidc: {
clientId: 'ignored-by-journey',
discoveryEndpoint: mockWellknownUrl,
scopes: ['openid'],
redirectUri: 'https://example.com/callback',
},
} as unknown as JourneyClientConfig;

const client = await journey({ config: unifiedConfig });
expect(client).toHaveProperty('start');
expect(client).toHaveProperty('next');
});

test('throws when unified JSON config has missing required field', async () => {
const invalidConfig = {
oidc: {
// discoveryEndpoint missing — required even for journey
},
} as unknown as JourneyClientConfig;

await expect(journey({ config: invalidConfig })).rejects.toThrow(
/Invalid unified SDK config/,
);
});

test('throws when unified JSON config has wrong field type', async () => {
const invalidConfig = {
oidc: {
clientId: '123',
discoveryEndpoint: mockWellknownUrl,
scopes: 'openid', // should be array
redirectUri: 'https://example.com/callback',
},
} as unknown as JourneyClientConfig;

await expect(journey({ config: invalidConfig })).rejects.toThrow(
/Invalid unified SDK config/,
);
});
});
});
Loading
Loading