Skip to content

Commit 352aa56

Browse files
committed
chore: updates
1 parent e825261 commit 352aa56

33 files changed

Lines changed: 2055 additions & 151 deletions

e2e/journey-app/main.ts

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

99
import { journey } from '@forgerock/journey-client';
10+
import { attachJourneyBridge } from '@forgerock/devtools-bridge';
1011

1112
import type { JourneyClient, RequestMiddleware } from '@forgerock/journey-client/types';
1213

@@ -65,6 +66,7 @@ if (searchParams.get('middleware') === 'true') {
6566
let journeyClient: JourneyClient;
6667
try {
6768
journeyClient = await journey({ config: config, requestMiddleware });
69+
attachJourneyBridge(journeyClient, config);
6870
} catch (error) {
6971
const message = error instanceof Error ? error.message : 'Unknown error';
7072
console.error('Failed to initialize journey client:', message);

e2e/journey-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"serve": "pnpm nx nxServe"
1212
},
1313
"dependencies": {
14+
"@forgerock/devtools-bridge": "workspace:*",
1415
"@forgerock/journey-client": "workspace:*",
1516
"@forgerock/oidc-client": "workspace:*",
1617
"@forgerock/protect": "workspace:*",

e2e/journey-app/tsconfig.app.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@
1515
],
1616
"references": [
1717
{
18-
"path": "../../packages/sdk-effects/logger/tsconfig.lib.json"
18+
"path": "../../packages/device-client/tsconfig.lib.json"
1919
},
2020
{
21-
"path": "../../packages/device-client/tsconfig.lib.json"
21+
"path": "../../packages/sdk-effects/logger/tsconfig.lib.json"
2222
},
2323
{
2424
"path": "../../packages/oidc-client/tsconfig.lib.json"
2525
},
26+
{
27+
"path": "../../packages/devtools-bridge/tsconfig.lib.json"
28+
},
2629
{
2730
"path": "../../packages/protect/tsconfig.lib.json"
2831
},

e2e/oidc-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"serve": "pnpm nx nxServe"
1010
},
1111
"dependencies": {
12+
"@forgerock/devtools-bridge": "workspace:*",
1213
"@forgerock/oidc-client": "workspace:*"
1314
},
1415
"nx": {

e2e/oidc-app/src/utils/oidc-app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* of the MIT license. See the LICENSE file for details.
77
*
88
*/
9+
import { attachOidcBridge } from '@forgerock/devtools-bridge';
910
import { oidc } from '@forgerock/oidc-client';
1011
import type {
1112
AuthorizationError,
@@ -54,6 +55,7 @@ export async function oidcApp({ config, urlParams }) {
5455
if ('error' in oidcClient) {
5556
displayError(oidcClient);
5657
}
58+
attachOidcBridge(oidcClient, config);
5759

5860
document.getElementById('login-background').addEventListener('click', async () => {
5961
const authorizeOptions: GetAuthorizationUrlOptions =

e2e/oidc-app/tsconfig.app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
"references": [
2222
{
2323
"path": "../../packages/oidc-client/tsconfig.lib.json"
24+
},
25+
{
26+
"path": "../../packages/devtools-bridge/tsconfig.lib.json"
2427
}
2528
]
2629
}

packages/devtools-bridge/README.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# @forgerock/devtools-bridge
2+
3+
Opt-in SDK adapter that connects your Ping Identity / ForgeRock application to the [Ping DevTools extension](../devtools-extension). Add it to your app in one line — it is a no-op when the extension is not installed, so it is safe to ship in production builds.
4+
5+
## Contents
6+
7+
- [Installation](#installation)
8+
- [Bridges](#bridges)
9+
- [DaVinci — `attachDevToolsBridge`](#davinci--attachdevtoolsbridge)
10+
- [AM Journey — `attachJourneyBridge`](#am-journey--attachjourneybridge)
11+
- [OIDC / OAuth — `attachOidcBridge`](#oidc--oauth--attachoidcbridge)
12+
- [Low-level API](#low-level-api)
13+
- [How it works](#how-it-works)
14+
- [Safety](#safety)
15+
16+
---
17+
18+
## Installation
19+
20+
```bash
21+
pnpm add @forgerock/devtools-bridge
22+
```
23+
24+
`effect` is a peer dependency. `@forgerock/davinci-client` is an optional peer dependency required only if you use `attachDevToolsBridge`.
25+
26+
---
27+
28+
## Bridges
29+
30+
### DaVinci — `attachDevToolsBridge`
31+
32+
Subscribes to a DaVinci client store and emits `sdk:node-change` on every node status transition, plus `session:cookie` / `session:storage` diffs after each transition.
33+
34+
```ts
35+
import { davinci } from '@forgerock/davinci-client';
36+
import { attachDevToolsBridge } from '@forgerock/devtools-bridge';
37+
38+
const client = await davinci({ config });
39+
40+
// Pass config as the second argument — emitted once as sdk:config on the first transition
41+
const bridge = attachDevToolsBridge(client, config);
42+
43+
// Unsubscribe when the component unmounts
44+
bridge.detach();
45+
```
46+
47+
**What it captures per node transition:**
48+
49+
| Field | Source |
50+
| ---------------- | --------------------------------------------- |
51+
| `nodeStatus` | DaVinci node `.status` |
52+
| `previousStatus` | Previous status (tracked locally) |
53+
| `interactionId` | `server.interactionId` |
54+
| `nodeName` | `client.name` |
55+
| `collectors` | `client.collectors` (full objects) |
56+
| `error` | `error.code / message / type` |
57+
| `session` | `server.session` (DaVinci session token) |
58+
| `responseBody` | Full DaVinci server response (from RTK cache) |
59+
60+
The bridge only emits when `nodeStatus` actually changes, so rapid store updates that don't advance the node do not generate noise.
61+
62+
---
63+
64+
### AM Journey — `attachJourneyBridge`
65+
66+
Subscribes to a Journey RTK store and emits `sdk:journey-step` for each mutation that settles (`fulfilled` or `rejected`). Each event carries the full AM step response including all callbacks with their `input`/`output` arrays.
67+
68+
```ts
69+
import { journey } from '@forgerock/journey-client'; // your RTK-based journey client
70+
import { attachJourneyBridge } from '@forgerock/devtools-bridge';
71+
72+
const client = await journey({ config });
73+
74+
attachJourneyBridge(client, config);
75+
```
76+
77+
**`JourneySubscribable` interface** — any object with this shape works:
78+
79+
```ts
80+
interface JourneySubscribable {
81+
subscribe: (listener: () => void) => () => void;
82+
getState: () => unknown; // must expose { journeyReducer: { mutations: Record<string, MutationEntry> } }
83+
}
84+
```
85+
86+
**Emitted events by step type:**
87+
88+
| `stepType` | When | Notable fields |
89+
| -------------- | --------------------------------- | ------------------------------------------ |
90+
| `Step` | AM returns `authId` | `callbacks`, `authId`, `stage`, `header` |
91+
| `LoginSuccess` | AM returns `tokenId` | `tokenId`, `successUrl` |
92+
| `LoginFailure` | AM returns an error / RTK rejects | `errorCode`, `errorMessage`, `errorReason` |
93+
94+
---
95+
96+
### OIDC / OAuth — `attachOidcBridge`
97+
98+
Subscribes to an OIDC client RTK store and emits `sdk:oidc-state` for each settled mutation. Maps RTK endpoint names to human-readable phases.
99+
100+
```ts
101+
import { oidcClient } from '@forgerock/oidc-client'; // your RTK-based OIDC client
102+
import { attachOidcBridge } from '@forgerock/devtools-bridge';
103+
104+
const client = oidcClient({ config });
105+
106+
attachOidcBridge(client, config);
107+
```
108+
109+
**`OidcSubscribable` interface:**
110+
111+
```ts
112+
interface OidcSubscribable {
113+
subscribe: (listener: () => void) => () => void;
114+
getState: () => unknown; // must expose { oidc: { mutations: Record<string, MutationEntry> } }
115+
}
116+
```
117+
118+
**Endpoint → phase mapping:**
119+
120+
| RTK endpoint name | Emitted phase |
121+
| ----------------- | ------------- |
122+
| `authorizeFetch` | `authorize` |
123+
| `authorizeIframe` | `authorize` |
124+
| `exchange` | `exchange` |
125+
| `revoke` | `revoke` |
126+
| `userInfo` | `userinfo` |
127+
| `endSession` | `logout` |
128+
129+
Pass `config.clientId` to surface it in the extension's node detail card:
130+
131+
```ts
132+
attachOidcBridge(client, { clientId: 'my-spa-client', ...rest });
133+
```
134+
135+
---
136+
137+
## Low-level API
138+
139+
If you need to emit events from outside a supported client, use the primitives directly.
140+
141+
```ts
142+
import { emitAuthEvent, emitConfigEvent, DEVTOOLS_EVENT_NAME } from '@forgerock/devtools-bridge';
143+
144+
emitAuthEvent({
145+
id: crypto.randomUUID(),
146+
timestamp: performance.now(),
147+
type: 'sdk:node-change',
148+
source: 'sdk',
149+
flowId: null,
150+
causedBy: null,
151+
data: { _tag: 'sdk', nodeStatus: 'next' },
152+
flags: { isCors: false, isError: false, isAuthRelated: true },
153+
});
154+
155+
emitConfigEvent({ clientId: 'my-app', environment: 'dev' });
156+
```
157+
158+
Both functions dispatch a `CustomEvent` named `DEVTOOLS_EVENT_NAME` (`'pingDevtools'`) on `window`. The content script picks this up and forwards it to the extension service worker.
159+
160+
---
161+
162+
## How it works
163+
164+
```
165+
Your app
166+
├── attachDevToolsBridge(davinciClient) ─┐
167+
├── attachJourneyBridge(journeyClient) ─┤─ emitAuthEvent()
168+
└── attachOidcBridge(oidcClient) ─┘
169+
170+
│ window.dispatchEvent(new CustomEvent('pingDevtools', { detail: event }))
171+
172+
content-script.js
173+
174+
│ chrome.runtime.sendMessage({ type: 'SDK_EVENT', payload: event })
175+
176+
service-worker.ts ──(validates via AuthEventSchema)──▶ EventStore
177+
178+
│ chrome.runtime.sendMessage({ type: 'EVENTS_UPDATED' })
179+
180+
panel (Elm) ── Timeline view + Flow view
181+
```
182+
183+
Each bridge function:
184+
185+
1. Subscribes to the client store
186+
2. Validates the current state with an Effect Schema decoder (returns `Option.none` on mismatch — never throws)
187+
3. Deduplicates by tracking already-emitted request IDs in a `Set`
188+
4. Trims that `Set` to only IDs still present in the store, bounding memory use
189+
5. Dispatches the event only when `window.__PING_DEVTOOLS_EXTENSION__` is present
190+
191+
---
192+
193+
## Safety
194+
195+
- **No-op without the extension** — all bridges check for `window.__PING_DEVTOOLS_EXTENSION__` before dispatching. If the marker is absent, nothing is emitted.
196+
- **No-op in SSR / Node** — all bridges return `{ detach: () => undefined }` immediately when `typeof window === 'undefined'`.
197+
- **Tree-shakeable**`sideEffects: false` in `package.json`; unused bridges are eliminated by your bundler.
198+
- **No sensitive data leakage** — the bridge never reads passwords or form values; it only observes the client's Redux/RTK state.
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export { attachDevToolsBridge } from './lib/bridge.js';
22
export type { BridgeHandle } from './lib/bridge.js';
3-
export { DEVTOOLS_EVENT_NAME, emitAuthEvent } from './lib/emit.js';
3+
export { attachJourneyBridge } from './lib/journey-bridge.js';
4+
export type { JourneyBridgeHandle } from './lib/journey-bridge.js';
5+
export { attachOidcBridge } from './lib/oidc-bridge.js';
6+
export type { OidcBridgeHandle } from './lib/oidc-bridge.js';
7+
export { DEVTOOLS_EVENT_NAME, emitAuthEvent, emitConfigEvent } from './lib/emit.js';

packages/devtools-bridge/src/lib/bridge.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Schema, Option, pipe } from 'effect';
22
import type { SdkData } from '@forgerock/devtools-types';
33
import { SdkErrorSchema, SdkAuthorizationSchema } from '@forgerock/devtools-types';
4-
import { emitAuthEvent } from './emit.js';
4+
import { emitAuthEvent, emitConfigEvent } from './emit.js';
55

66
interface Subscribable {
77
subscribe: (listener: () => void) => () => void;
@@ -147,19 +147,6 @@ function emitSessionDiffs(
147147
// Event builders
148148
// ---------------------------------------------------------------------------
149149

150-
function emitConfigEvent(config: object): void {
151-
emitAuthEvent({
152-
id: crypto.randomUUID(),
153-
timestamp: performance.now(),
154-
type: 'sdk:config',
155-
source: 'sdk',
156-
flowId: null,
157-
causedBy: null,
158-
data: { _tag: 'sdk-config', config },
159-
flags: { isCors: false, isError: false, isAuthRelated: true },
160-
});
161-
}
162-
163150
function emitNodeChange(data: SdkData): void {
164151
emitAuthEvent({
165152
id: crypto.randomUUID(),

packages/devtools-bridge/src/lib/emit.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,16 @@ export function emitAuthEvent(event: AuthEvent): void {
66
if (typeof window === 'undefined') return;
77
window.dispatchEvent(new CustomEvent(DEVTOOLS_EVENT_NAME, { detail: event }));
88
}
9+
10+
export function emitConfigEvent(config: object): void {
11+
emitAuthEvent({
12+
id: crypto.randomUUID(),
13+
timestamp: performance.now(),
14+
type: 'sdk:config',
15+
source: 'sdk',
16+
flowId: null,
17+
causedBy: null,
18+
data: { _tag: 'sdk-config', config },
19+
flags: { isCors: false, isError: false, isAuthRelated: true },
20+
});
21+
}

0 commit comments

Comments
 (0)