Skip to content

Commit 60cd9a0

Browse files
committed
feat(journey-client): wellknown-endpoint-config-support
Re-implement well known configuration and abstract into reusable package - Remove internal re-export file (wellknown.api.ts) - Import directly from source packages in internal modules - Keep consumer-facing exports in types.ts as the single public API - Add IPv6 localhost comment explaining design decision - Remove redundant inline comments (JSDoc is sufficient)
1 parent 296c8c2 commit 60cd9a0

46 files changed

Lines changed: 1674 additions & 541 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/rich-cows-try.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
'@forgerock/journey-client': minor
3+
'@forgerock/sdk-oidc': minor
4+
'@forgerock/sdk-utilities': minor
5+
'@forgerock/davinci-client': patch
6+
'@forgerock/oidc-client': patch
7+
---
8+
9+
### @forgerock/journey-client
10+
11+
Add well-known OIDC endpoint discovery support. The journey client can now fetch configuration from the `.well-known/openid-configuration` endpoint:
12+
13+
```typescript
14+
const client = await journey({
15+
serverConfig: {
16+
baseUrl: 'https://am.example.com/am/',
17+
wellknown:
18+
'https://am.example.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
19+
},
20+
});
21+
```
22+
23+
The realm path can be automatically inferred from the well-known issuer URL.
24+
25+
### @forgerock/sdk-oidc
26+
27+
Add shared well-known module with RTK Query API for OIDC endpoint discovery:
28+
29+
- `wellknownApi` - RTK Query API for fetching well-known configuration
30+
- `createWellknownSelector` - Selector factory for cached well-known data
31+
- `createWellknownError` - Typed error creation from fetch failures
32+
- Re-exports pure utilities from `@forgerock/sdk-utilities`
33+
34+
### @forgerock/sdk-utilities
35+
36+
Add pure well-known utilities:
37+
38+
- `inferRealmFromIssuer` - Extract realm path from AM issuer URLs
39+
- `isValidWellknownUrl` - Validate well-known URLs (HTTPS required, HTTP allowed for localhost)
40+
41+
### @forgerock/davinci-client
42+
43+
Refactored to use shared well-known module from `@forgerock/sdk-oidc`.
44+
45+
### @forgerock/oidc-client
46+
47+
Refactored to use shared well-known module from `@forgerock/sdk-oidc`.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@forgerock/journey-client': major
3+
---
4+
5+
BREAKING: Unify journey-client around wellknown-only configuration
6+
7+
This release simplifies the configuration API by requiring the `wellknown` URL and automatically inferring `baseUrl` and `realmPath`.
8+
9+
## Breaking Changes
10+
11+
- **Removed `baseUrl` from `JourneyServerConfig`**: The `baseUrl` is now always inferred from the wellknown URL. If inference fails (non-AM server), an error is returned.
12+
- **Removed `hasWellknownConfig` export**: This type guard is no longer needed since all configs use wellknown.
13+
14+
## Migration
15+
16+
**Before:**
17+
18+
```typescript
19+
journey({
20+
config: {
21+
serverConfig: { baseUrl: 'https://am.example.com/am/' },
22+
realmPath: 'alpha',
23+
},
24+
});
25+
```
26+
27+
**After:**
28+
29+
```typescript
30+
journey({
31+
config: {
32+
serverConfig: {
33+
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
34+
},
35+
// realmPath is now optional - inferred from wellknown issuer
36+
},
37+
});
38+
```
39+
40+
## Features
41+
42+
- Automatic `baseUrl` inference from wellknown URL (extracts path before `/oauth2/`)
43+
- Automatic `realmPath` inference from wellknown issuer
44+
- Improved error messages for non-AM servers, guiding users to appropriate clients
45+
- Updated README with comprehensive API documentation

‎e2e/am-mock-api/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,6 @@
1616
"uuid": "^13.0.0"
1717
},
1818
"devDependencies": {
19-
"@types/express": "^4.17.17"
19+
"@types/express": "^5.0.0"
2020
}
2121
}

‎e2e/am-mock-api/src/app/routes.auth.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,11 @@ export default function (app) {
668668
res.send(wellKnownForgeRock);
669669
});
670670

671+
// Standard AM wellknown endpoint path (used by journey-client wellknown discovery)
672+
app.get('/am/oauth2/realms/root/.well-known/openid-configuration', (req, res) => {
673+
res.send(wellKnownForgeRock);
674+
});
675+
671676
app.get('/as/.well-known/new-oidc-configuration', (req, res) => {
672677
res.send(newPiWellKnown);
673678
});

‎e2e/journey-app/main.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import './style.css';
88

9-
import { journey } from '@forgerock/journey-client';
9+
import { journey, isJourneyClient } from '@forgerock/journey-client';
1010

1111
import type { RequestMiddleware } from '@forgerock/journey-client/types';
1212

@@ -51,12 +51,23 @@ if (searchParams.get('middleware') === 'true') {
5151
}
5252

