Skip to content

Commit 7ab96b1

Browse files
committed
feat: add interface mapping validator/generator and migrate to lefthook
Add tools/interface-mapping-validator — a CLI tool that validates and generates the interface_mapping.md and MIGRATION.md documents from a TypeScript mapping config combined with auto-discovered SDK exports. Three CLI modes: - validate: check for drift between docs and SDK exports - generate (--generate): rebuild sections 0, 1, 5 from config - fix (--fix): patch existing tables Also: - Migrate git hooks from husky/lint-staged to lefthook - Add CI step that validates interface mapping on PRs with auto-updating comments - Add root-level scripts: pnpm run mapping:validate, pnpm run mapping:generate 97 tests, functional core / imperative shell architecture.
1 parent 037b012 commit 7ab96b1

48 files changed

Lines changed: 5547 additions & 297 deletions

Some content is hidden

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

.github/workflows/ci.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,68 @@ jobs:
5959
with:
6060
message: Deployed ${{ github.sha }} to https://ForgeRock.github.io/ping-javascript-sdk/pr-${{ github.event.number }}/${{github.sha}} branch gh-pages in ForgeRock/ping-javascript-sdk
6161

62+
- name: Validate interface mapping
63+
id: interface-mapping
64+
continue-on-error: true
65+
run: |
66+
OUTPUT=$(pnpm mapping:validate 2>&1)
67+
EXIT_CODE=$?
68+
echo "$OUTPUT"
69+
echo 'report<<MAPPING_EOF' >> "$GITHUB_OUTPUT"
70+
echo "$OUTPUT" >> "$GITHUB_OUTPUT"
71+
echo 'MAPPING_EOF' >> "$GITHUB_OUTPUT"
72+
exit $EXIT_CODE
73+
74+
- name: Find interface mapping comment
75+
id: find-mapping-comment
76+
if: always()
77+
uses: peter-evans/find-comment@v4
78+
with:
79+
issue-number: ${{ github.event.pull_request.number }}
80+
comment-author: 'github-actions[bot]'
81+
body-includes: <!-- interface-mapping-check -->
82+
83+
- name: Update interface mapping comment
84+
if: always() && steps.interface-mapping.outcome == 'failure'
85+
uses: peter-evans/create-or-update-comment@v5
86+
with:
87+
comment-id: ${{ steps.find-mapping-comment.outputs.comment-id }}
88+
issue-number: ${{ github.event.pull_request.number }}
89+
edit-mode: replace
90+
body: |
91+
<!-- interface-mapping-check -->
92+
## Interface Mapping Out of Date
93+
94+
The `interface_mapping.md` document is out of sync with the SDK exports.
95+
96+
<details>
97+
<summary>Drift report</summary>
98+
99+
```
100+
${{ steps.interface-mapping.outputs.report }}
101+
```
102+
103+
</details>
104+
105+
**To fix**, run:
106+
```bash
107+
pnpm mapping:generate
108+
```
109+
Then commit the updated `interface_mapping.md`.
110+
111+
- name: Update interface mapping comment (passing)
112+
if: always() && steps.interface-mapping.outcome == 'success' && steps.find-mapping-comment.outputs.comment-id != ''
113+
uses: peter-evans/create-or-update-comment@v5
114+
with:
115+
comment-id: ${{ steps.find-mapping-comment.outputs.comment-id }}
116+
issue-number: ${{ github.event.pull_request.number }}
117+
edit-mode: replace
118+
body: |
119+
<!-- interface-mapping-check -->
120+
## Interface Mapping Up to Date
121+
122+
The `interface_mapping.md` document is in sync with the SDK exports.
123+
62124
- name: Download baseline bundle sizes
63125
uses: dawidd6/action-download-artifact@v3
64126
with:

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ out-tsc/
2828
.swc
2929
.vite
3030

31+
# Lefthook
32+
.lefthook-local.yml
33+
.lefthook/
34+
3135
# IDEs
3236
.vscode
3337

.husky/commit-msg

