From f733403bea32daeb1efe53754d0729dd8acf8d67 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Tue, 28 Jul 2026 17:01:50 -0600 Subject: [PATCH 1/8] feat(sdk): shared store for multiple SDK clients + @forgerock/sdk-store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow davinci(), journey(), and oidc() to share a single Redux store, so the OpenID Connect discovery document is fetched once regardless of how many clients are initialised. Three ownership modes are supported: // Mode 1 — implicit (unchanged default) const client = await oidc({ config }); // Mode 2 — one client owns the store (primary sharing story) const dv = await davinci({ config: davinciConfig }); const oc = await oidc({ config: oidcConfig, store: dv.store }); // Mode 3 — consumer owns the store const store = createSdkStore(); await davinci({ config: davinciConfig, store }); await oidc({ config: oidcConfig, store }); New @forgerock/sdk-store package (scope:sdk-effects) owns: - The single canonical wellknownApi instance and its RTK Query selectors. createWellknownSelector was rebuilt on every call; it is now memoized per URL via a module-level Map, restoring the memoization that was always cold. - initWellknownQuery and isValidWellknownResponse (moved from sdk-oidc). - The shared store contract: SdkStore / SdkStoreHandle / createSdkStore / injectClient / isSdkStoreHandle. - clientExtra() — the per-client slot resolver used by every *.api.ts. Request middleware and logger are scoped per client (security fix): Before, every *.api.ts resolved its middleware and logger from the store's store-wide thunk extraArgument. On a shared store that extra belongs to the owning client, so DaVinci middleware ran against AUTHORIZE, PAR, TOKEN_EXCHANGE, REVOKE, USER_INFO and END_SESSION, and oidc's own logger was silently discarded. extraArgument is now a registry keyed by each api's reducerPath; clientExtra() returns only the calling client's slot. A missing or malformed slot yields empty middleware and an error-level fallback logger — never another client's values. Public API changes: - oidc() takes store as part of its options object (not a positional second arg), consistent with every other factory in the SDK. - davinci() and journey() expose store: SdkStore on the returned client. Both factories also accept store?: SdkStore as input (mode 2 / 3). - One OIDC client per store: a second oidc() with a different clientId returns argument_error instead of silently overwriting token state. - oidc() validates arguments before injecting into a store (RTK inject is irreversible); a rejected call leaves a caller-owned store unchanged. - A value that fails isSdkStoreHandle() returns argument_error, not TypeError. Store contract is declared once structurally: InjectableStore was declared three times — twice identically and once as a weaker mirror — joined only by `as unknown as`. The fictional __sdkStoreBrand required casts on both sides with no compile-time link between them. There is now one structural interface in sdk-store; producers satisfy it without a cast, consumers receive it without a cast. The two unavoidable widenings live in store.effects.ts with documented rationale. Deleted: toSdkStore, fromSdkStore x3, InjectableStore x3, __sdkStoreBrand, JourneyStore, injectIntoStore, the requestMiddleware log.warn (obsolete under per-client scoping), and the two tests that existed only to cover that warning. Layering enforcement: sdk-store previously depended on sdk-oidc to access initWellknownQuery, which violated the scope:sdk-effects constraint that allows only sdk-types and sdk-utilities. Moving the file removes the only cross-effects dependency and promotes enforce-module-boundaries from warn to error. All 23 affected projects lint clean. Tests: - sdk-store: 40 tests (was 0; passWithNoTests removed). - wellknownApi: cache-per-URL, error mapping, selector memoization (instance identity asserted). - clientExtra: never returns another client's slot; degrades gracefully on any malformed extra. - Middleware isolation: DaVinci middleware does not run against OIDC requests, proven for both the foreign-slot shape and the old flat shape. - State-shape assertions: combineSlices keys are now implicit (via slice.name) rather than literal; pinned so a rename fails here, not in selectors. - shared-store.test.ts fully rewritten: fake handle removed; all three D1 modes tested against the real factories and real createSdkStore(). - store-lifecycle.test.ts: validate-before-inject; clientId conflict guard. - store.effects.test.ts: createSdkStore, isSdkStoreHandle, injectClient. E2E: - Playwright suite in davinci-suites asserts exactly one .well-known network request when davinci() and oidc() share a store (mode 2). Requests are intercepted via page.route() so no live credential is needed. BREAKING (sdk-oidc): initWellknownQuery and isValidWellknownResponse are removed from @forgerock/sdk-oidc and are now exported from @forgerock/sdk-store. Update imports if you were using them directly. --- .changeset/nice-sails-trade.md | 28 ++ e2e/davinci-app/shared-store.html | 12 + e2e/davinci-app/shared-store.ts | 62 +++ e2e/davinci-app/vite.config.ts | 1 + e2e/davinci-suites/src/shared-store.test.ts | 84 ++++ eslint.config.mjs | 2 +- packages/davinci-client/README.md | 44 ++ .../api-report/davinci-client.api.md | 3 + .../api-report/davinci-client.types.api.md | 3 + packages/davinci-client/package.json | 1 + .../src/lib/client.store.effects.ts | 6 +- .../davinci-client/src/lib/client.store.ts | 14 +- .../src/lib/client.store.utils.ts | 84 ++-- .../davinci-client/src/lib/davinci.api.ts | 55 ++- .../src/lib/store-shape.test.ts | 46 +++ .../davinci-client/src/lib/wellknown.api.ts | 57 --- packages/davinci-client/tsconfig.json | 3 + packages/davinci-client/tsconfig.lib.json | 3 + packages/journey-client/README.md | 45 ++ .../api-report/journey-client.api.md | 4 + .../api-report/journey-client.types.api.md | 3 + packages/journey-client/package.json | 2 +- .../journey-client/src/lib/client.store.ts | 14 +- .../src/lib/client.store.utils.ts | 59 +-- .../journey-client/src/lib/journey.api.ts | 42 +- .../src/lib/store-shape.test.ts | 46 +++ .../journey-client/src/lib/wellknown.api.ts | 43 -- packages/journey-client/tsconfig.lib.json | 4 +- packages/oidc-client/README.md | 66 +++ .../oidc-client/api-report/oidc-client.api.md | 270 ++++++------ .../api-report/oidc-client.types.api.md | 270 ++++++------ packages/oidc-client/package.json | 1 + .../oidc-client/src/lib/client-extra.test.ts | 192 +++++++++ packages/oidc-client/src/lib/client.store.ts | 62 ++- .../oidc-client/src/lib/client.store.utils.ts | 85 ++-- packages/oidc-client/src/lib/client.types.ts | 8 +- .../src/lib/logout.request.test.ts | 2 +- packages/oidc-client/src/lib/oidc.api.ts | 55 ++- .../oidc-client/src/lib/shared-store.test.ts | 215 ++++++++++ .../src/lib/store-lifecycle.test.ts | 169 ++++++++ packages/oidc-client/src/types.ts | 6 +- packages/oidc-client/tsconfig.lib.json | 3 + packages/sdk-effects/oidc/src/index.ts | 3 +- packages/sdk-effects/store/README.md | 166 ++++++++ packages/sdk-effects/store/eslint.config.mjs | 28 ++ packages/sdk-effects/store/package.json | 40 ++ packages/sdk-effects/store/src/index.ts | 23 ++ .../store/src/lib/store.effects.test.ts | 199 +++++++++ .../store/src/lib/store.effects.ts | 140 +++++++ .../sdk-effects/store/src/lib/store.types.ts | 105 +++++ .../store/src/lib/store.utils.test.ts | 86 ++++ .../sdk-effects/store/src/lib/store.utils.ts | 64 +++ .../store/src/lib/wellknown.api.test.ts | 221 ++++++++++ .../store}/src/lib/wellknown.api.ts | 61 ++- .../src/lib/wellknown.effects.test.ts | 2 +- .../src/lib/wellknown.effects.ts | 2 +- packages/sdk-effects/store/tsconfig.json | 16 + packages/sdk-effects/store/tsconfig.lib.json | 34 ++ packages/sdk-effects/store/tsconfig.spec.json | 41 ++ packages/sdk-effects/store/vite.config.ts | 43 ++ packages/sdk-types/src/index.ts | 2 +- pnpm-lock.yaml | 21 +- ...26-07-28-centralize-redux-stores-rework.md | 383 ++++++++++++++++++ tsconfig.json | 3 + 64 files changed, 3353 insertions(+), 504 deletions(-) create mode 100644 .changeset/nice-sails-trade.md create mode 100644 e2e/davinci-app/shared-store.html create mode 100644 e2e/davinci-app/shared-store.ts create mode 100644 e2e/davinci-suites/src/shared-store.test.ts create mode 100644 packages/davinci-client/src/lib/store-shape.test.ts delete mode 100644 packages/davinci-client/src/lib/wellknown.api.ts create mode 100644 packages/journey-client/src/lib/store-shape.test.ts delete mode 100644 packages/journey-client/src/lib/wellknown.api.ts create mode 100644 packages/oidc-client/src/lib/client-extra.test.ts create mode 100644 packages/oidc-client/src/lib/shared-store.test.ts create mode 100644 packages/oidc-client/src/lib/store-lifecycle.test.ts create mode 100644 packages/sdk-effects/store/README.md create mode 100644 packages/sdk-effects/store/eslint.config.mjs create mode 100644 packages/sdk-effects/store/package.json create mode 100644 packages/sdk-effects/store/src/index.ts create mode 100644 packages/sdk-effects/store/src/lib/store.effects.test.ts create mode 100644 packages/sdk-effects/store/src/lib/store.effects.ts create mode 100644 packages/sdk-effects/store/src/lib/store.types.ts create mode 100644 packages/sdk-effects/store/src/lib/store.utils.test.ts create mode 100644 packages/sdk-effects/store/src/lib/store.utils.ts create mode 100644 packages/sdk-effects/store/src/lib/wellknown.api.test.ts rename packages/{oidc-client => sdk-effects/store}/src/lib/wellknown.api.ts (52%) rename packages/sdk-effects/{oidc => store}/src/lib/wellknown.effects.test.ts (98%) rename packages/sdk-effects/{oidc => store}/src/lib/wellknown.effects.ts (98%) create mode 100644 packages/sdk-effects/store/tsconfig.json create mode 100644 packages/sdk-effects/store/tsconfig.lib.json create mode 100644 packages/sdk-effects/store/tsconfig.spec.json create mode 100644 packages/sdk-effects/store/vite.config.ts create mode 100644 tasks/plans/2026-07-28-centralize-redux-stores-rework.md diff --git a/.changeset/nice-sails-trade.md b/.changeset/nice-sails-trade.md new file mode 100644 index 0000000000..e1bc0538d1 --- /dev/null +++ b/.changeset/nice-sails-trade.md @@ -0,0 +1,28 @@ +--- +'@forgerock/davinci-client': minor +'@forgerock/journey-client': minor +'@forgerock/oidc-client': minor +'@forgerock/sdk-store': minor +'@forgerock/sdk-oidc': patch +--- + +Allow multiple SDK clients to share a single Redux store. + +`davinci()`, `journey()`, and `oidc()` now accept an optional `store` option. When two clients share a store they share the OpenID Connect discovery cache, so `.well-known/openid-configuration` is fetched once instead of once per client. `davinci()` and `journey()` expose the store they create as `client.store`; applications that want to own the store themselves can build one with `createSdkStore()` from the new `@forgerock/sdk-store` package. + +Omitting `store` is unchanged behaviour: the client creates its own store, exactly as before. + +**Request middleware and logging are scoped per client.** Each client's `requestMiddleware` and `logger` are registered against that client alone and are resolved only by its own requests. Middleware passed to `davinci()` or `journey()` is never applied to OIDC requests (`AUTHORIZE`, `PAR`, `TOKEN_EXCHANGE`, `REVOKE`, `USER_INFO`, `END_SESSION`), and middleware passed to `oidc()` is never applied to DaVinci or Journey requests. Both options are honoured on a shared store. + +**`oidc()` takes `store` as part of its options object**, alongside `config`, `requestMiddleware`, `logger`, and `storage`, consistent with every other factory in the SDK. + +**One OIDC client per store.** `oidc()` mounts at a fixed key, so initialising a second OIDC client on the same store with a different `clientId` returns an `argument_error` rather than silently overwriting the first client's token state. Re-initialising with the same `clientId` is allowed and idempotent. Use a separate store per `clientId`. + +Also in this release: + +- New `@forgerock/sdk-store` package (`scope:sdk-effects`) holding the single canonical `wellknownApi` instance, the shared store contract (`SdkStore`, `SdkStoreHandle`, `createSdkStore`, `injectClient`), and OpenID Connect discovery helpers (`initWellknownQuery`, `isValidWellknownResponse`). Previously each client package defined its own `wellknownApi`, which meant a separate discovery cache per client. +- `oidc()` validates its arguments before attaching to a store, so a rejected call no longer leaves a caller-provided store modified. +- Passing a value that is not an SDK store to `store` returns an `argument_error` instead of throwing. +- Well-known selectors are now memoized per URL. `createWellknownSelector` previously rebuilt its selector on every call, so its cache never took effect. +- `@forgerock/sdk-oidc`: `initWellknownQuery` and `isValidWellknownResponse` move to `@forgerock/sdk-store`. Update imports if you were using them directly. +- `enforce-module-boundaries` lint rule promoted from `warn` to `error` across the repo. All packages pass. diff --git a/e2e/davinci-app/shared-store.html b/e2e/davinci-app/shared-store.html new file mode 100644 index 0000000000..e0551b54f4 --- /dev/null +++ b/e2e/davinci-app/shared-store.html @@ -0,0 +1,12 @@ + + + + + + Shared Store Test + + +
initialising…
+ + + diff --git a/e2e/davinci-app/shared-store.ts b/e2e/davinci-app/shared-store.ts new file mode 100644 index 0000000000..555ecb1914 --- /dev/null +++ b/e2e/davinci-app/shared-store.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 - 2026 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. + */ + +/** + * Shared-store smoke test entry point. + * + * This page is navigated to by the Playwright e2e suite + * `shared-store.test.ts` only. It does not connect to any real PingOne + * endpoint — the test intercepts every `.well-known` request via + * `page.route()` and returns a minimal synthetic response. + * + * The page reports results by writing to `#status` so the test can + * assert via `page.textContent` without any app-specific UI. + */ +import { davinci } from '@forgerock/davinci-client'; +import type { DaVinciConfig } from '@forgerock/davinci-client/types'; +import { oidc } from '@forgerock/oidc-client'; +import type { OidcConfig } from '@forgerock/oidc-client/types'; + +const WELLKNOWN_URL = 'https://sdk-test.example.com/as/.well-known/openid-configuration'; + +const davinciConfig: DaVinciConfig = { + clientId: 'test-davinci-client', + redirectUri: window.location.origin, + scope: 'openid profile', + serverConfig: { wellknown: WELLKNOWN_URL }, +}; + +const oidcConfig: OidcConfig = { + clientId: 'test-oidc-client', + redirectUri: window.location.origin, + scope: 'openid profile', + responseType: 'code', + serverConfig: { wellknown: WELLKNOWN_URL }, +}; + +const statusEl = document.getElementById('status')!; + +async function run() { + // ── Mode 2: davinci creates the store, oidc attaches ───────────────────── + const dvClient = await davinci({ config: davinciConfig }); + if ('error' in dvClient) { + statusEl.textContent = `davinci init error: ${dvClient.error}`; + return; + } + + const ocClient = await oidc({ config: oidcConfig, store: dvClient.store }); + if ('error' in ocClient) { + statusEl.textContent = `oidc init error: ${ocClient.error}`; + return; + } + + statusEl.textContent = 'ready'; +} + +run().catch((err) => { + statusEl.textContent = `unexpected error: ${String(err)}`; +}); diff --git a/e2e/davinci-app/vite.config.ts b/e2e/davinci-app/vite.config.ts index 8625a27297..b885b3911c 100644 --- a/e2e/davinci-app/vite.config.ts +++ b/e2e/davinci-app/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ rollupOptions: { input: { main: path.resolve(__dirname, 'index.html'), + 'shared-store': path.resolve(__dirname, 'shared-store.html'), }, output: { entryFileNames: 'main.js', diff --git a/e2e/davinci-suites/src/shared-store.test.ts b/e2e/davinci-suites/src/shared-store.test.ts new file mode 100644 index 0000000000..8ec1416445 --- /dev/null +++ b/e2e/davinci-suites/src/shared-store.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 - 2026 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 { expect, test } from '@playwright/test'; + +/** + * Verifies that two SDK clients sharing a store fetch the OpenID Connect + * discovery document exactly once, regardless of which client initialises + * first and regardless of the ownership model. + * + * The page under test (`/shared-store`) exercises both Mode 2 (davinci owns + * the store) and Mode 3 (consumer-created store). Requests to the well-known + * URL are intercepted and served with a synthetic response so the test does + * not require a live PingOne endpoint. + */ + +const WELLKNOWN_URL = 'https://sdk-test.example.com/as/.well-known/openid-configuration'; + +const WELLKNOWN_RESPONSE = { + issuer: 'https://sdk-test.example.com/as', + authorization_endpoint: 'https://sdk-test.example.com/as/authorize', + token_endpoint: 'https://sdk-test.example.com/as/token', + userinfo_endpoint: 'https://sdk-test.example.com/as/userinfo', + jwks_uri: 'https://sdk-test.example.com/as/jwks', + revocation_endpoint: 'https://sdk-test.example.com/as/revoke', + introspection_endpoint: 'https://sdk-test.example.com/as/introspect', + pushed_authorization_request_endpoint: 'https://sdk-test.example.com/as/par', +}; + +test('shared store — one .well-known fetch across two clients (mode 2: client-owned)', async ({ + page, +}) => { + let discoveryFetchCount = 0; + + // Intercept and count every discovery request; fulfil with a synthetic response + // so no live credential or network is needed. + await page.route(`**/.well-known/**`, async (route) => { + discoveryFetchCount++; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(WELLKNOWN_RESPONSE), + }); + }); + + await page.goto('/shared-store.html', { waitUntil: 'networkidle' }); + + // The page reports its own status so we know initialisation completed. + await expect(page.locator('#status')).toHaveText('ready', { timeout: 15_000 }); + + // Mode 2 (davinci owns the store, oidc attaches): davinci fetches once, + // oidc reads from cache — exactly 1 network request for 2 clients. + expect(discoveryFetchCount).toBe(1); +}); + +test("shared store — oidc attaches to davinci's store, reads discovery from cache", async ({ + page, +}) => { + const fetchedUrls: string[] = []; + + await page.route(`**/.well-known/**`, async (route) => { + fetchedUrls.push(route.request().url()); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(WELLKNOWN_RESPONSE), + }); + }); + + await page.goto('/shared-store.html', { waitUntil: 'networkidle' }); + await expect(page.locator('#status')).toHaveText('ready', { timeout: 15_000 }); + + // Both modes use the same WELLKNOWN_URL, so each URL appears exactly once + // across the two calls despite four total client initialisations. + const unique = [...new Set(fetchedUrls)]; + expect(unique).toHaveLength(1); + expect(unique[0]).toContain('.well-known'); + + // Mode 2: 2 clients (davinci + oidc) on 1 store → exactly 1 fetch. + expect(fetchedUrls.length).toBe(1); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index a2c14760ae..b6a0c15b10 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -111,7 +111,7 @@ export default [ rules: { 'import/extensions': [2, 'ignorePackages'], '@nx/enforce-module-boundaries': [ - 'warn', + 'error', { enforceBuildableLibDependency: true, allow: [], diff --git a/packages/davinci-client/README.md b/packages/davinci-client/README.md index 5534564ffd..92240d3e01 100644 --- a/packages/davinci-client/README.md +++ b/packages/davinci-client/README.md @@ -63,6 +63,50 @@ interface DaVinciConfig { } ``` +### Sharing a store with another client + +If your application also uses `@forgerock/oidc-client`, the two can share one Redux store so the well-known discovery document is fetched once rather than once per client. + +`davinci()` exposes the store it created as `client.store`. Pass it to the other client: + +```ts +import { davinci } from '@forgerock/davinci-client'; +import { oidc } from '@forgerock/oidc-client'; + +const davinciClient = await davinci({ config }); + +// Attaches to davinci's store; the discovery document is already cached there. +const oidcClient = await oidc({ config: oidcConfig, store: davinciClient.store }); +``` + +Or create the store yourself when neither client is the natural owner: + +```ts +import { createSdkStore } from '@forgerock/sdk-store'; + +const store = createSdkStore(); +const davinciClient = await davinci({ config, store }); +const oidcClient = await oidc({ config: oidcConfig, store }); +``` + +Omitting `store` is always valid — the client creates its own, which is the default behaviour. + +#### Middleware and logging stay private + +Sharing a store shares cached data, not configuration. `requestMiddleware` and `logger` are registered against the client you pass them to, and are resolved only by that client's own requests: + +```ts +const store = createSdkStore(); + +// Runs for DAVINCI_START, DAVINCI_NEXT, DAVINCI_FLOW and the other DaVinci actions only. +await davinci({ config, store, requestMiddleware: [davinciMiddleware] }); + +// Runs for OIDC requests only. +await oidc({ config: oidcConfig, store, requestMiddleware: [oidcMiddleware] }); +``` + +Middleware passed here will never run against an OIDC token exchange, and vice versa. + ### Start a DaVinci flow Call the `start` method on the returned client API: diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index ea01559bcd..128a991038 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -16,6 +16,7 @@ import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query'; import { Reducer } from '@reduxjs/toolkit'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; import { SerializedError } from '@reduxjs/toolkit'; import { Unsubscribe } from '@reduxjs/toolkit'; @@ -277,7 +278,9 @@ export function davinci(input: { level: LogLevel; custom?: CustomLogger; }; + store?: SdkStore; }): Promise<{ + store: SdkStore; subscribe: (listener: () => void) => Unsubscribe; externalIdp: () => (() => Promise); flow: (action: DaVinciAction) => InitFlow; diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index 4ae2da4a09..a8dc82d60a 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -16,6 +16,7 @@ import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; import { QueryStatus } from '@reduxjs/toolkit/query'; import { Reducer } from '@reduxjs/toolkit'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; import { SerializedError } from '@reduxjs/toolkit'; import { Unsubscribe } from '@reduxjs/toolkit'; @@ -277,7 +278,9 @@ export function davinci(input: { level: LogLevel; custom?: CustomLogger; }; + store?: SdkStore; }): Promise<{ + store: SdkStore; subscribe: (listener: () => void) => Unsubscribe; externalIdp: () => (() => Promise); flow: (action: DaVinciAction) => InitFlow; diff --git a/packages/davinci-client/package.json b/packages/davinci-client/package.json index 77af118fcb..c7dbe96e94 100644 --- a/packages/davinci-client/package.json +++ b/packages/davinci-client/package.json @@ -29,6 +29,7 @@ "@forgerock/sdk-logger": "workspace:*", "@forgerock/sdk-oidc": "workspace:*", "@forgerock/sdk-request-middleware": "workspace:*", + "@forgerock/sdk-store": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", "@forgerock/storage": "workspace:*", diff --git a/packages/davinci-client/src/lib/client.store.effects.ts b/packages/davinci-client/src/lib/client.store.effects.ts index 9923ea4b67..de97bc0c39 100644 --- a/packages/davinci-client/src/lib/client.store.effects.ts +++ b/packages/davinci-client/src/lib/client.store.effects.ts @@ -11,7 +11,7 @@ import { FetchBaseQueryError } from '@reduxjs/toolkit/query/react'; import type { logger as loggerFn } from '@forgerock/sdk-logger'; -import type { ClientStore, RootState } from './client.store.utils.js'; +import type { DavinciStore, RootState } from './client.store.utils.js'; import type { PollingStatus, InternalErrorResponse } from './client.types.js'; import type { PollingCollector } from './collector.types.js'; @@ -239,7 +239,7 @@ function challengePollingµ({ }: { collector: PollingCollector; challenge: string; - store: ReturnType; + store: DavinciStore; log: ReturnType; }): Micro.Micro { const maxRetries = collector.output.config.pollRetries ?? 60; @@ -295,7 +295,7 @@ export function pollingµ({ }: { mode: PollingMode; collector: PollingCollector; - store: ReturnType; + store: DavinciStore; log: ReturnType; }): Micro.Micro { if (mode._tag === 'challenge') { diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index a95401dd91..2e7b29264a 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -23,9 +23,10 @@ import { pollingµ, getPollingModeµ } from './client.store.effects.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; import { configSlice } from './config.slice.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; /** * Import the DaVinciRequest types */ @@ -69,6 +70,7 @@ export async function davinci({ config, requestMiddleware, logger, + store: sharedStore, }: { config: DaVinciConfig; requestMiddleware?: RequestMiddleware[]; @@ -76,12 +78,18 @@ export async function davinci({ level: LogLevel; custom?: CustomLogger; }; + /** + * An existing SDK store to attach to, so discovery caching and state are + * shared with another client. Omit to create a store for this client alone. + */ + store?: SdkStore; }) { const log = loggerFn({ level: logger?.level ?? config.log ?? 'error', custom: logger?.custom, }); - const store = createClientStore({ requestMiddleware, logger: log }); + const handle = createClientStore({ requestMiddleware, logger: log, store: sharedStore }); + const store = handle.store; const serverInfo = createStorage({ type: 'localStorage', name: 'serverInfo', @@ -113,6 +121,8 @@ export async function davinci({ store.dispatch(configSlice.actions.set({ ...config, wellknownResponse: openIdResponse })); return { + /** Pass to another SDK client's `store` option to share this store. */ + store: handle as SdkStore, // Pass store methods to the client subscribe: store.subscribe, diff --git a/packages/davinci-client/src/lib/client.store.utils.ts b/packages/davinci-client/src/lib/client.store.utils.ts index 32f265c45a..fc8465bc2d 100644 --- a/packages/davinci-client/src/lib/client.store.utils.ts +++ b/packages/davinci-client/src/lib/client.store.utils.ts @@ -1,10 +1,9 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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 { configureStore } from '@reduxjs/toolkit'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { logger as loggerFn } from '@forgerock/sdk-logger'; @@ -13,43 +12,64 @@ import type { GenericError } from '@forgerock/sdk-types'; import type { ErrorNode, ContinueNode, StartNode, SuccessNode } from '../types.js'; import type { InternalErrorResponse } from './client.types.js'; +import { combineSlices } from '@reduxjs/toolkit'; + import { configSlice } from './config.slice.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; -import { wellknownApi } from './wellknown.api.js'; +import { createSdkStore, injectClient, wellknownApi } from '@forgerock/sdk-store'; +import type { SdkStore, SdkStoreHandle } from '@forgerock/sdk-store'; + +/** + * The canonical description of the state this client contributes. + * + * The runtime store is assembled by `injectClient`, which TypeScript cannot + * follow across lazy injection. Combining the same slices here lets the state + * type be *derived* from them rather than hand-written, so it cannot drift from + * what is actually mounted. Exported so the derived state type resolves for + * consumers, and so an application can compose the reducer itself if it wants. + */ +export const rootReducer = combineSlices(configSlice, nodeSlice, davinciApi, wellknownApi); + +export type RootState = ReturnType; +export interface RootStateWithNode< + T extends ErrorNode | ContinueNode | StartNode | SuccessNode, +> extends RootState { + node: T; +} + +/** + * Creates, or attaches to, the store backing a DaVinci client. + * + * Passing `store` attaches to an existing SDK store so that discovery caching + * and state are shared; omitting it creates one, which is the default. + */ export function createClientStore({ requestMiddleware, logger, + store, }: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}) { - return configureStore({ - reducer: { - config: configSlice.reducer, - node: nodeSlice.reducer, - [davinciApi.reducerPath]: davinciApi.reducer, - [wellknownApi.reducerPath]: wellknownApi.reducer, - }, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - thunk: { - extraArgument: { - /** - * This becomes the `api.extra` argument, and will be passed into the - * customer query wrapper for `baseQuery` - */ - requestMiddleware, - logger, - }, - }, - }) - .concat(davinciApi.middleware) - .concat(wellknownApi.middleware), + store?: SdkStore; +}): SdkStoreHandle { + return injectClient(store ?? createSdkStore(), { + api: davinciApi, + reducerPath: davinciApi.reducerPath, + slices: [configSlice, nodeSlice], + requestMiddleware, + logger, }); } +export type ClientStore = typeof createClientStore; + +/** The inner Redux store type — used by effects that need dispatch/getState. */ +export type DavinciStore = SdkStoreHandle['store']; + +export type AppDispatch = DavinciStore['dispatch']; + export function handleUpdateValidateError( message: string, type: 'argument_error' | 'state_error', @@ -67,18 +87,6 @@ export function handleUpdateValidateError( }; } -export type ClientStore = typeof createClientStore; - -export type RootState = ReturnType['getState']>; - -export interface RootStateWithNode< - T extends ErrorNode | ContinueNode | StartNode | SuccessNode, -> extends RootState { - node: T; -} - -export type AppDispatch = ReturnType['dispatch']>; - /** * @function createInternalError * @description - Creates an InternalErrorResponse object diff --git a/packages/davinci-client/src/lib/davinci.api.ts b/packages/davinci-client/src/lib/davinci.api.ts index 05e94872ef..30771b0d37 100644 --- a/packages/davinci-client/src/lib/davinci.api.ts +++ b/packages/davinci-client/src/lib/davinci.api.ts @@ -25,7 +25,8 @@ import { createAuthorizeUrl } from '@forgerock/sdk-oidc'; import { handleResponse, transformActionRequest, transformSubmitRequest } from './davinci.utils.js'; -import type { logger as loggerFn } from '@forgerock/sdk-logger'; +import { logger as loggerFn } from '@forgerock/sdk-logger'; +import { clientExtra } from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; /** @@ -47,9 +48,35 @@ type BaseQueryResponse = Promise< QueryReturnValue >; +const DAVINCI_REDUCER_PATH = 'davinci'; + +/** + * This client's private slot on the store's `extraArgument`. + * + * Both fields are optional because a shared store may not have had a davinci + * slot registered yet; `davinciExtra` substitutes safe defaults so an endpoint + * can never fail on a missing slot. + */ interface Extras { - requestMiddleware: RequestMiddleware[]; - logger: ReturnType; + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; +} + +/** Fallback so a missing slot degrades to error-level logging, never a crash. */ +const fallbackLogger = loggerFn({ level: 'error' }); + +/** + * Resolves this client's own middleware and logger. + * + * Reads only the `davinci` slot — never a store-wide value, which on a shared + * store would belong to whichever client created it. + */ +function davinciExtra(extra: unknown): Required { + const slot = clientExtra(extra, DAVINCI_REDUCER_PATH); + return { + requestMiddleware: slot.requestMiddleware ?? [], + logger: slot.logger ?? fallbackLogger, + }; } /** @@ -57,7 +84,7 @@ interface Extras { @@ -81,9 +108,9 @@ export const davinciApi = createApi({ const requestBody = transformActionRequest( state.node, params.action, - (api.extra as Extras).logger, + davinciExtra(api.extra).logger, ); - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = davinciExtra(api.extra); let href = ''; @@ -126,7 +153,7 @@ export const davinciApi = createApi({ * parameters are pre-typed from the library. */ async onQueryStarted(_, api) { - const logger = (api.extra as Extras).logger; + const { logger } = davinciExtra(api.extra); let response; try { @@ -160,7 +187,7 @@ export const davinciApi = createApi({ async queryFn(body, api, __, baseQuery) { const state = api.getState() as RootStateWithNode; const links = state.node.server._links; - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = davinciExtra(api.extra); let requestBody; let href = ''; @@ -237,7 +264,7 @@ export const davinciApi = createApi({ * parameters are pre-typed from the library. */ async onQueryStarted(_, api) { - const logger = (api.extra as Extras).logger; + const { logger } = davinciExtra(api.extra); let response; try { @@ -270,7 +297,7 @@ export const davinciApi = createApi({ * @method queryFn - This is just a wrapper around the fetch call */ async queryFn(options, api, __, baseQuery) { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = davinciExtra(api.extra); const state = api.getState() as RootStateWithNode; if (!state) { @@ -352,7 +379,7 @@ export const davinciApi = createApi({ * parameters are pre-typed from the library. */ async onQueryStarted(_, api) { - const logger = (api.extra as Extras).logger; + const { logger } = davinciExtra(api.extra); let response; try { @@ -381,7 +408,7 @@ export const davinciApi = createApi({ */ resume: builder.query({ async queryFn({ serverInfo, continueToken }, api, _c, baseQuery) { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = davinciExtra(api.extra); const links = serverInfo._links; if (!continueToken) { @@ -430,7 +457,7 @@ export const davinciApi = createApi({ return response; }, async onQueryStarted(_, api) { - const logger = (api.extra as Extras).logger; + const { logger } = davinciExtra(api.extra); let response; try { @@ -464,7 +491,7 @@ export const davinciApi = createApi({ */ poll: builder.mutation({ async queryFn({ endpoint, interactionId }, api, _c, baseQuery) { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = davinciExtra(api.extra); const request: FetchArgs = { url: endpoint, diff --git a/packages/davinci-client/src/lib/store-shape.test.ts b/packages/davinci-client/src/lib/store-shape.test.ts new file mode 100644 index 0000000000..69b1885332 --- /dev/null +++ b/packages/davinci-client/src/lib/store-shape.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 - 2026 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, expect, it } from 'vitest'; + +import { createClientStore } from './client.store.utils.js'; + +/** + * `combineSlices` keys each reducer off `slice.reducerPath ?? slice.name`, where + * the previous `configureStore({ reducer: { ... } })` form spelled the keys out + * literally. That makes the published state shape an implicit consequence of + * slice metadata: renaming `nodeSlice.name` would silently reshape the store. + * + * These assertions pin the shape so such a rename fails loudly here instead of + * in a consumer's selectors. + */ +describe('davinci store shape', () => { + it('exposes exactly the expected top-level state keys', () => { + // Arrange + const { store } = createClientStore({}); + + // Act + const keys = Object.keys(store.getState()).sort(); + + // Assert + expect(keys).toEqual(['config', 'davinci', 'node', 'wellknown']); + }); + + it('registers this client\u2019s slot on the store extra, keyed by reducerPath', async () => { + // Arrange + const { store } = createClientStore({}); + let observed: unknown; + + // Act — a thunk is the supported way to observe extraArgument + await store.dispatch(((_dispatch: unknown, _getState: unknown, extra: unknown) => { + observed = extra; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any); + + // Assert + expect(observed).toHaveProperty('clients.davinci'); + }); +}); diff --git a/packages/davinci-client/src/lib/wellknown.api.ts b/packages/davinci-client/src/lib/wellknown.api.ts deleted file mode 100644 index 251d24a04d..0000000000 --- a/packages/davinci-client/src/lib/wellknown.api.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * 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 { createSelector } from '@reduxjs/toolkit'; -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -import { initWellknownQuery } from '@forgerock/sdk-oidc'; - -import type { WellknownResponse } from '@forgerock/sdk-types'; -import type { - FetchBaseQueryError, - FetchBaseQueryMeta, - QueryReturnValue, -} from '@reduxjs/toolkit/query'; - -/** - * RTK Query API for well-known endpoint discovery. - * - * Uses the `initWellknownQuery` builder pattern from `@forgerock/sdk-oidc`. - * The builder constructs the request and validates the response; - * `fetchBaseQuery` handles the HTTP transport through RTK Query's pipeline. - */ -export const wellknownApi = createApi({ - reducerPath: 'wellknown', - baseQuery: fetchBaseQuery(), - endpoints: (builder) => ({ - configuration: builder.query({ - queryFn: async (url, _api, _extra, baseQuery) => { - const result = await initWellknownQuery(url).applyQuery(async (req) => { - const queryResult = await baseQuery(req); - return queryResult as QueryReturnValue; - }); - return result as QueryReturnValue< - WellknownResponse, - FetchBaseQueryError, - FetchBaseQueryMeta - >; - }, - }), - }), -}); - -/** - * Creates a memoized selector for cached well-known data. - * - * @param wellknownUrl - The well-known endpoint URL used as the cache key - * @returns A memoized selector that extracts the WellknownResponse from state, or undefined if not yet fetched - */ -export function createWellknownSelector(wellknownUrl: string) { - return createSelector( - wellknownApi.endpoints.configuration.select(wellknownUrl), - (result) => result?.data, - ); -} diff --git a/packages/davinci-client/tsconfig.json b/packages/davinci-client/tsconfig.json index 141b4ebf5f..df68746659 100644 --- a/packages/davinci-client/tsconfig.json +++ b/packages/davinci-client/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../sdk-effects/sdk-request-middleware" }, + { + "path": "../sdk-effects/store" + }, { "path": "../sdk-effects/oidc" }, diff --git a/packages/davinci-client/tsconfig.lib.json b/packages/davinci-client/tsconfig.lib.json index 6c7bbdefef..6db7b33e75 100644 --- a/packages/davinci-client/tsconfig.lib.json +++ b/packages/davinci-client/tsconfig.lib.json @@ -48,6 +48,9 @@ }, { "path": "../sdk-effects/logger/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/store/tsconfig.lib.json" } ] } diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md index 910ca9462e..52afa8300e 100644 --- a/packages/journey-client/README.md +++ b/packages/journey-client/README.md @@ -13,6 +13,7 @@ - [API Reference](#api-reference) - [Working with Callbacks](#working-with-callbacks) - [Request Middleware](#request-middleware) +- [Sharing a Store With Another Client](#sharing-a-store-with-another-client) - [Error Handling](#error-handling) - [Building](#building) - [Testing](#testing) @@ -232,6 +233,50 @@ const client = await journey({ | `JOURNEY_NEXT` | Submitting a step | | `JOURNEY_TERMINATE` | Terminating the session | +## Sharing a Store With Another Client + +If your application also uses `@forgerock/oidc-client`, the two can share one Redux store so the well-known discovery document is fetched once rather than once per client. + +`journey()` exposes the store it created as `client.store`. Pass it to the other client: + +```typescript +import { journey } from '@forgerock/journey-client'; +import { oidc } from '@forgerock/oidc-client'; + +const journeyClient = await journey({ config }); + +// Attaches to journey's store; the discovery document is already cached there. +const oidcClient = await oidc({ config: oidcConfig, store: journeyClient.store }); +``` + +Or create the store yourself when neither client is the natural owner: + +```typescript +import { createSdkStore } from '@forgerock/sdk-store'; + +const store = createSdkStore(); +const journeyClient = await journey({ config, store }); +const oidcClient = await oidc({ config: oidcConfig, store }); +``` + +Omitting `store` is always valid — the client creates its own, which is the default behaviour. + +### Middleware and logging stay private + +Sharing a store shares cached data, not configuration. `requestMiddleware` and `logger` are registered against the client you pass them to, and are resolved only by that client's own requests: + +```typescript +const store = createSdkStore(); + +// Runs for JOURNEY_START, JOURNEY_NEXT and JOURNEY_TERMINATE only. +await journey({ config, store, requestMiddleware: [journeyMiddleware] }); + +// Runs for OIDC requests only. +await oidc({ config: oidcConfig, store, requestMiddleware: [oidcMiddleware] }); +``` + +Middleware passed here will never run against an OIDC token exchange, and vice versa. + ## Error Handling The `journey()` factory throws on initialization failure. Use try/catch: diff --git a/packages/journey-client/api-report/journey-client.api.md b/packages/journey-client/api-report/journey-client.api.md index 35a4a51dbe..d3cd42dba5 100644 --- a/packages/journey-client/api-report/journey-client.api.md +++ b/packages/journey-client/api-report/journey-client.api.md @@ -23,6 +23,7 @@ import { PolicyKey } from '@forgerock/sdk-types'; import { PolicyParams } from '@forgerock/sdk-types'; import { PolicyRequirement } from '@forgerock/sdk-types'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; import { Step } from '@forgerock/sdk-types'; import { StepDetail } from '@forgerock/sdk-types'; import { StepType } from '@forgerock/sdk-types'; @@ -184,6 +185,7 @@ export function journey(input: { level: LogLevel; custom?: CustomLogger; }; + store?: SdkStore; }): Promise; // @public @@ -197,6 +199,8 @@ export interface JourneyClient { // (undocumented) start: (options?: StartParam) => Promise; // (undocumented) + store: SdkStore; + // (undocumented) subscribe: (listener: () => void) => () => void; // (undocumented) terminate: (options?: { diff --git a/packages/journey-client/api-report/journey-client.types.api.md b/packages/journey-client/api-report/journey-client.types.api.md index d9219a9710..0840dcc3cd 100644 --- a/packages/journey-client/api-report/journey-client.types.api.md +++ b/packages/journey-client/api-report/journey-client.types.api.md @@ -22,6 +22,7 @@ import { PolicyKey } from '@forgerock/sdk-types'; import { PolicyParams } from '@forgerock/sdk-types'; import { PolicyRequirement } from '@forgerock/sdk-types'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; import { Step } from '@forgerock/sdk-types'; import { StepDetail } from '@forgerock/sdk-types'; import { StepType } from '@forgerock/sdk-types'; @@ -184,6 +185,8 @@ export interface JourneyClient { // (undocumented) start: (options?: StartParam) => Promise; // (undocumented) + store: SdkStore; + // (undocumented) subscribe: (listener: () => void) => () => void; // (undocumented) terminate: (options?: { diff --git a/packages/journey-client/package.json b/packages/journey-client/package.json index 886639e90c..4ccf62829a 100644 --- a/packages/journey-client/package.json +++ b/packages/journey-client/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@forgerock/sdk-logger": "workspace:*", - "@forgerock/sdk-oidc": "workspace:*", "@forgerock/sdk-request-middleware": "workspace:*", + "@forgerock/sdk-store": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", "@forgerock/storage": "workspace:*", diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 32c2689a64..e91cd86dfb 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -13,6 +13,7 @@ import { createWellknownError, } from '@forgerock/sdk-utilities'; import type { GenericError } from '@forgerock/sdk-types'; +import type { SdkStore } from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { Step } from '@forgerock/sdk-types'; @@ -23,7 +24,7 @@ import { createStorage } from '@forgerock/storage'; import * as Either from 'effect/Either'; import { createJourneyObject, parseJourneyResponse } from './journey.utils.js'; import type { JourneyResult } from './journey.utils.js'; -import { wellknownApi } from './wellknown.api.js'; +import { wellknownApi } from '@forgerock/sdk-store'; import type { JourneyStep } from './step.utils.js'; import type { JourneyClientConfig } from './config.types.js'; @@ -32,6 +33,7 @@ import type { NextOptions, StartParam, ResumeOptions } from './interfaces.js'; /** The journey client instance returned by the `journey()` function. */ export interface JourneyClient { + store: SdkStore; subscribe: (listener: () => void) => () => void; start: (options?: StartParam) => Promise; next: (step: JourneyStep, options?: NextOptions) => Promise; @@ -73,6 +75,7 @@ export async function journey({ config, requestMiddleware, logger, + store: sharedStore, }: { config: JourneyClientConfig; requestMiddleware?: RequestMiddleware[]; @@ -80,6 +83,11 @@ export async function journey({ level: LogLevel; custom?: CustomLogger; }; + /** + * An existing SDK store to attach to, so discovery caching and state are + * shared with another client. Omit to create a store for this client alone. + */ + store?: SdkStore; }): Promise { const log = loggerFn({ level: logger?.level ?? config.log ?? 'error', @@ -113,7 +121,8 @@ export async function journey({ ); } - const store = createJourneyStore({ requestMiddleware, logger: log }); + const handle = createJourneyStore({ requestMiddleware, logger: log, store: sharedStore }); + const store = handle.store; const { wellknown } = config.serverConfig; @@ -154,6 +163,7 @@ export async function journey({ }); const self: JourneyClient = { + store: handle as SdkStore, subscribe: store.subscribe, start: async (options?: StartParam) => { diff --git a/packages/journey-client/src/lib/client.store.utils.ts b/packages/journey-client/src/lib/client.store.utils.ts index 0e0b05c794..c9a939caec 100644 --- a/packages/journey-client/src/lib/client.store.utils.ts +++ b/packages/journey-client/src/lib/client.store.utils.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -7,40 +7,47 @@ import { logger as loggerFn } from '@forgerock/sdk-logger'; import { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import { combineReducers, configureStore } from '@reduxjs/toolkit'; + +import { combineSlices } from '@reduxjs/toolkit'; import { configSlice } from './config.slice.js'; import { journeyApi } from './journey.api.js'; -import { wellknownApi } from './wellknown.api.js'; +import { createSdkStore, injectClient, wellknownApi } from '@forgerock/sdk-store'; + +import type { SdkStore, SdkStoreHandle } from '@forgerock/sdk-store'; + +/** + * The canonical description of the state this client contributes. + * + * The runtime store is assembled by `injectClient`, which TypeScript cannot + * follow across lazy injection. Combining the same slices here lets the state + * type be *derived* from them rather than hand-written, so it cannot drift from + * what is actually mounted. Exported so the derived state type resolves for + * consumers, and so an application can compose the reducer itself if it wants. + */ +export const rootReducer = combineSlices(journeyApi, configSlice, wellknownApi); -const rootReducer = combineReducers({ - [journeyApi.reducerPath]: journeyApi.reducer, - [configSlice.name]: configSlice.reducer, - [wellknownApi.reducerPath]: wellknownApi.reducer, -}); +export type RootState = ReturnType; +/** + * Creates, or attaches to, the store backing a Journey client. + * + * Passing `store` attaches to an existing SDK store so that discovery caching + * and state are shared; omitting it creates one, which is the default. + */ export const createJourneyStore = ({ requestMiddleware, logger, + store, }: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}) => { - return configureStore({ - reducer: rootReducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - serializableCheck: true, - thunk: { - extraArgument: { - requestMiddleware, - logger, - }, - }, - }) - .concat(journeyApi.middleware) - .concat(wellknownApi.middleware), + store?: SdkStore; +}): SdkStoreHandle => + injectClient(store ?? createSdkStore(), { + api: journeyApi, + reducerPath: journeyApi.reducerPath, + slices: [configSlice], + requestMiddleware, + logger, }); -}; - -export type RootState = ReturnType; diff --git a/packages/journey-client/src/lib/journey.api.ts b/packages/journey-client/src/lib/journey.api.ts index 66c4da9a29..74a3f1f5b9 100644 --- a/packages/journey-client/src/lib/journey.api.ts +++ b/packages/journey-client/src/lib/journey.api.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 - 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2020 - 2026 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. @@ -10,7 +10,8 @@ import { REQUESTED_WITH, getEndpointPath, stringify, resolve } from '@forgerock/ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; import type { Step } from '@forgerock/sdk-types'; -import type { logger as loggerFn } from '@forgerock/sdk-logger'; +import { logger as loggerFn } from '@forgerock/sdk-logger'; +import { clientExtra } from '@forgerock/sdk-store'; import type { BaseQueryApi, BaseQueryFn, @@ -84,13 +85,38 @@ function configureSessionRequest(): RequestInit { return init; } +const JOURNEY_REDUCER_PATH = 'journeyReducer'; + +/** + * This client's private slot on the store's `extraArgument`. + * + * Optional because a shared store may not have had a journey slot registered + * yet; `journeyExtra` substitutes safe defaults. + */ interface Extras { - requestMiddleware: RequestMiddleware[]; - logger: ReturnType; + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; +} + +/** Fallback so a missing slot degrades to error-level logging, never a crash. */ +const fallbackLogger = loggerFn({ level: 'error' }); + +/** + * Resolves this client's own middleware and logger. + * + * Reads only the `journeyReducer` slot — never a store-wide value, which on a + * shared store would belong to whichever client created it. + */ +function journeyExtra(extra: unknown): Required { + const slot = clientExtra(extra, JOURNEY_REDUCER_PATH); + return { + requestMiddleware: slot.requestMiddleware ?? [], + logger: slot.logger ?? fallbackLogger, + }; } export const journeyApi = createApi({ - reducerPath: 'journeyReducer', + reducerPath: JOURNEY_REDUCER_PATH, baseQuery: fetchBaseQuery({ baseUrl: '/', prepareHeaders: (headers: Headers) => { @@ -121,7 +147,7 @@ export const journeyApi = createApi({ const url = constructUrl(serverConfig, options?.journey, query); const request = configureRequest(); - const { requestMiddleware } = api.extra as Extras; + const { requestMiddleware } = journeyExtra(api.extra); const response = await initQuery({ ...request, url: url }, 'begin', { type: 'service', @@ -153,7 +179,7 @@ export const journeyApi = createApi({ const url = constructUrl(serverConfig, undefined, query); const request = configureRequest(step); - const { requestMiddleware } = api.extra as Extras; + const { requestMiddleware } = journeyExtra(api.extra); const response = await initQuery({ ...request, url }, 'continue') .applyMiddleware(requestMiddleware) @@ -183,7 +209,7 @@ export const journeyApi = createApi({ const url = constructSessionsUrl(serverConfig, query); const request = configureSessionRequest(); - const { requestMiddleware } = api.extra as Extras; + const { requestMiddleware } = journeyExtra(api.extra); const response = await initQuery({ ...request, url }, 'terminate') .applyMiddleware(requestMiddleware) diff --git a/packages/journey-client/src/lib/store-shape.test.ts b/packages/journey-client/src/lib/store-shape.test.ts new file mode 100644 index 0000000000..d2ea592667 --- /dev/null +++ b/packages/journey-client/src/lib/store-shape.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 - 2026 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, expect, it } from 'vitest'; + +import { createJourneyStore } from './client.store.utils.js'; + +/** + * `combineSlices` keys each reducer off `slice.reducerPath ?? slice.name`, where + * the previous `combineReducers({ ... })` form spelled the keys out literally. + * That makes the published state shape an implicit consequence of slice + * metadata: renaming `configSlice.name` would silently reshape the store. + * + * These assertions pin the shape so such a rename fails loudly here instead of + * in a consumer's selectors. + */ +describe('journey store shape', () => { + it('exposes exactly the expected top-level state keys', () => { + // Arrange + const { store } = createJourneyStore({}); + + // Act + const keys = Object.keys(store.getState()).sort(); + + // Assert + expect(keys).toEqual(['config', 'journeyReducer', 'wellknown']); + }); + + it('registers this client\u2019s slot on the store extra, keyed by reducerPath', async () => { + // Arrange + const { store } = createJourneyStore({}); + let observed: unknown; + + // Act — a thunk is the supported way to observe extraArgument + await store.dispatch(((_dispatch: unknown, _getState: unknown, extra: unknown) => { + observed = extra; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any); + + // Assert + expect(observed).toHaveProperty('clients.journeyReducer'); + }); +}); diff --git a/packages/journey-client/src/lib/wellknown.api.ts b/packages/journey-client/src/lib/wellknown.api.ts deleted file mode 100644 index 2c1c41e5a2..0000000000 --- a/packages/journey-client/src/lib/wellknown.api.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -import { initWellknownQuery } from '@forgerock/sdk-oidc'; - -import type { WellknownResponse } from '@forgerock/sdk-types'; -import type { - FetchBaseQueryError, - FetchBaseQueryMeta, - QueryReturnValue, -} from '@reduxjs/toolkit/query'; - -/** - * RTK Query API for well-known endpoint discovery. - * - * Uses the `initWellknownQuery` builder pattern from `@forgerock/sdk-oidc`. - * The builder constructs the request and validates the response; - * `fetchBaseQuery` handles the HTTP transport through RTK Query's pipeline. - */ -export const wellknownApi = createApi({ - reducerPath: 'wellknown', - baseQuery: fetchBaseQuery(), - endpoints: (builder) => ({ - configuration: builder.query({ - queryFn: async (url, _api, _extra, baseQuery) => { - const result = await initWellknownQuery(url).applyQuery(async (req) => { - const queryResult = await baseQuery(req); - return queryResult as QueryReturnValue; - }); - return result as QueryReturnValue< - WellknownResponse, - FetchBaseQueryError, - FetchBaseQueryMeta - >; - }, - }), - }), -}); diff --git a/packages/journey-client/tsconfig.lib.json b/packages/journey-client/tsconfig.lib.json index ca3f899b8d..573c9f707e 100644 --- a/packages/journey-client/tsconfig.lib.json +++ b/packages/journey-client/tsconfig.lib.json @@ -28,10 +28,10 @@ "path": "../sdk-types/tsconfig.lib.json" }, { - "path": "../sdk-effects/sdk-request-middleware/tsconfig.lib.json" + "path": "../sdk-effects/store/tsconfig.lib.json" }, { - "path": "../sdk-effects/oidc/tsconfig.lib.json" + "path": "../sdk-effects/sdk-request-middleware/tsconfig.lib.json" }, { "path": "../sdk-effects/logger/tsconfig.lib.json" diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index 4119d84729..324e6e98be 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -10,6 +10,7 @@ The oidc module follows the [OIDC](https://openid.net/specs/openid-connect-core- - [Initialization](#initialization) - [Configuration Options](#configuration-options) - [Quick Start](#quick-start) +- [Sharing a Store With Another Client](#sharing-a-store-with-another-client) - [API Reference](#api-reference) - [authorize](#authorize) - [token](#token) @@ -59,6 +60,12 @@ The `oidc()` initialization function accepts the following configuration: - **timeout** (optional) - Request timeout in milliseconds - **additionalParameters** (optional) - Additional parameters to include in authorization requests +The `oidc()` function also accepts: + +- **requestMiddleware** (optional) - Middleware applied to this client's requests only +- **logger** (optional) - Log level and custom logger for this client only +- **store** (optional) - An existing SDK store to attach to. See [Sharing a Store](#sharing-a-store-with-another-client) + ## Quick Start Here's a minimal example to get started: @@ -82,6 +89,65 @@ const user = await oidcClient.user.info(); await oidcClient.user.logout(); ``` +## Sharing a Store With Another Client + +If your application also uses `@forgerock/davinci-client` or `@forgerock/journey-client`, the clients can share one Redux store. The well-known discovery document is then fetched once rather than once per client. + +Pass the other client's `store` handle: + +```js +import { davinci } from '@forgerock/davinci-client'; +import { oidc } from '@forgerock/oidc-client'; + +const davinciClient = await davinci({ config: davinciConfig }); + +// Attaches to davinci's store; the discovery document is already cached there. +const oidcClient = await oidc({ config: oidcConfig, store: davinciClient.store }); +``` + +Or create the store yourself when neither client is the natural owner: + +```js +import { createSdkStore } from '@forgerock/sdk-store'; + +const store = createSdkStore(); +const davinciClient = await davinci({ config: davinciConfig, store }); +const oidcClient = await oidc({ config: oidcConfig, store }); +``` + +Omitting `store` is always valid — the client creates its own, which is the default behaviour. + +### Middleware and logging stay private + +Sharing a store shares cached data, not configuration. Your `requestMiddleware` and `logger` are registered against this client alone and are only applied to OIDC requests: + +```js +const oidcClient = await oidc({ + config, + store, + // Runs for AUTHORIZE, PAR, TOKEN_EXCHANGE, REVOKE, USER_INFO and END_SESSION only. + requestMiddleware: [myOidcMiddleware], + logger: { level: 'debug' }, +}); +``` + +Middleware passed to `davinci()` or `journey()` will never run against an OIDC token exchange, and middleware passed here will never run against their requests. + +### One OIDC client per store + +`oidc()` mounts at a fixed key in the store, so two OIDC clients sharing one store would overwrite each other's token state. Initialising a second client with a different `clientId` returns an `argument_error`: + +```js +const store = createSdkStore(); +await oidc({ config: { ...config, clientId: 'app-one' }, store }); + +const second = await oidc({ config: { ...config, clientId: 'app-two' }, store }); +// { error: "This store is already in use by an OIDC client with clientId 'app-one'. ...", +// type: 'argument_error' } +``` + +Re-initialising with the _same_ `clientId` is allowed and idempotent. If you need two clientIds, give each its own store. + ## API Reference ### authorize diff --git a/packages/oidc-client/api-report/oidc-client.api.md b/packages/oidc-client/api-report/oidc-client.api.md index 02a90f352a..d1583848a8 100644 --- a/packages/oidc-client/api-report/oidc-client.api.md +++ b/packages/oidc-client/api-report/oidc-client.api.md @@ -6,9 +6,9 @@ import { ActionTypes } from '@forgerock/sdk-request-middleware'; import { BaseQueryFn } from '@reduxjs/toolkit/query'; +import { CombinedSliceReducer } from '@reduxjs/toolkit'; import { CombinedState } from '@reduxjs/toolkit/query'; import { CustomLogger } from '@forgerock/sdk-logger'; -import { EnhancedStore } from '@reduxjs/toolkit'; import { FetchArgs } from '@reduxjs/toolkit/query'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; @@ -17,17 +17,14 @@ import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; import type { JWTPayload } from 'jose'; import { logger } from '@forgerock/sdk-logger'; import { LogLevel } from '@forgerock/sdk-logger'; -import { LogMessage } from '@forgerock/sdk-logger'; import { MutationDefinition } from '@reduxjs/toolkit/query'; import { OidcConfig } from '@forgerock/sdk-types'; import { QueryDefinition } from '@reduxjs/toolkit/query'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; +import type { SdkStore } from '@forgerock/sdk-store'; +import type { SdkStoreHandle } from '@forgerock/sdk-store'; import { StorageConfig } from '@forgerock/storage'; -import { StoreEnhancer } from '@reduxjs/toolkit'; -import { ThunkDispatch } from '@reduxjs/toolkit'; -import { Tuple } from '@reduxjs/toolkit'; -import { UnknownAction } from '@reduxjs/toolkit'; import { Unsubscribe } from '@reduxjs/toolkit'; import { WellknownResponse } from '@forgerock/sdk-types'; @@ -113,119 +110,16 @@ export interface AuthorizeSuccessResponse { // @public (undocumented) export type BuildAuthorizationData = [string, GetAuthorizationUrlOptions]; -// @public (undocumented) -export type ClientStore = ReturnType; +// @public +export type ClientStore = ReturnType['store']; // @public export function createClientStore(input: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}): EnhancedStore< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -sessionCheckIframe: MutationDefinition< { -url: string; -responseType: SessionCheckResponseType; -}, BaseQueryFn, never, { -params: Record; -}, "oidc", unknown>; -sessionCheckFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, { -status: 204; -}, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -signOutRedirectUri?: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, UnknownAction, Tuple<[StoreEnhancer< { -dispatch: ThunkDispatch< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -sessionCheckIframe: MutationDefinition< { -url: string; -responseType: SessionCheckResponseType; -}, BaseQueryFn, never, { -params: Record; -}, "oidc", unknown>; -sessionCheckFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, { -status: 204; -}, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -signOutRedirectUri?: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, { -requestMiddleware: RequestMiddleware[] | undefined; -logger: { -changeLevel: (level: LogLevel) => void; -error: (...args: LogMessage[]) => void; -warn: (...args: LogMessage[]) => void; -info: (...args: LogMessage[]) => void; -debug: (...args: LogMessage[]) => void; -} | undefined; -}, UnknownAction>; -}>, StoreEnhancer]>>; + store?: SdkStore; + clientId?: string; +}): SdkStoreHandle; export { CustomLogger } @@ -283,6 +177,7 @@ export function oidc(input: { custom?: CustomLogger; }; storage?: Partial; + store?: SdkStore; }): Promise<{ error: string; type: string; @@ -315,6 +210,9 @@ export type OidcClient = Awaited>; export { OidcConfig } +// @public (undocumented) +export type OidcRootState = ReturnType; + // @public (undocumented) export type OptionalAuthorizeOptions = Partial; @@ -343,6 +241,150 @@ export type RevokeSuccessResult = { deleteResponse: null; }; +// @public +export const rootReducer: CombinedSliceReducer< { +oidc: CombinedState< { +authorizeFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; +par: MutationDefinition< { +endpoint: string; +body: URLSearchParams; +}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; +sessionCheckIframe: MutationDefinition< { +url: string; +responseType: SessionCheckResponseType; +}, BaseQueryFn, never, { +params: Record; +}, "oidc", unknown>; +sessionCheckFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, { +status: 204; +}, "oidc", unknown>; +authorizeIframe: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; +endSession: MutationDefinition< { +idToken: string; +endpoint: string; +signOutRedirectUri?: string; +}, BaseQueryFn, never, null, "oidc", unknown>; +exchange: MutationDefinition< { +code: string; +config: OidcConfig; +endpoint: string; +verifier?: string; +}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; +revoke: MutationDefinition< { +accessToken: string; +clientId?: string; +endpoint: string; +}, BaseQueryFn, never, object, "oidc", unknown>; +userInfo: MutationDefinition< { +accessToken: string; +endpoint: string; +}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; +}, never, "oidc">; +wellknown: CombinedState< { +configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; +}, never, "wellknown">; +}, { +oidc: CombinedState< { +authorizeFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; +par: MutationDefinition< { +endpoint: string; +body: URLSearchParams; +}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; +sessionCheckIframe: MutationDefinition< { +url: string; +responseType: SessionCheckResponseType; +}, BaseQueryFn, never, { +params: Record; +}, "oidc", unknown>; +sessionCheckFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, { +status: 204; +}, "oidc", unknown>; +authorizeIframe: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; +endSession: MutationDefinition< { +idToken: string; +endpoint: string; +signOutRedirectUri?: string; +}, BaseQueryFn, never, null, "oidc", unknown>; +exchange: MutationDefinition< { +code: string; +config: OidcConfig; +endpoint: string; +verifier?: string; +}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; +revoke: MutationDefinition< { +accessToken: string; +clientId?: string; +endpoint: string; +}, BaseQueryFn, never, object, "oidc", unknown>; +userInfo: MutationDefinition< { +accessToken: string; +endpoint: string; +}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; +}, never, "oidc">; +wellknown: CombinedState< { +configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; +}, never, "wellknown">; +}, Partial<{ +oidc: CombinedState< { +authorizeFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; +par: MutationDefinition< { +endpoint: string; +body: URLSearchParams; +}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; +sessionCheckIframe: MutationDefinition< { +url: string; +responseType: SessionCheckResponseType; +}, BaseQueryFn, never, { +params: Record; +}, "oidc", unknown>; +sessionCheckFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, { +status: 204; +}, "oidc", unknown>; +authorizeIframe: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; +endSession: MutationDefinition< { +idToken: string; +endpoint: string; +signOutRedirectUri?: string; +}, BaseQueryFn, never, null, "oidc", unknown>; +exchange: MutationDefinition< { +code: string; +config: OidcConfig; +endpoint: string; +verifier?: string; +}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; +revoke: MutationDefinition< { +accessToken: string; +clientId?: string; +endpoint: string; +}, BaseQueryFn, never, object, "oidc", unknown>; +userInfo: MutationDefinition< { +accessToken: string; +endpoint: string; +}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; +}, never, "oidc">; +wellknown: CombinedState< { +configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; +}, never, "wellknown">; +}>>; + // @public (undocumented) export type RootState = ReturnType; diff --git a/packages/oidc-client/api-report/oidc-client.types.api.md b/packages/oidc-client/api-report/oidc-client.types.api.md index 02a90f352a..d1583848a8 100644 --- a/packages/oidc-client/api-report/oidc-client.types.api.md +++ b/packages/oidc-client/api-report/oidc-client.types.api.md @@ -6,9 +6,9 @@ import { ActionTypes } from '@forgerock/sdk-request-middleware'; import { BaseQueryFn } from '@reduxjs/toolkit/query'; +import { CombinedSliceReducer } from '@reduxjs/toolkit'; import { CombinedState } from '@reduxjs/toolkit/query'; import { CustomLogger } from '@forgerock/sdk-logger'; -import { EnhancedStore } from '@reduxjs/toolkit'; import { FetchArgs } from '@reduxjs/toolkit/query'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; @@ -17,17 +17,14 @@ import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; import type { JWTPayload } from 'jose'; import { logger } from '@forgerock/sdk-logger'; import { LogLevel } from '@forgerock/sdk-logger'; -import { LogMessage } from '@forgerock/sdk-logger'; import { MutationDefinition } from '@reduxjs/toolkit/query'; import { OidcConfig } from '@forgerock/sdk-types'; import { QueryDefinition } from '@reduxjs/toolkit/query'; import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import { ResponseType as ResponseType_2 } from '@forgerock/sdk-types'; +import type { SdkStore } from '@forgerock/sdk-store'; +import type { SdkStoreHandle } from '@forgerock/sdk-store'; import { StorageConfig } from '@forgerock/storage'; -import { StoreEnhancer } from '@reduxjs/toolkit'; -import { ThunkDispatch } from '@reduxjs/toolkit'; -import { Tuple } from '@reduxjs/toolkit'; -import { UnknownAction } from '@reduxjs/toolkit'; import { Unsubscribe } from '@reduxjs/toolkit'; import { WellknownResponse } from '@forgerock/sdk-types'; @@ -113,119 +110,16 @@ export interface AuthorizeSuccessResponse { // @public (undocumented) export type BuildAuthorizationData = [string, GetAuthorizationUrlOptions]; -// @public (undocumented) -export type ClientStore = ReturnType; +// @public +export type ClientStore = ReturnType['store']; // @public export function createClientStore(input: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}): EnhancedStore< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -sessionCheckIframe: MutationDefinition< { -url: string; -responseType: SessionCheckResponseType; -}, BaseQueryFn, never, { -params: Record; -}, "oidc", unknown>; -sessionCheckFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, { -status: 204; -}, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -signOutRedirectUri?: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, UnknownAction, Tuple<[StoreEnhancer< { -dispatch: ThunkDispatch< { -oidc: CombinedState< { -authorizeFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; -par: MutationDefinition< { -endpoint: string; -body: URLSearchParams; -}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; -sessionCheckIframe: MutationDefinition< { -url: string; -responseType: SessionCheckResponseType; -}, BaseQueryFn, never, { -params: Record; -}, "oidc", unknown>; -sessionCheckFetch: MutationDefinition< { -url: string; -}, BaseQueryFn, never, { -status: 204; -}, "oidc", unknown>; -authorizeIframe: MutationDefinition< { -url: string; -}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; -endSession: MutationDefinition< { -idToken: string; -endpoint: string; -signOutRedirectUri?: string; -}, BaseQueryFn, never, null, "oidc", unknown>; -exchange: MutationDefinition< { -code: string; -config: OidcConfig; -endpoint: string; -verifier?: string; -}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; -revoke: MutationDefinition< { -accessToken: string; -clientId?: string; -endpoint: string; -}, BaseQueryFn, never, object, "oidc", unknown>; -userInfo: MutationDefinition< { -accessToken: string; -endpoint: string; -}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; -}, never, "oidc">; -wellknown: CombinedState< { -configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; -}, never, "wellknown">; -}, { -requestMiddleware: RequestMiddleware[] | undefined; -logger: { -changeLevel: (level: LogLevel) => void; -error: (...args: LogMessage[]) => void; -warn: (...args: LogMessage[]) => void; -info: (...args: LogMessage[]) => void; -debug: (...args: LogMessage[]) => void; -} | undefined; -}, UnknownAction>; -}>, StoreEnhancer]>>; + store?: SdkStore; + clientId?: string; +}): SdkStoreHandle; export { CustomLogger } @@ -283,6 +177,7 @@ export function oidc(input: { custom?: CustomLogger; }; storage?: Partial; + store?: SdkStore; }): Promise<{ error: string; type: string; @@ -315,6 +210,9 @@ export type OidcClient = Awaited>; export { OidcConfig } +// @public (undocumented) +export type OidcRootState = ReturnType; + // @public (undocumented) export type OptionalAuthorizeOptions = Partial; @@ -343,6 +241,150 @@ export type RevokeSuccessResult = { deleteResponse: null; }; +// @public +export const rootReducer: CombinedSliceReducer< { +oidc: CombinedState< { +authorizeFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; +par: MutationDefinition< { +endpoint: string; +body: URLSearchParams; +}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; +sessionCheckIframe: MutationDefinition< { +url: string; +responseType: SessionCheckResponseType; +}, BaseQueryFn, never, { +params: Record; +}, "oidc", unknown>; +sessionCheckFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, { +status: 204; +}, "oidc", unknown>; +authorizeIframe: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; +endSession: MutationDefinition< { +idToken: string; +endpoint: string; +signOutRedirectUri?: string; +}, BaseQueryFn, never, null, "oidc", unknown>; +exchange: MutationDefinition< { +code: string; +config: OidcConfig; +endpoint: string; +verifier?: string; +}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; +revoke: MutationDefinition< { +accessToken: string; +clientId?: string; +endpoint: string; +}, BaseQueryFn, never, object, "oidc", unknown>; +userInfo: MutationDefinition< { +accessToken: string; +endpoint: string; +}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; +}, never, "oidc">; +wellknown: CombinedState< { +configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; +}, never, "wellknown">; +}, { +oidc: CombinedState< { +authorizeFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; +par: MutationDefinition< { +endpoint: string; +body: URLSearchParams; +}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; +sessionCheckIframe: MutationDefinition< { +url: string; +responseType: SessionCheckResponseType; +}, BaseQueryFn, never, { +params: Record; +}, "oidc", unknown>; +sessionCheckFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, { +status: 204; +}, "oidc", unknown>; +authorizeIframe: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; +endSession: MutationDefinition< { +idToken: string; +endpoint: string; +signOutRedirectUri?: string; +}, BaseQueryFn, never, null, "oidc", unknown>; +exchange: MutationDefinition< { +code: string; +config: OidcConfig; +endpoint: string; +verifier?: string; +}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; +revoke: MutationDefinition< { +accessToken: string; +clientId?: string; +endpoint: string; +}, BaseQueryFn, never, object, "oidc", unknown>; +userInfo: MutationDefinition< { +accessToken: string; +endpoint: string; +}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; +}, never, "oidc">; +wellknown: CombinedState< { +configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; +}, never, "wellknown">; +}, Partial<{ +oidc: CombinedState< { +authorizeFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; +par: MutationDefinition< { +endpoint: string; +body: URLSearchParams; +}, BaseQueryFn, never, PushAuthorizationResponse, "oidc", unknown>; +sessionCheckIframe: MutationDefinition< { +url: string; +responseType: SessionCheckResponseType; +}, BaseQueryFn, never, { +params: Record; +}, "oidc", unknown>; +sessionCheckFetch: MutationDefinition< { +url: string; +}, BaseQueryFn, never, { +status: 204; +}, "oidc", unknown>; +authorizeIframe: MutationDefinition< { +url: string; +}, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; +endSession: MutationDefinition< { +idToken: string; +endpoint: string; +signOutRedirectUri?: string; +}, BaseQueryFn, never, null, "oidc", unknown>; +exchange: MutationDefinition< { +code: string; +config: OidcConfig; +endpoint: string; +verifier?: string; +}, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; +revoke: MutationDefinition< { +accessToken: string; +clientId?: string; +endpoint: string; +}, BaseQueryFn, never, object, "oidc", unknown>; +userInfo: MutationDefinition< { +accessToken: string; +endpoint: string; +}, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; +}, never, "oidc">; +wellknown: CombinedState< { +configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; +}, never, "wellknown">; +}>>; + // @public (undocumented) export type RootState = ReturnType; diff --git a/packages/oidc-client/package.json b/packages/oidc-client/package.json index bf154c54ca..859ee248e7 100644 --- a/packages/oidc-client/package.json +++ b/packages/oidc-client/package.json @@ -31,6 +31,7 @@ "@forgerock/sdk-logger": "workspace:*", "@forgerock/sdk-oidc": "workspace:*", "@forgerock/sdk-request-middleware": "workspace:*", + "@forgerock/sdk-store": "workspace:*", "@forgerock/sdk-types": "workspace:*", "@forgerock/sdk-utilities": "workspace:*", "@forgerock/storage": "workspace:*", diff --git a/packages/oidc-client/src/lib/client-extra.test.ts b/packages/oidc-client/src/lib/client-extra.test.ts new file mode 100644 index 0000000000..8101b69076 --- /dev/null +++ b/packages/oidc-client/src/lib/client-extra.test.ts @@ -0,0 +1,192 @@ +// @vitest-environment node +/* + * Copyright (c) 2025 - 2026 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 { configureStore } from '@reduxjs/toolkit'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { wellknownApi } from '@forgerock/sdk-store'; +import { oidcApi } from './oidc.api.js'; + +import type { RequestMiddleware } from '@forgerock/sdk-request-middleware'; + +/** + * Regression coverage for the shared-store middleware leak. + * + * OIDC endpoints resolve their middleware and logger from the store's thunk + * `extraArgument`. When a store is shared with davinci/journey, that `extra` + * belongs to the owning client — so before the per-client registry, DaVinci + * middleware executed against TOKEN_EXCHANGE, REVOKE, END_SESSION and friends, + * and oidc's own logger was silently discarded. + * + * These tests build a store whose `extra` carries slots for two clients and + * assert that oidc endpoints only ever see their own. + */ + +const REVOKE_URL = 'https://example.pingone.com/test-env/as/revoke'; + +function recordingMiddleware(calls: string[], label: string): RequestMiddleware { + return (_req, action, next) => { + calls.push(`${label}:${action.type}`); + next(); + }; +} + +function makeLoggerSpy() { + return { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }; +} + +/** A store shaped like a shared SDK store: one `extra`, one slot per client. */ +function makeTwoClientStore(slots: Record) { + return configureStore({ + reducer: { + [oidcApi.reducerPath]: oidcApi.reducer, + [wellknownApi.reducerPath]: wellknownApi.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ thunk: { extraArgument: { clients: slots } } }) + .concat(wellknownApi.middleware) + .concat(oidcApi.middleware), + }); +} + +describe('oidc endpoints resolve middleware and logger per client', () => { + beforeEach(() => { + vi.spyOn(globalThis, 'fetch').mockImplementation( + async () => + new Response(JSON.stringify({ access_token: 'at', token_type: 'Bearer' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + oidcApi.util.resetApiState(); + }); + + it('does not run another client\u2019s requestMiddleware against an OIDC request', async () => { + // Arrange + const calls: string[] = []; + const store = makeTwoClientStore({ + davinci: { + requestMiddleware: [recordingMiddleware(calls, 'davinci')], + logger: makeLoggerSpy(), + }, + oidc: { requestMiddleware: [], logger: makeLoggerSpy() }, + }); + + // Act + await store.dispatch( + oidcApi.endpoints.revoke.initiate({ + accessToken: 'test-access-token', + clientId: 'test-client-id', + endpoint: REVOKE_URL, + }), + ); + + // Assert — before the per-client registry this recorded 'davinci:TOKEN_EXCHANGE' + expect(calls).toEqual([]); + }); + + it("runs the OIDC client's own requestMiddleware against an OIDC request", async () => { + // Arrange + const calls: string[] = []; + const store = makeTwoClientStore({ + davinci: { requestMiddleware: [recordingMiddleware(calls, 'davinci')] }, + oidc: { + requestMiddleware: [recordingMiddleware(calls, 'oidc')], + logger: makeLoggerSpy(), + }, + }); + + // Act + await store.dispatch( + oidcApi.endpoints.revoke.initiate({ + accessToken: 'test-access-token', + clientId: 'test-client-id', + endpoint: REVOKE_URL, + }), + ); + + // Assert + expect(calls.filter((c) => c.startsWith('oidc:'))).not.toHaveLength(0); + expect(calls.filter((c) => c.startsWith('davinci:'))).toHaveLength(0); + }); + + it('ignores a store-wide flat middleware list (the original leak shape)', async () => { + // Arrange — this is exactly what davinci/journey used to put in extraArgument. + // If oidc ever reads the whole `extra` again instead of its own slot, this fails. + const calls: string[] = []; + const store = configureStore({ + reducer: { + [oidcApi.reducerPath]: oidcApi.reducer, + [wellknownApi.reducerPath]: wellknownApi.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: { + requestMiddleware: [recordingMiddleware(calls, 'store-wide')], + logger: makeLoggerSpy(), + }, + }, + }) + .concat(wellknownApi.middleware) + .concat(oidcApi.middleware), + }); + + // Act + await store.dispatch( + oidcApi.endpoints.revoke.initiate({ + accessToken: 'test-access-token', + clientId: 'test-client-id', + endpoint: REVOKE_URL, + }), + ); + + // Assert + expect(calls).toEqual([]); + }); + + it("uses the OIDC client's own logger, not the owning client's", async () => { + // Arrange + const oidcLogger = makeLoggerSpy(); + const davinciLogger = makeLoggerSpy(); + const store = makeTwoClientStore({ + davinci: { requestMiddleware: [], logger: davinciLogger }, + oidc: { requestMiddleware: [], logger: oidcLogger }, + }); + + // Act + await store.dispatch( + oidcApi.endpoints.revoke.initiate({ + accessToken: 'test-access-token', + clientId: 'test-client-id', + endpoint: REVOKE_URL, + }), + ); + + // Assert + const oidcCalls = + oidcLogger.debug.mock.calls.length + + oidcLogger.info.mock.calls.length + + oidcLogger.error.mock.calls.length; + const davinciCalls = + davinciLogger.debug.mock.calls.length + + davinciLogger.info.mock.calls.length + + davinciLogger.error.mock.calls.length; + + expect(oidcCalls).toBeGreaterThan(0); + expect(davinciCalls).toBe(0); + }); +}); diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index b7824d3178..52ae0480e9 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2025 - 2026 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -12,16 +12,17 @@ import { causeIsDie, exitIsFail, exitIsSuccess } from 'effect/Micro'; import { authorizeµ, createParAuthorizeUrlµ } from './authorize.request.js'; import { buildTokenExchangeµ } from './exchange.request.js'; -import { createClientStore, createTokenError } from './client.store.utils.js'; +import { conflictingClientId, createClientStore, createTokenError } from './client.store.utils.js'; import { handleMicroExit } from '@forgerock/sdk-utilities'; import { isExpiryWithinThreshold } from './token.utils.js'; import { logoutµ } from './logout.request.js'; import { oidcApi } from './oidc.api.js'; import { sessionCheckNoneµ, sessionCheckIdTokenµ } from './session.micros.js'; -import { wellknownApi, wellknownSelector } from './wellknown.api.js'; +import { isSdkStoreHandle, wellknownApi, wellknownSelector } from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; +import type { SdkStore } from '@forgerock/sdk-store'; import type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; import type { StorageConfig } from '@forgerock/storage'; @@ -56,6 +57,7 @@ export async function oidc({ requestMiddleware, logger, storage, + store: sharedStore, }: { config: OidcConfig; requestMiddleware?: RequestMiddleware[]; @@ -64,20 +66,30 @@ export async function oidc({ custom?: CustomLogger; }; storage?: Partial; + /** + * An existing SDK store to attach to, so discovery caching and state are + * shared with another client. Omit to create a store for this client alone. + */ + store?: SdkStore; }) { const log = loggerFn({ level: logger?.level ?? config.log ?? 'error', custom: logger?.custom, }); const oauthThreshold = config.oauthThreshold || 30 * 1000; // Default to 30 seconds - const storageClient = createStorage({ - type: storage?.type || 'localStorage', - name: storage?.name || config.clientId, - prefix: storage?.prefix || 'pic', - ...storage, - } as StorageConfig); - const store = createClientStore({ requestMiddleware, logger: log }); - + /** + * Validate before touching the store. RTK's `inject` is irreversible, so + * mutating a caller-owned store and *then* rejecting the arguments would leave + * them permanently carrying a slice from a call that never succeeded. + */ + if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { + return { + error: + 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + + 'another SDK client, or one created with `createSdkStore()`.', + type: 'argument_error', + }; + } if (!config?.serverConfig?.wellknown) { return { error: 'Requires a wellknown url initializing this factory.', @@ -91,6 +103,34 @@ export async function oidc({ }; } + /** + * `oidcApi.reducerPath` is a fixed string, so a second client on the same + * store would share one cache slice and clobber the first client's tokens. + * Re-initialising the same clientId is fine and stays idempotent. + */ + const conflict = conflictingClientId(sharedStore, config.clientId); + if (conflict) { + return { + error: + `This store is already in use by an OIDC client with clientId '${conflict}'. ` + + 'Use a separate store per clientId.', + type: 'argument_error', + }; + } + + const storageClient = createStorage({ + type: storage?.type || 'localStorage', + name: storage?.name || config.clientId, + prefix: storage?.prefix || 'pic', + ...storage, + } as StorageConfig); + const { store } = createClientStore({ + requestMiddleware, + logger: log, + store: sharedStore, + clientId: config.clientId, + }); + const wellknownUrl = config.serverConfig.wellknown; const { data, error } = await store.dispatch( wellknownApi.endpoints.configuration.initiate(wellknownUrl), diff --git a/packages/oidc-client/src/lib/client.store.utils.ts b/packages/oidc-client/src/lib/client.store.utils.ts index f7c5f30792..dc0e24d0aa 100644 --- a/packages/oidc-client/src/lib/client.store.utils.ts +++ b/packages/oidc-client/src/lib/client.store.utils.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -7,51 +7,80 @@ import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import { logger as loggerFn } from '@forgerock/sdk-logger'; -import { configureStore, type SerializedError } from '@reduxjs/toolkit'; +import { combineSlices, type SerializedError } from '@reduxjs/toolkit'; import { oidcApi } from './oidc.api.js'; -import { wellknownApi } from './wellknown.api.js'; +import { createSdkStore, injectClient, wellknownApi } from '@forgerock/sdk-store'; import type { GenericError } from '@forgerock/sdk-types'; +import type { SdkStore, SdkStoreHandle } from '@forgerock/sdk-store'; import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +/** + * The canonical description of the state this client contributes. + * + * The runtime store is assembled by `injectClient`, which TypeScript cannot + * follow across lazy injection. Combining the same slices here lets the state + * type be *derived* from them rather than hand-written, so it cannot drift from + * what is actually mounted. Exported so the derived state type resolves for + * consumers, and so an application can compose the reducer itself if it wants. + */ +export const rootReducer = combineSlices(oidcApi, wellknownApi); + +export type OidcRootState = ReturnType; + /** * @function createClientStore - * @description Creates a Redux store configured with OIDC and well-known APIs. + * @description Creates, or attaches to, the store backing an OIDC client. * @param param - Configuration options for the client store. - * @param {RequestMiddleware} param.requestMiddleware - An array of request middleware functions to be applied to the store. - * @param {ReturnType} param.logger - An optional logger function for logging messages. - * @returns { ReturnType } - Returns a configured Redux store with OIDC and well-known APIs. + * @param {RequestMiddleware} param.requestMiddleware - Request middleware applied to this client's requests only. + * @param {ReturnType} param.logger - An optional logger for this client only. + * @param {SdkStore} param.store - An existing SDK store to attach to. Omit to create one. + * @returns {SdkStoreHandle} - A handle to the store this client is mounted on. */ export function createClientStore({ requestMiddleware, logger, + store, + clientId, }: { requestMiddleware?: RequestMiddleware[]; logger?: ReturnType; -}) { - return configureStore({ - reducer: { - [oidcApi.reducerPath]: oidcApi.reducer, - [wellknownApi.reducerPath]: wellknownApi.reducer, - }, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - thunk: { - extraArgument: { - /** - * This becomes the `api.extra` argument, and will be passed into the - * customer query wrapper for `baseQuery` - */ - requestMiddleware, - logger, - }, - }, - }) - .concat(wellknownApi.middleware) - .concat(oidcApi.middleware), + store?: SdkStore; + clientId?: string; +}): SdkStoreHandle { + return injectClient(store ?? createSdkStore(), { + api: oidcApi, + reducerPath: oidcApi.reducerPath, + requestMiddleware, + logger, + clientId, }); } +/** + * Reports the clientId already occupying a store's OIDC slot, when it differs + * from the one being initialised. + * + * `oidcApi.reducerPath` is the fixed string 'oidc', so two clients on one store + * would share a single RTK Query cache slice and silently overwrite each other's + * token state. Detecting that is cheaper than namespacing per clientId, and + * failing loudly beats corrupting tokens. + * + * @returns The conflicting clientId, or `undefined` when there is no conflict. + */ +export function conflictingClientId( + store: SdkStore | undefined, + clientId: string, +): string | undefined { + const existing = store?.extra.clients[oidcApi.reducerPath]; + if (!existing) { + return undefined; + } + + const existingClientId = (existing as { clientId?: string }).clientId; + return existingClientId && existingClientId !== clientId ? existingClientId : undefined; +} + /** * @function createLogoutError * @description Creates a logout error object based on the provided data and error. diff --git a/packages/oidc-client/src/lib/client.types.ts b/packages/oidc-client/src/lib/client.types.ts index 6a04c919b3..48e77159e5 100644 --- a/packages/oidc-client/src/lib/client.types.ts +++ b/packages/oidc-client/src/lib/client.types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -11,7 +11,11 @@ import { oidc } from './client.store.js'; export type OidcClient = Awaited>; -export type ClientStore = ReturnType; +/** + * The inner Redux store. `createClientStore` returns a handle carrying the + * store plus the injection seams; internal code only ever needs the store. + */ +export type ClientStore = ReturnType['store']; export type RootState = ReturnType; diff --git a/packages/oidc-client/src/lib/logout.request.test.ts b/packages/oidc-client/src/lib/logout.request.test.ts index 1beaf712f4..608bf7e75c 100644 --- a/packages/oidc-client/src/lib/logout.request.test.ts +++ b/packages/oidc-client/src/lib/logout.request.test.ts @@ -84,7 +84,7 @@ const storageClient = createStorage({ }); const logger = loggerFn({ level: 'error' }); -const store = createClientStore({ logger }); +const { store } = createClientStore({ logger }); const tokens = { accessToken: '1234567890', diff --git a/packages/oidc-client/src/lib/oidc.api.ts b/packages/oidc-client/src/lib/oidc.api.ts index be666dcf7b..bfbd4221d2 100644 --- a/packages/oidc-client/src/lib/oidc.api.ts +++ b/packages/oidc-client/src/lib/oidc.api.ts @@ -1,5 +1,5 @@ /* - * Copyright © 2025 - 2026 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -21,7 +21,8 @@ import { type RequestMiddleware, } from '@forgerock/sdk-request-middleware'; -import type { logger as loggerFn } from '@forgerock/sdk-logger'; +import { logger as loggerFn } from '@forgerock/sdk-logger'; +import { clientExtra } from '@forgerock/sdk-store'; import type { TokenExchangeResponse } from './exchange.types.js'; import type { AuthorizationSuccess, AuthorizeSuccessResponse } from './authorize.request.types.js'; import type { UserInfoResponse } from './client.types.js'; @@ -31,18 +32,44 @@ import type { SessionCheckResponseType } from './session.types.js'; const IFRAME_TIMEOUT_MS = 3000; +const OIDC_REDUCER_PATH = 'oidc'; + +/** + * This client's private slot on the store's `extraArgument`. + * + * Both fields are optional because a shared store may not have had an oidc slot + * registered yet; `oidcExtra` substitutes safe defaults so an endpoint can never + * fail on a missing slot. + */ interface Extras { - requestMiddleware: RequestMiddleware[]; - logger: ReturnType; + requestMiddleware?: RequestMiddleware[]; + logger?: ReturnType; +} + +/** Fallback so a missing slot degrades to error-level logging, never a crash. */ +const fallbackLogger = loggerFn({ level: 'error' }); + +/** + * Resolves this client's own middleware and logger. + * + * Reads only the `oidc` slot — never a store-wide value, which on a shared + * store would belong to whichever client created it. + */ +function oidcExtra(extra: unknown): Required { + const slot = clientExtra(extra, OIDC_REDUCER_PATH); + return { + requestMiddleware: slot.requestMiddleware ?? [], + logger: slot.logger ?? fallbackLogger, + }; } export const oidcApi = createApi({ - reducerPath: 'oidc', + reducerPath: OIDC_REDUCER_PATH, baseQuery: fetchBaseQuery(), endpoints: (builder) => ({ authorizeFetch: builder.mutation({ queryFn: async ({ url }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const request: FetchArgs = { url, @@ -111,7 +138,7 @@ export const oidcApi = createApi({ }), par: builder.mutation({ queryFn: async ({ endpoint, body }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const request: FetchArgs = { url: endpoint, @@ -187,7 +214,7 @@ export const oidcApi = createApi({ { url: string; responseType: SessionCheckResponseType } >({ queryFn: async ({ url, responseType }, api) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const errorParams = ['error', 'error_description']; const request: FetchArgs = { url }; @@ -271,7 +298,7 @@ export const oidcApi = createApi({ }), sessionCheckFetch: builder.mutation<{ status: 204 }, { url: string }>({ queryFn: async ({ url }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const request: FetchArgs = { url, @@ -314,7 +341,7 @@ export const oidcApi = createApi({ }), authorizeIframe: builder.mutation({ queryFn: async ({ url }, api) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const request: FetchArgs = { url, @@ -401,7 +428,7 @@ export const oidcApi = createApi({ { idToken: string; endpoint: string; signOutRedirectUri?: string } >({ queryFn: async ({ idToken, endpoint, signOutRedirectUri }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const url = new URL(endpoint); url.searchParams.append('id_token_hint', idToken); @@ -456,7 +483,7 @@ export const oidcApi = createApi({ } >({ queryFn: async ({ code, config, endpoint, verifier }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const { clientId, redirectUri } = config; const body = new URLSearchParams({ @@ -515,7 +542,7 @@ export const oidcApi = createApi({ }), revoke: builder.mutation({ queryFn: async ({ accessToken, clientId, endpoint }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const body = new URLSearchParams({ ...(clientId ? { client_id: clientId } : {}), @@ -565,7 +592,7 @@ export const oidcApi = createApi({ }), userInfo: builder.mutation({ queryFn: async ({ accessToken, endpoint }, api, _, baseQuery) => { - const { requestMiddleware, logger } = api.extra as Extras; + const { requestMiddleware, logger } = oidcExtra(api.extra); const request: FetchArgs = { url: endpoint, diff --git a/packages/oidc-client/src/lib/shared-store.test.ts b/packages/oidc-client/src/lib/shared-store.test.ts new file mode 100644 index 0000000000..517749e22c --- /dev/null +++ b/packages/oidc-client/src/lib/shared-store.test.ts @@ -0,0 +1,215 @@ +// @vitest-environment node +/* + * Copyright (c) 2025 - 2026 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createSdkStore, isSdkStoreHandle, wellknownApi } from '@forgerock/sdk-store'; +import { oidc } from './client.store.js'; +import { createClientStore } from './client.store.utils.js'; +import { oidcApi } from './oidc.api.js'; + +import type { OidcConfig } from './config.types.js'; + +/** + * Coverage for the three store-ownership modes. + * + * An earlier version of these tests hand-built a lookalike handle from RTK + * primitives specifically to avoid importing another client package. That made + * them blind to the only failure mode this feature really has: the producing and + * consuming sides of the handle drifting apart. Everything here goes through the + * real `createSdkStore()` and the real `oidc()` factory instead. + */ + +const TEST_WELLKNOWN_URL = + 'https://example.pingone.com/test-env/as/.well-known/openid-configuration'; + +const mockWellknownResponse = { + issuer: 'https://example.pingone.com/test-env/as', + authorization_endpoint: 'https://example.pingone.com/test-env/as/authorize', + token_endpoint: 'https://example.pingone.com/test-env/as/token', + userinfo_endpoint: 'https://example.pingone.com/test-env/as/userinfo', + jwks_uri: 'https://example.pingone.com/test-env/as/jwks', + revocation_endpoint: 'https://example.pingone.com/test-env/as/revoke', + introspection_endpoint: 'https://example.pingone.com/test-env/as/introspect', + pushed_authorization_request_endpoint: 'https://example.pingone.com/test-env/as/par', +}; + +const oidcConfig: OidcConfig = { + clientId: 'test-client-id', + redirectUri: 'http://localhost/callback', + scope: 'openid profile', + serverConfig: { wellknown: TEST_WELLKNOWN_URL }, + responseType: 'code', +}; + +function makeStorageStub() { + const store = new Map(); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + clear: () => store.clear(), + get length() { + return store.size; + }, + key: (i: number) => [...store.keys()][i] ?? null, + }; +} + +let wellknownFetchCount = 0; + +beforeEach(() => { + wellknownFetchCount = 0; + vi.stubGlobal('localStorage', makeStorageStub()); + vi.stubGlobal('sessionStorage', makeStorageStub()); + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; + + if (url.includes('.well-known')) { + wellknownFetchCount++; + return new Response(JSON.stringify(mockWellknownResponse), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response(JSON.stringify({}), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + wellknownApi.util.resetApiState(); + oidcApi.util.resetApiState(); +}); + +describe('mode 1 — implicit store (default, unchanged behaviour)', () => { + it('creates its own store and returns a working client', async () => { + // Act + const client = await oidc({ config: oidcConfig }); + + // Assert + if ('error' in client) throw new Error(`Expected oidc client, got ${client.error}`); + expect(client.subscribe).toBeInstanceOf(Function); + expect(client.token).toBeDefined(); + expect(client.authorize).toBeDefined(); + }); + + it('fetches the discovery document exactly once', async () => { + // Act + await oidc({ config: oidcConfig }); + + // Assert — absolute, not "greater than zero" + expect(wellknownFetchCount).toBe(1); + }); + + it('does not share state between two independent clients', async () => { + // Act + const first = createClientStore({}); + const second = createClientStore({}); + + // Assert + expect(first.store).not.toBe(second.store); + }); +}); + +describe('mode 3 — consumer-owned store', () => { + it('accepts a store created by createSdkStore()', async () => { + // Arrange + const store = createSdkStore(); + + // Act + const client = await oidc({ config: oidcConfig, store }); + + // Assert + if ('error' in client) throw new Error(`Expected oidc client, got ${client.error}`); + expect(client.token).toBeDefined(); + }); + + it('mounts the oidc slice onto the provided store', async () => { + // Arrange + const store = createSdkStore(); + + // Act + await oidc({ config: oidcConfig, store }); + + // Assert + expect(Object.keys(store.store.getState() as object)).toContain(oidcApi.reducerPath); + }); + + it('registers only the oidc slot on the shared registry', async () => { + // Arrange + const store = createSdkStore(); + + // Act + await oidc({ config: oidcConfig, store }); + + // Assert + expect(Object.keys(store.extra.clients)).toEqual([oidcApi.reducerPath]); + }); + + it('reuses a discovery document already cached on the shared store', async () => { + // Arrange — simulate the owning client having already fetched it + const store = createSdkStore(); + await store.store.dispatch( + wellknownApi.endpoints.configuration.initiate(TEST_WELLKNOWN_URL) as never, + ); + expect(wellknownFetchCount).toBe(1); + + // Act + await oidc({ config: oidcConfig, store }); + + // Assert — still exactly one, so oidc served from cache + expect(wellknownFetchCount).toBe(1); + }); + + it('honours this client\u2019s own requestMiddleware on a shared store', async () => { + // Arrange + const store = createSdkStore(); + + // Act + await oidc({ + config: oidcConfig, + store, + requestMiddleware: [ + (_req, _action, next) => { + next(); + }, + ], + }); + + // Assert — middleware is no longer discarded on the shared path + expect(store.extra.clients[oidcApi.reducerPath]?.requestMiddleware).toHaveLength(1); + }); +}); + +describe('store handle contract', () => { + it('the handle produced for a client satisfies the shared guard', () => { + // Arrange / Act + const handle = createClientStore({}); + + // Assert — the producing side of the contract + expect(isSdkStoreHandle(handle)).toBe(true); + }); + + it('rejects a value that is not a store handle', async () => { + // Act + const client = await oidc({ + config: oidcConfig, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + store: { not: 'a store' } as any, + }); + + // Assert — a clear argument error, not a TypeError from inside RTK + expect(client).toHaveProperty('type', 'argument_error'); + expect((client as { error: string }).error).toMatch(/not a valid SDK store/i); + }); +}); diff --git a/packages/oidc-client/src/lib/store-lifecycle.test.ts b/packages/oidc-client/src/lib/store-lifecycle.test.ts new file mode 100644 index 0000000000..df43202f18 --- /dev/null +++ b/packages/oidc-client/src/lib/store-lifecycle.test.ts @@ -0,0 +1,169 @@ +// @vitest-environment node +/* + * Copyright (c) 2025 - 2026 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 { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createSdkStore, wellknownApi } from '@forgerock/sdk-store'; +import { oidc } from './client.store.js'; +import { oidcApi } from './oidc.api.js'; + +import type { OidcConfig } from './config.types.js'; + +/** + * Lifecycle guarantees for a store the caller owns. + * + * RTK's `inject` is irreversible, so mutating a caller's store before the + * arguments have been validated leaves it permanently carrying a slice from a + * call that never succeeded. And because `oidcApi.reducerPath` is the fixed + * string 'oidc', two clients with different clientIds on one store would share + * a single cache slice and silently clobber each other's tokens. + */ + +const TEST_WELLKNOWN_URL = + 'https://example.pingone.com/test-env/as/.well-known/openid-configuration'; + +const mockWellknownResponse = { + issuer: 'https://example.pingone.com/test-env/as', + authorization_endpoint: 'https://example.pingone.com/test-env/as/authorize', + token_endpoint: 'https://example.pingone.com/test-env/as/token', + userinfo_endpoint: 'https://example.pingone.com/test-env/as/userinfo', + jwks_uri: 'https://example.pingone.com/test-env/as/jwks', + revocation_endpoint: 'https://example.pingone.com/test-env/as/revoke', +}; + +const oidcConfig: OidcConfig = { + clientId: 'test-client-id', + redirectUri: 'http://localhost/callback', + scope: 'openid profile', + serverConfig: { wellknown: TEST_WELLKNOWN_URL }, + responseType: 'code', +}; + +function makeStorageStub() { + const store = new Map(); + return { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + clear: () => store.clear(), + get length() { + return store.size; + }, + key: (i: number) => [...store.keys()][i] ?? null, + }; +} + +beforeEach(() => { + vi.stubGlobal('localStorage', makeStorageStub()); + vi.stubGlobal('sessionStorage', makeStorageStub()); + vi.spyOn(globalThis, 'fetch').mockImplementation( + async () => + new Response(JSON.stringify(mockWellknownResponse), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); + wellknownApi.util.resetApiState(); + oidcApi.util.resetApiState(); +}); + +describe('a failed oidc() leaves a caller-owned store untouched', () => { + it('does not mount the oidc slice when wellknown is missing', async () => { + // Arrange + const store = createSdkStore(); + + // Act + const result = await oidc({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: { ...oidcConfig, serverConfig: {} } as any, + store, + }); + + // Assert + expect(result).toHaveProperty('type', 'argument_error'); + expect(Object.keys(store.store.getState() as object)).not.toContain(oidcApi.reducerPath); + }); + + it('does not register a client slot when clientId is missing', async () => { + // Arrange + const store = createSdkStore(); + + // Act + const result = await oidc({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config: { ...oidcConfig, clientId: '' } as any, + store, + }); + + // Assert + expect(result).toHaveProperty('type', 'argument_error'); + expect(store.extra.clients).toEqual({}); + }); + + it('leaves the store reusable for a subsequent valid call', async () => { + // Arrange + const store = createSdkStore(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await oidc({ config: { ...oidcConfig, clientId: '' } as any, store }); + + // Act — the caller fixes their config and retries on the same store + const client = await oidc({ config: oidcConfig, store }); + + // Assert + if ('error' in client) throw new Error(`Expected oidc client, got ${client.error}`); + expect(Object.keys(store.extra.clients)).toEqual([oidcApi.reducerPath]); + }); +}); + +describe('one oidc client per shared store', () => { + it('is idempotent when re-initialised with the same clientId', async () => { + // Arrange + const store = createSdkStore(); + await oidc({ config: oidcConfig, store }); + + // Act + const second = await oidc({ config: oidcConfig, store }); + + // Assert — re-init is a legitimate operation + if ('error' in second) throw new Error(`Expected oidc client, got ${second.error}`); + expect(second.token).toBeDefined(); + }); + + it('errors when a second client with a different clientId joins the same store', async () => { + // Arrange + const store = createSdkStore(); + await oidc({ config: oidcConfig, store }); + + // Act + const second = await oidc({ + config: { ...oidcConfig, clientId: 'a-different-client' }, + store, + }); + + // Assert — silently sharing one 'oidc' cache slice would clobber tokens + expect(second).toHaveProperty('type', 'argument_error'); + expect((second as { error: string }).error).toMatch(/clientId/i); + }); + + it('allows different clientIds on separate stores', async () => { + // Act + const first = await oidc({ config: oidcConfig, store: createSdkStore() }); + const second = await oidc({ + config: { ...oidcConfig, clientId: 'a-different-client' }, + store: createSdkStore(), + }); + + // Assert + expect('error' in first).toBe(false); + expect('error' in second).toBe(false); + }); +}); diff --git a/packages/oidc-client/src/types.ts b/packages/oidc-client/src/types.ts index 6a17dcbfef..e53519c10c 100644 --- a/packages/oidc-client/src/types.ts +++ b/packages/oidc-client/src/types.ts @@ -1,4 +1,5 @@ -/* Copyright © 2025 - 2026 Ping Identity Corporation. All rights reserved. +/* + * Copyright (c) 2025 - 2026 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. @@ -23,3 +24,6 @@ export type { StorageConfig } from '@forgerock/storage'; // Re-export functions needed to resolve OidcClient and ClientStore type aliases export { oidc } from './lib/client.store.js'; export { createClientStore } from './lib/client.store.utils.js'; +// Referenced by createClientStore's return type, so consumers need the names. +export type { OidcRootState } from './lib/client.store.utils.js'; +export { rootReducer } from './lib/client.store.utils.js'; diff --git a/packages/oidc-client/tsconfig.lib.json b/packages/oidc-client/tsconfig.lib.json index d689866941..7335f65895 100644 --- a/packages/oidc-client/tsconfig.lib.json +++ b/packages/oidc-client/tsconfig.lib.json @@ -39,6 +39,9 @@ }, { "path": "../sdk-effects/iframe-manager/tsconfig.lib.json" + }, + { + "path": "../sdk-effects/store/tsconfig.lib.json" } ], "exclude": [ diff --git a/packages/sdk-effects/oidc/src/index.ts b/packages/sdk-effects/oidc/src/index.ts index 6ed59d5aa9..48e3fb9726 100644 --- a/packages/sdk-effects/oidc/src/index.ts +++ b/packages/sdk-effects/oidc/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -8,4 +8,3 @@ export * from './lib/authorize.effects.js'; export * from './lib/authorize.utils.js'; export * from './lib/state-pkce.effects.js'; -export * from './lib/wellknown.effects.js'; diff --git a/packages/sdk-effects/store/README.md b/packages/sdk-effects/store/README.md new file mode 100644 index 0000000000..e38e50dff8 --- /dev/null +++ b/packages/sdk-effects/store/README.md @@ -0,0 +1,166 @@ +# @forgerock/sdk-store + +Shared Redux store infrastructure and OpenID Connect discovery API for the Ping JavaScript SDK. + +This is an internal building block. You do not normally install it directly — `@forgerock/oidc-client`, `@forgerock/davinci-client`, and `@forgerock/journey-client` depend on it. Install it explicitly only if your application wants to own the SDK store itself. + +## Table of Contents + +- [Why this package exists](#why-this-package-exists) +- [Installation](#installation) +- [Sharing a store between clients](#sharing-a-store-between-clients) +- [Well-known discovery](#well-known-discovery) +- [API Reference](#api-reference) +- [Building](#building) +- [Testing](#testing) + +## Why this package exists + +Every SDK client needs the authorization server's OpenID Connect discovery document. Before this package, each client package defined its own `wellknownApi` instance. Three separate RTK Query APIs meant three separate caches, so an application using more than one client fetched the same `.well-known/openid-configuration` document once per client. + +This package holds the **single canonical `wellknownApi` instance**. When SDK clients share a Redux store, they share one cache entry per URL, and the discovery document is fetched exactly once. + +## Installation + +```bash +pnpm add @forgerock/sdk-store +``` + +## Sharing a store between clients + +An application using more than one SDK client can have them share a single Redux store, so the discovery document is fetched once and state lives in one place. There are three ways to arrange this, and the default requires no changes at all. + +### Mode 1 — implicit (default) + +Each client creates its own store. Nothing is shared. This is the behaviour you get by omitting `store`, and it is what every existing application already does. + +```typescript +const client = await oidc({ config }); +``` + +### Mode 2 — one client owns the store + +`davinci()` and `journey()` expose the store they created as `client.store`. Pass it to another client to attach to it. This is the common case. + +```typescript +const davinciClient = await davinci({ config: davinciConfig }); +const oidcClient = await oidc({ config: oidcConfig, store: davinciClient.store }); +// The discovery document was fetched by davinci(); oidc() reads it from cache. +``` + +### Mode 3 — the application owns the store + +Call `createSdkStore()` yourself when you want to control the store's lifetime, or to attach clients in an order that has no natural owner. + +```typescript +import { createSdkStore } from '@forgerock/sdk-store'; + +const store = createSdkStore(); +const davinciClient = await davinci({ config: davinciConfig, store }); +const oidcClient = await oidc({ config: oidcConfig, store }); +``` + +### What is shared, and what is not + +Sharing a store shares **cached data**, not **configuration**. This distinction matters: + +| Shared across clients | Private to each client | +| -------------------------------------------------- | ---------------------- | +| The well-known discovery cache (one fetch per URL) | `requestMiddleware` | +| The Redux state tree and its subscribers | `logger` | + +Each client's `requestMiddleware` and `logger` are registered under its own key on the store and are resolved only by that client's own requests. A middleware you pass to `davinci()` will never run against an OIDC token exchange, and vice versa. Pass them to the client whose requests they are meant to affect: + +```typescript +const store = createSdkStore(); + +// Applies only to DaVinci flow requests. +await davinci({ config: davinciConfig, store, requestMiddleware: [davinciMiddleware] }); + +// Applies only to OIDC requests (authorize, token, revoke, userinfo, end session). +await oidc({ config: oidcConfig, store, requestMiddleware: [oidcMiddleware] }); +``` + +### Limitations + +- **One OIDC client per store.** `oidcApi` mounts at a fixed key, so two OIDC clients on one store would share a single cache slice and overwrite each other's token state. Initialising a second client with a _different_ `clientId` returns an `argument_error`; re-initialising the _same_ `clientId` is allowed and idempotent. Use a separate store per `clientId`. +- **Attaching is permanent.** A client cannot be detached from a store. Discard the store instead. + +## Well-known discovery + +`wellknownApi` is a standard RTK Query API. Mount its reducer and middleware, then dispatch the `configuration` endpoint with the discovery URL as the cache key. + +```typescript +import { configureStore } from '@reduxjs/toolkit'; +import { wellknownApi, wellknownSelector } from '@forgerock/sdk-store'; + +const store = configureStore({ + reducer: { [wellknownApi.reducerPath]: wellknownApi.reducer }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(wellknownApi.middleware), +}); + +const url = 'https://auth.example.com/as/.well-known/openid-configuration'; + +// Fetches on first call; served from cache thereafter. +const { data, error } = await store.dispatch(wellknownApi.endpoints.configuration.initiate(url)); + +// Or read the cached value out of state at any time. +const wellknown = wellknownSelector(url, store.getState()); +``` + +The endpoint never throws. A network failure, a non-2xx response, or a payload missing the required `issuer` / `authorization_endpoint` / `token_endpoint` fields all resolve to `{ error }`. + +## API Reference + +### `wellknownApi` + +The canonical RTK Query API instance. `reducerPath` is `'wellknown'`. + +| Member | Description | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `wellknownApi.reducer` | Mount at `wellknownApi.reducerPath` | +| `wellknownApi.middleware` | Add to the store middleware chain | +| `wellknownApi.endpoints.configuration` | `builder.query` — the argument is the discovery URL and doubles as the cache key | + +### `createWellknownSelector(wellknownUrl)` + +Returns a memoized selector for the cached discovery document, or `undefined` if it has not been fetched. + +Repeated calls with the same URL return the **same selector instance**, so memoization is shared across call sites rather than being rebuilt (and therefore always cold) on each call. + +```typescript +const selectWellknown = createWellknownSelector(url); +const wellknown = selectWellknown(store.getState()); + +createWellknownSelector(url) === createWellknownSelector(url); // true +``` + +### `wellknownSelector(wellknownUrl, state)` + +Convenience wrapper that resolves the selector and immediately applies it to `state`. Use this for one-off reads; use `createWellknownSelector` when you want to hold the selector. + +```typescript +const wellknown = wellknownSelector(url, store.getState()); +``` + +### `WellknownState` + +The minimum state shape required by the selectors. Useful for constraining generic state types: + +```typescript +function readIssuer(url: string, state: S) { + return wellknownSelector(url, state)?.issuer; +} +``` + +## Building + +```bash +pnpm nx build @forgerock/sdk-store +``` + +## Testing + +```bash +pnpm nx test @forgerock/sdk-store +``` diff --git a/packages/sdk-effects/store/eslint.config.mjs b/packages/sdk-effects/store/eslint.config.mjs new file mode 100644 index 0000000000..550383b33f --- /dev/null +++ b/packages/sdk-effects/store/eslint.config.mjs @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 - 2026 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 baseConfig from '../../../eslint.config.mjs'; + +export default [ + ...baseConfig, + { + files: ['**/*.json'], + rules: { + '@nx/dependency-checks': [ + 'warn', + { + ignoredFiles: [ + '{projectRoot}/eslint.config.{js,cjs,mjs}', + '{projectRoot}/vite.config.{js,ts,mjs,mts}', + ], + }, + ], + }, + languageOptions: { + parser: (await import('jsonc-eslint-parser')).default, + }, + }, +]; diff --git a/packages/sdk-effects/store/package.json b/packages/sdk-effects/store/package.json new file mode 100644 index 0000000000..9a2eb69452 --- /dev/null +++ b/packages/sdk-effects/store/package.json @@ -0,0 +1,40 @@ +{ + "name": "@forgerock/sdk-store", + "version": "0.0.0", + "private": false, + "repository": { + "type": "git", + "url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git", + "directory": "packages/sdk-effects/store" + }, + "description": "Shared Redux store and OpenID Connect discovery API for the Ping JavaScript SDK", + "license": "MIT", + "author": "ForgeRock", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "pnpm nx nxBuild", + "lint": "pnpm nx nxLint", + "test": "pnpm nx nxTest", + "test:watch": "pnpm nx nxTest --watch" + }, + "dependencies": { + "@forgerock/sdk-types": "workspace:*", + "@reduxjs/toolkit": "catalog:" + }, + "nx": { + "tags": ["scope:sdk-effects"] + } +} diff --git a/packages/sdk-effects/store/src/index.ts b/packages/sdk-effects/store/src/index.ts new file mode 100644 index 0000000000..beb61b9208 --- /dev/null +++ b/packages/sdk-effects/store/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 - 2026 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. + */ + +export { wellknownApi, wellknownSelector, createWellknownSelector } from './lib/wellknown.api.js'; +export type { WellknownState } from './lib/wellknown.api.js'; + +export { initWellknownQuery, isValidWellknownResponse } from './lib/wellknown.effects.js'; + +export { clientExtra, createStoreExtra } from './lib/store.utils.js'; +export type { SdkStoreExtra } from './lib/store.utils.js'; + +export { createSdkStore, injectClient, isSdkStoreHandle } from './lib/store.effects.js'; +export type { + ClientSlot, + InjectClientOptions, + SdkStore, + SdkStoreHandle, + SdkStoreRegistry, +} from './lib/store.types.js'; diff --git a/packages/sdk-effects/store/src/lib/store.effects.test.ts b/packages/sdk-effects/store/src/lib/store.effects.test.ts new file mode 100644 index 0000000000..c0ecc3008a --- /dev/null +++ b/packages/sdk-effects/store/src/lib/store.effects.test.ts @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2025 - 2026 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 { createSlice } from '@reduxjs/toolkit'; +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; +import { describe, expect, it, vi } from 'vitest'; + +import { createSdkStore, injectClient, isSdkStoreHandle } from './store.effects.js'; +import { wellknownApi } from './wellknown.api.js'; + +const fakeApi = createApi({ + reducerPath: 'fake', + baseQuery: fetchBaseQuery(), + endpoints: (builder) => ({ + ping: builder.query({ queryFn: async () => ({ data: {} }) }), + }), +}); + +const otherApi = createApi({ + reducerPath: 'other', + baseQuery: fetchBaseQuery(), + endpoints: (builder) => ({ + ping: builder.query({ queryFn: async () => ({ data: {} }) }), + }), +}); + +const fakeSlice = createSlice({ + name: 'fakeSlice', + initialState: { value: 0 }, + reducers: { bump: (state) => ({ value: state.value + 1 }) }, +}); + +/** Stand-in for a request middleware; sdk-store does not depend on its type. */ +function noopMiddleware() { + return (_req: unknown, _action: unknown, next: () => unknown) => { + next(); + }; +} + +describe('createSdkStore', () => { + it('produces a handle that satisfies the shared contract', () => { + // Act + const handle = createSdkStore(); + + // Assert + expect(isSdkStoreHandle(handle)).toBe(true); + }); + + it('mounts the wellknown slice so discovery is shared from the start', () => { + // Act + const handle = createSdkStore(); + + // Assert + expect(Object.keys(handle.store.getState() as object)).toContain(wellknownApi.reducerPath); + }); + + it('starts with an empty client registry on the store extra', () => { + // Act + const handle = createSdkStore(); + + // Assert + expect(handle.extra.clients).toEqual({}); + }); + + it('returns an independent store on each call', () => { + // Act + const a = createSdkStore(); + const b = createSdkStore(); + + // Assert + expect(a.store).not.toBe(b.store); + expect(a.extra).not.toBe(b.extra); + }); +}); + +describe('isSdkStoreHandle', () => { + it.each([ + ['undefined', undefined], + ['null', null], + ['a number', 42], + ['a string', 'store'], + ['an empty object', {}], + [ + 'an object missing dynamicMiddleware', + { store: {}, rootReducer: { inject: () => undefined } }, + ], + ['a bare redux store', { dispatch: () => undefined, getState: () => ({}) }], + ])('rejects %s', (_label, candidate) => { + // Assert — a bad handle must be detectable before we mutate anything + expect(isSdkStoreHandle(candidate)).toBe(false); + }); + + it('accepts a real handle', () => { + expect(isSdkStoreHandle(createSdkStore())).toBe(true); + }); +}); + +describe('injectClient', () => { + it('mounts the client api reducer and makes it readable immediately', () => { + // Arrange + const handle = createSdkStore(); + + // Act + injectClient(handle, { api: fakeApi, reducerPath: fakeApi.reducerPath }); + + // Assert — no manual dispatch required by the caller + expect(Object.keys(handle.store.getState() as object)).toContain('fake'); + }); + + it('mounts additional slices supplied by the client', () => { + // Arrange + const handle = createSdkStore(); + + // Act + injectClient(handle, { + api: fakeApi, + reducerPath: fakeApi.reducerPath, + slices: [fakeSlice], + }); + + // Assert + expect(Object.keys(handle.store.getState() as object)).toContain('fakeSlice'); + }); + + it('registers the client slot under its reducerPath', () => { + // Arrange + const handle = createSdkStore(); + const mw = noopMiddleware(); + + // Act + injectClient(handle, { + api: fakeApi, + reducerPath: fakeApi.reducerPath, + requestMiddleware: [mw], + }); + + // Assert + expect(handle.extra.clients['fake']?.requestMiddleware).toEqual([mw]); + }); + + it('keeps each client slot separate', () => { + // Arrange + const handle = createSdkStore(); + const first = noopMiddleware(); + const second = noopMiddleware(); + + // Act + injectClient(handle, { + api: fakeApi, + reducerPath: fakeApi.reducerPath, + requestMiddleware: [first], + }); + injectClient(handle, { + api: otherApi, + reducerPath: otherApi.reducerPath, + requestMiddleware: [second], + }); + + // Assert + expect(handle.extra.clients['fake']?.requestMiddleware).toEqual([first]); + expect(handle.extra.clients['other']?.requestMiddleware).toEqual([second]); + }); + + it('adds the api middleware to the dynamic chain', () => { + // Arrange + const handle = createSdkStore(); + const spy = vi.spyOn(handle.dynamicMiddleware, 'addMiddleware'); + + // Act + injectClient(handle, { api: fakeApi, reducerPath: fakeApi.reducerPath }); + + // Assert + expect(spy).toHaveBeenCalledWith(fakeApi.middleware); + }); + + it('is idempotent for repeated injection of the same client', () => { + // Arrange + const handle = createSdkStore(); + + // Act + injectClient(handle, { api: fakeApi, reducerPath: fakeApi.reducerPath }); + const afterFirst = Object.keys(handle.store.getState() as object).sort(); + injectClient(handle, { api: fakeApi, reducerPath: fakeApi.reducerPath }); + + // Assert + expect(Object.keys(handle.store.getState() as object).sort()).toEqual(afterFirst); + }); + + it('throws a descriptive error when handed something that is not a handle', () => { + // Assert — better than a TypeError from deep inside a factory + expect(() => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + injectClient({} as any, { api: fakeApi, reducerPath: fakeApi.reducerPath }), + ).toThrow(/not a valid SDK store/i); + }); +}); diff --git a/packages/sdk-effects/store/src/lib/store.effects.ts b/packages/sdk-effects/store/src/lib/store.effects.ts new file mode 100644 index 0000000000..c799403ff3 --- /dev/null +++ b/packages/sdk-effects/store/src/lib/store.effects.ts @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025 - 2026 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 { combineSlices, configureStore, createDynamicMiddleware } from '@reduxjs/toolkit'; + +import { wellknownApi } from './wellknown.api.js'; + +import type { + InjectClientOptions, + SdkStore, + SdkStoreHandle, + SdkStoreRegistry, +} from './store.types.js'; + +/** + * Dispatched after a slice injection to force `combineSlices` to recompute. + * + * `inject()` registers the reducer but does not itself recalculate state, so + * without this a caller reading `getState()` straight after injection would not + * see its own slice. Doing it here means no client has to know that. + */ +const RECOMPUTE_ACTION = { type: '@@sdk-store/recompute' } as const; + +/** + * Creates a Redux store that SDK clients can share. + * + * Only the well-known discovery slice is mounted up front — that is the one + * piece every client needs, and mounting it once is what makes the discovery + * document fetch exactly once per URL no matter how many clients attach. + * Everything else arrives through {@link injectClient}. + * + * Applications may call this directly to own the store themselves, but they do + * not have to: each client factory creates one on demand when none is passed. + */ +export function createSdkStore(): SdkStore { + const dynamicMiddleware = createDynamicMiddleware(); + const rootReducer = combineSlices(wellknownApi).withLazyLoadedSlices(); + + // Captured by reference so slots registered later are visible to every request. + const extra: SdkStoreRegistry = { clients: {} }; + + const store = configureStore({ + reducer: rootReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ thunk: { extraArgument: extra } }) + .concat(wellknownApi.middleware) + .concat(dynamicMiddleware.middleware), + }); + + return { + store, + rootReducer, + dynamicMiddleware, + extra, + } as unknown as SdkStore; +} + +/** + * Narrows an unknown value to a usable store handle. + * + * Client factories accept a store from application code, so the value cannot be + * trusted. Checking it here turns a bad argument into a clear error instead of a + * `TypeError` raised somewhere inside RTK. + */ +export function isSdkStoreHandle(value: unknown): value is SdkStore { + if (typeof value !== 'object' || value === null) { + return false; + } + + const candidate = value as Partial; + + return ( + typeof candidate.store === 'object' && + candidate.store !== null && + typeof candidate.store.dispatch === 'function' && + typeof candidate.store.getState === 'function' && + typeof candidate.rootReducer === 'function' && + typeof candidate.rootReducer.inject === 'function' && + typeof candidate.dynamicMiddleware === 'object' && + candidate.dynamicMiddleware !== null && + typeof candidate.dynamicMiddleware.addMiddleware === 'function' && + typeof candidate.extra === 'object' && + candidate.extra !== null && + typeof candidate.extra.clients === 'object' + ); +} + +/** + * Attaches a client to a store: mounts its reducers and middleware, and + * registers its private slot on the store's client registry. + * + * Safe to call more than once for the same client — RTK deduplicates reducer + * injection, and re-registering a slot simply overwrites it with equal values. + * + * @throws If `handle` is not a valid SDK store handle. + */ +export function injectClient>( + handle: SdkStore, + options: InjectClientOptions, +): SdkStoreHandle { + if (!isSdkStoreHandle(handle)) { + throw new Error( + 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + + 'another SDK client, or one created with `createSdkStore()`.', + ); + } + + const { api, reducerPath, slices = [], requestMiddleware, logger, clientId } = options; + + const inject = handle.rootReducer.inject as (slice: unknown) => unknown; + inject(api); + for (const slice of slices) { + inject(slice); + } + + const addMiddleware = handle.dynamicMiddleware.addMiddleware as (mw: unknown) => unknown; + addMiddleware(api.middleware); + + // The registry is readonly to consumers but mutable here by design: this is + // the only place a slot is created, and it must work on a store built earlier. + (handle.extra.clients as Record)[reducerPath] = { + requestMiddleware, + logger, + clientId, + }; + + handle.store.dispatch(RECOMPUTE_ACTION as never); + + /** + * The only widening in the shared-store path, and it is unavoidable: + * TypeScript cannot compute a state shape that is assembled by successive + * lazy `inject()` calls. The caller states the shape its own slices produce, + * and it is correct by construction because those slices were just injected + * above. Keeping it here means no client package needs a cast of its own. + */ + return handle as unknown as SdkStoreHandle; +} diff --git a/packages/sdk-effects/store/src/lib/store.types.ts b/packages/sdk-effects/store/src/lib/store.types.ts new file mode 100644 index 0000000000..d4b87637f6 --- /dev/null +++ b/packages/sdk-effects/store/src/lib/store.types.ts @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 - 2026 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 type { Middleware, Reducer, Store, ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'; + +/** + * Per-client slot on a store's thunk `extraArgument`. + * + * The fields are intentionally loose: `sdk-store` sits in the effects layer + * alongside `sdk-request-middleware` and `sdk-logger`, so it cannot depend on + * their types without crossing a module boundary. Each client narrows this to + * its own concrete shape at the point of use. + */ +export interface ClientSlot { + readonly requestMiddleware?: readonly unknown[]; + readonly logger?: unknown; + /** + * Identifies which client instance owns this slot, so a second client can be + * detected before it silently shares the first one's cache slice. + */ + readonly clientId?: string; +} + +/** + * The mutable client registry carried as the store's `extraArgument`. + * + * `configureStore` captures this object by reference, so slots added after the + * store is built are visible to every subsequent request. That is what allows a + * second client to attach itself to a store it did not create. + */ +export interface SdkStoreRegistry { + readonly clients: Record; +} + +/** A reducer that supports lazy slice injection, as produced by `combineSlices`. */ +export interface InjectableRootReducer { + inject: (slice: never) => unknown; +} + +/** Middleware chain that accepts additions after the store is created. */ +export interface DynamicMiddleware { + addMiddleware: (...middleware: never[]) => unknown; +} + +/** + * A shared SDK Redux store, independent of the state shape. + * + * This is the single declaration of the contract between client packages, and + * the type that travels between them: `davinci()` and `journey()` expose one, + * `oidc()` accepts one. It is deliberately state-agnostic so a handle typed + * with DaVinci's state is assignable to it without a cast — which is what lets + * the whole path stay cast-free. + * + * An earlier design used a branded interface whose brand existed on no runtime + * object. That forced `as unknown as` on both sides — four unchecked casts to + * move one object across a package boundary, with no compile-time link between + * producer and consumer. A structural type gives real checking instead. + */ +export interface SdkStore { + readonly store: { + getState: () => unknown; + /** `never` parameter keeps any concrete dispatch assignable to this. */ + dispatch: (action: never) => unknown; + subscribe: (listener: () => void) => () => void; + }; + readonly rootReducer: InjectableRootReducer; + readonly dynamicMiddleware: DynamicMiddleware; + readonly extra: SdkStoreRegistry; +} + +/** + * A store handle with a known state shape. + * + * Returned by each client's own store factory so that internal code gets full + * typing on `getState()` and `dispatch()`. Assignable to {@link SdkStore} for + * hand-off to another client. + */ +export interface SdkStoreHandle> extends SdkStore { + /** + * `dispatch` keeps RTK's thunk typing so clients can await the result of + * `endpoint.initiate(...)` exactly as they would on a store they built + * themselves. + */ + readonly store: Store & { + dispatch: ThunkDispatch; + }; + readonly rootReducer: InjectableRootReducer & Reducer; +} + +/** Options describing the client attaching itself to a store. */ +export interface InjectClientOptions { + /** The client's RTK Query api. Its reducer and middleware are both mounted. */ + readonly api: { reducerPath: string; reducer: Reducer; middleware: Middleware }; + /** Key for this client's slot on the registry. Normally `api.reducerPath`. */ + readonly reducerPath: string; + /** Additional slices the client owns, e.g. its config and node slices. */ + readonly slices?: readonly { name: string; reducer: Reducer }[]; + readonly requestMiddleware?: readonly unknown[]; + readonly logger?: unknown; + /** Recorded on the slot so repeat injections can be distinguished. */ + readonly clientId?: string; +} diff --git a/packages/sdk-effects/store/src/lib/store.utils.test.ts b/packages/sdk-effects/store/src/lib/store.utils.test.ts new file mode 100644 index 0000000000..2b5266c595 --- /dev/null +++ b/packages/sdk-effects/store/src/lib/store.utils.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 - 2026 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, expect, it } from 'vitest'; + +import { clientExtra } from './store.utils.js'; + +interface TestExtra { + requestMiddleware?: string[]; + logger?: string; +} + +describe('clientExtra', () => { + it('returns the slot registered for the requested reducerPath', () => { + // Arrange + const extra = { + clients: { + oidc: { requestMiddleware: ['oidc-mw'], logger: 'oidc-logger' }, + davinci: { requestMiddleware: ['davinci-mw'], logger: 'davinci-logger' }, + }, + }; + + // Act + const result = clientExtra(extra, 'oidc'); + + // Assert + expect(result).toEqual({ requestMiddleware: ['oidc-mw'], logger: 'oidc-logger' }); + }); + + it('never returns another client slot', () => { + // Arrange — this is the §1.1 leak, expressed as a unit property + const extra = { + clients: { + davinci: { requestMiddleware: ['davinci-mw'], logger: 'davinci-logger' }, + }, + }; + + // Act + const result = clientExtra(extra, 'oidc'); + + // Assert + expect(result.requestMiddleware).toBeUndefined(); + expect(result.logger).toBeUndefined(); + }); + + it('returns an empty slot for an unregistered reducerPath', () => { + // Arrange + const extra = { clients: {} }; + + // Act + const result = clientExtra(extra, 'oidc'); + + // Assert + expect(result).toEqual({}); + }); + + it.each([ + ['undefined', undefined], + ['null', null], + ['a non-object', 42], + ['an object without a clients key', { requestMiddleware: ['legacy'] }], + ['an object whose clients value is not an object', { clients: 'nope' }], + ])('returns an empty slot when extra is %s', (_label, extra) => { + // Act + const result = clientExtra(extra, 'oidc'); + + // Assert — must never throw; endpoints call this on every request + expect(result).toEqual({}); + }); + + it('does not expose the registry itself', () => { + // Arrange + const slot = { requestMiddleware: ['oidc-mw'] }; + const extra = { clients: { oidc: slot } }; + + // Act + const result = clientExtra(extra, 'oidc'); + + // Assert + expect(result).toBe(slot); + expect(result).not.toHaveProperty('clients'); + }); +}); diff --git a/packages/sdk-effects/store/src/lib/store.utils.ts b/packages/sdk-effects/store/src/lib/store.utils.ts new file mode 100644 index 0000000000..f8708fa5d4 --- /dev/null +++ b/packages/sdk-effects/store/src/lib/store.utils.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 - 2026 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. + */ + +/** + * Per-client slots carried on a store's thunk `extraArgument`. + * + * A Redux store has exactly one `extraArgument`, but a shared SDK store serves + * several clients. Keying the contents by the owning api's `reducerPath` gives + * each client a private slot, so one client's request middleware and logger can + * never be resolved by another client's endpoints. + * + * The slot type is left generic because each client's needs differ — journey + * only uses request middleware, oidc and davinci use both middleware and a + * logger. Callers narrow it at the point of use. + */ +export interface SdkStoreExtra { + readonly clients: Record; +} + +/** + * Resolves the calling client's own slot from a store's `extraArgument`. + * + * This runs on every request, so it never throws. An unrecognised or malformed + * `extra` yields an empty slot rather than an error — and, critically, never + * falls back to a store-wide value that would belong to a different client. + * + * @param extra - The thunk `extraArgument`, as received from `api.extra` + * @param reducerPath - The calling api's `reducerPath`, used as the slot key + * @returns The client's own slot, or an empty object if none is registered + */ +export function clientExtra(extra: unknown, reducerPath: string): Slot { + if (typeof extra !== 'object' || extra === null || !('clients' in extra)) { + return {} as Slot; + } + + const { clients } = extra as { clients: unknown }; + if (typeof clients !== 'object' || clients === null) { + return {} as Slot; + } + + const slot = (clients as Record)[reducerPath]; + if (typeof slot !== 'object' || slot === null) { + return {} as Slot; + } + + return slot as Slot; +} + +/** + * Builds an `extraArgument` registry holding a single client's slot. + * + * Used by each client factory when it creates its own store. When a store is + * shared, additional slots are added at injection time. + */ +export function createStoreExtra( + reducerPath: string, + slot: Slot, +): SdkStoreExtra { + return { clients: { [reducerPath]: slot } }; +} diff --git a/packages/sdk-effects/store/src/lib/wellknown.api.test.ts b/packages/sdk-effects/store/src/lib/wellknown.api.test.ts new file mode 100644 index 0000000000..cd126216f5 --- /dev/null +++ b/packages/sdk-effects/store/src/lib/wellknown.api.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 - 2026 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 { configureStore } from '@reduxjs/toolkit'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createWellknownSelector, wellknownApi, wellknownSelector } from './wellknown.api.js'; + +import type { WellknownResponse } from '@forgerock/sdk-types'; + +const URL_A = 'https://a.example.com/as/.well-known/openid-configuration'; +const URL_B = 'https://b.example.com/as/.well-known/openid-configuration'; + +function wellknownFixture(overrides: Partial = {}) { + return { + issuer: 'https://a.example.com/as', + authorization_endpoint: 'https://a.example.com/as/authorize', + token_endpoint: 'https://a.example.com/as/token', + userinfo_endpoint: 'https://a.example.com/as/userinfo', + jwks_uri: 'https://a.example.com/as/jwks', + ...overrides, + }; +} + +function makeStore() { + return configureStore({ + reducer: { [wellknownApi.reducerPath]: wellknownApi.reducer }, + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(wellknownApi.middleware), + }); +} + +function jsonResponse(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); +} + +describe('wellknownApi', () => { + let fetchSpy: ReturnType; + let calledUrls: string[]; + + beforeEach(() => { + calledUrls = []; + fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const url = typeof input === 'string' ? input : (input as Request).url; + calledUrls.push(url); + return jsonResponse(wellknownFixture()); + }) as ReturnType; + }); + + afterEach(() => { + vi.restoreAllMocks(); + wellknownApi.util.resetApiState(); + }); + + it('fetches the configuration exactly once for a single URL', async () => { + // Arrange + const store = makeStore(); + + // Act + const result = await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert + expect(result.data).toMatchObject({ issuer: 'https://a.example.com/as' }); + expect(calledUrls).toHaveLength(1); + }); + + it('serves the second identical request from cache without a second network call', async () => { + // Arrange + const store = makeStore(); + + // Act + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert — this is the property the shared store depends on + expect(calledUrls).toEqual([URL_A]); + }); + + it('keeps a separate cache entry per URL', async () => { + // Arrange + const store = makeStore(); + + // Act + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_B)); + + // Assert + expect(calledUrls).toEqual([URL_A, URL_B]); + expect(wellknownSelector(URL_A, store.getState())).toBeDefined(); + expect(wellknownSelector(URL_B, store.getState())).toBeDefined(); + }); + + it('maps a non-2xx response to an error rather than throwing', async () => { + // Arrange + fetchSpy.mockImplementation(async () => jsonResponse({ message: 'nope' }, 500)); + const store = makeStore(); + + // Act + const result = await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + }); + + it('maps a structurally invalid well-known payload to an error', async () => { + // Arrange — missing issuer/authorization_endpoint/token_endpoint + fetchSpy.mockImplementation(async () => + jsonResponse({ jwks_uri: 'https://a.example.com/jwks' }), + ); + const store = makeStore(); + + // Act + const result = await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + }); + + it('maps a network rejection to an error rather than throwing', async () => { + // Arrange + fetchSpy.mockImplementation(async () => { + throw new Error('connection refused'); + }); + const store = makeStore(); + + // Act + const result = await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert + expect(result.error).toBeDefined(); + expect(result.data).toBeUndefined(); + }); +}); + +describe('createWellknownSelector', () => { + it('returns the same selector instance for the same URL', () => { + // Arrange / Act + const first = createWellknownSelector(URL_A); + const second = createWellknownSelector(URL_A); + + // Assert — without this, memoization is rebuilt on every call and never hits + expect(first).toBe(second); + }); + + it('returns distinct selector instances for distinct URLs', () => { + // Arrange / Act + const a = createWellknownSelector(URL_A); + const b = createWellknownSelector(URL_B); + + // Assert + expect(a).not.toBe(b); + }); + + it('does not recompute when called repeatedly against unchanged state', async () => { + // Arrange + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => jsonResponse(wellknownFixture())); + const store = makeStore(); + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + const selector = createWellknownSelector(URL_A); + + // Act + selector(store.getState()); + const afterFirst = selector.recomputations(); + selector(store.getState()); + selector(store.getState()); + + // Assert + expect(selector.recomputations()).toBe(afterFirst); + + vi.restoreAllMocks(); + wellknownApi.util.resetApiState(); + }); +}); + +describe('wellknownSelector', () => { + afterEach(() => { + vi.restoreAllMocks(); + wellknownApi.util.resetApiState(); + }); + + it('returns undefined before the configuration has been fetched', () => { + // Arrange + const store = makeStore(); + + // Act / Assert + expect(wellknownSelector(URL_A, store.getState())).toBeUndefined(); + }); + + it('returns the cached configuration after a successful fetch', async () => { + // Arrange + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => jsonResponse(wellknownFixture())); + const store = makeStore(); + + // Act + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert + expect(wellknownSelector(URL_A, store.getState())).toMatchObject({ + issuer: 'https://a.example.com/as', + }); + }); + + it('returns undefined when the fetch failed', async () => { + // Arrange + vi.spyOn(globalThis, 'fetch').mockImplementation(async () => jsonResponse({}, 500)); + const store = makeStore(); + + // Act + await store.dispatch(wellknownApi.endpoints.configuration.initiate(URL_A)); + + // Assert + expect(wellknownSelector(URL_A, store.getState())).toBeUndefined(); + }); +}); diff --git a/packages/oidc-client/src/lib/wellknown.api.ts b/packages/sdk-effects/store/src/lib/wellknown.api.ts similarity index 52% rename from packages/oidc-client/src/lib/wellknown.api.ts rename to packages/sdk-effects/store/src/lib/wellknown.api.ts index b4da332e53..8a3c6a8e97 100644 --- a/packages/oidc-client/src/lib/wellknown.api.ts +++ b/packages/sdk-effects/store/src/lib/wellknown.api.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. @@ -7,7 +7,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'; -import { initWellknownQuery } from '@forgerock/sdk-oidc'; +import { initWellknownQuery } from './wellknown.effects.js'; import type { WellknownResponse } from '@forgerock/sdk-types'; import type { @@ -15,14 +15,16 @@ import type { FetchBaseQueryMeta, QueryReturnValue, } from '@reduxjs/toolkit/query'; -import type { RootState } from './client.types.js'; /** * RTK Query API for well-known endpoint discovery. * - * Uses the `initWellknownQuery` builder pattern from `@forgerock/sdk-oidc`. + * Uses the `initWellknownQuery` builder pattern from `./wellknown.effects.js`. * The builder constructs the request and validates the response; * `fetchBaseQuery` handles the HTTP transport through RTK Query's pipeline. + * + * This is the canonical single instance — all SDK client packages import from here + * so that a shared Redux store gets a single cache entry per URL. */ export const wellknownApi = createApi({ reducerPath: 'wellknown', @@ -44,33 +46,64 @@ export const wellknownApi = createApi({ }), }); +/** Minimum state shape required to use wellknown selectors. */ +export type WellknownState = { + [wellknownApi.reducerPath]: ReturnType; +}; + +/** + * Per-URL selector cache. + * + * `createSelector` memoizes against its inputs, so a fresh instance per call + * would start with a cold cache every time and never hit. Keying instances by + * URL is what makes the memoization actually effective. This is a pure cache: + * the same URL always yields the same selector, and it is unobservable from + * outside beyond that identity. + */ +const selectorCache = new Map(); + +type WellknownStateSelector = ReturnType< + typeof createSelector< + [ReturnType], + WellknownResponse | undefined + > +>; + /** * Creates a memoized selector for cached well-known data. * + * Repeated calls with the same URL return the *same* selector instance, so the + * memoization is shared across every call site. + * * @param wellknownUrl - The well-known endpoint URL used as the cache key * @returns A memoized selector that extracts the WellknownResponse from state, or undefined if not yet fetched */ -export function createWellknownSelector(wellknownUrl: string) { - return createSelector( +export function createWellknownSelector(wellknownUrl: string): WellknownStateSelector { + const cached = selectorCache.get(wellknownUrl); + if (cached) { + return cached; + } + + const selector = createSelector( wellknownApi.endpoints.configuration.select(wellknownUrl), (result) => result?.data, - ); + ) as WellknownStateSelector; + + selectorCache.set(wellknownUrl, selector); + return selector; } /** - * Convenience selector for oidc-client's RootState type. + * Convenience selector for any state that contains the wellknown slice. * * Unlike {@link createWellknownSelector}, this immediately evaluates the * selector against the provided state rather than returning a reusable selector. * * @param wellknownUrl - The well-known endpoint URL used as the cache key - * @param state - The oidc-client Redux root state + * @param state - Any Redux state that includes the wellknown slice * @returns The cached WellknownResponse or undefined if not yet fetched */ -export function wellknownSelector(wellknownUrl: string, state: RootState) { - const selector = createSelector( - wellknownApi.endpoints.configuration.select(wellknownUrl), - (result) => result?.data, - ); +export function wellknownSelector(wellknownUrl: string, state: S) { + const selector = createWellknownSelector(wellknownUrl); return selector(state); } diff --git a/packages/sdk-effects/oidc/src/lib/wellknown.effects.test.ts b/packages/sdk-effects/store/src/lib/wellknown.effects.test.ts similarity index 98% rename from packages/sdk-effects/oidc/src/lib/wellknown.effects.test.ts rename to packages/sdk-effects/store/src/lib/wellknown.effects.test.ts index cbf7033e30..ab62f71017 100644 --- a/packages/sdk-effects/oidc/src/lib/wellknown.effects.test.ts +++ b/packages/sdk-effects/store/src/lib/wellknown.effects.test.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. diff --git a/packages/sdk-effects/oidc/src/lib/wellknown.effects.ts b/packages/sdk-effects/store/src/lib/wellknown.effects.ts similarity index 98% rename from packages/sdk-effects/oidc/src/lib/wellknown.effects.ts rename to packages/sdk-effects/store/src/lib/wellknown.effects.ts index 3836173422..f58c36c625 100644 --- a/packages/sdk-effects/oidc/src/lib/wellknown.effects.ts +++ b/packages/sdk-effects/store/src/lib/wellknown.effects.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. diff --git a/packages/sdk-effects/store/tsconfig.json b/packages/sdk-effects/store/tsconfig.json new file mode 100644 index 0000000000..3a5af05d8e --- /dev/null +++ b/packages/sdk-effects/store/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "nx": { + "addTypecheckTarget": false + } +} diff --git a/packages/sdk-effects/store/tsconfig.lib.json b/packages/sdk-effects/store/tsconfig.lib.json new file mode 100644 index 0000000000..79c8dd7977 --- /dev/null +++ b/packages/sdk-effects/store/tsconfig.lib.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [{ "path": "../../sdk-types/tsconfig.lib.json" }], + "exclude": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ] +} diff --git a/packages/sdk-effects/store/tsconfig.spec.json b/packages/sdk-effects/store/tsconfig.spec.json new file mode 100644 index 0000000000..c8fc6b21fa --- /dev/null +++ b/packages/sdk-effects/store/tsconfig.spec.json @@ -0,0 +1,41 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "nodenext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "importHelpers": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/packages/sdk-effects/store/vite.config.ts b/packages/sdk-effects/store/vite.config.ts new file mode 100644 index 0000000000..57986fd5ea --- /dev/null +++ b/packages/sdk-effects/store/vite.config.ts @@ -0,0 +1,43 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/packages/sdk-effects/store', + plugins: [], + test: { + watch: false, + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + include: ['src/**/*.{js,ts}'], + exclude: [ + 'src/**/*.mock.{js,ts}', + 'src/**/*.data.{js,ts}', + 'src/**/*.test.{js,ts}', + 'coverage/**', + 'dist/**', + '**/node_modules/**', + '**/[.]**', + 'packages/*/test?(s)/**', + '**/*.d.ts', + '**/virtual:*', + '**/__x00__*', + '**/ *', + 'cypress/**', + 'test?(s)/**', + 'test?(-*).?(c|m)[jt]s?(x)', + '**/*{.,-}{test,spec,bench,benchmark}?(-d).?(c|m)[jt]s?(x)', + '**/__tests__/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*', + '**/vitest.{workspace,projects}.[jt]s?(on)', + '**/.{eslint,mocha,prettier}rc.{?(c|m)js,yml}', + ], + reporter: ['text', 'html', 'json'], + enabled: Boolean(process.env['CI']), + reportsDirectory: './coverage', + provider: 'v8' as const, + }, + }, +})); diff --git a/packages/sdk-types/src/index.ts b/packages/sdk-types/src/index.ts index 7d5ffabb36..54036b802a 100644 --- a/packages/sdk-types/src/index.ts +++ b/packages/sdk-types/src/index.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * Copyright (c) 2025 - 2026 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. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9cd3e060fe..a898d00991 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -446,6 +446,9 @@ importers: '@forgerock/sdk-request-middleware': specifier: workspace:* version: link:../sdk-effects/sdk-request-middleware + '@forgerock/sdk-store': + specifier: workspace:* + version: link:../sdk-effects/store '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types @@ -490,12 +493,12 @@ importers: '@forgerock/sdk-logger': specifier: workspace:* version: link:../sdk-effects/logger - '@forgerock/sdk-oidc': - specifier: workspace:* - version: link:../sdk-effects/oidc '@forgerock/sdk-request-middleware': specifier: workspace:* version: link:../sdk-effects/sdk-request-middleware + '@forgerock/sdk-store': + specifier: workspace:* + version: link:../sdk-effects/store '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types @@ -542,6 +545,9 @@ importers: '@forgerock/sdk-request-middleware': specifier: workspace:* version: link:../sdk-effects/sdk-request-middleware + '@forgerock/sdk-store': + specifier: workspace:* + version: link:../sdk-effects/store '@forgerock/sdk-types': specifier: workspace:* version: link:../sdk-types @@ -599,6 +605,15 @@ importers: specifier: workspace:* version: link:../../sdk-types + packages/sdk-effects/store: + dependencies: + '@forgerock/sdk-types': + specifier: workspace:* + version: link:../../sdk-types + '@reduxjs/toolkit': + specifier: 'catalog:' + version: 2.10.1 + packages/sdk-types: {} packages/sdk-utilities: diff --git a/tasks/plans/2026-07-28-centralize-redux-stores-rework.md b/tasks/plans/2026-07-28-centralize-redux-stores-rework.md new file mode 100644 index 0000000000..e0f58ac8f9 --- /dev/null +++ b/tasks/plans/2026-07-28-centralize-redux-stores-rework.md @@ -0,0 +1,383 @@ +# Plan: Rework `centralize-redux-stores` + +**Branch:** `centralize-redux-stores` +**Base:** `493f2643a` (`chore(copyright): add sync-header-years script`) +**Status:** Decisions locked — ready to implement +**Nothing on this branch is released yet**, so every public signature below is still free to change without a breaking changeset. + +--- + +## 1. Why we're reworking + +The branch has the right idea — one `wellknownApi`, one Redux store shared across SDK clients — but three things must change before it ships. + +### 1.1 OIDC token traffic is silently routed through DaVinci's request middleware + +Every `oidcApi` endpoint resolves its middleware and logger from the store's thunk `extraArgument`: + +```ts +// packages/oidc-client/src/lib/oidc.api.ts:45, 114, 190, 274, 317, 404, 459, 518, 568 +const { requestMiddleware, logger } = api.extra as Extras; +``` + +On the shared path that `extra` belongs to **davinci's** store (`davinci-client/src/lib/client.store.utils.ts:66-72`). So: + +- Middleware registered for DaVinci flows now executes against `AUTHORIZE`, `PAR`, `TOKEN_EXCHANGE`, `REVOKE`, `USER_INFO`, and `END_SESSION` requests. `ActionTypes` is a single flat union across all three products (`sdk-request-middleware/src/lib/request-mware.derived.ts`), so a middleware that doesn't defensively `switch (action.type)` mutates the authorization-code exchange. +- `oidc({ logger: { level: 'debug' } }, sharedStore)` is silently ignored — the store's `extra.logger` is davinci's, at davinci's level. This one isn't even warned about. + +The current mitigation (`oidc-client/src/lib/client.store.ts:82-87`) is a runtime `log.warn` that _instructs_ the user into the leak: + +> "Pass request middleware to the davinci() or journey() factory that owns the store." + +`api.extra` as an implicit, store-wide DI channel is the root cause. The shared-store feature exposed it. + +### 1.2 The cross-package contract is enforced by nothing + +`InjectableStore` is declared three times — twice identically, once as a structurally weaker hand-mirror: + +```ts +// davinci-client/src/lib/client.store.utils.ts:150 +// journey-client/src/lib/client.store.utils.ts:442 +export interface InjectableStore { + readonly store: ReturnType>; + readonly rootReducer: typeof rootReducer; + readonly dynamicMiddleware: ReturnType; +} + +// oidc-client/src/lib/client.store.utils.ts:21 ← weaker copy, no compile-time link +interface InjectableStore { + readonly store: ReturnType; + readonly rootReducer: { inject: (api: unknown) => void }; + readonly dynamicMiddleware: { addMiddleware: (...mw: unknown[]) => void }; +} +``` + +Producer and consumer are joined only by `as unknown as`. Rename `dynamicMiddleware` in davinci and TypeScript says nothing; `oidc()` throws `Cannot read properties of undefined` from inside a factory. + +Stacked on top: `SdkStore`'s brand is fictional (`__sdkStoreBrand: symbol` exists on no runtime object), and `toSdkStore` returns `object` — a no-op cast that forces a _second_ `as SdkStore` at each call site. + +```ts +// davinci-client/src/lib/client.store.utils.ts:135 +export function toSdkStore(injectable: InjectableStore): object { + return injectable as unknown as object; // InjectableStore is already assignable to object +} +// client.store.ts:120 +store: toSdkStore(injectable) as SdkStore, // second cast +``` + +Four unchecked casts to move one object across a package boundary, in a repo whose architecture is otherwise compiler-enforced. + +### 1.3 Test suite is green but proves little + +`shared-store.test.ts` hand-builds a fake handle from RTK primitives (`makeSharedStore`, line 776) specifically to avoid importing davinci/journey. **Every test passes even if `davinci()`'s handle has a completely different shape** — the one thing the branch needs to prove is the one thing untested. Details in §5. + +--- + +## 2. Decisions — all resolved + +| # | Decision | Resolution | +| ------ | ------------------------------------ | -------------------------------------------------------------------------------------------- | +| **D1** | Store ownership model | All three modes supported; `createSdkStore()` folded into a renamed `@forgerock/sdk-store` | +| **D2** | `requestMiddleware`/`logger` scoping | **Per-client registry** in `extraArgument`, keyed by `reducerPath` | +| **D3** | Public API shape | Options-object, key `store`. No mutual exclusion — `requestMiddleware` is honored per client | +| **D4** | Two `oidc()` clients on one store | Detect and **error** on a second injection with a different `clientId` | +| **D5** | Delivery | Rework in place on `centralize-redux-stores` | + +### D1 — Store ownership: three modes, none mandatory + +The zero-config path stays zero-config. + +| Mode | Call shape | Who owns the store | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **1 — Implicit** (existing, unchanged) | `oidc({ config })` | The client. Each factory builds its own store when none is passed. Default path; must behave exactly as it does today. | +| **2 — Client-owned, shared** | `const dv = await davinci({ config });`
`await oidc({ config, store: dv.store })` | `davinci()`/`journey()`. They continue to expose a `store` handle on the returned client. **Primary sharing story.** | +| **3 — Consumer-owned** (opt-in) | `const store = createSdkStore();`
`await davinci({ config, store });`
`await oidc({ config, store })` | The application. Offered for consumers who want lifecycle control; never required. | + +- `davinci()`/`journey()` **keep** the public `store: SdkStore` field added on this branch. The api-report additions stay. +- `createSdkStore()` is an _additional_ export, not a new required step. Escape hatch, not headline. +- All three modes converge on the same `SdkStoreHandle` contract (§3), so there's one code path to test, not three. +- Mode 1 must be provably unaffected — it's the path every existing consumer is on. Regression coverage in §5.3. + +**Package location:** rename `packages/sdk-effects/wellknown` → `packages/sdk-effects/store`, published as `@forgerock/sdk-store`, exporting both `wellknownApi` and `createSdkStore()`. This avoids publishing two brand-new artifacts in one release. Since `@forgerock/sdk-wellknown` has never been published, the rename costs nothing. + +### D2 — Per-client `extra` registry + +`extraArgument` becomes a keyed registry; each `*.api.ts` reads **only its own slot**. + +```ts +// @forgerock/sdk-store +export interface ClientExtra { + readonly requestMiddleware?: RequestMiddleware[]; + readonly logger: ReturnType; +} + +export interface SdkStoreExtra { + /** Keyed by the owning api's `reducerPath`. Mutable slots, stable object identity. */ + readonly clients: Record; +} + +/** Pure resolver used by every *.api.ts. */ +export function clientExtra(extra: unknown, reducerPath: string): ClientExtra; +``` + +Each api file replaces `api.extra as Extras` with `clientExtra(api.extra, oidcApi.reducerPath)` — ~9 sites in `oidc.api.ts`, ~10 in `davinci.api.ts`, 3 in `journey.api.ts`. Mechanical, and it deletes three duplicated private `Extras` interfaces. + +`extraArgument` is a stable object reference held by `configureStore`, so `injectClient()` filling in a new slot at injection time is visible to every subsequent request. That's what makes per-client scoping work on a store created before the second client existed. + +**Consequences — this is why D3 has no mutual exclusion:** + +- `oidc({ config, store, requestMiddleware })` now works _correctly_. oidc's middleware lands in the `oidc` slot and applies only to OIDC endpoints. +- The `log.warn` at `client.store.ts:82-87` is **deleted**, not hardened. The condition it warns about is no longer a problem. +- Its two tests (`'warns when requestMiddleware is passed alongside sharedStore'`, `'does not warn when...'`) are **deleted and replaced** by the inverse assertions: oidc's middleware _is_ invoked for OIDC endpoints, and is _not_ invoked for DaVinci endpoints. +- The `logger` drop is fixed for free. + +### D3 — Options-object, key `store` + +```ts +export async function oidc(input: { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; // honored in all three modes, per D2 + logger?: { level: LogLevel; custom?: CustomLogger }; + storage?: Partial; + store?: SdkStore; // omit → mode 1 +}): Promise<…>; +``` + +Consistent with every other factory in the SDK, self-documenting at the call site, and extensible. Replaces the positional second parameter currently in `api-report/oidc-client.api.md`. `davinci()` and `journey()` gain the same optional `store` input. + +### D4 — Error on conflicting re-injection + +`oidcApi.reducerPath` is the fixed string `'oidc'`. A second `oidc()` against the same store with a _different_ `clientId` would silently share one cache slice and clobber token state. Detect it and return a typed SDK error; document as unsupported. + +Re-injecting with the **same** `clientId` stays idempotent — that's a legitimate re-init. + +Namespacing per `clientId` (which would genuinely support multi-tenant and step-up auth) is explicitly out of scope. Record it as a known limitation in the READMEs. + +### D5 — Rework in place + +Nothing is released, so history on `centralize-redux-stores` can be rewritten freely. Single PR, single review. Phase 2 is large but behavior-preserving; the full e2e run is the safety net. + +--- + +## 3. Target architecture + +``` +sdk-types/ + store.types.ts SdkStore, SdkStoreHandle, ClientExtra, SdkStoreExtra ← declared ONCE + +sdk-effects/store/ ← renamed from sdk-effects/wellknown, published @forgerock/sdk-store + store.effects.ts createSdkStore(), injectClient() ← effectful, correctly named + store.utils.ts clientExtra(), isSdkStoreHandle() ← pure + wellknown.api.ts wellknownApi + selectors (as extracted on this branch) + +davinci-client/ journey-client/ oidc-client/ + client.store.ts factory accepts optional `store` + client.store.effects.ts injection lives here, not in *.utils.ts + *.api.ts clientExtra(api.extra, thisApi.reducerPath) +``` + +**One contract, declared once:** + +```ts +// sdk-types/src/lib/store.types.ts +export interface SdkStoreHandle { + readonly store: Store & { dispatch: ThunkDispatch }; + readonly rootReducer: { inject: (slice: InjectableSlice) => void }; + readonly dynamicMiddleware: { addMiddleware: (...mw: Middleware[]) => void }; + readonly extra: SdkStoreExtra; +} + +/** Public-facing alias. Structural, so no cast is needed to produce or consume it. */ +export type SdkStore = SdkStoreHandle; +``` + +**Deleted by this change:** + +- `toSdkStore` × 2, `fromSdkStore` × 3 (two of which are **already dead exports** in publishable packages) +- The three `InjectableStore` declarations +- The three private `Extras` interfaces +- The fictional `__sdkStoreBrand` +- The `requestMiddleware`-ignored `log.warn` and its two tests (obsolete under D2) +- All 4 `as unknown as` casts on the handle path, plus `store as unknown as ReturnType` (`oidc-client/src/lib/client.store.utils.ts:45`) +- Dead exports `JourneyStore`, `fromSdkStore` × 2 + +--- + +## 4. Implementation phases + +Strict RED → GREEN → REFACTOR → GREEN. Each phase ends green and committable. + +### Phase 0 — Reset + +- [ ] `git switch -c centralize-redux-stores-v2` from `493f2643a`; cherry-pick what's worth keeping (the wellknown extraction, tsconfig/nx wiring, api-report regeneration). +- [ ] Baseline green: `pnpm exec nx affected -t build lint test`. + +### Phase 1 — `@forgerock/sdk-store` (rename + harden) + +The extraction was correct; it shipped untested and mis-versioned. + +- [ ] Rename `packages/sdk-effects/wellknown` → `packages/sdk-effects/store`; package name → `@forgerock/sdk-store`; update the 3 consumer `package.json`s, all tsconfig references, root `tsconfig.json`, and the changeset. +- [ ] **RED** — `wellknown.api.test.ts`: + - one cache entry per distinct URL; **exactly one** network call for two identical requests + - a second distinct URL produces a second entry, not a cache hit + - `queryFn` maps a non-2xx into `FetchBaseQueryError`, not a thrown rejection + - `createWellknownSelector` returns `undefined` before fetch, data after + - `wellknownSelector` returns the same reference across calls once cached +- [ ] **GREEN** — fix `wellknownSelector` memoization (below). +- [ ] Packaging: `version` → `0.0.0`; add `README.md`; add the license header to `eslint.config.mjs`; fix `vite.config.ts` `cacheDir`; drop `passWithNoTests`. + +`wellknownSelector` currently rebuilds its memoized selector on every call, so the cache is always cold — across 8 call sites in `oidc-client/src/lib/client.store.ts`: + +```ts +// before — memoization never hits +export function wellknownSelector(url: string, state: S) { + return createWellknownSelector(url)(state); +} + +// after — one selector per URL +const selectorCache = new Map>(); +export function wellknownSelector(url: string, state: S) { + let selector = selectorCache.get(url); + if (!selector) { + selector = createWellknownSelector(url); + selectorCache.set(url, selector); + } + return selector(state); +} +``` + +**Proceeding with the `Map`** unless flagged. It's module state in a package marked `sideEffects: false`, but it's a pure cache — same input always yields the same selector, and it's unobservable from outside. + +### Phase 2 — Per-client `extra` (D2) + +No behavior change while standalone. Do this **before** any sharing so the sharing phase inherits correct scoping. + +- [ ] **RED** — `clientExtra()` unit tests: returns the slot for a known `reducerPath`; returns an empty-middleware default for an unknown one; never returns another client's slot. +- [ ] **RED** — **the §1.1 regression test.** Per package: a middleware registered for client X is not invoked for client Y's endpoints. Must fail before the fix. +- [ ] **RED** — logger isolation: `oidc({ logger: { level: 'debug' }, store })` uses oidc's level, not the owner's. +- [ ] **GREEN** — add `SdkStoreExtra`/`ClientExtra` to `sdk-types` and `clientExtra()` to `sdk-store`; migrate all three `configureStore` calls to `extraArgument: { clients: { [thisApi.reducerPath]: { requestMiddleware, logger } } }`; replace all ~22 `api.extra as Extras` sites; delete the three private `Extras` interfaces. +- [ ] **REFACTOR** — delete the `requestMiddleware` `log.warn` and its two tests; replace with the inverse assertions. +- [ ] State-shape assertion per package: `expect(Object.keys(store.getState()).sort()).toEqual([...])`. + +### Phase 3 — The shared store contract (D1, D3, §1.2) + +- [ ] **RED** — a **real** cross-package integration test covering all three D1 modes (§5.1). Must fail before implementation. +- [ ] **RED** — mode-1 regression: `oidc({ config })` with no `store` builds its own store and behaves byte-identically to `main`. +- [ ] **RED** — `isSdkStoreHandle()` guard: `{}` / `null` / a plain object passed as `store` yields a typed SDK error, not a `TypeError`. +- [ ] **GREEN** — declare `SdkStoreHandle` once in `sdk-types`; add `createSdkStore()` + `injectClient()` to `sdk-store`; all three factories accept optional `store` and produce/consume the handle **structurally, with zero casts**. +- [ ] **GREEN** — D3: move `sharedStore` from positional arg into the options object as `store`. `davinci()`/`journey()` gain the same input and keep exposing `store: SdkStore`. +- [ ] **REFACTOR** — delete `toSdkStore`/`fromSdkStore`/`InjectableStore` × 3/`__sdkStoreBrand`; move injection out of `*.utils.ts` into `client.store.effects.ts` per `AGENTS.md` ("never put effectful logic in `*.utils.ts`"). +- [ ] Type `createDynamicMiddleware()` so `addMiddleware` is actually checked — the untyped call is how the weak `(...mw: unknown[]) => void` mirror slipped through. + +### Phase 4 — Lifecycle correctness (D4) + +- [ ] **RED** — `oidc({ config: /* no wellknown */, store })` returns an error **and leaves the store unmutated**. Today injection happens at `client.store.ts:88`, before the guards at `:91` and `:98`, and RTK's `inject` is irreversible — a failed call permanently contaminates the caller's store. +- [ ] **GREEN** — validate all arguments, then inject. +- [ ] **RED/GREEN** — D4: second `oidc()` on the same store with a _different_ `clientId` returns a typed error; with the _same_ `clientId` stays idempotent. + +### Phase 5 — Docs, packaging, e2e + +- [ ] READMEs: `oidc-client`, `davinci-client`, `journey-client` — all three D1 modes with a worked example each; an explicit statement that each client owns its own middleware/logger (D2); the D4 single-`clientId`-per-store limitation. Plus the new `sdk-store` README from Phase 1. +- [ ] Rewrite the changeset: new package + three changed public signatures + per-client middleware scoping. Name the middleware/logger semantics explicitly. +- [ ] Regenerate api-reports. Expected additions: `store: SdkStore` on the davinci/journey clients (kept — mode 2), the new `store?` input on all three factories, `createSdkStore`/`clientExtra` on `sdk-store`. Nothing should be _removed_. +- [ ] E2E: one suite where a single app shares a store across davinci + oidc and asserts a single `.well-known` request over the wire. (`CLAUDE.md`: e2e is part of done unless untestable end-to-end.) +- [ ] Remove dead exports: `fromSdkStore` × 2, `JourneyStore`. +- [ ] Full CI parity: `pnpm exec nx affected -t build lint test e2e-ci`; `pnpm nx sync:check`. + +--- + +## 5. Test plan + +### 5.1 Replace the fake handle with a real one + +`makeSharedStore()` (`shared-store.test.ts:776`) constructs a lookalike from RTK primitives. It cannot detect contract drift, which is the single failure mode this feature has. Every D1 mode needs a real test: + +```ts +// Mode 1 — implicit (regression: the path all existing consumers are on) +await oidc({ config: oidcConfig }); +expect(wellknownFetchCount).toBe(1); + +// Mode 2 — client-owned, shared (primary sharing story) +const dv = await davinci({ config: davinciConfig }); +await oidc({ config: oidcConfig, store: dv.store }); +expect(wellknownFetchCount).toBe(1); // davinci fetched it; oidc hits cache +expect(Object.keys(dvState())).toContain('oidc'); + +// Mode 3 — consumer-owned (opt-in) +const store = createSdkStore(); +await davinci({ config: davinciConfig, store }); +await oidc({ config: oidcConfig, store }); +expect(wellknownFetchCount).toBe(1); +``` + +Mode 2 catches contract drift between `davinci()`'s real handle and what `oidc()` consumes — the gap that makes the current suite meaningless. Mode 1 catches regressions for shipped consumers. + +**Placement:** an e2e suite, so no `scope:package` → `scope:package` boundary is crossed and the assertion is made against real network traffic. If a faster unit-level version is wanted too, add `packages/integration-tests` rather than relaxing the lint rule for `*.test.ts`. + +### 5.2 Fix the tests that assert nothing + +| Test | Problem | Fix | +| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `'subscribe() fires when oidcApi state changes'` (:936) | Dispatches `{ type: 'test/action' }` — not an `oidcApi` action. Redux notifies **every** subscriber on **every** dispatch, so this passes unconditionally and the title is false. | Dispatch a real `oidcApi` action; assert on the resulting slice state. | +| `'reuses cached wellknown response'` (:952) | `expect(count).toBe(fetchesAfterOwnerInit)` is `0 === 0` if the owner's fetch silently failed. | `expect(count).toBe(1)`. | +| `'fetches wellknown during init'` (:900) | `toBeGreaterThan(0)` — but "exactly one" is the property that matters for a dedupe feature. | `toBe(1)`. | +| `'is idempotent'` (:857) | Injects three times (twice in the `not.toThrow` block, once more after). | Assert 2 injections explicitly; assert cache contents unchanged, not just "didn't throw". | +| Arrange blocks (:946, :956) | Call the internal `injectIntoStore` to obtain a typed store, coupling tests to internals. | Go through the public factory. | +| `'warns when requestMiddleware is passed alongside sharedStore'` + its negative twin | Tests a warning that D2 makes obsolete. | **Delete both.** Replace with: oidc's middleware _is_ invoked for OIDC endpoints on the shared path, and is _not_ invoked for DaVinci endpoints. | + +### 5.3 New coverage + +- `@forgerock/sdk-store`: currently zero tests behind `passWithNoTests: true` (Phase 1). +- **Middleware isolation** — DaVinci middleware not invoked for `TOKEN_EXCHANGE` (Phase 2). This is the §1.1 regression test. +- **Logger isolation** — `oidc({ logger: { level: 'debug' }, store })` uses oidc's level. +- **Middleware honored on the shared path** — the inverse of the deleted warn tests. +- Failure path leaves the shared store clean (Phase 4). +- D4: conflicting `clientId` errors; matching `clientId` is idempotent. +- Invalid handle → typed SDK error, not `TypeError`. +- **Mode-1 regression suite** — every existing `oidc()`/`davinci()`/`journey()` test must pass untouched. If a test needs editing to accommodate the new `store` option, that's a signal we broke the default path. +- State-shape assertions per package, guarding the implicit `combineSlices` keying (verified correct today: `config`, `node`, `davinci`, `journeyReducer`, `wellknown` — but it's now an implicit invariant off `slice.name`, where it used to be an explicit literal map). + +--- + +## 6. Risk register + +| Risk | Mitigation | +| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| ~22-site `api.extra` migration touches every network call in the SDK | Behavior-preserving by construction; RED tests first; full e2e run; Phase 2 lands as its own commit for reviewability | +| `combineSlices` reshapes published state if a `slice.name` is ever renamed | State-shape assertion test per package (Phase 2) | +| RTK `inject` is irreversible — no teardown story | Phase 4 validate-before-inject; D4 errors on conflicting re-injection; documented limitation | +| Package rename ripples through 3 `package.json`s + all tsconfig refs | Never published, so no consumer impact; `pnpm nx sync:check` catches missed references | +| Mode 1 regression for existing consumers | Dedicated regression suite; "no existing test needed editing" is a DoD gate | + +--- + +## 7. What was already right on this branch + +Worth preserving through the rework: + +- Extracting `wellknownApi` into one canonical instance — the "single cache entry per URL" rationale in the file header is exactly the correct framing. +- `combineSlices().withLazyLoadedSlices()` + `createDynamicMiddleware()` is the idiomatic RTK 2.x mechanism. Not a homegrown `replaceReducer` hack. +- Generalizing `wellknownSelector` from a concrete `RootState` to `` — clean decoupling, keep it. +- Collapsing `ReturnType['getState']>` into named `DavinciStore` / `RootState` aliases — genuinely more readable at 4 call sites. +- `sdk-types/store.types.ts` is correctly type-only per the `*.types.ts` convention. +- TS project references, root `tsconfig.json` refs, and Nx tags are wired correctly and consistently with siblings. +- The `requestMiddleware` warning shows the coupling _was_ noticed — the fix just needs to go a layer deeper. + +--- + +## 8. Definition of done + +- [ ] Zero `as unknown as` on the store-handle path +- [ ] `SdkStoreHandle` declared exactly once +- [ ] No client's `requestMiddleware` or `logger` reaches another client's endpoints, with a test proving it +- [ ] Each client's own `requestMiddleware` and `logger` work in all three modes +- [ ] All three D1 modes tested: implicit, client-owned-shared, consumer-owned +- [ ] Mode 1 byte-compatible with `main` — no existing test needed editing to keep passing +- [ ] A test that fails if `davinci()`'s real handle drifts from what `oidc()` consumes +- [ ] Injection is effectful code in `*.effects.ts`, not `*.utils.ts` +- [ ] Failed factory calls leave a shared store unmutated +- [ ] Conflicting `clientId` on one store errors; matching `clientId` is idempotent +- [ ] `@forgerock/sdk-store` has tests, a README, and a correct first version (`0.0.0`) +- [ ] Three package READMEs document all three modes, the middleware/logger semantics, and the D4 limitation +- [ ] E2E proves one `.well-known` request across two clients sharing a store +- [ ] `pnpm exec nx affected -t build lint test e2e-ci` and `pnpm nx sync:check` both green diff --git a/tsconfig.json b/tsconfig.json index c22692ddac..b6b3551923 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,6 +84,9 @@ }, { "path": "./tools/api-report" + }, + { + "path": "./packages/sdk-effects/store" } ] } From a8df7419a091c13ec2e3a94c00a80760aaead3b1 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 29 Jul 2026 16:03:17 -0600 Subject: [PATCH 2/8] fix(davinci-client): validate config before attaching shared store --- .../davinci-client/src/lib/client.store.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 2e7b29264a..1832bbfbf2 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -23,7 +23,7 @@ import { pollingµ, getPollingModeµ } from './client.store.effects.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; import { configSlice } from './config.slice.js'; -import { wellknownApi } from '@forgerock/sdk-store'; +import { wellknownApi, isSdkStoreHandle } from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { SdkStore } from '@forgerock/sdk-store'; @@ -88,12 +88,15 @@ export async function davinci({ level: logger?.level ?? config.log ?? 'error', custom: logger?.custom, }); - const handle = createClientStore({ requestMiddleware, logger: log, store: sharedStore }); - const store = handle.store; - const serverInfo = createStorage({ - type: 'localStorage', - name: 'serverInfo', - }); + + if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { + const message = + 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + + 'another SDK client, or one created with `createSdkStore()`.'; + log.error(message); + throw new Error(message); + } + if (!config.serverConfig.wellknown) { const error = new Error( '`wellknown` property is a required as part of the `config.serverConfig`', @@ -108,6 +111,13 @@ export async function davinci({ throw error; } + const handle = createClientStore({ requestMiddleware, logger: log, store: sharedStore }); + const store = handle.store; + const serverInfo = createStorage({ + type: 'localStorage', + name: 'serverInfo', + }); + const { data: openIdResponse, error: fetchError } = await store.dispatch( wellknownApi.endpoints.configuration.initiate(config.serverConfig.wellknown), ); From 6264f6ad48ef1101fff8408fd0083c1d09dbc55d Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 29 Jul 2026 16:05:18 -0600 Subject: [PATCH 3/8] fix(journey-client): validate config before attaching shared store --- packages/journey-client/src/lib/client.store.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index e91cd86dfb..df42229842 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -24,7 +24,7 @@ import { createStorage } from '@forgerock/storage'; import * as Either from 'effect/Either'; import { createJourneyObject, parseJourneyResponse } from './journey.utils.js'; import type { JourneyResult } from './journey.utils.js'; -import { wellknownApi } from '@forgerock/sdk-store'; +import { wellknownApi, isSdkStoreHandle } from '@forgerock/sdk-store'; import type { JourneyStep } from './step.utils.js'; import type { JourneyClientConfig } from './config.types.js'; @@ -121,8 +121,13 @@ export async function journey({ ); } - const handle = createJourneyStore({ requestMiddleware, logger: log, store: sharedStore }); - const store = handle.store; + if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { + const message = + 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + + 'another SDK client, or one created with `createSdkStore()`.'; + log.error(message); + throw new Error(message); + } const { wellknown } = config.serverConfig; @@ -132,6 +137,9 @@ export async function journey({ throw new Error(message); } + const handle = createJourneyStore({ requestMiddleware, logger: log, store: sharedStore }); + const store = handle.store; + const { data: wellknownResponse, error: fetchError } = await store.dispatch( wellknownApi.endpoints.configuration.initiate(wellknown), ); From ec888bfaf8e47a1238922089ae077a4483d37ecb Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 29 Jul 2026 16:05:44 -0600 Subject: [PATCH 4/8] fix(store): prevent double middleware registration in injectClient --- packages/sdk-effects/store/src/lib/store.effects.test.ts | 2 ++ packages/sdk-effects/store/src/lib/store.effects.ts | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sdk-effects/store/src/lib/store.effects.test.ts b/packages/sdk-effects/store/src/lib/store.effects.test.ts index c0ecc3008a..a5de9b56b3 100644 --- a/packages/sdk-effects/store/src/lib/store.effects.test.ts +++ b/packages/sdk-effects/store/src/lib/store.effects.test.ts @@ -179,6 +179,7 @@ describe('injectClient', () => { it('is idempotent for repeated injection of the same client', () => { // Arrange const handle = createSdkStore(); + const spy = vi.spyOn(handle.dynamicMiddleware, 'addMiddleware'); // Act injectClient(handle, { api: fakeApi, reducerPath: fakeApi.reducerPath }); @@ -187,6 +188,7 @@ describe('injectClient', () => { // Assert expect(Object.keys(handle.store.getState() as object).sort()).toEqual(afterFirst); + expect(spy).toHaveBeenCalledTimes(1); // middleware must not be double-registered }); it('throws a descriptive error when handed something that is not a handle', () => { diff --git a/packages/sdk-effects/store/src/lib/store.effects.ts b/packages/sdk-effects/store/src/lib/store.effects.ts index c799403ff3..195b952a48 100644 --- a/packages/sdk-effects/store/src/lib/store.effects.ts +++ b/packages/sdk-effects/store/src/lib/store.effects.ts @@ -117,7 +117,10 @@ export function injectClient>( } const addMiddleware = handle.dynamicMiddleware.addMiddleware as (mw: unknown) => unknown; - addMiddleware(api.middleware); + const alreadyInjected = reducerPath in (handle.extra.clients as Record); + if (!alreadyInjected) { + addMiddleware(api.middleware); + } // The registry is readonly to consumers but mutable here by design: this is // the only place a slot is created, and it must work on a store built earlier. From 46bcfd251408915d8816f5d5f645e8128c3ebe11 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 29 Jul 2026 16:06:34 -0600 Subject: [PATCH 5/8] refactor(store): remove unused createStoreExtra export --- .../api-report/davinci-client.api.md | 4444 +++++++++-------- .../api-report/davinci-client.types.api.md | 4438 ++++++++-------- packages/sdk-effects/store/src/index.ts | 3 +- .../sdk-effects/store/src/lib/store.utils.ts | 29 - ...26-07-28-centralize-redux-stores-rework.md | 383 -- 5 files changed, 4860 insertions(+), 4437 deletions(-) delete mode 100644 tasks/plans/2026-07-28-centralize-redux-stores-rework.md diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index 128a991038..f476aa69ae 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -1,2013 +1,2431 @@ -## API Report File for "@forgerock/davinci-client" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; -import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import { CustomLogger } from '@forgerock/sdk-logger'; -import type { DaVinciConfig } from '@forgerock/sdk-types'; -import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; -import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; -import { GenericError } from '@forgerock/sdk-types'; -import { LogLevel } from '@forgerock/sdk-logger'; -import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; -import { QueryStatus } from '@reduxjs/toolkit/query'; -import { Reducer } from '@reduxjs/toolkit'; -import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import type { SdkStore } from '@forgerock/sdk-store'; -import { SerializedError } from '@reduxjs/toolkit'; -import { Unsubscribe } from '@reduxjs/toolkit'; - -// @public (undocumented) -export type ActionCollector = ActionCollectorNoUrl | ActionCollectorWithUrl; - -// @public (undocumented) -export interface ActionCollectorNoUrl { - // (undocumented) - category: 'ActionCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type ActionCollectors = ActionCollectorWithUrl<'IdpCollector'> | ActionCollectorNoUrl<'ActionCollector'> | ActionCollectorNoUrl<'FlowCollector'> | ActionCollectorNoUrl<'SubmitCollector'>; - -// @public -export type ActionCollectorTypes = 'FlowCollector' | 'SubmitCollector' | 'IdpCollector' | 'ActionCollector'; - -// @public (undocumented) -export interface ActionCollectorWithUrl { - // (undocumented) - category: 'ActionCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - url?: string | null; - }; - // (undocumented) - type: T; -} - -export { ActionTypes } - -// @public (undocumented) -export type AgreementField = { - type: 'AGREEMENT'; - key: string; - content: string; - titleEnabled: boolean; - title?: string; - agreement: { - id: string; - useDynamicAgreement: boolean; - }; - enabled: boolean; -}; - -// @public (undocumented) -export interface AssertionValue extends Omit { - // (undocumented) - rawId: string; - // (undocumented) - response: { - clientDataJSON: string; - authenticatorData: string; - signature: string; - userHandle: string | null; - }; -} - -// @public (undocumented) -export interface AttestationValue extends Omit { - // (undocumented) - rawId: string; - // (undocumented) - response: { - clientDataJSON: string; - attestationObject: string; - }; -} - -// @public (undocumented) -export interface AutoCollector> { - // (undocumented) - category: C; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: IV; - type: string; - validation?: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - type: string; - config: OV; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector'; - -// @public (undocumented) -export type AutoCollectors = ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | MetadataCollector | PollingCollector | SingleValueAutoCollector | ObjectValueAutoCollector; - -// @public (undocumented) -export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes; - -// @public (undocumented) -export interface BooleanCollector extends SingleValueCollectorWithValue<'BooleanCollector', boolean> { - // (undocumented) - output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { - appearance: string; - richContent?: CollectorRichContent; - }; -} - -// @public (undocumented) -export interface CollectorErrors { - // (undocumented) - code: string; - // (undocumented) - message: string; - // (undocumented) - target: string; -} - -// @public -export interface CollectorRichContent { - // (undocumented) - content: string; - // (undocumented) - replacements: RichContentLink[]; -} - -// @public (undocumented) -export type Collectors = FlowCollector | MetadataCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | ImageCollector | UnknownCollector; - -// @public -export type CollectorValueType = T extends { - type: 'PasswordCollector'; -} | { - type: 'ValidatedPasswordCollector'; -} | { - type: 'SingleSelectCollector'; -} | { - type: 'DeviceRegistrationCollector'; -} | { - type: 'DeviceAuthenticationCollector'; -} | { - type: 'ProtectCollector'; -} | { - type: 'PollingCollector'; -} ? string : T extends { - type: 'TextCollector'; - category: 'SingleValueCollector'; -} ? string : T extends { - type: 'TextCollector'; - category: 'ValidatedSingleValueCollector'; -} ? string : T extends { - type: 'BooleanCollector'; -} | { - type: 'ValidatedBooleanCollector'; -} ? boolean : T extends { - type: 'MultiSelectCollector'; -} ? string[] : T extends { - type: 'PhoneNumberCollector'; -} ? PhoneNumberInputValue : T extends { - type: 'PhoneNumberExtensionCollector'; -} ? PhoneNumberExtensionInputValue : T extends { - type: 'FidoRegistrationCollector'; -} ? FidoRegistrationInputValue | GenericError : T extends { - type: 'FidoAuthenticationCollector'; -} ? FidoAuthenticationInputValue | GenericError : T extends { - type: 'MetadataCollector'; -} ? Record | MetadataError : T extends { - category: 'SingleValueCollector'; -} ? string : T extends { - category: 'ValidatedSingleValueCollector'; -} ? string : T extends { - category: 'SingleValueAutoCollector'; -} ? string : T extends { - category: 'MultiValueCollector'; -} ? string[] : T extends { - category: 'ActionCollector'; -} ? never : T extends { - category: 'NoValueCollector'; -} ? never : CollectorValueTypes; - -// @public -export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError | GenericError; - -// @public (undocumented) -export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField | MetadataField; - -// @public (undocumented) -export interface ContinueNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: 'continue'; - }; - // (undocumented) - error: null; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - eventName?: string; - status: 'continue'; - }; - // (undocumented) - status: 'continue'; -} - -export { CustomLogger } - -// @public -export type CustomPollingStatus = string & {}; - -// @public -export function davinci(input: { - config: DaVinciConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - store?: SdkStore; -}): Promise<{ - store: SdkStore; - subscribe: (listener: () => void) => Unsubscribe; - externalIdp: () => (() => Promise); - flow: (action: DaVinciAction) => InitFlow; - next: (args?: DaVinciRequest) => Promise; - resume: (input: { - continueToken: string; - }) => Promise; - start: (options?: StartOptions | undefined) => Promise; - update: (collector: T) => Updater; - validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; - pollStatus: (collector: PollingCollector) => Poller; - getClient: () => { - status: "start"; - } | { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: "continue"; - } | { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: "error"; - } | { - authorization?: { - code?: string; - state?: string; - }; - status: "success"; - } | { - status: "failure"; - } | null; - getCollectors: () => Collectors[]; - getError: () => DaVinciError | null; - getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; - getServer: () => { - _links?: Links; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - eventName?: string; - status: "continue"; - } | { - status: "start"; - } | { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: "error"; - } | { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - session?: string; - status: "success"; - } | { - _links?: Links; - eventName?: string; - href?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: "failure"; - } | null; - cache: { - getLatestResponse: () => ({ - status: QueryStatus.fulfilled; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "data" | "fulfilledTimeStamp"> & Required> & { - error: undefined; - } & { - status: QueryStatus.fulfilled; - isUninitialized: false; - isLoading: false; - isSuccess: true; - isError: false; - }) | ({ - status: QueryStatus.rejected; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "error"> & Required> & { - status: QueryStatus.rejected; - isUninitialized: false; - isLoading: false; - isSuccess: false; - isError: true; - }) | { - error: { - message: string; - type: string; - }; - }; - getResponseWithId: (requestId: string) => ({ - status: QueryStatus.fulfilled; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "data" | "fulfilledTimeStamp"> & Required> & { - error: undefined; - } & { - status: QueryStatus.fulfilled; - isUninitialized: false; - isLoading: false; - isSuccess: true; - isError: false; - }) | ({ - status: QueryStatus.rejected; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "error"> & Required> & { - status: QueryStatus.rejected; - isUninitialized: false; - isLoading: false; - isSuccess: false; - isError: true; - }) | { - error: { - message: string; - type: string; - }; - }; - }; -}>; - -// @public -export interface DaVinciAction { - // (undocumented) - action: string; -} - -// @public -export interface DaVinciBaseResponse { - // (undocumented) - capabilityName?: string; - // (undocumented) - companyId?: string; - // (undocumented) - connectionId?: string; - // (undocumented) - connectorId?: string; - // (undocumented) - id?: string; - // (undocumented) - interactionId?: string; - // (undocumented) - interactionToken?: string; - // (undocumented) - isResponseCompatibleWithMobileAndWebSdks?: boolean; - // (undocumented) - status?: string; -} - -// @public -export type DaVinciCacheEntry = { - data?: DaVinciBaseResponse; - error?: { - data: DaVinciBaseResponse; - status: number; - }; -} & { - data?: any; - error?: any; -} & MutationResultSelectorResult; - -// @public (undocumented) -export type DavinciClient = Awaited>; - -export { DaVinciConfig } - -// @public (undocumented) -export interface DaVinciError extends Omit { - // (undocumented) - collectors?: CollectorErrors[]; - // (undocumented) - internalHttpStatus?: number; - // (undocumented) - message: string; - // (undocumented) - status: 'error' | 'failure' | 'unknown'; -} - -// @public (undocumented) -export interface DaVinciErrorCacheEntry { - // (undocumented) - endpointName: 'next' | 'flow' | 'start'; - // (undocumented) - error: { - data: T; - }; - // (undocumented) - fulfilledTimeStamp: number; - // (undocumented) - isError: boolean; - // (undocumented) - isLoading: boolean; - // (undocumented) - isSuccess: boolean; - // (undocumented) - isUninitialized: boolean; - // (undocumented) - requestId: string; - // (undocumented) - startedTimeStamp: number; - // (undocumented) - status: 'fulfilled' | 'pending' | 'rejected'; -} - -// @public (undocumented) -export interface DavinciErrorResponse extends DaVinciBaseResponse { - // (undocumented) - cause?: string | null; - // (undocumented) - code: string | number; - // (undocumented) - details?: ErrorDetail[]; - // (undocumented) - doNotSendToOE?: boolean; - // (undocumented) - error?: { - code?: string; - message?: string; - }; - // (undocumented) - errorCategory?: string; - // (undocumented) - errorMessage?: string; - // (undocumented) - expected?: boolean; - // (undocumented) - httpResponseCode: number; - // (undocumented) - isErrorCustomized?: boolean; - // (undocumented) - message: string; - // (undocumented) - metricAttributes?: { - [key: string]: unknown; - }; -} - -// @public (undocumented) -export interface DaVinciFailureResponse extends DaVinciBaseResponse { - // (undocumented) - error?: { - code?: string; - message?: string; - [key: string]: unknown; - }; -} - -// @public (undocumented) -export type DaVinciField = ComplexValueFields | MultiValueFields | ReadOnlyFields | RedirectFields | SingleValueFields; - -// @public -export interface DaVinciNextResponse extends DaVinciBaseResponse { - // (undocumented) - eventName?: string; - // (undocumented) - form?: { - name?: string; - description?: string; - components?: { - fields?: DaVinciField[]; - }; - }; - // (undocumented) - formData?: { - value?: { - [key: string]: string; - }; - }; - // (undocumented) - _links?: Links; -} - -// @public -export interface DaVinciPollResponse extends DaVinciBaseResponse { - // (undocumented) - eventName?: string; - // (undocumented) - _links?: Links; - // (undocumented) - success?: boolean; -} - -// @public (undocumented) -export interface DaVinciRequest { - // (undocumented) - eventName: string; - // (undocumented) - id: string; - // (undocumented) - interactionId: string; - // (undocumented) - parameters: { - eventType: 'submit' | 'action' | 'polling'; - data: { - actionKey: string; - formData?: Record; - }; - }; -} - -// @public -export type DaVinciRequestValueTypes = string | number | boolean | (string | number | boolean)[] | DeviceValue | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError | GenericError; - -// @public (undocumented) -export interface DaVinciSuccessResponse extends DaVinciBaseResponse { - // (undocumented) - authorizeResponse?: OAuthDetails; - // (undocumented) - environment: { - id: string; - [key: string]: unknown; - }; - // (undocumented) - _links?: Links; - // (undocumented) - resetCookie?: boolean; - // (undocumented) - session?: { - id?: string; - [key: string]: unknown; - }; - // (undocumented) - sessionToken?: string; - // (undocumented) - sessionTokenMaxAge?: number; - // (undocumented) - status: string; - // (undocumented) - subFlowSettings?: { - cssLinks?: unknown[]; - cssUrl?: unknown; - jsLinks?: unknown[]; - loadingScreenSettings?: unknown; - reactSkUrl?: unknown; - }; - // (undocumented) - success: true; -} - -// @public (undocumented) -export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue<'DeviceAuthenticationCollector', DeviceValue>; - -// @public (undocumented) -export type DeviceAuthenticationField = { - type: 'DEVICE_AUTHENTICATION'; - key: string; - label: string; - options: { - type: string; - iconSrc: string; - title: string; - id: string; - default: boolean; - description: string; - }[]; - required: boolean; -}; - -// @public (undocumented) -export interface DeviceOptionNoDefault { - // (undocumented) - content: string; - // (undocumented) - key: string; - // (undocumented) - label: string; - // (undocumented) - type: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export interface DeviceOptionWithDefault { - // (undocumented) - content: string; - // (undocumented) - default: boolean; - // (undocumented) - key: string; - // (undocumented) - label: string; - // (undocumented) - type: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue<'DeviceRegistrationCollector', string>; - -// @public (undocumented) -export type DeviceRegistrationField = { - type: 'DEVICE_REGISTRATION'; - key: string; - label: string; - options: { - type: string; - iconSrc: string; - title: string; - description: string; - }[]; - required: boolean; -}; - -// @public (undocumented) -export interface DeviceValue { - // (undocumented) - id: string; - // (undocumented) - type: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export interface ErrorDetail { - // (undocumented) - message?: string; - // (undocumented) - rawResponse?: { - _embedded?: { - users?: Array; - }; - code?: string; - count?: number; - details?: NestedErrorDetails[]; - id?: string; - message?: string; - size?: number; - userFilter?: string; - [key: string]: unknown; - }; - // (undocumented) - statusCode?: number; -} - -// @public (undocumented) -export interface ErrorNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: 'error'; - }; - // (undocumented) - error: DaVinciError; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: 'error'; - } | null; - // (undocumented) - status: 'error'; -} - -// @public (undocumented) -export interface FailureNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - status: 'failure'; - }; - // (undocumented) - error: DaVinciError; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - eventName?: string; - href?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: 'failure'; - } | null; - // (undocumented) - status: 'failure'; -} - -// @public -export function fido(): FidoClient; - -// @public (undocumented) -export type FidoAuthenticationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoAuthenticationCollector', FidoAuthenticationInputValue | GenericError, FidoAuthenticationOutputValue>; - -// @public (undocumented) -export type FidoAuthenticationField = { - type: 'FIDO2'; - key: string; - label: string; - publicKeyCredentialRequestOptions: FidoAuthenticationOptions; - action: 'AUTHENTICATE'; - trigger: string; - required: boolean; -}; - -// @public (undocumented) -export interface FidoAuthenticationInputValue { - // (undocumented) - assertionValue?: AssertionValue; -} - -// @public (undocumented) -export interface FidoAuthenticationOptions extends Omit { - // (undocumented) - allowCredentials?: { - id: number[]; - transports?: AuthenticatorTransport[]; - type: PublicKeyCredentialType; - }[]; - // (undocumented) - challenge: number[]; -} - -// @public (undocumented) -export interface FidoAuthenticationOutputValue { - // (undocumented) - action: 'AUTHENTICATE'; - // (undocumented) - publicKeyCredentialRequestOptions: FidoAuthenticationOptions; - // (undocumented) - trigger: string; -} - -// @public (undocumented) -export interface FidoClient { - authenticate: (options: FidoAuthenticationOptions) => Promise; - register: (options: FidoRegistrationOptions) => Promise; -} - -// @public -export type FidoErrorCode = 'NotAllowedError' | 'AbortError' | 'InvalidStateError' | 'NotSupportedError' | 'SecurityError' | 'TimeoutError' | 'UnknownError'; - -// @public (undocumented) -export type FidoRegistrationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoRegistrationCollector', FidoRegistrationInputValue | GenericError, FidoRegistrationOutputValue>; - -// @public (undocumented) -export type FidoRegistrationField = { - type: 'FIDO2'; - key: string; - label: string; - publicKeyCredentialCreationOptions: FidoRegistrationOptions; - action: 'REGISTER'; - trigger: string; - required: boolean; -}; - -// @public (undocumented) -export interface FidoRegistrationInputValue { - // (undocumented) - attestationValue?: AttestationValue; -} - -// @public (undocumented) -export interface FidoRegistrationOptions extends Omit { - // (undocumented) - challenge: number[]; - // (undocumented) - excludeCredentials?: { - id: number[]; - transports?: AuthenticatorTransport[]; - type: PublicKeyCredentialType; - }[]; - // (undocumented) - pubKeyCredParams: { - alg: string | number; - type: PublicKeyCredentialType; - }[]; - // (undocumented) - user: { - id: number[]; - name: string; - displayName: string; - }; -} - -// @public (undocumented) -export interface FidoRegistrationOutputValue { - // (undocumented) - action: 'REGISTER'; - // (undocumented) - publicKeyCredentialCreationOptions: FidoRegistrationOptions; - // (undocumented) - trigger: string; -} - -// @public (undocumented) -export type FlowCollector = ActionCollectorNoUrl<'FlowCollector'>; - -// @public (undocumented) -export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; - -// @public -export type GetClient = StartNode['client'] | ContinueNode['client'] | ErrorNode['client'] | SuccessNode['client'] | FailureNode['client']; - -// @public (undocumented) -export type IdpCollector = ActionCollectorWithUrl<'IdpCollector'>; - -// @public -export interface ImageCollector extends NoValueCollectorBase<'ImageCollector'> { - // (undocumented) - output: NoValueCollectorBase<'ImageCollector'>['output'] & { - src: string; - alt: string; - href?: string; - }; -} - -// @public (undocumented) -export type ImageField = { - type: 'IMAGE'; - key: string; - description: string; - imageUrl: string; - hyperlinkUrl?: string; -}; - -// @public (undocumented) -export type InferActionCollectorType = T extends 'IdpCollector' ? IdpCollector : T extends 'SubmitCollector' ? SubmitCollector : T extends 'FlowCollector' ? FlowCollector : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>; - -// @public -export type InferAutoCollectorType = T extends 'ProtectCollector' ? ProtectCollector : T extends 'PollingCollector' ? PollingCollector : T extends 'FidoRegistrationCollector' ? FidoRegistrationCollector : T extends 'FidoAuthenticationCollector' ? FidoAuthenticationCollector : T extends 'MetadataCollector' ? MetadataCollector : T extends 'ObjectValueAutoCollector' ? ObjectValueAutoCollector : SingleValueAutoCollector; - -// @public -export type InferMultiValueCollectorType = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>; - -// @public -export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'ImageCollector' ? ImageCollector : NoValueCollectorBase<'NoValueCollector'>; - -// @public -export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'BooleanCollector' ? BooleanCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; - -// @public (undocumented) -export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; - -// @public (undocumented) -export type InitFlow = () => Promise; - -// @public (undocumented) -export interface InternalErrorResponse { - // (undocumented) - error: Omit & { - message: string; - }; - // (undocumented) - type: 'internal_error'; -} - -// @public (undocumented) -export interface Links { - // (undocumented) - [key: string]: { - href?: string; - }; -} - -export { LogLevel } - -// @public (undocumented) -export type MetadataCollector = AutoCollector<'ObjectValueAutoCollector', 'MetadataCollector', MetadataCollectorInputValue, Record>; - -// @public (undocumented) -export type MetadataCollectorInputValue = Record | MetadataError; - -// @public -export interface MetadataError { - // (undocumented) - code: string; - // (undocumented) - message: string; -} - -// @public (undocumented) -export type MetadataField = { - type: 'METADATA'; - key: string; - payload: Record; -}; - -// @public (undocumented) -export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectCollector'>; - -// @public (undocumented) -export type MultiSelectField = { - inputType: 'MULTI_SELECT'; - key: string; - label: string; - options: { - label: string; - value: string; - }[]; - required?: boolean; - type: 'CHECKBOX' | 'COMBOBOX'; -}; - -// @public (undocumented) -export type MultiValueCollector = MultiValueCollectorWithValue | MultiValueCollectorNoValue; - -// @public (undocumented) -export interface MultiValueCollectorNoValue { - // (undocumented) - category: 'MultiValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string[]; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type MultiValueCollectors = MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorWithValue<'MultiSelectCollector'>; - -// @public -export type MultiValueCollectorTypes = 'MultiSelectCollector' | 'MultiValueCollector'; - -// @public (undocumented) -export interface MultiValueCollectorWithValue { - // (undocumented) - category: 'MultiValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string[]; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: string[]; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type MultiValueFields = MultiSelectField; - -// @public -export interface NestedErrorDetails { - // (undocumented) - code?: string; - // (undocumented) - innerError?: { - history?: string; - unsatisfiedRequirements?: string[]; - failuresRemaining?: number; - }; - // (undocumented) - message?: string; - // (undocumented) - target?: string; -} - -// @public -export const nextCollectorValues: ActionCreatorWithPayload< { -fields: DaVinciField[]; -formData: { -value: Record; -}; -}, string>; - -// @public -export const nodeCollectorReducer: Reducer & { - getInitialState: () => Collectors[]; -}; - -// @public (undocumented) -export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode; - -// @public (undocumented) -export type NoValueCollector = InferNoValueCollectorType; - -// @public (undocumented) -export interface NoValueCollectorBase { - // (undocumented) - category: 'NoValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | ImageCollector; - -// @public -export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'ImageCollector'; - -// @public -export interface OAuthDetails { - // (undocumented) - [key: string]: unknown; - // (undocumented) - code?: string; - // (undocumented) - state?: string; -} - -// @public (undocumented) -export interface ObjectOptionsCollectorWithObjectValue, D = Record> { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: V; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: DeviceOptionWithDefault[]; - value?: D | null; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export interface ObjectOptionsCollectorWithStringValue { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: V; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: DeviceOptionNoDefault[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type ObjectValueAutoCollector = AutoCollector<'ObjectValueAutoCollector', 'ObjectValueAutoCollector', Record>; - -// @public (undocumented) -export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRegistrationCollector' | 'FidoAuthenticationCollector' | 'MetadataCollector'; - -// @public (undocumented) -export type ObjectValueCollector = ObjectOptionsCollectorWithObjectValue | ObjectOptionsCollectorWithStringValue | ObjectValueCollectorWithObjectValue; - -// @public (undocumented) -export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; - -// @public -export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; - -// @public (undocumented) -export interface ObjectValueCollectorWithObjectValue, OV = Record> { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: IV; - type: string; - validation: (ValidationRequired | ValidationPhoneNumber)[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value?: OV | null; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export interface OutgoingQueryParams { - // (undocumented) - [key: string]: string | string[]; -} - -// @public (undocumented) -export interface PasswordCollector { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - verify: boolean; - }; - // (undocumented) - type: 'PasswordCollector'; -} - -// @public -export type PasswordField = { - type: 'PASSWORD' | 'PASSWORD_VERIFY'; - key: string; - label: string; - required?: boolean; - verify?: boolean; - passwordPolicy?: PasswordPolicy; -}; - -// @public -export interface PasswordPolicy { - // (undocumented) - createdAt?: string; - // (undocumented) - default?: boolean; - // (undocumented) - description?: string; - // (undocumented) - excludesCommonlyUsed?: boolean; - // (undocumented) - excludesProfileData?: boolean; - // (undocumented) - history?: { - count?: number; - retentionDays?: number; - }; - // (undocumented) - id?: string; - // (undocumented) - length?: { - min?: number; - max?: number; - }; - // (undocumented) - lockout?: { - failureCount?: number; - durationSeconds?: number; - }; - // (undocumented) - maxAgeDays?: number; - // (undocumented) - maxRepeatedCharacters?: number; - // (undocumented) - minAgeDays?: number; - // (undocumented) - minCharacters?: Record; - // (undocumented) - minUniqueCharacters?: number; - // (undocumented) - name?: string; - // (undocumented) - notSimilarToCurrent?: boolean; - // (undocumented) - populationCount?: number; - // (undocumented) - updatedAt?: string; -} - -// @public (undocumented) -export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>; - -// @public (undocumented) -export interface PhoneNumberExtensionCollector { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: PhoneNumberExtensionInputValue; - type: string; - validation: (ValidationRequired | ValidationPhoneNumber)[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - extensionLabel: string; - value: PhoneNumberExtensionOutputValue; - }; - // (undocumented) - type: 'PhoneNumberExtensionCollector'; -} - -// @public (undocumented) -export type PhoneNumberExtensionField = PhoneNumberField & { - showExtension: boolean; - extensionLabel: string; -}; - -// @public (undocumented) -export interface PhoneNumberExtensionInputValue { - // (undocumented) - countryCode: string; - // (undocumented) - extension: string; - // (undocumented) - phoneNumber: string; -} - -// @public (undocumented) -export interface PhoneNumberExtensionOutputValue { - // (undocumented) - countryCode?: string; - // (undocumented) - extension?: string; - // (undocumented) - phoneNumber?: string; -} - -// @public (undocumented) -export type PhoneNumberField = { - type: 'PHONE_NUMBER'; - key: string; - label: string; - required: boolean; - defaultCountryCode: string | null; - validatePhoneNumber: boolean; -}; - -// @public (undocumented) -export interface PhoneNumberInputValue { - // (undocumented) - countryCode: string; - // (undocumented) - phoneNumber: string; -} - -// @public (undocumented) -export interface PhoneNumberOutputValue { - // (undocumented) - countryCode?: string; - // (undocumented) - phoneNumber?: string; -} - -// @public -export type Poller = () => Promise; - -// @public (undocumented) -export type PollingCollector = AutoCollector<'SingleValueAutoCollector', 'PollingCollector', string, PollingOutputValue>; - -// @public (undocumented) -export type PollingField = { - type: 'POLLING'; - key: string; - pollInterval: number; - pollRetries: number; - pollChallengeStatus?: boolean; - challenge?: string; -}; - -// @public (undocumented) -export interface PollingOutputValue { - // (undocumented) - challenge?: string; - // (undocumented) - pollChallengeStatus?: boolean; - // (undocumented) - pollInterval: number; - // (undocumented) - pollRetries: number; - // (undocumented) - retriesRemaining?: number; -} - -// @public (undocumented) -export type PollingStatus = PollingStatusContinue | PollingStatusChallenge; - -// @public (undocumented) -export type PollingStatusChallenge = PollingStatusChallengeComplete | 'expired' | 'timedOut' | 'error'; - -// @public (undocumented) -export type PollingStatusChallengeComplete = 'approved' | 'denied' | 'continue' | CustomPollingStatus; - -// @public (undocumented) -export type PollingStatusContinue = 'continue' | 'timedOut'; - -// @public (undocumented) -export type ProtectCollector = AutoCollector<'SingleValueAutoCollector', 'ProtectCollector', string, ProtectOutputValue>; - -// @public (undocumented) -export type ProtectField = { - type: 'PROTECT'; - key: string; - behavioralDataCollection: boolean; - universalDeviceIdentification: boolean; -}; - -// @public -export interface ProtectOutputValue { - // (undocumented) - behavioralDataCollection: boolean; - // (undocumented) - universalDeviceIdentification: boolean; -} - -// @public -export interface QrCodeCollector extends NoValueCollectorBase<'QrCodeCollector'> { - // (undocumented) - output: NoValueCollectorBase<'QrCodeCollector'>['output'] & { - src: string; - }; -} - -// @public (undocumented) -export type QrCodeField = { - type: 'QR_CODE'; - key: string; - content: string; - fallbackText?: string; -}; - -// @public -export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollector'> { - // (undocumented) - output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { - content: string; - title?: string; - }; -} - -// @public (undocumented) -export type ReadOnlyField = { - type: 'LABEL'; - content: string; - richContent?: RichContent; - key?: string; -}; - -// @public (undocumented) -export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField | ImageField; - -// @public (undocumented) -export type RedirectField = { - type: 'SOCIAL_LOGIN_BUTTON'; - key: string; - label: string; - links: Links; -}; - -// @public (undocumented) -export type RedirectFields = RedirectField; - -export { RequestMiddleware } - -// @public -export type RichContent = { - content: string; - replacements?: Record; -}; - -// @public -export interface RichContentLink { - // (undocumented) - href: string; - // (undocumented) - key: string; - // (undocumented) - target?: '_self' | '_blank'; - // (undocumented) - type: 'link'; - // (undocumented) - value: string; -} - -// @public -export type RichContentReplacement = { - type: 'link'; - value: string; - href: string; - target?: '_self' | '_blank'; -}; - -// @public -export interface RichTextCollector extends NoValueCollectorBase<'RichTextCollector'> { - // (undocumented) - output: NoValueCollectorBase<'RichTextCollector'>['output'] & { - content: string; - richContent: CollectorRichContent; - }; -} - -// @public (undocumented) -export interface SelectorOption { - // (undocumented) - label: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export type SingleCheckboxField = { - type: 'SINGLE_CHECKBOX'; - inputType: 'BOOLEAN'; - key: string; - label: string; - required?: boolean; - errorMessage?: string; - appearance: string; - richContent?: RichContent; -}; - -// @public (undocumented) -export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>; - -// @public (undocumented) -export interface SingleSelectCollectorNoValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export interface SingleSelectCollectorWithValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: string | number | boolean; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type SingleSelectField = { - inputType: 'SINGLE_SELECT'; - key: string; - label: string; - options: { - label: string; - value: string; - }[]; - required?: boolean; - type: 'RADIO' | 'DROPDOWN'; -}; - -// @public (undocumented) -export type SingleValueAutoCollector = AutoCollector<'SingleValueAutoCollector', 'SingleValueAutoCollector', string>; - -// @public (undocumented) -export type SingleValueAutoCollectorTypes = 'SingleValueAutoCollector' | 'ProtectCollector' | 'PollingCollector'; - -// @public -export type SingleValueCollector = SingleValueCollectorWithValue | SingleValueCollectorNoValue; - -// @public (undocumented) -export interface SingleValueCollectorNoValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type SingleValueCollectors = PasswordCollector | ValidatedPasswordCollector | SingleSelectCollector | TextCollector | ValidatedTextCollector | BooleanCollector | ValidatedBooleanCollector | SingleValueCollectorWithValue<'SingleValueCollector'>; - -// @public -export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'BooleanCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; - -// @public (undocumented) -export interface SingleValueCollectorWithValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: V; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: V; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type SingleValueFields = StandardField | PasswordField | ValidatedField | SingleCheckboxField | SingleSelectField | ProtectField; - -// @public (undocumented) -export type StandardField = { - type: 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON'; - key: string; - label: string; - required?: boolean; -}; - -// @public (undocumented) -export interface StartNode { - // (undocumented) - cache: null; - // (undocumented) - client: { - status: 'start'; - }; - // (undocumented) - error: DaVinciError | null; - // (undocumented) - server: { - status: 'start'; - }; - // (undocumented) - status: 'start'; -} - -// @public (undocumented) -export interface StartOptions { - // (undocumented) - query: Query; -} - -// @public (undocumented) -export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>; - -// @public (undocumented) -export interface SuccessNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - authorization?: { - code?: string; - state?: string; - }; - status: 'success'; - } | null; - // (undocumented) - error: null; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - session?: string; - status: 'success'; - }; - // (undocumented) - status: 'success'; -} - -// @public (undocumented) -export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>; - -// @public (undocumented) -export interface ThrownQueryError { - // (undocumented) - error: FetchBaseQueryError; - // (undocumented) - isHandledError: boolean; - // (undocumented) - meta: FetchBaseQueryMeta; -} - -// @public -export type UnknownCollector = { - category: 'UnknownCollector'; - error: string | null; - type: 'UnknownCollector'; - id: string; - name: string; - output: { - key: string; - label: string; - type: string; - }; -}; - -// @public (undocumented) -export type UnknownField = Record; - -// @public (undocumented) -export const updateCollectorValues: ActionCreatorWithPayload< { -id: string; -value: CollectorValueTypes; -index?: number; -}, string>; - -// @public -export type Updater = (value: CollectorValueType, index?: number) => InternalErrorResponse | null; - -// @public (undocumented) -export interface ValidatedBooleanCollector extends ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean> { - // (undocumented) - output: ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>['output'] & { - appearance: string; - richContent?: CollectorRichContent; - }; -} - -// @public -export type ValidatedCollectors = ValidatedTextCollector | ValidatedBooleanCollector | ValidatedPasswordCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors; - -// @public (undocumented) -export type ValidatedField = { - type: 'TEXT'; - key: string; - label: string; - required: boolean; - validation: { - regex: string; - errorMessage: string; - }; -}; - -// @public (undocumented) -export interface ValidatedPasswordCollector { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - validation: PasswordPolicy; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - verify: boolean; - }; - // (undocumented) - type: 'ValidatedPasswordCollector'; -} - -// @public (undocumented) -export interface ValidatedSingleValueCollectorWithValue { - // (undocumented) - category: 'ValidatedSingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - type: string; - validation: (ValidationRequired | ValidationRegex)[]; - value: V; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: V; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>; - -// @public (undocumented) -export interface ValidationPhoneNumber { - // (undocumented) - message: string; - // (undocumented) - rule: boolean; - // (undocumented) - type: 'validatePhoneNumber'; -} - -// @public (undocumented) -export interface ValidationRegex { - // (undocumented) - message: string; - // (undocumented) - rule: string; - // (undocumented) - type: 'regex'; -} - -// @public (undocumented) -export interface ValidationRequired { - // (undocumented) - message: string; - // (undocumented) - rule: boolean; - // (undocumented) - type: 'required'; -} - -// @public -export type Validator = (value: CollectorValueType) => string[] | { - error: { - message: string; - type: string; - }; - type: string; -}; - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@forgerock/davinci-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; +import { ActionTypes } from '@forgerock/sdk-request-middleware'; +import { CustomLogger } from '@forgerock/sdk-logger'; +import type { DaVinciConfig } from '@forgerock/sdk-types'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; +import { GenericError } from '@forgerock/sdk-types'; +import { LogLevel } from '@forgerock/sdk-logger'; +import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; +import { QueryStatus } from '@reduxjs/toolkit/query'; +import { Reducer } from '@reduxjs/toolkit'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; +import { SerializedError } from '@reduxjs/toolkit'; +import { Unsubscribe } from '@reduxjs/toolkit'; + +// @public (undocumented) +export type ActionCollector = + | ActionCollectorNoUrl + | ActionCollectorWithUrl; + +// @public (undocumented) +export interface ActionCollectorNoUrl { + // (undocumented) + category: 'ActionCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type ActionCollectors = + | ActionCollectorWithUrl<'IdpCollector'> + | ActionCollectorNoUrl<'ActionCollector'> + | ActionCollectorNoUrl<'FlowCollector'> + | ActionCollectorNoUrl<'SubmitCollector'>; + +// @public +export type ActionCollectorTypes = + | 'FlowCollector' + | 'SubmitCollector' + | 'IdpCollector' + | 'ActionCollector'; + +// @public (undocumented) +export interface ActionCollectorWithUrl { + // (undocumented) + category: 'ActionCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + url?: string | null; + }; + // (undocumented) + type: T; +} + +export { ActionTypes }; + +// @public (undocumented) +export type AgreementField = { + type: 'AGREEMENT'; + key: string; + content: string; + titleEnabled: boolean; + title?: string; + agreement: { + id: string; + useDynamicAgreement: boolean; + }; + enabled: boolean; +}; + +// @public (undocumented) +export interface AssertionValue extends Omit< + PublicKeyCredential, + 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON' +> { + // (undocumented) + rawId: string; + // (undocumented) + response: { + clientDataJSON: string; + authenticatorData: string; + signature: string; + userHandle: string | null; + }; +} + +// @public (undocumented) +export interface AttestationValue extends Omit< + PublicKeyCredential, + 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON' +> { + // (undocumented) + rawId: string; + // (undocumented) + response: { + clientDataJSON: string; + attestationObject: string; + }; +} + +// @public (undocumented) +export interface AutoCollector< + C extends AutoCollectorCategories, + T extends AutoCollectorTypes, + IV = string, + OV = Record, +> { + // (undocumented) + category: C; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: IV; + type: string; + validation?: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + type: string; + config: OV; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector'; + +// @public (undocumented) +export type AutoCollectors = + | ProtectCollector + | FidoRegistrationCollector + | FidoAuthenticationCollector + | MetadataCollector + | PollingCollector + | SingleValueAutoCollector + | ObjectValueAutoCollector; + +// @public (undocumented) +export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes; + +// @public (undocumented) +export interface BooleanCollector extends SingleValueCollectorWithValue< + 'BooleanCollector', + boolean +> { + // (undocumented) + output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + +// @public (undocumented) +export interface CollectorErrors { + // (undocumented) + code: string; + // (undocumented) + message: string; + // (undocumented) + target: string; +} + +// @public +export interface CollectorRichContent { + // (undocumented) + content: string; + // (undocumented) + replacements: RichContentLink[]; +} + +// @public (undocumented) +export type Collectors = + | FlowCollector + | MetadataCollector + | PasswordCollector + | ValidatedPasswordCollector + | TextCollector + | BooleanCollector + | ValidatedBooleanCollector + | SingleSelectCollector + | IdpCollector + | SubmitCollector + | ActionCollector<'ActionCollector'> + | SingleValueCollector<'SingleValueCollector'> + | MultiSelectCollector + | DeviceAuthenticationCollector + | DeviceRegistrationCollector + | PhoneNumberCollector + | PhoneNumberExtensionCollector + | ReadOnlyCollector + | RichTextCollector + | ValidatedTextCollector + | ProtectCollector + | PollingCollector + | FidoRegistrationCollector + | FidoAuthenticationCollector + | QrCodeCollector + | ImageCollector + | UnknownCollector; + +// @public +export type CollectorValueType = T extends + | { + type: 'PasswordCollector'; + } + | { + type: 'ValidatedPasswordCollector'; + } + | { + type: 'SingleSelectCollector'; + } + | { + type: 'DeviceRegistrationCollector'; + } + | { + type: 'DeviceAuthenticationCollector'; + } + | { + type: 'ProtectCollector'; + } + | { + type: 'PollingCollector'; + } + ? string + : T extends { + type: 'TextCollector'; + category: 'SingleValueCollector'; + } + ? string + : T extends { + type: 'TextCollector'; + category: 'ValidatedSingleValueCollector'; + } + ? string + : T extends + | { + type: 'BooleanCollector'; + } + | { + type: 'ValidatedBooleanCollector'; + } + ? boolean + : T extends { + type: 'MultiSelectCollector'; + } + ? string[] + : T extends { + type: 'PhoneNumberCollector'; + } + ? PhoneNumberInputValue + : T extends { + type: 'PhoneNumberExtensionCollector'; + } + ? PhoneNumberExtensionInputValue + : T extends { + type: 'FidoRegistrationCollector'; + } + ? FidoRegistrationInputValue | GenericError + : T extends { + type: 'FidoAuthenticationCollector'; + } + ? FidoAuthenticationInputValue | GenericError + : T extends { + type: 'MetadataCollector'; + } + ? Record | MetadataError + : T extends { + category: 'SingleValueCollector'; + } + ? string + : T extends { + category: 'ValidatedSingleValueCollector'; + } + ? string + : T extends { + category: 'SingleValueAutoCollector'; + } + ? string + : T extends { + category: 'MultiValueCollector'; + } + ? string[] + : T extends { + category: 'ActionCollector'; + } + ? never + : T extends { + category: 'NoValueCollector'; + } + ? never + : CollectorValueTypes; + +// @public +export type CollectorValueTypes = + | string + | string[] + | boolean + | PhoneNumberInputValue + | PhoneNumberExtensionInputValue + | FidoRegistrationInputValue + | FidoAuthenticationInputValue + | MetadataError + | GenericError; + +// @public (undocumented) +export type ComplexValueFields = + | DeviceAuthenticationField + | DeviceRegistrationField + | PhoneNumberField + | PhoneNumberExtensionField + | FidoRegistrationField + | FidoAuthenticationField + | PollingField + | MetadataField; + +// @public (undocumented) +export interface ContinueNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'continue'; + }; + // (undocumented) + error: null; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + id?: string; + interactionId?: string; + interactionToken?: string; + href?: string; + eventName?: string; + status: 'continue'; + }; + // (undocumented) + status: 'continue'; +} + +export { CustomLogger }; + +// @public +export type CustomPollingStatus = string & {}; + +// @public +export function davinci(input: { + config: DaVinciConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + store?: SdkStore; +}): Promise<{ + store: SdkStore; + subscribe: (listener: () => void) => Unsubscribe; + externalIdp: () => () => Promise; + flow: (action: DaVinciAction) => InitFlow; + next: (args?: DaVinciRequest) => Promise; + resume: (input: { continueToken: string }) => Promise; + start: ( + options?: StartOptions | undefined, + ) => Promise; + update: < + T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors, + >( + collector: T, + ) => Updater; + validate: ( + collector: + | SingleValueCollectors + | ObjectValueCollectors + | MultiValueCollectors + | AutoCollectors, + ) => Validator; + pollStatus: (collector: PollingCollector) => Poller; + getClient: () => + | { + status: 'start'; + } + | { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'continue'; + } + | { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'error'; + } + | { + authorization?: { + code?: string; + state?: string; + }; + status: 'success'; + } + | { + status: 'failure'; + } + | null; + getCollectors: () => Collectors[]; + getError: () => DaVinciError | null; + getErrorCollectors: () => CollectorErrors[]; + getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; + getServer: () => + | { + status: 'start'; + } + | { + status: 'start'; + } + | { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'error'; + } + | { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + href?: string; + session?: string; + status: 'success'; + } + | { + _links?: Links; + eventName?: string; + href?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'failure'; + } + | null; + cache: { + getLatestResponse: () => + | ({ + status: QueryStatus.fulfilled; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > + > & { + error: undefined; + } & { + status: QueryStatus.fulfilled; + isUninitialized: false; + isLoading: false; + isSuccess: true; + isError: false; + }) + | ({ + status: QueryStatus.rejected; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > + > & { + status: QueryStatus.rejected; + isUninitialized: false; + isLoading: false; + isSuccess: false; + isError: true; + }) + | { + error: { + message: string; + type: string; + }; + }; + getResponseWithId: (requestId: string) => + | ({ + status: QueryStatus.fulfilled; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > + > & { + error: undefined; + } & { + status: QueryStatus.fulfilled; + isUninitialized: false; + isLoading: false; + isSuccess: true; + isError: false; + }) + | ({ + status: QueryStatus.rejected; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > + > & { + status: QueryStatus.rejected; + isUninitialized: false; + isLoading: false; + isSuccess: false; + isError: true; + }) + | { + error: { + message: string; + type: string; + }; + }; + }; +}>; + +// @public +export interface DaVinciAction { + // (undocumented) + action: string; +} + +// @public +export interface DaVinciBaseResponse { + // (undocumented) + capabilityName?: string; + // (undocumented) + companyId?: string; + // (undocumented) + connectionId?: string; + // (undocumented) + connectorId?: string; + // (undocumented) + id?: string; + // (undocumented) + interactionId?: string; + // (undocumented) + interactionToken?: string; + // (undocumented) + isResponseCompatibleWithMobileAndWebSdks?: boolean; + // (undocumented) + status?: string; +} + +// @public +export type DaVinciCacheEntry = { + data?: DaVinciBaseResponse; + error?: { + data: DaVinciBaseResponse; + status: number; + }; +} & { + data?: any; + error?: any; +} & MutationResultSelectorResult; + +// @public (undocumented) +export type DavinciClient = Awaited>; + +export { DaVinciConfig }; + +// @public (undocumented) +export interface DaVinciError extends Omit { + // (undocumented) + collectors?: CollectorErrors[]; + // (undocumented) + internalHttpStatus?: number; + // (undocumented) + message: string; + // (undocumented) + status: 'error' | 'failure' | 'unknown'; +} + +// @public (undocumented) +export interface DaVinciErrorCacheEntry { + // (undocumented) + endpointName: 'next' | 'flow' | 'start'; + // (undocumented) + error: { + data: T; + }; + // (undocumented) + fulfilledTimeStamp: number; + // (undocumented) + isError: boolean; + // (undocumented) + isLoading: boolean; + // (undocumented) + isSuccess: boolean; + // (undocumented) + isUninitialized: boolean; + // (undocumented) + requestId: string; + // (undocumented) + startedTimeStamp: number; + // (undocumented) + status: 'fulfilled' | 'pending' | 'rejected'; +} + +// @public (undocumented) +export interface DavinciErrorResponse extends DaVinciBaseResponse { + // (undocumented) + cause?: string | null; + // (undocumented) + code: string | number; + // (undocumented) + details?: ErrorDetail[]; + // (undocumented) + doNotSendToOE?: boolean; + // (undocumented) + error?: { + code?: string; + message?: string; + }; + // (undocumented) + errorCategory?: string; + // (undocumented) + errorMessage?: string; + // (undocumented) + expected?: boolean; + // (undocumented) + httpResponseCode: number; + // (undocumented) + isErrorCustomized?: boolean; + // (undocumented) + message: string; + // (undocumented) + metricAttributes?: { + [key: string]: unknown; + }; +} + +// @public (undocumented) +export interface DaVinciFailureResponse extends DaVinciBaseResponse { + // (undocumented) + error?: { + code?: string; + message?: string; + [key: string]: unknown; + }; +} + +// @public (undocumented) +export type DaVinciField = + | ComplexValueFields + | MultiValueFields + | ReadOnlyFields + | RedirectFields + | SingleValueFields; + +// @public +export interface DaVinciNextResponse extends DaVinciBaseResponse { + // (undocumented) + eventName?: string; + // (undocumented) + form?: { + name?: string; + description?: string; + components?: { + fields?: DaVinciField[]; + }; + }; + // (undocumented) + formData?: { + value?: { + [key: string]: string; + }; + }; + // (undocumented) + _links?: Links; +} + +// @public +export interface DaVinciPollResponse extends DaVinciBaseResponse { + // (undocumented) + eventName?: string; + // (undocumented) + _links?: Links; + // (undocumented) + success?: boolean; +} + +// @public (undocumented) +export interface DaVinciRequest { + // (undocumented) + eventName: string; + // (undocumented) + id: string; + // (undocumented) + interactionId: string; + // (undocumented) + parameters: { + eventType: 'submit' | 'action' | 'polling'; + data: { + actionKey: string; + formData?: Record; + }; + }; +} + +// @public +export type DaVinciRequestValueTypes = + | string + | number + | boolean + | (string | number | boolean)[] + | DeviceValue + | PhoneNumberInputValue + | FidoRegistrationInputValue + | FidoAuthenticationInputValue + | MetadataError + | GenericError; + +// @public (undocumented) +export interface DaVinciSuccessResponse extends DaVinciBaseResponse { + // (undocumented) + authorizeResponse?: OAuthDetails; + // (undocumented) + environment: { + id: string; + [key: string]: unknown; + }; + // (undocumented) + _links?: Links; + // (undocumented) + resetCookie?: boolean; + // (undocumented) + session?: { + id?: string; + [key: string]: unknown; + }; + // (undocumented) + sessionToken?: string; + // (undocumented) + sessionTokenMaxAge?: number; + // (undocumented) + status: string; + // (undocumented) + subFlowSettings?: { + cssLinks?: unknown[]; + cssUrl?: unknown; + jsLinks?: unknown[]; + loadingScreenSettings?: unknown; + reactSkUrl?: unknown; + }; + // (undocumented) + success: true; +} + +// @public (undocumented) +export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue< + 'DeviceAuthenticationCollector', + DeviceValue +>; + +// @public (undocumented) +export type DeviceAuthenticationField = { + type: 'DEVICE_AUTHENTICATION'; + key: string; + label: string; + options: { + type: string; + iconSrc: string; + title: string; + id: string; + default: boolean; + description: string; + }[]; + required: boolean; +}; + +// @public (undocumented) +export interface DeviceOptionNoDefault { + // (undocumented) + content: string; + // (undocumented) + key: string; + // (undocumented) + label: string; + // (undocumented) + type: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface DeviceOptionWithDefault { + // (undocumented) + content: string; + // (undocumented) + default: boolean; + // (undocumented) + key: string; + // (undocumented) + label: string; + // (undocumented) + type: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue< + 'DeviceRegistrationCollector', + string +>; + +// @public (undocumented) +export type DeviceRegistrationField = { + type: 'DEVICE_REGISTRATION'; + key: string; + label: string; + options: { + type: string; + iconSrc: string; + title: string; + description: string; + }[]; + required: boolean; +}; + +// @public (undocumented) +export interface DeviceValue { + // (undocumented) + id: string; + // (undocumented) + type: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface ErrorDetail { + // (undocumented) + message?: string; + // (undocumented) + rawResponse?: { + _embedded?: { + users?: Array; + }; + code?: string; + count?: number; + details?: NestedErrorDetails[]; + id?: string; + message?: string; + size?: number; + userFilter?: string; + [key: string]: unknown; + }; + // (undocumented) + statusCode?: number; +} + +// @public (undocumented) +export interface ErrorNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'error'; + }; + // (undocumented) + error: DaVinciError; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'error'; + } | null; + // (undocumented) + status: 'error'; +} + +// @public (undocumented) +export interface FailureNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + status: 'failure'; + }; + // (undocumented) + error: DaVinciError; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + eventName?: string; + href?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'failure'; + } | null; + // (undocumented) + status: 'failure'; +} + +// @public +export function fido(): FidoClient; + +// @public (undocumented) +export type FidoAuthenticationCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'FidoAuthenticationCollector', + FidoAuthenticationInputValue | GenericError, + FidoAuthenticationOutputValue +>; + +// @public (undocumented) +export type FidoAuthenticationField = { + type: 'FIDO2'; + key: string; + label: string; + publicKeyCredentialRequestOptions: FidoAuthenticationOptions; + action: 'AUTHENTICATE'; + trigger: string; + required: boolean; +}; + +// @public (undocumented) +export interface FidoAuthenticationInputValue { + // (undocumented) + assertionValue?: AssertionValue; +} + +// @public (undocumented) +export interface FidoAuthenticationOptions extends Omit< + PublicKeyCredentialRequestOptions, + 'challenge' | 'allowCredentials' +> { + // (undocumented) + allowCredentials?: { + id: number[]; + transports?: AuthenticatorTransport[]; + type: PublicKeyCredentialType; + }[]; + // (undocumented) + challenge: number[]; +} + +// @public (undocumented) +export interface FidoAuthenticationOutputValue { + // (undocumented) + action: 'AUTHENTICATE'; + // (undocumented) + publicKeyCredentialRequestOptions: FidoAuthenticationOptions; + // (undocumented) + trigger: string; +} + +// @public (undocumented) +export interface FidoClient { + authenticate: ( + options: FidoAuthenticationOptions, + ) => Promise; + register: ( + options: FidoRegistrationOptions, + ) => Promise; +} + +// @public +export type FidoErrorCode = + | 'NotAllowedError' + | 'AbortError' + | 'InvalidStateError' + | 'NotSupportedError' + | 'SecurityError' + | 'TimeoutError' + | 'UnknownError'; + +// @public (undocumented) +export type FidoRegistrationCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'FidoRegistrationCollector', + FidoRegistrationInputValue | GenericError, + FidoRegistrationOutputValue +>; + +// @public (undocumented) +export type FidoRegistrationField = { + type: 'FIDO2'; + key: string; + label: string; + publicKeyCredentialCreationOptions: FidoRegistrationOptions; + action: 'REGISTER'; + trigger: string; + required: boolean; +}; + +// @public (undocumented) +export interface FidoRegistrationInputValue { + // (undocumented) + attestationValue?: AttestationValue; +} + +// @public (undocumented) +export interface FidoRegistrationOptions extends Omit< + PublicKeyCredentialCreationOptions, + 'challenge' | 'user' | 'pubKeyCredParams' | 'excludeCredentials' +> { + // (undocumented) + challenge: number[]; + // (undocumented) + excludeCredentials?: { + id: number[]; + transports?: AuthenticatorTransport[]; + type: PublicKeyCredentialType; + }[]; + // (undocumented) + pubKeyCredParams: { + alg: string | number; + type: PublicKeyCredentialType; + }[]; + // (undocumented) + user: { + id: number[]; + name: string; + displayName: string; + }; +} + +// @public (undocumented) +export interface FidoRegistrationOutputValue { + // (undocumented) + action: 'REGISTER'; + // (undocumented) + publicKeyCredentialCreationOptions: FidoRegistrationOptions; + // (undocumented) + trigger: string; +} + +// @public (undocumented) +export type FlowCollector = ActionCollectorNoUrl<'FlowCollector'>; + +// @public (undocumented) +export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; + +// @public +export type GetClient = + | StartNode['client'] + | ContinueNode['client'] + | ErrorNode['client'] + | SuccessNode['client'] + | FailureNode['client']; + +// @public (undocumented) +export type IdpCollector = ActionCollectorWithUrl<'IdpCollector'>; + +// @public +export interface ImageCollector extends NoValueCollectorBase<'ImageCollector'> { + // (undocumented) + output: NoValueCollectorBase<'ImageCollector'>['output'] & { + src: string; + alt: string; + href?: string; + }; +} + +// @public (undocumented) +export type ImageField = { + type: 'IMAGE'; + key: string; + description: string; + imageUrl: string; + hyperlinkUrl?: string; +}; + +// @public (undocumented) +export type InferActionCollectorType = T extends 'IdpCollector' + ? IdpCollector + : T extends 'SubmitCollector' + ? SubmitCollector + : T extends 'FlowCollector' + ? FlowCollector + : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>; + +// @public +export type InferAutoCollectorType = T extends 'ProtectCollector' + ? ProtectCollector + : T extends 'PollingCollector' + ? PollingCollector + : T extends 'FidoRegistrationCollector' + ? FidoRegistrationCollector + : T extends 'FidoAuthenticationCollector' + ? FidoAuthenticationCollector + : T extends 'MetadataCollector' + ? MetadataCollector + : T extends 'ObjectValueAutoCollector' + ? ObjectValueAutoCollector + : SingleValueAutoCollector; + +// @public +export type InferMultiValueCollectorType = + T extends 'MultiSelectCollector' + ? MultiValueCollectorWithValue<'MultiSelectCollector'> + : + | MultiValueCollectorWithValue<'MultiValueCollector'> + | MultiValueCollectorNoValue<'MultiValueCollector'>; + +// @public +export type InferNoValueCollectorType = + T extends 'ReadOnlyCollector' + ? ReadOnlyCollector + : T extends 'RichTextCollector' + ? RichTextCollector + : T extends 'QrCodeCollector' + ? QrCodeCollector + : T extends 'ImageCollector' + ? ImageCollector + : NoValueCollectorBase<'NoValueCollector'>; + +// @public +export type InferSingleValueCollectorType = + T extends 'TextCollector' + ? TextCollector + : T extends 'SingleSelectCollector' + ? SingleSelectCollector + : T extends 'ValidatedTextCollector' + ? ValidatedTextCollector + : T extends 'PasswordCollector' + ? PasswordCollector + : T extends 'ValidatedPasswordCollector' + ? ValidatedPasswordCollector + : T extends 'BooleanCollector' + ? BooleanCollector + : T extends 'ValidatedBooleanCollector' + ? ValidatedBooleanCollector + : + | SingleValueCollectorWithValue<'SingleValueCollector'> + | SingleValueCollectorNoValue<'SingleValueCollector'>; + +// @public (undocumented) +export type InferValueObjectCollectorType = + T extends 'DeviceAuthenticationCollector' + ? DeviceAuthenticationCollector + : T extends 'DeviceRegistrationCollector' + ? DeviceRegistrationCollector + : T extends 'PhoneNumberCollector' + ? PhoneNumberCollector + : T extends 'PhoneNumberExtensionCollector' + ? PhoneNumberExtensionCollector + : + | ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> + | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; + +// @public (undocumented) +export type InitFlow = () => Promise; + +// @public (undocumented) +export interface InternalErrorResponse { + // (undocumented) + error: Omit & { + message: string; + }; + // (undocumented) + type: 'internal_error'; +} + +// @public (undocumented) +export interface Links { + // (undocumented) + [key: string]: { + href?: string; + }; +} + +export { LogLevel }; + +// @public (undocumented) +export type MetadataCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'MetadataCollector', + MetadataCollectorInputValue, + Record +>; + +// @public (undocumented) +export type MetadataCollectorInputValue = Record | MetadataError; + +// @public +export interface MetadataError { + // (undocumented) + code: string; + // (undocumented) + message: string; +} + +// @public (undocumented) +export type MetadataField = { + type: 'METADATA'; + key: string; + payload: Record; +}; + +// @public (undocumented) +export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectCollector'>; + +// @public (undocumented) +export type MultiSelectField = { + inputType: 'MULTI_SELECT'; + key: string; + label: string; + options: { + label: string; + value: string; + }[]; + required?: boolean; + type: 'CHECKBOX' | 'COMBOBOX'; +}; + +// @public (undocumented) +export type MultiValueCollector = + | MultiValueCollectorWithValue + | MultiValueCollectorNoValue; + +// @public (undocumented) +export interface MultiValueCollectorNoValue { + // (undocumented) + category: 'MultiValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string[]; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type MultiValueCollectors = + | MultiValueCollectorWithValue<'MultiValueCollector'> + | MultiValueCollectorWithValue<'MultiSelectCollector'>; + +// @public +export type MultiValueCollectorTypes = 'MultiSelectCollector' | 'MultiValueCollector'; + +// @public (undocumented) +export interface MultiValueCollectorWithValue { + // (undocumented) + category: 'MultiValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string[]; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: string[]; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type MultiValueFields = MultiSelectField; + +// @public +export interface NestedErrorDetails { + // (undocumented) + code?: string; + // (undocumented) + innerError?: { + history?: string; + unsatisfiedRequirements?: string[]; + failuresRemaining?: number; + }; + // (undocumented) + message?: string; + // (undocumented) + target?: string; +} + +// @public +export const nextCollectorValues: ActionCreatorWithPayload< + { + fields: DaVinciField[]; + formData: { + value: Record; + }; + }, + string +>; + +// @public +export const nodeCollectorReducer: Reducer & { + getInitialState: () => Collectors[]; +}; + +// @public (undocumented) +export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode; + +// @public (undocumented) +export type NoValueCollector = InferNoValueCollectorType; + +// @public (undocumented) +export interface NoValueCollectorBase { + // (undocumented) + category: 'NoValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type NoValueCollectors = + | NoValueCollectorBase<'NoValueCollector'> + | ReadOnlyCollector + | RichTextCollector + | QrCodeCollector + | ImageCollector; + +// @public +export type NoValueCollectorTypes = + | 'ReadOnlyCollector' + | 'RichTextCollector' + | 'NoValueCollector' + | 'QrCodeCollector' + | 'ImageCollector'; + +// @public +export interface OAuthDetails { + // (undocumented) + [key: string]: unknown; + // (undocumented) + code?: string; + // (undocumented) + state?: string; +} + +// @public (undocumented) +export interface ObjectOptionsCollectorWithObjectValue< + T extends ObjectValueCollectorTypes, + V = Record, + D = Record, +> { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: V; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: DeviceOptionWithDefault[]; + value?: D | null; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export interface ObjectOptionsCollectorWithStringValue< + T extends ObjectValueCollectorTypes, + V = string, +> { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: V; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: DeviceOptionNoDefault[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type ObjectValueAutoCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'ObjectValueAutoCollector', + Record +>; + +// @public (undocumented) +export type ObjectValueAutoCollectorTypes = + | 'ObjectValueAutoCollector' + | 'FidoRegistrationCollector' + | 'FidoAuthenticationCollector' + | 'MetadataCollector'; + +// @public (undocumented) +export type ObjectValueCollector = + | ObjectOptionsCollectorWithObjectValue + | ObjectOptionsCollectorWithStringValue + | ObjectValueCollectorWithObjectValue; + +// @public (undocumented) +export type ObjectValueCollectors = + | DeviceAuthenticationCollector + | DeviceRegistrationCollector + | PhoneNumberCollector + | PhoneNumberExtensionCollector + | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> + | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; + +// @public +export type ObjectValueCollectorTypes = + | 'DeviceAuthenticationCollector' + | 'DeviceRegistrationCollector' + | 'PhoneNumberCollector' + | 'PhoneNumberExtensionCollector' + | 'ObjectOptionsCollector' + | 'ObjectValueCollector' + | 'ObjectSelectCollector'; + +// @public (undocumented) +export interface ObjectValueCollectorWithObjectValue< + T extends ObjectValueCollectorTypes, + IV = Record, + OV = Record, +> { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: IV; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value?: OV | null; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export interface OutgoingQueryParams { + // (undocumented) + [key: string]: string | string[]; +} + +// @public (undocumented) +export interface PasswordCollector { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + verify: boolean; + }; + // (undocumented) + type: 'PasswordCollector'; +} + +// @public +export type PasswordField = { + type: 'PASSWORD' | 'PASSWORD_VERIFY'; + key: string; + label: string; + required?: boolean; + verify?: boolean; + passwordPolicy?: PasswordPolicy; +}; + +// @public +export interface PasswordPolicy { + // (undocumented) + createdAt?: string; + // (undocumented) + default?: boolean; + // (undocumented) + description?: string; + // (undocumented) + excludesCommonlyUsed?: boolean; + // (undocumented) + excludesProfileData?: boolean; + // (undocumented) + history?: { + count?: number; + retentionDays?: number; + }; + // (undocumented) + id?: string; + // (undocumented) + length?: { + min?: number; + max?: number; + }; + // (undocumented) + lockout?: { + failureCount?: number; + durationSeconds?: number; + }; + // (undocumented) + maxAgeDays?: number; + // (undocumented) + maxRepeatedCharacters?: number; + // (undocumented) + minAgeDays?: number; + // (undocumented) + minCharacters?: Record; + // (undocumented) + minUniqueCharacters?: number; + // (undocumented) + name?: string; + // (undocumented) + notSimilarToCurrent?: boolean; + // (undocumented) + populationCount?: number; + // (undocumented) + updatedAt?: string; +} + +// @public (undocumented) +export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue< + 'PhoneNumberCollector', + PhoneNumberInputValue, + PhoneNumberOutputValue +>; + +// @public (undocumented) +export interface PhoneNumberExtensionCollector { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: PhoneNumberExtensionInputValue; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + extensionLabel: string; + value: PhoneNumberExtensionOutputValue; + }; + // (undocumented) + type: 'PhoneNumberExtensionCollector'; +} + +// @public (undocumented) +export type PhoneNumberExtensionField = PhoneNumberField & { + showExtension: boolean; + extensionLabel: string; +}; + +// @public (undocumented) +export interface PhoneNumberExtensionInputValue { + // (undocumented) + countryCode: string; + // (undocumented) + extension: string; + // (undocumented) + phoneNumber: string; +} + +// @public (undocumented) +export interface PhoneNumberExtensionOutputValue { + // (undocumented) + countryCode?: string; + // (undocumented) + extension?: string; + // (undocumented) + phoneNumber?: string; +} + +// @public (undocumented) +export type PhoneNumberField = { + type: 'PHONE_NUMBER'; + key: string; + label: string; + required: boolean; + defaultCountryCode: string | null; + validatePhoneNumber: boolean; +}; + +// @public (undocumented) +export interface PhoneNumberInputValue { + // (undocumented) + countryCode: string; + // (undocumented) + phoneNumber: string; +} + +// @public (undocumented) +export interface PhoneNumberOutputValue { + // (undocumented) + countryCode?: string; + // (undocumented) + phoneNumber?: string; +} + +// @public +export type Poller = () => Promise; + +// @public (undocumented) +export type PollingCollector = AutoCollector< + 'SingleValueAutoCollector', + 'PollingCollector', + string, + PollingOutputValue +>; + +// @public (undocumented) +export type PollingField = { + type: 'POLLING'; + key: string; + pollInterval: number; + pollRetries: number; + pollChallengeStatus?: boolean; + challenge?: string; +}; + +// @public (undocumented) +export interface PollingOutputValue { + // (undocumented) + challenge?: string; + // (undocumented) + pollChallengeStatus?: boolean; + // (undocumented) + pollInterval: number; + // (undocumented) + pollRetries: number; + // (undocumented) + retriesRemaining?: number; +} + +// @public (undocumented) +export type PollingStatus = PollingStatusContinue | PollingStatusChallenge; + +// @public (undocumented) +export type PollingStatusChallenge = + | PollingStatusChallengeComplete + | 'expired' + | 'timedOut' + | 'error'; + +// @public (undocumented) +export type PollingStatusChallengeComplete = + | 'approved' + | 'denied' + | 'continue' + | CustomPollingStatus; + +// @public (undocumented) +export type PollingStatusContinue = 'continue' | 'timedOut'; + +// @public (undocumented) +export type ProtectCollector = AutoCollector< + 'SingleValueAutoCollector', + 'ProtectCollector', + string, + ProtectOutputValue +>; + +// @public (undocumented) +export type ProtectField = { + type: 'PROTECT'; + key: string; + behavioralDataCollection: boolean; + universalDeviceIdentification: boolean; +}; + +// @public +export interface ProtectOutputValue { + // (undocumented) + behavioralDataCollection: boolean; + // (undocumented) + universalDeviceIdentification: boolean; +} + +// @public +export interface QrCodeCollector extends NoValueCollectorBase<'QrCodeCollector'> { + // (undocumented) + output: NoValueCollectorBase<'QrCodeCollector'>['output'] & { + src: string; + }; +} + +// @public (undocumented) +export type QrCodeField = { + type: 'QR_CODE'; + key: string; + content: string; + fallbackText?: string; +}; + +// @public +export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollector'> { + // (undocumented) + output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { + content: string; + title?: string; + }; +} + +// @public (undocumented) +export type ReadOnlyField = { + type: 'LABEL'; + content: string; + richContent?: RichContent; + key?: string; +}; + +// @public (undocumented) +export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField | ImageField; + +// @public (undocumented) +export type RedirectField = { + type: 'SOCIAL_LOGIN_BUTTON'; + key: string; + label: string; + links: Links; +}; + +// @public (undocumented) +export type RedirectFields = RedirectField; + +export { RequestMiddleware }; + +// @public +export type RichContent = { + content: string; + replacements?: Record; +}; + +// @public +export interface RichContentLink { + // (undocumented) + href: string; + // (undocumented) + key: string; + // (undocumented) + target?: '_self' | '_blank'; + // (undocumented) + type: 'link'; + // (undocumented) + value: string; +} + +// @public +export type RichContentReplacement = { + type: 'link'; + value: string; + href: string; + target?: '_self' | '_blank'; +}; + +// @public +export interface RichTextCollector extends NoValueCollectorBase<'RichTextCollector'> { + // (undocumented) + output: NoValueCollectorBase<'RichTextCollector'>['output'] & { + content: string; + richContent: CollectorRichContent; + }; +} + +// @public (undocumented) +export interface SelectorOption { + // (undocumented) + label: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export type SingleCheckboxField = { + type: 'SINGLE_CHECKBOX'; + inputType: 'BOOLEAN'; + key: string; + label: string; + required?: boolean; + errorMessage?: string; + appearance: string; + richContent?: RichContent; +}; + +// @public (undocumented) +export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>; + +// @public (undocumented) +export interface SingleSelectCollectorNoValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export interface SingleSelectCollectorWithValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: string | number | boolean; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type SingleSelectField = { + inputType: 'SINGLE_SELECT'; + key: string; + label: string; + options: { + label: string; + value: string; + }[]; + required?: boolean; + type: 'RADIO' | 'DROPDOWN'; +}; + +// @public (undocumented) +export type SingleValueAutoCollector = AutoCollector< + 'SingleValueAutoCollector', + 'SingleValueAutoCollector', + string +>; + +// @public (undocumented) +export type SingleValueAutoCollectorTypes = + | 'SingleValueAutoCollector' + | 'ProtectCollector' + | 'PollingCollector'; + +// @public +export type SingleValueCollector = + | SingleValueCollectorWithValue + | SingleValueCollectorNoValue; + +// @public (undocumented) +export interface SingleValueCollectorNoValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type SingleValueCollectors = + | PasswordCollector + | ValidatedPasswordCollector + | SingleSelectCollector + | TextCollector + | ValidatedTextCollector + | BooleanCollector + | ValidatedBooleanCollector + | SingleValueCollectorWithValue<'SingleValueCollector'>; + +// @public +export type SingleValueCollectorTypes = + | 'PasswordCollector' + | 'ValidatedPasswordCollector' + | 'BooleanCollector' + | 'ValidatedBooleanCollector' + | 'SingleValueCollector' + | 'SingleSelectCollector' + | 'SingleSelectObjectCollector' + | 'TextCollector' + | 'ValidatedTextCollector'; + +// @public (undocumented) +export interface SingleValueCollectorWithValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: V; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: V; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type SingleValueFields = + | StandardField + | PasswordField + | ValidatedField + | SingleCheckboxField + | SingleSelectField + | ProtectField; + +// @public (undocumented) +export type StandardField = { + type: 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON'; + key: string; + label: string; + required?: boolean; +}; + +// @public (undocumented) +export interface StartNode { + // (undocumented) + cache: null; + // (undocumented) + client: { + status: 'start'; + }; + // (undocumented) + error: DaVinciError | null; + // (undocumented) + server: { + status: 'start'; + }; + // (undocumented) + status: 'start'; +} + +// @public (undocumented) +export interface StartOptions { + // (undocumented) + query: Query; +} + +// @public (undocumented) +export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>; + +// @public (undocumented) +export interface SuccessNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + authorization?: { + code?: string; + state?: string; + }; + status: 'success'; + } | null; + // (undocumented) + error: null; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + href?: string; + session?: string; + status: 'success'; + }; + // (undocumented) + status: 'success'; +} + +// @public (undocumented) +export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>; + +// @public (undocumented) +export interface ThrownQueryError { + // (undocumented) + error: FetchBaseQueryError; + // (undocumented) + isHandledError: boolean; + // (undocumented) + meta: FetchBaseQueryMeta; +} + +// @public +export type UnknownCollector = { + category: 'UnknownCollector'; + error: string | null; + type: 'UnknownCollector'; + id: string; + name: string; + output: { + key: string; + label: string; + type: string; + }; +}; + +// @public (undocumented) +export type UnknownField = Record; + +// @public (undocumented) +export const updateCollectorValues: ActionCreatorWithPayload< + { + id: string; + value: CollectorValueTypes; + index?: number; + }, + string +>; + +// @public +export type Updater = ( + value: CollectorValueType, + index?: number, +) => InternalErrorResponse | null; + +// @public (undocumented) +export interface ValidatedBooleanCollector extends ValidatedSingleValueCollectorWithValue< + 'ValidatedBooleanCollector', + boolean +> { + // (undocumented) + output: ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + +// @public +export type ValidatedCollectors = + | ValidatedTextCollector + | ValidatedBooleanCollector + | ValidatedPasswordCollector + | ObjectValueCollectors + | MultiValueCollectors + | AutoCollectors; + +// @public (undocumented) +export type ValidatedField = { + type: 'TEXT'; + key: string; + label: string; + required: boolean; + validation: { + regex: string; + errorMessage: string; + }; +}; + +// @public (undocumented) +export interface ValidatedPasswordCollector { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + validation: PasswordPolicy; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + verify: boolean; + }; + // (undocumented) + type: 'ValidatedPasswordCollector'; +} + +// @public (undocumented) +export interface ValidatedSingleValueCollectorWithValue< + T extends SingleValueCollectorTypes, + V = string, +> { + // (undocumented) + category: 'ValidatedSingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + type: string; + validation: (ValidationRequired | ValidationRegex)[]; + value: V; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: V; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>; + +// @public (undocumented) +export interface ValidationPhoneNumber { + // (undocumented) + message: string; + // (undocumented) + rule: boolean; + // (undocumented) + type: 'validatePhoneNumber'; +} + +// @public (undocumented) +export interface ValidationRegex { + // (undocumented) + message: string; + // (undocumented) + rule: string; + // (undocumented) + type: 'regex'; +} + +// @public (undocumented) +export interface ValidationRequired { + // (undocumented) + message: string; + // (undocumented) + rule: boolean; + // (undocumented) + type: 'required'; +} + +// @public +export type Validator = ( + value: CollectorValueType, +) => + | string[] + | { + error: { + message: string; + type: string; + }; + type: string; + }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index a8dc82d60a..5b0cd81f11 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -1,2010 +1,2428 @@ -## API Report File for "@forgerock/davinci-client" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; -import { ActionTypes } from '@forgerock/sdk-request-middleware'; -import { CustomLogger } from '@forgerock/sdk-logger'; -import type { DaVinciConfig } from '@forgerock/sdk-types'; -import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; -import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; -import { GenericError } from '@forgerock/sdk-types'; -import { LogLevel } from '@forgerock/sdk-logger'; -import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; -import { QueryStatus } from '@reduxjs/toolkit/query'; -import { Reducer } from '@reduxjs/toolkit'; -import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; -import type { SdkStore } from '@forgerock/sdk-store'; -import { SerializedError } from '@reduxjs/toolkit'; -import { Unsubscribe } from '@reduxjs/toolkit'; - -// @public (undocumented) -export type ActionCollector = ActionCollectorNoUrl | ActionCollectorWithUrl; - -// @public (undocumented) -export interface ActionCollectorNoUrl { - // (undocumented) - category: 'ActionCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type ActionCollectors = ActionCollectorWithUrl<'IdpCollector'> | ActionCollectorNoUrl<'ActionCollector'> | ActionCollectorNoUrl<'FlowCollector'> | ActionCollectorNoUrl<'SubmitCollector'>; - -// @public -export type ActionCollectorTypes = 'FlowCollector' | 'SubmitCollector' | 'IdpCollector' | 'ActionCollector'; - -// @public (undocumented) -export interface ActionCollectorWithUrl { - // (undocumented) - category: 'ActionCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - url?: string | null; - }; - // (undocumented) - type: T; -} - -export { ActionTypes } - -// @public (undocumented) -export type AgreementField = { - type: 'AGREEMENT'; - key: string; - content: string; - titleEnabled: boolean; - title?: string; - agreement: { - id: string; - useDynamicAgreement: boolean; - }; - enabled: boolean; -}; - -// @public (undocumented) -export interface AssertionValue extends Omit { - // (undocumented) - rawId: string; - // (undocumented) - response: { - clientDataJSON: string; - authenticatorData: string; - signature: string; - userHandle: string | null; - }; -} - -// @public (undocumented) -export interface AttestationValue extends Omit { - // (undocumented) - rawId: string; - // (undocumented) - response: { - clientDataJSON: string; - attestationObject: string; - }; -} - -// @public (undocumented) -export interface AutoCollector> { - // (undocumented) - category: C; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: IV; - type: string; - validation?: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - type: string; - config: OV; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector'; - -// @public (undocumented) -export type AutoCollectors = ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | MetadataCollector | PollingCollector | SingleValueAutoCollector | ObjectValueAutoCollector; - -// @public (undocumented) -export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes; - -// @public (undocumented) -export interface BooleanCollector extends SingleValueCollectorWithValue<'BooleanCollector', boolean> { - // (undocumented) - output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { - appearance: string; - richContent?: CollectorRichContent; - }; -} - -// @public (undocumented) -export interface CollectorErrors { - // (undocumented) - code: string; - // (undocumented) - message: string; - // (undocumented) - target: string; -} - -// @public -export interface CollectorRichContent { - // (undocumented) - content: string; - // (undocumented) - replacements: RichContentLink[]; -} - -// @public (undocumented) -export type Collectors = FlowCollector | MetadataCollector | PasswordCollector | ValidatedPasswordCollector | TextCollector | BooleanCollector | ValidatedBooleanCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | RichTextCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | ImageCollector | UnknownCollector; - -// @public -export type CollectorValueType = T extends { - type: 'PasswordCollector'; -} | { - type: 'ValidatedPasswordCollector'; -} | { - type: 'SingleSelectCollector'; -} | { - type: 'DeviceRegistrationCollector'; -} | { - type: 'DeviceAuthenticationCollector'; -} | { - type: 'ProtectCollector'; -} | { - type: 'PollingCollector'; -} ? string : T extends { - type: 'TextCollector'; - category: 'SingleValueCollector'; -} ? string : T extends { - type: 'TextCollector'; - category: 'ValidatedSingleValueCollector'; -} ? string : T extends { - type: 'BooleanCollector'; -} | { - type: 'ValidatedBooleanCollector'; -} ? boolean : T extends { - type: 'MultiSelectCollector'; -} ? string[] : T extends { - type: 'PhoneNumberCollector'; -} ? PhoneNumberInputValue : T extends { - type: 'PhoneNumberExtensionCollector'; -} ? PhoneNumberExtensionInputValue : T extends { - type: 'FidoRegistrationCollector'; -} ? FidoRegistrationInputValue | GenericError : T extends { - type: 'FidoAuthenticationCollector'; -} ? FidoAuthenticationInputValue | GenericError : T extends { - type: 'MetadataCollector'; -} ? Record | MetadataError : T extends { - category: 'SingleValueCollector'; -} ? string : T extends { - category: 'ValidatedSingleValueCollector'; -} ? string : T extends { - category: 'SingleValueAutoCollector'; -} ? string : T extends { - category: 'MultiValueCollector'; -} ? string[] : T extends { - category: 'ActionCollector'; -} ? never : T extends { - category: 'NoValueCollector'; -} ? never : CollectorValueTypes; - -// @public -export type CollectorValueTypes = string | string[] | boolean | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError | GenericError; - -// @public (undocumented) -export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField | MetadataField; - -// @public (undocumented) -export interface ContinueNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: 'continue'; - }; - // (undocumented) - error: null; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - eventName?: string; - status: 'continue'; - }; - // (undocumented) - status: 'continue'; -} - -export { CustomLogger } - -// @public -export type CustomPollingStatus = string & {}; - -// @public -export function davinci(input: { - config: DaVinciConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - store?: SdkStore; -}): Promise<{ - store: SdkStore; - subscribe: (listener: () => void) => Unsubscribe; - externalIdp: () => (() => Promise); - flow: (action: DaVinciAction) => InitFlow; - next: (args?: DaVinciRequest) => Promise; - resume: (input: { - continueToken: string; - }) => Promise; - start: (options?: StartOptions | undefined) => Promise; - update: (collector: T) => Updater; - validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; - pollStatus: (collector: PollingCollector) => Poller; - getClient: () => { - status: "start"; - } | { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: "continue"; - } | { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: "error"; - } | { - authorization?: { - code?: string; - state?: string; - }; - status: "success"; - } | { - status: "failure"; - } | null; - getCollectors: () => Collectors[]; - getError: () => DaVinciError | null; - getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; - getServer: () => { - _links?: Links; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - eventName?: string; - status: "continue"; - } | { - status: "start"; - } | { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: "error"; - } | { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - session?: string; - status: "success"; - } | { - _links?: Links; - eventName?: string; - href?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: "failure"; - } | null; - cache: { - getLatestResponse: () => ({ - status: QueryStatus.fulfilled; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "data" | "fulfilledTimeStamp"> & Required> & { - error: undefined; - } & { - status: QueryStatus.fulfilled; - isUninitialized: false; - isLoading: false; - isSuccess: true; - isError: false; - }) | ({ - status: QueryStatus.rejected; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "error"> & Required> & { - status: QueryStatus.rejected; - isUninitialized: false; - isLoading: false; - isSuccess: false; - isError: true; - }) | { - error: { - message: string; - type: string; - }; - }; - getResponseWithId: (requestId: string) => ({ - status: QueryStatus.fulfilled; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "data" | "fulfilledTimeStamp"> & Required> & { - error: undefined; - } & { - status: QueryStatus.fulfilled; - isUninitialized: false; - isLoading: false; - isSuccess: true; - isError: false; - }) | ({ - status: QueryStatus.rejected; - } & Omit<{ - requestId: string; - data?: unknown; - error?: FetchBaseQueryError | SerializedError | undefined; - endpointName: string; - startedTimeStamp: number; - fulfilledTimeStamp?: number; - }, "error"> & Required> & { - status: QueryStatus.rejected; - isUninitialized: false; - isLoading: false; - isSuccess: false; - isError: true; - }) | { - error: { - message: string; - type: string; - }; - }; - }; -}>; - -// @public -export interface DaVinciAction { - // (undocumented) - action: string; -} - -// @public -export interface DaVinciBaseResponse { - // (undocumented) - capabilityName?: string; - // (undocumented) - companyId?: string; - // (undocumented) - connectionId?: string; - // (undocumented) - connectorId?: string; - // (undocumented) - id?: string; - // (undocumented) - interactionId?: string; - // (undocumented) - interactionToken?: string; - // (undocumented) - isResponseCompatibleWithMobileAndWebSdks?: boolean; - // (undocumented) - status?: string; -} - -// @public -export type DaVinciCacheEntry = { - data?: DaVinciBaseResponse; - error?: { - data: DaVinciBaseResponse; - status: number; - }; -} & { - data?: any; - error?: any; -} & MutationResultSelectorResult; - -// @public (undocumented) -export type DavinciClient = Awaited>; - -export { DaVinciConfig } - -// @public (undocumented) -export interface DaVinciError extends Omit { - // (undocumented) - collectors?: CollectorErrors[]; - // (undocumented) - internalHttpStatus?: number; - // (undocumented) - message: string; - // (undocumented) - status: 'error' | 'failure' | 'unknown'; -} - -// @public (undocumented) -export interface DaVinciErrorCacheEntry { - // (undocumented) - endpointName: 'next' | 'flow' | 'start'; - // (undocumented) - error: { - data: T; - }; - // (undocumented) - fulfilledTimeStamp: number; - // (undocumented) - isError: boolean; - // (undocumented) - isLoading: boolean; - // (undocumented) - isSuccess: boolean; - // (undocumented) - isUninitialized: boolean; - // (undocumented) - requestId: string; - // (undocumented) - startedTimeStamp: number; - // (undocumented) - status: 'fulfilled' | 'pending' | 'rejected'; -} - -// @public (undocumented) -export interface DavinciErrorResponse extends DaVinciBaseResponse { - // (undocumented) - cause?: string | null; - // (undocumented) - code: string | number; - // (undocumented) - details?: ErrorDetail[]; - // (undocumented) - doNotSendToOE?: boolean; - // (undocumented) - error?: { - code?: string; - message?: string; - }; - // (undocumented) - errorCategory?: string; - // (undocumented) - errorMessage?: string; - // (undocumented) - expected?: boolean; - // (undocumented) - httpResponseCode: number; - // (undocumented) - isErrorCustomized?: boolean; - // (undocumented) - message: string; - // (undocumented) - metricAttributes?: { - [key: string]: unknown; - }; -} - -// @public (undocumented) -export interface DaVinciFailureResponse extends DaVinciBaseResponse { - // (undocumented) - error?: { - code?: string; - message?: string; - [key: string]: unknown; - }; -} - -// @public (undocumented) -export type DaVinciField = ComplexValueFields | MultiValueFields | ReadOnlyFields | RedirectFields | SingleValueFields; - -// @public -export interface DaVinciNextResponse extends DaVinciBaseResponse { - // (undocumented) - eventName?: string; - // (undocumented) - form?: { - name?: string; - description?: string; - components?: { - fields?: DaVinciField[]; - }; - }; - // (undocumented) - formData?: { - value?: { - [key: string]: string; - }; - }; - // (undocumented) - _links?: Links; -} - -// @public -export interface DaVinciPollResponse extends DaVinciBaseResponse { - // (undocumented) - eventName?: string; - // (undocumented) - _links?: Links; - // (undocumented) - success?: boolean; -} - -// @public (undocumented) -export interface DaVinciRequest { - // (undocumented) - eventName: string; - // (undocumented) - id: string; - // (undocumented) - interactionId: string; - // (undocumented) - parameters: { - eventType: 'submit' | 'action' | 'polling'; - data: { - actionKey: string; - formData?: Record; - }; - }; -} - -// @public -export type DaVinciRequestValueTypes = string | number | boolean | (string | number | boolean)[] | DeviceValue | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue | MetadataError | GenericError; - -// @public (undocumented) -export interface DaVinciSuccessResponse extends DaVinciBaseResponse { - // (undocumented) - authorizeResponse?: OAuthDetails; - // (undocumented) - environment: { - id: string; - [key: string]: unknown; - }; - // (undocumented) - _links?: Links; - // (undocumented) - resetCookie?: boolean; - // (undocumented) - session?: { - id?: string; - [key: string]: unknown; - }; - // (undocumented) - sessionToken?: string; - // (undocumented) - sessionTokenMaxAge?: number; - // (undocumented) - status: string; - // (undocumented) - subFlowSettings?: { - cssLinks?: unknown[]; - cssUrl?: unknown; - jsLinks?: unknown[]; - loadingScreenSettings?: unknown; - reactSkUrl?: unknown; - }; - // (undocumented) - success: true; -} - -// @public (undocumented) -export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue<'DeviceAuthenticationCollector', DeviceValue>; - -// @public (undocumented) -export type DeviceAuthenticationField = { - type: 'DEVICE_AUTHENTICATION'; - key: string; - label: string; - options: { - type: string; - iconSrc: string; - title: string; - id: string; - default: boolean; - description: string; - }[]; - required: boolean; -}; - -// @public (undocumented) -export interface DeviceOptionNoDefault { - // (undocumented) - content: string; - // (undocumented) - key: string; - // (undocumented) - label: string; - // (undocumented) - type: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export interface DeviceOptionWithDefault { - // (undocumented) - content: string; - // (undocumented) - default: boolean; - // (undocumented) - key: string; - // (undocumented) - label: string; - // (undocumented) - type: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue<'DeviceRegistrationCollector', string>; - -// @public (undocumented) -export type DeviceRegistrationField = { - type: 'DEVICE_REGISTRATION'; - key: string; - label: string; - options: { - type: string; - iconSrc: string; - title: string; - description: string; - }[]; - required: boolean; -}; - -// @public (undocumented) -export interface DeviceValue { - // (undocumented) - id: string; - // (undocumented) - type: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export interface ErrorDetail { - // (undocumented) - message?: string; - // (undocumented) - rawResponse?: { - _embedded?: { - users?: Array; - }; - code?: string; - count?: number; - details?: NestedErrorDetails[]; - id?: string; - message?: string; - size?: number; - userFilter?: string; - [key: string]: unknown; - }; - // (undocumented) - statusCode?: number; -} - -// @public (undocumented) -export interface ErrorNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - action: string; - collectors: Collectors[]; - description?: string; - name?: string; - status: 'error'; - }; - // (undocumented) - error: DaVinciError; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: 'error'; - } | null; - // (undocumented) - status: 'error'; -} - -// @public (undocumented) -export interface FailureNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - status: 'failure'; - }; - // (undocumented) - error: DaVinciError; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - eventName?: string; - href?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - status: 'failure'; - } | null; - // (undocumented) - status: 'failure'; -} - -// @public (undocumented) -export type FidoAuthenticationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoAuthenticationCollector', FidoAuthenticationInputValue | GenericError, FidoAuthenticationOutputValue>; - -// @public (undocumented) -export type FidoAuthenticationField = { - type: 'FIDO2'; - key: string; - label: string; - publicKeyCredentialRequestOptions: FidoAuthenticationOptions; - action: 'AUTHENTICATE'; - trigger: string; - required: boolean; -}; - -// @public (undocumented) -export interface FidoAuthenticationInputValue { - // (undocumented) - assertionValue?: AssertionValue; -} - -// @public (undocumented) -export interface FidoAuthenticationOptions extends Omit { - // (undocumented) - allowCredentials?: { - id: number[]; - transports?: AuthenticatorTransport[]; - type: PublicKeyCredentialType; - }[]; - // (undocumented) - challenge: number[]; -} - -// @public (undocumented) -export interface FidoAuthenticationOutputValue { - // (undocumented) - action: 'AUTHENTICATE'; - // (undocumented) - publicKeyCredentialRequestOptions: FidoAuthenticationOptions; - // (undocumented) - trigger: string; -} - -// @public (undocumented) -export interface FidoClient { - authenticate: (options: FidoAuthenticationOptions) => Promise; - register: (options: FidoRegistrationOptions) => Promise; -} - -// @public -export type FidoErrorCode = 'NotAllowedError' | 'AbortError' | 'InvalidStateError' | 'NotSupportedError' | 'SecurityError' | 'TimeoutError' | 'UnknownError'; - -// @public (undocumented) -export type FidoRegistrationCollector = AutoCollector<'ObjectValueAutoCollector', 'FidoRegistrationCollector', FidoRegistrationInputValue | GenericError, FidoRegistrationOutputValue>; - -// @public (undocumented) -export type FidoRegistrationField = { - type: 'FIDO2'; - key: string; - label: string; - publicKeyCredentialCreationOptions: FidoRegistrationOptions; - action: 'REGISTER'; - trigger: string; - required: boolean; -}; - -// @public (undocumented) -export interface FidoRegistrationInputValue { - // (undocumented) - attestationValue?: AttestationValue; -} - -// @public (undocumented) -export interface FidoRegistrationOptions extends Omit { - // (undocumented) - challenge: number[]; - // (undocumented) - excludeCredentials?: { - id: number[]; - transports?: AuthenticatorTransport[]; - type: PublicKeyCredentialType; - }[]; - // (undocumented) - pubKeyCredParams: { - alg: string | number; - type: PublicKeyCredentialType; - }[]; - // (undocumented) - user: { - id: number[]; - name: string; - displayName: string; - }; -} - -// @public (undocumented) -export interface FidoRegistrationOutputValue { - // (undocumented) - action: 'REGISTER'; - // (undocumented) - publicKeyCredentialCreationOptions: FidoRegistrationOptions; - // (undocumented) - trigger: string; -} - -// @public (undocumented) -export type FlowCollector = ActionCollectorNoUrl<'FlowCollector'>; - -// @public (undocumented) -export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; - -// @public -export type GetClient = StartNode['client'] | ContinueNode['client'] | ErrorNode['client'] | SuccessNode['client'] | FailureNode['client']; - -// @public (undocumented) -export type IdpCollector = ActionCollectorWithUrl<'IdpCollector'>; - -// @public -export interface ImageCollector extends NoValueCollectorBase<'ImageCollector'> { - // (undocumented) - output: NoValueCollectorBase<'ImageCollector'>['output'] & { - src: string; - alt: string; - href?: string; - }; -} - -// @public (undocumented) -export type ImageField = { - type: 'IMAGE'; - key: string; - description: string; - imageUrl: string; - hyperlinkUrl?: string; -}; - -// @public (undocumented) -export type InferActionCollectorType = T extends 'IdpCollector' ? IdpCollector : T extends 'SubmitCollector' ? SubmitCollector : T extends 'FlowCollector' ? FlowCollector : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>; - -// @public -export type InferAutoCollectorType = T extends 'ProtectCollector' ? ProtectCollector : T extends 'PollingCollector' ? PollingCollector : T extends 'FidoRegistrationCollector' ? FidoRegistrationCollector : T extends 'FidoAuthenticationCollector' ? FidoAuthenticationCollector : T extends 'MetadataCollector' ? MetadataCollector : T extends 'ObjectValueAutoCollector' ? ObjectValueAutoCollector : SingleValueAutoCollector; - -// @public -export type InferMultiValueCollectorType = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>; - -// @public -export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'ImageCollector' ? ImageCollector : NoValueCollectorBase<'NoValueCollector'>; - -// @public -export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'BooleanCollector' ? BooleanCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; - -// @public (undocumented) -export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; - -// @public (undocumented) -export type InitFlow = () => Promise; - -// @public (undocumented) -export interface InternalErrorResponse { - // (undocumented) - error: Omit & { - message: string; - }; - // (undocumented) - type: 'internal_error'; -} - -// @public (undocumented) -export interface Links { - // (undocumented) - [key: string]: { - href?: string; - }; -} - -export { LogLevel } - -// @public (undocumented) -export type MetadataCollector = AutoCollector<'ObjectValueAutoCollector', 'MetadataCollector', MetadataCollectorInputValue, Record>; - -// @public (undocumented) -export type MetadataCollectorInputValue = Record | MetadataError; - -// @public -export interface MetadataError { - // (undocumented) - code: string; - // (undocumented) - message: string; -} - -// @public (undocumented) -export type MetadataField = { - type: 'METADATA'; - key: string; - payload: Record; -}; - -// @public (undocumented) -export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectCollector'>; - -// @public (undocumented) -export type MultiSelectField = { - inputType: 'MULTI_SELECT'; - key: string; - label: string; - options: { - label: string; - value: string; - }[]; - required?: boolean; - type: 'CHECKBOX' | 'COMBOBOX'; -}; - -// @public (undocumented) -export type MultiValueCollector = MultiValueCollectorWithValue | MultiValueCollectorNoValue; - -// @public (undocumented) -export interface MultiValueCollectorNoValue { - // (undocumented) - category: 'MultiValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string[]; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type MultiValueCollectors = MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorWithValue<'MultiSelectCollector'>; - -// @public -export type MultiValueCollectorTypes = 'MultiSelectCollector' | 'MultiValueCollector'; - -// @public (undocumented) -export interface MultiValueCollectorWithValue { - // (undocumented) - category: 'MultiValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string[]; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: string[]; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type MultiValueFields = MultiSelectField; - -// @public -export interface NestedErrorDetails { - // (undocumented) - code?: string; - // (undocumented) - innerError?: { - history?: string; - unsatisfiedRequirements?: string[]; - failuresRemaining?: number; - }; - // (undocumented) - message?: string; - // (undocumented) - target?: string; -} - -// @public -export const nextCollectorValues: ActionCreatorWithPayload< { -fields: DaVinciField[]; -formData: { -value: Record; -}; -}, string>; - -// @public -export const nodeCollectorReducer: Reducer & { - getInitialState: () => Collectors[]; -}; - -// @public (undocumented) -export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode; - -// @public (undocumented) -export type NoValueCollector = InferNoValueCollectorType; - -// @public (undocumented) -export interface NoValueCollectorBase { - // (undocumented) - category: 'NoValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | ImageCollector; - -// @public -export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'ImageCollector'; - -// @public -export interface OAuthDetails { - // (undocumented) - [key: string]: unknown; - // (undocumented) - code?: string; - // (undocumented) - state?: string; -} - -// @public (undocumented) -export interface ObjectOptionsCollectorWithObjectValue, D = Record> { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: V; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: DeviceOptionWithDefault[]; - value?: D | null; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export interface ObjectOptionsCollectorWithStringValue { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: V; - type: string; - validation: ValidationRequired[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: DeviceOptionNoDefault[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type ObjectValueAutoCollector = AutoCollector<'ObjectValueAutoCollector', 'ObjectValueAutoCollector', Record>; - -// @public (undocumented) -export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRegistrationCollector' | 'FidoAuthenticationCollector' | 'MetadataCollector'; - -// @public (undocumented) -export type ObjectValueCollector = ObjectOptionsCollectorWithObjectValue | ObjectOptionsCollectorWithStringValue | ObjectValueCollectorWithObjectValue; - -// @public (undocumented) -export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; - -// @public -export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; - -// @public (undocumented) -export interface ObjectValueCollectorWithObjectValue, OV = Record> { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: IV; - type: string; - validation: (ValidationRequired | ValidationPhoneNumber)[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value?: OV | null; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export interface OutgoingQueryParams { - // (undocumented) - [key: string]: string | string[]; -} - -// @public (undocumented) -export interface PasswordCollector { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - verify: boolean; - }; - // (undocumented) - type: 'PasswordCollector'; -} - -// @public -export type PasswordField = { - type: 'PASSWORD' | 'PASSWORD_VERIFY'; - key: string; - label: string; - required?: boolean; - verify?: boolean; - passwordPolicy?: PasswordPolicy; -}; - -// @public -export interface PasswordPolicy { - // (undocumented) - createdAt?: string; - // (undocumented) - default?: boolean; - // (undocumented) - description?: string; - // (undocumented) - excludesCommonlyUsed?: boolean; - // (undocumented) - excludesProfileData?: boolean; - // (undocumented) - history?: { - count?: number; - retentionDays?: number; - }; - // (undocumented) - id?: string; - // (undocumented) - length?: { - min?: number; - max?: number; - }; - // (undocumented) - lockout?: { - failureCount?: number; - durationSeconds?: number; - }; - // (undocumented) - maxAgeDays?: number; - // (undocumented) - maxRepeatedCharacters?: number; - // (undocumented) - minAgeDays?: number; - // (undocumented) - minCharacters?: Record; - // (undocumented) - minUniqueCharacters?: number; - // (undocumented) - name?: string; - // (undocumented) - notSimilarToCurrent?: boolean; - // (undocumented) - populationCount?: number; - // (undocumented) - updatedAt?: string; -} - -// @public (undocumented) -export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>; - -// @public (undocumented) -export interface PhoneNumberExtensionCollector { - // (undocumented) - category: 'ObjectValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: PhoneNumberExtensionInputValue; - type: string; - validation: (ValidationRequired | ValidationPhoneNumber)[] | null; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - extensionLabel: string; - value: PhoneNumberExtensionOutputValue; - }; - // (undocumented) - type: 'PhoneNumberExtensionCollector'; -} - -// @public (undocumented) -export type PhoneNumberExtensionField = PhoneNumberField & { - showExtension: boolean; - extensionLabel: string; -}; - -// @public (undocumented) -export interface PhoneNumberExtensionInputValue { - // (undocumented) - countryCode: string; - // (undocumented) - extension: string; - // (undocumented) - phoneNumber: string; -} - -// @public (undocumented) -export interface PhoneNumberExtensionOutputValue { - // (undocumented) - countryCode?: string; - // (undocumented) - extension?: string; - // (undocumented) - phoneNumber?: string; -} - -// @public (undocumented) -export type PhoneNumberField = { - type: 'PHONE_NUMBER'; - key: string; - label: string; - required: boolean; - defaultCountryCode: string | null; - validatePhoneNumber: boolean; -}; - -// @public (undocumented) -export interface PhoneNumberInputValue { - // (undocumented) - countryCode: string; - // (undocumented) - phoneNumber: string; -} - -// @public (undocumented) -export interface PhoneNumberOutputValue { - // (undocumented) - countryCode?: string; - // (undocumented) - phoneNumber?: string; -} - -// @public -export type Poller = () => Promise; - -// @public (undocumented) -export type PollingCollector = AutoCollector<'SingleValueAutoCollector', 'PollingCollector', string, PollingOutputValue>; - -// @public (undocumented) -export type PollingField = { - type: 'POLLING'; - key: string; - pollInterval: number; - pollRetries: number; - pollChallengeStatus?: boolean; - challenge?: string; -}; - -// @public (undocumented) -export interface PollingOutputValue { - // (undocumented) - challenge?: string; - // (undocumented) - pollChallengeStatus?: boolean; - // (undocumented) - pollInterval: number; - // (undocumented) - pollRetries: number; - // (undocumented) - retriesRemaining?: number; -} - -// @public (undocumented) -export type PollingStatus = PollingStatusContinue | PollingStatusChallenge; - -// @public (undocumented) -export type PollingStatusChallenge = PollingStatusChallengeComplete | 'expired' | 'timedOut' | 'error'; - -// @public (undocumented) -export type PollingStatusChallengeComplete = 'approved' | 'denied' | 'continue' | CustomPollingStatus; - -// @public (undocumented) -export type PollingStatusContinue = 'continue' | 'timedOut'; - -// @public (undocumented) -export type ProtectCollector = AutoCollector<'SingleValueAutoCollector', 'ProtectCollector', string, ProtectOutputValue>; - -// @public (undocumented) -export type ProtectField = { - type: 'PROTECT'; - key: string; - behavioralDataCollection: boolean; - universalDeviceIdentification: boolean; -}; - -// @public -export interface ProtectOutputValue { - // (undocumented) - behavioralDataCollection: boolean; - // (undocumented) - universalDeviceIdentification: boolean; -} - -// @public -export interface QrCodeCollector extends NoValueCollectorBase<'QrCodeCollector'> { - // (undocumented) - output: NoValueCollectorBase<'QrCodeCollector'>['output'] & { - src: string; - }; -} - -// @public (undocumented) -export type QrCodeField = { - type: 'QR_CODE'; - key: string; - content: string; - fallbackText?: string; -}; - -// @public -export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollector'> { - // (undocumented) - output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { - content: string; - title?: string; - }; -} - -// @public (undocumented) -export type ReadOnlyField = { - type: 'LABEL'; - content: string; - richContent?: RichContent; - key?: string; -}; - -// @public (undocumented) -export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField | ImageField; - -// @public (undocumented) -export type RedirectField = { - type: 'SOCIAL_LOGIN_BUTTON'; - key: string; - label: string; - links: Links; -}; - -// @public (undocumented) -export type RedirectFields = RedirectField; - -export { RequestMiddleware } - -// @public -export type RichContent = { - content: string; - replacements?: Record; -}; - -// @public -export interface RichContentLink { - // (undocumented) - href: string; - // (undocumented) - key: string; - // (undocumented) - target?: '_self' | '_blank'; - // (undocumented) - type: 'link'; - // (undocumented) - value: string; -} - -// @public -export type RichContentReplacement = { - type: 'link'; - value: string; - href: string; - target?: '_self' | '_blank'; -}; - -// @public -export interface RichTextCollector extends NoValueCollectorBase<'RichTextCollector'> { - // (undocumented) - output: NoValueCollectorBase<'RichTextCollector'>['output'] & { - content: string; - richContent: CollectorRichContent; - }; -} - -// @public (undocumented) -export interface SelectorOption { - // (undocumented) - label: string; - // (undocumented) - value: string; -} - -// @public (undocumented) -export type SingleCheckboxField = { - type: 'SINGLE_CHECKBOX'; - inputType: 'BOOLEAN'; - key: string; - label: string; - required?: boolean; - errorMessage?: string; - appearance: string; - richContent?: RichContent; -}; - -// @public (undocumented) -export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>; - -// @public (undocumented) -export interface SingleSelectCollectorNoValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export interface SingleSelectCollectorWithValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: string | number | boolean; - options: SelectorOption[]; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type SingleSelectField = { - inputType: 'SINGLE_SELECT'; - key: string; - label: string; - options: { - label: string; - value: string; - }[]; - required?: boolean; - type: 'RADIO' | 'DROPDOWN'; -}; - -// @public (undocumented) -export type SingleValueAutoCollector = AutoCollector<'SingleValueAutoCollector', 'SingleValueAutoCollector', string>; - -// @public (undocumented) -export type SingleValueAutoCollectorTypes = 'SingleValueAutoCollector' | 'ProtectCollector' | 'PollingCollector'; - -// @public -export type SingleValueCollector = SingleValueCollectorWithValue | SingleValueCollectorNoValue; - -// @public (undocumented) -export interface SingleValueCollectorNoValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type SingleValueCollectors = PasswordCollector | ValidatedPasswordCollector | SingleSelectCollector | TextCollector | ValidatedTextCollector | BooleanCollector | ValidatedBooleanCollector | SingleValueCollectorWithValue<'SingleValueCollector'>; - -// @public -export type SingleValueCollectorTypes = 'PasswordCollector' | 'ValidatedPasswordCollector' | 'BooleanCollector' | 'ValidatedBooleanCollector' | 'SingleValueCollector' | 'SingleSelectCollector' | 'SingleSelectObjectCollector' | 'TextCollector' | 'ValidatedTextCollector'; - -// @public (undocumented) -export interface SingleValueCollectorWithValue { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: V; - type: string; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: V; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type SingleValueFields = StandardField | PasswordField | ValidatedField | SingleCheckboxField | SingleSelectField | ProtectField; - -// @public (undocumented) -export type StandardField = { - type: 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON'; - key: string; - label: string; - required?: boolean; -}; - -// @public (undocumented) -export interface StartNode { - // (undocumented) - cache: null; - // (undocumented) - client: { - status: 'start'; - }; - // (undocumented) - error: DaVinciError | null; - // (undocumented) - server: { - status: 'start'; - }; - // (undocumented) - status: 'start'; -} - -// @public (undocumented) -export interface StartOptions { - // (undocumented) - query: Query; -} - -// @public (undocumented) -export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>; - -// @public (undocumented) -export interface SuccessNode { - // (undocumented) - cache: { - key: string; - }; - // (undocumented) - client: { - authorization?: { - code?: string; - state?: string; - }; - status: 'success'; - } | null; - // (undocumented) - error: null; - // (undocumented) - httpStatus: number; - // (undocumented) - server: { - _links?: Links; - eventName?: string; - id?: string; - interactionId?: string; - interactionToken?: string; - href?: string; - session?: string; - status: 'success'; - }; - // (undocumented) - status: 'success'; -} - -// @public (undocumented) -export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>; - -// @public (undocumented) -export interface ThrownQueryError { - // (undocumented) - error: FetchBaseQueryError; - // (undocumented) - isHandledError: boolean; - // (undocumented) - meta: FetchBaseQueryMeta; -} - -// @public -export type UnknownCollector = { - category: 'UnknownCollector'; - error: string | null; - type: 'UnknownCollector'; - id: string; - name: string; - output: { - key: string; - label: string; - type: string; - }; -}; - -// @public (undocumented) -export type UnknownField = Record; - -// @public (undocumented) -export const updateCollectorValues: ActionCreatorWithPayload< { -id: string; -value: CollectorValueTypes; -index?: number; -}, string>; - -// @public -export type Updater = (value: CollectorValueType, index?: number) => InternalErrorResponse | null; - -// @public (undocumented) -export interface ValidatedBooleanCollector extends ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean> { - // (undocumented) - output: ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>['output'] & { - appearance: string; - richContent?: CollectorRichContent; - }; -} - -// @public -export type ValidatedCollectors = ValidatedTextCollector | ValidatedBooleanCollector | ValidatedPasswordCollector | ObjectValueCollectors | MultiValueCollectors | AutoCollectors; - -// @public (undocumented) -export type ValidatedField = { - type: 'TEXT'; - key: string; - label: string; - required: boolean; - validation: { - regex: string; - errorMessage: string; - }; -}; - -// @public (undocumented) -export interface ValidatedPasswordCollector { - // (undocumented) - category: 'SingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - value: string | number | boolean; - type: string; - validation: PasswordPolicy; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - verify: boolean; - }; - // (undocumented) - type: 'ValidatedPasswordCollector'; -} - -// @public (undocumented) -export interface ValidatedSingleValueCollectorWithValue { - // (undocumented) - category: 'ValidatedSingleValueCollector'; - // (undocumented) - error: string | null; - // (undocumented) - id: string; - // (undocumented) - input: { - key: string; - type: string; - validation: (ValidationRequired | ValidationRegex)[]; - value: V; - }; - // (undocumented) - name: string; - // (undocumented) - output: { - key: string; - label: string; - type: string; - value: V; - }; - // (undocumented) - type: T; -} - -// @public (undocumented) -export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>; - -// @public (undocumented) -export interface ValidationPhoneNumber { - // (undocumented) - message: string; - // (undocumented) - rule: boolean; - // (undocumented) - type: 'validatePhoneNumber'; -} - -// @public (undocumented) -export interface ValidationRegex { - // (undocumented) - message: string; - // (undocumented) - rule: string; - // (undocumented) - type: 'regex'; -} - -// @public (undocumented) -export interface ValidationRequired { - // (undocumented) - message: string; - // (undocumented) - rule: boolean; - // (undocumented) - type: 'required'; -} - -// @public -export type Validator = (value: CollectorValueType) => string[] | { - error: { - message: string; - type: string; - }; - type: string; -}; - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@forgerock/davinci-client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ActionCreatorWithPayload } from '@reduxjs/toolkit'; +import { ActionTypes } from '@forgerock/sdk-request-middleware'; +import { CustomLogger } from '@forgerock/sdk-logger'; +import type { DaVinciConfig } from '@forgerock/sdk-types'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import type { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; +import { GenericError } from '@forgerock/sdk-types'; +import { LogLevel } from '@forgerock/sdk-logger'; +import type { MutationResultSelectorResult } from '@reduxjs/toolkit/query'; +import { QueryStatus } from '@reduxjs/toolkit/query'; +import { Reducer } from '@reduxjs/toolkit'; +import { RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; +import { SerializedError } from '@reduxjs/toolkit'; +import { Unsubscribe } from '@reduxjs/toolkit'; + +// @public (undocumented) +export type ActionCollector = + | ActionCollectorNoUrl + | ActionCollectorWithUrl; + +// @public (undocumented) +export interface ActionCollectorNoUrl { + // (undocumented) + category: 'ActionCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type ActionCollectors = + | ActionCollectorWithUrl<'IdpCollector'> + | ActionCollectorNoUrl<'ActionCollector'> + | ActionCollectorNoUrl<'FlowCollector'> + | ActionCollectorNoUrl<'SubmitCollector'>; + +// @public +export type ActionCollectorTypes = + | 'FlowCollector' + | 'SubmitCollector' + | 'IdpCollector' + | 'ActionCollector'; + +// @public (undocumented) +export interface ActionCollectorWithUrl { + // (undocumented) + category: 'ActionCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + url?: string | null; + }; + // (undocumented) + type: T; +} + +export { ActionTypes }; + +// @public (undocumented) +export type AgreementField = { + type: 'AGREEMENT'; + key: string; + content: string; + titleEnabled: boolean; + title?: string; + agreement: { + id: string; + useDynamicAgreement: boolean; + }; + enabled: boolean; +}; + +// @public (undocumented) +export interface AssertionValue extends Omit< + PublicKeyCredential, + 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON' +> { + // (undocumented) + rawId: string; + // (undocumented) + response: { + clientDataJSON: string; + authenticatorData: string; + signature: string; + userHandle: string | null; + }; +} + +// @public (undocumented) +export interface AttestationValue extends Omit< + PublicKeyCredential, + 'rawId' | 'response' | 'getClientExtensionResults' | 'toJSON' +> { + // (undocumented) + rawId: string; + // (undocumented) + response: { + clientDataJSON: string; + attestationObject: string; + }; +} + +// @public (undocumented) +export interface AutoCollector< + C extends AutoCollectorCategories, + T extends AutoCollectorTypes, + IV = string, + OV = Record, +> { + // (undocumented) + category: C; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: IV; + type: string; + validation?: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + type: string; + config: OV; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type AutoCollectorCategories = 'SingleValueAutoCollector' | 'ObjectValueAutoCollector'; + +// @public (undocumented) +export type AutoCollectors = + | ProtectCollector + | FidoRegistrationCollector + | FidoAuthenticationCollector + | MetadataCollector + | PollingCollector + | SingleValueAutoCollector + | ObjectValueAutoCollector; + +// @public (undocumented) +export type AutoCollectorTypes = SingleValueAutoCollectorTypes | ObjectValueAutoCollectorTypes; + +// @public (undocumented) +export interface BooleanCollector extends SingleValueCollectorWithValue< + 'BooleanCollector', + boolean +> { + // (undocumented) + output: SingleValueCollectorWithValue<'BooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + +// @public (undocumented) +export interface CollectorErrors { + // (undocumented) + code: string; + // (undocumented) + message: string; + // (undocumented) + target: string; +} + +// @public +export interface CollectorRichContent { + // (undocumented) + content: string; + // (undocumented) + replacements: RichContentLink[]; +} + +// @public (undocumented) +export type Collectors = + | FlowCollector + | MetadataCollector + | PasswordCollector + | ValidatedPasswordCollector + | TextCollector + | BooleanCollector + | ValidatedBooleanCollector + | SingleSelectCollector + | IdpCollector + | SubmitCollector + | ActionCollector<'ActionCollector'> + | SingleValueCollector<'SingleValueCollector'> + | MultiSelectCollector + | DeviceAuthenticationCollector + | DeviceRegistrationCollector + | PhoneNumberCollector + | PhoneNumberExtensionCollector + | ReadOnlyCollector + | RichTextCollector + | ValidatedTextCollector + | ProtectCollector + | PollingCollector + | FidoRegistrationCollector + | FidoAuthenticationCollector + | QrCodeCollector + | ImageCollector + | UnknownCollector; + +// @public +export type CollectorValueType = T extends + | { + type: 'PasswordCollector'; + } + | { + type: 'ValidatedPasswordCollector'; + } + | { + type: 'SingleSelectCollector'; + } + | { + type: 'DeviceRegistrationCollector'; + } + | { + type: 'DeviceAuthenticationCollector'; + } + | { + type: 'ProtectCollector'; + } + | { + type: 'PollingCollector'; + } + ? string + : T extends { + type: 'TextCollector'; + category: 'SingleValueCollector'; + } + ? string + : T extends { + type: 'TextCollector'; + category: 'ValidatedSingleValueCollector'; + } + ? string + : T extends + | { + type: 'BooleanCollector'; + } + | { + type: 'ValidatedBooleanCollector'; + } + ? boolean + : T extends { + type: 'MultiSelectCollector'; + } + ? string[] + : T extends { + type: 'PhoneNumberCollector'; + } + ? PhoneNumberInputValue + : T extends { + type: 'PhoneNumberExtensionCollector'; + } + ? PhoneNumberExtensionInputValue + : T extends { + type: 'FidoRegistrationCollector'; + } + ? FidoRegistrationInputValue | GenericError + : T extends { + type: 'FidoAuthenticationCollector'; + } + ? FidoAuthenticationInputValue | GenericError + : T extends { + type: 'MetadataCollector'; + } + ? Record | MetadataError + : T extends { + category: 'SingleValueCollector'; + } + ? string + : T extends { + category: 'ValidatedSingleValueCollector'; + } + ? string + : T extends { + category: 'SingleValueAutoCollector'; + } + ? string + : T extends { + category: 'MultiValueCollector'; + } + ? string[] + : T extends { + category: 'ActionCollector'; + } + ? never + : T extends { + category: 'NoValueCollector'; + } + ? never + : CollectorValueTypes; + +// @public +export type CollectorValueTypes = + | string + | string[] + | boolean + | PhoneNumberInputValue + | PhoneNumberExtensionInputValue + | FidoRegistrationInputValue + | FidoAuthenticationInputValue + | MetadataError + | GenericError; + +// @public (undocumented) +export type ComplexValueFields = + | DeviceAuthenticationField + | DeviceRegistrationField + | PhoneNumberField + | PhoneNumberExtensionField + | FidoRegistrationField + | FidoAuthenticationField + | PollingField + | MetadataField; + +// @public (undocumented) +export interface ContinueNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'continue'; + }; + // (undocumented) + error: null; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + id?: string; + interactionId?: string; + interactionToken?: string; + href?: string; + eventName?: string; + status: 'continue'; + }; + // (undocumented) + status: 'continue'; +} + +export { CustomLogger }; + +// @public +export type CustomPollingStatus = string & {}; + +// @public +export function davinci(input: { + config: DaVinciConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + store?: SdkStore; +}): Promise<{ + store: SdkStore; + subscribe: (listener: () => void) => Unsubscribe; + externalIdp: () => () => Promise; + flow: (action: DaVinciAction) => InitFlow; + next: (args?: DaVinciRequest) => Promise; + resume: (input: { continueToken: string }) => Promise; + start: ( + options?: StartOptions | undefined, + ) => Promise; + update: < + T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors, + >( + collector: T, + ) => Updater; + validate: ( + collector: + | SingleValueCollectors + | ObjectValueCollectors + | MultiValueCollectors + | AutoCollectors, + ) => Validator; + pollStatus: (collector: PollingCollector) => Poller; + getClient: () => + | { + status: 'start'; + } + | { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'continue'; + } + | { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'error'; + } + | { + authorization?: { + code?: string; + state?: string; + }; + status: 'success'; + } + | { + status: 'failure'; + } + | null; + getCollectors: () => Collectors[]; + getError: () => DaVinciError | null; + getErrorCollectors: () => CollectorErrors[]; + getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; + getServer: () => + | { + status: 'start'; + } + | { + status: 'start'; + } + | { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'error'; + } + | { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + href?: string; + session?: string; + status: 'success'; + } + | { + _links?: Links; + eventName?: string; + href?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'failure'; + } + | null; + cache: { + getLatestResponse: () => + | ({ + status: QueryStatus.fulfilled; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > + > & { + error: undefined; + } & { + status: QueryStatus.fulfilled; + isUninitialized: false; + isLoading: false; + isSuccess: true; + isError: false; + }) + | ({ + status: QueryStatus.rejected; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > + > & { + status: QueryStatus.rejected; + isUninitialized: false; + isLoading: false; + isSuccess: false; + isError: true; + }) + | { + error: { + message: string; + type: string; + }; + }; + getResponseWithId: (requestId: string) => + | ({ + status: QueryStatus.fulfilled; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'data' | 'fulfilledTimeStamp' + > + > & { + error: undefined; + } & { + status: QueryStatus.fulfilled; + isUninitialized: false; + isLoading: false; + isSuccess: true; + isError: false; + }) + | ({ + status: QueryStatus.rejected; + } & Omit< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > & + Required< + Pick< + { + requestId: string; + data?: unknown; + error?: FetchBaseQueryError | SerializedError | undefined; + endpointName: string; + startedTimeStamp: number; + fulfilledTimeStamp?: number; + }, + 'error' + > + > & { + status: QueryStatus.rejected; + isUninitialized: false; + isLoading: false; + isSuccess: false; + isError: true; + }) + | { + error: { + message: string; + type: string; + }; + }; + }; +}>; + +// @public +export interface DaVinciAction { + // (undocumented) + action: string; +} + +// @public +export interface DaVinciBaseResponse { + // (undocumented) + capabilityName?: string; + // (undocumented) + companyId?: string; + // (undocumented) + connectionId?: string; + // (undocumented) + connectorId?: string; + // (undocumented) + id?: string; + // (undocumented) + interactionId?: string; + // (undocumented) + interactionToken?: string; + // (undocumented) + isResponseCompatibleWithMobileAndWebSdks?: boolean; + // (undocumented) + status?: string; +} + +// @public +export type DaVinciCacheEntry = { + data?: DaVinciBaseResponse; + error?: { + data: DaVinciBaseResponse; + status: number; + }; +} & { + data?: any; + error?: any; +} & MutationResultSelectorResult; + +// @public (undocumented) +export type DavinciClient = Awaited>; + +export { DaVinciConfig }; + +// @public (undocumented) +export interface DaVinciError extends Omit { + // (undocumented) + collectors?: CollectorErrors[]; + // (undocumented) + internalHttpStatus?: number; + // (undocumented) + message: string; + // (undocumented) + status: 'error' | 'failure' | 'unknown'; +} + +// @public (undocumented) +export interface DaVinciErrorCacheEntry { + // (undocumented) + endpointName: 'next' | 'flow' | 'start'; + // (undocumented) + error: { + data: T; + }; + // (undocumented) + fulfilledTimeStamp: number; + // (undocumented) + isError: boolean; + // (undocumented) + isLoading: boolean; + // (undocumented) + isSuccess: boolean; + // (undocumented) + isUninitialized: boolean; + // (undocumented) + requestId: string; + // (undocumented) + startedTimeStamp: number; + // (undocumented) + status: 'fulfilled' | 'pending' | 'rejected'; +} + +// @public (undocumented) +export interface DavinciErrorResponse extends DaVinciBaseResponse { + // (undocumented) + cause?: string | null; + // (undocumented) + code: string | number; + // (undocumented) + details?: ErrorDetail[]; + // (undocumented) + doNotSendToOE?: boolean; + // (undocumented) + error?: { + code?: string; + message?: string; + }; + // (undocumented) + errorCategory?: string; + // (undocumented) + errorMessage?: string; + // (undocumented) + expected?: boolean; + // (undocumented) + httpResponseCode: number; + // (undocumented) + isErrorCustomized?: boolean; + // (undocumented) + message: string; + // (undocumented) + metricAttributes?: { + [key: string]: unknown; + }; +} + +// @public (undocumented) +export interface DaVinciFailureResponse extends DaVinciBaseResponse { + // (undocumented) + error?: { + code?: string; + message?: string; + [key: string]: unknown; + }; +} + +// @public (undocumented) +export type DaVinciField = + | ComplexValueFields + | MultiValueFields + | ReadOnlyFields + | RedirectFields + | SingleValueFields; + +// @public +export interface DaVinciNextResponse extends DaVinciBaseResponse { + // (undocumented) + eventName?: string; + // (undocumented) + form?: { + name?: string; + description?: string; + components?: { + fields?: DaVinciField[]; + }; + }; + // (undocumented) + formData?: { + value?: { + [key: string]: string; + }; + }; + // (undocumented) + _links?: Links; +} + +// @public +export interface DaVinciPollResponse extends DaVinciBaseResponse { + // (undocumented) + eventName?: string; + // (undocumented) + _links?: Links; + // (undocumented) + success?: boolean; +} + +// @public (undocumented) +export interface DaVinciRequest { + // (undocumented) + eventName: string; + // (undocumented) + id: string; + // (undocumented) + interactionId: string; + // (undocumented) + parameters: { + eventType: 'submit' | 'action' | 'polling'; + data: { + actionKey: string; + formData?: Record; + }; + }; +} + +// @public +export type DaVinciRequestValueTypes = + | string + | number + | boolean + | (string | number | boolean)[] + | DeviceValue + | PhoneNumberInputValue + | FidoRegistrationInputValue + | FidoAuthenticationInputValue + | MetadataError + | GenericError; + +// @public (undocumented) +export interface DaVinciSuccessResponse extends DaVinciBaseResponse { + // (undocumented) + authorizeResponse?: OAuthDetails; + // (undocumented) + environment: { + id: string; + [key: string]: unknown; + }; + // (undocumented) + _links?: Links; + // (undocumented) + resetCookie?: boolean; + // (undocumented) + session?: { + id?: string; + [key: string]: unknown; + }; + // (undocumented) + sessionToken?: string; + // (undocumented) + sessionTokenMaxAge?: number; + // (undocumented) + status: string; + // (undocumented) + subFlowSettings?: { + cssLinks?: unknown[]; + cssUrl?: unknown; + jsLinks?: unknown[]; + loadingScreenSettings?: unknown; + reactSkUrl?: unknown; + }; + // (undocumented) + success: true; +} + +// @public (undocumented) +export type DeviceAuthenticationCollector = ObjectOptionsCollectorWithObjectValue< + 'DeviceAuthenticationCollector', + DeviceValue +>; + +// @public (undocumented) +export type DeviceAuthenticationField = { + type: 'DEVICE_AUTHENTICATION'; + key: string; + label: string; + options: { + type: string; + iconSrc: string; + title: string; + id: string; + default: boolean; + description: string; + }[]; + required: boolean; +}; + +// @public (undocumented) +export interface DeviceOptionNoDefault { + // (undocumented) + content: string; + // (undocumented) + key: string; + // (undocumented) + label: string; + // (undocumented) + type: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface DeviceOptionWithDefault { + // (undocumented) + content: string; + // (undocumented) + default: boolean; + // (undocumented) + key: string; + // (undocumented) + label: string; + // (undocumented) + type: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export type DeviceRegistrationCollector = ObjectOptionsCollectorWithStringValue< + 'DeviceRegistrationCollector', + string +>; + +// @public (undocumented) +export type DeviceRegistrationField = { + type: 'DEVICE_REGISTRATION'; + key: string; + label: string; + options: { + type: string; + iconSrc: string; + title: string; + description: string; + }[]; + required: boolean; +}; + +// @public (undocumented) +export interface DeviceValue { + // (undocumented) + id: string; + // (undocumented) + type: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export interface ErrorDetail { + // (undocumented) + message?: string; + // (undocumented) + rawResponse?: { + _embedded?: { + users?: Array; + }; + code?: string; + count?: number; + details?: NestedErrorDetails[]; + id?: string; + message?: string; + size?: number; + userFilter?: string; + [key: string]: unknown; + }; + // (undocumented) + statusCode?: number; +} + +// @public (undocumented) +export interface ErrorNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + action: string; + collectors: Collectors[]; + description?: string; + name?: string; + status: 'error'; + }; + // (undocumented) + error: DaVinciError; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'error'; + } | null; + // (undocumented) + status: 'error'; +} + +// @public (undocumented) +export interface FailureNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + status: 'failure'; + }; + // (undocumented) + error: DaVinciError; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + eventName?: string; + href?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + status: 'failure'; + } | null; + // (undocumented) + status: 'failure'; +} + +// @public (undocumented) +export type FidoAuthenticationCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'FidoAuthenticationCollector', + FidoAuthenticationInputValue | GenericError, + FidoAuthenticationOutputValue +>; + +// @public (undocumented) +export type FidoAuthenticationField = { + type: 'FIDO2'; + key: string; + label: string; + publicKeyCredentialRequestOptions: FidoAuthenticationOptions; + action: 'AUTHENTICATE'; + trigger: string; + required: boolean; +}; + +// @public (undocumented) +export interface FidoAuthenticationInputValue { + // (undocumented) + assertionValue?: AssertionValue; +} + +// @public (undocumented) +export interface FidoAuthenticationOptions extends Omit< + PublicKeyCredentialRequestOptions, + 'challenge' | 'allowCredentials' +> { + // (undocumented) + allowCredentials?: { + id: number[]; + transports?: AuthenticatorTransport[]; + type: PublicKeyCredentialType; + }[]; + // (undocumented) + challenge: number[]; +} + +// @public (undocumented) +export interface FidoAuthenticationOutputValue { + // (undocumented) + action: 'AUTHENTICATE'; + // (undocumented) + publicKeyCredentialRequestOptions: FidoAuthenticationOptions; + // (undocumented) + trigger: string; +} + +// @public (undocumented) +export interface FidoClient { + authenticate: ( + options: FidoAuthenticationOptions, + ) => Promise; + register: ( + options: FidoRegistrationOptions, + ) => Promise; +} + +// @public +export type FidoErrorCode = + | 'NotAllowedError' + | 'AbortError' + | 'InvalidStateError' + | 'NotSupportedError' + | 'SecurityError' + | 'TimeoutError' + | 'UnknownError'; + +// @public (undocumented) +export type FidoRegistrationCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'FidoRegistrationCollector', + FidoRegistrationInputValue | GenericError, + FidoRegistrationOutputValue +>; + +// @public (undocumented) +export type FidoRegistrationField = { + type: 'FIDO2'; + key: string; + label: string; + publicKeyCredentialCreationOptions: FidoRegistrationOptions; + action: 'REGISTER'; + trigger: string; + required: boolean; +}; + +// @public (undocumented) +export interface FidoRegistrationInputValue { + // (undocumented) + attestationValue?: AttestationValue; +} + +// @public (undocumented) +export interface FidoRegistrationOptions extends Omit< + PublicKeyCredentialCreationOptions, + 'challenge' | 'user' | 'pubKeyCredParams' | 'excludeCredentials' +> { + // (undocumented) + challenge: number[]; + // (undocumented) + excludeCredentials?: { + id: number[]; + transports?: AuthenticatorTransport[]; + type: PublicKeyCredentialType; + }[]; + // (undocumented) + pubKeyCredParams: { + alg: string | number; + type: PublicKeyCredentialType; + }[]; + // (undocumented) + user: { + id: number[]; + name: string; + displayName: string; + }; +} + +// @public (undocumented) +export interface FidoRegistrationOutputValue { + // (undocumented) + action: 'REGISTER'; + // (undocumented) + publicKeyCredentialCreationOptions: FidoRegistrationOptions; + // (undocumented) + trigger: string; +} + +// @public (undocumented) +export type FlowCollector = ActionCollectorNoUrl<'FlowCollector'>; + +// @public (undocumented) +export type FlowNode = ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode; + +// @public +export type GetClient = + | StartNode['client'] + | ContinueNode['client'] + | ErrorNode['client'] + | SuccessNode['client'] + | FailureNode['client']; + +// @public (undocumented) +export type IdpCollector = ActionCollectorWithUrl<'IdpCollector'>; + +// @public +export interface ImageCollector extends NoValueCollectorBase<'ImageCollector'> { + // (undocumented) + output: NoValueCollectorBase<'ImageCollector'>['output'] & { + src: string; + alt: string; + href?: string; + }; +} + +// @public (undocumented) +export type ImageField = { + type: 'IMAGE'; + key: string; + description: string; + imageUrl: string; + hyperlinkUrl?: string; +}; + +// @public (undocumented) +export type InferActionCollectorType = T extends 'IdpCollector' + ? IdpCollector + : T extends 'SubmitCollector' + ? SubmitCollector + : T extends 'FlowCollector' + ? FlowCollector + : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>; + +// @public +export type InferAutoCollectorType = T extends 'ProtectCollector' + ? ProtectCollector + : T extends 'PollingCollector' + ? PollingCollector + : T extends 'FidoRegistrationCollector' + ? FidoRegistrationCollector + : T extends 'FidoAuthenticationCollector' + ? FidoAuthenticationCollector + : T extends 'MetadataCollector' + ? MetadataCollector + : T extends 'ObjectValueAutoCollector' + ? ObjectValueAutoCollector + : SingleValueAutoCollector; + +// @public +export type InferMultiValueCollectorType = + T extends 'MultiSelectCollector' + ? MultiValueCollectorWithValue<'MultiSelectCollector'> + : + | MultiValueCollectorWithValue<'MultiValueCollector'> + | MultiValueCollectorNoValue<'MultiValueCollector'>; + +// @public +export type InferNoValueCollectorType = + T extends 'ReadOnlyCollector' + ? ReadOnlyCollector + : T extends 'RichTextCollector' + ? RichTextCollector + : T extends 'QrCodeCollector' + ? QrCodeCollector + : T extends 'ImageCollector' + ? ImageCollector + : NoValueCollectorBase<'NoValueCollector'>; + +// @public +export type InferSingleValueCollectorType = + T extends 'TextCollector' + ? TextCollector + : T extends 'SingleSelectCollector' + ? SingleSelectCollector + : T extends 'ValidatedTextCollector' + ? ValidatedTextCollector + : T extends 'PasswordCollector' + ? PasswordCollector + : T extends 'ValidatedPasswordCollector' + ? ValidatedPasswordCollector + : T extends 'BooleanCollector' + ? BooleanCollector + : T extends 'ValidatedBooleanCollector' + ? ValidatedBooleanCollector + : + | SingleValueCollectorWithValue<'SingleValueCollector'> + | SingleValueCollectorNoValue<'SingleValueCollector'>; + +// @public (undocumented) +export type InferValueObjectCollectorType = + T extends 'DeviceAuthenticationCollector' + ? DeviceAuthenticationCollector + : T extends 'DeviceRegistrationCollector' + ? DeviceRegistrationCollector + : T extends 'PhoneNumberCollector' + ? PhoneNumberCollector + : T extends 'PhoneNumberExtensionCollector' + ? PhoneNumberExtensionCollector + : + | ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> + | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; + +// @public (undocumented) +export type InitFlow = () => Promise; + +// @public (undocumented) +export interface InternalErrorResponse { + // (undocumented) + error: Omit & { + message: string; + }; + // (undocumented) + type: 'internal_error'; +} + +// @public (undocumented) +export interface Links { + // (undocumented) + [key: string]: { + href?: string; + }; +} + +export { LogLevel }; + +// @public (undocumented) +export type MetadataCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'MetadataCollector', + MetadataCollectorInputValue, + Record +>; + +// @public (undocumented) +export type MetadataCollectorInputValue = Record | MetadataError; + +// @public +export interface MetadataError { + // (undocumented) + code: string; + // (undocumented) + message: string; +} + +// @public (undocumented) +export type MetadataField = { + type: 'METADATA'; + key: string; + payload: Record; +}; + +// @public (undocumented) +export type MultiSelectCollector = MultiValueCollectorWithValue<'MultiSelectCollector'>; + +// @public (undocumented) +export type MultiSelectField = { + inputType: 'MULTI_SELECT'; + key: string; + label: string; + options: { + label: string; + value: string; + }[]; + required?: boolean; + type: 'CHECKBOX' | 'COMBOBOX'; +}; + +// @public (undocumented) +export type MultiValueCollector = + | MultiValueCollectorWithValue + | MultiValueCollectorNoValue; + +// @public (undocumented) +export interface MultiValueCollectorNoValue { + // (undocumented) + category: 'MultiValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string[]; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type MultiValueCollectors = + | MultiValueCollectorWithValue<'MultiValueCollector'> + | MultiValueCollectorWithValue<'MultiSelectCollector'>; + +// @public +export type MultiValueCollectorTypes = 'MultiSelectCollector' | 'MultiValueCollector'; + +// @public (undocumented) +export interface MultiValueCollectorWithValue { + // (undocumented) + category: 'MultiValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string[]; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: string[]; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type MultiValueFields = MultiSelectField; + +// @public +export interface NestedErrorDetails { + // (undocumented) + code?: string; + // (undocumented) + innerError?: { + history?: string; + unsatisfiedRequirements?: string[]; + failuresRemaining?: number; + }; + // (undocumented) + message?: string; + // (undocumented) + target?: string; +} + +// @public +export const nextCollectorValues: ActionCreatorWithPayload< + { + fields: DaVinciField[]; + formData: { + value: Record; + }; + }, + string +>; + +// @public +export const nodeCollectorReducer: Reducer & { + getInitialState: () => Collectors[]; +}; + +// @public (undocumented) +export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode; + +// @public (undocumented) +export type NoValueCollector = InferNoValueCollectorType; + +// @public (undocumented) +export interface NoValueCollectorBase { + // (undocumented) + category: 'NoValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type NoValueCollectors = + | NoValueCollectorBase<'NoValueCollector'> + | ReadOnlyCollector + | RichTextCollector + | QrCodeCollector + | ImageCollector; + +// @public +export type NoValueCollectorTypes = + | 'ReadOnlyCollector' + | 'RichTextCollector' + | 'NoValueCollector' + | 'QrCodeCollector' + | 'ImageCollector'; + +// @public +export interface OAuthDetails { + // (undocumented) + [key: string]: unknown; + // (undocumented) + code?: string; + // (undocumented) + state?: string; +} + +// @public (undocumented) +export interface ObjectOptionsCollectorWithObjectValue< + T extends ObjectValueCollectorTypes, + V = Record, + D = Record, +> { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: V; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: DeviceOptionWithDefault[]; + value?: D | null; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export interface ObjectOptionsCollectorWithStringValue< + T extends ObjectValueCollectorTypes, + V = string, +> { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: V; + type: string; + validation: ValidationRequired[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: DeviceOptionNoDefault[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type ObjectValueAutoCollector = AutoCollector< + 'ObjectValueAutoCollector', + 'ObjectValueAutoCollector', + Record +>; + +// @public (undocumented) +export type ObjectValueAutoCollectorTypes = + | 'ObjectValueAutoCollector' + | 'FidoRegistrationCollector' + | 'FidoAuthenticationCollector' + | 'MetadataCollector'; + +// @public (undocumented) +export type ObjectValueCollector = + | ObjectOptionsCollectorWithObjectValue + | ObjectOptionsCollectorWithStringValue + | ObjectValueCollectorWithObjectValue; + +// @public (undocumented) +export type ObjectValueCollectors = + | DeviceAuthenticationCollector + | DeviceRegistrationCollector + | PhoneNumberCollector + | PhoneNumberExtensionCollector + | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> + | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; + +// @public +export type ObjectValueCollectorTypes = + | 'DeviceAuthenticationCollector' + | 'DeviceRegistrationCollector' + | 'PhoneNumberCollector' + | 'PhoneNumberExtensionCollector' + | 'ObjectOptionsCollector' + | 'ObjectValueCollector' + | 'ObjectSelectCollector'; + +// @public (undocumented) +export interface ObjectValueCollectorWithObjectValue< + T extends ObjectValueCollectorTypes, + IV = Record, + OV = Record, +> { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: IV; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value?: OV | null; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export interface OutgoingQueryParams { + // (undocumented) + [key: string]: string | string[]; +} + +// @public (undocumented) +export interface PasswordCollector { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + verify: boolean; + }; + // (undocumented) + type: 'PasswordCollector'; +} + +// @public +export type PasswordField = { + type: 'PASSWORD' | 'PASSWORD_VERIFY'; + key: string; + label: string; + required?: boolean; + verify?: boolean; + passwordPolicy?: PasswordPolicy; +}; + +// @public +export interface PasswordPolicy { + // (undocumented) + createdAt?: string; + // (undocumented) + default?: boolean; + // (undocumented) + description?: string; + // (undocumented) + excludesCommonlyUsed?: boolean; + // (undocumented) + excludesProfileData?: boolean; + // (undocumented) + history?: { + count?: number; + retentionDays?: number; + }; + // (undocumented) + id?: string; + // (undocumented) + length?: { + min?: number; + max?: number; + }; + // (undocumented) + lockout?: { + failureCount?: number; + durationSeconds?: number; + }; + // (undocumented) + maxAgeDays?: number; + // (undocumented) + maxRepeatedCharacters?: number; + // (undocumented) + minAgeDays?: number; + // (undocumented) + minCharacters?: Record; + // (undocumented) + minUniqueCharacters?: number; + // (undocumented) + name?: string; + // (undocumented) + notSimilarToCurrent?: boolean; + // (undocumented) + populationCount?: number; + // (undocumented) + updatedAt?: string; +} + +// @public (undocumented) +export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue< + 'PhoneNumberCollector', + PhoneNumberInputValue, + PhoneNumberOutputValue +>; + +// @public (undocumented) +export interface PhoneNumberExtensionCollector { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: PhoneNumberExtensionInputValue; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + extensionLabel: string; + value: PhoneNumberExtensionOutputValue; + }; + // (undocumented) + type: 'PhoneNumberExtensionCollector'; +} + +// @public (undocumented) +export type PhoneNumberExtensionField = PhoneNumberField & { + showExtension: boolean; + extensionLabel: string; +}; + +// @public (undocumented) +export interface PhoneNumberExtensionInputValue { + // (undocumented) + countryCode: string; + // (undocumented) + extension: string; + // (undocumented) + phoneNumber: string; +} + +// @public (undocumented) +export interface PhoneNumberExtensionOutputValue { + // (undocumented) + countryCode?: string; + // (undocumented) + extension?: string; + // (undocumented) + phoneNumber?: string; +} + +// @public (undocumented) +export type PhoneNumberField = { + type: 'PHONE_NUMBER'; + key: string; + label: string; + required: boolean; + defaultCountryCode: string | null; + validatePhoneNumber: boolean; +}; + +// @public (undocumented) +export interface PhoneNumberInputValue { + // (undocumented) + countryCode: string; + // (undocumented) + phoneNumber: string; +} + +// @public (undocumented) +export interface PhoneNumberOutputValue { + // (undocumented) + countryCode?: string; + // (undocumented) + phoneNumber?: string; +} + +// @public +export type Poller = () => Promise; + +// @public (undocumented) +export type PollingCollector = AutoCollector< + 'SingleValueAutoCollector', + 'PollingCollector', + string, + PollingOutputValue +>; + +// @public (undocumented) +export type PollingField = { + type: 'POLLING'; + key: string; + pollInterval: number; + pollRetries: number; + pollChallengeStatus?: boolean; + challenge?: string; +}; + +// @public (undocumented) +export interface PollingOutputValue { + // (undocumented) + challenge?: string; + // (undocumented) + pollChallengeStatus?: boolean; + // (undocumented) + pollInterval: number; + // (undocumented) + pollRetries: number; + // (undocumented) + retriesRemaining?: number; +} + +// @public (undocumented) +export type PollingStatus = PollingStatusContinue | PollingStatusChallenge; + +// @public (undocumented) +export type PollingStatusChallenge = + | PollingStatusChallengeComplete + | 'expired' + | 'timedOut' + | 'error'; + +// @public (undocumented) +export type PollingStatusChallengeComplete = + | 'approved' + | 'denied' + | 'continue' + | CustomPollingStatus; + +// @public (undocumented) +export type PollingStatusContinue = 'continue' | 'timedOut'; + +// @public (undocumented) +export type ProtectCollector = AutoCollector< + 'SingleValueAutoCollector', + 'ProtectCollector', + string, + ProtectOutputValue +>; + +// @public (undocumented) +export type ProtectField = { + type: 'PROTECT'; + key: string; + behavioralDataCollection: boolean; + universalDeviceIdentification: boolean; +}; + +// @public +export interface ProtectOutputValue { + // (undocumented) + behavioralDataCollection: boolean; + // (undocumented) + universalDeviceIdentification: boolean; +} + +// @public +export interface QrCodeCollector extends NoValueCollectorBase<'QrCodeCollector'> { + // (undocumented) + output: NoValueCollectorBase<'QrCodeCollector'>['output'] & { + src: string; + }; +} + +// @public (undocumented) +export type QrCodeField = { + type: 'QR_CODE'; + key: string; + content: string; + fallbackText?: string; +}; + +// @public +export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollector'> { + // (undocumented) + output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { + content: string; + title?: string; + }; +} + +// @public (undocumented) +export type ReadOnlyField = { + type: 'LABEL'; + content: string; + richContent?: RichContent; + key?: string; +}; + +// @public (undocumented) +export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField | ImageField; + +// @public (undocumented) +export type RedirectField = { + type: 'SOCIAL_LOGIN_BUTTON'; + key: string; + label: string; + links: Links; +}; + +// @public (undocumented) +export type RedirectFields = RedirectField; + +export { RequestMiddleware }; + +// @public +export type RichContent = { + content: string; + replacements?: Record; +}; + +// @public +export interface RichContentLink { + // (undocumented) + href: string; + // (undocumented) + key: string; + // (undocumented) + target?: '_self' | '_blank'; + // (undocumented) + type: 'link'; + // (undocumented) + value: string; +} + +// @public +export type RichContentReplacement = { + type: 'link'; + value: string; + href: string; + target?: '_self' | '_blank'; +}; + +// @public +export interface RichTextCollector extends NoValueCollectorBase<'RichTextCollector'> { + // (undocumented) + output: NoValueCollectorBase<'RichTextCollector'>['output'] & { + content: string; + richContent: CollectorRichContent; + }; +} + +// @public (undocumented) +export interface SelectorOption { + // (undocumented) + label: string; + // (undocumented) + value: string; +} + +// @public (undocumented) +export type SingleCheckboxField = { + type: 'SINGLE_CHECKBOX'; + inputType: 'BOOLEAN'; + key: string; + label: string; + required?: boolean; + errorMessage?: string; + appearance: string; + richContent?: RichContent; +}; + +// @public (undocumented) +export type SingleSelectCollector = SingleSelectCollectorWithValue<'SingleSelectCollector'>; + +// @public (undocumented) +export interface SingleSelectCollectorNoValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export interface SingleSelectCollectorWithValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: string | number | boolean; + options: SelectorOption[]; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type SingleSelectField = { + inputType: 'SINGLE_SELECT'; + key: string; + label: string; + options: { + label: string; + value: string; + }[]; + required?: boolean; + type: 'RADIO' | 'DROPDOWN'; +}; + +// @public (undocumented) +export type SingleValueAutoCollector = AutoCollector< + 'SingleValueAutoCollector', + 'SingleValueAutoCollector', + string +>; + +// @public (undocumented) +export type SingleValueAutoCollectorTypes = + | 'SingleValueAutoCollector' + | 'ProtectCollector' + | 'PollingCollector'; + +// @public +export type SingleValueCollector = + | SingleValueCollectorWithValue + | SingleValueCollectorNoValue; + +// @public (undocumented) +export interface SingleValueCollectorNoValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type SingleValueCollectors = + | PasswordCollector + | ValidatedPasswordCollector + | SingleSelectCollector + | TextCollector + | ValidatedTextCollector + | BooleanCollector + | ValidatedBooleanCollector + | SingleValueCollectorWithValue<'SingleValueCollector'>; + +// @public +export type SingleValueCollectorTypes = + | 'PasswordCollector' + | 'ValidatedPasswordCollector' + | 'BooleanCollector' + | 'ValidatedBooleanCollector' + | 'SingleValueCollector' + | 'SingleSelectCollector' + | 'SingleSelectObjectCollector' + | 'TextCollector' + | 'ValidatedTextCollector'; + +// @public (undocumented) +export interface SingleValueCollectorWithValue { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: V; + type: string; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: V; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type SingleValueFields = + | StandardField + | PasswordField + | ValidatedField + | SingleCheckboxField + | SingleSelectField + | ProtectField; + +// @public (undocumented) +export type StandardField = { + type: 'TEXT' | 'SUBMIT_BUTTON' | 'FLOW_BUTTON' | 'FLOW_LINK' | 'BUTTON'; + key: string; + label: string; + required?: boolean; +}; + +// @public (undocumented) +export interface StartNode { + // (undocumented) + cache: null; + // (undocumented) + client: { + status: 'start'; + }; + // (undocumented) + error: DaVinciError | null; + // (undocumented) + server: { + status: 'start'; + }; + // (undocumented) + status: 'start'; +} + +// @public (undocumented) +export interface StartOptions { + // (undocumented) + query: Query; +} + +// @public (undocumented) +export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>; + +// @public (undocumented) +export interface SuccessNode { + // (undocumented) + cache: { + key: string; + }; + // (undocumented) + client: { + authorization?: { + code?: string; + state?: string; + }; + status: 'success'; + } | null; + // (undocumented) + error: null; + // (undocumented) + httpStatus: number; + // (undocumented) + server: { + _links?: Links; + eventName?: string; + id?: string; + interactionId?: string; + interactionToken?: string; + href?: string; + session?: string; + status: 'success'; + }; + // (undocumented) + status: 'success'; +} + +// @public (undocumented) +export type TextCollector = SingleValueCollectorWithValue<'TextCollector'>; + +// @public (undocumented) +export interface ThrownQueryError { + // (undocumented) + error: FetchBaseQueryError; + // (undocumented) + isHandledError: boolean; + // (undocumented) + meta: FetchBaseQueryMeta; +} + +// @public +export type UnknownCollector = { + category: 'UnknownCollector'; + error: string | null; + type: 'UnknownCollector'; + id: string; + name: string; + output: { + key: string; + label: string; + type: string; + }; +}; + +// @public (undocumented) +export type UnknownField = Record; + +// @public (undocumented) +export const updateCollectorValues: ActionCreatorWithPayload< + { + id: string; + value: CollectorValueTypes; + index?: number; + }, + string +>; + +// @public +export type Updater = ( + value: CollectorValueType, + index?: number, +) => InternalErrorResponse | null; + +// @public (undocumented) +export interface ValidatedBooleanCollector extends ValidatedSingleValueCollectorWithValue< + 'ValidatedBooleanCollector', + boolean +> { + // (undocumented) + output: ValidatedSingleValueCollectorWithValue<'ValidatedBooleanCollector', boolean>['output'] & { + appearance: string; + richContent?: CollectorRichContent; + }; +} + +// @public +export type ValidatedCollectors = + | ValidatedTextCollector + | ValidatedBooleanCollector + | ValidatedPasswordCollector + | ObjectValueCollectors + | MultiValueCollectors + | AutoCollectors; + +// @public (undocumented) +export type ValidatedField = { + type: 'TEXT'; + key: string; + label: string; + required: boolean; + validation: { + regex: string; + errorMessage: string; + }; +}; + +// @public (undocumented) +export interface ValidatedPasswordCollector { + // (undocumented) + category: 'SingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: string | number | boolean; + type: string; + validation: PasswordPolicy; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + verify: boolean; + }; + // (undocumented) + type: 'ValidatedPasswordCollector'; +} + +// @public (undocumented) +export interface ValidatedSingleValueCollectorWithValue< + T extends SingleValueCollectorTypes, + V = string, +> { + // (undocumented) + category: 'ValidatedSingleValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + type: string; + validation: (ValidationRequired | ValidationRegex)[]; + value: V; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + value: V; + }; + // (undocumented) + type: T; +} + +// @public (undocumented) +export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>; + +// @public (undocumented) +export interface ValidationPhoneNumber { + // (undocumented) + message: string; + // (undocumented) + rule: boolean; + // (undocumented) + type: 'validatePhoneNumber'; +} + +// @public (undocumented) +export interface ValidationRegex { + // (undocumented) + message: string; + // (undocumented) + rule: string; + // (undocumented) + type: 'regex'; +} + +// @public (undocumented) +export interface ValidationRequired { + // (undocumented) + message: string; + // (undocumented) + rule: boolean; + // (undocumented) + type: 'required'; +} + +// @public +export type Validator = ( + value: CollectorValueType, +) => + | string[] + | { + error: { + message: string; + type: string; + }; + type: string; + }; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/sdk-effects/store/src/index.ts b/packages/sdk-effects/store/src/index.ts index beb61b9208..bfa0457171 100644 --- a/packages/sdk-effects/store/src/index.ts +++ b/packages/sdk-effects/store/src/index.ts @@ -10,8 +10,7 @@ export type { WellknownState } from './lib/wellknown.api.js'; export { initWellknownQuery, isValidWellknownResponse } from './lib/wellknown.effects.js'; -export { clientExtra, createStoreExtra } from './lib/store.utils.js'; -export type { SdkStoreExtra } from './lib/store.utils.js'; +export { clientExtra } from './lib/store.utils.js'; export { createSdkStore, injectClient, isSdkStoreHandle } from './lib/store.effects.js'; export type { diff --git a/packages/sdk-effects/store/src/lib/store.utils.ts b/packages/sdk-effects/store/src/lib/store.utils.ts index f8708fa5d4..ad3f396323 100644 --- a/packages/sdk-effects/store/src/lib/store.utils.ts +++ b/packages/sdk-effects/store/src/lib/store.utils.ts @@ -5,22 +5,6 @@ * of the MIT license. See the LICENSE file for details. */ -/** - * Per-client slots carried on a store's thunk `extraArgument`. - * - * A Redux store has exactly one `extraArgument`, but a shared SDK store serves - * several clients. Keying the contents by the owning api's `reducerPath` gives - * each client a private slot, so one client's request middleware and logger can - * never be resolved by another client's endpoints. - * - * The slot type is left generic because each client's needs differ — journey - * only uses request middleware, oidc and davinci use both middleware and a - * logger. Callers narrow it at the point of use. - */ -export interface SdkStoreExtra { - readonly clients: Record; -} - /** * Resolves the calling client's own slot from a store's `extraArgument`. * @@ -49,16 +33,3 @@ export function clientExtra(extra: unknown, reducerPath: st return slot as Slot; } - -/** - * Builds an `extraArgument` registry holding a single client's slot. - * - * Used by each client factory when it creates its own store. When a store is - * shared, additional slots are added at injection time. - */ -export function createStoreExtra( - reducerPath: string, - slot: Slot, -): SdkStoreExtra { - return { clients: { [reducerPath]: slot } }; -} diff --git a/tasks/plans/2026-07-28-centralize-redux-stores-rework.md b/tasks/plans/2026-07-28-centralize-redux-stores-rework.md deleted file mode 100644 index e0f58ac8f9..0000000000 --- a/tasks/plans/2026-07-28-centralize-redux-stores-rework.md +++ /dev/null @@ -1,383 +0,0 @@ -# Plan: Rework `centralize-redux-stores` - -**Branch:** `centralize-redux-stores` -**Base:** `493f2643a` (`chore(copyright): add sync-header-years script`) -**Status:** Decisions locked — ready to implement -**Nothing on this branch is released yet**, so every public signature below is still free to change without a breaking changeset. - ---- - -## 1. Why we're reworking - -The branch has the right idea — one `wellknownApi`, one Redux store shared across SDK clients — but three things must change before it ships. - -### 1.1 OIDC token traffic is silently routed through DaVinci's request middleware - -Every `oidcApi` endpoint resolves its middleware and logger from the store's thunk `extraArgument`: - -```ts -// packages/oidc-client/src/lib/oidc.api.ts:45, 114, 190, 274, 317, 404, 459, 518, 568 -const { requestMiddleware, logger } = api.extra as Extras; -``` - -On the shared path that `extra` belongs to **davinci's** store (`davinci-client/src/lib/client.store.utils.ts:66-72`). So: - -- Middleware registered for DaVinci flows now executes against `AUTHORIZE`, `PAR`, `TOKEN_EXCHANGE`, `REVOKE`, `USER_INFO`, and `END_SESSION` requests. `ActionTypes` is a single flat union across all three products (`sdk-request-middleware/src/lib/request-mware.derived.ts`), so a middleware that doesn't defensively `switch (action.type)` mutates the authorization-code exchange. -- `oidc({ logger: { level: 'debug' } }, sharedStore)` is silently ignored — the store's `extra.logger` is davinci's, at davinci's level. This one isn't even warned about. - -The current mitigation (`oidc-client/src/lib/client.store.ts:82-87`) is a runtime `log.warn` that _instructs_ the user into the leak: - -> "Pass request middleware to the davinci() or journey() factory that owns the store." - -`api.extra` as an implicit, store-wide DI channel is the root cause. The shared-store feature exposed it. - -### 1.2 The cross-package contract is enforced by nothing - -`InjectableStore` is declared three times — twice identically, once as a structurally weaker hand-mirror: - -```ts -// davinci-client/src/lib/client.store.utils.ts:150 -// journey-client/src/lib/client.store.utils.ts:442 -export interface InjectableStore { - readonly store: ReturnType>; - readonly rootReducer: typeof rootReducer; - readonly dynamicMiddleware: ReturnType; -} - -// oidc-client/src/lib/client.store.utils.ts:21 ← weaker copy, no compile-time link -interface InjectableStore { - readonly store: ReturnType; - readonly rootReducer: { inject: (api: unknown) => void }; - readonly dynamicMiddleware: { addMiddleware: (...mw: unknown[]) => void }; -} -``` - -Producer and consumer are joined only by `as unknown as`. Rename `dynamicMiddleware` in davinci and TypeScript says nothing; `oidc()` throws `Cannot read properties of undefined` from inside a factory. - -Stacked on top: `SdkStore`'s brand is fictional (`__sdkStoreBrand: symbol` exists on no runtime object), and `toSdkStore` returns `object` — a no-op cast that forces a _second_ `as SdkStore` at each call site. - -```ts -// davinci-client/src/lib/client.store.utils.ts:135 -export function toSdkStore(injectable: InjectableStore): object { - return injectable as unknown as object; // InjectableStore is already assignable to object -} -// client.store.ts:120 -store: toSdkStore(injectable) as SdkStore, // second cast -``` - -Four unchecked casts to move one object across a package boundary, in a repo whose architecture is otherwise compiler-enforced. - -### 1.3 Test suite is green but proves little - -`shared-store.test.ts` hand-builds a fake handle from RTK primitives (`makeSharedStore`, line 776) specifically to avoid importing davinci/journey. **Every test passes even if `davinci()`'s handle has a completely different shape** — the one thing the branch needs to prove is the one thing untested. Details in §5. - ---- - -## 2. Decisions — all resolved - -| # | Decision | Resolution | -| ------ | ------------------------------------ | -------------------------------------------------------------------------------------------- | -| **D1** | Store ownership model | All three modes supported; `createSdkStore()` folded into a renamed `@forgerock/sdk-store` | -| **D2** | `requestMiddleware`/`logger` scoping | **Per-client registry** in `extraArgument`, keyed by `reducerPath` | -| **D3** | Public API shape | Options-object, key `store`. No mutual exclusion — `requestMiddleware` is honored per client | -| **D4** | Two `oidc()` clients on one store | Detect and **error** on a second injection with a different `clientId` | -| **D5** | Delivery | Rework in place on `centralize-redux-stores` | - -### D1 — Store ownership: three modes, none mandatory - -The zero-config path stays zero-config. - -| Mode | Call shape | Who owns the store | -| -------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| **1 — Implicit** (existing, unchanged) | `oidc({ config })` | The client. Each factory builds its own store when none is passed. Default path; must behave exactly as it does today. | -| **2 — Client-owned, shared** | `const dv = await davinci({ config });`
`await oidc({ config, store: dv.store })` | `davinci()`/`journey()`. They continue to expose a `store` handle on the returned client. **Primary sharing story.** | -| **3 — Consumer-owned** (opt-in) | `const store = createSdkStore();`
`await davinci({ config, store });`
`await oidc({ config, store })` | The application. Offered for consumers who want lifecycle control; never required. | - -- `davinci()`/`journey()` **keep** the public `store: SdkStore` field added on this branch. The api-report additions stay. -- `createSdkStore()` is an _additional_ export, not a new required step. Escape hatch, not headline. -- All three modes converge on the same `SdkStoreHandle` contract (§3), so there's one code path to test, not three. -- Mode 1 must be provably unaffected — it's the path every existing consumer is on. Regression coverage in §5.3. - -**Package location:** rename `packages/sdk-effects/wellknown` → `packages/sdk-effects/store`, published as `@forgerock/sdk-store`, exporting both `wellknownApi` and `createSdkStore()`. This avoids publishing two brand-new artifacts in one release. Since `@forgerock/sdk-wellknown` has never been published, the rename costs nothing. - -### D2 — Per-client `extra` registry - -`extraArgument` becomes a keyed registry; each `*.api.ts` reads **only its own slot**. - -```ts -// @forgerock/sdk-store -export interface ClientExtra
{ - readonly requestMiddleware?: RequestMiddleware[]; - readonly logger: ReturnType; -} - -export interface SdkStoreExtra { - /** Keyed by the owning api's `reducerPath`. Mutable slots, stable object identity. */ - readonly clients: Record; -} - -/** Pure resolver used by every *.api.ts. */ -export function clientExtra(extra: unknown, reducerPath: string): ClientExtra; -``` - -Each api file replaces `api.extra as Extras` with `clientExtra(api.extra, oidcApi.reducerPath)` — ~9 sites in `oidc.api.ts`, ~10 in `davinci.api.ts`, 3 in `journey.api.ts`. Mechanical, and it deletes three duplicated private `Extras` interfaces. - -`extraArgument` is a stable object reference held by `configureStore`, so `injectClient()` filling in a new slot at injection time is visible to every subsequent request. That's what makes per-client scoping work on a store created before the second client existed. - -**Consequences — this is why D3 has no mutual exclusion:** - -- `oidc({ config, store, requestMiddleware })` now works _correctly_. oidc's middleware lands in the `oidc` slot and applies only to OIDC endpoints. -- The `log.warn` at `client.store.ts:82-87` is **deleted**, not hardened. The condition it warns about is no longer a problem. -- Its two tests (`'warns when requestMiddleware is passed alongside sharedStore'`, `'does not warn when...'`) are **deleted and replaced** by the inverse assertions: oidc's middleware _is_ invoked for OIDC endpoints, and is _not_ invoked for DaVinci endpoints. -- The `logger` drop is fixed for free. - -### D3 — Options-object, key `store` - -```ts -export async function oidc(input: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; // honored in all three modes, per D2 - logger?: { level: LogLevel; custom?: CustomLogger }; - storage?: Partial; - store?: SdkStore; // omit → mode 1 -}): Promise<…>; -``` - -Consistent with every other factory in the SDK, self-documenting at the call site, and extensible. Replaces the positional second parameter currently in `api-report/oidc-client.api.md`. `davinci()` and `journey()` gain the same optional `store` input. - -### D4 — Error on conflicting re-injection - -`oidcApi.reducerPath` is the fixed string `'oidc'`. A second `oidc()` against the same store with a _different_ `clientId` would silently share one cache slice and clobber token state. Detect it and return a typed SDK error; document as unsupported. - -Re-injecting with the **same** `clientId` stays idempotent — that's a legitimate re-init. - -Namespacing per `clientId` (which would genuinely support multi-tenant and step-up auth) is explicitly out of scope. Record it as a known limitation in the READMEs. - -### D5 — Rework in place - -Nothing is released, so history on `centralize-redux-stores` can be rewritten freely. Single PR, single review. Phase 2 is large but behavior-preserving; the full e2e run is the safety net. - ---- - -## 3. Target architecture - -``` -sdk-types/ - store.types.ts SdkStore, SdkStoreHandle, ClientExtra, SdkStoreExtra ← declared ONCE - -sdk-effects/store/ ← renamed from sdk-effects/wellknown, published @forgerock/sdk-store - store.effects.ts createSdkStore(), injectClient() ← effectful, correctly named - store.utils.ts clientExtra(), isSdkStoreHandle() ← pure - wellknown.api.ts wellknownApi + selectors (as extracted on this branch) - -davinci-client/ journey-client/ oidc-client/ - client.store.ts factory accepts optional `store` - client.store.effects.ts injection lives here, not in *.utils.ts - *.api.ts clientExtra(api.extra, thisApi.reducerPath) -``` - -**One contract, declared once:** - -```ts -// sdk-types/src/lib/store.types.ts -export interface SdkStoreHandle { - readonly store: Store & { dispatch: ThunkDispatch }; - readonly rootReducer: { inject: (slice: InjectableSlice) => void }; - readonly dynamicMiddleware: { addMiddleware: (...mw: Middleware[]) => void }; - readonly extra: SdkStoreExtra; -} - -/** Public-facing alias. Structural, so no cast is needed to produce or consume it. */ -export type SdkStore = SdkStoreHandle; -``` - -**Deleted by this change:** - -- `toSdkStore` × 2, `fromSdkStore` × 3 (two of which are **already dead exports** in publishable packages) -- The three `InjectableStore` declarations -- The three private `Extras` interfaces -- The fictional `__sdkStoreBrand` -- The `requestMiddleware`-ignored `log.warn` and its two tests (obsolete under D2) -- All 4 `as unknown as` casts on the handle path, plus `store as unknown as ReturnType` (`oidc-client/src/lib/client.store.utils.ts:45`) -- Dead exports `JourneyStore`, `fromSdkStore` × 2 - ---- - -## 4. Implementation phases - -Strict RED → GREEN → REFACTOR → GREEN. Each phase ends green and committable. - -### Phase 0 — Reset - -- [ ] `git switch -c centralize-redux-stores-v2` from `493f2643a`; cherry-pick what's worth keeping (the wellknown extraction, tsconfig/nx wiring, api-report regeneration). -- [ ] Baseline green: `pnpm exec nx affected -t build lint test`. - -### Phase 1 — `@forgerock/sdk-store` (rename + harden) - -The extraction was correct; it shipped untested and mis-versioned. - -- [ ] Rename `packages/sdk-effects/wellknown` → `packages/sdk-effects/store`; package name → `@forgerock/sdk-store`; update the 3 consumer `package.json`s, all tsconfig references, root `tsconfig.json`, and the changeset. -- [ ] **RED** — `wellknown.api.test.ts`: - - one cache entry per distinct URL; **exactly one** network call for two identical requests - - a second distinct URL produces a second entry, not a cache hit - - `queryFn` maps a non-2xx into `FetchBaseQueryError`, not a thrown rejection - - `createWellknownSelector` returns `undefined` before fetch, data after - - `wellknownSelector` returns the same reference across calls once cached -- [ ] **GREEN** — fix `wellknownSelector` memoization (below). -- [ ] Packaging: `version` → `0.0.0`; add `README.md`; add the license header to `eslint.config.mjs`; fix `vite.config.ts` `cacheDir`; drop `passWithNoTests`. - -`wellknownSelector` currently rebuilds its memoized selector on every call, so the cache is always cold — across 8 call sites in `oidc-client/src/lib/client.store.ts`: - -```ts -// before — memoization never hits -export function wellknownSelector(url: string, state: S) { - return createWellknownSelector(url)(state); -} - -// after — one selector per URL -const selectorCache = new Map>(); -export function wellknownSelector(url: string, state: S) { - let selector = selectorCache.get(url); - if (!selector) { - selector = createWellknownSelector(url); - selectorCache.set(url, selector); - } - return selector(state); -} -``` - -**Proceeding with the `Map`** unless flagged. It's module state in a package marked `sideEffects: false`, but it's a pure cache — same input always yields the same selector, and it's unobservable from outside. - -### Phase 2 — Per-client `extra` (D2) - -No behavior change while standalone. Do this **before** any sharing so the sharing phase inherits correct scoping. - -- [ ] **RED** — `clientExtra()` unit tests: returns the slot for a known `reducerPath`; returns an empty-middleware default for an unknown one; never returns another client's slot. -- [ ] **RED** — **the §1.1 regression test.** Per package: a middleware registered for client X is not invoked for client Y's endpoints. Must fail before the fix. -- [ ] **RED** — logger isolation: `oidc({ logger: { level: 'debug' }, store })` uses oidc's level, not the owner's. -- [ ] **GREEN** — add `SdkStoreExtra`/`ClientExtra` to `sdk-types` and `clientExtra()` to `sdk-store`; migrate all three `configureStore` calls to `extraArgument: { clients: { [thisApi.reducerPath]: { requestMiddleware, logger } } }`; replace all ~22 `api.extra as Extras` sites; delete the three private `Extras` interfaces. -- [ ] **REFACTOR** — delete the `requestMiddleware` `log.warn` and its two tests; replace with the inverse assertions. -- [ ] State-shape assertion per package: `expect(Object.keys(store.getState()).sort()).toEqual([...])`. - -### Phase 3 — The shared store contract (D1, D3, §1.2) - -- [ ] **RED** — a **real** cross-package integration test covering all three D1 modes (§5.1). Must fail before implementation. -- [ ] **RED** — mode-1 regression: `oidc({ config })` with no `store` builds its own store and behaves byte-identically to `main`. -- [ ] **RED** — `isSdkStoreHandle()` guard: `{}` / `null` / a plain object passed as `store` yields a typed SDK error, not a `TypeError`. -- [ ] **GREEN** — declare `SdkStoreHandle` once in `sdk-types`; add `createSdkStore()` + `injectClient()` to `sdk-store`; all three factories accept optional `store` and produce/consume the handle **structurally, with zero casts**. -- [ ] **GREEN** — D3: move `sharedStore` from positional arg into the options object as `store`. `davinci()`/`journey()` gain the same input and keep exposing `store: SdkStore`. -- [ ] **REFACTOR** — delete `toSdkStore`/`fromSdkStore`/`InjectableStore` × 3/`__sdkStoreBrand`; move injection out of `*.utils.ts` into `client.store.effects.ts` per `AGENTS.md` ("never put effectful logic in `*.utils.ts`"). -- [ ] Type `createDynamicMiddleware()` so `addMiddleware` is actually checked — the untyped call is how the weak `(...mw: unknown[]) => void` mirror slipped through. - -### Phase 4 — Lifecycle correctness (D4) - -- [ ] **RED** — `oidc({ config: /* no wellknown */, store })` returns an error **and leaves the store unmutated**. Today injection happens at `client.store.ts:88`, before the guards at `:91` and `:98`, and RTK's `inject` is irreversible — a failed call permanently contaminates the caller's store. -- [ ] **GREEN** — validate all arguments, then inject. -- [ ] **RED/GREEN** — D4: second `oidc()` on the same store with a _different_ `clientId` returns a typed error; with the _same_ `clientId` stays idempotent. - -### Phase 5 — Docs, packaging, e2e - -- [ ] READMEs: `oidc-client`, `davinci-client`, `journey-client` — all three D1 modes with a worked example each; an explicit statement that each client owns its own middleware/logger (D2); the D4 single-`clientId`-per-store limitation. Plus the new `sdk-store` README from Phase 1. -- [ ] Rewrite the changeset: new package + three changed public signatures + per-client middleware scoping. Name the middleware/logger semantics explicitly. -- [ ] Regenerate api-reports. Expected additions: `store: SdkStore` on the davinci/journey clients (kept — mode 2), the new `store?` input on all three factories, `createSdkStore`/`clientExtra` on `sdk-store`. Nothing should be _removed_. -- [ ] E2E: one suite where a single app shares a store across davinci + oidc and asserts a single `.well-known` request over the wire. (`CLAUDE.md`: e2e is part of done unless untestable end-to-end.) -- [ ] Remove dead exports: `fromSdkStore` × 2, `JourneyStore`. -- [ ] Full CI parity: `pnpm exec nx affected -t build lint test e2e-ci`; `pnpm nx sync:check`. - ---- - -## 5. Test plan - -### 5.1 Replace the fake handle with a real one - -`makeSharedStore()` (`shared-store.test.ts:776`) constructs a lookalike from RTK primitives. It cannot detect contract drift, which is the single failure mode this feature has. Every D1 mode needs a real test: - -```ts -// Mode 1 — implicit (regression: the path all existing consumers are on) -await oidc({ config: oidcConfig }); -expect(wellknownFetchCount).toBe(1); - -// Mode 2 — client-owned, shared (primary sharing story) -const dv = await davinci({ config: davinciConfig }); -await oidc({ config: oidcConfig, store: dv.store }); -expect(wellknownFetchCount).toBe(1); // davinci fetched it; oidc hits cache -expect(Object.keys(dvState())).toContain('oidc'); - -// Mode 3 — consumer-owned (opt-in) -const store = createSdkStore(); -await davinci({ config: davinciConfig, store }); -await oidc({ config: oidcConfig, store }); -expect(wellknownFetchCount).toBe(1); -``` - -Mode 2 catches contract drift between `davinci()`'s real handle and what `oidc()` consumes — the gap that makes the current suite meaningless. Mode 1 catches regressions for shipped consumers. - -**Placement:** an e2e suite, so no `scope:package` → `scope:package` boundary is crossed and the assertion is made against real network traffic. If a faster unit-level version is wanted too, add `packages/integration-tests` rather than relaxing the lint rule for `*.test.ts`. - -### 5.2 Fix the tests that assert nothing - -| Test | Problem | Fix | -| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -| `'subscribe() fires when oidcApi state changes'` (:936) | Dispatches `{ type: 'test/action' }` — not an `oidcApi` action. Redux notifies **every** subscriber on **every** dispatch, so this passes unconditionally and the title is false. | Dispatch a real `oidcApi` action; assert on the resulting slice state. | -| `'reuses cached wellknown response'` (:952) | `expect(count).toBe(fetchesAfterOwnerInit)` is `0 === 0` if the owner's fetch silently failed. | `expect(count).toBe(1)`. | -| `'fetches wellknown during init'` (:900) | `toBeGreaterThan(0)` — but "exactly one" is the property that matters for a dedupe feature. | `toBe(1)`. | -| `'is idempotent'` (:857) | Injects three times (twice in the `not.toThrow` block, once more after). | Assert 2 injections explicitly; assert cache contents unchanged, not just "didn't throw". | -| Arrange blocks (:946, :956) | Call the internal `injectIntoStore` to obtain a typed store, coupling tests to internals. | Go through the public factory. | -| `'warns when requestMiddleware is passed alongside sharedStore'` + its negative twin | Tests a warning that D2 makes obsolete. | **Delete both.** Replace with: oidc's middleware _is_ invoked for OIDC endpoints on the shared path, and is _not_ invoked for DaVinci endpoints. | - -### 5.3 New coverage - -- `@forgerock/sdk-store`: currently zero tests behind `passWithNoTests: true` (Phase 1). -- **Middleware isolation** — DaVinci middleware not invoked for `TOKEN_EXCHANGE` (Phase 2). This is the §1.1 regression test. -- **Logger isolation** — `oidc({ logger: { level: 'debug' }, store })` uses oidc's level. -- **Middleware honored on the shared path** — the inverse of the deleted warn tests. -- Failure path leaves the shared store clean (Phase 4). -- D4: conflicting `clientId` errors; matching `clientId` is idempotent. -- Invalid handle → typed SDK error, not `TypeError`. -- **Mode-1 regression suite** — every existing `oidc()`/`davinci()`/`journey()` test must pass untouched. If a test needs editing to accommodate the new `store` option, that's a signal we broke the default path. -- State-shape assertions per package, guarding the implicit `combineSlices` keying (verified correct today: `config`, `node`, `davinci`, `journeyReducer`, `wellknown` — but it's now an implicit invariant off `slice.name`, where it used to be an explicit literal map). - ---- - -## 6. Risk register - -| Risk | Mitigation | -| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| ~22-site `api.extra` migration touches every network call in the SDK | Behavior-preserving by construction; RED tests first; full e2e run; Phase 2 lands as its own commit for reviewability | -| `combineSlices` reshapes published state if a `slice.name` is ever renamed | State-shape assertion test per package (Phase 2) | -| RTK `inject` is irreversible — no teardown story | Phase 4 validate-before-inject; D4 errors on conflicting re-injection; documented limitation | -| Package rename ripples through 3 `package.json`s + all tsconfig refs | Never published, so no consumer impact; `pnpm nx sync:check` catches missed references | -| Mode 1 regression for existing consumers | Dedicated regression suite; "no existing test needed editing" is a DoD gate | - ---- - -## 7. What was already right on this branch - -Worth preserving through the rework: - -- Extracting `wellknownApi` into one canonical instance — the "single cache entry per URL" rationale in the file header is exactly the correct framing. -- `combineSlices().withLazyLoadedSlices()` + `createDynamicMiddleware()` is the idiomatic RTK 2.x mechanism. Not a homegrown `replaceReducer` hack. -- Generalizing `wellknownSelector` from a concrete `RootState` to `` — clean decoupling, keep it. -- Collapsing `ReturnType['getState']>` into named `DavinciStore` / `RootState` aliases — genuinely more readable at 4 call sites. -- `sdk-types/store.types.ts` is correctly type-only per the `*.types.ts` convention. -- TS project references, root `tsconfig.json` refs, and Nx tags are wired correctly and consistently with siblings. -- The `requestMiddleware` warning shows the coupling _was_ noticed — the fix just needs to go a layer deeper. - ---- - -## 8. Definition of done - -- [ ] Zero `as unknown as` on the store-handle path -- [ ] `SdkStoreHandle` declared exactly once -- [ ] No client's `requestMiddleware` or `logger` reaches another client's endpoints, with a test proving it -- [ ] Each client's own `requestMiddleware` and `logger` work in all three modes -- [ ] All three D1 modes tested: implicit, client-owned-shared, consumer-owned -- [ ] Mode 1 byte-compatible with `main` — no existing test needed editing to keep passing -- [ ] A test that fails if `davinci()`'s real handle drifts from what `oidc()` consumes -- [ ] Injection is effectful code in `*.effects.ts`, not `*.utils.ts` -- [ ] Failed factory calls leave a shared store unmutated -- [ ] Conflicting `clientId` on one store errors; matching `clientId` is idempotent -- [ ] `@forgerock/sdk-store` has tests, a README, and a correct first version (`0.0.0`) -- [ ] Three package READMEs document all three modes, the middleware/logger semantics, and the D4 limitation -- [ ] E2E proves one `.well-known` request across two clients sharing a store -- [ ] `pnpm exec nx affected -t build lint test e2e-ci` and `pnpm nx sync:check` both green From 5185826b930efedec01439a89f6986dab8585fae Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 29 Jul 2026 20:05:24 -0600 Subject: [PATCH 6/8] refactor(sdk): centralise invalid-store message and clientExtra defaults - Export INVALID_STORE_MESSAGE from sdk-store and consume it in davinci, journey, and oidc client factories (removes 4x string duplication) - Add optional `defaults` param to clientExtra; simplify *Extra wrapper functions in davinci.api, journey.api, and oidc.api - Expose `store` in oidc-client return for API symmetry with davinci/journey - Restore 2025 - 2026 copyright in davinci client.types.ts --- .../davinci-client/src/lib/client.store.ts | 9 ++--- .../davinci-client/src/lib/davinci.api.ts | 9 ++--- .../journey-client/src/lib/client.store.ts | 9 ++--- .../journey-client/src/lib/journey.api.ts | 9 ++--- packages/oidc-client/src/lib/client.store.ts | 16 +++++--- packages/oidc-client/src/lib/oidc.api.ts | 9 ++--- packages/sdk-effects/store/src/index.ts | 7 +++- .../store/src/lib/store.effects.ts | 16 ++++++-- .../sdk-effects/store/src/lib/store.utils.ts | 39 +++++++++++++++---- 9 files changed, 78 insertions(+), 45 deletions(-) diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index 1832bbfbf2..981c7c99d1 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -23,7 +23,7 @@ import { pollingµ, getPollingModeµ } from './client.store.effects.js'; import { nodeSlice } from './node.slice.js'; import { davinciApi } from './davinci.api.js'; import { configSlice } from './config.slice.js'; -import { wellknownApi, isSdkStoreHandle } from '@forgerock/sdk-store'; +import { wellknownApi, isSdkStoreHandle, INVALID_STORE_MESSAGE } from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { SdkStore } from '@forgerock/sdk-store'; @@ -90,11 +90,8 @@ export async function davinci({ }); if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { - const message = - 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + - 'another SDK client, or one created with `createSdkStore()`.'; - log.error(message); - throw new Error(message); + log.error(INVALID_STORE_MESSAGE); + throw new Error(INVALID_STORE_MESSAGE); } if (!config.serverConfig.wellknown) { diff --git a/packages/davinci-client/src/lib/davinci.api.ts b/packages/davinci-client/src/lib/davinci.api.ts index 30771b0d37..9c5f144d23 100644 --- a/packages/davinci-client/src/lib/davinci.api.ts +++ b/packages/davinci-client/src/lib/davinci.api.ts @@ -72,11 +72,10 @@ const fallbackLogger = loggerFn({ level: 'error' }); * store would belong to whichever client created it. */ function davinciExtra(extra: unknown): Required { - const slot = clientExtra(extra, DAVINCI_REDUCER_PATH); - return { - requestMiddleware: slot.requestMiddleware ?? [], - logger: slot.logger ?? fallbackLogger, - }; + return clientExtra(extra, DAVINCI_REDUCER_PATH, { + requestMiddleware: [], + logger: fallbackLogger, + }); } /** diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index df42229842..f1012301ea 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -24,7 +24,7 @@ import { createStorage } from '@forgerock/storage'; import * as Either from 'effect/Either'; import { createJourneyObject, parseJourneyResponse } from './journey.utils.js'; import type { JourneyResult } from './journey.utils.js'; -import { wellknownApi, isSdkStoreHandle } from '@forgerock/sdk-store'; +import { wellknownApi, isSdkStoreHandle, INVALID_STORE_MESSAGE } from '@forgerock/sdk-store'; import type { JourneyStep } from './step.utils.js'; import type { JourneyClientConfig } from './config.types.js'; @@ -122,11 +122,8 @@ export async function journey({ } if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { - const message = - 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + - 'another SDK client, or one created with `createSdkStore()`.'; - log.error(message); - throw new Error(message); + log.error(INVALID_STORE_MESSAGE); + throw new Error(INVALID_STORE_MESSAGE); } const { wellknown } = config.serverConfig; diff --git a/packages/journey-client/src/lib/journey.api.ts b/packages/journey-client/src/lib/journey.api.ts index 74a3f1f5b9..e503df9940 100644 --- a/packages/journey-client/src/lib/journey.api.ts +++ b/packages/journey-client/src/lib/journey.api.ts @@ -108,11 +108,10 @@ const fallbackLogger = loggerFn({ level: 'error' }); * shared store would belong to whichever client created it. */ function journeyExtra(extra: unknown): Required { - const slot = clientExtra(extra, JOURNEY_REDUCER_PATH); - return { - requestMiddleware: slot.requestMiddleware ?? [], - logger: slot.logger ?? fallbackLogger, - }; + return clientExtra(extra, JOURNEY_REDUCER_PATH, { + requestMiddleware: [], + logger: fallbackLogger, + }); } export const journeyApi = createApi({ diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index 52ae0480e9..1629e4b1fd 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -18,7 +18,12 @@ import { isExpiryWithinThreshold } from './token.utils.js'; import { logoutµ } from './logout.request.js'; import { oidcApi } from './oidc.api.js'; import { sessionCheckNoneµ, sessionCheckIdTokenµ } from './session.micros.js'; -import { isSdkStoreHandle, wellknownApi, wellknownSelector } from '@forgerock/sdk-store'; +import { + isSdkStoreHandle, + INVALID_STORE_MESSAGE, + wellknownApi, + wellknownSelector, +} from '@forgerock/sdk-store'; import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; @@ -84,9 +89,7 @@ export async function oidc({ */ if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { return { - error: - 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + - 'another SDK client, or one created with `createSdkStore()`.', + error: INVALID_STORE_MESSAGE, type: 'argument_error', }; } @@ -124,12 +127,13 @@ export async function oidc({ prefix: storage?.prefix || 'pic', ...storage, } as StorageConfig); - const { store } = createClientStore({ + const handle = createClientStore({ requestMiddleware, logger: log, store: sharedStore, clientId: config.clientId, }); + const { store } = handle; const wellknownUrl = config.serverConfig.wellknown; const { data, error } = await store.dispatch( @@ -155,6 +159,8 @@ export async function oidc({ const useParFlow = config.par ?? data?.require_pushed_authorization_requests === true; return { + /** Pass to another SDK client's `store` option to share this store. */ + store: handle as SdkStore, // Pass store methods to the client subscribe: store.subscribe, diff --git a/packages/oidc-client/src/lib/oidc.api.ts b/packages/oidc-client/src/lib/oidc.api.ts index bfbd4221d2..36eb5dc8a2 100644 --- a/packages/oidc-client/src/lib/oidc.api.ts +++ b/packages/oidc-client/src/lib/oidc.api.ts @@ -56,11 +56,10 @@ const fallbackLogger = loggerFn({ level: 'error' }); * store would belong to whichever client created it. */ function oidcExtra(extra: unknown): Required { - const slot = clientExtra(extra, OIDC_REDUCER_PATH); - return { - requestMiddleware: slot.requestMiddleware ?? [], - logger: slot.logger ?? fallbackLogger, - }; + return clientExtra(extra, OIDC_REDUCER_PATH, { + requestMiddleware: [], + logger: fallbackLogger, + }); } export const oidcApi = createApi({ diff --git a/packages/sdk-effects/store/src/index.ts b/packages/sdk-effects/store/src/index.ts index bfa0457171..c300461013 100644 --- a/packages/sdk-effects/store/src/index.ts +++ b/packages/sdk-effects/store/src/index.ts @@ -12,7 +12,12 @@ export { initWellknownQuery, isValidWellknownResponse } from './lib/wellknown.ef export { clientExtra } from './lib/store.utils.js'; -export { createSdkStore, injectClient, isSdkStoreHandle } from './lib/store.effects.js'; +export { + createSdkStore, + injectClient, + isSdkStoreHandle, + INVALID_STORE_MESSAGE, +} from './lib/store.effects.js'; export type { ClientSlot, InjectClientOptions, diff --git a/packages/sdk-effects/store/src/lib/store.effects.ts b/packages/sdk-effects/store/src/lib/store.effects.ts index 195b952a48..78cd304e83 100644 --- a/packages/sdk-effects/store/src/lib/store.effects.ts +++ b/packages/sdk-effects/store/src/lib/store.effects.ts @@ -58,6 +58,17 @@ export function createSdkStore(): SdkStore { } as unknown as SdkStore; } +/** + * Human-readable explanation used whenever an argument fails the + * {@link isSdkStoreHandle} check. + * + * Exported so client packages can reference the same string rather than + * duplicating it. + */ +export const INVALID_STORE_MESSAGE = + 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + + 'another SDK client, or one created with `createSdkStore()`.'; + /** * Narrows an unknown value to a usable store handle. * @@ -102,10 +113,7 @@ export function injectClient>( options: InjectClientOptions, ): SdkStoreHandle { if (!isSdkStoreHandle(handle)) { - throw new Error( - 'The provided `store` is not a valid SDK store. Pass the `store` returned by ' + - 'another SDK client, or one created with `createSdkStore()`.', - ); + throw new Error(INVALID_STORE_MESSAGE); } const { api, reducerPath, slices = [], requestMiddleware, logger, clientId } = options; diff --git a/packages/sdk-effects/store/src/lib/store.utils.ts b/packages/sdk-effects/store/src/lib/store.utils.ts index ad3f396323..1cced6851d 100644 --- a/packages/sdk-effects/store/src/lib/store.utils.ts +++ b/packages/sdk-effects/store/src/lib/store.utils.ts @@ -9,27 +9,50 @@ * Resolves the calling client's own slot from a store's `extraArgument`. * * This runs on every request, so it never throws. An unrecognised or malformed - * `extra` yields an empty slot rather than an error — and, critically, never - * falls back to a store-wide value that would belong to a different client. + * `extra` yields the provided `defaults` (or an empty object) rather than an + * error — and, critically, never falls back to a store-wide value that would + * belong to a different client. + * + * When `defaults` are supplied, any slot key that is absent or `undefined` is + * filled in from `defaults`, letting callers express their fallback values once + * at the call site instead of with repeated `?? x` expressions. * * @param extra - The thunk `extraArgument`, as received from `api.extra` * @param reducerPath - The calling api's `reducerPath`, used as the slot key - * @returns The client's own slot, or an empty object if none is registered + * @param defaults - Optional fallback values for missing or undefined slot fields + * @returns The client's own slot merged with defaults, or just defaults / {} if no slot is registered */ -export function clientExtra(extra: unknown, reducerPath: string): Slot { +export function clientExtra( + extra: unknown, + reducerPath: string, + defaults?: Partial, +): Slot { + const fallback = (defaults ?? {}) as Slot; + if (typeof extra !== 'object' || extra === null || !('clients' in extra)) { - return {} as Slot; + return fallback; } const { clients } = extra as { clients: unknown }; if (typeof clients !== 'object' || clients === null) { - return {} as Slot; + return fallback; } const slot = (clients as Record)[reducerPath]; if (typeof slot !== 'object' || slot === null) { - return {} as Slot; + return fallback; } - return slot as Slot; + if (defaults === undefined) { + return slot as Slot; + } + + // Merge: defaults fill in any key that is absent or undefined in the slot. + const merged = { ...slot } as Record; + for (const [key, value] of Object.entries(defaults as Record)) { + if (merged[key] === undefined) { + merged[key] = value; + } + } + return merged as Slot; } From 006ce321e36add52e89fba320ed0cc0be9d4e039 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 29 Jul 2026 20:26:35 -0600 Subject: [PATCH 7/8] docs: fix README inaccuracies in davinci, oidc, journey, and sdk-store --- packages/davinci-client/README.md | 41 +++----- packages/journey-client/README.md | 3 +- packages/oidc-client/README.md | 4 +- packages/oidc-client/src/types.ts | 3 + packages/sdk-effects/store/README.md | 140 +++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 30 deletions(-) diff --git a/packages/davinci-client/README.md b/packages/davinci-client/README.md index 92240d3e01..a12ab1e61e 100644 --- a/packages/davinci-client/README.md +++ b/packages/davinci-client/README.md @@ -25,7 +25,7 @@ Configure DaVinci Client with the following minimum, required properties: ```ts // Demo with example values -import { davinci } from '@forgerock/davinci'; +import { davinci } from '@forgerock/davinci-client'; const davinciClient = await davinci({ config: { @@ -42,7 +42,7 @@ If you have a need for more than one client, say you need to use two or more dif ```ts // Demo with example values -import { davinci } from '@forgerock/davinci'; +import { davinci } from '@forgerock/davinci-client'; const firstDavinciClient = await davinci(/** config 1 **/); const secondDavinciClient = await davinci(/** config 2 **/); @@ -226,10 +226,10 @@ Upon each collector in the array, some will need an `updater`, like the collecto ```ts // Example SingleValueCollector using the TextCollector -const collectors = davinci.collectors(); +const collectors = davinciClient.getCollectors(); collectors.map((collector) => { if (collector.type === 'TextCollector') { - renderTextCollector(collector, davinci.update(collector)); + renderTextCollector(collector, davinciClient.update(collector)); } }); ``` @@ -258,7 +258,7 @@ The `SubmitCollector` is associated with the submission of the current node and ```ts // Example SubmitCollector mapping -const collectors = davinci.collectors(); +const collectors = davinciClient.getCollectors(); collectors.map((collector) => { if (collector.type === 'SubmitCollector') { renderSubmitCollector( @@ -278,7 +278,7 @@ To do this, you call the `flow` method on the `davinciClient` passing the `key` ```ts // Example FlowCollector mapping -const collectors = davinci.collectors(); +const collectors = davinciClient.getCollectors(); collectors.map((collector) => { if (collector.type === 'FlowCollector') { renderFlowCollector(collector, davinciClient.flow(collector)); @@ -304,7 +304,7 @@ function renderFlowCollector(collector, startFlow) { After collecting the needed data, you proceed to the next node in the DaVinci flow by calling the `.next()` method on the same `davinci` client object. This can be the result of a user clicking on the button rendered from the `SubmitCollector`, from the "submit" event of the HTML form itself, or from programmatically triggering the submission in the application layer. ```ts -let nextStep = davinci.next(); +const nextStep = await davinciClient.next(); ``` Note: There's no need to pass anything into the `next` method as the DaVinci Client internally stores the updated object needed for the server. @@ -328,25 +328,12 @@ When you receive a success node, you will likely want to use the Authorization C Here's a brief sample of what that might look like in pseudocode: ```ts -// ... other imports - -import { Config, TokenManager } from '@forgerock/javascript-sdk'; - -// ... other config or initialization code - -// This Config.set accepts the same config schema as the davinci function -Config.set(config); - -const node = await davinciClient.next(); - -if (node.status === 'success') { - const clientInfo = davinciClient.getClient(); - - const code = clientInfo.authorization?.code || ''; - const state = clientInfo.authorization?.state || ''; - - const tokens = await TokenManager.getTokens({ query: { code, state } }); - // user now has session and OIDC tokens +// oidcClient is an instance of oidc() from @forgerock/oidc-client, configured earlier +const tokens = await oidcClient.token.exchange(code, state); +if ('error' in tokens) { + console.error('Token exchange failed:', tokens.error); +} else { + console.log('Access token:', tokens.accessToken); } ``` @@ -364,7 +351,7 @@ if (node.status === 'failure') { renderError(error); // ... user clicks button to restart flow - const freshNode = davinciClient.start(); + const freshNode = await davinciClient.start(); } ``` diff --git a/packages/journey-client/README.md b/packages/journey-client/README.md index 52afa8300e..919deeb07b 100644 --- a/packages/journey-client/README.md +++ b/packages/journey-client/README.md @@ -117,12 +117,13 @@ const client = await journey({ config: JourneyClientConfig, requestMiddleware?: RequestMiddleware[], logger?: { level: LogLevel; custom?: CustomLogger }, + store?: SdkStore, }); ``` **Returns**: `Promise` -**Throws**: `Error` if the wellknown URL is invalid, the fetch fails, or the server is not a ForgeRock AM instance. +**Throws**: `Error` if the wellknown URL is invalid, the fetch fails, or the server is not a ForgeRock AM instance. Throws if the `store` argument is provided but is not a valid `SdkStore` handle. ```typescript try { diff --git a/packages/oidc-client/README.md b/packages/oidc-client/README.md index 324e6e98be..27bea7eb71 100644 --- a/packages/oidc-client/README.md +++ b/packages/oidc-client/README.md @@ -55,7 +55,7 @@ The `oidc()` initialization function accepts the following configuration: - **wellknown** (required) - URL to the OIDC provider's well-known configuration endpoint - **clientId** (required) - Your application's client ID registered with the OIDC provider - **redirectUri** (required) - The URI where the OIDC provider will redirect after authentication -- **scope** (required) - Space-separated list of requested scopes (e.g., `'openid profile email'`) +- **scope** (optional, default: `'openid'`) - Space-separated list of requested scopes (e.g., `'openid profile email'`) - **storage** (optional) - Storage configuration for tokens (defaults to localStorage) - **timeout** (optional) - Request timeout in milliseconds - **additionalParameters** (optional) - Additional parameters to include in authorization requests @@ -392,7 +392,7 @@ const tokens = await oidcClient.token.get({ if ('error' in tokens) { console.error('Failed to retrieve tokens:', tokens.error); } else { - console.log('Access token:', tokens.access_token); + console.log('Access token:', tokens.accessToken); } ``` diff --git a/packages/oidc-client/src/types.ts b/packages/oidc-client/src/types.ts index e53519c10c..4b8eefd766 100644 --- a/packages/oidc-client/src/types.ts +++ b/packages/oidc-client/src/types.ts @@ -27,3 +27,6 @@ export { createClientStore } from './lib/client.store.utils.js'; // Referenced by createClientStore's return type, so consumers need the names. export type { OidcRootState } from './lib/client.store.utils.js'; export { rootReducer } from './lib/client.store.utils.js'; + +import { oidc } from './lib/client.store.js'; +export type OidcClient = Awaited>; diff --git a/packages/sdk-effects/store/README.md b/packages/sdk-effects/store/README.md index e38e50dff8..0a872aaf9a 100644 --- a/packages/sdk-effects/store/README.md +++ b/packages/sdk-effects/store/README.md @@ -112,6 +112,146 @@ The endpoint never throws. A network failure, a non-2xx response, or a payload m ## API Reference +### `createSdkStore()` + +```typescript +function createSdkStore(): SdkStore; +``` + +Creates a Redux store that SDK clients can share. The well-known discovery slice is mounted up front; all other slices are injected lazily via `injectClient()`. Applications may call this directly when they want to own the store's lifetime; otherwise each client factory creates one on demand. + +### `injectClient(handle, options)` + +```typescript +function injectClient>( + handle: SdkStore, + options: InjectClientOptions, +): SdkStoreHandle; +``` + +Attaches a client to an existing store: mounts its RTK Query API reducer and middleware, injects any additional slices, and registers the client's private configuration slot. Safe to call multiple times for the same client — RTK deduplicates reducer injection. Throws `Error` (using `INVALID_STORE_MESSAGE`) if `handle` is not a valid SDK store handle. + +### `isSdkStoreHandle(value)` + +```typescript +function isSdkStoreHandle(value: unknown): value is SdkStore; +``` + +Type guard. Returns `true` when `value` structurally matches a valid SDK store handle (i.e. it was produced by `createSdkStore()` or returned by a client factory). Used by client factories to validate caller-supplied `store` arguments before touching the store. + +### `INVALID_STORE_MESSAGE` + +```typescript +const INVALID_STORE_MESSAGE: string; +``` + +Human-readable error message emitted whenever an argument fails the `isSdkStoreHandle()` check. Exported so every client package references the same string rather than duplicating it. + +### `clientExtra(extra, reducerPath, defaults?)` + +```typescript +function clientExtra( + extra: unknown, + reducerPath: string, + defaults?: Partial, +): Slot; +``` + +Resolves the calling client's own per-client slot from a store's thunk `extraArgument`. Runs on every request and never throws — an unrecognised `extra` yields `defaults` (or `{}`) instead of an error. When `defaults` are supplied, any slot key that is absent or `undefined` is filled from `defaults`. + +### `initWellknownQuery(url)` + +```typescript +function initWellknownQuery(url: string): { + applyQuery(callback: QueryCallback): Promise>; +}; +``` + +Creates a well-known query builder for use inside an RTK Query `queryFn`. Constructs the fetch request (with `Accept: application/json`), executes it through the provided `callback`, and validates the response via `isValidWellknownResponse`. Network failures and invalid responses are both normalised to a structured `WellknownQueryError`. + +```typescript +queryFn: async (url, _api, _extra, baseQuery) => { + return initWellknownQuery(url).applyQuery(async (req) => await baseQuery(req)); +}; +``` + +### `isValidWellknownResponse(data)` + +```typescript +function isValidWellknownResponse(data: unknown): data is WellknownResponse; +``` + +Returns `true` when `data` contains the minimum required OIDC well-known fields: `issuer`, `authorization_endpoint`, and `token_endpoint` (all strings). + +### Types + +#### `SdkStore` + +```typescript +interface SdkStore { + readonly store: { + getState: () => unknown; + dispatch: (action: never) => unknown; + subscribe: (listener: () => void) => () => void; + }; + readonly rootReducer: InjectableRootReducer; + readonly dynamicMiddleware: DynamicMiddleware; + readonly extra: SdkStoreRegistry; +} +``` + +State-agnostic shared Redux store contract. This is the type that travels between client packages: `davinci()` and `journey()` expose one as `client.store`; `oidc()` and `journey()` accept one as the `store` option. + +#### `SdkStoreHandle` + +```typescript +interface SdkStoreHandle> extends SdkStore { + readonly store: Store & { + dispatch: ThunkDispatch; + }; + readonly rootReducer: InjectableRootReducer & Reducer; +} +``` + +Store handle with a known state shape `S`. Returned by each client's own store factory so internal code gets full typing on `getState()` and `dispatch()`. Assignable to `SdkStore` for hand-off to another client. + +#### `SdkStoreRegistry` + +```typescript +interface SdkStoreRegistry { + readonly clients: Record; +} +``` + +The mutable client registry carried as the store's `extraArgument`. `configureStore` captures this by reference, so slots added after store creation are visible to every subsequent request. + +#### `ClientSlot` + +```typescript +interface ClientSlot { + readonly requestMiddleware?: readonly unknown[]; + readonly logger?: unknown; + readonly clientId?: string; +} +``` + +Per-client slot on a store's thunk `extraArgument`. Intentionally loose so `sdk-store` does not depend on `sdk-request-middleware` or `sdk-logger` types. Each client narrows this to its own concrete shape at the point of use. + +#### `InjectClientOptions` + +```typescript +interface InjectClientOptions { + readonly api: { reducerPath: string; reducer: Reducer; middleware: Middleware }; + readonly reducerPath: string; + readonly slices?: readonly { name: string; reducer: Reducer }[]; + readonly requestMiddleware?: readonly unknown[]; + readonly logger?: unknown; + readonly clientId?: string; +} +``` + +Options passed to `injectClient()` describing the client attaching itself to a store. `api` provides the RTK Query API (its reducer and middleware are both mounted). `slices` lists any additional Redux slices the client owns. `reducerPath` is used as the registry key — normally matches `api.reducerPath`. + ### `wellknownApi` The canonical RTK Query API instance. `reducerPath` is `'wellknown'`. From 60ece353fe15fb5203662af98dc6e6cd63b21a35 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 30 Jul 2026 09:19:51 -0600 Subject: [PATCH 8/8] refactor(oidc-client): parse don't validate oidc() arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract all structural argument checks from oidc() into a pure, synchronous parseOidcArgs() function. The parser returns a ParsedOidcArgs on success — a narrow type where required config fields are non-optional strings and store is SdkStore | undefined — or a GenericError on failure. This removes scattered guard clauses from the oidc() body and lets the type system carry the proof of validity downstream. --- .../oidc-client/api-report/oidc-client.api.md | 25 +++-- .../api-report/oidc-client.types.api.md | 25 +++-- packages/oidc-client/src/lib/client.store.ts | 95 ++++++++++--------- .../src/lib/client.store.types.test.ts | 85 +++++++++++++++++ .../oidc-client/src/lib/client.store.types.ts | 48 ++++++++++ packages/oidc-client/src/types.ts | 2 + 6 files changed, 217 insertions(+), 63 deletions(-) create mode 100644 packages/oidc-client/src/lib/client.store.types.test.ts create mode 100644 packages/oidc-client/src/lib/client.store.types.ts diff --git a/packages/oidc-client/api-report/oidc-client.api.md b/packages/oidc-client/api-report/oidc-client.api.md index d1583848a8..4d282f83a1 100644 --- a/packages/oidc-client/api-report/oidc-client.api.md +++ b/packages/oidc-client/api-report/oidc-client.api.md @@ -169,23 +169,16 @@ export interface OauthTokens { } // @public -export function oidc(input: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - storage?: Partial; - store?: SdkStore; -}): Promise<{ +export function oidc(raw: RawOidcArgs): Promise void) => Unsubscribe; authorize: { url: (options?: GetAuthorizationUrlOptions) => Promise; @@ -224,6 +217,18 @@ export interface PushAuthorizationResponse { request_uri: string; } +// @public +export type RawOidcArgs = { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + storage?: Partial; + store?: unknown; +}; + export { RequestMiddleware } export { ResponseType_2 as ResponseType } diff --git a/packages/oidc-client/api-report/oidc-client.types.api.md b/packages/oidc-client/api-report/oidc-client.types.api.md index d1583848a8..4d282f83a1 100644 --- a/packages/oidc-client/api-report/oidc-client.types.api.md +++ b/packages/oidc-client/api-report/oidc-client.types.api.md @@ -169,23 +169,16 @@ export interface OauthTokens { } // @public -export function oidc(input: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - storage?: Partial; - store?: SdkStore; -}): Promise<{ +export function oidc(raw: RawOidcArgs): Promise void) => Unsubscribe; authorize: { url: (options?: GetAuthorizationUrlOptions) => Promise; @@ -224,6 +217,18 @@ export interface PushAuthorizationResponse { request_uri: string; } +// @public +export type RawOidcArgs = { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { + level: LogLevel; + custom?: CustomLogger; + }; + storage?: Partial; + store?: unknown; +}; + export { RequestMiddleware } export { ResponseType_2 as ResponseType } diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index 1629e4b1fd..7bad29270d 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -25,10 +25,9 @@ import { wellknownSelector, } from '@forgerock/sdk-store'; -import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { ActionTypes } from '@forgerock/sdk-request-middleware'; import type { GenericError, GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; import type { SdkStore } from '@forgerock/sdk-store'; -import type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; import type { StorageConfig } from '@forgerock/storage'; import type { @@ -39,67 +38,45 @@ import type { RevokeSuccessResult, UserInfoResponse, } from './client.types.js'; -import type { OauthTokens, OidcConfig } from './config.types.js'; +import type { OauthTokens } from './config.types.js'; import type { AuthorizationError, AuthorizationSuccess } from './authorize.request.types.js'; import type { TokenExchangeErrorResponse } from './exchange.types.js'; import type { SessionCheckOptions, SessionCheckSuccess } from './session.types.js'; +import type { ParsedOidcArgs, RawOidcArgs } from './client.store.types.js'; /** - * @function oidc - * @description Factory function to create an OIDC client with methods for authorization, token exchange, - * user info retrieval, and logout. It initializes the client with the provided configuration, - * request middleware, logger, and storage options. - * @param param - configuration object containing the OIDC client configuration, request middleware, logger, - * @param {OidcConfig} param.config - OIDC configuration including server details, client ID, redirect URI, - * storage options, scope, and response type. - * @param {RequestMiddleware} param.requestMiddleware - optional array of request middleware functions to process requests. - * @param {{ level: LogLevel, custom: CustomLogger }} param.logger - optional logger configuration with log level and custom logger. - * @param {Partial} param.storage - optional storage configuration for persisting OIDC tokens. - * @returns {ReturnType} - Returns an object with methods for authorization, token exchange, user info retrieval, and logout. + * @function parseOidcArgs + * @description Pure, synchronous parser for OIDC factory arguments that implements + * the "parse, don't validate" pattern. Returns a narrowed + * {@link ParsedOidcArgs} on success, or a {@link GenericError} describing + * the first structural failure found. + * + * The PAR check (which requires a network round-trip) is intentionally + * excluded — it belongs in `oidc()` after the wellknown fetch. + * @param raw - The unvalidated arguments to parse. + * @returns {ParsedOidcArgs | GenericError} */ -export async function oidc({ - config, - requestMiddleware, - logger, - storage, - store: sharedStore, -}: { - config: OidcConfig; - requestMiddleware?: RequestMiddleware[]; - logger?: { - level: LogLevel; - custom?: CustomLogger; - }; - storage?: Partial; - /** - * An existing SDK store to attach to, so discovery caching and state are - * shared with another client. Omit to create a store for this client alone. - */ - store?: SdkStore; -}) { - const log = loggerFn({ - level: logger?.level ?? config.log ?? 'error', - custom: logger?.custom, - }); - const oauthThreshold = config.oauthThreshold || 30 * 1000; // Default to 30 seconds +export function parseOidcArgs( + raw: RawOidcArgs, +): ParsedOidcArgs | GenericError { /** * Validate before touching the store. RTK's `inject` is irreversible, so * mutating a caller-owned store and *then* rejecting the arguments would leave * them permanently carrying a slice from a call that never succeeded. */ - if (sharedStore !== undefined && !isSdkStoreHandle(sharedStore)) { + if (raw.store !== undefined && !isSdkStoreHandle(raw.store)) { return { error: INVALID_STORE_MESSAGE, type: 'argument_error', }; } - if (!config?.serverConfig?.wellknown) { + if (!raw.config?.serverConfig?.wellknown) { return { error: 'Requires a wellknown url initializing this factory.', type: 'argument_error', }; } - if (!config?.clientId) { + if (!raw.config?.clientId) { return { error: 'Requires a clientId.', type: 'argument_error', @@ -111,7 +88,8 @@ export async function oidc({ * store would share one cache slice and clobber the first client's tokens. * Re-initialising the same clientId is fine and stays idempotent. */ - const conflict = conflictingClientId(sharedStore, config.clientId); + const validatedStore = raw.store as SdkStore | undefined; + const conflict = conflictingClientId(validatedStore, raw.config.clientId); if (conflict) { return { error: @@ -121,6 +99,37 @@ export async function oidc({ }; } + return raw as unknown as ParsedOidcArgs; +} + +/** + * @function oidc + * @description Factory function to create an OIDC client with methods for authorization, token exchange, + * user info retrieval, and logout. It initializes the client with the provided configuration, + * request middleware, logger, and storage options. + * @param raw - configuration object containing the OIDC client configuration, request middleware, logger, + * @param {OidcConfig} raw.config - OIDC configuration including server details, client ID, redirect URI, + * storage options, scope, and response type. + * @param {RequestMiddleware} raw.requestMiddleware - optional array of request middleware functions to process requests. + * @param {{ level: LogLevel, custom: CustomLogger }} raw.logger - optional logger configuration with log level and custom logger. + * @param {Partial} raw.storage - optional storage configuration for persisting OIDC tokens. + * @param {unknown} raw.store - optional existing SDK store to share across clients; validated at runtime via `isSdkStoreHandle`. + * @returns {ReturnType} - Returns an object with methods for authorization, token exchange, user info retrieval, and logout. + */ +export async function oidc( + raw: RawOidcArgs, +) { + const parsed = parseOidcArgs(raw); + if ('type' in parsed) return parsed; + + const { config, requestMiddleware, logger, storage, store: sharedStore } = parsed; + + const log = loggerFn({ + level: logger?.level ?? config.log ?? 'error', + custom: logger?.custom, + }); + const oauthThreshold = config.oauthThreshold || 30 * 1000; // Default to 30 seconds + const storageClient = createStorage({ type: storage?.type || 'localStorage', name: storage?.name || config.clientId, diff --git a/packages/oidc-client/src/lib/client.store.types.test.ts b/packages/oidc-client/src/lib/client.store.types.test.ts new file mode 100644 index 0000000000..9ca2d78acd --- /dev/null +++ b/packages/oidc-client/src/lib/client.store.types.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 - 2026 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 { it, expect, describe } from 'vitest'; +import { logger as loggerFn } from '@forgerock/sdk-logger'; + +import { parseOidcArgs } from './client.store.js'; +import { createClientStore } from './client.store.utils.js'; + +import type { OidcConfig } from './config.types.js'; + +const validConfig: OidcConfig = { + clientId: 'test-client', + redirectUri: 'https://example.com/callback.html', + scope: 'openid profile', + serverConfig: { wellknown: 'https://example.com/.well-known/openid-configuration' }, + responseType: 'code', +}; + +describe('parseOidcArgs', () => { + it('returns ParsedOidcArgs (no error) when all required fields are present', () => { + const result = parseOidcArgs({ config: validConfig }); + + expect('type' in result).toBe(false); + if ('type' in result) return; + expect(result.config.clientId).toBe('test-client'); + expect(result.config.serverConfig.wellknown).toBe( + 'https://example.com/.well-known/openid-configuration', + ); + expect(result.store).toBeUndefined(); + }); + + it('returns argument_error when store is a non-SDK-store object', () => { + const result = parseOidcArgs({ + config: validConfig, + store: { notAnSdkStore: true, dispatch: () => void 0 }, + }); + + expect(result).toMatchObject({ type: 'argument_error' }); + }); + + it('returns argument_error when config.serverConfig.wellknown is missing', () => { + const result = parseOidcArgs({ + config: { ...validConfig, serverConfig: {} as OidcConfig['serverConfig'] }, + }); + + expect(result).toMatchObject({ + type: 'argument_error', + error: 'Requires a wellknown url initializing this factory.', + }); + }); + + it('returns argument_error when config.clientId is missing', () => { + const result = parseOidcArgs({ + config: { ...validConfig, clientId: '' }, + }); + + expect(result).toMatchObject({ + type: 'argument_error', + error: 'Requires a clientId.', + }); + }); + + it('returns argument_error when clientId conflicts with the existing store', () => { + const log = loggerFn({ level: 'error' }); + // Create a real store already registered with 'existing-client'. + // createClientStore returns a handle that passes isSdkStoreHandle at runtime. + const existingHandle = createClientStore({ clientId: 'existing-client', logger: log }); + + const result = parseOidcArgs({ + config: { ...validConfig, clientId: 'different-client' }, + store: existingHandle, + }); + + expect(result).toMatchObject({ type: 'argument_error' }); + if ('type' in result) { + expect(result.error).toContain('existing-client'); + expect(result.error).toContain('Use a separate store per clientId'); + } + }); +}); diff --git a/packages/oidc-client/src/lib/client.store.types.ts b/packages/oidc-client/src/lib/client.store.types.ts new file mode 100644 index 0000000000..2f3d6a323d --- /dev/null +++ b/packages/oidc-client/src/lib/client.store.types.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 - 2026 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 type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware'; +import type { SdkStore } from '@forgerock/sdk-store'; +import type { CustomLogger, LogLevel } from '@forgerock/sdk-logger'; +import type { StorageConfig } from '@forgerock/storage'; + +import type { OidcConfig } from './config.types.js'; + +/** + * The raw, unvalidated input type — this is what public callers pass. + * `store` is typed as `unknown` so the parser can perform a runtime + * brand-check via `isSdkStoreHandle` before narrowing to `SdkStore`. + */ +export type RawOidcArgs = { + config: OidcConfig; + requestMiddleware?: RequestMiddleware[]; + logger?: { level: LogLevel; custom?: CustomLogger }; + storage?: Partial; + /** + * An existing SDK store to attach to, so discovery caching and state are + * shared with another client. Omit to create a store for this client alone. + * Typed as `unknown` — the parser validates this at runtime via `isSdkStoreHandle`. + */ + store?: unknown; +}; + +/** + * The parsed, trusted type — all structural validation checks have passed. + * The type system records the narrowed facts: + * - `config.serverConfig.wellknown` is a non-empty string + * - `config.clientId` is a non-empty string + * - `store` is either a valid `SdkStore` handle or `undefined` + */ +export type ParsedOidcArgs = { + config: OidcConfig & { + serverConfig: { wellknown: string }; + clientId: string; + }; + requestMiddleware?: RequestMiddleware[]; + logger?: { level: LogLevel; custom?: CustomLogger }; + storage?: Partial; + store: SdkStore | undefined; +}; diff --git a/packages/oidc-client/src/types.ts b/packages/oidc-client/src/types.ts index 4b8eefd766..ab2c3a7f08 100644 --- a/packages/oidc-client/src/types.ts +++ b/packages/oidc-client/src/types.ts @@ -23,6 +23,8 @@ export type { StorageConfig } from '@forgerock/storage'; // Re-export functions needed to resolve OidcClient and ClientStore type aliases export { oidc } from './lib/client.store.js'; +// RawOidcArgs is a parameter type of oidc() and must be re-exported so consumers can type call-sites +export type { RawOidcArgs } from './lib/client.store.types.js'; export { createClientStore } from './lib/client.store.utils.js'; // Referenced by createClientStore's return type, so consumers need the names. export type { OidcRootState } from './lib/client.store.utils.js';