5353
(async () => {
54-
const journeyClient = await journey({ config: config, requestMiddleware });
55-
5654
const errorEl = document.getElementById('error') as HTMLDivElement;
5755
const formEl = document.getElementById('form') as HTMLFormElement;
5856
const journeyEl = document.getElementById('journey') as HTMLDivElement;
5957

58+
const journeyClientResult = await journey({ config: config, requestMiddleware });
59+
if (!isJourneyClient(journeyClientResult)) {
60+
console.error('Failed to initialize journey client:', journeyClientResult.message);
61+
errorEl.textContent = journeyClientResult.message ?? 'Unknown error';
62+
return;
63+
}
64+
/**
65+
* Re-assign to a new const after type narrowing.
66+
* TypeScript's type narrowing doesn't persist into closures (event handlers, callbacks)
67+
* because it can't prove the variable wasn't reassigned between the guard and closure execution.
68+
* Creating a new const binding after the guard preserves the narrowed type for nested functions.
69+
*/
70+
const journeyClient = journeyClientResult;
6071
let step = await journeyClient.start({ journey: journeyName });
6172

6273
function renderComplete() {

‎e2e/journey-app/server-configs.ts‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,27 @@
66
*/
77
import type { JourneyClientConfig } from '@forgerock/journey-client/types';
88

9+
/**
10+
* Server configurations for E2E tests.
11+
*
12+
* Both baseUrl and realmPath are automatically inferred from the wellknown URL:
13+
* - baseUrl: extracted from the path before `/oauth2/`
14+
* - realmPath: extracted from the issuer URL in the wellknown response
15+
*/
916
export const serverConfigs: Record<string, JourneyClientConfig> = {
1017
basic: {
1118
serverConfig: {
12-
baseUrl: 'http://localhost:9443/am',
19+
wellknown: 'http://localhost:9443/am/oauth2/realms/root/.well-known/openid-configuration',
20+
// baseUrl inferred: http://localhost:9443/am/
21+
// realmPath inferred from issuer: 'root'
1322
},
14-
realmPath: 'root',
1523
},
1624
tenant: {
1725
serverConfig: {
18-
baseUrl: 'https://openam-sdks.forgeblocks.com/am',
26+
wellknown:
27+
'https://openam-sdks.forgeblocks.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
28+
// baseUrl inferred: https://openam-sdks.forgeblocks.com/am/
29+
// realmPath inferred from issuer: 'alpha'
1930
},
20-
realmPath: 'alpha',
2131
},
2232
};

‎e2e/mock-api-v2/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"nanoid": "5.1.6"
2727
},
2828
"devDependencies": {
29-
"@effect/vitest": "catalog:effect"
29+
"@effect/vitest": "catalog:effect",
30+
"vitest": "catalog:vitest"
3031
},
3132
"nx": {
3233
"tags": ["scope:e2e"],

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { createClientStore, handleUpdateValidateError, RootState } from './clien
1515
import { nodeSlice } from './node.slice.js';
1616
import { davinciApi } from './davinci.api.js';
1717
import { configSlice } from './config.slice.js';
18-
import { wellknownApi } from './wellknown.api.js';
18+
import { wellknownApi, createWellknownError } from '@forgerock/sdk-oidc';
1919

2020
import type { ActionTypes, RequestMiddleware } from '@forgerock/sdk-request-middleware';
2121
/**
@@ -88,14 +88,14 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
8888
throw error;
8989
}
9090

91-
const { data: openIdResponse } = await store.dispatch(
92-
wellknownApi.endpoints.wellknown.initiate(config.serverConfig.wellknown),
91+
const { data: openIdResponse, error: fetchError } = await store.dispatch(
92+
wellknownApi.endpoints.configuration.initiate(config.serverConfig.wellknown),
9393
);
9494

95-
if (!openIdResponse) {
96-
const error = new Error('error fetching `wellknown` response for OpenId Configuration');
97-
log.error(error.message);
98-
throw error;
95+
if (fetchError || !openIdResponse) {
96+
const genericError = createWellknownError(fetchError);
97+
log.error(`${genericError.error}: ${genericError.message}`);
98+
throw new Error(genericError.message);
9999
}
100100

101101
store.dispatch(configSlice.actions.set({ ...config, wellknownResponse: openIdResponse }));
@@ -249,11 +249,11 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
249249

250250
return node;
251251
} catch (err) {
252-
const error = err as Error;
253-
log.error(error.message);
252+
const errorMessage = err instanceof Error ? err.message : String(err);
253+
log.error(errorMessage);
254254
return {
255255
error: {
256-
message: error.message ?? 'An unexpected error occurred during resume operation',
256+
message: errorMessage || 'An unexpected error occurred during resume operation',
257257
type: 'internal_error',
258258
},
259259
type: 'internal_error',
@@ -336,10 +336,10 @@ export async function davinci<ActionType extends ActionTypes = ActionTypes>({
336336
store.dispatch(nodeSlice.actions.update({ id, value, index }));
337337
return null;
338338
} catch (err) {
339-
const error = err as Error;
339+
const errorMessage = err instanceof Error ? err.message : String(err);
340340
return {
341341
type: 'internal_error',
342-
error: { message: error.message, type: 'internal_error' },
342+
error: { message: errorMessage, type: 'internal_error' },
343343
};
344344
}
345345
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { configSlice } from './config.slice.js';
1313
import { nodeSlice } from './node.slice.js';
1414
import { davinciApi } from './davinci.api.js';
1515
import { ErrorNode, ContinueNode, StartNode, SuccessNode } from '../types.js';
16-
import { wellknownApi } from './wellknown.api.js';
16+
import { wellknownApi } from '@forgerock/sdk-oidc';
1717
import { InternalErrorResponse } from './client.types.js';
1818

1919
export function createClientStore<ActionType extends ActionTypes>({

0 commit comments

Comments
 (0)