@wolfcola/devtools-bridge
v1.1.1
Published
Opt-in SDK adapter that emits AuthEvents to the WolfCola DevTools extension
Downloads
2,058
Readme
@wolfcola/devtools-bridge
Opt-in SDK adapter that connects your Ping Identity / ForgeRock application to WolfCola DevTools — either the browser extension or the VS Code 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.
Contents
Installation
pnpm add @wolfcola/devtools-bridgeeffect is a peer dependency. @forgerock/davinci-client is an optional peer dependency required only if you use attachDevToolsBridge.
Bridges
DaVinci — attachDevToolsBridge
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.
import { davinci } from '@forgerock/davinci-client';
import { attachDevToolsBridge } from '@wolfcola/devtools-bridge';
const client = await davinci({ config });
// Pass config as the second argument — emitted once as sdk:config on the first transition
const bridge = attachDevToolsBridge(client, config);
// Unsubscribe when the component unmounts
bridge.detach();What it captures per node transition:
| Field | Source |
| ---------------- | --------------------------------------------- |
| nodeStatus | DaVinci node .status |
| previousStatus | Previous status (tracked locally) |
| interactionId | server.interactionId |
| nodeName | client.name |
| collectors | client.collectors (full objects) |
| error | error.code / message / type |
| session | server.session (DaVinci session token) |
| responseBody | Full DaVinci server response (from RTK cache) |
The bridge only emits when nodeStatus actually changes, so rapid store updates that don't advance the node do not generate noise.
AM Journey — attachJourneyBridge
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.
import { journey } from '@forgerock/journey-client'; // your RTK-based journey client
import { attachJourneyBridge } from '@wolfcola/devtools-bridge';
const client = await journey({ config });
attachJourneyBridge(client, config);JourneySubscribable interface — any object with this shape works:
interface JourneySubscribable {
subscribe: (listener: () => void) => () => void;
getState: () => unknown; // must expose { journeyReducer: { mutations: Record<string, MutationEntry> } }
}Emitted events by step type:
| stepType | When | Notable fields |
| -------------- | --------------------------------- | ------------------------------------------ |
| Step | AM returns authId | callbacks, authId, stage, header |
| LoginSuccess | AM returns tokenId | tokenId, successUrl |
| LoginFailure | AM returns an error / RTK rejects | errorCode, errorMessage, errorReason |
OIDC / OAuth — attachOidcBridge
Subscribes to an OIDC client RTK store and emits sdk:oidc-state for each settled mutation. Maps RTK endpoint names to human-readable phases.
import { oidcClient } from '@forgerock/oidc-client'; // your RTK-based OIDC client
import { attachOidcBridge } from '@wolfcola/devtools-bridge';
const client = oidcClient({ config });
attachOidcBridge(client, config);OidcSubscribable interface:
interface OidcSubscribable {
subscribe: (listener: () => void) => () => void;
getState: () => unknown; // must expose { oidc: { mutations: Record<string, MutationEntry> } }
}Endpoint → phase mapping:
| RTK endpoint name | Emitted phase |
| ----------------- | ------------- |
| authorizeFetch | authorize |
| authorizeIframe | authorize |
| exchange | exchange |
| revoke | revoke |
| userInfo | userinfo |
| endSession | logout |
Pass config.clientId to surface it in the extension's node detail card:
attachOidcBridge(client, { clientId: 'my-spa-client', ...rest });Low-level API
If you need to emit events from outside a supported client, use the primitives directly.
import { emitAuthEvent, emitConfigEvent, DEVTOOLS_EVENT_NAME } from '@wolfcola/devtools-bridge';
emitAuthEvent({
id: crypto.randomUUID(),
timestamp: performance.now(),
type: 'sdk:node-change',
source: 'sdk',
flowId: null,
causedBy: null,
data: { _tag: 'sdk', nodeStatus: 'next' },
flags: { isCors: false, isError: false, isAuthRelated: true },
});
emitConfigEvent({ clientId: 'my-app', environment: 'dev' });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.
How it works
Your app
├── attachDevToolsBridge(davinciClient) ─┐
├── attachJourneyBridge(journeyClient) ─┤─ emitAuthEvent()
└── attachOidcBridge(oidcClient) ─┘
│
│ window.dispatchEvent(new CustomEvent('pingDevtools', { detail: event }))
▼
content-script.js
│
│ chrome.runtime.sendMessage({ type: 'SDK_EVENT', payload: event })
▼
service-worker.ts ──(validates via AuthEventSchema)──▶ EventStore
│
│ chrome.runtime.sendMessage({ type: 'EVENTS_UPDATED' })
▼
panel (Elm) ── Timeline view + Flow view
── OR (VS Code extension) ──
Your app
└── emitAuthEvent()
│
│ window.postMessage({ type: '__pingDevtools', ... })
▼
CDP-injected script (Page.addScriptToEvaluateOnNewDocument)
│
│ Runtime.bindingCalled('__wolfcolaBridge', payload)
▼
VS Code extension host ──▶ TreeView + WebView (Elm)The VS Code extension captures SDK events via a CDP-injected script that listens for the same __pingDevtools postMessage — no browser extension needed.
Each bridge function:
- Subscribes to the client store
- Validates the current state with an Effect Schema decoder (returns
Option.noneon mismatch — never throws) - Deduplicates by tracking already-emitted request IDs in a
Set - Trims that
Setto only IDs still present in the store, bounding memory use - Dispatches the event only when
window.__PING_DEVTOOLS_EXTENSION__is present
Safety
- No-op without the extension — all bridges check for
window.__PING_DEVTOOLS_EXTENSION__before dispatching. If the marker is absent, nothing is emitted. - No-op in SSR / Node — all bridges return
{ detach: () => undefined }immediately whentypeof window === 'undefined'. - Tree-shakeable —
sideEffects: falseinpackage.json; unused bridges are eliminated by your bundler. - No sensitive data leakage — the bridge never reads passwords or form values; it only observes the client's Redux/RTK state.
