Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,68 @@ jobs:
with:
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

- name: Validate interface mapping
id: interface-mapping
continue-on-error: true
run: |
OUTPUT=$(pnpm mapping:validate 2>&1)
EXIT_CODE=$?
echo "$OUTPUT"
echo 'report<<MAPPING_EOF' >> "$GITHUB_OUTPUT"
echo "$OUTPUT" >> "$GITHUB_OUTPUT"
echo 'MAPPING_EOF' >> "$GITHUB_OUTPUT"
exit $EXIT_CODE

- name: Find interface mapping comment
id: find-mapping-comment
if: always()
uses: peter-evans/find-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: <!-- interface-mapping-check -->

- name: Update interface mapping comment
if: always() && steps.interface-mapping.outcome == 'failure'
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.find-mapping-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
<!-- interface-mapping-check -->
## Interface Mapping Out of Date

The `interface_mapping.md` document is out of sync with the SDK exports.

<details>
<summary>Drift report</summary>

```
${{ steps.interface-mapping.outputs.report }}
```

</details>

**To fix**, run:
```bash
pnpm mapping:generate
```
Then commit the updated `interface_mapping.md`.

- name: Update interface mapping comment (passing)
if: always() && steps.interface-mapping.outcome == 'success' && steps.find-mapping-comment.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v5
with:
comment-id: ${{ steps.find-mapping-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
<!-- interface-mapping-check -->
## Interface Mapping Up to Date

The `interface_mapping.md` document is in sync with the SDK exports.

- name: Download baseline bundle sizes
uses: dawidd6/action-download-artifact@v3
with:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ out-tsc/
.swc
.vite

# Lefthook
.lefthook-local.yml
.lefthook/

# IDEs
.vscode

Expand Down
1 change: 0 additions & 1 deletion .husky/commit-msg

This file was deleted.

6 changes: 0 additions & 6 deletions .husky/install.mjs

This file was deleted.

1 change: 0 additions & 1 deletion .husky/pre-commit

This file was deleted.

1 change: 0 additions & 1 deletion .husky/pre-push

This file was deleted.

3 changes: 0 additions & 3 deletions .husky/prepare-commit-msg

This file was deleted.

236 changes: 236 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# Migration Guide: Legacy JavaScript SDK to Ping SDK

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).

## Package Dependencies

| Legacy | New | Purpose |
| --------------------------- | --------------------------- | ------------------------------------------------------------------ |
| `@forgerock/javascript-sdk` | `@forgerock/journey-client` | Authentication tree/journey flows |
| `@forgerock/javascript-sdk` | `@forgerock/oidc-client` | OAuth2/OIDC token management, user info, logout |
| `@forgerock/javascript-sdk` | `@forgerock/sdk-types` | Shared types and enums |
| `@forgerock/javascript-sdk` | `@forgerock/device-client` | Device profile & management (OATH, Push, WebAuthn, Bound, Profile) |
| `@forgerock/ping-protect` | `@forgerock/protect` | PingOne Protect/Signals integration |

---

## SDK Initialization & Configuration

The legacy SDK uses a global static `Config.set()`. The new SDK uses **async factory functions** that each return an independent client instance.

| Legacy | New | Notes |
| ----------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------ |
| `import { Config } from '@forgerock/javascript-sdk'` | `import { journey } from '@forgerock/journey-client'` | Journey client factory |
| — | `import { oidc } from '@forgerock/oidc-client'` | OIDC client factory (separate package) |
| `Config.set({ clientId, redirectUri, scope, serverConfig, realmPath, tree })` | `const journeyClient = await journey({ config })` | Async initialization; config per-client, not global |
| `TokenStorage.get()` | `const tokens = await oidcClient.token.get()` | Now part of oidcClient; check errors with `if ('error' in tokens)` |

### Wellknown Configuration

Both clients require only the OIDC wellknown endpoint URL. All other configuration (baseUrl, paths, realm) is derived automatically:

```typescript
import type { JourneyClientConfig } from '@forgerock/journey-client/types';

// Shared config — journey-client only needs serverConfig.wellknown
const config: JourneyClientConfig = {
serverConfig: {
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
},
};

// Journey client — for authentication tree flows
const journeyClient = await journey({ config });

// OIDC client — for token management, user info, logout
const oidcClient = await oidc({
config: {
...config,
clientId: 'my-app',
redirectUri: `${window.location.origin}/callback`,
scope: 'openid profile',
},
});
```

---

## Authentication & Journey Flow

| Legacy Method | New Method | Notes |
| -------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------ |
| `FRAuth.start({ tree: 'Login' })` | `journeyClient.start({ journey: 'Login' })` | Tree name passed per-call via `journey` param (not in config) |
| `FRAuth.next(step, { tree: 'Login' })` | `journeyClient.next(step)` | Tree is set at `start()`, not repeated on `next()` |
| `FRAuth.redirect(step)` | `await journeyClient.redirect(step)` | Now async. Step stored in `sessionStorage` (was `localStorage`) |
| `FRAuth.resume(resumeUrl)` | `await journeyClient.resume(resumeUrl)` | Previous step retrieved from `sessionStorage` (was `localStorage`) |
| No equivalent | `await journeyClient.terminate()` | **New.** Ends the AM session via `/sessions` endpoint |

### Before/After: Login Flow

**Legacy:**

```typescript
import { FRAuth, StepType } from '@forgerock/javascript-sdk';

Config.set({
serverConfig: { baseUrl: 'https://am.example.com/am' },
realmPath: 'alpha',
tree: 'Login',
});

let step = await FRAuth.start();
while (step.type === StepType.Step) {
// ... handle callbacks ...
step = await FRAuth.next(step);
}
if (step.type === StepType.LoginSuccess) {
console.log('Session token:', step.getSessionToken());
}
```

**New:**

```typescript
import { journey } from '@forgerock/journey-client';

const journeyClient = await journey({
config: {
serverConfig: {
wellknown: 'https://am.example.com/am/oauth2/alpha/.well-known/openid-configuration',
},
},
});

let result = await journeyClient.start({ journey: 'Login' });
while (result.type === 'Step') {
// ... handle callbacks ...
result = await journeyClient.next(result);
}
if ('error' in result) {
console.error('Journey error:', result);
} else if (result.type === 'LoginSuccess') {
console.log('Session token:', result.getSessionToken());
}
```

---

## User Management

| Legacy | New | Notes |
| -------------------------------------- | -------------------------------- | -------------------------------------------------------- |
| `UserManager.getCurrentUser()` | `await oidcClient.user.info()` | Returns typed `UserInfoResponse` or `GenericError` |
| `FRUser.logout({ logoutRedirectUri })` | `await oidcClient.user.logout()` | Revokes tokens + ends session. Returns structured result |

---

## Token Management & OAuth Flow

| Legacy | New | Notes |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `TokenManager.getTokens({ forceRenew: true })` | `await oidcClient.token.get({ forceRenew: true, backgroundRenew: true })` | Single call with auto-renewal. Returns tokens or error |
| `TokenManager.getTokens()` then manual code+state | `await oidcClient.authorize.background()` then `await oidcClient.token.exchange(code, state)` | Two-step when you need explicit control |
| `TokenManager.deleteTokens()` | `await oidcClient.token.revoke()` | Revokes remotely AND deletes locally |
| `TokenStorage.get()` | `await oidcClient.token.get()` | Auto-retrieves from storage; check `'error' in tokens` |
| `TokenStorage.set(tokens)` | Handled automatically by `oidcClient.token.exchange()` | Tokens stored after exchange |
| `TokenStorage.remove()` | `await oidcClient.token.revoke()` | Combined revoke + delete |

### Token Type Change

| Legacy | New | Change |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `Tokens { accessToken?, idToken?, refreshToken?, tokenExpiry? }` | `OauthTokens { accessToken, idToken, refreshToken?, expiresAt?, expiryTimestamp? }` | `accessToken` and `idToken` now required. `tokenExpiry` renamed to `expiryTimestamp` |

---

## Callback & WebAuthn

| Legacy | New | Notes |
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------- |
| `import { CallbackType } from '@forgerock/javascript-sdk'` | `import { callbackType } from '@forgerock/journey-client'` | PascalCase enum → camelCase object |
| `CallbackType.RedirectCallback` | `callbackType.RedirectCallback` | Values remain the same strings |
| `import { FRCallback } from '@forgerock/javascript-sdk'` | `import { BaseCallback } from '@forgerock/journey-client/types'` | Base class renamed |
| `import { NameCallback, ... } from '@forgerock/javascript-sdk'` | `import { NameCallback, ... } from '@forgerock/journey-client/types'` | Same class names, different import path |
| `import { FRWebAuthn, WebAuthnStepType } from '@forgerock/javascript-sdk'` | `import { WebAuthn, WebAuthnStepType } from '@forgerock/journey-client/webauthn'` | `FRWebAuthn` → `WebAuthn`, submodule import |

---

## Step Types

| Legacy | New | Notes |
| --------------------------------- | ----------------------------------- | ----------------------------------------------------- |
| `FRStep` (class instance) | `JourneyStep` (object type) | Cannot use `instanceof`; use `result.type === 'Step'` |
| `FRLoginSuccess` (class instance) | `JourneyLoginSuccess` (object type) | Use `result.type === 'LoginSuccess'` |
| `FRLoginFailure` (class instance) | `JourneyLoginFailure` (object type) | Use `result.type === 'LoginFailure'` |

All step methods (`getCallbackOfType`, `getDescription`, `getHeader`, `getStage`, `getSessionToken`, etc.) remain identical.

---

## HTTP Client

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:

```typescript
const tokens = await oidcClient.token.get({ backgroundRenew: true });
if ('error' in tokens) {
throw new Error('No valid tokens');
}

const response = await fetch('https://api.example.com/resource', {
method: 'GET',
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
```

---

## Error Handling Pattern

The new SDK returns error objects instead of throwing exceptions:

| Legacy | New |
| ------------------------------------------------ | ------------------------------------------------------------------------ |
| `try { ... } catch (err) { console.error(err) }` | `if ('error' in result) { console.error(result.error, result.message) }` |

**Legacy:**

```typescript
try {
const user = await UserManager.getCurrentUser();
setUser(user);
} catch (err) {
console.error(`Error: get current user; ${err}`);
setUser({});
}
```

**New:**

```typescript
const user = await oidcClient.user.info();
if ('error' in user) {
console.error('Error getting user:', user);
setUser({});
} else {
setUser(user);
}
```

---

## Summary of Key Changes

1. **Async-First Initialization**: SDK clients initialized asynchronously with factory functions (`journey()`, `oidc()`)
2. **Instance-Based APIs**: Methods called on client instances, not static classes
3. **Separated Packages**: Journey auth (`@forgerock/journey-client`), OIDC (`@forgerock/oidc-client`), Device (`@forgerock/device-client`), Protect (`@forgerock/protect`)
4. **Explicit Error Handling**: Response objects contain `{ error }` property instead of throwing exceptions
5. **Wellknown-Based Discovery**: Only `serverConfig.wellknown` is required; all paths derived automatically
6. **No Built-in HTTP Client**: Protected API requests require manual token retrieval and header management
7. **Config Fixed at Creation**: Per-call config overrides removed; create separate client instances for different configs

---

## Further Reference

For the complete API-level mapping (every class, method, parameter change, return type change, and behavioral note), see [`interface_mapping.md`](./interface_mapping.md).
Loading
Loading