Lines changed: 0 additions & 1 deletion
This file was deleted.

.husky/install.mjs

Lines changed: 0 additions & 6 deletions
This file was deleted.

.husky/pre-commit

Lines changed: 0 additions & 1 deletion
This file was deleted.

.husky/pre-push

Lines changed: 0 additions & 1 deletion
This file was deleted.

.husky/prepare-commit-msg

Lines changed: 0 additions & 3 deletions
This file was deleted.

MIGRATION.md

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Migration Guide: Legacy JavaScript SDK to Ping SDK
2+
3+
This guide documents the conversion from `@forgerock/javascript-sdk` (legacy) to the newer Ping SDK packages. For the complete API-level mapping of every class, method, parameter, and return type, see [`interface_mapping.md`](./interface_mapping.md).
4+
5+
## Package Dependencies
6+
7+
| Legacy | New | Purpose |
8+
| --------------------------- | --------------------------- | ------------------------------------------------------------------ |
9+
| `@forgerock/javascript-sdk` | `@forgerock/journey-client` | Authentication tree/journey flows |
10+
| `@forgerock/javascript-sdk` | `@forgerock/oidc-client` | OAuth2/OIDC token management, user info, logout |
11+
| `@forgerock/javascript-sdk` | `@forgerock/sdk-types` | Shared types and enums |
12+
| `@forgerock/javascript-sdk` | `@forgerock/device-client` | Device profile & management (OATH, Push, WebAuthn, Bound, Profile) |
13+
| `@forgerock/ping-protect` | `@forgerock/protect` | PingOne Protect/Signals integration |
14+
15+
---
16+
17+
## SDK Initialization & Configuration
18+
19+
The legacy SDK uses a global static `Config.set()`. The new SDK uses **async factory functions** that each return an independent client instance.
20+
21+
| Legacy | New | Notes |
22+
| ----------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------ |
23+
| `import { Config } from '@forgerock/javascript-sdk'` | `import { journey } from '@forgerock/journey-client'` | Journey client factory |
24+
|| `import { oidc } from '@forgerock/oidc-client'` | OIDC client factory (separate package) |
25+
| `Config.set({ clientId, redirectUri, scope, serverConfig, realmPath, tree })` | `const journeyClient = await journey({ config })` | Async initialization; config per-client, not global |
26+
| `TokenStorage.get()` | `const tokens = await oidcClient.token.get()` | Now part of oidcClient; check errors with `if ('error' in tokens)` |
27+
28+
### Wellknown Configuration
29+
30+
Both clients require only the OIDC wellknown endpoint URL. All other configuration (baseUrl, paths, realm) is derived automatically:
31+
32+
```typescript
33+
import type { JourneyClientConfig } from '@forgerock/journey-client/types';
34+
35+
// Shared config — journey-client only needs serverConfig.wellknown
36+
const config: JourneyClientConfig = {
37+
serverConfig: {
38+
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
39+
},
40+
};
41+
42+
// Journey client — for authentication tree flows
43+
const journeyClient = await journey({ config });
44+
45+
// OIDC client — for token management, user info, logout
46+
const oidcClient = await oidc({
47+
config: {
48+
...config,
49+
clientId: 'my-app',
50+
redirectUri: `${window.location.origin}/callback`,
51+
scope: 'openid profile',
52+
},
53+
});
54+
```
55+
56+
---
57+
58+
## Authentication & Journey Flow
59+
60+
| Legacy Method | New Method | Notes |
61+
| -------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------ |
62+
| `FRAuth.start({ tree: 'Login' })` | `journeyClient.start({ journey: 'Login' })` | Tree name passed per-call via `journey` param (not in config) |
63+
| `FRAuth.next(step, { tree: 'Login' })` | `journeyClient.next(step)` | Tree is set at `start()`, not repeated on `next()` |
64+
| `FRAuth.redirect(step)` | `await journeyClient.redirect(step)` | Now async. Step stored in `sessionStorage` (was `localStorage`) |
65+
| `FRAuth.resume(resumeUrl)` | `await journeyClient.resume(resumeUrl)` | Previous step retrieved from `sessionStorage` (was `localStorage`) |
66+
| No equivalent | `await journeyClient.terminate()` | **New.** Ends the AM session via `/sessions` endpoint |
67+
68+
### Before/After: Login Flow
69+
70+
**Legacy:**
71+
72+
```typescript
73+
import { FRAuth, StepType } from '@forgerock/javascript-sdk';
74+
75+
Config.set({
76+
serverConfig: { baseUrl: 'https://am.example.com/am' },
77+
realmPath: 'alpha',
78+
tree: 'Login',
79+
});
80+
81+
let step = await FRAuth.start();
82+
while (step.type === StepType.Step) {
83+
// ... handle callbacks ...
84+
step = await FRAuth.next(step);
85+
}
86+
if (step.type === StepType.LoginSuccess) {
87+
console.log('Session token:', step.getSessionToken());
88+
}
89+
```
90+
91+
**New:**
92+
93+
```typescript
94+
import { journey } from '@forgerock/journey-client';
95+
96+
const journeyClient = await journey({
97+
config: {
98+
serverConfig: {
99+
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
100+
},
101+
},
102+
});
103+
104+
let result = await journeyClient.start({ journey: 'Login' });
105+
while (result.type === 'Step') {
106+
// ... handle callbacks ...
107+
result = await journeyClient.next(result);
108+
}
109+
if ('error' in result) {
110+
console.error('Journey error:', result);
111+
} else if (result.type === 'LoginSuccess') {
112+
console.log('Session token:', result.getSessionToken());
113+
}
114+
```
115+
116+
---
117+
118+
## User Management
119+
120+
| Legacy | New | Notes |
121+
| -------------------------------------- | -------------------------------- | -------------------------------------------------------- |
122+
| `UserManager.getCurrentUser()` | `await oidcClient.user.info()` | Returns typed `UserInfoResponse` or `GenericError` |
123+
| `FRUser.logout({ logoutRedirectUri })` | `await oidcClient.user.logout()` | Revokes tokens + ends session. Returns structured result |
124+
125+
---
126+
127+
## Token Management & OAuth Flow
128+
129+
| Legacy | New | Notes |
130+
| ------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
131+
| `TokenManager.getTokens({ forceRenew: true })` | `await oidcClient.token.get({ forceRenew: true, backgroundRenew: true })` | Single call with auto-renewal. Returns tokens or error |
132+
| `TokenManager.getTokens()` then manual code+state | `await oidcClient.authorize.background()` then `await oidcClient.token.exchange(code, state)` | Two-step when you need explicit control |
133+
| `TokenManager.deleteTokens()` | `await oidcClient.token.revoke()` | Revokes remotely AND deletes locally |
134+
| `TokenStorage.get()` | `await oidcClient.token.get()` | Auto-retrieves from storage; check `'error' in tokens` |
135+
| `TokenStorage.set(tokens)` | Handled automatically by `oidcClient.token.exchange()` | Tokens stored after exchange |
136+
| `TokenStorage.remove()` | `await oidcClient.token.revoke()` | Combined revoke + delete |
137+
138+
### Token Type Change
139+
140+
| Legacy | New | Change |
141+
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
142+
| `Tokens { accessToken?, idToken?, refreshToken?, tokenExpiry? }` | `OauthTokens { accessToken, idToken, refreshToken?, expiresAt?, expiryTimestamp? }` | `accessToken` and `idToken` now required. `tokenExpiry` renamed to `expiryTimestamp` |
143+
144+
---
145+
146+
## Callback & WebAuthn
147+
148+
| Legacy | New | Notes |
149+
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------- |
150+
| `import { CallbackType } from '@forgerock/javascript-sdk'` | `import { callbackType } from '@forgerock/journey-client'` | PascalCase enum → camelCase object |
151+
| `CallbackType.RedirectCallback` | `callbackType.RedirectCallback` | Values remain the same strings |
152+
| `import { FRCallback } from '@forgerock/javascript-sdk'` | `import { BaseCallback } from '@forgerock/journey-client/types'` | Base class renamed |
153+
| `import { NameCallback, ... } from '@forgerock/javascript-sdk'` | `import { NameCallback, ... } from '@forgerock/journey-client/types'` | Same class names, different import path |
154+
| `import { FRWebAuthn, WebAuthnStepType } from '@forgerock/javascript-sdk'` | `import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn'` | `FRWebAuthn``WebAuthn`, submodule import |
155+
156+
---
157+
158+
## Step Types
159+
160+
| Legacy | New | Notes |
161+
| --------------------------------- | ----------------------------------- | ----------------------------------------------------- |
162+
| `FRStep` (class instance) | `JourneyStep` (object type) | Cannot use `instanceof`; use `result.type === 'Step'` |
163+
| `FRLoginSuccess` (class instance) | `JourneyLoginSuccess` (object type) | Use `result.type === 'LoginSuccess'` |
164+
| `FRLoginFailure` (class instance) | `JourneyLoginFailure` (object type) | Use `result.type === 'LoginFailure'` |
165+
166+
All step methods (`getCallbackOfType`, `getDescription`, `getHeader`, `getStage`, `getSessionToken`, etc.) remain identical.
167+
168+
---
169+
170+
## HTTP Client
171+
172+
The legacy `HttpClient` is removed. It provided auto bearer-token injection, 401 token refresh, and policy advice handling. In the new SDK, manage tokens manually:
173+
174+
```typescript
175+
const tokens = await oidcClient.token.get({ backgroundRenew: true });
176+
if ('error' in tokens) {
177+
throw new Error('No valid tokens');
178+
}
179+
180+
const response = await fetch('https://api.example.com/resource', {
181+
method: 'GET',
182+
headers: { Authorization: `Bearer ${tokens.accessToken}` },
183+
});
184+
```
185+
186+
---
187+
188+
## Error Handling Pattern
189+
190+
The new SDK returns error objects instead of throwing exceptions:
191+
192+
| Legacy | New |
193+
| ------------------------------------------------ | ------------------------------------------------------------------------ |
194+
| `try { ... } catch (err) { console.error(err) }` | `if ('error' in result) { console.error(result.error, result.message) }` |
195+
196+
**Legacy:**
197+
198+
```typescript
199+
try {
200+
const user = await UserManager.getCurrentUser();
201+
setUser(user);
202+
} catch (err) {
203+
console.error(`Error: get current user; ${err}`);
204+
setUser({});
205+
}
206+
```
207+
208+
**New:**
209+
210+
```typescript
211+
const user = await oidcClient.user.info();
212+
if ('error' in user) {
213+
console.error('Error getting user:', user);
214+
setUser({});
215+
} else {
216+
setUser(user);
217+
}
218+
```
219+
220+
---
221+
222+
## Summary of Key Changes
223+
224+
1. **Async-First Initialization**: SDK clients initialized asynchronously with factory functions (`journey()`, `oidc()`)
225+
2. **Instance-Based APIs**: Methods called on client instances, not static classes
226+
3. **Separated Packages**: Journey auth (`@forgerock/journey-client`), OIDC (`@forgerock/oidc-client`), Device (`@forgerock/device-client`), Protect (`@forgerock/protect`)
227+
4. **Explicit Error Handling**: Response objects contain `{ error }` property instead of throwing exceptions
228+
5. **Wellknown-Based Discovery**: Only `serverConfig.wellknown` is required; all paths derived automatically
229+
6. **No Built-in HTTP Client**: Protected API requests require manual token retrieval and header management
230+
7. **Config Fixed at Creation**: Per-call config overrides removed; create separate client instances for different configs
231+
232+
---
233+
234+
## Further Reference
235+
236+
For the complete API-level mapping (every class, method, parameter change, return type change, and behavioral note), see [`interface_mapping.md`](./interface_mapping.md).

0 commit comments

Comments
 (0)