From 14916718908bdf3865a24863d391369c181fead9 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Wed, 6 May 2026 18:38:21 -0600 Subject: [PATCH 1/8] feat(devtools-types): scaffold shared AuthEvent schema and types Chrome extension for clients within the SDK ecosystem (javascript). Contains a bridge, extension, and types package. README.md documents feature set. chore: readme --- .changeset/config.json | 3 + .changeset/quiet-onions-study.md | 6 + .changeset/seven-hoops-win.md | 5 + .gitignore | 3 + .prototools | 5 + e2e/davinci-app/main.ts | 8 +- e2e/davinci-app/package.json | 1 + e2e/davinci-app/tsconfig.app.json | 3 + e2e/journey-app/main.ts | 2 + e2e/journey-app/package.json | 1 + e2e/journey-app/tsconfig.app.json | 7 +- e2e/oidc-app/package.json | 1 + e2e/oidc-app/src/utils/oidc-app.ts | 2 + e2e/oidc-app/tsconfig.app.json | 3 + eslint.config.mjs | 8 + .../api-report/davinci-client.api.md | 88 +- .../api-report/davinci-client.types.api.md | 88 +- .../davinci-client/src/lib/client.store.ts | 15 + packages/devtools-bridge/README.md | 198 +++ packages/devtools-bridge/eslint.config.mjs | 3 + packages/devtools-bridge/package.json | 48 + packages/devtools-bridge/src/index.ts | 13 + .../devtools-bridge/src/lib/bridge.test.ts | 426 ++++++ packages/devtools-bridge/src/lib/bridge.ts | 230 +++ packages/devtools-bridge/src/lib/emit.test.ts | 54 + packages/devtools-bridge/src/lib/emit.ts | 47 + .../src/lib/journey-bridge.test.ts | 375 +++++ .../devtools-bridge/src/lib/journey-bridge.ts | 186 +++ .../src/lib/oidc-bridge.test.ts | 312 ++++ .../devtools-bridge/src/lib/oidc-bridge.ts | 172 +++ packages/devtools-bridge/tsconfig.json | 10 + packages/devtools-bridge/tsconfig.lib.json | 29 + packages/devtools-bridge/tsconfig.spec.json | 19 + packages/devtools-bridge/vite.config.ts | 13 + packages/devtools-extension/README.md | 229 +++ packages/devtools-extension/elm-tooling.json | 5 + packages/devtools-extension/elm.json | 23 + packages/devtools-extension/eslint.config.mjs | 7 + packages/devtools-extension/manifest.json | 29 + packages/devtools-extension/package.json | 30 + packages/devtools-extension/project.json | 36 + .../src/background/diagnosis-engine.test.ts | 533 +++++++ .../src/background/diagnosis-engine.ts | 507 +++++++ .../background/event-store.service.test.ts | 119 ++ .../src/background/event-store.service.ts | 78 + .../src/background/message-handler.ts | 46 + .../src/background/service-worker.ts | 86 ++ .../src/content/content-script.test.ts | 22 + .../src/content/content-script.ts | 12 + .../devtools-extension/src/content/relay.ts | 9 + .../src/devtools/cors-detector.test.ts | 95 ++ .../src/devtools/cors-detector.ts | 36 + .../src/devtools/devtools.html | 9 + .../src/devtools/devtools.ts | 35 + .../src/devtools/network-observer.test.ts | 145 ++ .../src/devtools/network-observer.ts | 90 ++ .../src/export/markdown.test.ts | 154 ++ .../devtools-extension/src/export/markdown.ts | 96 ++ .../src/export/redact.test.ts | 214 +++ .../devtools-extension/src/export/redact.ts | 125 ++ .../devtools-extension/src/panel/Main.elm | 185 +++ .../devtools-extension/src/panel/panel.html | 1262 +++++++++++++++++ .../devtools-extension/src/panel/panel.ts | 395 ++++++ .../src/panel/src/Decode.elm | 230 +++ .../src/panel/src/FlowView.elm | 669 +++++++++ .../src/panel/src/Graph.elm | 235 +++ .../src/panel/src/Helpers.elm | 173 +++ .../src/panel/src/Inspector.elm | 596 ++++++++ .../src/panel/src/JsonTree.elm | 154 ++ .../src/panel/src/Model.elm | 58 + .../src/panel/src/Timeline.elm | 162 +++ .../src/panel/src/Types.elm | 185 +++ .../src/panel/src/Update.elm | 315 ++++ .../devtools-extension/src/panel/src/View.elm | 332 +++++ packages/devtools-extension/tsconfig.json | 10 + packages/devtools-extension/tsconfig.lib.json | 21 + .../devtools-extension/tsconfig.spec.json | 20 + packages/devtools-extension/vite.config.ts | 13 + packages/devtools-types/README.md | 237 ++++ packages/devtools-types/eslint.config.mjs | 3 + packages/devtools-types/package.json | 38 + packages/devtools-types/src/index.ts | 6 + .../src/lib/auth-event.schema.test.ts | 125 ++ .../src/lib/auth-event.schema.ts | 160 +++ .../src/lib/auth-event.types.ts | 24 + .../devtools-types/src/lib/cors-flag.types.ts | 2 + .../src/lib/flow-export.schema.test.ts | 80 ++ .../src/lib/flow-export.schema.ts | 11 + .../src/lib/flow-state.schema.ts | 21 + .../src/lib/flow-state.types.ts | 2 + packages/devtools-types/tsconfig.json | 12 + packages/devtools-types/tsconfig.lib.json | 21 + packages/devtools-types/tsconfig.spec.json | 20 + packages/devtools-types/vite.config.ts | 13 + .../api-report/journey-client.api.md | 4 + .../api-report/journey-client.types.api.md | 4 + .../journey-client/src/lib/client.store.ts | 4 + .../oidc-client/api-report/oidc-client.api.md | 44 +- .../api-report/oidc-client.types.api.md | 44 +- packages/oidc-client/src/lib/client.store.ts | 12 +- pnpm-lock.yaml | 463 +++++- tsconfig.json | 9 + 102 files changed, 11405 insertions(+), 123 deletions(-) create mode 100644 .changeset/quiet-onions-study.md create mode 100644 .changeset/seven-hoops-win.md create mode 100644 .prototools create mode 100644 packages/devtools-bridge/README.md create mode 100644 packages/devtools-bridge/eslint.config.mjs create mode 100644 packages/devtools-bridge/package.json create mode 100644 packages/devtools-bridge/src/index.ts create mode 100644 packages/devtools-bridge/src/lib/bridge.test.ts create mode 100644 packages/devtools-bridge/src/lib/bridge.ts create mode 100644 packages/devtools-bridge/src/lib/emit.test.ts create mode 100644 packages/devtools-bridge/src/lib/emit.ts create mode 100644 packages/devtools-bridge/src/lib/journey-bridge.test.ts create mode 100644 packages/devtools-bridge/src/lib/journey-bridge.ts create mode 100644 packages/devtools-bridge/src/lib/oidc-bridge.test.ts create mode 100644 packages/devtools-bridge/src/lib/oidc-bridge.ts create mode 100644 packages/devtools-bridge/tsconfig.json create mode 100644 packages/devtools-bridge/tsconfig.lib.json create mode 100644 packages/devtools-bridge/tsconfig.spec.json create mode 100644 packages/devtools-bridge/vite.config.ts create mode 100644 packages/devtools-extension/README.md create mode 100644 packages/devtools-extension/elm-tooling.json create mode 100644 packages/devtools-extension/elm.json create mode 100644 packages/devtools-extension/eslint.config.mjs create mode 100644 packages/devtools-extension/manifest.json create mode 100644 packages/devtools-extension/package.json create mode 100644 packages/devtools-extension/project.json create mode 100644 packages/devtools-extension/src/background/diagnosis-engine.test.ts create mode 100644 packages/devtools-extension/src/background/diagnosis-engine.ts create mode 100644 packages/devtools-extension/src/background/event-store.service.test.ts create mode 100644 packages/devtools-extension/src/background/event-store.service.ts create mode 100644 packages/devtools-extension/src/background/message-handler.ts create mode 100644 packages/devtools-extension/src/background/service-worker.ts create mode 100644 packages/devtools-extension/src/content/content-script.test.ts create mode 100644 packages/devtools-extension/src/content/content-script.ts create mode 100644 packages/devtools-extension/src/content/relay.ts create mode 100644 packages/devtools-extension/src/devtools/cors-detector.test.ts create mode 100644 packages/devtools-extension/src/devtools/cors-detector.ts create mode 100644 packages/devtools-extension/src/devtools/devtools.html create mode 100644 packages/devtools-extension/src/devtools/devtools.ts create mode 100644 packages/devtools-extension/src/devtools/network-observer.test.ts create mode 100644 packages/devtools-extension/src/devtools/network-observer.ts create mode 100644 packages/devtools-extension/src/export/markdown.test.ts create mode 100644 packages/devtools-extension/src/export/markdown.ts create mode 100644 packages/devtools-extension/src/export/redact.test.ts create mode 100644 packages/devtools-extension/src/export/redact.ts create mode 100644 packages/devtools-extension/src/panel/Main.elm create mode 100644 packages/devtools-extension/src/panel/panel.html create mode 100644 packages/devtools-extension/src/panel/panel.ts create mode 100644 packages/devtools-extension/src/panel/src/Decode.elm create mode 100644 packages/devtools-extension/src/panel/src/FlowView.elm create mode 100644 packages/devtools-extension/src/panel/src/Graph.elm create mode 100644 packages/devtools-extension/src/panel/src/Helpers.elm create mode 100644 packages/devtools-extension/src/panel/src/Inspector.elm create mode 100644 packages/devtools-extension/src/panel/src/JsonTree.elm create mode 100644 packages/devtools-extension/src/panel/src/Model.elm create mode 100644 packages/devtools-extension/src/panel/src/Timeline.elm create mode 100644 packages/devtools-extension/src/panel/src/Types.elm create mode 100644 packages/devtools-extension/src/panel/src/Update.elm create mode 100644 packages/devtools-extension/src/panel/src/View.elm create mode 100644 packages/devtools-extension/tsconfig.json create mode 100644 packages/devtools-extension/tsconfig.lib.json create mode 100644 packages/devtools-extension/tsconfig.spec.json create mode 100644 packages/devtools-extension/vite.config.ts create mode 100644 packages/devtools-types/README.md create mode 100644 packages/devtools-types/eslint.config.mjs create mode 100644 packages/devtools-types/package.json create mode 100644 packages/devtools-types/src/index.ts create mode 100644 packages/devtools-types/src/lib/auth-event.schema.test.ts create mode 100644 packages/devtools-types/src/lib/auth-event.schema.ts create mode 100644 packages/devtools-types/src/lib/auth-event.types.ts create mode 100644 packages/devtools-types/src/lib/cors-flag.types.ts create mode 100644 packages/devtools-types/src/lib/flow-export.schema.test.ts create mode 100644 packages/devtools-types/src/lib/flow-export.schema.ts create mode 100644 packages/devtools-types/src/lib/flow-state.schema.ts create mode 100644 packages/devtools-types/src/lib/flow-state.types.ts create mode 100644 packages/devtools-types/tsconfig.json create mode 100644 packages/devtools-types/tsconfig.lib.json create mode 100644 packages/devtools-types/tsconfig.spec.json create mode 100644 packages/devtools-types/vite.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index eeab345fdc..d9f44e5375 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -14,6 +14,9 @@ "updateInternalDependencies": "patch", "ignore": [ "scratchpad", + "@forgerock/devtools-extension", + "@forgerock/devtools-bridge", + "@forgerock/devtools-types", "@forgerock/pingone-scripts", "@forgerock/device-client-app", "@forgerock/davinci-app", diff --git a/.changeset/quiet-onions-study.md b/.changeset/quiet-onions-study.md new file mode 100644 index 0000000000..7c5df1f6e7 --- /dev/null +++ b/.changeset/quiet-onions-study.md @@ -0,0 +1,6 @@ +--- +'@forgerock/journey-client': minor +'@forgerock/oidc-client': minor +--- + +Adds subscribe method to public api diff --git a/.changeset/seven-hoops-win.md b/.changeset/seven-hoops-win.md new file mode 100644 index 0000000000..26c65248e1 --- /dev/null +++ b/.changeset/seven-hoops-win.md @@ -0,0 +1,5 @@ +--- +'@forgerock/davinci-client': minor +--- + +Adds a get cache method to expose the cache for consumers like devtools diff --git a/.gitignore b/.gitignore index 327a931c1a..922c4ca6c1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ .pnpm-store/* .npm/_logs +# Elm +elm-stuff/ + # Generated code logs/* tmp/ diff --git a/.prototools b/.prototools new file mode 100644 index 0000000000..adf7bdaa9d --- /dev/null +++ b/.prototools @@ -0,0 +1,5 @@ +node = "22" +pnpm = "10" + +[settings] +auto-install=true diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index 119a4dec6e..ed9df0b86a 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -8,6 +8,7 @@ import './style.css'; import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk'; import { davinci } from '@forgerock/davinci-client'; +import { attachDevToolsBridge } from '@forgerock/devtools-bridge'; import type { CustomLogger, DaVinciConfig, @@ -334,16 +335,13 @@ const urlParams = new URLSearchParams(window.location.search); } } - /** - * Optionally subscribe to the store to listen for all store updates - * This is useful for debugging and logging - * It returns an unsubscribe function that you can call to stop listening - */ davinciClient.subscribe(() => { const node = davinciClient.getNode(); console.log('Event emitted from store:', node); }); + attachDevToolsBridge(davinciClient, config); + const qs = window.location.search; const searchParams = new URLSearchParams(qs); diff --git a/e2e/davinci-app/package.json b/e2e/davinci-app/package.json index 4dfc5cee73..61581ff106 100644 --- a/e2e/davinci-app/package.json +++ b/e2e/davinci-app/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@forgerock/davinci-client": "workspace:*", + "@forgerock/devtools-bridge": "workspace:*", "@forgerock/javascript-sdk": "4.7.0", "@forgerock/protect": "workspace:*", "@forgerock/sdk-logger": "workspace:*" diff --git a/e2e/davinci-app/tsconfig.app.json b/e2e/davinci-app/tsconfig.app.json index 9027cd9d88..80e86dfaff 100644 --- a/e2e/davinci-app/tsconfig.app.json +++ b/e2e/davinci-app/tsconfig.app.json @@ -18,6 +18,9 @@ { "path": "../../packages/protect/tsconfig.lib.json" }, + { + "path": "../../packages/devtools-bridge/tsconfig.lib.json" + }, { "path": "../../packages/davinci-client/tsconfig.lib.json" } diff --git a/e2e/journey-app/main.ts b/e2e/journey-app/main.ts index 3b61558b4d..db50c2358f 100644 --- a/e2e/journey-app/main.ts +++ b/e2e/journey-app/main.ts @@ -7,6 +7,7 @@ import './style.css'; import { journey } from '@forgerock/journey-client'; +import { attachJourneyBridge } from '@forgerock/devtools-bridge'; import type { JourneyClient, RequestMiddleware } from '@forgerock/journey-client/types'; @@ -65,6 +66,7 @@ if (searchParams.get('middleware') === 'true') { let journeyClient: JourneyClient; try { journeyClient = await journey({ config: config, requestMiddleware }); + attachJourneyBridge(journeyClient, config); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; console.error('Failed to initialize journey client:', message); diff --git a/e2e/journey-app/package.json b/e2e/journey-app/package.json index 7cc9e67485..921c91bf47 100644 --- a/e2e/journey-app/package.json +++ b/e2e/journey-app/package.json @@ -11,6 +11,7 @@ "serve": "pnpm nx nxServe" }, "dependencies": { + "@forgerock/devtools-bridge": "workspace:*", "@forgerock/journey-client": "workspace:*", "@forgerock/oidc-client": "workspace:*", "@forgerock/protect": "workspace:*", diff --git a/e2e/journey-app/tsconfig.app.json b/e2e/journey-app/tsconfig.app.json index 417f5a64c2..8bf226a28a 100644 --- a/e2e/journey-app/tsconfig.app.json +++ b/e2e/journey-app/tsconfig.app.json @@ -15,14 +15,17 @@ ], "references": [ { - "path": "../../packages/sdk-effects/logger/tsconfig.lib.json" + "path": "../../packages/device-client/tsconfig.lib.json" }, { - "path": "../../packages/device-client/tsconfig.lib.json" + "path": "../../packages/sdk-effects/logger/tsconfig.lib.json" }, { "path": "../../packages/oidc-client/tsconfig.lib.json" }, + { + "path": "../../packages/devtools-bridge/tsconfig.lib.json" + }, { "path": "../../packages/protect/tsconfig.lib.json" }, diff --git a/e2e/oidc-app/package.json b/e2e/oidc-app/package.json index 5845594e28..8b388fa100 100644 --- a/e2e/oidc-app/package.json +++ b/e2e/oidc-app/package.json @@ -9,6 +9,7 @@ "serve": "pnpm nx nxServe" }, "dependencies": { + "@forgerock/devtools-bridge": "workspace:*", "@forgerock/oidc-client": "workspace:*" }, "nx": { diff --git a/e2e/oidc-app/src/utils/oidc-app.ts b/e2e/oidc-app/src/utils/oidc-app.ts index 69289580a0..70e5902980 100644 --- a/e2e/oidc-app/src/utils/oidc-app.ts +++ b/e2e/oidc-app/src/utils/oidc-app.ts @@ -6,6 +6,7 @@ * of the MIT license. See the LICENSE file for details. * */ +import { attachOidcBridge } from '@forgerock/devtools-bridge'; import { oidc } from '@forgerock/oidc-client'; import type { AuthorizationError, @@ -54,6 +55,7 @@ export async function oidcApp({ config, urlParams }) { if ('error' in oidcClient) { displayError(oidcClient); } + attachOidcBridge(oidcClient, config); document.getElementById('login-background').addEventListener('click', async () => { const authorizeOptions: GetAuthorizationUrlOptions = diff --git a/e2e/oidc-app/tsconfig.app.json b/e2e/oidc-app/tsconfig.app.json index d15af865e3..56e92d4814 100644 --- a/e2e/oidc-app/tsconfig.app.json +++ b/e2e/oidc-app/tsconfig.app.json @@ -21,6 +21,9 @@ "references": [ { "path": "../../packages/oidc-client/tsconfig.lib.json" + }, + { + "path": "../../packages/devtools-bridge/tsconfig.lib.json" } ] } diff --git a/eslint.config.mjs b/eslint.config.mjs index b38bd77ab9..89caa94813 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -109,6 +109,14 @@ export default [ sourceTag: 'scope:sdk-types', onlyDependOnLibsWithTags: [], }, + { + sourceTag: 'scope:devtools-types', + onlyDependOnLibsWithTags: [], + }, + { + sourceTag: 'scope:devtools-bridge', + onlyDependOnLibsWithTags: ['scope:devtools-types', 'scope:package'], + }, ], }, ], diff --git a/packages/davinci-client/api-report/davinci-client.api.md b/packages/davinci-client/api-report/davinci-client.api.md index b2528bf664..e73693581b 100644 --- a/packages/davinci-client/api-report/davinci-client.api.md +++ b/packages/davinci-client/api-report/davinci-client.api.md @@ -178,7 +178,7 @@ export interface CollectorErrors { } // @public (undocumented) -export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector; +export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector; // @public export type CollectorValueType = T extends { @@ -212,7 +212,7 @@ export type CollectorValueType = T extends { } ? string[] : string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; // @public (undocumented) -export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | FidoRegistrationField | FidoAuthenticationField | PollingField; +export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField; // @public (undocumented) export interface ContinueNode { @@ -267,13 +267,11 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; - poll: (collector: PollingCollector) => Poller; + pollStatus: (collector: PollingCollector) => Poller; getClient: () => { - status: "start"; - } | { action: string; collectors: Collectors[]; description?: string; @@ -287,6 +285,8 @@ export function davinci(input: { status: "error"; } | { status: "failure"; + } | { + status: "start"; } | { authorization?: { code?: string; @@ -297,7 +297,7 @@ export function davinci(input: { getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; getServer: () => { _links?: Links; id?: string; @@ -306,8 +306,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -323,6 +321,8 @@ export function davinci(input: { interactionId?: string; interactionToken?: string; status: "failure"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; @@ -524,6 +524,7 @@ export function davinci(input: { type: string; }; }; + getCache: (requestId: string) => unknown; }; }>; @@ -1035,7 +1036,7 @@ export type InferNoValueCollectorType = T exten export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; // @public (undocumented) -export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; +export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; // @public (undocumented) export type InitFlow = () => Promise; @@ -1170,8 +1171,8 @@ value: Record; }, string>; // @public -export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { - getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; +export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { + getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; }; // @public (undocumented) @@ -1283,10 +1284,10 @@ export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRe export type ObjectValueCollector = ObjectOptionsCollectorWithObjectValue | ObjectOptionsCollectorWithStringValue | ObjectValueCollectorWithObjectValue; // @public (undocumented) -export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; +export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; // @public -export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; +export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; // @public (undocumented) export interface ObjectValueCollectorWithObjectValue, OV = Record> { @@ -1328,13 +1329,68 @@ export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'> // @public (undocumented) export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>; +// @public (undocumented) +export interface PhoneNumberExtensionCollector { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: PhoneNumberExtensionInputValue; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + extensionLabel: string; + value: PhoneNumberExtensionOutputValue; + }; + // (undocumented) + type: 'PhoneNumberExtensionCollector'; +} + +// @public (undocumented) +export type PhoneNumberExtensionField = PhoneNumberField & { + showExtension: boolean; + extensionLabel: string; +}; + +// @public (undocumented) +export interface PhoneNumberExtensionInputValue { + // (undocumented) + countryCode: string; + // (undocumented) + extension: string; + // (undocumented) + phoneNumber: string; +} + +// @public (undocumented) +export interface PhoneNumberExtensionOutputValue { + // (undocumented) + countryCode?: string; + // (undocumented) + extension?: string; + // (undocumented) + phoneNumber?: string; +} + // @public (undocumented) export type PhoneNumberField = { type: 'PHONE_NUMBER'; key: string; label: string; - defaultCountryCode: string | null; required: boolean; + defaultCountryCode: string | null; validatePhoneNumber: boolean; }; @@ -1724,7 +1780,7 @@ export type UnknownField = Record; // @public (undocumented) export const updateCollectorValues: ActionCreatorWithPayload< { id: string; -value: string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; +value: string | string[] | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; index?: number; }, string>; diff --git a/packages/davinci-client/api-report/davinci-client.types.api.md b/packages/davinci-client/api-report/davinci-client.types.api.md index 2321431a0a..07fb72dd81 100644 --- a/packages/davinci-client/api-report/davinci-client.types.api.md +++ b/packages/davinci-client/api-report/davinci-client.types.api.md @@ -178,7 +178,7 @@ export interface CollectorErrors { } // @public (undocumented) -export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector; +export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | AgreementCollector | UnknownCollector; // @public export type CollectorValueType = T extends { @@ -212,7 +212,7 @@ export type CollectorValueType = T extends { } ? string[] : string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; // @public (undocumented) -export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | FidoRegistrationField | FidoAuthenticationField | PollingField; +export type ComplexValueFields = DeviceAuthenticationField | DeviceRegistrationField | PhoneNumberField | PhoneNumberExtensionField | FidoRegistrationField | FidoAuthenticationField | PollingField; // @public (undocumented) export interface ContinueNode { @@ -267,13 +267,11 @@ export function davinci(input: { resume: (input: { continueToken: string; }) => Promise; - start: (options?: StartOptions | undefined) => Promise; + start: (options?: StartOptions | undefined) => Promise; update: (collector: T) => Updater; validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator; - poll: (collector: PollingCollector) => Poller; + pollStatus: (collector: PollingCollector) => Poller; getClient: () => { - status: "start"; - } | { action: string; collectors: Collectors[]; description?: string; @@ -287,6 +285,8 @@ export function davinci(input: { status: "error"; } | { status: "failure"; + } | { + status: "start"; } | { authorization?: { code?: string; @@ -297,7 +297,7 @@ export function davinci(input: { getCollectors: () => Collectors[]; getError: () => DaVinciError | null; getErrorCollectors: () => CollectorErrors[]; - getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode; + getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode; getServer: () => { _links?: Links; id?: string; @@ -306,8 +306,6 @@ export function davinci(input: { href?: string; eventName?: string; status: "continue"; - } | { - status: "start"; } | { _links?: Links; eventName?: string; @@ -323,6 +321,8 @@ export function davinci(input: { interactionId?: string; interactionToken?: string; status: "failure"; + } | { + status: "start"; } | { _links?: Links; eventName?: string; @@ -524,6 +524,7 @@ export function davinci(input: { type: string; }; }; + getCache: (requestId: string) => unknown; }; }>; @@ -1032,7 +1033,7 @@ export type InferNoValueCollectorType = T exten export type InferSingleValueCollectorType = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>; // @public (undocumented) -export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; +export type InferValueObjectCollectorType = T extends 'DeviceAuthenticationCollector' ? DeviceAuthenticationCollector : T extends 'DeviceRegistrationCollector' ? DeviceRegistrationCollector : T extends 'PhoneNumberCollector' ? PhoneNumberCollector : T extends 'PhoneNumberExtensionCollector' ? PhoneNumberExtensionCollector : ObjectOptionsCollectorWithObjectValue<'ObjectValueCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectValueCollector'>; // @public (undocumented) export type InitFlow = () => Promise; @@ -1167,8 +1168,8 @@ value: Record; }, string>; // @public -export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { - getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; +export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & { + getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | PhoneNumberExtensionCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | AgreementCollector | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]; }; // @public (undocumented) @@ -1280,10 +1281,10 @@ export type ObjectValueAutoCollectorTypes = 'ObjectValueAutoCollector' | 'FidoRe export type ObjectValueCollector = ObjectOptionsCollectorWithObjectValue | ObjectOptionsCollectorWithStringValue | ObjectValueCollectorWithObjectValue; // @public (undocumented) -export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; +export type ObjectValueCollectors = DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ObjectOptionsCollectorWithObjectValue<'ObjectSelectCollector'> | ObjectOptionsCollectorWithStringValue<'ObjectSelectCollector'>; // @public -export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; +export type ObjectValueCollectorTypes = 'DeviceAuthenticationCollector' | 'DeviceRegistrationCollector' | 'PhoneNumberCollector' | 'PhoneNumberExtensionCollector' | 'ObjectOptionsCollector' | 'ObjectValueCollector' | 'ObjectSelectCollector'; // @public (undocumented) export interface ObjectValueCollectorWithObjectValue, OV = Record> { @@ -1325,13 +1326,68 @@ export type PasswordCollector = SingleValueCollectorNoValue<'PasswordCollector'> // @public (undocumented) export type PhoneNumberCollector = ObjectValueCollectorWithObjectValue<'PhoneNumberCollector', PhoneNumberInputValue, PhoneNumberOutputValue>; +// @public (undocumented) +export interface PhoneNumberExtensionCollector { + // (undocumented) + category: 'ObjectValueCollector'; + // (undocumented) + error: string | null; + // (undocumented) + id: string; + // (undocumented) + input: { + key: string; + value: PhoneNumberExtensionInputValue; + type: string; + validation: (ValidationRequired | ValidationPhoneNumber)[] | null; + }; + // (undocumented) + name: string; + // (undocumented) + output: { + key: string; + label: string; + type: string; + extensionLabel: string; + value: PhoneNumberExtensionOutputValue; + }; + // (undocumented) + type: 'PhoneNumberExtensionCollector'; +} + +// @public (undocumented) +export type PhoneNumberExtensionField = PhoneNumberField & { + showExtension: boolean; + extensionLabel: string; +}; + +// @public (undocumented) +export interface PhoneNumberExtensionInputValue { + // (undocumented) + countryCode: string; + // (undocumented) + extension: string; + // (undocumented) + phoneNumber: string; +} + +// @public (undocumented) +export interface PhoneNumberExtensionOutputValue { + // (undocumented) + countryCode?: string; + // (undocumented) + extension?: string; + // (undocumented) + phoneNumber?: string; +} + // @public (undocumented) export type PhoneNumberField = { type: 'PHONE_NUMBER'; key: string; label: string; - defaultCountryCode: string | null; required: boolean; + defaultCountryCode: string | null; validatePhoneNumber: boolean; }; @@ -1721,7 +1777,7 @@ export type UnknownField = Record; // @public (undocumented) export const updateCollectorValues: ActionCreatorWithPayload< { id: string; -value: string | string[] | PhoneNumberInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; +value: string | string[] | PhoneNumberInputValue | PhoneNumberExtensionInputValue | FidoRegistrationInputValue | FidoAuthenticationInputValue; index?: number; }, string>; diff --git a/packages/davinci-client/src/lib/client.store.ts b/packages/davinci-client/src/lib/client.store.ts index e99dc64018..d4281c6176 100644 --- a/packages/davinci-client/src/lib/client.store.ts +++ b/packages/davinci-client/src/lib/client.store.ts @@ -535,6 +535,21 @@ export async function davinci({ return flowItem || nextItem || startItem; }, + /** + * Returns the raw cached response data for a given requestId (cache key). + * Checks all three endpoints (flow, next, start) and returns the first with data. + */ + getCache: (requestId: string): unknown => { + if (!requestId) return undefined; + const state = store.getState(); + const flow = davinciApi.endpoints.flow.select(requestId)(state); + if (flow?.data !== undefined) return flow.data; + const next = davinciApi.endpoints.next.select(requestId)(state); + if (next?.data !== undefined) return next.data; + const start = davinciApi.endpoints.start.select(requestId)(state); + if (start?.data !== undefined) return start.data; + return undefined; + }, }, }; } diff --git a/packages/devtools-bridge/README.md b/packages/devtools-bridge/README.md new file mode 100644 index 0000000000..fa7920e516 --- /dev/null +++ b/packages/devtools-bridge/README.md @@ -0,0 +1,198 @@ +# @forgerock/devtools-bridge + +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. + +## Contents + +- [Installation](#installation) +- [Bridges](#bridges) + - [DaVinci — `attachDevToolsBridge`](#davinci--attachdevtoolsbridge) + - [AM Journey — `attachJourneyBridge`](#am-journey--attachjourneybridge) + - [OIDC / OAuth — `attachOidcBridge`](#oidc--oauth--attachoidcbridge) +- [Low-level API](#low-level-api) +- [How it works](#how-it-works) +- [Safety](#safety) + +--- + +## Installation + +```bash +pnpm add @forgerock/devtools-bridge +``` + +`effect` 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. + +```ts +import { davinci } from '@forgerock/davinci-client'; +import { attachDevToolsBridge } from '@forgerock/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. + +```ts +import { journey } from '@forgerock/journey-client'; // your RTK-based journey client +import { attachJourneyBridge } from '@forgerock/devtools-bridge'; + +const client = await journey({ config }); + +attachJourneyBridge(client, config); +``` + +**`JourneySubscribable` interface** — any object with this shape works: + +```ts +interface JourneySubscribable { + subscribe: (listener: () => void) => () => void; + getState: () => unknown; // must expose { journeyReducer: { mutations: Record } } +} +``` + +**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. + +```ts +import { oidcClient } from '@forgerock/oidc-client'; // your RTK-based OIDC client +import { attachOidcBridge } from '@forgerock/devtools-bridge'; + +const client = oidcClient({ config }); + +attachOidcBridge(client, config); +``` + +**`OidcSubscribable` interface:** + +```ts +interface OidcSubscribable { + subscribe: (listener: () => void) => () => void; + getState: () => unknown; // must expose { oidc: { mutations: Record } } +} +``` + +**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: + +```ts +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. + +```ts +import { emitAuthEvent, emitConfigEvent, DEVTOOLS_EVENT_NAME } from '@forgerock/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 +``` + +Each bridge function: + +1. Subscribes to the client store +2. Validates the current state with an Effect Schema decoder (returns `Option.none` on mismatch — never throws) +3. Deduplicates by tracking already-emitted request IDs in a `Set` +4. Trims that `Set` to only IDs still present in the store, bounding memory use +5. 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 when `typeof window === 'undefined'`. +- **Tree-shakeable** — `sideEffects: false` in `package.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. diff --git a/packages/devtools-bridge/eslint.config.mjs b/packages/devtools-bridge/eslint.config.mjs new file mode 100644 index 0000000000..cec2c4bf81 --- /dev/null +++ b/packages/devtools-bridge/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [{ ignores: ['**/dist'] }, ...baseConfig, { files: ['**/*.ts'], rules: {} }]; diff --git a/packages/devtools-bridge/package.json b/packages/devtools-bridge/package.json new file mode 100644 index 0000000000..a9a4e0cca5 --- /dev/null +++ b/packages/devtools-bridge/package.json @@ -0,0 +1,48 @@ +{ + "name": "@forgerock/devtools-bridge", + "version": "2.0.0", + "private": true, + "description": "Opt-in Ping SDK adapter that emits AuthEvents to the DevTools extension", + "license": "MIT", + "author": "ForgeRock", + "repository": { + "type": "git", + "url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git", + "directory": "packages/devtools-bridge" + }, + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "pnpm nx nxBuild", + "lint": "pnpm nx nxLint", + "test": "pnpm nx nxTest" + }, + "dependencies": { + "@forgerock/devtools-types": "workspace:*", + "effect": "catalog:effect" + }, + "devDependencies": { + "@forgerock/davinci-client": "workspace:*" + }, + "peerDependencies": { + "@forgerock/davinci-client": "workspace:*" + }, + "peerDependenciesMeta": { + "@forgerock/davinci-client": { "optional": true } + }, + "nx": { + "tags": ["scope:devtools-bridge"] + } +} diff --git a/packages/devtools-bridge/src/index.ts b/packages/devtools-bridge/src/index.ts new file mode 100644 index 0000000000..f63fb5f5de --- /dev/null +++ b/packages/devtools-bridge/src/index.ts @@ -0,0 +1,13 @@ +export { attachDevToolsBridge } from './lib/bridge.js'; +export type { BridgeHandle } from './lib/bridge.js'; +export { attachJourneyBridge } from './lib/journey-bridge.js'; +export type { JourneyBridgeHandle } from './lib/journey-bridge.js'; +export { attachOidcBridge } from './lib/oidc-bridge.js'; +export type { OidcBridgeHandle } from './lib/oidc-bridge.js'; +export { + DEVTOOLS_EVENT_NAME, + emitAuthEvent, + emitConfigEvent, + configureDevtools, +} from './lib/emit.js'; +export type { DevtoolsOptions } from './lib/emit.js'; diff --git a/packages/devtools-bridge/src/lib/bridge.test.ts b/packages/devtools-bridge/src/lib/bridge.test.ts new file mode 100644 index 0000000000..01f18a4bf0 --- /dev/null +++ b/packages/devtools-bridge/src/lib/bridge.test.ts @@ -0,0 +1,426 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { attachDevToolsBridge, nodeToSdkData } from './bridge.js'; +import { DEVTOOLS_EVENT_NAME } from './emit.js'; +import type { AuthEvent } from '@forgerock/devtools-types'; + +// --------------------------------------------------------------------------- +// nodeToSdkData unit tests (pure — no DOM, no events) +// --------------------------------------------------------------------------- + +describe('nodeToSdkData', () => { + it('maps a minimal node with only status', () => { + const result = nodeToSdkData({ status: 'start' }, undefined); + expect(result).toEqual({ + _tag: 'sdk', + nodeStatus: 'start', + previousStatus: undefined, + }); + }); + + it('uses "unknown" when status is absent', () => { + const result = nodeToSdkData({}, undefined); + expect(result.nodeStatus).toBe('unknown'); + }); + + it('carries previousStatus through', () => { + const result = nodeToSdkData({ status: 'continue' }, 'start'); + expect(result.previousStatus).toBe('start'); + }); + + it('maps nested server fields', () => { + const result = nodeToSdkData( + { + status: 'continue', + server: { + interactionId: 'iid-1', + interactionToken: 'tok-1', + id: 'node-id-1', + eventName: 'LoginNode', + session: 'sess-1', + }, + }, + undefined, + ); + expect(result.interactionId).toBe('iid-1'); + expect(result.interactionToken).toBe('tok-1'); + expect(result.nodeId).toBe('node-id-1'); + expect(result.eventName).toBe('LoginNode'); + expect(result.session).toBe('sess-1'); + }); + + it('maps nested client fields', () => { + const result = nodeToSdkData( + { + status: 'continue', + client: { + name: 'UsernameNode', + description: 'Enter username', + collectors: [{ type: 'TextCollector' }], + authorization: { code: 'auth-code', state: 'state-1' }, + }, + }, + undefined, + ); + expect(result.nodeName).toBe('UsernameNode'); + expect(result.nodeDescription).toBe('Enter username'); + expect(result.collectors).toEqual([{ type: 'TextCollector' }]); + expect(result.authorization).toEqual({ code: 'auth-code', state: 'state-1' }); + }); + + it('maps error and cache fields', () => { + const result = nodeToSdkData( + { + status: 'error', + httpStatus: 401, + error: { code: 'UNAUTHORIZED', message: 'Bad creds', type: 'auth' }, + cache: { key: 'req-key-1' }, + }, + 'continue', + ); + expect(result.httpStatus).toBe(401); + expect(result.error).toEqual({ code: 'UNAUTHORIZED', message: 'Bad creds', type: 'auth' }); + expect(result.requestId).toBe('req-key-1'); + }); + + it('ignores unrecognised fields — does not bleed unknown keys', () => { + const result = nodeToSdkData( + { status: 'start', someUnknownField: 'x', server: { unknownServerKey: 99 } } as never, + undefined, + ); + expect(result).not.toHaveProperty('someUnknownField'); + expect(result).not.toHaveProperty('unknownServerKey'); + }); + + it('coerces null error to undefined (success/continue nodes set error: null)', () => { + const result = nodeToSdkData({ status: 'continue', error: null } as never, undefined); + expect(result.error).toBeUndefined(); + }); + + it('coerces null cache to undefined requestId', () => { + const result = nodeToSdkData({ status: 'continue', cache: null } as never, undefined); + expect(result.requestId).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// Mock client factory +// --------------------------------------------------------------------------- + +function makeClient(initialNode: Record) { + let listener: (() => void) | null = null; + let node = initialNode; + return { + subscribe: vi.fn((cb: () => void) => { + listener = cb; + return () => { + listener = null; + }; + }), + getNode: vi.fn(() => node), + /** Test helper: update internal node and fire the subscribed listener. */ + trigger: (newNode: Record) => { + node = newNode; + listener?.(); + }, + }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function captureDevtoolsEvents(): { events: CustomEvent[]; stop: () => void } { + const events: CustomEvent[] = []; + const handler = (e: Event) => events.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + return { events, stop: () => window.removeEventListener(DEVTOOLS_EVENT_NAME, handler) }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('attachDevToolsBridge', () => { + beforeEach(() => { + // Simulate extension presence for all tests except the no-op test. + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(async () => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + // Flush any deferred setTimeout(0) callbacks so they don't bleed into later tests. + await new Promise((r) => setTimeout(r, 10)); + }); + + it('returns a BridgeHandle with a detach function', () => { + const client = makeClient({ status: 'start' }); + const handle = attachDevToolsBridge(client); + + expect(handle).toHaveProperty('detach'); + expect(typeof handle.detach).toBe('function'); + + handle.detach(); + }); + + it('emits sdk:node-change when node status transitions (start → continue)', () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + + // Trigger a status transition. + client.trigger({ status: 'continue' }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + expect(events[0].detail.type).toBe('sdk:node-change'); + expect(events[0].detail.data._tag).toBe('sdk'); + }); + + it('emits events when node has error: null and cache: null (real DaVinci ContinueNode shape)', () => { + // DaVinci reducers set error: null on ContinueNode and SuccessNode. + // Schema.optional(SdkErrorSchema) rejects null — this test guards that regression. + const continueNode = { + status: 'continue', + httpStatus: 200, + error: null, + cache: null, + server: { + interactionId: 'iid-abc', + interactionToken: 'tok-xyz', + id: 'node-1', + eventName: 'UsernameNode', + }, + client: { + name: 'Sign In', + description: 'Enter your username', + collectors: [{ type: 'TextCollector', id: 'username-0' }], + }, + }; + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + client.trigger(continueNode); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + expect(events[0].detail.type).toBe('sdk:node-change'); + const data = events[0].detail.data as { + _tag: string; + nodeStatus: string; + interactionId?: string; + nodeName?: string; + error?: unknown; + }; + expect(data._tag).toBe('sdk'); + expect(data.nodeStatus).toBe('continue'); + expect(data.interactionId).toBe('iid-abc'); + expect(data.nodeName).toBe('Sign In'); + expect(data.error).toBeUndefined(); + }); + + it('does NOT emit when status has not changed', () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + + // First trigger sets previousStatus = 'start'. + client.trigger({ status: 'start' }); + // Second trigger with the same status — should be suppressed. + client.trigger({ status: 'start' }); + + handle.detach(); + stop(); + + // The first trigger fires because previousStatus was undefined → 'start'. + // The second trigger must be suppressed because status did not change. + expect(events).toHaveLength(1); + expect(events[0].detail.type).toBe('sdk:node-change'); + expect(events[0].detail.data._tag).toBe('sdk'); + }); + + it('detach() unsubscribes the listener', () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + + // Verify subscribe was wired up. + expect(client.subscribe).toHaveBeenCalledTimes(1); + + handle.detach(); + + // Fire after detach — should produce no new events. + client.trigger({ status: 'continue' }); + + stop(); + + expect(events).toHaveLength(0); + }); + + it('emits sdk:config event on first transition when config is provided', () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client, { + clientId: 'my-app', + redirectUri: 'https://app.example.com/callback', + }); + + client.trigger({ status: 'continue' }); + + handle.detach(); + stop(); + + // First event should be sdk:config, second should be sdk:node-change + expect(events).toHaveLength(2); + expect(events[0].detail.type).toBe('sdk:config'); + expect(events[0].detail.data._tag).toBe('sdk-config'); + expect((events[0].detail.data as { _tag: string; config: unknown }).config).toEqual({ + clientId: 'my-app', + redirectUri: 'https://app.example.com/callback', + }); + expect(events[1].detail.type).toBe('sdk:node-change'); + }); + + it('emits sdk:config only once across multiple transitions', () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client, { clientId: 'my-app' }); + + client.trigger({ status: 'continue' }); + client.trigger({ status: 'success' }); + + handle.detach(); + stop(); + + const configEvents = events.filter((e) => e.detail.type === 'sdk:config'); + expect(configEvents).toHaveLength(1); + }); + + it('does not emit sdk:config when no config is provided', () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + + client.trigger({ status: 'continue' }); + + handle.detach(); + stop(); + + const configEvents = events.filter((e) => e.detail.type === 'sdk:config'); + expect(configEvents).toHaveLength(0); + }); + + it('is a no-op when __PING_DEVTOOLS_EXTENSION__ is absent', () => { + // Remove extension flag to exercise the guard branch. + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + + // subscribe is called (the bridge still subscribes), but no events should be dispatched. + expect(client.subscribe).toHaveBeenCalledTimes(1); + + client.trigger({ status: 'continue' }); + + handle.detach(); // should not throw + stop(); + + expect(events).toHaveLength(0); + }); + + it('does not emit sdk:config when extension is absent even if config is provided', () => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client, { clientId: 'my-app' }); + client.trigger({ status: 'continue' }); + + handle.detach(); + stop(); + + // Re-add for cleanup + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + + const configEvents = events.filter((e) => e.detail.type === 'sdk:config'); + expect(configEvents).toHaveLength(0); + }); +}); + +describe('attachDevToolsBridge session tracking', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + localStorage.clear(); + // Reset cookie (jsdom allows setting document.cookie) + Object.defineProperty(document, 'cookie', { + writable: true, + value: '', + }); + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + localStorage.clear(); + }); + + it('emits session:storage event when localStorage changes after a node transition', async () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + + // Trigger a node transition, then mutate storage in the same tick + client.trigger({ status: 'continue' }); + localStorage.setItem('ping:session', 'abc123'); + + // Wait for setTimeout(0) deferred diff + await new Promise((r) => setTimeout(r, 10)); + + handle.detach(); + stop(); + + const sessionEvents = events.filter((e) => e.detail.type === 'session:storage'); + expect(sessionEvents).toHaveLength(1); + expect(sessionEvents[0].detail.data._tag).toBe('session'); + const data = sessionEvents[0].detail.data as { + _tag: string; + key: string; + before?: string; + after?: string; + }; + expect(data.key).toBe('ping:session'); + expect(data.before).toBeUndefined(); + expect(data.after).toBe('abc123'); + }); + + it('does not emit session events when nothing changes', async () => { + const client = makeClient({ status: 'start' }); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachDevToolsBridge(client); + client.trigger({ status: 'continue' }); + + await new Promise((r) => setTimeout(r, 10)); + + handle.detach(); + stop(); + + const sessionEvents = events.filter( + (e) => e.detail.type === 'session:storage' || e.detail.type === 'session:cookie', + ); + expect(sessionEvents).toHaveLength(0); + }); +}); diff --git a/packages/devtools-bridge/src/lib/bridge.ts b/packages/devtools-bridge/src/lib/bridge.ts new file mode 100644 index 0000000000..a52c9e6756 --- /dev/null +++ b/packages/devtools-bridge/src/lib/bridge.ts @@ -0,0 +1,230 @@ +import { Schema, Option, pipe } from 'effect'; +import type { SdkData } from '@forgerock/devtools-types'; +import { SdkErrorSchema, SdkAuthorizationSchema } from '@forgerock/devtools-types'; +import { emitAuthEvent, emitConfigEvent, configureDevtools } from './emit.js'; +import type { DevtoolsOptions } from './emit.js'; + +interface Subscribable { + subscribe: (listener: () => void) => () => void; + getNode: () => unknown; + cache?: { + getCache: (requestId: string) => unknown; + }; +} + +export interface BridgeHandle { + detach: () => void; +} + +export interface SdkConfig { + clientId?: string; + redirectUri?: string; + scope?: string; + serverConfig?: unknown; +} + +// --------------------------------------------------------------------------- +// DaVinci node schema — local structural contract, not a public type +// --------------------------------------------------------------------------- + +const DaVinciNodeSchema = Schema.Struct({ + status: Schema.optional(Schema.String), + httpStatus: Schema.optional(Schema.Number), + server: Schema.optional( + Schema.Struct({ + interactionId: Schema.optional(Schema.String), + interactionToken: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + eventName: Schema.optional(Schema.String), + session: Schema.optional(Schema.String), + }), + ), + client: Schema.optional( + Schema.Struct({ + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + collectors: Schema.optional(Schema.Array(Schema.Unknown)), + authorization: Schema.optional(SdkAuthorizationSchema), + }), + ), + error: Schema.optional(Schema.NullOr(SdkErrorSchema)), + cache: Schema.optional(Schema.NullOr(Schema.Struct({ key: Schema.optional(Schema.String) }))), +}); + +type DaVinciNode = Schema.Schema.Type; + +const decodeDaVinciNode = Schema.decodeUnknownOption(DaVinciNodeSchema); + +// --------------------------------------------------------------------------- +// Pure mapping — fully testable, no side effects +// --------------------------------------------------------------------------- + +export function nodeToSdkData( + node: DaVinciNode, + previousStatus: string | undefined, + responseBody?: unknown, +): SdkData { + return { + _tag: 'sdk', + nodeStatus: node.status ?? 'unknown', + previousStatus, + interactionId: node.server?.interactionId, + interactionToken: node.server?.interactionToken, + nodeId: node.server?.id, + requestId: node.cache?.key ?? undefined, + nodeName: node.client?.name, + nodeDescription: node.client?.description, + eventName: node.server?.eventName, + httpStatus: node.httpStatus, + collectors: node.client?.collectors as SdkData['collectors'], + error: node.error ?? undefined, + authorization: node.client?.authorization, + session: node.server?.session, + responseBody, + }; +} + +// --------------------------------------------------------------------------- +// Session snapshot helpers (imperative shell) +// --------------------------------------------------------------------------- + +interface SessionSnapshot { + cookie: string; + storage: Record; +} + +function snapshotSession(): SessionSnapshot { + const storage: Record = {}; + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (k) storage[k] = localStorage.getItem(k) ?? ''; + } + return { cookie: document.cookie, storage }; +} + +function emitSessionDiffs( + before: SessionSnapshot, + after: SessionSnapshot, + flowId: string | null, +): void { + if (before.cookie !== after.cookie) { + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'session:cookie', + source: 'session', + flowId, + causedBy: null, + data: { + _tag: 'session', + key: 'document.cookie', + before: before.cookie || undefined, + after: after.cookie || undefined, + }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + }); + } + + const allKeys = new Set([...Object.keys(before.storage), ...Object.keys(after.storage)]); + for (const key of allKeys) { + const beforeVal = before.storage[key]; + const afterVal = after.storage[key]; + if (beforeVal !== afterVal) { + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'session:storage', + source: 'session', + flowId, + causedBy: null, + data: { _tag: 'session', key, before: beforeVal, after: afterVal }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + }); + } + } +} + +// --------------------------------------------------------------------------- +// Event builders +// --------------------------------------------------------------------------- + +function emitNodeChange(data: SdkData): void { + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'sdk:node-change', + source: 'sdk', + flowId: data.interactionId ?? null, + causedBy: null, + data, + flags: { + isCors: false, + isError: data.nodeStatus === 'error' || data.nodeStatus === 'failure', + isAuthRelated: true, + }, + }); +} + +// --------------------------------------------------------------------------- +// Bridge +// --------------------------------------------------------------------------- + +/** + * Attaches the Ping DevTools bridge to a subscribable client (e.g. DaVinci client). + * + * Pass the SDK's client config as the optional second argument — it is emitted once + * as an `sdk:config` event on the first node transition, letting the extension display + * app-level context alongside auth flow events. + * + * Returns a no-op handle when run outside a browser. Always call `detach()` on cleanup. + */ +export function attachDevToolsBridge( + client: Subscribable, + config?: object, + devtoolsOptions?: DevtoolsOptions, +): BridgeHandle { + if (typeof window === 'undefined') { + return { detach: () => undefined }; + } + + if (devtoolsOptions) { + configureDevtools(devtoolsOptions); + } + + let previousStatus: string | undefined; + let configEmitted = false; + let lastSnapshot: SessionSnapshot = snapshotSession(); + + const unsubscribe = client.subscribe(() => { + pipe( + client.getNode(), + decodeDaVinciNode, + // Advance previousStatus before the extension check so we always track + // transitions, even when the panel is closed. + Option.flatMap((node) => { + if (node.status === previousStatus) return Option.none(); + const priorStatus = previousStatus; + previousStatus = node.status; + const cachedResponse = node.cache?.key ? client.cache?.getCache(node.cache.key) : undefined; + return Option.some(nodeToSdkData(node, priorStatus, cachedResponse)); + }), + Option.filter(() => '__PING_DEVTOOLS_EXTENSION__' in window), + Option.map((data) => { + if (config && !configEmitted) { + configEmitted = true; + emitConfigEvent(config); + } + emitNodeChange(data); + // Snapshot before deferring so mutations in the same call stack are captured. + const snapshotBefore = lastSnapshot; + setTimeout(() => { + const snapshotAfter = snapshotSession(); + emitSessionDiffs(snapshotBefore, snapshotAfter, data.interactionId ?? null); + lastSnapshot = snapshotAfter; + }, 0); + }), + ); + }); + + return { detach: unsubscribe }; +} diff --git a/packages/devtools-bridge/src/lib/emit.test.ts b/packages/devtools-bridge/src/lib/emit.test.ts new file mode 100644 index 0000000000..22d8832c1b --- /dev/null +++ b/packages/devtools-bridge/src/lib/emit.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect } from 'vitest'; +import type { AuthEvent } from '@forgerock/devtools-types'; +import { DEVTOOLS_EVENT_NAME, emitAuthEvent } from './emit.js'; + +// Minimal valid AuthEvent fixture — _tag: 'sdk' satisfies the SdkDataSchema discriminant. +const makeEvent = (): AuthEvent => ({ + id: 'test-id-1', + timestamp: 0, + type: 'sdk:node-change', + source: 'sdk', + flowId: null, + causedBy: null, + data: { + _tag: 'sdk', + nodeStatus: 'continue', + }, + flags: { + isCors: false, + isError: false, + isAuthRelated: true, + }, +}); + +describe('emitAuthEvent', () => { + it('dispatches a CustomEvent with DEVTOOLS_EVENT_NAME and the event as detail', () => { + const captured: CustomEvent[] = []; + const handler = (e: Event) => { + captured.push(e as CustomEvent); + }; + + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + + const event = makeEvent(); + emitAuthEvent(event); + + window.removeEventListener(DEVTOOLS_EVENT_NAME, handler); + + expect(captured).toHaveLength(1); + expect(captured[0].type).toBe(DEVTOOLS_EVENT_NAME); + expect(captured[0].detail).toBe(event); + }); + + it('does not throw when window is undefined', () => { + // jsdom always defines window, so we temporarily remove it to exercise the guard branch. + const saved = globalThis.window; + // @ts-expect-error — intentionally deleting window to test the undefined guard + delete globalThis.window; + + expect(() => emitAuthEvent(makeEvent())).not.toThrow(); + + // Restore window so subsequent tests are unaffected. + globalThis.window = saved; + }); +}); diff --git a/packages/devtools-bridge/src/lib/emit.ts b/packages/devtools-bridge/src/lib/emit.ts new file mode 100644 index 0000000000..6ed02abaf8 --- /dev/null +++ b/packages/devtools-bridge/src/lib/emit.ts @@ -0,0 +1,47 @@ +import type { AuthEvent } from '@forgerock/devtools-types'; + +export const DEVTOOLS_EVENT_NAME = 'pingDevtools'; + +export interface DevtoolsOptions { + consoleLog?: boolean; +} + +declare global { + interface Window { + __PING_DEVTOOLS_STATE__?: AuthEvent[]; + } +} + +let options: DevtoolsOptions = {}; + +export function configureDevtools(opts: DevtoolsOptions): void { + options = opts; +} + +export function emitAuthEvent(event: AuthEvent): void { + if (typeof window === 'undefined') return; + + if (!window.__PING_DEVTOOLS_STATE__) { + window.__PING_DEVTOOLS_STATE__ = []; + } + window.__PING_DEVTOOLS_STATE__.push(event); + + if (options.consoleLog) { + console.log('[ping-devtools]', event.type, event); + } + + window.dispatchEvent(new CustomEvent(DEVTOOLS_EVENT_NAME, { detail: event })); +} + +export function emitConfigEvent(config: object): void { + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'sdk:config', + source: 'sdk', + flowId: null, + causedBy: null, + data: { _tag: 'sdk-config', config }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + }); +} diff --git a/packages/devtools-bridge/src/lib/journey-bridge.test.ts b/packages/devtools-bridge/src/lib/journey-bridge.test.ts new file mode 100644 index 0000000000..e3f82b191d --- /dev/null +++ b/packages/devtools-bridge/src/lib/journey-bridge.test.ts @@ -0,0 +1,375 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { attachJourneyBridge } from './journey-bridge.js'; +import { DEVTOOLS_EVENT_NAME } from './emit.js'; +import type { AuthEvent } from '@forgerock/devtools-types'; + +// --------------------------------------------------------------------------- +// Mock client factory +// --------------------------------------------------------------------------- + +type JourneyState = { + journeyReducer: { + mutations: Record< + string, + { status: string; endpointName?: string; data?: unknown; error?: unknown } + >; + }; +}; + +function makeClient(initialState: JourneyState) { + let listener: (() => void) | null = null; + let state = initialState; + return { + subscribe: vi.fn((cb: () => void) => { + listener = cb; + return () => { + listener = null; + }; + }), + getState: vi.fn(() => state), + trigger: (newState: JourneyState) => { + state = newState; + listener?.(); + }, + }; +} + +function emptyState(): JourneyState { + return { journeyReducer: { mutations: {} } }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function captureDevtoolsEvents(): { events: CustomEvent[]; stop: () => void } { + const events: CustomEvent[] = []; + const handler = (e: Event) => events.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + return { events, stop: () => window.removeEventListener(DEVTOOLS_EVENT_NAME, handler) }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('attachJourneyBridge', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + }); + + it('returns a handle with a detach function', () => { + const client = makeClient(emptyState()); + const handle = attachJourneyBridge(client); + expect(handle).toHaveProperty('detach'); + expect(typeof handle.detach).toBe('function'); + handle.detach(); + }); + + it('emits sdk:journey-step with stepType=Step for a fulfilled step response (has authId)', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'fulfilled', + endpointName: 'next', + data: { + authId: 'abc123', + stage: 'UsernamePassword', + header: 'Sign In', + description: 'Enter your credentials', + callbacks: [{ type: 'NameCallback' }, { type: 'PasswordCallback' }], + }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + expect(events[0].detail.type).toBe('sdk:journey-step'); + const data = events[0].detail.data as { + _tag: string; + stepType: string; + callbacks: unknown[]; + authId?: string; + stage?: string; + header?: string; + }; + expect(data._tag).toBe('journey'); + expect(data.stepType).toBe('Step'); + expect(data.authId).toBe('abc123'); + expect(data.stage).toBe('UsernamePassword'); + expect(data.header).toBe('Sign In'); + expect(data.callbacks).toEqual([{ type: 'NameCallback' }, { type: 'PasswordCallback' }]); + expect(events[0].detail.flags.isError).toBe(false); + }); + + it('emits stepType=LoginSuccess when successUrl is present', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'fulfilled', + endpointName: 'next', + data: { successUrl: 'https://app.example.com/dashboard' }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { stepType: string }; + expect(data.stepType).toBe('LoginSuccess'); + expect(events[0].detail.flags.isError).toBe(false); + }); + + it('emits stepType=LoginFailure with error fields when no authId or successUrl', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'fulfilled', + endpointName: 'next', + data: { + code: 110, + message: 'Authentication Failed', + reason: 'LoginFailure', + }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { + stepType: string; + errorCode?: number; + errorMessage?: string; + errorReason?: string; + }; + expect(data.stepType).toBe('LoginFailure'); + expect(data.errorCode).toBe(110); + expect(data.errorMessage).toBe('Authentication Failed'); + expect(data.errorReason).toBe('LoginFailure'); + expect(events[0].detail.flags.isError).toBe(true); + }); + + it('emits stepType=LoginFailure for a rejected mutation with error message', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'rejected', + endpointName: 'next', + error: { message: 'Network error' }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + expect(events[0].detail.type).toBe('sdk:journey-step'); + const data = events[0].detail.data as unknown as { + stepType: string; + errorMessage?: string; + }; + expect(data.stepType).toBe('LoginFailure'); + expect(data.errorMessage).toBe('Network error'); + expect(events[0].detail.flags.isError).toBe(true); + }); + + it('extracts errorMessage from nested error.data.message for rejected mutation', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'rejected', + endpointName: 'next', + error: { data: { message: 'Session expired' } }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Session expired'); + }); + + it('falls back to "Unknown error" when rejected mutation has no extractable message', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'rejected', endpointName: 'next', error: { status: 500 } }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Unknown error'); + }); + + it('does NOT emit for pending mutations', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { 'req-1': { status: 'pending', endpointName: 'next' } }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('does NOT re-emit for the same requestId on a second trigger', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + const state: JourneyState = { + journeyReducer: { + mutations: { + 'req-1': { status: 'fulfilled', data: { authId: 'abc' } }, + }, + }, + }; + client.trigger(state); + client.trigger(state); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + }); + + it('emits sdk:config on first mutation when config is provided', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client, { realm: '/alpha' }); + client.trigger({ + journeyReducer: { + mutations: { 'req-1': { status: 'fulfilled', data: { authId: 'abc' } } }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(2); + expect(events[0].detail.type).toBe('sdk:config'); + expect(events[1].detail.type).toBe('sdk:journey-step'); + }); + + it('emits sdk:config only once across multiple mutations', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client, { realm: '/alpha' }); + + client.trigger({ + journeyReducer: { + mutations: { 'req-1': { status: 'fulfilled', data: { authId: 'abc' } } }, + }, + }); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'fulfilled', data: { authId: 'abc' } }, + 'req-2': { status: 'fulfilled', data: { successUrl: '/home' } }, + }, + }, + }); + + handle.detach(); + stop(); + + const configEvents = events.filter((e) => e.detail.type === 'sdk:config'); + expect(configEvents).toHaveLength(1); + }); + + it('does NOT emit when __PING_DEVTOOLS_EXTENSION__ is absent', () => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { 'req-1': { status: 'fulfilled', data: { authId: 'abc' } } }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('detach() stops the listener', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + handle.detach(); + + client.trigger({ + journeyReducer: { + mutations: { 'req-1': { status: 'fulfilled', data: { authId: 'abc' } } }, + }, + }); + + stop(); + + expect(events).toHaveLength(0); + }); +}); diff --git a/packages/devtools-bridge/src/lib/journey-bridge.ts b/packages/devtools-bridge/src/lib/journey-bridge.ts new file mode 100644 index 0000000000..795c2d5ec6 --- /dev/null +++ b/packages/devtools-bridge/src/lib/journey-bridge.ts @@ -0,0 +1,186 @@ +import { Schema, Option, pipe } from 'effect'; +import { emitAuthEvent, emitConfigEvent, configureDevtools } from './emit.js'; +import type { DevtoolsOptions } from './emit.js'; +import type { JourneyData } from '@forgerock/devtools-types'; + +export interface JourneyBridgeHandle { + detach: () => void; +} + +interface JourneySubscribable { + subscribe: (listener: () => void) => () => void; + getState: () => unknown; +} + +// --------------------------------------------------------------------------- +// Local schemas — structural contracts for RTK Query state, not public types +// --------------------------------------------------------------------------- + +const MutationEntrySchema = Schema.Struct({ + status: Schema.String, + endpointName: Schema.optional(Schema.String), + data: Schema.optional(Schema.Unknown), + error: Schema.optional(Schema.Unknown), +}); + +const JourneyStateSchema = Schema.Struct({ + journeyReducer: Schema.Struct({ + mutations: Schema.Record({ key: Schema.String, value: MutationEntrySchema }), + }), +}); + +const decodeMutationEntry = Schema.decodeUnknownOption(MutationEntrySchema); +const decodeJourneyState = Schema.decodeUnknownOption(JourneyStateSchema); + +// --------------------------------------------------------------------------- +// Pure mapping — Step payload → JourneyData +// --------------------------------------------------------------------------- + +const StepPayloadSchema = Schema.Struct({ + authId: Schema.optional(Schema.String), + successUrl: Schema.optional(Schema.String), + tokenId: Schema.optional(Schema.String), + code: Schema.optional(Schema.Number), + message: Schema.optional(Schema.String), + reason: Schema.optional(Schema.String), + realm: Schema.optional(Schema.String), + stage: Schema.optional(Schema.String), + header: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + callbacks: Schema.optional(Schema.Array(Schema.Unknown)), +}); + +const decodeStepPayload = Schema.decodeUnknownOption(StepPayloadSchema); + +function stepPayloadToJourneyData(data: unknown): JourneyData | null { + return pipe( + data, + decodeStepPayload, + Option.map((step) => { + const stepType: JourneyData['stepType'] = step.authId + ? 'Step' + : step.successUrl + ? 'LoginSuccess' + : 'LoginFailure'; + + return { + _tag: 'journey' as const, + stepType, + callbacks: step.callbacks, + authId: step.authId, + tokenId: step.tokenId, + successUrl: step.successUrl, + realm: step.realm, + stage: step.stage, + header: step.header, + description: step.description, + errorCode: stepType === 'LoginFailure' ? step.code : undefined, + errorMessage: stepType === 'LoginFailure' ? step.message : undefined, + errorReason: stepType === 'LoginFailure' ? step.reason : undefined, + } satisfies JourneyData; + }), + Option.getOrNull, + ); +} + +function extractErrorMessage(error: unknown): string { + if (typeof error === 'string') return error; + if (typeof error === 'object' && error !== null) { + const e = error as Record; + if (typeof e['message'] === 'string') return e['message']; + if (typeof e['data'] === 'object' && e['data'] !== null) { + const d = e['data'] as Record; + if (typeof d['message'] === 'string') return d['message']; + } + } + return 'Unknown error'; +} + +// --------------------------------------------------------------------------- +// Bridge +// --------------------------------------------------------------------------- + +export function attachJourneyBridge( + client: JourneySubscribable, + config?: object, + devtoolsOptions?: DevtoolsOptions, +): JourneyBridgeHandle { + if (typeof window === 'undefined') { + return { detach: () => undefined }; + } + + if (devtoolsOptions) { + configureDevtools(devtoolsOptions); + } + + let configEmitted = false; + let emittedRequests = new Set(); + + const unsubscribe = client.subscribe(() => { + if (!('__PING_DEVTOOLS_EXTENSION__' in window)) return; + + pipe( + client.getState(), + decodeJourneyState, + Option.map(({ journeyReducer: { mutations } }) => { + // Trim stale IDs no longer in the cache to bound memory usage + emittedRequests = new Set([...emittedRequests].filter((id) => id in mutations)); + + for (const [requestId, rawEntry] of Object.entries(mutations)) { + if (emittedRequests.has(requestId)) continue; + + pipe( + rawEntry, + decodeMutationEntry, + Option.filter((entry) => entry.status === 'fulfilled' || entry.status === 'rejected'), + Option.map((entry) => { + emittedRequests.add(requestId); + + if (config && !configEmitted) { + emitConfigEvent(config); + configEmitted = true; + } + + if (entry.status === 'fulfilled') { + const journeyData = stepPayloadToJourneyData(entry.data); + if (!journeyData) return; + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'sdk:journey-step', + source: 'sdk', + flowId: null, + causedBy: null, + data: journeyData, + flags: { + isCors: false, + isError: journeyData.stepType === 'LoginFailure', + isAuthRelated: true, + }, + }); + } else { + const journeyData: JourneyData = { + _tag: 'journey', + stepType: 'LoginFailure', + errorMessage: extractErrorMessage(entry.error), + }; + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'sdk:journey-step', + source: 'sdk', + flowId: null, + causedBy: null, + data: journeyData, + flags: { isCors: false, isError: true, isAuthRelated: true }, + }); + } + }), + ); + } + }), + ); + }); + + return { detach: unsubscribe }; +} diff --git a/packages/devtools-bridge/src/lib/oidc-bridge.test.ts b/packages/devtools-bridge/src/lib/oidc-bridge.test.ts new file mode 100644 index 0000000000..3b4088c4dc --- /dev/null +++ b/packages/devtools-bridge/src/lib/oidc-bridge.test.ts @@ -0,0 +1,312 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { attachOidcBridge } from './oidc-bridge.js'; +import { DEVTOOLS_EVENT_NAME } from './emit.js'; +import type { AuthEvent } from '@forgerock/devtools-types'; + +// --------------------------------------------------------------------------- +// Mock client factory +// --------------------------------------------------------------------------- + +type OidcState = { + oidc: { + mutations: Record< + string, + { status: string; endpointName?: string; data?: unknown; error?: unknown } + >; + }; +}; + +function makeClient(initialState: OidcState) { + let listener: (() => void) | null = null; + let state = initialState; + return { + subscribe: vi.fn((cb: () => void) => { + listener = cb; + return () => { + listener = null; + }; + }), + getState: vi.fn(() => state), + /** Test helper: replace state and fire the subscribed listener. */ + trigger: (newState: OidcState) => { + state = newState; + listener?.(); + }, + }; +} + +function emptyState(): OidcState { + return { oidc: { mutations: {} } }; +} + +function fulfilledMutation(endpointName: string): OidcState['oidc']['mutations'][string] { + return { status: 'fulfilled', endpointName }; +} + +function rejectedMutation( + endpointName: string, + error: unknown, +): OidcState['oidc']['mutations'][string] { + return { status: 'rejected', endpointName, error }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function captureDevtoolsEvents(): { events: CustomEvent[]; stop: () => void } { + const events: CustomEvent[] = []; + const handler = (e: Event) => events.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + return { events, stop: () => window.removeEventListener(DEVTOOLS_EVENT_NAME, handler) }; +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('attachOidcBridge', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + }); + + it('returns a handle with a detach function', () => { + const client = makeClient(emptyState()); + const handle = attachOidcBridge(client); + expect(handle).toHaveProperty('detach'); + expect(typeof handle.detach).toBe('function'); + handle.detach(); + }); + + it('emits sdk:oidc-state for a fulfilled authorizeFetch mutation', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('authorizeFetch') } } }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + expect(events[0].detail.type).toBe('sdk:oidc-state'); + const data = events[0].detail.data as { _tag: string; phase: string; status: string }; + expect(data._tag).toBe('oidc'); + expect(data.phase).toBe('authorize'); + expect(data.status).toBe('success'); + }); + + it('maps authorizeIframe → authorize phase', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('authorizeIframe') } } }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { phase: string }; + expect(data.phase).toBe('authorize'); + }); + + it.each([ + ['exchange', 'exchange'], + ['revoke', 'revoke'], + ['userInfo', 'userinfo'], + ['endSession', 'logout'], + ])('maps %s → %s phase', (endpointName, expectedPhase) => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation(endpointName) } } }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { phase: string }; + expect(data.phase).toBe(expectedPhase); + }); + + it('emits status:error and extracts errorCode/errorMessage for rejected mutation', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': rejectedMutation('exchange', { + status: 400, + data: { error: 'invalid_grant', error_description: 'Token expired' }, + }), + }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + const data = events[0].detail.data as { + _tag: string; + phase: string; + status: string; + errorCode?: string; + errorMessage?: string; + }; + expect(data.status).toBe('error'); + expect(data.errorCode).toBe('invalid_grant'); + expect(data.errorMessage).toBe('Token expired'); + expect(events[0].detail.flags.isError).toBe(true); + }); + + it('falls back to HTTP status code when data.error is absent', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { mutations: { 'req-1': rejectedMutation('revoke', { status: 401 }) } }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorCode?: string }; + expect(data.errorCode).toBe('401'); + }); + + it('does NOT emit for pending mutations (status = pending)', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { mutations: { 'req-1': { status: 'pending', endpointName: 'exchange' } } }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('does NOT emit for an unknown endpointName (no phase mapping)', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { mutations: { 'req-1': fulfilledMutation('unknownEndpoint') } }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('does NOT re-emit for the same requestId on a second trigger', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + + const state: OidcState = { + oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } }, + }; + client.trigger(state); + // Same requestId still present — should not emit again. + client.trigger(state); + + handle.detach(); + stop(); + + expect(events).toHaveLength(1); + }); + + it('emits sdk:config on the first mutation when config is provided', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client, { clientId: 'my-app' }); + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } } }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(2); + expect(events[0].detail.type).toBe('sdk:config'); + expect(events[1].detail.type).toBe('sdk:oidc-state'); + }); + + it('emits sdk:config only once across multiple mutations', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client, { clientId: 'my-app' }); + + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } } }); + client.trigger({ + oidc: { + mutations: { 'req-1': fulfilledMutation('exchange'), 'req-2': fulfilledMutation('revoke') }, + }, + }); + + handle.detach(); + stop(); + + const configEvents = events.filter((e) => e.detail.type === 'sdk:config'); + expect(configEvents).toHaveLength(1); + }); + + it('includes clientId in the emitted oidc data', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client, { clientId: 'ping-app' }); + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } } }); + + handle.detach(); + stop(); + + const oidcEvent = events.find((e) => e.detail.type === 'sdk:oidc-state'); + const data = oidcEvent?.detail.data as { clientId?: string }; + expect(data.clientId).toBe('ping-app'); + }); + + it('does NOT emit when __PING_DEVTOOLS_EXTENSION__ is absent', () => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } } }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('detach() stops the listener', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + handle.detach(); + + client.trigger({ oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } } }); + + stop(); + + expect(events).toHaveLength(0); + }); +}); diff --git a/packages/devtools-bridge/src/lib/oidc-bridge.ts b/packages/devtools-bridge/src/lib/oidc-bridge.ts new file mode 100644 index 0000000000..243f05fb77 --- /dev/null +++ b/packages/devtools-bridge/src/lib/oidc-bridge.ts @@ -0,0 +1,172 @@ +import { Schema, Option, pipe } from 'effect'; +import { emitAuthEvent, emitConfigEvent, configureDevtools } from './emit.js'; +import type { DevtoolsOptions } from './emit.js'; +import type { OidcData } from '@forgerock/devtools-types'; + +export interface OidcBridgeHandle { + detach: () => void; +} + +interface OidcSubscribable { + subscribe: (listener: () => void) => () => void; + getState: () => unknown; +} + +// --------------------------------------------------------------------------- +// Local schemas — structural contracts for RTK Query state, not public types +// --------------------------------------------------------------------------- + +const MutationEntrySchema = Schema.Struct({ + status: Schema.String, + endpointName: Schema.optional(Schema.String), + data: Schema.optional(Schema.Unknown), + error: Schema.optional(Schema.Unknown), +}); + +const OidcStateSchema = Schema.Struct({ + oidc: Schema.Struct({ + mutations: Schema.Record({ key: Schema.String, value: MutationEntrySchema }), + }), +}); + +const decodeMutationEntry = Schema.decodeUnknownOption(MutationEntrySchema); +const decodeOidcState = Schema.decodeUnknownOption(OidcStateSchema); + +// --------------------------------------------------------------------------- +// Endpoint name → OidcData phase +// --------------------------------------------------------------------------- + +const ENDPOINT_PHASE_MAP: Record = { + authorizeFetch: 'authorize', + authorizeIframe: 'authorize', + exchange: 'exchange', + revoke: 'revoke', + userInfo: 'userinfo', + endSession: 'logout', +}; + +// --------------------------------------------------------------------------- +// Pure mapping — RTK mutation entry → OidcData +// --------------------------------------------------------------------------- + +function extractOidcError(error: unknown): { errorCode?: string; errorMessage?: string } { + if (typeof error === 'string') return { errorMessage: error }; + if (typeof error !== 'object' || error === null) return {}; + const e = error as Record; + const errData = + typeof e['data'] === 'object' && e['data'] !== null + ? (e['data'] as Record) + : undefined; + return { + errorCode: + typeof errData?.['error'] === 'string' + ? errData['error'] + : typeof e['status'] === 'number' + ? String(e['status']) + : undefined, + errorMessage: + typeof errData?.['error_description'] === 'string' + ? errData['error_description'] + : typeof errData?.['message'] === 'string' + ? errData['message'] + : typeof e['message'] === 'string' + ? e['message'] + : undefined, + }; +} + +function mutationToOidcData( + endpointName: string | undefined, + status: 'fulfilled' | 'rejected', + error: unknown, + clientId: string | undefined, +): OidcData | null { + const phase = ENDPOINT_PHASE_MAP[endpointName ?? '']; + if (!phase) return null; + + if (status === 'fulfilled') { + return { _tag: 'oidc', phase, status: 'success', clientId }; + } + + return { _tag: 'oidc', phase, status: 'error', clientId, ...extractOidcError(error) }; +} + +// --------------------------------------------------------------------------- +// Bridge +// --------------------------------------------------------------------------- + +export function attachOidcBridge( + client: OidcSubscribable, + config?: { clientId?: string } & object, + devtoolsOptions?: DevtoolsOptions, +): OidcBridgeHandle { + if (typeof window === 'undefined') { + return { detach: () => undefined }; + } + + if (devtoolsOptions) { + configureDevtools(devtoolsOptions); + } + + let configEmitted = false; + let emittedRequests = new Set(); + + const unsubscribe = client.subscribe(() => { + if (!('__PING_DEVTOOLS_EXTENSION__' in window)) return; + + pipe( + client.getState(), + decodeOidcState, + Option.map(({ oidc: { mutations } }) => { + // Trim stale IDs no longer in the cache to bound memory usage + emittedRequests = new Set([...emittedRequests].filter((id) => id in mutations)); + + for (const [requestId, rawEntry] of Object.entries(mutations)) { + if (emittedRequests.has(requestId)) continue; + + pipe( + rawEntry, + decodeMutationEntry, + Option.filter( + (entry): entry is typeof entry & { status: 'fulfilled' | 'rejected' } => + entry.status === 'fulfilled' || entry.status === 'rejected', + ), + Option.map((entry) => { + emittedRequests.add(requestId); + + if (config && !configEmitted) { + emitConfigEvent(config); + configEmitted = true; + } + + const oidcData = mutationToOidcData( + entry.endpointName, + entry.status, + entry.error, + config?.clientId, + ); + if (!oidcData) return; + + emitAuthEvent({ + id: crypto.randomUUID(), + timestamp: performance.now(), + type: 'sdk:oidc-state', + source: 'sdk', + flowId: null, + causedBy: null, + data: oidcData, + flags: { + isCors: false, + isError: oidcData.status === 'error', + isAuthRelated: true, + }, + }); + }), + ); + } + }), + ); + }); + + return { detach: unsubscribe }; +} diff --git a/packages/devtools-bridge/tsconfig.json b/packages/devtools-bridge/tsconfig.json new file mode 100644 index 0000000000..329ef5038f --- /dev/null +++ b/packages/devtools-bridge/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.spec.json" } + ], + "nx": { "addTypecheckTarget": false } +} diff --git a/packages/devtools-bridge/tsconfig.lib.json b/packages/devtools-bridge/tsconfig.lib.json new file mode 100644 index 0000000000..63236d56bb --- /dev/null +++ b/packages/devtools-bridge/tsconfig.lib.json @@ -0,0 +1,29 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "nodenext", + "module": "NodeNext", + "target": "ES2022", + "outDir": "./dist", + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "strict": true, + "noImplicitOverride": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "sourceMap": true, + "lib": ["es2022", "dom", "dom.iterable"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], + "references": [ + { + "path": "../devtools-types/tsconfig.lib.json" + }, + { + "path": "../davinci-client/tsconfig.lib.json" + } + ] +} diff --git a/packages/devtools-bridge/tsconfig.spec.json b/packages/devtools-bridge/tsconfig.spec.json new file mode 100644 index 0000000000..5b057b3f75 --- /dev/null +++ b/packages/devtools-bridge/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "NodeNext", + "moduleResolution": "nodenext", + "strict": true, + "noImplicitOverride": true + }, + "include": ["vite.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"], + "references": [{ "path": "./tsconfig.lib.json" }] +} diff --git a/packages/devtools-bridge/vite.config.ts b/packages/devtools-bridge/vite.config.ts new file mode 100644 index 0000000000..c8961e30f3 --- /dev/null +++ b/packages/devtools-bridge/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/devtools-bridge', + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,ts}'], + reporters: ['default'], + }, +})); diff --git a/packages/devtools-extension/README.md b/packages/devtools-extension/README.md new file mode 100644 index 0000000000..e57076d271 --- /dev/null +++ b/packages/devtools-extension/README.md @@ -0,0 +1,229 @@ +# Ping DevTools + +**Captures, correlates, and diagnoses** Ping Identity / ForgeRock authentication flows in real time. + +Most auth debugging starts in the Network panel and stays there — copying tokens into jwt.io, cross-referencing timestamps, guessing which 400 was the CORS preflight and which was a bad grant. Ping DevTools replaces that with a single panel that captures both network traffic and SDK-level events, correlates them into flows, and runs an automated diagnosis engine that tells you _what went wrong and how to fix it_. + + + + +--- + +## Status + +**v0.1.0 — alpha, active development.** The extension is functional and loadable as an unpacked Chrome extension. It is not published to the Chrome Web Store. The package is private (`@forgerock/devtools-extension`). + +--- + +## Diagnosis + +Every captured event is run through a rule engine that produces flow-level and per-event diagnostics with severity ratings and numbered remediation steps. + +**Flow Health** — a banner at the top of the Flow view surfaces the worst-severity issue across the entire flow. It stays hidden when everything is healthy, expands automatically when a new error arrives during recording, and each issue is clickable — jumping directly to the related event in the Timeline. + +**Per-event annotations** — the Inspector's Diagnosis tab appears only when the selected event has issues. Errors get a solid dot on the tab label; warnings get a half dot. Each annotation includes a title, description, relevant data pairs, and step-by-step remediation. + +The engine currently covers: + +| Category | Examples | +| --------------- | --------------------------------------------------------------------------------------------------------------- | +| **CORS** | Status-zero failures, missing `Access-Control-Allow-Origin`, wildcard + credentials conflict | +| **Token** | Missing `interactionToken` on non-initial nodes, expired JWTs in request headers (decoded and checked at `exp`) | +| **Flow config** | Node error/failure status, connector errors, policy-not-found | +| **OIDC** | State mismatch, missing PKCE, redirect URI mismatch | + +--- + +## Import and export + +Flows can be exported for sharing or offline analysis, and imported from JSON. + +**Export** — the toolbar dropdown offers two formats: + +- **JSON** — full flow state including all events, a summary (node count, error count, CORS flags, duration), and metadata. Supports optional redaction of sensitive data (tokens, passwords). +- **Markdown** — a human-readable report with a flow summary and event timeline grouped by type. + +**Import** — paste exported JSON into the import modal. The imported flow replaces live recording (recording pauses automatically). A metadata banner shows the flow ID, capture timestamp, and redaction status. Click **Clear** to discard the import and resume live capture. + +--- + +## Snapshots + +Click **Snapshot** to save the current flow state to local storage (up to 5 snapshots, oldest dropped when full). The dropdown arrow next to the button opens a list of saved snapshots showing flow ID, timestamp, and event count. Click an entry to load it (same as importing — recording pauses, import banner appears). Click **✕** to delete a snapshot. + +--- + +## Time-travel playback + +The Flow view includes transport controls (**Prev / Play / Pause / Reset**) that step through SDK nodes in sequence. During playback the interval between steps mirrors the real elapsed time, clamped to 300 ms – 1500 ms, so you can watch the flow unfold at roughly the pace it happened. + +--- + +## Why not just use the Network panel? + +The Network panel shows HTTP requests. Auth flows are not HTTP requests — they are multi-step state machines that span dozens of requests, involve two independent event streams (network and SDK), and fail in ways that only make sense when you see the full sequence. + +Ping DevTools gives you: + +- **Two-stream correlation** — network responses and SDK state transitions are merged into a single timeline, linked by flow ID and causal references ("Triggered by SDK Node `a1b2c3d4`"). +- **Automated diagnosis** — CORS misconfigurations, expired JWTs, missing PKCE, and connector errors are detected and explained with remediation steps, not left as a 400 status code. +- **Flow-level structure** — the Flow view shows the authentication flow as a sequence of nodes with detail cards, not a flat list of URLs. +- **Playback** — step through the flow to see exactly what the SDK saw at each point. + + + + +--- + +## Architecture + +TypeScript with Effect-TS on the data plane, Elm on the view, Schema-validated at the boundary. Elm was chosen because of it's runtime guarantee's so the devtool should almost always, function and have no runtime errors (likely). + +``` +Host page + ├── attachDevToolsBridge(davinciClient) ─┐ + ├── attachJourneyBridge(journeyClient) ─┤─ CustomEvent('pingDevtools') + └── attachOidcBridge(oidcClient) ─┘ + │ + content-script.ts (MAIN world — postMessage only, no chrome.runtime) + │ + relay.ts (isolated world — chrome.runtime.sendMessage) + │ + service-worker.ts (Effect ManagedRuntime) + ├── AuthEventSchema validation (Effect Schema — untrusted input decoded or dropped) + ├── EventStore (Effect Ref + chrome.storage.local) + ├── diagnosis-engine.ts (flow rules + event rules) + └── broadcast to panel(s) + │ + panel/Main.elm (Elm 0.19) + ├── Timeline view — chronological event table with Inspector + └── Flow view — node rail + detail card + health banner +``` + +Network events follow a parallel path: `network-observer.ts` uses `chrome.devtools.network.onRequestFinished` to capture HAR entries, filters them against auth URL patterns, and sends them to the same service worker. + +Diagnosis results include per-event annotations and numbered remediation steps that surface protocol-level context (CORS mechanics, OAuth error codes, JWT claims) inline in the Inspector, so you understand _why_ something failed without leaving the panel. + +--- + +## Captured event types + +| Type | Source | Description | +| ------------------- | ------- | ---------------------------------------------------- | +| `network:request` | network | Outgoing HTTP request to an auth endpoint | +| `network:response` | network | Response received | +| `network:cors-flag` | network | CORS failure detected (status 0, missing headers) | +| `sdk:node-change` | sdk | DaVinci node transition (start → continue → …) | +| `sdk:config` | sdk | SDK configuration snapshot (emitted once per bridge) | +| `sdk:journey-step` | sdk | AM Journey step fulfilled or rejected | +| `sdk:oidc-state` | sdk | OIDC endpoint settled (authorize, exchange, …) | +| `dom:form-submit` | dom | Form submission captured | +| `dom:redirect` | dom | Page redirect detected | +| `session:cookie` | session | Cookie value changed | +| `session:storage` | session | `localStorage` value changed | + +Events are linked by `flowId` and an optional `causedBy` reference pointing to the originating event, enabling two-stream correlation in the Timeline. + +--- + +## Security and privacy + +The extension requests only `storage, and clipboardWrite/clipboardRead` (for copying collectors from the view if wanted) — no `cookies`, `webRequest`, `tabs``, or other sensitive APIs. Content scripts use a two-world architecture: `content-script.ts`runs in the MAIN world (page access, no`chrome.runtime`), while `relay.ts`runs in the isolated world (runtime access, guarded by a sentinel flag and same-source check), preventing arbitrary page code from injecting messages into the service worker. All SDK events are decoded through`AuthEventSchema`(Effect Schema) before reaching the EventStore — malformed payloads are dropped with a console warning. Captured data is stored in`chrome.storage.local` under a namespaced key and never transmitted off-device. No remote code is loaded or executed. + +--- + +## Build + +```bash +nx run devtools-extension:build +``` + +Output is written to `packages/devtools-extension/dist/`. + +> **Prerequisite:** [Elm](https://guide.elm-lang.org/install/elm.html) must be installed and on your `PATH`. The build step compiles `src/panel/Main.elm` into a single JS bundle. + +--- + +## Load in Chrome + +1. Open `chrome://extensions` +2. Enable **Developer mode** (top-right toggle) +3. Click **Load unpacked** +4. Select `packages/devtools-extension/dist/` +5. Open DevTools on any page → **Ping DevTools** tab + +After rebuilding, click the refresh icon on the extension card at `chrome://extensions`, then close and reopen DevTools. + +--- + +## Wiring up your app + +The extension captures all network traffic automatically. To also see SDK-level events (node transitions, journey steps, OIDC phases, session diffs), add the bridge adapter to your app. + +```bash +pnpm add @forgerock/devtools-bridge +``` + +All `attach*` functions are safe to call unconditionally — they are no-ops when the extension is not installed and when running in SSR/Node. + +### DaVinci + +```ts +import { davinci } from '@forgerock/davinci-client'; +import { attachDevToolsBridge } from '@forgerock/devtools-bridge'; + +const client = await davinci({ config }); +attachDevToolsBridge(client, config); +``` + +### AM Journey + +```ts +import { attachJourneyBridge } from '@forgerock/devtools-bridge'; + +attachJourneyBridge(journeyClient, config); +``` + +### OIDC / OAuth + +```ts +import { attachOidcBridge } from '@forgerock/devtools-bridge'; + +attachOidcBridge(oidcClient, { clientId: 'my-spa-client', ...config }); +``` + +--- + +## Panel views + +### Timeline + +A chronological table of all captured events. Each row shows timestamp, event type, source, and status with colour-coded error/CORS flags. A **graph sidebar** draws a vertical SVG rail of SDK node-change events with status-coloured circles and connector lines — click a node in the rail to jump to it in the table. Click any row to open its Inspector panel. + +**Inspector tabs** — the right-hand panel shows contextual tabs depending on the selected event: + +| Tab | Shows | Appears for | +| -------------- | ----------------------------------------------------------------------------- | ---------------------- | +| **Headers** | Request and response headers with copy-to-clipboard | Network events | +| **Body** | Request/response bodies with a collapsible JSON tree viewer | Network events | +| **SDK State** | Full node data — status, tokens, errors, collectors, authorization | SDK events | +| **Collectors** | Interactive collector list with copy-all button | SDK node-change events | +| **Cookies** | Cookie values with before/after diff highlighting | Session cookie events | +| **Session** | Before/after values for localStorage changes | Session storage events | +| **Config** | SDK configuration JSON | Config events | +| **CORS** | Failure reason, preflight status, `Allow-Origin` / `Allow-Credentials` values | CORS-flagged events | +| **Diagnosis** | Severity, title, description, relevant data pairs, remediation steps | Events with issues | + +### Flow + +A visual representation of the authentication flow as a sequence of SDK nodes. The node rail draws coloured circles for each node with arrows connecting them, status and node-name labels, and a glow effect on the selected node. Selecting a node opens a detail card with contextual information — collectors for DaVinci, callbacks for Journey steps, phase and error data for OIDC — plus any causally linked network requests with expandable request/response bodies. The Flow Health banner appears above the rail when the diagnosis engine detects issues. + +--- + +## Packages + +| Package | Description | +| ------------------------------- | ----------------------------------------------------------------- | +| `@forgerock/devtools-extension` | The Chrome extension (this package — private, not published) | +| `@forgerock/devtools-bridge` | Opt-in SDK adapter — emits `AuthEvent`s from subscribable clients | +| `@forgerock/devtools-types` | Shared `AuthEvent` Effect Schema definitions and TypeScript types | diff --git a/packages/devtools-extension/elm-tooling.json b/packages/devtools-extension/elm-tooling.json new file mode 100644 index 0000000000..3ceee7dd78 --- /dev/null +++ b/packages/devtools-extension/elm-tooling.json @@ -0,0 +1,5 @@ +{ + "tools": { + "elm": "0.19.1" + } +} diff --git a/packages/devtools-extension/elm.json b/packages/devtools-extension/elm.json new file mode 100644 index 0000000000..43146d7ded --- /dev/null +++ b/packages/devtools-extension/elm.json @@ -0,0 +1,23 @@ +{ + "type": "application", + "source-directories": ["src/panel/src", "src/panel"], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/browser": "1.0.2", + "elm/core": "1.0.5", + "elm/html": "1.0.1", + "elm/json": "1.1.4", + "elm/svg": "1.0.1", + "elm/time": "1.0.0" + }, + "indirect": { + "elm/url": "1.0.0", + "elm/virtual-dom": "1.0.5" + } + }, + "test-dependencies": { + "direct": {}, + "indirect": {} + } +} diff --git a/packages/devtools-extension/eslint.config.mjs b/packages/devtools-extension/eslint.config.mjs new file mode 100644 index 0000000000..42816a85ca --- /dev/null +++ b/packages/devtools-extension/eslint.config.mjs @@ -0,0 +1,7 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [ + { ignores: ['**/dist', '**/elm-stuff'] }, + ...baseConfig, + { files: ['**/*.ts'], rules: {} }, +]; diff --git a/packages/devtools-extension/manifest.json b/packages/devtools-extension/manifest.json new file mode 100644 index 0000000000..6620f9698a --- /dev/null +++ b/packages/devtools-extension/manifest.json @@ -0,0 +1,29 @@ +{ + "manifest_version": 3, + "name": "Ping DevTools", + "version": "0.1.0", + "description": "Debug ForgeRock AM and PingOne auth flows", + "permissions": ["storage", "clipboardWrite", "clipboardRead"], + "host_permissions": [""], + "devtools_page": "devtools.html", + "background": { + "service_worker": "background/service-worker.js", + "type": "module" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["content/content-script.js"], + "run_at": "document_idle", + "world": "MAIN" + }, + { + "matches": [""], + "js": ["content/relay.js"], + "run_at": "document_idle" + } + ], + "action": { + "default_title": "Ping DevTools" + } +} diff --git a/packages/devtools-extension/package.json b/packages/devtools-extension/package.json new file mode 100644 index 0000000000..74996d081a --- /dev/null +++ b/packages/devtools-extension/package.json @@ -0,0 +1,30 @@ +{ + "name": "@forgerock/devtools-extension", + "version": "2.0.0", + "private": true, + "description": "Ping Auth DevTools Chrome Extension", + "license": "MIT", + "author": "ForgeRock", + "repository": { + "type": "git", + "url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git", + "directory": "packages/devtools-extension" + }, + "type": "module", + "scripts": { + "postinstall": "elm-tooling install" + }, + "dependencies": { + "@forgerock/devtools-types": "workspace:*", + "effect": "catalog:effect" + }, + "devDependencies": { + "@types/chrome": "^0.1.40", + "elm-tooling": "^1.15.1", + "esbuild": "^0.28.0", + "terser": "^5.47.1" + }, + "nx": { + "tags": ["scope:devtools-extension"] + } +} diff --git a/packages/devtools-extension/project.json b/packages/devtools-extension/project.json new file mode 100644 index 0000000000..639e4b587a --- /dev/null +++ b/packages/devtools-extension/project.json @@ -0,0 +1,36 @@ +{ + "name": "devtools-extension", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/devtools-extension/src", + "projectType": "application", + "targets": { + "build": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/devtools-extension", + "commands": [ + "node_modules/.bin/esbuild src/devtools/devtools.ts --bundle --outfile=dist/devtools.js --format=esm --platform=browser", + "node_modules/.bin/esbuild src/panel/panel.ts --bundle --outfile=dist/panel/panel.js --format=esm --platform=browser", + "node_modules/.bin/esbuild src/background/service-worker.ts --bundle --outfile=dist/background/service-worker.js --format=esm --platform=browser --footer:js=\"export {}\"", + "node_modules/.bin/esbuild src/content/content-script.ts --bundle --outfile=dist/content/content-script.js --format=iife --platform=browser", + "node_modules/.bin/esbuild src/content/relay.ts --bundle --outfile=dist/content/relay.js --format=iife --platform=browser", + "mkdir -p dist/panel && node_modules/.bin/elm make src/panel/Main.elm --output=dist/panel/elm.js --optimize", + "node_modules/.bin/terser dist/panel/elm.js --compress 'pure_funcs=[\"F2\",\"F3\",\"F4\",\"F5\",\"F6\",\"F7\",\"F8\",\"F9\",\"A2\",\"A3\",\"A4\",\"A5\",\"A6\",\"A7\",\"A8\",\"A9\"],pure_getters,keep_fargs=false,unsafe_comps,unsafe' --mangle --output dist/panel/elm.js", + "cp manifest.json dist/manifest.json", + "cp src/devtools/devtools.html dist/devtools.html", + "cp src/panel/panel.html dist/panel/panel.html" + ], + "parallel": false + }, + "outputs": ["{projectRoot}/dist"], + "dependsOn": ["^build"] + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["{projectRoot}/coverage"], + "options": { + "passWithNoTests": true + } + } + } +} diff --git a/packages/devtools-extension/src/background/diagnosis-engine.test.ts b/packages/devtools-extension/src/background/diagnosis-engine.test.ts new file mode 100644 index 0000000000..18e9e79c9f --- /dev/null +++ b/packages/devtools-extension/src/background/diagnosis-engine.test.ts @@ -0,0 +1,533 @@ +import { describe, it, expect } from 'vitest'; +import { runFlowRules, runEventRules, runDiagnosis } from './diagnosis-engine.js'; +import type { AuthEvent } from '@forgerock/devtools-types'; + +// ─── Fixtures ──────────────────────────────────────────────────────────────── + +const makeNetworkEvent = (overrides: Partial = {}): AuthEvent => ({ + id: 'net-1', + timestamp: 1000, + type: 'network:response', + source: 'network', + flowId: 'flow-1', + causedBy: null, + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 200, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: { 'content-type': 'application/json' }, + duration: 100, + }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + ...overrides, +}); + +const makeSdkEvent = (overrides: Partial = {}): AuthEvent => ({ + id: 'sdk-1', + timestamp: 2000, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + data: { + _tag: 'sdk', + nodeStatus: 'continue', + interactionId: 'int-abc', + interactionToken: 'tok-xyz', + }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + ...overrides, +}); + +// ─── CORS Rules ─────────────────────────────────────────────────────────────── + +describe('CORS rules', () => { + it('flags status 0 as CORS network failure', () => { + const events = [ + makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'cors:status-zero')).toBe(true); + const issue = result.find((i) => i.id === 'cors:status-zero')!; + expect(issue.severity).toBe('error'); + expect(issue.category).toBe('cors'); + expect(issue.steps.length).toBeGreaterThan(0); + expect(issue.relatedEventIds).toContain('net-1'); + }); + + it('flags missing access-control-allow-origin when origin was sent', () => { + const events = [ + makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 403, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 50, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'cors:missing-allow-origin')).toBe(true); + }); + + it('does not flag missing allow-origin when origin was NOT sent', () => { + const events = [ + makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 403, + requestHeaders: {}, + responseHeaders: {}, + duration: 50, + }, + flags: { isCors: false, isError: true, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'cors:missing-allow-origin')).toBe(false); + }); + + it('flags wildcard CORS with credentials', () => { + const events = [ + makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 200, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: { + 'access-control-allow-origin': '*', + 'access-control-allow-credentials': 'true', + }, + duration: 50, + }, + flags: { isCors: true, isError: false, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'cors:wildcard-with-credentials')).toBe(true); + const issue = result.find((i) => i.id === 'cors:wildcard-with-credentials')!; + expect(issue.severity).toBe('error'); + }); + + it('deduplicates CORS issues — same origin produces one issue', () => { + const events = [ + makeNetworkEvent({ + id: 'net-1', + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }), + makeNetworkEvent({ + id: 'net-2', + data: { + _tag: 'network', + url: '/davinci/flows/step2', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + const corsStatusZeroIssues = result.filter((i) => i.id === 'cors:status-zero'); + expect(corsStatusZeroIssues.length).toBe(1); + // But both event IDs should be in relatedEventIds + expect(corsStatusZeroIssues[0].relatedEventIds).toContain('net-1'); + expect(corsStatusZeroIssues[0].relatedEventIds).toContain('net-2'); + }); +}); + +// ─── Token / Session Rules ──────────────────────────────────────────────────── + +describe('Token/Session rules', () => { + it('flags interactionToken missing on non-first sdk:node-change', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-1', + data: { + _tag: 'sdk', + nodeStatus: 'continue', + interactionId: 'int-abc', + interactionToken: 'tok-xyz', + }, + }), + makeSdkEvent({ + id: 'sdk-2', + timestamp: 3000, + data: { _tag: 'sdk', nodeStatus: 'continue', interactionId: 'int-abc' }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'token:missing-interaction-token')).toBe(true); + }); + + it('does not flag missing interactionToken on the first sdk:node-change', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-1', + data: { _tag: 'sdk', nodeStatus: 'continue', interactionId: 'int-abc' }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'token:missing-interaction-token')).toBe(false); + }); + + it('flags SESSION_NOT_FOUND error code', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-err', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { + _tag: 'sdk', + nodeStatus: 'error', + error: { code: 'SESSION_NOT_FOUND', message: 'Session not found', type: 'SESSION_ERROR' }, + }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'token:session-not-found')).toBe(true); + const issue = result.find((i) => i.id === 'token:session-not-found')!; + expect(issue.severity).toBe('error'); + }); + + it('flags INVALID_SESSION error code', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-err', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { + _tag: 'sdk', + nodeStatus: 'error', + error: { code: 'INVALID_SESSION', message: 'Invalid session', type: 'SESSION_ERROR' }, + }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'token:session-not-found')).toBe(true); + }); +}); + +// ─── Flow Config Rules ──────────────────────────────────────────────────────── + +describe('Flow Config rules', () => { + it('flags nodeStatus error', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-err', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { _tag: 'sdk', nodeStatus: 'error', nodeName: 'Registration Form' }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'flow:node-error')).toBe(true); + const issue = result.find((i) => i.id === 'flow:node-error')!; + expect(issue.severity).toBe('error'); + expect(issue.title).toContain('Registration Form'); + expect(issue.relatedEventIds).toContain('sdk-err'); + }); + + it('flags nodeStatus failure', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-fail', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { _tag: 'sdk', nodeStatus: 'failure' }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'flow:node-error')).toBe(true); + }); + + it('flags CONNECTOR_ERROR', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-conn', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { + _tag: 'sdk', + nodeStatus: 'error', + error: { + code: 'CONNECTOR_ERROR', + message: 'Connector failed', + type: 'CONNECTOR', + internalHttpStatus: 400, + }, + }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'flow:connector-error')).toBe(true); + const issue = result.find((i) => i.id === 'flow:connector-error')!; + expect(issue.title).toContain('400'); + }); + + it('flags NOT_FOUND error code', () => { + const events = [ + makeSdkEvent({ + id: 'sdk-nf', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { + _tag: 'sdk', + nodeStatus: 'error', + error: { code: 'NOT_FOUND', message: 'Policy not found', type: 'NOT_FOUND' }, + }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'flow:policy-not-found')).toBe(true); + }); +}); + +// ─── OIDC Rules ─────────────────────────────────────────────────────────────── + +describe('OIDC rules', () => { + it('flags state_mismatch in redirect URI', () => { + const events = [ + makeNetworkEvent({ + id: 'oidc-1', + type: 'dom:redirect', + source: 'dom', + data: { + _tag: 'dom', + url: 'https://app.example.com/callback?error=state_mismatch', + }, + flags: { isCors: false, isError: true, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'oidc:state-mismatch')).toBe(true); + const issue = result.find((i) => i.id === 'oidc:state-mismatch')!; + expect(issue.severity).toBe('error'); + }); + + it('flags PKCE challenge missing', () => { + const events = [ + makeNetworkEvent({ + id: 'oidc-2', + type: 'dom:redirect', + source: 'dom', + data: { + _tag: 'dom', + url: 'https://app.example.com/callback?error=invalid_request&error_description=code_challenge+missing', + }, + flags: { isCors: false, isError: true, isAuthRelated: true }, + }), + ]; + const result = runFlowRules(events); + expect(result.some((i) => i.id === 'oidc:pkce-missing')).toBe(true); + }); +}); + +// ─── runEventRules ──────────────────────────────────────────────────────────── + +describe('runEventRules', () => { + it('returns empty array for a clean network event', () => { + const event = makeNetworkEvent(); + const result = runEventRules(event, [event]); + expect(result).toEqual([]); + }); + + it('annotates status-0 network event', () => { + const event = makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }); + const result = runEventRules(event, [event]); + expect(result.length).toBeGreaterThan(0); + expect(result[0].severity).toBe('error'); + }); + + it('annotates sdk:node-change with error status', () => { + const event = makeSdkEvent({ + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { _tag: 'sdk', nodeStatus: 'error', nodeName: 'Login Form' }, + }); + const result = runEventRules(event, [event]); + expect(result.some((i) => i.title.includes('Node error'))).toBe(true); + }); +}); + +// ─── runDiagnosis (integration) ─────────────────────────────────────────────── + +describe('runDiagnosis', () => { + it('returns healthy when no issues', () => { + const events = [makeNetworkEvent(), makeSdkEvent()]; + const result = runDiagnosis(events); + expect(result.flowHealth).toBe('healthy'); + expect(result.issues).toHaveLength(0); + }); + + it('returns error health when error issues present', () => { + const events = [ + makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }), + ]; + const result = runDiagnosis(events); + expect(result.flowHealth).toBe('error'); + }); + + it('returns warning health when only warning issues present', () => { + const events = [ + makeNetworkEvent({ + id: 'net-warn', + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 200, + // cookie in request triggers credentials-not-allowed check + requestHeaders: { origin: 'http://localhost:3000', cookie: 'session=abc' }, + responseHeaders: { + // include allow-origin so cors:missing-allow-origin error doesn't fire + 'access-control-allow-origin': 'http://localhost:3000', + 'access-control-allow-credentials': 'false', + }, + duration: 50, + }, + flags: { isCors: true, isError: false, isAuthRelated: true }, + }), + ]; + const result = runDiagnosis(events); + // credentials not allowed warning + expect(['warning', 'healthy']).toContain(result.flowHealth); + }); + + it('populates annotatedEvents for affected events', () => { + const event = makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }); + const result = runDiagnosis([event]); + expect(result.annotatedEvents.has('net-1')).toBe(true); + }); + + it('issues are ordered: error before warning before info', () => { + const events = [ + makeNetworkEvent({ + id: 'net-1', + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 0, + requestHeaders: { origin: 'http://localhost:3000' }, + responseHeaders: {}, + duration: 0, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }), + makeSdkEvent({ + id: 'sdk-1', + flags: { isCors: false, isError: true, isAuthRelated: true }, + data: { _tag: 'sdk', nodeStatus: 'error' }, + }), + ]; + const result = runDiagnosis(events); + const severities = result.issues.map((i) => i.severity); + // All errors should come before warnings + const firstWarningIdx = severities.indexOf('warning'); + const lastErrorIdx = severities.lastIndexOf('error'); + if (firstWarningIdx !== -1 && lastErrorIdx !== -1) { + expect(lastErrorIdx).toBeLessThan(firstWarningIdx); + } + }); +}); + +// ─── Expired JWT via runEventRules ──────────────────────────────────────────── + +describe('expired JWT detection in runEventRules', () => { + const makeExpiredJwt = () => { + // header: {"alg":"RS256","typ":"JWT"} + const header = btoa(JSON.stringify({ alg: 'RS256', typ: 'JWT' })) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + // payload: {"sub":"user","exp": 1 (way in the past)} + const payload = btoa(JSON.stringify({ sub: 'user', exp: 1 })) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return `${header}.${payload}.fakesig`; + }; + + it('flags expired JWT in authorization header', () => { + const event = makeNetworkEvent({ + data: { + _tag: 'network', + url: '/davinci/flows', + method: 'POST', + status: 401, + requestHeaders: { authorization: `Bearer ${makeExpiredJwt()}` }, + responseHeaders: {}, + duration: 50, + }, + flags: { isCors: false, isError: true, isAuthRelated: true }, + }); + const result = runEventRules(event, [event]); + expect( + result.some( + (i) => + i.title.includes('expired') || + i.title.includes('Expired') || + i.title.toLowerCase().includes('token'), + ), + ).toBe(true); + }); +}); diff --git a/packages/devtools-extension/src/background/diagnosis-engine.ts b/packages/devtools-extension/src/background/diagnosis-engine.ts new file mode 100644 index 0000000000..20f3dc35e3 --- /dev/null +++ b/packages/devtools-extension/src/background/diagnosis-engine.ts @@ -0,0 +1,507 @@ +import type { AuthEvent } from '@forgerock/devtools-types'; + +export type Severity = 'error' | 'warning' | 'info'; + +export interface FlowIssue { + id: string; + severity: Severity; + category: 'cors' | 'token' | 'flow-config' | 'oidc'; + title: string; + description: string; + steps: string[]; + relatedEventIds: string[]; + relevantData?: Record; +} + +export interface EventIssue { + severity: Severity; + title: string; + description: string; + steps: string[]; + relevantData?: Record; +} + +export interface DiagnosisResult { + issues: FlowIssue[]; + annotatedEvents: Map; + flowHealth: 'healthy' | 'warning' | 'error'; +} + +// ─── JWT helpers ────────────────────────────────────────────────────────────── + +const JWT_PATTERN = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/; + +function decodeJwtPayload(token: string): Record | null { + const parts = token.split('.'); + if (parts.length !== 3) return null; + try { + const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); + return JSON.parse(atob(b64)) as Record; + } catch { + return null; + } +} + +function extractJwt(value: string): string | null { + const bearer = value.match(/^Bearer\s+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/i); + if (bearer) return bearer[1]; + if (JWT_PATTERN.test(value)) return value; + return null; +} + +function findExpiredJwtsInHeaders(headers: Record): string[] { + const expired: string[] = []; + for (const value of Object.values(headers)) { + const token = extractJwt(value); + if (!token) continue; + const payload = decodeJwtPayload(token); + if (payload && typeof payload['exp'] === 'number' && payload['exp'] * 1000 < Date.now()) { + expired.push(token); + } + } + return expired; +} + +// ─── Deduplication helper ───────────────────────────────────────────────────── + +type IssueCandidate = { + dedupKey: string; + eventId: string; + issue: Omit; +}; + +function mergeByDedupKey(candidates: IssueCandidate[]): FlowIssue[] { + const merged = new Map(); + for (const { dedupKey, eventId, issue } of candidates) { + const existing = merged.get(dedupKey); + if (existing) { + merged.set(dedupKey, { + ...existing, + relatedEventIds: [...existing.relatedEventIds, eventId], + }); + } else { + merged.set(dedupKey, { ...issue, relatedEventIds: [eventId] }); + } + } + return [...merged.values()]; +} + +// ─── CORS rules ─────────────────────────────────────────────────────────────── + +function collectCorsIssues(events: readonly AuthEvent[]): IssueCandidate[] { + const candidates: IssueCandidate[] = []; + + for (const event of events) { + if (event.data._tag !== 'network') continue; + const { data } = event; + const origin = data.requestHeaders['origin'] ?? ''; + const allowOrigin = data.responseHeaders['access-control-allow-origin'] ?? ''; + const allowCredentials = data.responseHeaders['access-control-allow-credentials'] ?? ''; + const hasOriginHeader = 'origin' in data.requestHeaders; + + if (data.status === 0 && event.flags.isCors) { + candidates.push({ + dedupKey: `cors:status-zero:${origin}`, + eventId: event.id, + issue: { + id: 'cors:status-zero', + severity: 'error', + category: 'cors', + title: 'Network failure (status 0)', + description: + 'The request never reached the server. This is almost always a CORS preflight rejection.', + steps: [ + `Your auth server must include this origin in allowed origins: ${origin || '(unknown)'}`, + 'Check the OPTIONS preflight request in the Network tab.', + 'If using credentials, wildcard (*) is not allowed as the allowed origin.', + ], + relevantData: origin ? { origin } : undefined, + }, + }); + } + + if (hasOriginHeader && !allowOrigin && data.status !== 0 && event.flags.isCors) { + candidates.push({ + dedupKey: `cors:missing-allow-origin:${origin}`, + eventId: event.id, + issue: { + id: 'cors:missing-allow-origin', + severity: 'error', + category: 'cors', + title: 'Missing CORS header', + description: 'The server response is missing Access-Control-Allow-Origin.', + steps: [ + `Add ${origin} to allowed origins on your auth server.`, + 'Verify the request origin matches what is configured in your AS CORS settings.', + ], + relevantData: { 'missing-header': 'access-control-allow-origin', origin }, + }, + }); + } + + if (allowOrigin === '*' && allowCredentials === 'true') { + candidates.push({ + dedupKey: `cors:wildcard-with-credentials:${data.url}`, + eventId: event.id, + issue: { + id: 'cors:wildcard-with-credentials', + severity: 'error', + category: 'cors', + title: 'Wildcard CORS with credentials', + description: + 'access-control-allow-origin: * cannot be used together with access-control-allow-credentials: true.', + steps: [ + `Replace wildcard with an explicit origin: ${origin || '(your app origin)'}`, + 'Configure your auth server to reflect the specific requesting origin.', + ], + relevantData: { + 'access-control-allow-origin': '*', + 'access-control-allow-credentials': 'true', + }, + }, + }); + } + + if ( + hasOriginHeader && + allowCredentials === 'false' && + data.requestHeaders['cookie'] !== undefined + ) { + candidates.push({ + dedupKey: `cors:credentials-not-allowed:${origin}`, + eventId: event.id, + issue: { + id: 'cors:credentials-not-allowed', + severity: 'warning', + category: 'cors', + title: 'Credentials not allowed by server', + description: + 'The server set access-control-allow-credentials: false but cookies were sent.', + steps: [ + 'Enable credentials on the auth server CORS config.', + 'Or remove the cookie from the request.', + ], + }, + }); + } + } + + return candidates; +} + +// ─── Token / Session rules ──────────────────────────────────────────────────── + +function collectTokenIssues(events: readonly AuthEvent[]): IssueCandidate[] { + const candidates: IssueCandidate[] = []; + + const sdkNodeEvents = events.filter((e) => e.type === 'sdk:node-change'); + + // Missing interactionToken on non-first sdk:node-change + if (sdkNodeEvents.length > 1) { + for (const event of sdkNodeEvents.slice(1)) { + if (event.data._tag !== 'sdk') continue; + if (!event.data.interactionToken) { + candidates.push({ + dedupKey: `token:missing-interaction-token:${event.id}`, + eventId: event.id, + issue: { + id: 'token:missing-interaction-token', + severity: 'warning', + category: 'token', + title: 'Missing interaction token', + description: 'interactionToken was absent on a node transition that required it.', + steps: [ + 'Check SDK initialization — do not cache or reuse stale tokens across flows.', + 'Ensure each flow starts fresh rather than resuming an expired interaction.', + ], + }, + }); + } + } + } + + // Session error codes + for (const event of events) { + if (event.data._tag !== 'sdk') continue; + const errorCode = event.data.error?.code ?? ''; + if (errorCode.includes('SESSION_NOT_FOUND') || errorCode.includes('INVALID_SESSION')) { + candidates.push({ + dedupKey: `token:session-not-found`, + eventId: event.id, + issue: { + id: 'token:session-not-found', + severity: 'error', + category: 'token', + title: 'Session not found', + description: 'The session referenced by this flow no longer exists on the server.', + steps: [ + 'Session may have expired — reinitialize the SDK.', + 'Avoid persisting flowId or interactionId across page reloads without validation.', + ], + relevantData: { 'error-code': errorCode }, + }, + }); + } + } + + return candidates; +} + +// ─── Flow Config rules ──────────────────────────────────────────────────────── + +function collectFlowConfigIssues(events: readonly AuthEvent[]): IssueCandidate[] { + const candidates: IssueCandidate[] = []; + + for (const event of events) { + if (event.data._tag !== 'sdk') continue; + const { data } = event; + const { nodeStatus } = data; + const errorCode = data.error?.code ?? ''; + + if (nodeStatus === 'error' || nodeStatus === 'failure') { + const nodeName = data.nodeName ?? ''; + candidates.push({ + dedupKey: `flow:node-error:${event.id}`, + eventId: event.id, + issue: { + id: 'flow:node-error', + severity: 'error', + category: 'flow-config', + title: nodeName ? `Node error: ${nodeName}` : 'Node error', + description: `A DaVinci node returned status "${nodeStatus}".`, + steps: [ + 'Check connector configuration in DaVinci admin.', + 'Review the error code in the SDK State tab.', + ], + relevantData: nodeName ? { node: nodeName, status: nodeStatus } : { status: nodeStatus }, + }, + }); + } + + if (errorCode === 'CONNECTOR_ERROR') { + const httpStatus = data.error?.internalHttpStatus; + candidates.push({ + dedupKey: `flow:connector-error:${event.id}`, + eventId: event.id, + issue: { + id: 'flow:connector-error', + severity: 'error', + category: 'flow-config', + title: httpStatus ? `Connector error (HTTP ${httpStatus})` : 'Connector error', + description: 'A DaVinci connector returned an HTTP error from its upstream endpoint.', + steps: [ + 'Verify connector credentials and endpoint URL in DaVinci admin.', + 'Check the upstream service is reachable from your DaVinci environment.', + ], + relevantData: httpStatus ? { 'internal-http-status': String(httpStatus) } : undefined, + }, + }); + } + + if (errorCode === 'NOT_FOUND') { + candidates.push({ + dedupKey: `flow:policy-not-found`, + eventId: event.id, + issue: { + id: 'flow:policy-not-found', + severity: 'error', + category: 'flow-config', + title: 'Flow policy not found', + description: 'The policy ID used to start this flow does not exist in the environment.', + steps: [ + 'Verify the policy ID (acr_values or flowId) matches your DaVinci environment.', + 'Check that the policy is published and assigned to the correct application.', + ], + }, + }); + } + } + + return candidates; +} + +// ─── OIDC rules ─────────────────────────────────────────────────────────────── + +function collectOidcIssues(events: readonly AuthEvent[]): IssueCandidate[] { + const candidates: IssueCandidate[] = []; + + for (const event of events) { + if (event.data._tag !== 'dom') continue; + const url = event.data.url ?? ''; + + if (url.includes('error=state_mismatch')) { + candidates.push({ + dedupKey: `oidc:state-mismatch`, + eventId: event.id, + issue: { + id: 'oidc:state-mismatch', + severity: 'error', + category: 'oidc', + title: 'State mismatch', + description: + 'The OAuth state parameter in the callback does not match the one sent in the authorization request.', + steps: [ + 'Do not share auth state across tabs.', + 'Check your PKCE/state implementation for race conditions.', + 'Ensure the state is stored and compared correctly on the callback.', + ], + }, + }); + } + + if (url.includes('error=invalid_request') && url.includes('code_challenge')) { + candidates.push({ + dedupKey: `oidc:pkce-missing`, + eventId: event.id, + issue: { + id: 'oidc:pkce-missing', + severity: 'error', + category: 'oidc', + title: 'PKCE challenge missing', + description: 'The authorization request was missing the required PKCE code_challenge.', + steps: [ + 'Ensure the SDK is configured with PKCE enabled.', + 'Verify the client application requires PKCE in your AS client configuration.', + ], + }, + }); + } + + if (url.includes('error=invalid_request') && url.includes('redirect_uri')) { + candidates.push({ + dedupKey: `oidc:redirect-uri-mismatch`, + eventId: event.id, + issue: { + id: 'oidc:redirect-uri-mismatch', + severity: 'error', + category: 'oidc', + title: 'Redirect URI mismatch', + description: + 'The redirect URI in the request does not match any URI registered in the AS client.', + steps: [ + 'Register the exact redirect URI used by your app in the AS client configuration.', + 'Ensure no trailing slashes or protocol mismatches.', + ], + }, + }); + } + } + + return candidates; +} + +// ─── Public API ─────────────────────────────────────────────────────────────── + +const SEVERITY_ORDER: Record = { error: 0, warning: 1, info: 2 }; + +export function runFlowRules(events: readonly AuthEvent[]): FlowIssue[] { + const candidates: IssueCandidate[] = [ + ...collectCorsIssues(events), + ...collectTokenIssues(events), + ...collectFlowConfigIssues(events), + ...collectOidcIssues(events), + ]; + + return mergeByDedupKey(candidates).sort( + (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity], + ); +} + +export function runEventRules(event: AuthEvent, allEvents: readonly AuthEvent[]): EventIssue[] { + const issues: EventIssue[] = []; + + if (event.data._tag === 'network') { + const { data } = event; + + if (data.status === 0 && event.flags.isCors) { + const origin = data.requestHeaders['origin'] ?? ''; + issues.push({ + severity: 'error', + title: 'Network failure (status 0)', + description: + 'The request never reached the server. This is almost always a CORS preflight rejection.', + steps: [ + `Your AS must include this origin in allowed origins: ${origin || '(unknown)'}`, + 'If using credentials, wildcard (*) is not allowed.', + 'Check the OPTIONS preflight in the Network tab.', + ], + relevantData: { + 'access-control-allow-origin': + data.responseHeaders['access-control-allow-origin'] ?? '(not present)', + 'access-control-allow-credentials': + data.responseHeaders['access-control-allow-credentials'] ?? '(not present)', + }, + }); + } + + // Expired JWT in request headers + const expiredJwts = findExpiredJwtsInHeaders(data.requestHeaders); + for (const token of expiredJwts) { + const payload = decodeJwtPayload(token); + const exp = payload && typeof payload['exp'] === 'number' ? payload['exp'] : null; + issues.push({ + severity: 'error', + title: 'Token expired', + description: 'A JWT in the request headers has an expired exp claim.', + steps: ['Restart the flow to obtain a fresh token.', 'Check your SDK token refresh logic.'], + relevantData: exp ? { exp: new Date(exp * 1000).toISOString() } : undefined, + }); + } + + const hasOriginHeader = 'origin' in data.requestHeaders; + const allowOrigin = data.responseHeaders['access-control-allow-origin'] ?? ''; + if (hasOriginHeader && !allowOrigin && data.status !== 0 && event.flags.isCors) { + issues.push({ + severity: 'error', + title: 'Missing CORS header', + description: 'The server response is missing Access-Control-Allow-Origin.', + steps: [`Add ${data.requestHeaders['origin']} to allowed origins on your auth server.`], + relevantData: { 'missing-header': 'access-control-allow-origin' }, + }); + } + } + + if (event.data._tag === 'sdk') { + const { data } = event; + if (data.nodeStatus === 'error' || data.nodeStatus === 'failure') { + const nodeName = data.nodeName ?? ''; + issues.push({ + severity: 'error', + title: nodeName ? `Node error: ${nodeName}` : 'Node error', + description: `Node returned status "${data.nodeStatus}".`, + steps: [ + 'Check DaVinci connector configuration.', + 'Review the error code in the SDK State tab.', + ], + relevantData: data.error + ? { code: data.error.code, message: data.error.message } + : undefined, + }); + } + } + + // Suppress unused-parameter warning — allEvents available for future cross-event per-event rules + void allEvents; + + return issues; +} + +export function runDiagnosis(events: readonly AuthEvent[]): DiagnosisResult { + const issues = runFlowRules(events); + + const annotatedEvents = new Map(); + for (const event of events) { + const eventIssues = runEventRules(event, events); + if (eventIssues.length > 0) { + annotatedEvents.set(event.id, eventIssues); + } + } + + const flowHealth = issues.some((i) => i.severity === 'error') + ? 'error' + : issues.some((i) => i.severity === 'warning') + ? 'warning' + : 'healthy'; + + return { issues, annotatedEvents, flowHealth }; +} diff --git a/packages/devtools-extension/src/background/event-store.service.test.ts b/packages/devtools-extension/src/background/event-store.service.test.ts new file mode 100644 index 0000000000..cb4746d7ce --- /dev/null +++ b/packages/devtools-extension/src/background/event-store.service.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect } from 'vitest'; +import { Effect } from 'effect'; +import { EventStoreService, EventStoreLive, makeEmptyFlowState } from './event-store.service.js'; +import type { AuthEvent } from '@forgerock/devtools-types'; + +const makeEvent = (overrides: Partial = {}): AuthEvent => ({ + id: 'e1', + timestamp: 100, + type: 'network:response', + source: 'network', + flowId: null, + causedBy: null, + data: { + _tag: 'network', + url: '/authorize', + method: 'POST', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 50, + }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + ...overrides, +}); + +describe('EventStoreService', () => { + it('appends events to state', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent()); + yield* store.append(makeEvent({ id: 'e2' })); + return yield* store.getState(); + }); + + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + + expect(state.events).toHaveLength(2); + expect(state.events[0].id).toBe('e1'); + }); + + it('increments errorCount for error events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append( + makeEvent({ flags: { isCors: false, isError: true, isAuthRelated: true } }), + ); + return yield* store.getState(); + }); + + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.errorCount).toBe(1); + }); + + it('clears state', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent()); + yield* store.clear(); + return yield* store.getState(); + }); + + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.events).toHaveLength(0); + }); +}); + +describe('lastSdkEventId tracking', () => { + it('sets lastSdkEventId when an sdk:node-change event is appended', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append( + makeEvent({ + id: 'sdk-1', + type: 'sdk:node-change', + source: 'sdk', + data: { _tag: 'sdk', nodeStatus: 'continue' }, + }), + ); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.lastSdkEventId).toBe('sdk-1'); + }); + + it('does not update lastSdkEventId for network events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'net-1' })); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.lastSdkEventId).toBeNull(); + }); + + it('updates lastSdkEventId to the most recent sdk event', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append( + makeEvent({ + id: 'sdk-1', + type: 'sdk:node-change', + source: 'sdk', + data: { _tag: 'sdk', nodeStatus: 'continue' }, + }), + ); + yield* store.append( + makeEvent({ + id: 'sdk-2', + type: 'sdk:node-change', + source: 'sdk', + data: { _tag: 'sdk', nodeStatus: 'success' }, + }), + ); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.lastSdkEventId).toBe('sdk-2'); + }); +}); diff --git a/packages/devtools-extension/src/background/event-store.service.ts b/packages/devtools-extension/src/background/event-store.service.ts new file mode 100644 index 0000000000..a1d913da19 --- /dev/null +++ b/packages/devtools-extension/src/background/event-store.service.ts @@ -0,0 +1,78 @@ +import { Context, Effect, Layer, Ref, pipe } from 'effect'; +import type { AuthEvent, FlowState } from '@forgerock/devtools-types'; + +export function makeEmptyFlowState(): FlowState { + return { + flowId: null, + capturedAt: new Date().toISOString(), + events: [], + summary: { nodeCount: 0, errorCount: 0, corsFlags: [], duration: 0, sdkConnected: false }, + lastSdkEventId: null, + }; +} + +function updateSummary(state: FlowState, event: AuthEvent): FlowState { + const summary = { ...state.summary }; + + if (event.flags.isError) summary.errorCount += 1; + if (event.type === 'sdk:node-change') { + summary.nodeCount += 1; + summary.sdkConnected = true; + } + if (event.flags.isCors && event.data._tag === 'network' && event.data.corsFlag) { + summary.corsFlags = [...summary.corsFlags, event.data.corsFlag]; + } + + const timestamps = [...state.events, event].map((e) => e.timestamp); + summary.duration = timestamps.length > 1 ? Math.max(...timestamps) - Math.min(...timestamps) : 0; + + return { + ...state, + flowId: state.flowId ?? event.flowId, + events: [...state.events, event], + summary, + lastSdkEventId: event.type === 'sdk:node-change' ? event.id : state.lastSdkEventId, + }; +} + +export interface EventStoreServiceShape { + append: (event: AuthEvent) => Effect.Effect; + getState: () => Effect.Effect; + clear: () => Effect.Effect; + persist: () => Effect.Effect; + rehydrate: () => Effect.Effect; +} + +export class EventStoreService extends Context.Tag('EventStoreService')< + EventStoreService, + EventStoreServiceShape +>() {} + +export const EventStoreLive = Layer.effect( + EventStoreService, + pipe( + Ref.make(makeEmptyFlowState()), + Effect.map((stateRef) => ({ + append: (event: AuthEvent) => Ref.update(stateRef, (s) => updateSummary(s, event)), + getState: () => Ref.get(stateRef), + clear: () => Ref.set(stateRef, makeEmptyFlowState()), + persist: () => + pipe( + Ref.get(stateRef), + Effect.flatMap((state) => + Effect.tryPromise(() => chrome.storage.local.set({ 'ping:auth-flow': state })), + ), + Effect.orDie, + ), + rehydrate: () => + pipe( + Effect.tryPromise(() => chrome.storage.local.get('ping:auth-flow')), + Effect.orDie, + Effect.flatMap((result) => { + const stored = result['ping:auth-flow'] as FlowState | undefined; + return stored ? Ref.set(stateRef, stored) : Effect.void; + }), + ), + })), + ), +); diff --git a/packages/devtools-extension/src/background/message-handler.ts b/packages/devtools-extension/src/background/message-handler.ts new file mode 100644 index 0000000000..e6d7a44e3c --- /dev/null +++ b/packages/devtools-extension/src/background/message-handler.ts @@ -0,0 +1,46 @@ +import { Effect, Schema, Either } from 'effect'; +import { buildNetworkEvent } from '../devtools/network-observer.js'; +import { EventStoreService } from './event-store.service.js'; +import { AuthEventSchema } from '@forgerock/devtools-types'; +import type { HarEntry } from '../devtools/network-observer.js'; + +type IncomingMessage = + | { type: 'NETWORK_EVENT'; payload: HarEntry } + | { type: 'SDK_EVENT'; payload: unknown } + | { type: 'CLEAR' } + | { type: 'GET_STATE' }; + +export function handleMessage(message: IncomingMessage) { + return Effect.gen(function* () { + const store = yield* EventStoreService; + + switch (message.type) { + case 'NETWORK_EVENT': { + const state = yield* store.getState(); + const event = buildNetworkEvent(message.payload, state.flowId); + if (!event.flags.isAuthRelated) return null; + const eventWithCause = { ...event, causedBy: state.lastSdkEventId }; + yield* store.append(eventWithCause); + yield* store.persist(); + return eventWithCause; + } + case 'SDK_EVENT': { + const result = Schema.decodeUnknownEither(AuthEventSchema)(message.payload); + if (Either.isLeft(result)) { + console.warn('[Ping DevTools] Malformed SDK event:', result.left.message); + return null; + } + yield* store.append(result.right); + yield* store.persist(); + return result.right; + } + case 'CLEAR': { + yield* store.clear(); + return null; + } + case 'GET_STATE': { + return yield* store.getState(); + } + } + }); +} diff --git a/packages/devtools-extension/src/background/service-worker.ts b/packages/devtools-extension/src/background/service-worker.ts new file mode 100644 index 0000000000..2eabcfeb50 --- /dev/null +++ b/packages/devtools-extension/src/background/service-worker.ts @@ -0,0 +1,86 @@ +import { ManagedRuntime, Effect } from 'effect'; +import { EventStoreLive, EventStoreService } from './event-store.service.js'; +import { handleMessage } from './message-handler.js'; +import { runDiagnosis } from './diagnosis-engine.js'; +import type { DiagnosisResult, FlowIssue, EventIssue } from './diagnosis-engine.js'; + +interface SerializableDiagnosisResult { + issues: FlowIssue[]; + annotatedEvents: Record; + flowHealth: 'healthy' | 'warning' | 'error'; +} + +function serializeDiagnosis(diagnosis: DiagnosisResult): SerializableDiagnosisResult { + return { + issues: diagnosis.issues, + annotatedEvents: Object.fromEntries(diagnosis.annotatedEvents), + flowHealth: diagnosis.flowHealth, + }; +} + +const AppLayer = EventStoreLive; +let runtime = ManagedRuntime.make(AppLayer); + +self.addEventListener('activate', () => { + runtime = ManagedRuntime.make(AppLayer); + runtime + .runPromise( + Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.rehydrate(); + }), + ) + .catch(console.error); +}); + +function broadcastToPanel(event: unknown, diagnosis: SerializableDiagnosisResult): void { + chrome.runtime.sendMessage({ type: 'PANEL_EVENT', payload: event, diagnosis }).catch(() => { + // Panel not open — ignore + }); +} + +function runDiagnosisEffect() { + return Effect.gen(function* () { + const store = yield* EventStoreService; + const state = yield* store.getState(); + return runDiagnosis(state.events); + }); +} + +chrome.runtime.onConnect.addListener((port) => { + if (port.name !== 'devtools') return; + port.onMessage.addListener((message) => { + runtime + .runPromise( + Effect.gen(function* () { + const result = yield* handleMessage(message); + if ( + (message.type === 'NETWORK_EVENT' || message.type === 'SDK_EVENT') && + result !== null + ) { + const diagnosis = yield* runDiagnosisEffect(); + broadcastToPanel(result, serializeDiagnosis(diagnosis)); + } + return result; + }), + ) + .catch(console.error); + }); +}); + +chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { + runtime + .runPromise( + Effect.gen(function* () { + const result = yield* handleMessage(message); + if ((message.type === 'NETWORK_EVENT' || message.type === 'SDK_EVENT') && result !== null) { + const diagnosis = yield* runDiagnosisEffect(); + broadcastToPanel(result, serializeDiagnosis(diagnosis)); + } + return result; + }), + ) + .then(sendResponse) + .catch(console.error); + return true; // keep channel open for async response +}); diff --git a/packages/devtools-extension/src/content/content-script.test.ts b/packages/devtools-extension/src/content/content-script.test.ts new file mode 100644 index 0000000000..9f9f0472c3 --- /dev/null +++ b/packages/devtools-extension/src/content/content-script.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +describe('content-script (MAIN world)', () => { + beforeEach(() => { + vi.stubGlobal('__PING_DEVTOOLS_EXTENSION__', undefined); + }); + + it('sets __PING_DEVTOOLS_EXTENSION__ on window', async () => { + await import('./content-script.js'); + expect(window.__PING_DEVTOOLS_EXTENSION__).toBe(true); + }); + + it('relays pingDevtools CustomEvent payload via postMessage', async () => { + const postSpy = vi.spyOn(window, 'postMessage'); + await import('./content-script.js'); + + const payload = { id: 'test', type: 'sdk:node-change' }; + window.dispatchEvent(new CustomEvent('pingDevtools', { detail: payload })); + + expect(postSpy).toHaveBeenCalledWith({ __pingDevtools: true, payload }, '*'); + }); +}); diff --git a/packages/devtools-extension/src/content/content-script.ts b/packages/devtools-extension/src/content/content-script.ts new file mode 100644 index 0000000000..05038502b6 --- /dev/null +++ b/packages/devtools-extension/src/content/content-script.ts @@ -0,0 +1,12 @@ +declare global { + interface Window { + __PING_DEVTOOLS_EXTENSION__?: boolean; + } +} + +window.__PING_DEVTOOLS_EXTENSION__ = true; + +window.addEventListener('pingDevtools', (raw: Event) => { + const event = raw as CustomEvent; + window.postMessage({ __pingDevtools: true, payload: event.detail }, '*'); +}); diff --git a/packages/devtools-extension/src/content/relay.ts b/packages/devtools-extension/src/content/relay.ts new file mode 100644 index 0000000000..d2a4e81c26 --- /dev/null +++ b/packages/devtools-extension/src/content/relay.ts @@ -0,0 +1,9 @@ +// Runs in the isolated world — relays postMessage events to the service worker +// via chrome.runtime, which is not available in the main world. +window.addEventListener('message', (e) => { + if (e.source !== window || !(e.data as { __pingDevtools?: boolean })?.__pingDevtools) return; + chrome.runtime.sendMessage({ + type: 'SDK_EVENT', + payload: (e.data as { payload: unknown }).payload, + }); +}); diff --git a/packages/devtools-extension/src/devtools/cors-detector.test.ts b/packages/devtools-extension/src/devtools/cors-detector.test.ts new file mode 100644 index 0000000000..6c5a0d9f1c --- /dev/null +++ b/packages/devtools-extension/src/devtools/cors-detector.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import { detectCorsFlags } from './cors-detector.js'; +import type { CorsFlag } from '@forgerock/devtools-types'; + +function makeEntry(overrides: { + url?: string; + method?: string; + status?: number; + requestHeaders?: Record; + responseHeaders?: Record; +}) { + return { + request: { + url: overrides.url ?? 'https://example.com/authorize', + method: overrides.method ?? 'POST', + headers: Object.entries(overrides.requestHeaders ?? {}).map(([name, value]) => ({ + name, + value, + })), + }, + response: { + status: overrides.status ?? 200, + headers: Object.entries(overrides.responseHeaders ?? {}).map(([name, value]) => ({ + name, + value, + })), + }, + time: 0, + }; +} + +describe('detectCorsFlags', () => { + it('returns empty array for a clean request', () => { + const entry = makeEntry({ + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: { + 'access-control-allow-origin': 'https://app.example.com', + 'access-control-allow-credentials': 'true', + }, + }); + const flags = detectCorsFlags(entry); + expect(flags).toHaveLength(0); + }); + + it('flags status 0 as status-zero', () => { + const entry = makeEntry({ status: 0 }); + const flags = detectCorsFlags(entry); + expect(flags.some((f: CorsFlag) => f.reason === 'status-zero')).toBe(true); + }); + + it('flags missing allow-origin when origin header is present', () => { + const entry = makeEntry({ + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: {}, + }); + const flags = detectCorsFlags(entry); + expect(flags.some((f: CorsFlag) => f.reason === 'missing-allow-origin')).toBe(true); + }); + + it('flags wildcard allow-origin with credentials', () => { + const entry = makeEntry({ + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: { + 'access-control-allow-origin': '*', + 'access-control-allow-credentials': 'true', + }, + }); + const flags = detectCorsFlags(entry); + expect(flags.some((f: CorsFlag) => f.reason === 'wildcard-with-credentials')).toBe(true); + }); + + it('flags credentials mismatch when allow-credentials is false', () => { + const entry = makeEntry({ + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: { + 'access-control-allow-origin': 'https://app.example.com', + 'access-control-allow-credentials': 'false', + }, + }); + const flags = detectCorsFlags(entry); + expect(flags.some((f: CorsFlag) => f.reason === 'credentials-mismatch')).toBe(true); + }); + + it('flags credentials mismatch when allow-credentials header is absent', () => { + const entry = makeEntry({ + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: { + 'access-control-allow-origin': 'https://app.example.com', + // no access-control-allow-credentials header + }, + }); + const flags = detectCorsFlags(entry); + expect(flags.some((f: CorsFlag) => f.reason === 'credentials-mismatch')).toBe(true); + }); +}); diff --git a/packages/devtools-extension/src/devtools/cors-detector.ts b/packages/devtools-extension/src/devtools/cors-detector.ts new file mode 100644 index 0000000000..a25d9569c2 --- /dev/null +++ b/packages/devtools-extension/src/devtools/cors-detector.ts @@ -0,0 +1,36 @@ +import type { CorsFlag } from '@forgerock/devtools-types'; +import type { HarHeader, HarEntry } from './network-observer.js'; + +function headerValue(headers: HarHeader[], name: string): string | undefined { + return headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value; +} + +export function detectCorsFlags(entry: HarEntry): CorsFlag[] { + const flags: CorsFlag[] = []; + const { url, method, headers: reqHeaders } = entry.request; + const { status, headers: resHeaders } = entry.response; + + const origin = headerValue(reqHeaders, 'origin'); + const allowOrigin = headerValue(resHeaders, 'access-control-allow-origin'); + const allowCredentials = headerValue(resHeaders, 'access-control-allow-credentials'); + + if (status === 0) { + flags.push({ url, method, reason: 'status-zero' }); + } + + if (origin && !allowOrigin) { + flags.push({ url, method, reason: 'missing-allow-origin' }); + } + + if (allowOrigin === '*' && allowCredentials === 'true') { + flags.push({ url, method, reason: 'wildcard-with-credentials', allowOrigin, allowCredentials }); + } + + const credentialsDenied = allowCredentials === 'false' || allowCredentials === undefined; + + if (origin && allowOrigin && allowOrigin !== '*' && credentialsDenied) { + flags.push({ url, method, reason: 'credentials-mismatch', allowOrigin, allowCredentials }); + } + + return flags; +} diff --git a/packages/devtools-extension/src/devtools/devtools.html b/packages/devtools-extension/src/devtools/devtools.html new file mode 100644 index 0000000000..6dcc11161e --- /dev/null +++ b/packages/devtools-extension/src/devtools/devtools.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/devtools-extension/src/devtools/devtools.ts b/packages/devtools-extension/src/devtools/devtools.ts new file mode 100644 index 0000000000..147c09a5c8 --- /dev/null +++ b/packages/devtools-extension/src/devtools/devtools.ts @@ -0,0 +1,35 @@ +// Runs in the devtools page context — has access to chrome.devtools.* + +let port: chrome.runtime.Port | null = null; + +function connect() { + try { + // chrome.runtime.id throws (not just returns undefined) when the extension + // context is invalidated — so we must catch, not just optional-chain. + if (!chrome.runtime.id) return; + port = chrome.runtime.connect({ name: 'devtools' }); + port.onDisconnect.addListener(() => { + port = null; + setTimeout(connect, 1000); + }); + } catch { + // Context invalidated — stop reconnecting silently. + } +} + +connect(); + +// panels.create is safe to call once — the devtools page is not reloaded +// while DevTools is open, so no need to guard with runtime.id here. +chrome.devtools.panels.create('Ping DevTools', '', 'panel/panel.html', undefined); + +chrome.devtools.network.onRequestFinished.addListener((entry) => { + port?.postMessage({ + type: 'NETWORK_EVENT', + payload: { + request: entry.request, + response: entry.response, + time: entry.time, + }, + }); +}); diff --git a/packages/devtools-extension/src/devtools/network-observer.test.ts b/packages/devtools-extension/src/devtools/network-observer.test.ts new file mode 100644 index 0000000000..b33aaf9090 --- /dev/null +++ b/packages/devtools-extension/src/devtools/network-observer.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect } from 'vitest'; +import { buildNetworkEvent, isAuthRelated } from './network-observer.js'; + +describe('isAuthRelated', () => { + it('matches known auth endpoints', () => { + expect(isAuthRelated('https://id.example.com/authorize')).toBe(true); + expect(isAuthRelated('https://id.example.com/oauth2/token')).toBe(true); + expect(isAuthRelated('https://id.example.com/davinci/connections')).toBe(true); + expect(isAuthRelated('https://id.example.com/am/json/authenticate')).toBe(true); + }); + + it('does not match unrelated URLs', () => { + expect(isAuthRelated('https://example.com/api/users')).toBe(false); + expect(isAuthRelated('https://cdn.example.com/logo.png')).toBe(false); + }); +}); + +describe('buildNetworkEvent', () => { + it('maps a HAR entry to an AuthEvent', () => { + const entry = { + request: { + url: 'https://id.example.com/authorize', + method: 'POST', + headers: [{ name: 'origin', value: 'https://app.example.com' }], + }, + response: { + status: 200, + headers: [{ name: 'access-control-allow-origin', value: 'https://app.example.com' }], + }, + time: 123, + }; + const event = buildNetworkEvent(entry, null); + expect(event.type).toBe('network:response'); + expect(event.source).toBe('network'); + expect(event.flags.isAuthRelated).toBe(true); + expect(event.flags.isCors).toBe(false); + expect(event.data).toMatchObject({ + _tag: 'network', + url: 'https://id.example.com/authorize', + method: 'POST', + status: 200, + }); + }); + + it('sets isCors flag when cors flags detected', () => { + const entry = { + request: { + url: 'https://id.example.com/authorize', + method: 'POST', + headers: [{ name: 'origin', value: 'https://app.example.com' }], + }, + response: { status: 0, headers: [] }, + time: 50, + }; + const event = buildNetworkEvent(entry, null); + expect(event.flags.isCors).toBe(true); + expect(event.flags.isError).toBe(true); + }); +}); + +describe('buildNetworkEvent body capture', () => { + it('parses a JSON request body from postData', () => { + const entry = { + request: { + url: 'https://id.example.com/davinci/connections', + method: 'POST', + headers: [], + postData: { text: '{"action":"continueNode"}' }, + }, + response: { status: 200, headers: [] }, + time: 10, + }; + const event = buildNetworkEvent(entry, null); + if (event.data._tag !== 'network') throw new Error('not network'); + expect(event.data.requestBody).toEqual({ action: 'continueNode' }); + }); + + it('falls back to raw string for non-JSON request body', () => { + const entry = { + request: { + url: 'https://id.example.com/davinci/connections', + method: 'POST', + headers: [], + postData: { text: 'not-json' }, + }, + response: { status: 200, headers: [] }, + time: 10, + }; + const event = buildNetworkEvent(entry, null); + if (event.data._tag !== 'network') throw new Error('not network'); + expect(event.data.requestBody).toBe('not-json'); + }); + + it('parses a JSON response body from content', () => { + const entry = { + request: { + url: 'https://id.example.com/oauth2/token', + method: 'POST', + headers: [], + }, + response: { + status: 200, + headers: [], + content: { text: '{"access_token":"abc","token_type":"Bearer"}' }, + }, + time: 20, + }; + const event = buildNetworkEvent(entry, null); + if (event.data._tag !== 'network') throw new Error('not network'); + expect(event.data.responseBody).toEqual({ access_token: 'abc', token_type: 'Bearer' }); + }); + + it('omits requestBody and responseBody when absent', () => { + const entry = { + request: { + url: 'https://id.example.com/authorize', + method: 'GET', + headers: [], + }, + response: { status: 302, headers: [] }, + time: 5, + }; + const event = buildNetworkEvent(entry, null); + if (event.data._tag !== 'network') throw new Error('not network'); + expect(event.data.requestBody).toBeUndefined(); + expect(event.data.responseBody).toBeUndefined(); + }); + + it('returns undefined for empty body text', () => { + const entry = { + request: { + url: 'https://id.example.com/davinci/connections', + method: 'POST', + headers: [], + postData: { text: ' ' }, + }, + response: { status: 200, headers: [], content: { text: '' } }, + time: 10, + }; + const event = buildNetworkEvent(entry, null); + if (event.data._tag !== 'network') throw new Error('not network'); + expect(event.data.requestBody).toBeUndefined(); + expect(event.data.responseBody).toBeUndefined(); + }); +}); diff --git a/packages/devtools-extension/src/devtools/network-observer.ts b/packages/devtools-extension/src/devtools/network-observer.ts new file mode 100644 index 0000000000..ec0ebc7503 --- /dev/null +++ b/packages/devtools-extension/src/devtools/network-observer.ts @@ -0,0 +1,90 @@ +import { detectCorsFlags } from './cors-detector.js'; +import type { AuthEvent, NetworkData } from '@forgerock/devtools-types'; + +const AUTH_URL_PATTERNS = [ + /\/authorize/, + /\/oauth2\/token/, + /\/davinci\//, + /\/am\/json\//, + /\/openid-connect\//, + /\/as\/token/, +] as const; + +export interface HarHeader { + name: string; + value: string; +} + +export interface HarEntry { + request: { + url: string; + method: string; + headers: HarHeader[]; + postData?: { text: string }; + }; + response: { + status: number; + headers: HarHeader[]; + content?: { text: string }; + }; + time: number; +} + +export function isAuthRelated(url: string): boolean { + return AUTH_URL_PATTERNS.some((p) => p.test(url)); +} + +function headersToRecord(headers: HarHeader[]): Record { + return Object.fromEntries(headers.map((h) => [h.name.toLowerCase(), h.value])); +} + +const MAX_BODY_PARSE_BYTES = 512 * 1024; + +function parseBody(text: string | undefined): unknown | undefined { + if (!text || text.trim() === '') return undefined; + if (text.length > MAX_BODY_PARSE_BYTES) return text; + try { + return JSON.parse(text) as unknown; + } catch { + return text; + } +} + +export function buildNetworkEvent(entry: HarEntry, flowId: string | null): AuthEvent { + const corsFlags = detectCorsFlags(entry); + const isCors = corsFlags.some( + (f) => + f.reason === 'status-zero' || + f.reason === 'missing-allow-origin' || + f.reason === 'wildcard-with-credentials', + ); + const isError = entry.response.status === 0 || entry.response.status >= 400; + + const data: NetworkData = { + _tag: 'network', + url: entry.request.url, + method: entry.request.method, + status: entry.response.status, + requestHeaders: headersToRecord(entry.request.headers), + responseHeaders: headersToRecord(entry.response.headers), + duration: entry.time, + corsFlag: corsFlags[0], + requestBody: parseBody(entry.request.postData?.text), + responseBody: parseBody(entry.response.content?.text), + }; + + return { + id: crypto.randomUUID(), + timestamp: Date.now(), + type: 'network:response', + source: 'network', + flowId, + causedBy: null, + data, + flags: { + isCors, + isError, + isAuthRelated: isAuthRelated(entry.request.url), + }, + }; +} diff --git a/packages/devtools-extension/src/export/markdown.test.ts b/packages/devtools-extension/src/export/markdown.test.ts new file mode 100644 index 0000000000..7be1ab2cfd --- /dev/null +++ b/packages/devtools-extension/src/export/markdown.test.ts @@ -0,0 +1,154 @@ +import { describe, expect, it } from 'vitest'; +import { renderFlowMarkdown } from './markdown.js'; +import type { FlowState, AuthEvent } from '@forgerock/devtools-types'; +import type { DiagnosisResult } from '../background/diagnosis-engine.js'; + +const baseFlags = { isCors: false, isError: false, isAuthRelated: true }; + +const networkEvent: AuthEvent = { + id: 'e1', + timestamp: 1000, + type: 'network:response', + source: 'network', + flowId: 'flow-abcd1234', + causedBy: null, + flags: baseFlags, + data: { + _tag: 'network', + url: 'https://auth.example.com/davinci/connections', + method: 'POST', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 120, + }, +}; + +const sdkEvent: AuthEvent = { + id: 'e2', + timestamp: 1120, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-abcd1234', + causedBy: null, + flags: baseFlags, + data: { _tag: 'sdk', nodeStatus: 'continue', nodeName: 'UsernamePassword' }, +}; + +function makeFlowState(events: AuthEvent[]): FlowState { + return { + flowId: 'flow-abcd1234', + capturedAt: '2026-05-08T14:30:00.000Z', + events, + summary: { nodeCount: 1, errorCount: 0, corsFlags: [], duration: 120, sdkConnected: true }, + lastSdkEventId: null, + }; +} + +describe('renderFlowMarkdown', () => { + it('renders header with flow ID prefix and health status', () => { + const md = renderFlowMarkdown(makeFlowState([networkEvent, sdkEvent]), null); + expect(md).toContain('## Flow: flow-abc'); + expect(md).toContain('HEALTHY'); + }); + + it('renders event table with relative timestamps', () => { + const md = renderFlowMarkdown(makeFlowState([networkEvent, sdkEvent]), null); + expect(md).toContain('+0ms'); + expect(md).toContain('+120ms'); + expect(md).toContain('network:response'); + expect(md).toContain('sdk:node-change'); + }); + + it('renders detail columns for network events', () => { + const md = renderFlowMarkdown(makeFlowState([networkEvent]), null); + expect(md).toContain('200'); + expect(md).toContain('POST /davinci/connections'); + }); + + it('renders detail columns for SDK events', () => { + const md = renderFlowMarkdown(makeFlowState([sdkEvent]), null); + expect(md).toContain('continue'); + expect(md).toContain('UsernamePassword'); + }); + + it('omits diagnosis section when healthy', () => { + const md = renderFlowMarkdown(makeFlowState([networkEvent]), null); + expect(md).not.toContain('### Diagnosis'); + }); + + it('renders diagnosis section when issues exist', () => { + const diagnosis: DiagnosisResult = { + flowHealth: 'error', + issues: [ + { + id: 'cors:status-zero', + severity: 'error', + category: 'cors', + title: 'Network failure (status 0)', + description: 'The request never reached the server.', + steps: ['Add origin to allowed origins.', 'Check preflight.'], + relatedEventIds: ['e1'], + }, + ], + annotatedEvents: new Map(), + }; + const md = renderFlowMarkdown(makeFlowState([networkEvent]), diagnosis); + expect(md).toContain('### Diagnosis'); + expect(md).toContain('ERROR'); + expect(md).toContain('Network failure (status 0)'); + expect(md).toContain('1. Add origin to allowed origins.'); + expect(md).toContain('2. Check preflight.'); + }); + + it('renders journey event detail', () => { + const journeyEvent: AuthEvent = { + id: 'e3', + timestamp: 1000, + type: 'sdk:journey-step', + source: 'sdk', + flowId: 'flow-abcd1234', + causedBy: null, + flags: baseFlags, + data: { _tag: 'journey', stepType: 'Step', stage: 'UsernamePassword' }, + }; + const md = renderFlowMarkdown(makeFlowState([journeyEvent]), null); + expect(md).toContain('Step'); + expect(md).toContain('UsernamePassword'); + }); + + it('renders OIDC event detail', () => { + const oidcEvent: AuthEvent = { + id: 'e4', + timestamp: 1000, + type: 'sdk:oidc-state', + source: 'sdk', + flowId: 'flow-abcd1234', + causedBy: null, + flags: baseFlags, + data: { _tag: 'oidc', phase: 'exchange', status: 'success' }, + }; + const md = renderFlowMarkdown(makeFlowState([oidcEvent]), null); + expect(md).toContain('success'); + expect(md).toContain('exchange'); + }); + + it('preserves redaction markers in output', () => { + const event: AuthEvent = { + id: 'e5', + timestamp: 1000, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-abcd1234', + causedBy: null, + flags: baseFlags, + data: { + _tag: 'sdk', + nodeStatus: 'continue', + interactionToken: '', + }, + }; + const md = renderFlowMarkdown(makeFlowState([event]), null); + expect(md).toContain('sdk:node-change'); + }); +}); diff --git a/packages/devtools-extension/src/export/markdown.ts b/packages/devtools-extension/src/export/markdown.ts new file mode 100644 index 0000000000..3d264bf54c --- /dev/null +++ b/packages/devtools-extension/src/export/markdown.ts @@ -0,0 +1,96 @@ +import type { AuthEvent, FlowState } from '@forgerock/devtools-types'; +import type { DiagnosisResult, FlowIssue } from '../background/diagnosis-engine.js'; + +function formatRelativeTime(timestamp: number, baseTimestamp: number): string { + return `+${Math.round(timestamp - baseTimestamp)}ms`; +} + +function eventStatus(event: AuthEvent): string { + switch (event.data._tag) { + case 'network': + return String(event.data.status); + case 'sdk': + return event.data.nodeStatus; + case 'journey': + return event.data.stepType; + case 'oidc': + return event.data.status; + default: + return ''; + } +} + +function eventDetail(event: AuthEvent): string { + switch (event.data._tag) { + case 'network': { + const path = extractPath(event.data.url); + return `${event.data.method} ${path}`; + } + case 'sdk': + return event.data.nodeName ?? ''; + case 'journey': + return event.data.stage ?? ''; + case 'oidc': + return event.data.phase; + case 'session': + return event.data.key; + default: + return ''; + } +} + +function extractPath(url: string): string { + try { + return new URL(url).pathname; + } catch { + return url; + } +} + +function renderIssue(issue: FlowIssue): string { + const severity = issue.severity.toUpperCase(); + const steps = issue.steps.map((s, i) => ` ${i + 1}. ${s}`).join('\n'); + return `- **[${severity}] ${issue.title}** — ${issue.description}\n${steps}`; +} + +export function renderFlowMarkdown(flow: FlowState, diagnosis: DiagnosisResult | null): string { + const lines: string[] = []; + + const flowIdPrefix = flow.flowId ? flow.flowId.slice(0, 8) : 'unknown'; + const health = diagnosis?.flowHealth?.toUpperCase() ?? 'HEALTHY'; + lines.push(`## Flow: ${flowIdPrefix} — ${health}`); + lines.push(''); + const eventCount = flow.events.length; + const errorCount = flow.summary.errorCount; + const durationSec = (flow.summary.duration / 1000).toFixed(1); + lines.push( + `Captured: ${flow.capturedAt} | ${eventCount} events | ${errorCount} errors | ${durationSec}s duration`, + ); + + if (diagnosis && diagnosis.flowHealth !== 'healthy' && diagnosis.issues.length > 0) { + lines.push(''); + lines.push('### Diagnosis'); + lines.push(''); + for (const issue of diagnosis.issues) { + lines.push(renderIssue(issue)); + lines.push(''); + } + } + + lines.push(''); + lines.push('### Events'); + lines.push(''); + lines.push('| # | Time | Type | Status | Detail |'); + lines.push('|---|------|------|--------|--------|'); + + const baseTimestamp = flow.events.length > 0 ? flow.events[0].timestamp : 0; + flow.events.forEach((event, index) => { + const time = formatRelativeTime(event.timestamp, baseTimestamp); + const status = eventStatus(event); + const detail = eventDetail(event); + lines.push(`| ${index + 1} | ${time} | ${event.type} | ${status} | ${detail} |`); + }); + + lines.push(''); + return lines.join('\n'); +} diff --git a/packages/devtools-extension/src/export/redact.test.ts b/packages/devtools-extension/src/export/redact.test.ts new file mode 100644 index 0000000000..e3e61dcfd6 --- /dev/null +++ b/packages/devtools-extension/src/export/redact.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, it } from 'vitest'; +import { redactFlowState } from './redact.js'; +import type { FlowState, AuthEvent } from '@forgerock/devtools-types'; + +function makeFlowState(events: AuthEvent[]): FlowState { + return { + flowId: 'flow-1', + capturedAt: '2026-05-08T14:30:00.000Z', + events, + summary: { nodeCount: 0, errorCount: 0, corsFlags: [], duration: 0, sdkConnected: false }, + lastSdkEventId: null, + }; +} + +const baseFlags = { isCors: false, isError: false, isAuthRelated: true }; + +describe('redactFlowState', () => { + it('redacts Authorization header', () => { + const event: AuthEvent = { + id: 'e1', + timestamp: 1000, + type: 'network:response', + source: 'network', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { + _tag: 'network', + url: '/token', + method: 'POST', + status: 200, + requestHeaders: { authorization: 'Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.sig' }, + responseHeaders: {}, + duration: 100, + }, + }; + const result = redactFlowState(makeFlowState([event])); + const headers = (result.events[0].data as { requestHeaders: Record }) + .requestHeaders; + expect(headers.authorization).toBe(''); + }); + + it('redacts Cookie and Set-Cookie headers', () => { + const event: AuthEvent = { + id: 'e2', + timestamp: 1000, + type: 'network:response', + source: 'network', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { + _tag: 'network', + url: '/token', + method: 'POST', + status: 200, + requestHeaders: { cookie: 'session=abc123' }, + responseHeaders: { 'set-cookie': 'session=def456; Path=/' }, + duration: 100, + }, + }; + const result = redactFlowState(makeFlowState([event])); + const data = result.events[0].data as { + requestHeaders: Record; + responseHeaders: Record; + }; + expect(data.requestHeaders.cookie).toBe(''); + expect(data.responseHeaders['set-cookie']).toBe(''); + }); + + it('redacts interactionToken in SDK data', () => { + const event: AuthEvent = { + id: 'e3', + timestamp: 2000, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { _tag: 'sdk', nodeStatus: 'continue', interactionToken: 'secret-tok-xyz' }, + }; + const result = redactFlowState(makeFlowState([event])); + const data = result.events[0].data as { interactionToken?: string }; + expect(data.interactionToken).toBe(''); + }); + + it('redacts authorization.code in SDK data', () => { + const event: AuthEvent = { + id: 'e4', + timestamp: 2000, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { _tag: 'sdk', nodeStatus: 'success', authorization: { code: 'auth-code-secret' } }, + }; + const result = redactFlowState(makeFlowState([event])); + const data = result.events[0].data as { authorization?: { code?: string } }; + expect(data.authorization?.code).toBe(''); + }); + + it('redacts tokenId in journey data', () => { + const event: AuthEvent = { + id: 'e5', + timestamp: 3000, + type: 'sdk:journey-step', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { _tag: 'journey', stepType: 'LoginSuccess', tokenId: 'token-secret-123' }, + }; + const result = redactFlowState(makeFlowState([event])); + const data = result.events[0].data as { tokenId?: string }; + expect(data.tokenId).toBe(''); + }); + + it('redacts token fields in response body objects', () => { + const event: AuthEvent = { + id: 'e6', + timestamp: 1000, + type: 'network:response', + source: 'network', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { + _tag: 'network', + url: '/token', + method: 'POST', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 100, + responseBody: { access_token: 'secret', refresh_token: 'secret2', scope: 'openid' }, + }, + }; + const result = redactFlowState(makeFlowState([event])); + const body = (result.events[0].data as { responseBody?: Record }).responseBody; + expect(body?.access_token).toBe(''); + expect(body?.refresh_token).toBe(''); + expect(body?.scope).toBe('openid'); + }); + + it('redacts sensitive callback values in journey data', () => { + const event: AuthEvent = { + id: 'e7', + timestamp: 3000, + type: 'sdk:journey-step', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { + _tag: 'journey', + stepType: 'Step', + callbacks: [ + { + input: [{ name: 'IDToken1', value: 'user@example.com' }], + output: [{ name: 'prompt', value: 'Username' }], + }, + { + input: [{ name: 'IDToken2_password', value: 's3cret' }], + output: [{ name: 'prompt', value: 'Password' }], + }, + ], + }, + }; + const result = redactFlowState(makeFlowState([event])); + const cbs = ( + result.events[0].data as { + callbacks?: Array<{ + input: Array<{ name: string; value: unknown }>; + output: Array<{ name: string; value: unknown }>; + }>; + } + ).callbacks!; + expect(cbs[0].input[0].value).toBe('user@example.com'); + expect(cbs[1].input[0].value).toBe(''); + }); + + it('does not mutate the original flow state', () => { + const event: AuthEvent = { + id: 'e8', + timestamp: 2000, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { _tag: 'sdk', nodeStatus: 'continue', interactionToken: 'original-token' }, + }; + const original = makeFlowState([event]); + redactFlowState(original); + const data = original.events[0].data as { interactionToken?: string }; + expect(data.interactionToken).toBe('original-token'); + }); + + it('passes through events with no sensitive data unchanged', () => { + const event: AuthEvent = { + id: 'e9', + timestamp: 4000, + type: 'session:cookie', + source: 'session', + flowId: 'flow-1', + causedBy: null, + flags: baseFlags, + data: { _tag: 'session', key: 'iPlanetDirectoryPro', before: 'old', after: 'new' }, + }; + const result = redactFlowState(makeFlowState([event])); + expect(result.events[0]).toEqual(event); + }); +}); diff --git a/packages/devtools-extension/src/export/redact.ts b/packages/devtools-extension/src/export/redact.ts new file mode 100644 index 0000000000..2b7ef1dcbc --- /dev/null +++ b/packages/devtools-extension/src/export/redact.ts @@ -0,0 +1,125 @@ +import type { AuthEvent, FlowState } from '@forgerock/devtools-types'; + +const SENSITIVE_HEADERS = new Set(['authorization', 'cookie', 'set-cookie']); +const SENSITIVE_BODY_FIELDS = new Set([ + 'access_token', + 'refresh_token', + 'id_token', + 'code', + 'assertion', +]); +const SENSITIVE_CALLBACK_NAME = /password|secret|credential|[_-]token$|^token[_-]/i; + +function redactHeaders(headers: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (SENSITIVE_HEADERS.has(key.toLowerCase())) { + result[key] = + key.toLowerCase() === 'authorization' ? '' : ''; + } else { + result[key] = value; + } + } + return result; +} + +function redactBodyFields(body: unknown): unknown { + if (body === null || body === undefined || typeof body !== 'object' || Array.isArray(body)) { + return body; + } + const obj = body as Record; + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (SENSITIVE_BODY_FIELDS.has(key)) { + result[key] = ``; + } else { + result[key] = value; + } + } + return result; +} + +function redactCallbacks(callbacks: readonly unknown[]): unknown[] { + return callbacks.map((cb) => { + if (cb === null || typeof cb !== 'object') return cb; + const obj = cb as Record; + return { + ...obj, + input: Array.isArray(obj.input) ? redactCallbackEntries(obj.input) : obj.input, + output: Array.isArray(obj.output) ? redactCallbackEntries(obj.output) : obj.output, + }; + }); +} + +function redactCallbackEntries(entries: unknown[]): unknown[] { + return entries.map((entry) => { + if (entry === null || typeof entry !== 'object') return entry; + const obj = entry as Record; + if (typeof obj.name === 'string' && SENSITIVE_CALLBACK_NAME.test(obj.name)) { + return { ...obj, value: '' }; + } + return obj; + }); +} + +function redactEvent(event: AuthEvent): AuthEvent { + const { data } = event; + + switch (data._tag) { + case 'network': { + return { + ...event, + data: { + ...data, + requestHeaders: redactHeaders(data.requestHeaders), + responseHeaders: redactHeaders(data.responseHeaders), + ...(data.requestBody !== undefined + ? { requestBody: redactBodyFields(data.requestBody) } + : {}), + ...(data.responseBody !== undefined + ? { responseBody: redactBodyFields(data.responseBody) } + : {}), + }, + }; + } + + case 'sdk': { + return { + ...event, + data: { + ...data, + ...(data.interactionToken !== undefined + ? { interactionToken: '' } + : {}), + ...(data.authorization?.code !== undefined + ? { authorization: { ...data.authorization, code: '' } } + : {}), + ...(data.responseBody !== undefined + ? { responseBody: redactBodyFields(data.responseBody) } + : {}), + }, + }; + } + + case 'journey': { + return { + ...event, + data: { + ...data, + ...(data.tokenId !== undefined ? { tokenId: '' } : {}), + ...(data.callbacks !== undefined ? { callbacks: redactCallbacks(data.callbacks) } : {}), + }, + }; + } + + default: + return event; + } +} + +export function redactFlowState(flow: FlowState): FlowState { + return { + ...flow, + events: flow.events.map(redactEvent), + }; +} diff --git a/packages/devtools-extension/src/panel/Main.elm b/packages/devtools-extension/src/panel/Main.elm new file mode 100644 index 0000000000..b83288625b --- /dev/null +++ b/packages/devtools-extension/src/panel/Main.elm @@ -0,0 +1,185 @@ +port module Main exposing (main) + +import Browser +import Decode +import Helpers +import Json.Decode as JD +import Time +import Model exposing (init) +import Types exposing (InspectorTab(..)) +import Update exposing (Msg(..), update) +import View exposing (view) + + +main : Program () Model.Model Msg +main = + Browser.element + { init = init + , update = updateWithPorts + , view = view + , subscriptions = subscriptions + } + + +updateWithPorts : Msg -> Model.Model -> ( Model.Model, Cmd Msg ) +updateWithPorts msg model = + let + ( newModel, cmd ) = + update msg model + in + case msg of + ExportJson -> + ( newModel, Cmd.batch [ cmd, exportJson () ] ) + + ExportMarkdown -> + ( newModel, Cmd.batch [ cmd, exportMarkdown () ] ) + + SubmitImportPaste -> + ( newModel, Cmd.batch [ cmd, submitImportPaste model.importPasteText ] ) + + ClearFlow -> + ( newModel, Cmd.batch [ cmd, clearFlow () ] ) + + SaveSnapshot -> + ( newModel, Cmd.batch [ cmd, saveSnapshot () ] ) + + CopyToClipboard text -> + ( newModel, Cmd.batch [ cmd, copyToClipboard text ] ) + + ToggleSnapshotMenu -> + if not model.snapshotMenuOpen then + -- Opening: request fresh list + ( newModel, Cmd.batch [ cmd, requestSnapshots () ] ) + else + ( newModel, cmd ) + + LoadSnapshot snapshotId -> + ( newModel, Cmd.batch [ cmd, loadSnapshot snapshotId ] ) + + DeleteSnapshot snapshotId -> + ( newModel, Cmd.batch [ cmd, deleteSnapshot snapshotId ] ) + + _ -> + ( newModel, cmd ) + + +subscriptions : Model.Model -> Sub Msg +subscriptions model = + let + playbackSub = + if model.isPlaying then + let + sdkNodes = + Helpers.sdkNodes model.events + + currentNode = + model.playbackIndex + |> Maybe.andThen (\n -> List.head (List.drop n sdkNodes)) + + nextNode = + model.playbackIndex + |> Maybe.andThen (\n -> List.head (List.drop (n + 1) sdkNodes)) + + interval = + case ( currentNode, nextNode ) of + ( Just cur, Just nxt ) -> + clamp 300.0 1500.0 (nxt.timestamp - cur.timestamp) + + _ -> + 600.0 + in + Time.every interval (\_ -> PlaybackTick) + + else + Sub.none + in + Sub.batch + [ receiveEvent + (\raw -> + case JD.decodeValue Decode.decodeAuthEvent raw of + Ok event -> + EventReceived event + + Err err -> + DecodeError (JD.errorToString err) + ) + , receiveDiagnosis + (\raw -> + case JD.decodeValue Decode.decodeDiagnosisResult raw of + Ok result -> + DiagnosisReceived result + + Err _ -> + DecodeError "Failed to decode diagnosis result" + ) + , receiveImportMeta + (\raw -> + case JD.decodeValue Decode.decodeImportMeta raw of + Ok meta -> + ImportMetaReceived meta + + Err _ -> + ImportError "Failed to decode import metadata" + ) + , receiveImportError + (\raw -> + case JD.decodeValue (JD.field "message" JD.string) raw of + Ok errMsg -> + ImportError errMsg + + Err _ -> + ImportError "Unknown import error" + ) + , receiveSnapshots + (\raw -> + case JD.decodeValue (JD.list Decode.decodeSnapshotMeta) raw of + Ok list -> + SnapshotsReceived list + + Err _ -> + SnapshotsReceived [] + ) + , playbackSub + ] + + +port receiveEvent : (JD.Value -> msg) -> Sub msg + + +port receiveDiagnosis : (JD.Value -> msg) -> Sub msg + + +port receiveImportMeta : (JD.Value -> msg) -> Sub msg + + +port receiveImportError : (JD.Value -> msg) -> Sub msg + + +port exportJson : () -> Cmd msg + + +port exportMarkdown : () -> Cmd msg + + +port submitImportPaste : String -> Cmd msg + + +port clearFlow : () -> Cmd msg + + +port saveSnapshot : () -> Cmd msg + + +port copyToClipboard : String -> Cmd msg + + +port requestSnapshots : () -> Cmd msg + + +port receiveSnapshots : (JD.Value -> msg) -> Sub msg + + +port loadSnapshot : String -> Cmd msg + + +port deleteSnapshot : String -> Cmd msg diff --git a/packages/devtools-extension/src/panel/panel.html b/packages/devtools-extension/src/panel/panel.html new file mode 100644 index 0000000000..57deab0ed2 --- /dev/null +++ b/packages/devtools-extension/src/panel/panel.html @@ -0,0 +1,1262 @@ + + + + + + + +
+ + + + diff --git a/packages/devtools-extension/src/panel/panel.ts b/packages/devtools-extension/src/panel/panel.ts new file mode 100644 index 0000000000..0e041fdf92 --- /dev/null +++ b/packages/devtools-extension/src/panel/panel.ts @@ -0,0 +1,395 @@ +import { Schema } from 'effect'; +import { FlowExportSchema } from '@forgerock/devtools-types'; +import type { FlowExport } from '@forgerock/devtools-types'; +import { redactFlowState } from '../export/redact.js'; +import { renderFlowMarkdown } from '../export/markdown.js'; +import { runDiagnosis } from '../background/diagnosis-engine.js'; + +declare const Elm: { + Main: { + init: (opts: { node: HTMLElement | null; flags: null }) => { + ports: { + receiveEvent: { send: (event: unknown) => void }; + receiveDiagnosis: { send: (diagnosis: unknown) => void }; + receiveImportMeta: { send: (meta: unknown) => void }; + receiveImportError: { send: (error: unknown) => void }; + exportJson: { subscribe: (cb: () => void) => void }; + exportMarkdown: { subscribe: (cb: () => void) => void }; + submitImportPaste: { subscribe: (cb: (text: string) => void) => void }; + clearFlow: { subscribe: (cb: () => void) => void }; + saveSnapshot: { subscribe: (cb: () => void) => void }; + requestSnapshots: { subscribe: (cb: () => void) => void }; + receiveSnapshots: { send: (snapshots: unknown[]) => void }; + loadSnapshot: { subscribe: (cb: (id: string) => void) => void }; + deleteSnapshot: { subscribe: (cb: (id: string) => void) => void }; + copyToClipboard: { subscribe: (cb: (text: string) => void) => void }; + }; + }; + }; +}; + +// ── Panel resize ───────────────────────────────────────────────────────────── + +const MIN_GRAPH_W = 120; +const MAX_GRAPH_W = 480; +const MIN_INSP_H = 80; +const MAX_INSP_H = 600; + +const root = document.documentElement; + +function setGraphW(px: number) { + const clamped = Math.min(MAX_GRAPH_W, Math.max(MIN_GRAPH_W, px)); + root.style.setProperty('--graph-w', `${clamped}px`); +} + +function setInspH(px: number) { + const clamped = Math.min(MAX_INSP_H, Math.max(MIN_INSP_H, px)); + root.style.setProperty('--insp-h', `${clamped}px`); +} + +function makeResizeHandle(cls: 'resize-handle-v' | 'resize-handle-h'): HTMLDivElement { + const el = document.createElement('div'); + el.className = `resize-handle ${cls}`; + document.body.appendChild(el); + return el; +} + +function initResizeHandles() { + const vHandle = makeResizeHandle('resize-handle-v'); + const hHandle = makeResizeHandle('resize-handle-h'); + + // ── vertical (graph width) ────────────────────────────────────── + vHandle.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + vHandle.classList.add('dragging'); + document.body.classList.add('resizing'); + const startX = e.clientX; + const startW = parseInt(getComputedStyle(root).getPropertyValue('--graph-w'), 10); + + function onMove(ev: MouseEvent) { + setGraphW(startW + (ev.clientX - startX)); + } + function onUp() { + vHandle.classList.remove('dragging'); + document.body.classList.remove('resizing'); + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + } + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }); + + // ── horizontal (inspector height) ────────────────────────────── + hHandle.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + hHandle.classList.add('dragging'); + document.body.classList.add('resizing'); + const startY = e.clientY; + const startH = parseInt(getComputedStyle(root).getPropertyValue('--insp-h'), 10); + + function onMove(ev: MouseEvent) { + // dragging up = larger inspector (bottom - cursor moves up) + setInspH(startH - (ev.clientY - startY)); + } + function onUp() { + hHandle.classList.remove('dragging'); + document.body.classList.remove('resizing'); + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + } + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }); +} + +// ── JWT Decoder ─────────────────────────────────────────────────────────────── + +function formatUnixTime(seconds: number): string { + try { + return new Date(seconds * 1000).toISOString().replace('T', ' ').replace('Z', ' UTC'); + } catch { + return String(seconds); + } +} + +function makeEl( + tag: K, + classes: string[], + textContent?: string, +): HTMLElementTagNameMap[K] { + const el = document.createElement(tag); + el.className = classes.join(' '); + if (textContent !== undefined) el.textContent = textContent; + return el; +} + +function buildJwtValueNodes(key: string, val: unknown): Node[] { + const nodes: Node[] = []; + const isTimestamp = (key === 'exp' || key === 'iat' || key === 'nbf') && typeof val === 'number'; + + if (val === null) { + nodes.push(makeEl('span', ['jwt-v', 'jwt-v-null'], 'null')); + } else if (typeof val === 'boolean') { + nodes.push(makeEl('span', ['jwt-v', 'jwt-v-bool'], String(val))); + } else if (typeof val === 'number') { + nodes.push(makeEl('span', ['jwt-v', 'jwt-v-num'], String(val))); + if (isTimestamp) { + nodes.push(makeEl('span', ['jwt-v-date'], `(${formatUnixTime(val)})`)); + } + if (key === 'exp' && val * 1000 < Date.now()) { + nodes.push(makeEl('span', ['jwt-expired'], '⚠ EXPIRED')); + } + } else if (typeof val === 'string') { + nodes.push(makeEl('span', ['jwt-v'], `"${val}"`)); + } else { + nodes.push(makeEl('span', ['jwt-v'], JSON.stringify(val))); + } + + return nodes; +} + +function buildJwtSection(title: string, obj: Record): DocumentFragment { + const frag = document.createDocumentFragment(); + + frag.appendChild(makeEl('div', ['jwt-section-hdr'], title)); + + for (const [k, v] of Object.entries(obj)) { + const row = makeEl('div', ['jwt-kv']); + row.appendChild(makeEl('span', ['jwt-k'], k)); + for (const node of buildJwtValueNodes(k, v)) { + row.appendChild(node); + } + frag.appendChild(row); + } + + return frag; +} + +function base64UrlDecode(s: string): string { + const b64 = s.replace(/-/g, '+').replace(/_/g, '/'); + const padded = b64 + '=='.slice((b64.length + 3) & 3); + return atob(padded); +} + +function buildJwtBody(jwt: string): HTMLElement { + const body = makeEl('div', ['jwt-body']); + + try { + const parts = jwt.split('.'); + if (parts.length !== 3) throw new Error('Not a 3-part JWT'); + + const header = JSON.parse(base64UrlDecode(parts[0]!)) as Record; + const payload = JSON.parse(base64UrlDecode(parts[1]!)) as Record; + const sigPreview = parts[2]!.slice(0, 16) + '…'; + + body.appendChild(buildJwtSection('Header', header)); + body.appendChild(buildJwtSection('Claims', payload)); + body.appendChild(makeEl('div', ['jwt-section-hdr'], 'Signature')); + body.appendChild(makeEl('span', ['jwt-sig'], `${sigPreview} (not verified)`)); + } catch (err) { + body.appendChild(makeEl('span', ['jwt-err'], `Could not decode JWT: ${String(err)}`)); + } + + return body; +} + +function initJwtObserver(appRoot: HTMLElement) { + function processAll() { + appRoot.querySelectorAll('.jwt-pending[data-jwt]').forEach((el) => { + const jwt = el.getAttribute('data-jwt')!; + el.removeAttribute('data-jwt'); + el.classList.remove('jwt-pending'); + el.appendChild(buildJwtBody(jwt)); + }); + } + + const observer = new MutationObserver(processAll); + observer.observe(appRoot, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['data-jwt'], + }); + + // Process any JWTs already in the DOM at init time + processAll(); +} + +// ── App init ────────────────────────────────────────────────────────────────── + +const app = Elm.Main.init({ node: document.getElementById('app'), flags: null }); + +initResizeHandles(); + +function copyToClipboard(text: string): void { + if (navigator.clipboard?.writeText) { + navigator.clipboard.writeText(text).catch(() => fallbackCopy(text)); + } else { + fallbackCopy(text); + } +} + +function fallbackCopy(text: string): void { + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); +} + +const appRoot = document.getElementById('app'); +if (appRoot) { + initJwtObserver(appRoot); +} + +chrome.runtime.onMessage.addListener((message) => { + if (message.type === 'PANEL_EVENT') { + app.ports.receiveEvent.send(message.payload); + if (message.diagnosis) { + app.ports.receiveDiagnosis.send(message.diagnosis); + } + } +}); + +chrome.runtime.sendMessage({ type: 'GET_STATE' }, (state) => { + if (state?.events) { + state.events.forEach((event: unknown) => app.ports.receiveEvent.send(event)); + } +}); + +app.ports.exportJson?.subscribe(() => { + chrome.runtime.sendMessage({ type: 'GET_STATE' }, (state) => { + if (!state) return; + const redacted = redactFlowState(state); + const envelope: FlowExport = { + version: 1, + exportedAt: new Date().toISOString(), + redacted: true, + flow: redacted, + }; + copyToClipboard(JSON.stringify(envelope, null, 2)); + }); +}); + +app.ports.exportMarkdown?.subscribe(() => { + chrome.runtime.sendMessage({ type: 'GET_STATE' }, (state) => { + if (!state) return; + const redacted = redactFlowState(state); + const diagnosis = runDiagnosis(redacted.events); + const md = renderFlowMarkdown(redacted, diagnosis); + copyToClipboard(md); + }); +}); + +app.ports.submitImportPaste?.subscribe((text: string) => { + try { + const parsed = JSON.parse(text); + const decoded = Schema.decodeUnknownSync(FlowExportSchema)(parsed); + + chrome.runtime.sendMessage({ type: 'CLEAR' }); + + for (const event of decoded.flow.events) { + app.ports.receiveEvent.send(event); + } + + const diagnosis = runDiagnosis(decoded.flow.events); + app.ports.receiveDiagnosis.send({ + ...diagnosis, + annotatedEvents: Object.fromEntries(diagnosis.annotatedEvents), + }); + + app.ports.receiveImportMeta.send({ + flowId: decoded.flow.flowId, + capturedAt: decoded.flow.capturedAt, + redacted: decoded.redacted, + }); + } catch (e) { + app.ports.receiveImportError.send({ + message: e instanceof Error ? e.message : 'Invalid export format', + }); + } +}); + +app.ports.copyToClipboard?.subscribe((text: string) => { + copyToClipboard(text); +}); + +app.ports.clearFlow?.subscribe(() => { + chrome.runtime.sendMessage({ type: 'CLEAR' }); +}); + +const SNAPSHOTS_KEY = 'ping:auth-flow:snapshots'; +const MAX_SNAPSHOTS = 5; + +app.ports.saveSnapshot?.subscribe(() => { + chrome.runtime.sendMessage({ type: 'GET_STATE' }, (state) => { + if (!state) return; + chrome.storage.local.get(SNAPSHOTS_KEY, (result) => { + const existing: unknown[] = Array.isArray(result[SNAPSHOTS_KEY]) ? result[SNAPSHOTS_KEY] : []; + const snapshot = { + id: crypto.randomUUID(), + savedAt: new Date().toISOString(), + flowState: state, + }; + const updated = [...existing, snapshot].slice(-MAX_SNAPSHOTS); + chrome.storage.local.set({ [SNAPSHOTS_KEY]: updated }); + }); + }); +}); + +app.ports.requestSnapshots?.subscribe(() => { + chrome.storage.local.get(SNAPSHOTS_KEY, (result) => { + const snapshots: unknown[] = Array.isArray(result[SNAPSHOTS_KEY]) ? result[SNAPSHOTS_KEY] : []; + const metas = ( + snapshots as Array<{ + id: string; + savedAt: string; + flowState: { flowId?: string | null; events?: unknown[] }; + }> + ).map((s) => ({ + id: s.id, + savedAt: s.savedAt, + flowId: s.flowState?.flowId ?? null, + eventCount: s.flowState?.events?.length ?? 0, + })); + app.ports.receiveSnapshots.send(metas); + }); +}); + +app.ports.loadSnapshot?.subscribe((snapshotId: string) => { + chrome.storage.local.get(SNAPSHOTS_KEY, (result) => { + const snapshots = Array.isArray(result[SNAPSHOTS_KEY]) ? result[SNAPSHOTS_KEY] : []; + const snapshot = snapshots.find((s: { id: string }) => s.id === snapshotId); + if (!snapshot) return; + + chrome.runtime.sendMessage({ type: 'CLEAR' }); + const state = snapshot.flowState; + + for (const event of state.events) { + app.ports.receiveEvent.send(event); + } + + const diagnosis = runDiagnosis(state.events); + app.ports.receiveDiagnosis.send({ + ...diagnosis, + annotatedEvents: Object.fromEntries(diagnosis.annotatedEvents), + }); + + app.ports.receiveImportMeta.send({ + flowId: state.flowId ?? null, + capturedAt: snapshot.savedAt, + redacted: false, + }); + }); +}); + +app.ports.deleteSnapshot?.subscribe((snapshotId: string) => { + chrome.storage.local.get(SNAPSHOTS_KEY, (result) => { + const snapshots = Array.isArray(result[SNAPSHOTS_KEY]) ? result[SNAPSHOTS_KEY] : []; + const updated = snapshots.filter((s: { id: string }) => s.id !== snapshotId); + chrome.storage.local.set({ [SNAPSHOTS_KEY]: updated }); + }); +}); diff --git a/packages/devtools-extension/src/panel/src/Decode.elm b/packages/devtools-extension/src/panel/src/Decode.elm new file mode 100644 index 0000000000..600d2ea7e5 --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Decode.elm @@ -0,0 +1,230 @@ +module Decode exposing (decodeAuthEvent, decodeDiagnosisResult, decodeImportMeta, decodeSnapshotMeta) + +import Json.Decode as JD +import Types + exposing + ( AuthEvent + , DiagnosisResult + , EventData(..) + , EventIssue + , FlowHealth(..) + , FlowIssue + , ImportMeta + , JourneyData + , NetworkData + , NodeData + , OidcData + , SdkAuthorization + , SdkError + , SessionData + , SnapshotMeta + ) + + +decodeSdkError : JD.Decoder SdkError +decodeSdkError = + JD.map4 SdkError + (JD.field "code" JD.string) + (JD.field "message" JD.string) + (JD.field "type" JD.string) + (JD.maybe (JD.field "internalHttpStatus" JD.int)) + + +decodeSdkAuthorization : JD.Decoder SdkAuthorization +decodeSdkAuthorization = + JD.map2 SdkAuthorization + (JD.maybe (JD.field "code" JD.string)) + (JD.maybe (JD.field "state" JD.string)) + + +decodeNetworkData : JD.Decoder NetworkData +decodeNetworkData = + JD.succeed NetworkData + |> andMap (JD.maybe (JD.field "status" JD.int)) + |> andMap (JD.maybe (JD.field "url" JD.string)) + |> andMap (JD.maybe (JD.field "method" JD.string)) + |> andMap (JD.maybe (JD.field "duration" JD.float)) + |> andMap (JD.maybe (JD.field "requestHeaders" JD.value)) + |> andMap (JD.maybe (JD.field "responseHeaders" JD.value)) + |> andMap (JD.maybe (JD.field "requestBody" JD.value)) + |> andMap (JD.maybe (JD.field "responseBody" JD.value)) + + +decodeNodeData : JD.Decoder NodeData +decodeNodeData = + JD.succeed NodeData + |> andMap (JD.maybe (JD.field "nodeStatus" JD.string)) + |> andMap (JD.maybe (JD.field "previousStatus" JD.string)) + |> andMap (JD.maybe (JD.field "interactionId" JD.string)) + |> andMap (JD.maybe (JD.field "interactionToken" JD.string)) + |> andMap (JD.maybe (JD.field "nodeId" JD.string)) + |> andMap (JD.maybe (JD.field "requestId" JD.string)) + |> andMap (JD.maybe (JD.field "nodeName" JD.string)) + |> andMap (JD.maybe (JD.field "nodeDescription" JD.string)) + |> andMap (JD.maybe (JD.field "eventName" JD.string)) + |> andMap (JD.maybe (JD.field "httpStatus" JD.int)) + |> andMap (JD.maybe (JD.field "error" decodeSdkError)) + |> andMap (JD.maybe (JD.field "authorization" decodeSdkAuthorization)) + |> andMap (JD.maybe (JD.field "session" JD.string)) + |> andMap (JD.maybe (JD.field "collectors" (JD.list JD.value))) + |> andMap (JD.maybe (JD.field "responseBody" JD.value)) + + +decodeJourneyData : JD.Decoder JourneyData +decodeJourneyData = + JD.succeed JourneyData + |> andMap (JD.maybe (JD.field "stepType" JD.string)) + |> andMap (JD.maybe (JD.field "stage" JD.string)) + |> andMap (JD.maybe (JD.field "header" JD.string)) + |> andMap (JD.maybe (JD.field "description" JD.string)) + |> andMap (JD.maybe (JD.field "callbacks" (JD.list JD.value))) + |> andMap (JD.maybe (JD.field "authId" JD.string)) + |> andMap (JD.maybe (JD.field "tokenId" JD.string)) + |> andMap (JD.maybe (JD.field "successUrl" JD.string)) + |> andMap (JD.maybe (JD.field "errorCode" JD.int)) + |> andMap (JD.maybe (JD.field "errorMessage" JD.string)) + |> andMap (JD.maybe (JD.field "errorReason" JD.string)) + + +decodeOidcData : JD.Decoder OidcData +decodeOidcData = + JD.succeed OidcData + |> andMap (JD.maybe (JD.field "phase" JD.string)) + |> andMap (JD.maybe (JD.field "status" JD.string)) + |> andMap (JD.maybe (JD.field "clientId" JD.string)) + |> andMap (JD.maybe (JD.field "errorCode" JD.string)) + |> andMap (JD.maybe (JD.field "errorMessage" JD.string)) + + +decodeSessionData : JD.Decoder SessionData +decodeSessionData = + JD.succeed SessionData + |> andMap (JD.maybe (JD.field "key" JD.string)) + |> andMap (JD.maybe (JD.field "before" JD.string)) + |> andMap (JD.maybe (JD.field "after" JD.string)) + + +decodeEventData : String -> String -> JD.Decoder EventData +decodeEventData eventTypeStr source = + case eventTypeStr of + "sdk:node-change" -> + JD.field "data" (JD.map DaVinciNode decodeNodeData) + + "sdk:journey-step" -> + JD.field "data" (JD.map Journey decodeJourneyData) + + "sdk:oidc-state" -> + JD.field "data" (JD.map Oidc decodeOidcData) + + "sdk:config" -> + JD.map Config (JD.maybe (JD.at [ "data", "config" ] JD.value)) + + _ -> + if source == "session" then + JD.field "data" (JD.map Session decodeSessionData) + + else + JD.field "data" (JD.map Network decodeNetworkData) + + +decodeAuthEvent : JD.Decoder AuthEvent +decodeAuthEvent = + JD.field "type" JD.string + |> JD.andThen + (\eventTypeStr -> + JD.field "source" JD.string + |> JD.andThen + (\source -> + JD.succeed AuthEvent + |> andMap (JD.field "id" JD.string) + |> andMap (JD.field "timestamp" JD.float) + |> andMap (JD.succeed eventTypeStr) + |> andMap (JD.succeed source) + |> andMap (JD.field "flowId" (JD.nullable JD.string)) + |> andMap (JD.at [ "flags", "isCors" ] JD.bool) + |> andMap (JD.at [ "flags", "isError" ] JD.bool) + |> andMap (JD.at [ "flags", "isAuthRelated" ] JD.bool) + |> andMap (JD.field "causedBy" (JD.nullable JD.string)) + |> andMap (decodeEventData eventTypeStr source) + ) + ) + + +andMap : JD.Decoder a -> JD.Decoder (a -> b) -> JD.Decoder b +andMap = + JD.map2 (|>) + + +decodeRelevantData : JD.Decoder (Maybe (List ( String, String ))) +decodeRelevantData = + JD.maybe + (JD.field "relevantData" + (JD.keyValuePairs JD.string) + ) + + +decodeEventIssue : JD.Decoder EventIssue +decodeEventIssue = + JD.map5 EventIssue + (JD.field "severity" JD.string) + (JD.field "title" JD.string) + (JD.field "description" JD.string) + (JD.field "steps" (JD.list JD.string)) + decodeRelevantData + + +decodeFlowIssue : JD.Decoder FlowIssue +decodeFlowIssue = + JD.map8 FlowIssue + (JD.field "id" JD.string) + (JD.field "severity" JD.string) + (JD.field "category" JD.string) + (JD.field "title" JD.string) + (JD.field "description" JD.string) + (JD.field "steps" (JD.list JD.string)) + (JD.field "relatedEventIds" (JD.list JD.string)) + decodeRelevantData + + +decodeFlowHealth : JD.Decoder FlowHealth +decodeFlowHealth = + JD.string + |> JD.andThen + (\s -> + case s of + "error" -> + JD.succeed Error + + "warning" -> + JD.succeed Warning + + _ -> + JD.succeed Healthy + ) + + +decodeDiagnosisResult : JD.Decoder DiagnosisResult +decodeDiagnosisResult = + JD.map3 DiagnosisResult + (JD.field "flowHealth" decodeFlowHealth) + (JD.field "issues" (JD.list decodeFlowIssue)) + (JD.field "annotatedEvents" + (JD.keyValuePairs (JD.list decodeEventIssue)) + ) + + +decodeImportMeta : JD.Decoder ImportMeta +decodeImportMeta = + JD.map3 ImportMeta + (JD.field "flowId" (JD.nullable JD.string)) + (JD.field "capturedAt" JD.string) + (JD.field "redacted" JD.bool) + + +decodeSnapshotMeta : JD.Decoder SnapshotMeta +decodeSnapshotMeta = + JD.map4 SnapshotMeta + (JD.field "id" JD.string) + (JD.field "savedAt" JD.string) + (JD.field "flowId" (JD.nullable JD.string)) + (JD.field "eventCount" JD.int) diff --git a/packages/devtools-extension/src/panel/src/FlowView.elm b/packages/devtools-extension/src/panel/src/FlowView.elm new file mode 100644 index 0000000000..435301932d --- /dev/null +++ b/packages/devtools-extension/src/panel/src/FlowView.elm @@ -0,0 +1,669 @@ +module FlowView exposing (view, viewPlaybackControls) + +import Helpers +import Html exposing (Html) +import Html.Attributes exposing (..) +import Html.Events +import Json.Encode as Encode +import JsonTree +import Set exposing (Set) +import Svg exposing (..) +import Svg.Attributes as SA +import Svg.Events +import Types exposing (AuthEvent, EventData(..), JourneyData, NetworkData, NodeData, OidcData) +import Update exposing (Msg(..)) + + +-- Unified status string for a node — works for DaVinci, Journey, and OIDC events. +nodeStatusLabel : AuthEvent -> String +nodeStatusLabel event = + case event.data of + Oidc oidc -> + Maybe.withDefault "unknown" oidc.status + + Journey journey -> + Maybe.withDefault "Step" journey.stepType + + DaVinciNode node -> + Maybe.withDefault "unknown" node.nodeStatus + + _ -> + "unknown" + + +-- Unified display label for the rail node. +nodeDisplayLabel : AuthEvent -> String +nodeDisplayLabel event = + case event.data of + Oidc oidc -> + Maybe.withDefault "oidc" oidc.phase + + Journey journey -> + journey.stage + |> orMaybe journey.header + |> orMaybe journey.stepType + |> Maybe.withDefault "—" + + DaVinciNode node -> + node.nodeName + |> orMaybe node.eventName + |> Maybe.withDefault "—" + + _ -> + "—" + + +orMaybe : Maybe a -> Maybe a -> Maybe a +orMaybe fallback primary = + case primary of + Just _ -> + primary + + Nothing -> + fallback + + +-- ── SVG Rail ────────────────────────────────────────────────────────────────── + + +nodeSpacing : Int +nodeSpacing = + 140 + + +nodeRadius : Int +nodeRadius = + 18 + + +railHeight : Int +railHeight = + 110 + + +viewRail : List AuthEvent -> Maybe Int -> Maybe String -> Html Msg +viewRail events playbackIndex selectedNodeId = + let + sdkNodes = + Helpers.sdkNodes events + + visibleNodes = + case playbackIndex of + Nothing -> + sdkNodes + + Just n -> + List.take (n + 1) sdkNodes + + count = + List.length visibleNodes + + svgWidth = + if count == 0 then + 200 + else + count * nodeSpacing + 60 + in + Html.div [ Html.Attributes.class "fv-rail" ] + [ if List.isEmpty sdkNodes then + Html.div [ Html.Attributes.class "fv-rail-empty" ] [ Html.text "No SDK nodes recorded yet." ] + + else + Svg.svg + [ SA.width (String.fromInt svgWidth) + , SA.height (String.fromInt railHeight) + , SA.viewBox ("0 0 " ++ String.fromInt svgWidth ++ " " ++ String.fromInt railHeight) + , SA.style "display:block" + ] + (railDefs + :: List.concat (List.indexedMap (renderRailNode selectedNodeId) visibleNodes) + ++ List.concat (List.indexedMap (\i _ -> renderArrow (List.length visibleNodes) i) visibleNodes) + ) + ] + + +railDefs : Svg Msg +railDefs = + Svg.defs [] + [ Svg.filter [ SA.id "fv-glow" ] + [ Svg.feGaussianBlur [ SA.stdDeviation "4", SA.result "blur" ] [] + , Svg.feMerge [] + [ Svg.feMergeNode [ SA.in_ "blur" ] [] + , Svg.feMergeNode [ SA.in_ "SourceGraphic" ] [] + ] + ] + , Svg.marker + [ SA.id "arrowhead" + , SA.markerWidth "8" + , SA.markerHeight "8" + , SA.refX "6" + , SA.refY "3" + , SA.orient "auto" + ] + [ Svg.polygon + [ SA.points "0 0, 8 3, 0 6" + , SA.fill "#30363D" + ] + [] + ] + ] + + +renderRailNode : Maybe String -> Int -> AuthEvent -> List (Svg Msg) +renderRailNode selectedNodeId index event = + let + cx_ = + index * nodeSpacing + 40 + + cy_ = + 44 + + status = + nodeStatusLabel event + + color = + Helpers.nodeColor status + + isSelected = + selectedNodeId == Just event.id + + label = + nodeDisplayLabel event + + glowRing = + if isSelected then + [ Svg.circle + [ SA.cx (String.fromInt cx_) + , SA.cy (String.fromInt cy_) + , SA.r (String.fromInt (nodeRadius + 6)) + , SA.fill "none" + , SA.stroke color + , SA.strokeWidth "2" + , SA.strokeOpacity "0.5" + , SA.filter "url(#fv-glow)" + ] + [] + ] + + else + [] + + nodeBg = + Svg.circle + [ SA.cx (String.fromInt cx_) + , SA.cy (String.fromInt cy_) + , SA.r (String.fromInt nodeRadius) + , SA.fill color + ] + [] + + nameLabel = + Svg.text_ + [ SA.x (String.fromInt cx_) + , SA.y (String.fromInt (cy_ + nodeRadius + 14)) + , SA.textAnchor "middle" + , SA.fontSize "10" + , SA.fill "#8B949E" + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text (truncate_ 14 label) ] + + statusLabel = + Svg.text_ + [ SA.x (String.fromInt cx_) + , SA.y (String.fromInt (cy_ + nodeRadius + 26)) + , SA.textAnchor "middle" + , SA.fontSize "10" + , SA.fill color + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text status ] + in + glowRing + ++ [ Svg.g + [ Svg.Events.onClick (SelectFlowNode event.id) + , SA.style "cursor:pointer" + ] + [ nodeBg, nameLabel, statusLabel ] + ] + + +renderArrow : Int -> Int -> List (Svg Msg) +renderArrow total index = + if index >= total - 1 then + [] + + else + let + x1_ = + index * nodeSpacing + 40 + nodeRadius + 16 + + x2_ = + (index + 1) * nodeSpacing + 40 - nodeRadius - 16 + + y_ = + 44 + in + [ Svg.line + [ SA.x1 (String.fromInt x1_) + , SA.y1 (String.fromInt y_) + , SA.x2 (String.fromInt x2_) + , SA.y2 (String.fromInt y_) + , SA.stroke "#30363D" + , SA.strokeWidth "1.5" + , SA.markerEnd "url(#arrowhead)" + ] + [] + ] + + +truncate_ : Int -> String -> String +truncate_ maxLen s = + if String.length s <= maxLen then + s + + else + String.left maxLen s ++ "…" + + +-- ── Detail card ─────────────────────────────────────────────────────────────── + + +viewDetail : List AuthEvent -> Maybe String -> Set String -> Html Msg +viewDetail events selectedNodeId expandedSubRows = + case selectedNodeId of + Nothing -> + Html.div [ Html.Attributes.class "fv-detail fv-detail-empty" ] + [ Html.text "Select a node to see its details." ] + + Just nodeId -> + let + maybeNode = + List.head (List.filter (\e -> e.id == nodeId) events) + + netEvents = + List.filter (\e -> e.causedBy == Just nodeId) events + |> List.sortBy .timestamp + in + Html.div [ Html.Attributes.class "fv-detail" ] + (viewNodeData maybeNode expandedSubRows + ++ viewNetworkSection nodeId netEvents expandedSubRows + ) + + +viewKvRow : String -> String -> Html Msg +viewKvRow key val = + Html.div [ Html.Attributes.class "fv-kv-row" ] + [ Html.span [ Html.Attributes.class "fv-kv-key" ] [ Html.text key ] + , Html.span [ Html.Attributes.class "fv-kv-val" ] [ Html.text val ] + , Html.button + [ Html.Attributes.class "fv-copy-btn" + , Html.Events.onClick (CopyToClipboard val) + ] + [ Html.text "⎘" ] + ] + + +viewNodeData : Maybe AuthEvent -> Set String -> List (Html Msg) +viewNodeData maybeNode expandedSubRows = + case maybeNode of + Nothing -> + [] + + Just node -> + case node.data of + Journey journey -> + viewJourneyNodeData node.id journey expandedSubRows + + Oidc oidc -> + viewOidcNodeData oidc + + DaVinciNode dvNode -> + viewDaVinciNodeData node.id dvNode expandedSubRows + + _ -> + [] + + +viewDaVinciNodeData : String -> NodeData -> Set String -> List (Html Msg) +viewDaVinciNodeData nodeId node expandedSubRows = + let + hasResponse = + node.responseBody /= Nothing + + collectorCount = + Maybe.withDefault 0 (Maybe.map List.length node.collectors) + + hasCollectors = + collectorCount > 0 + + responseKey = + nodeId ++ ":node-response" + + collectorsKey = + nodeId ++ ":node-collectors" + in + if not hasResponse && not hasCollectors then + [] + + else + [ Html.div [ Html.Attributes.class "fv-net-group" ] + [ Html.div [ Html.Attributes.class "fv-net-group-header" ] + [ Html.span [ Html.Attributes.class "fv-node-label" ] + [ Html.text (Maybe.withDefault "Node Response" node.nodeName) ] + ] + , if hasResponse then + viewSection responseKey "Response" expandedSubRows + [ case node.responseBody of + Just body -> JsonTree.view "Response" body + Nothing -> Html.text "" + ] + + else + Html.text "" + , if hasCollectors then + viewSection collectorsKey ("Collectors (" ++ String.fromInt collectorCount ++ ")") expandedSubRows + (case node.collectors of + Nothing -> [] + Just cs -> + Html.div [ Html.Attributes.class "coll-copy-all-row" ] + [ Html.button + [ Html.Attributes.class "fv-copy-btn coll-copy-all" + , Html.Events.onClick (CopyToClipboard (Encode.encode 4 (Encode.list identity cs))) + ] + [ Html.text "Copy all" ] + ] + :: List.indexedMap + (\i c -> + Html.div [ Html.Attributes.class "coll-card" ] + [ Html.div [ Html.Attributes.class "coll-card-header" ] + [ Html.span [] [ Html.text ("Collector " ++ String.fromInt (i + 1)) ] + , Html.button + [ Html.Attributes.class "fv-copy-btn" + , Html.Events.onClick (CopyToClipboard (Encode.encode 4 c)) + ] + [ Html.text "\u{2398}" ] + ] + , JsonTree.view ("Collector " ++ String.fromInt (i + 1)) c + ] + ) + cs + ) + + else + Html.text "" + ] + ] + + +viewJourneyNodeData : String -> JourneyData -> Set String -> List (Html Msg) +viewJourneyNodeData nodeId journey expandedSubRows = + let + stepType = + Maybe.withDefault "Step" journey.stepType + + callbacks = + Maybe.withDefault [] journey.callbacks + + cbCount = + List.length callbacks + + hasCallbacks = + cbCount > 0 + + isFailure = + stepType == "LoginFailure" + + isSuccess = + stepType == "LoginSuccess" + + callbacksKey = + nodeId ++ ":journey-callbacks" + + sessionKey = + nodeId ++ ":journey-session" + + title = + case journey.header of + Just h -> h + Nothing -> + case journey.stage of + Just s -> s + Nothing -> stepType + in + [ Html.div [ Html.Attributes.class "fv-net-group" ] + ([ Html.div [ Html.Attributes.class "fv-net-group-header" ] + [ Html.span [ Html.Attributes.class "fv-node-label" ] [ Html.text title ] + , Html.span + [ Html.Attributes.class + ("fv-net-status " + ++ (if isFailure then "st-err" else if isSuccess then "st-ok" else "st-nil") + ) + ] + [ Html.text stepType ] + ] + ] + ++ (if isFailure then + [ Html.div [ Html.Attributes.class "fv-journey-error" ] + [ case journey.errorMessage of + Just msg -> Html.p [] [ Html.text msg ] + Nothing -> Html.text "" + , case journey.errorReason of + Just reason -> Html.p [ Html.Attributes.class "fv-journey-reason" ] [ Html.text ("Reason: " ++ reason) ] + Nothing -> Html.text "" + ] + ] + + else if isSuccess then + [ viewSection sessionKey "Session" expandedSubRows + [ Html.div [ Html.Attributes.class "fv-kv-list" ] + (List.filterMap identity + [ Maybe.map (viewKvRow "tokenId") journey.tokenId + , Maybe.map (viewKvRow "successUrl") journey.successUrl + ] + ) + ] + ] + + else + [] + ) + ++ (if hasCallbacks then + [ viewSection callbacksKey ("Callbacks (" ++ String.fromInt cbCount ++ ")") expandedSubRows + (List.indexedMap + (\i cb -> + Html.div [ Html.Attributes.class "coll-card" ] + [ JsonTree.view ("Callback " ++ String.fromInt (i + 1)) cb ] + ) + callbacks + ) + ] + + else + [] + ) + ) + ] + + +viewOidcNodeData : OidcData -> List (Html Msg) +viewOidcNodeData oidc = + let + phase = + Maybe.withDefault "—" oidc.phase + + status = + Maybe.withDefault "—" oidc.status + + isError = + status == "error" + in + [ Html.div [ Html.Attributes.class "fv-net-group" ] + [ Html.div [ Html.Attributes.class "fv-net-group-header" ] + [ Html.span [ Html.Attributes.class "fv-node-label" ] [ Html.text phase ] + , Html.span + [ Html.Attributes.class ("fv-net-status " ++ (if isError then "st-err" else "st-ok")) ] + [ Html.text status ] + ] + , Html.div [ Html.Attributes.class "fv-kv-list" ] + (List.filterMap identity + [ Maybe.map (viewKvRow "clientId") oidc.clientId + , if isError then Maybe.map (viewKvRow "errorCode") oidc.errorCode else Nothing + , if isError then Maybe.map (viewKvRow "errorMessage") oidc.errorMessage else Nothing + ] + ) + ] + ] + + +viewNetworkSection : String -> List AuthEvent -> Set String -> List (Html Msg) +viewNetworkSection _ netEvents expandedSubRows = + if List.isEmpty netEvents then + [] + + else + List.map (\e -> viewNetGroup e expandedSubRows) netEvents + + +viewNetGroup : AuthEvent -> Set String -> Html Msg +viewNetGroup event expandedSubRows = + case event.data of + Network net -> + let + statusText = + Maybe.withDefault "—" (Maybe.map String.fromInt net.status) + + durationText = + case net.duration of + Nothing -> "" + Just ms -> + if ms < 1 then "<1ms" + else String.fromInt (round ms) ++ "ms" + + urlText = + Maybe.withDefault "—" net.url + + hasResponse = + net.responseBody /= Nothing + + hasRequest = + net.requestBody /= Nothing + + hasAny = + hasResponse || hasRequest + + responseKey = + event.id ++ ":response" + + requestKey = + event.id ++ ":request" + in + Html.div [ Html.Attributes.class "fv-net-group" ] + [ Html.div [ Html.Attributes.class "fv-net-group-header" ] + [ Html.span [ Html.Attributes.class ("tl-meth " ++ Helpers.methodClass net.method) ] + [ Html.text (Maybe.withDefault "—" net.method) ] + , Html.span [ Html.Attributes.class "fv-net-url" ] [ Html.text urlText ] + , Html.span [ Html.Attributes.class ("fv-net-status " ++ Helpers.statusClass net.status) ] + [ Html.text statusText ] + , Html.span [ Html.Attributes.class "fv-net-dur" ] [ Html.text durationText ] + ] + , if not hasAny then + Html.div [ Html.Attributes.class "fv-no-data" ] [ Html.text "No data captured for this request." ] + + else + Html.text "" + , if hasResponse then + viewSection responseKey "Response" expandedSubRows + [ case net.responseBody of + Just body -> JsonTree.view "Response" body + Nothing -> Html.text "" + ] + + else + Html.text "" + , if hasRequest then + viewSection requestKey "Request" expandedSubRows + [ case net.requestBody of + Just body -> JsonTree.view "Request" body + Nothing -> Html.text "" + ] + + else + Html.text "" + ] + + _ -> + Html.text "" + + +viewSection : String -> String -> Set String -> List (Html Msg) -> Html Msg +viewSection key label expandedSubRows content = + let + isOpen = + Set.member key expandedSubRows + + icon = + if isOpen then "▼ " else "▶ " + in + Html.div [] + [ Html.div + [ Html.Attributes.class "fv-section-row" + , Html.Events.onClick (ToggleSubRow key) + ] + [ Html.text (icon ++ label) ] + , if isOpen then + Html.div [ Html.Attributes.class "fv-section-body" ] content + + else + Html.text "" + ] + + +-- ── Main view ───────────────────────────────────────────────────────────────── + + +view : List AuthEvent -> Maybe Int -> Maybe String -> Set String -> Html Msg +view events playbackIndex selectedNodeId expandedSubRows = + Html.div [ Html.Attributes.class "fv-view" ] + [ viewRail events playbackIndex selectedNodeId + , viewDetail events selectedNodeId expandedSubRows + ] + + +-- ── Playback controls ───────────────────────────────────────────────────────── + + +viewPlaybackControls : List AuthEvent -> Maybe Int -> Bool -> Html Msg +viewPlaybackControls events playbackIndex isPlaying = + let + sdkNodes = + Helpers.sdkNodes events + + total = + List.length sdkNodes + + stepLabel = + case playbackIndex of + Just n -> "Step " ++ String.fromInt (n + 1) ++ " / " ++ String.fromInt total + Nothing -> "" + in + Html.div [ Html.Attributes.class "fv-playback-controls" ] + [ Html.button [ Html.Events.onClick ResetPlayback, Html.Attributes.class "tb-btn" ] [ Html.text "◀◀" ] + , if isPlaying then + Html.button [ Html.Events.onClick StopPlayback, Html.Attributes.class "tb-btn" ] [ Html.text "⏸ Pause" ] + + else + Html.button + [ Html.Events.onClick StartPlayback + , Html.Attributes.class "tb-btn" + , Html.Attributes.disabled (List.isEmpty sdkNodes) + ] + [ Html.text + (if playbackIndex == Nothing then "▶ Play" else "▶ Resume") + ] + , if stepLabel /= "" then + Html.span [ Html.Attributes.class "fv-step-label" ] [ Html.text stepLabel ] + + else + Html.text "" + ] diff --git a/packages/devtools-extension/src/panel/src/Graph.elm b/packages/devtools-extension/src/panel/src/Graph.elm new file mode 100644 index 0000000000..eb6a7fa09e --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Graph.elm @@ -0,0 +1,235 @@ +module Graph exposing (view) + +import Helpers +import Html exposing (Html, div) +import Html.Attributes exposing (class) +import Json.Decode as Decode +import Svg exposing (..) +import Svg.Attributes as SA +import Svg.Events +import Types exposing (AuthEvent, EventData(..)) +import Update exposing (Msg(..)) + + +view : List AuthEvent -> Maybe String -> Maybe String -> Html Msg +view events selectedId hoveredId = + let + sdkNodes = + List.filter (\e -> e.eventType == "sdk:node-change") events + + nodeSpacing = + 90 + + totalHeight = + Basics.max 60 ((List.length sdkNodes * nodeSpacing) + 48) + in + if List.isEmpty sdkNodes then + div [ class "graph-empty" ] [ Svg.text "No SDK nodes" ] + + else + svg + [ SA.width "210" + , SA.height (String.fromInt totalHeight) + , SA.viewBox ("0 0 210 " ++ String.fromInt totalHeight) + , SA.class "graph-svg" + ] + (defs_ + :: List.concat (List.indexedMap (renderNode nodeSpacing selectedId hoveredId) sdkNodes) + ) + + +defs_ : Svg Msg +defs_ = + defs [] + [ Svg.filter [ SA.id "glow-node" ] + [ feGaussianBlur [ SA.stdDeviation "4", SA.result "blur" ] [] + , feMerge [] + [ feMergeNode [ SA.in_ "blur" ] [] + , feMergeNode [ SA.in_ "SourceGraphic" ] [] + ] + ] + ] + + +renderNode : Int -> Maybe String -> Maybe String -> Int -> AuthEvent -> List (Svg Msg) +renderNode spacing selectedId hoveredId index event = + let + cy_ = + index * spacing + 28 + + ( status, maybeName, maybeCollectors ) = + case event.data of + DaVinciNode node -> + ( Maybe.withDefault "unknown" node.nodeStatus, node.nodeName, node.collectors ) + + _ -> + ( "unknown", Nothing, Nothing ) + + color = + Helpers.nodeColor status + + isSelected = + selectedId == Just event.id + + isHovered = + hoveredId == Just event.id + + connectorLine = + if index > 0 then + [ line + [ SA.x1 "26" + , SA.y1 (String.fromInt (cy_ - spacing + 28)) + , SA.x2 "26" + , SA.y2 (String.fromInt (cy_ - 28)) + , SA.stroke color + , SA.strokeWidth "1" + , SA.strokeOpacity + (if isHovered || isSelected then + "0.5" + + else + "0.2" + ) + , SA.strokeDasharray "4 4" + ] + [] + ] + + else + [] + + selectionRing = + if isSelected then + [ circle + [ SA.cx "26" + , SA.cy (String.fromInt cy_) + , SA.r "19" + , SA.fill "none" + , SA.stroke color + , SA.strokeWidth "1.5" + , SA.strokeOpacity "0.5" + , SA.class "graph-ring" + ] + [] + ] + + else + [] + + hoverRing = + if isHovered && not isSelected then + [ circle + [ SA.cx "26" + , SA.cy (String.fromInt cy_) + , SA.r "16" + , SA.fill "none" + , SA.stroke color + , SA.strokeWidth "1" + , SA.strokeOpacity "0.4" + ] + [] + ] + + else + [] + + nodeFilterAttr = + if isSelected then + SA.filter "url(#glow-node)" + + else if isHovered then + SA.filter "url(#glow-node)" + + else + SA.class "" + + nodeBg = + circle + [ SA.cx "26" + , SA.cy (String.fromInt cy_) + , SA.r "10" + , SA.fill color + , nodeFilterAttr + ] + [] + + statusLabel = + Svg.text_ + [ SA.x "44" + , SA.y (String.fromInt (cy_ + 4)) + , SA.fontSize "12" + , SA.fill + (if isHovered || isSelected then + "#ffffff" + + else + "#E6EDF3" + ) + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text status ] + + subLabel = + let + subFill = + if isHovered || isSelected then + "#c9d1d9" + + else + "#8B949E" + in + case maybeName of + Just name -> + [ Svg.text_ + [ SA.x "44" + , SA.y (String.fromInt (cy_ + 17)) + , SA.fontSize "10" + , SA.fill subFill + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text name ] + ] + + Nothing -> + case maybeCollectors of + Just cs -> + if List.length cs > 0 then + [ Svg.text_ + [ SA.x "44" + , SA.y (String.fromInt (cy_ + 17)) + , SA.fontSize "10" + , SA.fill subFill + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text (String.fromInt (List.length cs) ++ " collectors") ] + ] + + else + [] + + Nothing -> + [] + + -- Invisible hit area for reliable mouse events + hitArea = + rect + [ SA.x "0" + , SA.y (String.fromInt (cy_ - 20)) + , SA.width "200" + , SA.height "40" + , SA.fill "transparent" + , SA.style "cursor:pointer" + ] + [] + in + connectorLine + ++ selectionRing + ++ hoverRing + ++ [ g + [ Svg.Events.onClick (SelectNode event.id) + , Svg.Events.on "mouseenter" (Decode.succeed (HoverNode (Just event.id))) + , Svg.Events.on "mouseleave" (Decode.succeed (HoverNode Nothing)) + , SA.style "cursor:pointer" + ] + ([ hitArea, nodeBg, statusLabel ] ++ subLabel) + ] diff --git a/packages/devtools-extension/src/panel/src/Helpers.elm b/packages/devtools-extension/src/panel/src/Helpers.elm new file mode 100644 index 0000000000..ecbb8043d6 --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Helpers.elm @@ -0,0 +1,173 @@ +module Helpers exposing + ( EventSource(..) + , EventType(..) + , eventSource + , eventType + , findEvent + , findEventInList + , isSdkNode + , methodClass + , nodeColor + , sdkNodes + , statusClass + , truncateId + ) + +import Dict exposing (Dict) +import Types exposing (AuthEvent, EventData(..)) + + +type EventType + = NodeChange + | JourneyStep + | OidcState + | SdkConfig + | NetworkEvent + | OtherEvent String + + +type EventSource + = SessionSource + | OtherSource String + + +eventType : AuthEvent -> EventType +eventType event = + case event.eventType of + "sdk:node-change" -> + NodeChange + + "sdk:journey-step" -> + JourneyStep + + "sdk:oidc-state" -> + OidcState + + "sdk:config" -> + SdkConfig + + _ -> + if event.source == "network" then + NetworkEvent + + else + OtherEvent event.eventType + + +eventSource : AuthEvent -> EventSource +eventSource event = + case event.source of + "session" -> + SessionSource + + other -> + OtherSource other + + +isSdkNode : AuthEvent -> Bool +isSdkNode event = + case event.data of + DaVinciNode _ -> + True + + Journey _ -> + True + + Oidc _ -> + True + + _ -> + False + + +sdkNodes : List AuthEvent -> List AuthEvent +sdkNodes events = + List.filter isSdkNode events + |> List.sortBy .timestamp + + +findEvent : String -> Dict String AuthEvent -> Maybe AuthEvent +findEvent id eventsById = + Dict.get id eventsById + + +findEventInList : String -> List AuthEvent -> Maybe AuthEvent +findEventInList id events = + List.head (List.filter (\e -> e.id == id) events) + + +statusClass : Maybe Int -> String +statusClass maybeStatus = + case maybeStatus of + Nothing -> + "st-nil" + + Just 0 -> + "st-err" + + Just s -> + if s >= 400 then + "st-warn" + + else + "st-ok" + + +methodClass : Maybe String -> String +methodClass maybeMethod = + case maybeMethod of + Nothing -> + "m-other" + + Just m -> + case String.toUpper m of + "GET" -> + "m-get" + + "POST" -> + "m-post" + + "PUT" -> + "m-put" + + "PATCH" -> + "m-patch" + + "DELETE" -> + "m-del" + + _ -> + "m-other" + + +nodeColor : String -> String +nodeColor status = + case status of + "continue" -> + "#58A6FF" + + "success" -> + "#3FB950" + + "error" -> + "#F85149" + + "failure" -> + "#F85149" + + "Step" -> + "#58A6FF" + + "LoginSuccess" -> + "#3FB950" + + "LoginFailure" -> + "#F85149" + + _ -> + "#484F58" + + +truncateId : String -> String +truncateId id = + String.left 8 id diff --git a/packages/devtools-extension/src/panel/src/Inspector.elm b/packages/devtools-extension/src/panel/src/Inspector.elm new file mode 100644 index 0000000000..cab1afefa7 --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Inspector.elm @@ -0,0 +1,596 @@ +module Inspector exposing (view) + +import Helpers +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Json.Decode as Decode +import Json.Encode as Encode +import JsonTree +import Types exposing (AuthEvent, DiagnosisResult, EventData(..), EventIssue, InspectorTab(..), NetworkData, NodeData, SessionData, SdkAuthorization, SdkError) +import Update exposing (Msg(..)) + + +view : Maybe AuthEvent -> InspectorTab -> Maybe DiagnosisResult -> Html Msg +view selectedEvent activeTab maybeDiagnosis = + div [ style "display" "flex", style "flex-direction" "column", style "height" "100%" ] + [ viewTabs selectedEvent activeTab maybeDiagnosis + , div [ class "insp-body" ] + [ viewContent selectedEvent activeTab maybeDiagnosis ] + ] + + +eventIssues : Maybe AuthEvent -> Maybe DiagnosisResult -> List EventIssue +eventIssues maybeEvent maybeDiagnosis = + case ( maybeEvent, maybeDiagnosis ) of + ( Just event, Just diagnosis ) -> + diagnosis.annotatedEvents + |> List.filter (\( id, _ ) -> id == event.id) + |> List.concatMap (\( _, issues ) -> issues) + + _ -> + [] + + +viewTabs : Maybe AuthEvent -> InspectorTab -> Maybe DiagnosisResult -> Html Msg +viewTabs maybeEvent activeTab maybeDiagnosis = + let + isSdkEvent = + case maybeEvent of + Just event -> + case event.data of + DaVinciNode _ -> + True + + _ -> + False + + Nothing -> + False + + isSessionEvent = + case maybeEvent of + Just event -> + case event.data of + Session _ -> + True + + _ -> + False + + Nothing -> + False + + isConfigEvent = + case maybeEvent of + Just event -> + case event.data of + Config _ -> + True + + _ -> + False + + Nothing -> + False + + issues = + eventIssues maybeEvent maybeDiagnosis + + hasDiagnosis = + not (List.isEmpty issues) + + diagnosisTabLabel = + if List.any (\i -> i.severity == "error") issues then + "Diagnosis ●" + + else + "Diagnosis ◐" + in + div [ class "tab-bar" ] + ((if hasDiagnosis then + [ tabButton diagnosisTabLabel DiagnosisTab activeTab ] + + else + [] + ) + ++ [ tabButton "Headers" HeadersTab activeTab + , tabButton "Cookies" CookiesTab activeTab + , tabButton "CORS" CorsTab activeTab + , tabButton "SDK State" SdkStateTab activeTab + ] + ++ (if isSdkEvent then + [ tabButton "Collectors" CollectorsTab activeTab ] + + else + [] + ) + ++ (if isSessionEvent then + [ tabButton "Session" SessionTab activeTab ] + + else + [] + ) + ++ (if isConfigEvent then + [ tabButton "Config" ConfigTab activeTab ] + + else + [] + ) + ) + + +tabButton : String -> InspectorTab -> InspectorTab -> Html Msg +tabButton label tab activeTab = + let + cls = + if tab == activeTab then "tab-btn active" else "tab-btn" + in + button [ onClick (SwitchTab tab), class cls ] [ text label ] + + +viewContent : Maybe AuthEvent -> InspectorTab -> Maybe DiagnosisResult -> Html Msg +viewContent maybeEvent activeTab maybeDiagnosis = + case ( maybeEvent, activeTab ) of + ( Nothing, _ ) -> + div [ class "insp-empty" ] [ text "Select a request to inspect" ] + + ( Just event, DiagnosisTab ) -> + let + issues = + eventIssues (Just event) maybeDiagnosis + in + if List.isEmpty issues then + div [ class "insp-empty" ] [ text "No issues for this event." ] + + else + div [] (List.map viewEventIssue issues) + + ( Just event, HeadersTab ) -> + case event.data of + Network net -> + div [] + ([ div [ class "kv-row", style "margin-bottom" "8px" ] + [ span [ class "kv-key" ] [ text "URL" ] + , span [ class "kv-val" ] [ text (Maybe.withDefault "—" net.url) ] + ] + , div [ class "kv-row", style "margin-bottom" "8px" ] + [ span [ class "kv-key" ] [ text "Method" ] + , span [ class "kv-val" ] [ text (Maybe.withDefault "—" net.method) ] + ] + ] + ++ viewCausedBy event + ++ [ case net.requestHeaders of + Just h -> JsonTree.view "Request Headers" h + Nothing -> viewEmptySection "Request Headers" + , case net.responseHeaders of + Just h -> JsonTree.view "Response Headers" h + Nothing -> viewEmptySection "Response Headers" + ] + ++ (case net.requestBody of + Just b -> [ JsonTree.view "Request Body" b ] + Nothing -> [] + ) + ++ (case net.responseBody of + Just b -> [ JsonTree.view "Response Body" b ] + Nothing -> [] + ) + ) + + _ -> + div [ class "insp-empty" ] + [ text "Select a network request to see headers." ] + + ( Just event, CookiesTab ) -> + case event.data of + Network net -> + div [] (viewCookies net) + + _ -> + div [ class "insp-empty" ] [ text "No cookies found in headers." ] + + ( Just event, CorsTab ) -> + if event.isCors then + let + urlText = + case event.data of + Network net -> + Maybe.withDefault "—" net.url + + _ -> + "—" + in + div [ class "kv-row", style "color" "var(--red)", style "padding-top" "4px" ] + [ text ("CORS issue detected on " ++ urlText) ] + + else + div [ class "kv-row", style "color" "var(--green)", style "padding-top" "4px" ] + [ text "No CORS issues detected for this request." ] + + ( Just event, SdkStateTab ) -> + case event.data of + DaVinciNode node -> + viewSdkState node + + _ -> + div [ class "insp-empty" ] + [ text "Select an SDK node row to inspect." ] + + ( Just event, CollectorsTab ) -> + case event.data of + DaVinciNode node -> + div [] (viewCollectors node) + + _ -> + div [ class "insp-empty" ] [ text "No collectors for this event type." ] + + ( Just event, SessionTab ) -> + case event.data of + Session sess -> + div [] (viewSession sess) + + _ -> + div [ class "insp-empty" ] [ text "No session data for this event type." ] + + ( Just event, ConfigTab ) -> + case event.data of + Config maybeCfg -> + div [] (viewConfig maybeCfg) + + _ -> + div [ class "insp-empty" ] [ text "No config data for this event type." ] + + +viewSdkState : NodeData -> Html Msg +viewSdkState node = + div [] + (viewNodeSection node + ++ viewInteractionSection node + ++ viewErrorSection node + ++ viewAuthorizationSection node + ) + + +viewNodeSection : NodeData -> List (Html Msg) +viewNodeSection node = + let + statusRow = + case node.nodeStatus of + Just s -> + let + valClass = + case s of + "error" -> "kv-val kv-bold kv-err" + "failure" -> "kv-val kv-bold kv-err" + "success" -> "kv-val kv-bold kv-ok" + "continue" -> "kv-val kv-bold kv-cont" + _ -> "kv-val" + + arrow = + case node.previousStatus of + Just prev -> + span [] + [ span [ class "kv-arrow" ] [ text (prev ++ " ") ] + , span [ class "kv-arrow" ] [ text "→ " ] + , span [ class valClass ] [ text s ] + ] + + Nothing -> + span [ class valClass ] [ text s ] + in + [ viewRow "Status" arrow ] + + Nothing -> + [] + + httpRow = + case node.httpStatus of + Just code -> + let + cls = + if code >= 400 then "kv-val kv-err" else "kv-val kv-ok" + in + [ viewRow "HTTP" (span [ class cls ] [ text (String.fromInt code) ]) ] + + Nothing -> + [] + in + [ div [ class "sect-hdr" ] [ text "Node" ] ] + ++ statusRow + ++ httpRow + ++ viewOptionalRow "Event" node.eventName + ++ viewOptionalRow "Form Name" node.nodeName + ++ viewOptionalRow "Description" node.nodeDescription + + +viewInteractionSection : NodeData -> List (Html Msg) +viewInteractionSection node = + let + rows = + viewOptionalRow "Interaction ID" node.interactionId + ++ viewOptionalRow "Node ID" node.nodeId + ++ viewOptionalRow "Request ID" node.requestId + ++ viewOptionalRow "Token" node.interactionToken + in + if List.isEmpty rows then + [] + + else + [ div [ class "sect-hdr" ] [ text "Interaction" ] ] ++ rows + + +viewErrorSection : NodeData -> List (Html Msg) +viewErrorSection node = + case node.sdkError of + Nothing -> + [] + + Just err -> + let + httpRow = + case err.internalHttpStatus of + Just code -> + [ viewRow "HTTP Status" + (span [ class "kv-val kv-err" ] [ text (String.fromInt code) ]) + ] + + Nothing -> + [] + in + [ div [ class "sect-hdr" ] [ text "Error" ] ] + ++ [ viewRow "Code" (span [ class "kv-val kv-err kv-bold" ] [ text err.code ]) + , viewRow "Type" (span [ class "kv-val" ] [ text err.errorType ]) + , viewRow "Message" (span [ class "kv-val" ] [ text err.message ]) + ] + ++ httpRow + + +viewAuthorizationSection : NodeData -> List (Html Msg) +viewAuthorizationSection node = + let + authRows = + case node.authorization of + Nothing -> [] + Just auth -> + viewOptionalRow "Auth Code" auth.code + ++ viewOptionalRow "State" auth.state + + sessionRows = + viewOptionalRow "Session ID" node.session + + rows = + authRows ++ sessionRows + in + if List.isEmpty rows then + [] + + else + [ div [ class "sect-hdr" ] [ text "Authorization" ] ] ++ rows + + +viewRow : String -> Html Msg -> Html Msg +viewRow label valueHtml = + div [ class "kv-row" ] + [ span [ class "kv-key" ] [ text label ] + , valueHtml + ] + + +viewOptionalRow : String -> Maybe String -> List (Html Msg) +viewOptionalRow label maybeValue = + case maybeValue of + Nothing -> + [] + + Just value -> + [ viewRow label (span [ class "kv-val" ] [ text value ]) ] + + +viewEmptySection : String -> Html Msg +viewEmptySection label = + div [ class "jt-sec" ] + [ div [ class "jt-label" ] [ text label ] + , div [ style "color" "var(--dim)", style "font-style" "italic", style "font-size" "11px" ] + [ text "None" ] + ] + + +viewCookies : NetworkData -> List (Html Msg) +viewCookies net = + let + cookieRows = + case net.requestHeaders of + Nothing -> + [] + + Just rawHeaders -> + case Decode.decodeValue (Decode.field "cookie" Decode.string) rawHeaders of + Ok v -> + [ div [ class "kv-row" ] + [ span [ class "kv-key", style "color" "var(--orange)" ] [ text "cookie" ] + , span [ class "kv-val kv-ok" ] [ text v ] + ] + ] + + Err (Decode.Field "cookie" _) -> + [ div [ class "kv-val kv-err", style "font-size" "11px" ] + [ text "cookie header present but could not be decoded" ] + ] + + Err _ -> + [] + + setCookieRows = + case net.responseHeaders of + Nothing -> + [] + + Just rawHeaders -> + case Decode.decodeValue + (Decode.field "set-cookie" + (Decode.oneOf + [ Decode.map List.singleton Decode.string + , Decode.list Decode.string + ] + ) + ) + rawHeaders of + Ok values -> + List.map + (\v -> + div [ class "kv-row" ] + [ span [ class "kv-key", style "color" "var(--orange)" ] [ text "set-cookie" ] + , span [ class "kv-val kv-ok" ] [ text v ] + ] + ) + values + + Err (Decode.Field "set-cookie" innerErr) -> + [ div [ class "kv-val kv-err", style "font-size" "11px" ] + [ text ("set-cookie format unexpected: " ++ Decode.errorToString innerErr) ] + ] + + Err _ -> + [] + + rows = + cookieRows ++ setCookieRows + in + if List.isEmpty rows then + [ div [ class "insp-empty" ] [ text "No cookies found in headers." ] ] + + else + rows + + +viewCollectors : NodeData -> List (Html Msg) +viewCollectors node = + case node.collectors of + Nothing -> + [ div [ class "insp-empty" ] [ text "No collectors on this node." ] ] + + Just [] -> + [ div [ class "insp-empty" ] [ text "No collectors on this node." ] ] + + Just cs -> + div [ class "coll-copy-all-row" ] + [ button + [ class "fv-copy-btn coll-copy-all" + , onClick (CopyToClipboard (Encode.encode 4 (Encode.list identity cs))) + ] + [ text "Copy all" ] + ] + :: List.indexedMap + (\i c -> + div [ class "coll-card" ] + [ div [ class "coll-card-header" ] + [ span [] [ text ("Collector " ++ String.fromInt (i + 1)) ] + , button + [ class "fv-copy-btn" + , onClick (CopyToClipboard (Encode.encode 4 c)) + ] + [ text "\u{2398}" ] + ] + , JsonTree.view ("Collector " ++ String.fromInt (i + 1)) c + ] + ) + cs + + +viewCausedBy : AuthEvent -> List (Html Msg) +viewCausedBy event = + case event.causedBy of + Nothing -> + [] + + Just sdkEventId -> + [ div [ class "kv-row", style "margin-bottom" "8px" ] + [ span [ class "kv-key" ] [ text "Triggered by" ] + , button + [ onClick (SelectEvent sdkEventId) + , class "cause-btn" + ] + [ text ("SDK Node " ++ String.left 8 sdkEventId) ] + ] + ] + + +viewSession : SessionData -> List (Html Msg) +viewSession sess = + let + keyLabel = Maybe.withDefault "unknown" sess.key + beforeValue = Maybe.withDefault "—" sess.before + afterValue = Maybe.withDefault "—" sess.after + in + [ div [ class "sect-hdr" ] [ text "Session Diff" ] + , viewRow "Key" (span [ class "kv-val" ] [ text keyLabel ]) + , viewRow "Before" (span [ class "kv-val kv-err" ] [ text beforeValue ]) + , viewRow "After" (span [ class "kv-val kv-ok" ] [ text afterValue ]) + ] + + +viewConfig : Maybe Decode.Value -> List (Html Msg) +viewConfig maybeCfg = + case maybeCfg of + Nothing -> + [ div [ class "insp-empty" ] [ text "No config data on this event." ] ] + + Just cfg -> + [ JsonTree.view "SDK Config" cfg ] + + +viewEventIssue : EventIssue -> Html Msg +viewEventIssue issue = + let + severityClass = + case issue.severity of + "error" -> "diag-issue diag-issue-error" + "warning" -> "diag-issue diag-issue-warning" + _ -> "diag-issue diag-issue-info" + + severityIcon = + case issue.severity of + "error" -> "✕ " + "warning" -> "⚠ " + _ -> "ℹ " + + stepItems = + List.indexedMap + (\i step -> + div [ class "diag-step" ] + [ text (String.fromInt (i + 1) ++ ". " ++ step) ] + ) + issue.steps + + dataRows = + case issue.relevantData of + Nothing -> + [] + + Just pairs -> + [ div [ class "diag-data" ] + (List.map + (\( k, v ) -> + div [ class "diag-kv" ] + [ span [ class "diag-k" ] [ text k ] + , span [ class "diag-v" ] [ text v ] + ] + ) + pairs + ) + ] + in + div [ class severityClass ] + ([ div [ class "diag-title" ] [ text (severityIcon ++ issue.title) ] + , div [ class "diag-desc" ] [ text issue.description ] + ] + ++ (if List.isEmpty issue.steps then + [] + + else + [ div [ class "diag-steps-hdr" ] [ text "What to check:" ] + , div [] stepItems + ] + ) + ++ dataRows + ) diff --git a/packages/devtools-extension/src/panel/src/JsonTree.elm b/packages/devtools-extension/src/panel/src/JsonTree.elm new file mode 100644 index 0000000000..6195dd7c99 --- /dev/null +++ b/packages/devtools-extension/src/panel/src/JsonTree.elm @@ -0,0 +1,154 @@ +module JsonTree exposing (view) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Json.Decode as Decode + + +type JsonVal + = JString String + | JNumber Float + | JBool Bool + | JNull + | JArray (List JsonVal) + | JObject (List ( String, JsonVal )) + + +decodeJsonVal : Decode.Decoder JsonVal +decodeJsonVal = + Decode.oneOf + [ Decode.map JString Decode.string + , Decode.map JNumber Decode.float + , Decode.map JBool Decode.bool + , Decode.null JNull + , Decode.map JArray (Decode.list (Decode.lazy (\_ -> decodeJsonVal))) + , Decode.map JObject (Decode.keyValuePairs (Decode.lazy (\_ -> decodeJsonVal))) + ] + + +authKeys : List String +authKeys = + [ "authorization" + , "set-cookie" + , "cookie" + , "access-control-allow-origin" + , "access-control-allow-credentials" + , "www-authenticate" + ] + + +view : String -> Decode.Value -> Html msg +view label rawValue = + div [ class "jt-sec" ] + [ div [ class "jt-label" ] [ text label ] + , case Decode.decodeValue decodeJsonVal rawValue of + Ok jsonVal -> + div [ class "jt-tree" ] [ viewVal 0 Nothing jsonVal ] + + Err err -> + div [] + [ div [ class "jt-err" ] [ text "⚠ Could not decode value" ] + , div [ class "jt-errmsg" ] [ text (Decode.errorToString err) ] + ] + ] + + +isBase64Url : String -> Bool +isBase64Url s = + String.length s > 0 + && String.all (\c -> Char.isAlphaNum c || c == '-' || c == '_' || c == '=') s + + +isJwt : String -> Bool +isJwt s = + let + parts = + String.split "." s + in + List.length parts == 3 && List.all isBase64Url parts + + +viewJwt : String -> Html msg +viewJwt jwt = + Html.node "details" + [ class "jwt-details" ] + [ Html.node "summary" + [ class "jwt-summary" ] + [ text "JWT" ] + , span + [ class "jwt-pending" + , attribute "data-jwt" jwt + ] + [] + ] + + +viewVal : Int -> Maybe String -> JsonVal -> Html msg +viewVal depth maybeKey val = + case val of + JString s -> + if isJwt s then + viewJwt s + + else + span [ class "jt-str" ] [ text ("\"" ++ s ++ "\"") ] + + JNumber n -> + span [ class "jt-num" ] + [ text + (if n == toFloat (round n) then + String.fromInt (round n) + + else + String.fromFloat n + ) + ] + + JBool b -> + span [ class "jt-bool" ] + [ text (if b then "true" else "false") ] + + JNull -> + span [ class "jt-null" ] [ text "null" ] + + JArray [] -> + span [ class "jt-punct" ] [ text "[]" ] + + JArray items -> + div [ style "padding-left" (indentPx depth) ] + (List.indexedMap + (\i item -> + div [ style "margin" "1px 0" ] + [ span [ class "jt-punct" ] [ text (String.fromInt i ++ ": ") ] + , viewVal (depth + 1) Nothing item + ] + ) + items + ) + + JObject [] -> + span [ class "jt-punct" ] [ text "{}" ] + + JObject pairs -> + div [ style "padding-left" (indentPx depth) ] + (List.map + (\( k, v ) -> + let + isAuth = + List.member (String.toLower k) authKeys + + keyClass = + if isAuth then "jt-auth" else "jt-key" + in + div [ style "margin" "1px 0" ] + [ span [ class keyClass ] [ text (k ++ ": ") ] + , viewVal (depth + 1) (Just k) v + ] + ) + pairs + ) + + +indentPx : Int -> String +indentPx depth = + String.fromInt (depth * 14) ++ "px" diff --git a/packages/devtools-extension/src/panel/src/Model.elm b/packages/devtools-extension/src/panel/src/Model.elm new file mode 100644 index 0000000000..6ba3df5ecc --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Model.elm @@ -0,0 +1,58 @@ +module Model exposing (Model, init) + +import Dict exposing (Dict) +import Set exposing (Set) +import Types exposing (AuthEvent, DiagnosisResult, ImportMeta, InspectorTab(..), SnapshotMeta, ViewMode(..)) + + +type alias Model = + { events : List AuthEvent + , eventsById : Dict String AuthEvent + , selectedEventId : Maybe String + , activeTab : InspectorTab + , recording : Bool + , flowId : Maybe String + , lastDecodeError : Maybe String + , diagnosis : Maybe DiagnosisResult + , summaryCollapsed : Bool + , viewMode : ViewMode + , playbackIndex : Maybe Int + , isPlaying : Bool + , selectedNodeId : Maybe String + , expandedSubRows : Set String + , exportMenuOpen : Bool + , importedFlow : Maybe ImportMeta + , importPasteOpen : Bool + , importPasteText : String + , hoveredNodeId : Maybe String + , snapshotMenuOpen : Bool + , snapshots : List SnapshotMeta + } + + +init : () -> ( Model, Cmd msg ) +init _ = + ( { events = [] + , eventsById = Dict.empty + , selectedEventId = Nothing + , activeTab = HeadersTab + , recording = True + , flowId = Nothing + , lastDecodeError = Nothing + , diagnosis = Nothing + , summaryCollapsed = False + , viewMode = TimelineMode + , playbackIndex = Nothing + , isPlaying = False + , selectedNodeId = Nothing + , expandedSubRows = Set.empty + , exportMenuOpen = False + , importedFlow = Nothing + , importPasteOpen = False + , importPasteText = "" + , hoveredNodeId = Nothing + , snapshotMenuOpen = False + , snapshots = [] + } + , Cmd.none + ) diff --git a/packages/devtools-extension/src/panel/src/Timeline.elm b/packages/devtools-extension/src/panel/src/Timeline.elm new file mode 100644 index 0000000000..5fd844a2bc --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Timeline.elm @@ -0,0 +1,162 @@ +module Timeline exposing (view) + +import Helpers +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick) +import Types exposing (AuthEvent, EventData(..)) +import Update exposing (Msg(..)) + + +nodeStatusClass : String -> String +nodeStatusClass status = + case status of + "continue" -> "kv-cont" + "success" -> "kv-ok" + "error" -> "kv-err" + "failure" -> "kv-err" + _ -> "st-nil" + + +view : List AuthEvent -> Maybe String -> Html Msg +view events selectedId = + div [] + (List.map (renderRow selectedId) events) + + +renderRow : Maybe String -> AuthEvent -> Html Msg +renderRow selectedId event = + let + isSelected = + selectedId == Just event.id + + rowClass = + if isSelected then "tl-row sel" else "tl-row" + in + case Helpers.eventType event of + Helpers.NodeChange -> + renderSdkRow rowClass event + + Helpers.SdkConfig -> + renderConfigRow rowClass event + + _ -> + case Helpers.eventSource event of + Helpers.SessionSource -> + renderSessionRow rowClass event + + _ -> + renderNetworkRow rowClass event + + +renderConfigRow : String -> AuthEvent -> Html Msg +renderConfigRow rowClass event = + div [ class rowClass, onClick (SelectEvent event.id) ] + [ span [ class "tl-badge b-cfg" ] [ text "CFG" ] + , span [ class "tl-st st-nil" ] [ text "" ] + , span [ class "tl-meth" ] [ text "" ] + , span [ class "tl-desc" ] [ text "SDK Config" ] + ] + + +renderSessionRow : String -> AuthEvent -> Html Msg +renderSessionRow rowClass event = + let + label = + case event.data of + Session sess -> + Maybe.withDefault "session" sess.key + + _ -> + "session" + in + div [ class rowClass, onClick (SelectEvent event.id) ] + [ span [ class "tl-badge b-ses" ] [ text "SES" ] + , span [ class "tl-st st-nil" ] [ text "" ] + , span [ class "tl-meth" ] [ text "" ] + , span [ class "tl-desc" ] [ text label ] + ] + + +renderSdkRow : String -> AuthEvent -> Html Msg +renderSdkRow rowClass event = + case event.data of + DaVinciNode node -> + let + status = + Maybe.withDefault "unknown" node.nodeStatus + + transitionLabel = + case node.previousStatus of + Just prev -> + prev ++ " → " ++ status + + Nothing -> + status + + collectorTag = + case node.collectors of + Just cs -> + if List.length cs > 0 then + span [ class "tl-tag tag-coll" ] + [ text (String.fromInt (List.length cs) ++ " collectors") ] + + else + text "" + + Nothing -> + text "" + in + div [ class rowClass, onClick (SelectEvent event.id) ] + [ span [ class "tl-badge b-sdk" ] [ text "SDK" ] + , span [ class ("tl-st " ++ nodeStatusClass status) ] [ text "" ] + , span [ class "tl-meth" ] [ text "" ] + , span [ class "tl-desc" ] [ text transitionLabel ] + , collectorTag + ] + + _ -> + text "" + + +renderNetworkRow : String -> AuthEvent -> Html Msg +renderNetworkRow rowClass event = + case event.data of + Network net -> + let + statusText = + case net.status of + Nothing -> "—" + Just s -> String.fromInt s + + durationText = + case net.duration of + Nothing -> "" + Just ms -> + if ms < 1 then + "<1" + else + String.fromInt (round ms) + + corsTag = + if event.isCors then + span [ class "tl-tag tag-cors" ] [ text "CORS" ] + + else + text "" + + urlText = + Maybe.withDefault "—" net.url + in + div [ class rowClass, onClick (SelectEvent event.id) ] + [ span [ class "tl-badge b-net" ] [ text "NET" ] + , span [ class ("tl-st " ++ Helpers.statusClass net.status) ] [ text statusText ] + , span [ class ("tl-meth " ++ Helpers.methodClass net.method) ] + [ text (Maybe.withDefault "" net.method) ] + , span [ class "tl-desc" ] [ text urlText ] + , corsTag + , span [ class "tl-dur" ] [ text durationText ] + ] + + _ -> + text "" diff --git a/packages/devtools-extension/src/panel/src/Types.elm b/packages/devtools-extension/src/panel/src/Types.elm new file mode 100644 index 0000000000..fea7e0281f --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Types.elm @@ -0,0 +1,185 @@ +module Types exposing + ( AuthEvent + , DiagnosisResult + , EventData(..) + , EventIssue + , FlowHealth(..) + , FlowIssue + , ImportMeta + , InspectorTab(..) + , JourneyData + , NetworkData + , NodeData + , OidcData + , SdkAuthorization + , SdkError + , SessionData + , SnapshotMeta + , ViewMode(..) + ) + +import Json.Decode as Decode + + +type alias SdkError = + { code : String + , message : String + , errorType : String + , internalHttpStatus : Maybe Int + } + + +type alias SdkAuthorization = + { code : Maybe String + , state : Maybe String + } + + +type alias AuthEvent = + { id : String + , timestamp : Float + , eventType : String + , source : String + , flowId : Maybe String + , isCors : Bool + , isError : Bool + , isAuthRelated : Bool + , causedBy : Maybe String + , data : EventData + } + + +type EventData + = Network NetworkData + | DaVinciNode NodeData + | Journey JourneyData + | Oidc OidcData + | Session SessionData + | Config (Maybe Decode.Value) + + +type alias NetworkData = + { status : Maybe Int + , url : Maybe String + , method : Maybe String + , duration : Maybe Float + , requestHeaders : Maybe Decode.Value + , responseHeaders : Maybe Decode.Value + , requestBody : Maybe Decode.Value + , responseBody : Maybe Decode.Value + } + + +type alias NodeData = + { nodeStatus : Maybe String + , previousStatus : Maybe String + , interactionId : Maybe String + , interactionToken : Maybe String + , nodeId : Maybe String + , requestId : Maybe String + , nodeName : Maybe String + , nodeDescription : Maybe String + , eventName : Maybe String + , httpStatus : Maybe Int + , sdkError : Maybe SdkError + , authorization : Maybe SdkAuthorization + , session : Maybe String + , collectors : Maybe (List Decode.Value) + , responseBody : Maybe Decode.Value + } + + +type alias JourneyData = + { stepType : Maybe String + , stage : Maybe String + , header : Maybe String + , description : Maybe String + , callbacks : Maybe (List Decode.Value) + , authId : Maybe String + , tokenId : Maybe String + , successUrl : Maybe String + , errorCode : Maybe Int + , errorMessage : Maybe String + , errorReason : Maybe String + } + + +type alias OidcData = + { phase : Maybe String + , status : Maybe String + , clientId : Maybe String + , errorCode : Maybe String + , errorMessage : Maybe String + } + + +type alias SessionData = + { key : Maybe String + , before : Maybe String + , after : Maybe String + } + + +type InspectorTab + = DiagnosisTab + | HeadersTab + | CookiesTab + | CorsTab + | SdkStateTab + | CollectorsTab + | SessionTab + | ConfigTab + + +type FlowHealth + = Healthy + | Warning + | Error + + +type alias EventIssue = + { severity : String + , title : String + , description : String + , steps : List String + , relevantData : Maybe (List ( String, String )) + } + + +type alias FlowIssue = + { id : String + , severity : String + , category : String + , title : String + , description : String + , steps : List String + , relatedEventIds : List String + , relevantData : Maybe (List ( String, String )) + } + + +type alias DiagnosisResult = + { flowHealth : FlowHealth + , issues : List FlowIssue + , annotatedEvents : List ( String, List EventIssue ) + } + + +type ViewMode + = TimelineMode + | FlowMode + + +type alias ImportMeta = + { flowId : Maybe String + , capturedAt : String + , redacted : Bool + } + + +type alias SnapshotMeta = + { id : String + , savedAt : String + , flowId : Maybe String + , eventCount : Int + } diff --git a/packages/devtools-extension/src/panel/src/Update.elm b/packages/devtools-extension/src/panel/src/Update.elm new file mode 100644 index 0000000000..f1a4fecd2e --- /dev/null +++ b/packages/devtools-extension/src/panel/src/Update.elm @@ -0,0 +1,315 @@ +module Update exposing (Msg(..), update) + +import Dict +import Helpers +import Model exposing (Model) +import Set +import Types exposing (AuthEvent, DiagnosisResult, FlowHealth(..), ImportMeta, InspectorTab(..), SnapshotMeta, ViewMode(..)) + + +type Msg + = EventReceived AuthEvent + | SelectEvent String + | SelectNode String + | SwitchTab InspectorTab + | ToggleRecording + | ClearFlow + | ToggleExportMenu + | CloseExportMenu + | ExportJson + | ExportMarkdown + | ImportFlow + | UpdateImportPaste String + | SubmitImportPaste + | CancelImportPaste + | ImportMetaReceived ImportMeta + | ImportError String + | DecodeError String + | DiagnosisReceived DiagnosisResult + | ToggleSummary + | SaveSnapshot + | SwitchViewMode ViewMode + | StartPlayback + | StopPlayback + | PlaybackTick + | SelectFlowNode String + | ToggleSubRow String + | ResetPlayback + | HoverNode (Maybe String) + | CopyToClipboard String + | ToggleSnapshotMenu + | CloseSnapshotMenu + | SnapshotsReceived (List SnapshotMeta) + | LoadSnapshot String + | DeleteSnapshot String + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + EventReceived event -> + if model.importedFlow /= Nothing then + ( model, Cmd.none ) + + else + ( { model + | events = model.events ++ [ event ] + , eventsById = Dict.insert event.id event model.eventsById + , flowId = + case model.flowId of + Just _ -> + model.flowId + + Nothing -> + event.flowId + } + , Cmd.none + ) + + SelectEvent id -> + let + selectedEvent = + Helpers.findEvent id model.eventsById + + newTab = + case ( model.activeTab, selectedEvent ) of + ( CollectorsTab, Just e ) -> + if Helpers.eventType e /= Helpers.NodeChange then + HeadersTab + else + CollectorsTab + + ( SessionTab, Just e ) -> + if Helpers.eventSource e /= Helpers.SessionSource then + HeadersTab + else + SessionTab + + ( ConfigTab, Just e ) -> + if Helpers.eventType e /= Helpers.SdkConfig then + HeadersTab + else + ConfigTab + + _ -> + model.activeTab + in + ( { model | selectedEventId = Just id, activeTab = newTab }, Cmd.none ) + + SelectNode id -> + ( { model | selectedEventId = Just id, activeTab = SdkStateTab }, Cmd.none ) + + SwitchTab tab -> + ( { model | activeTab = tab }, Cmd.none ) + + ToggleRecording -> + ( { model | recording = not model.recording }, Cmd.none ) + + ClearFlow -> + ( { model + | events = [] + , eventsById = Dict.empty + , selectedEventId = Nothing + , flowId = Nothing + , selectedNodeId = Nothing + , expandedSubRows = Set.empty + , isPlaying = False + , playbackIndex = Nothing + , importedFlow = Nothing + , exportMenuOpen = False + , recording = True + , importPasteOpen = False + , importPasteText = "" + } + , Cmd.none + ) + + ToggleExportMenu -> + ( { model | exportMenuOpen = not model.exportMenuOpen }, Cmd.none ) + + CloseExportMenu -> + ( { model | exportMenuOpen = False }, Cmd.none ) + + ExportJson -> + ( { model | exportMenuOpen = False }, Cmd.none ) + + ExportMarkdown -> + ( { model | exportMenuOpen = False }, Cmd.none ) + + ImportFlow -> + ( { model | importPasteOpen = True, importPasteText = "", lastDecodeError = Nothing }, Cmd.none ) + + UpdateImportPaste text -> + ( { model | importPasteText = text }, Cmd.none ) + + SubmitImportPaste -> + ( { model | importPasteOpen = False, importPasteText = "" }, Cmd.none ) + + CancelImportPaste -> + ( { model | importPasteOpen = False, importPasteText = "" }, Cmd.none ) + + ImportMetaReceived meta -> + ( { model | importedFlow = Just meta, recording = False }, Cmd.none ) + + ImportError errMsg -> + ( { model | lastDecodeError = Just errMsg }, Cmd.none ) + + DecodeError errMsg -> + ( { model | lastDecodeError = Just errMsg }, Cmd.none ) + + DiagnosisReceived result -> + let + shouldExpand = + model.recording + && (result.flowHealth == Error) + && model.summaryCollapsed + in + ( { model + | diagnosis = Just result + , summaryCollapsed = + if shouldExpand then + False + + else + model.summaryCollapsed + } + , Cmd.none + ) + + ToggleSummary -> + ( { model | summaryCollapsed = not model.summaryCollapsed }, Cmd.none ) + + SaveSnapshot -> + ( model, Cmd.none ) + + SwitchViewMode mode -> + ( { model + | viewMode = mode + , isPlaying = False + , playbackIndex = Nothing + , selectedNodeId = Nothing + } + , Cmd.none + ) + + StartPlayback -> + let + sdkNodes = + Helpers.sdkNodes model.events + + startIndex = + case model.playbackIndex of + Just n -> + if n >= List.length sdkNodes - 1 then + 0 + else + n + + Nothing -> + 0 + + firstId = + sdkNodes + |> List.drop startIndex + |> List.head + |> Maybe.map .id + in + ( { model + | isPlaying = True + , playbackIndex = Just startIndex + , selectedNodeId = firstId + , expandedSubRows = Set.empty + } + , Cmd.none + ) + + StopPlayback -> + ( { model | isPlaying = False }, Cmd.none ) + + PlaybackTick -> + if not model.isPlaying then + ( model, Cmd.none ) + + else + let + sdkNodes = + Helpers.sdkNodes model.events + + total = + List.length sdkNodes + + nextIndex = + Maybe.map (\n -> n + 1) model.playbackIndex + |> Maybe.withDefault 0 + + isFinished = + nextIndex >= total + + nextId = + List.head (List.drop nextIndex sdkNodes) + |> Maybe.map .id + in + if isFinished then + ( { model | isPlaying = False, playbackIndex = Nothing }, Cmd.none ) + + else + ( { model + | playbackIndex = Just nextIndex + , selectedNodeId = nextId + , expandedSubRows = Set.empty + } + , Cmd.none + ) + + SelectFlowNode id -> + ( { model + | selectedNodeId = Just id + , expandedSubRows = Set.empty + } + , Cmd.none + ) + + ToggleSubRow key -> + ( { model + | expandedSubRows = + if Set.member key model.expandedSubRows then + Set.remove key model.expandedSubRows + else + Set.insert key model.expandedSubRows + } + , Cmd.none + ) + + ResetPlayback -> + ( { model + | playbackIndex = Nothing + , isPlaying = False + , selectedNodeId = Nothing + } + , Cmd.none + ) + + HoverNode maybeId -> + ( { model | hoveredNodeId = maybeId }, Cmd.none ) + + CopyToClipboard _ -> + ( model, Cmd.none ) + + ToggleSnapshotMenu -> + ( { model | snapshotMenuOpen = not model.snapshotMenuOpen }, Cmd.none ) + + CloseSnapshotMenu -> + ( { model | snapshotMenuOpen = False }, Cmd.none ) + + SnapshotsReceived list -> + ( { model | snapshots = list }, Cmd.none ) + + LoadSnapshot _ -> + ( { model | snapshotMenuOpen = False }, Cmd.none ) + + DeleteSnapshot id -> + ( { model + | snapshots = List.filter (\s -> s.id /= id) model.snapshots + } + , Cmd.none + ) diff --git a/packages/devtools-extension/src/panel/src/View.elm b/packages/devtools-extension/src/panel/src/View.elm new file mode 100644 index 0000000000..869f9b78ab --- /dev/null +++ b/packages/devtools-extension/src/panel/src/View.elm @@ -0,0 +1,332 @@ +module View exposing (view) + +import FlowView +import Graph +import Helpers +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (onClick, onInput) +import Inspector +import Model exposing (Model) +import Timeline +import Types exposing (AuthEvent, FlowHealth(..), FlowIssue, ImportMeta, SnapshotMeta, ViewMode(..)) +import Update exposing (Msg(..)) + + +view : Model -> Html Msg +view model = + let + selectedEvent = + model.selectedEventId + |> Maybe.andThen (\id -> Helpers.findEvent id model.eventsById) + + eventCount = + List.length model.events + in + div [ class "layout" ] + [ viewToolbar model eventCount + , case model.lastDecodeError of + Just errMsg -> + div [ class "err-banner" ] [ text ("Bridge decode error: " ++ errMsg) ] + + Nothing -> + text "" + , viewImportBanner model + , viewImportPaste model + , viewFlowHealthPanel model + , case model.viewMode of + FlowMode -> + FlowView.view + model.events + model.playbackIndex + model.selectedNodeId + model.expandedSubRows + + TimelineMode -> + div [ class "timeline-layout" ] + [ div [ class "main-area" ] + [ div [ class "graph-panel" ] + [ div [ class "graph-panel-label" ] [ text "Flow" ] + , Graph.view model.events model.selectedEventId model.hoveredNodeId + ] + , div [ class "timeline-panel" ] + [ viewTimelineHeader + , Timeline.view model.events model.selectedEventId + ] + ] + , div [ class "inspector-panel" ] + [ Inspector.view selectedEvent model.activeTab model.diagnosis ] + ] + ] + + +viewExportDropdown : Model -> Html Msg +viewExportDropdown model = + div [ class "tb-dropdown" ] + [ button [ onClick ToggleExportMenu, class "tb-btn" ] [ text "Export ▾" ] + , if model.exportMenuOpen then + div [ class "tb-dropdown-menu" ] + [ button [ onClick ExportJson, class "tb-dropdown-item" ] [ text "Export JSON" ] + , button [ onClick ExportMarkdown, class "tb-dropdown-item" ] [ text "Export Markdown" ] + ] + + else + text "" + ] + + +viewSnapshotDropdown : Model -> Html Msg +viewSnapshotDropdown model = + div [ class "tb-dropdown" ] + [ button [ onClick SaveSnapshot, class "tb-btn" ] [ text "Snapshot" ] + , button [ onClick ToggleSnapshotMenu, class "tb-btn tb-dropdown-arrow" ] [ text "▾" ] + , if model.snapshotMenuOpen then + div [ class "tb-dropdown-menu snapshot-menu" ] + (if List.isEmpty model.snapshots then + [ div [ class "snapshot-empty" ] [ text "No saved snapshots" ] ] + + else + List.map viewSnapshotItem model.snapshots + ) + + else + text "" + ] + + +viewSnapshotItem : SnapshotMeta -> Html Msg +viewSnapshotItem snap = + div [ class "snapshot-item" ] + [ div [ class "snapshot-item-info", onClick (LoadSnapshot snap.id) ] + [ span [ class "snapshot-flow" ] + [ text + (case snap.flowId of + Just fid -> + "flow " ++ Helpers.truncateId fid + + Nothing -> + "flow (none)" + ) + ] + , span [ class "snapshot-meta" ] + [ text + (" · " ++ String.left 16 snap.savedAt ++ " · " ++ String.fromInt snap.eventCount ++ " events") + ] + ] + , button + [ onClick (DeleteSnapshot snap.id) + , class "snapshot-delete" + ] + [ text "✕" ] + ] + + +viewImportBanner : Model -> Html Msg +viewImportBanner model = + case model.importedFlow of + Nothing -> + text "" + + Just meta -> + div [ class "import-banner" ] + [ span [] + [ text + ("Imported flow " + ++ (case meta.flowId of + Just id -> + Helpers.truncateId id + + Nothing -> + "(unknown)" + ) + ++ " · captured " + ++ String.left 16 meta.capturedAt + ++ (if meta.redacted then + " · redacted" + + else + "" + ) + ) + ] + , button [ onClick ClearFlow, class "import-banner-clear" ] [ text "Clear" ] + ] + + +viewImportPaste : Model -> Html Msg +viewImportPaste model = + if not model.importPasteOpen then + text "" + + else + div [ class "import-paste" ] + [ div [ class "import-paste-header" ] + [ span [] [ text "Paste exported flow JSON below" ] + , div [ class "import-paste-actions" ] + [ button + [ onClick SubmitImportPaste + , class "tb-btn" + , disabled (String.isEmpty (String.trim model.importPasteText)) + ] + [ text "Import" ] + , button [ onClick CancelImportPaste, class "tb-btn" ] [ text "Cancel" ] + ] + ] + , textarea + [ class "import-paste-textarea" + , placeholder "Paste exported JSON here (Ctrl/Cmd+V)" + , value model.importPasteText + , onInput UpdateImportPaste + ] + [] + ] + + +viewFlowHealthPanel : Model -> Html Msg +viewFlowHealthPanel model = + case model.diagnosis of + Nothing -> + text "" + + Just diagnosis -> + case diagnosis.flowHealth of + Healthy -> + text "" + + _ -> + let + ( healthClass, healthLabel ) = + case diagnosis.flowHealth of + Error -> ( "fh-panel fh-error", "● ERROR" ) + Warning -> ( "fh-panel fh-warning", "● WARNING" ) + Healthy -> ( "fh-panel", "" ) + + issueCount = + List.length diagnosis.issues + + flowLabel = + case model.flowId of + Just id -> "Flow: " ++ Helpers.truncateId id + Nothing -> "" + + summary = + healthLabel + ++ (if String.isEmpty flowLabel then "" else " " ++ flowLabel) + ++ " " + ++ String.fromInt issueCount + ++ (if issueCount == 1 then " issue found" else " issues found") + in + div [ class healthClass ] + [ div [ class "fh-header" ] + [ span [ class "fh-title" ] [ text "Flow Health" ] + , span [ class "fh-summary" ] [ text summary ] + , button [ onClick ToggleSummary, class "fh-collapse-btn" ] + [ text + (if model.summaryCollapsed then "▶" else "▼") + ] + ] + , if model.summaryCollapsed then + text "" + + else + div [ class "fh-issues" ] + (List.map viewFlowIssue diagnosis.issues) + ] + + +viewFlowIssue : FlowIssue -> Html Msg +viewFlowIssue issue = + let + ( issueClass, icon ) = + case issue.severity of + "error" -> ( "fh-issue fh-issue-error", "✕ " ) + "warning" -> ( "fh-issue fh-issue-warning", "⚠ " ) + _ -> ( "fh-issue fh-issue-info", "ℹ " ) + + -- Clicking the issue jumps to the first related event + firstEventId = + List.head issue.relatedEventIds + in + div + (class issueClass + :: (case firstEventId of + Just eid -> [ onClick (SelectEvent eid) ] + Nothing -> [] + ) + ) + [ span [ class "fh-issue-cat" ] [ text (String.toUpper issue.category) ] + , span [ class "fh-issue-title" ] [ text (" — " ++ icon ++ issue.title) ] + , div [ class "fh-issue-desc" ] [ text issue.description ] + ] + + +viewTimelineHeader : Html Msg +viewTimelineHeader = + div [ class "tl-header" ] + [ span [ class "tl-badge tl-hdr-label" ] [ text "Type" ] + , span [ class "tl-st tl-hdr-label" ] [ text "Status" ] + , span [ class "tl-meth tl-hdr-label" ] [ text "Method" ] + , span [ class "tl-desc tl-hdr-label" ] [ text "URL / Description" ] + , span [ class "tl-dur tl-hdr-label" ] [ text "Time" ] + ] + + +viewToolbar : Model -> Int -> Html Msg +viewToolbar model eventCount = + div [ class "toolbar" ] + [ if model.recording then + button [ onClick ToggleRecording, class "tb-btn recording" ] + [ span [ class "rec-dot" ] [] + , text "Recording" + ] + + else + button [ onClick ToggleRecording, class "tb-btn" ] + [ text "Record" ] + , div [ class "tb-sep" ] [] + , button [ onClick ClearFlow, class "tb-btn" ] [ text "Clear" ] + , viewExportDropdown model + , button [ onClick ImportFlow, class "tb-btn" ] [ text "Import" ] + , viewSnapshotDropdown model + , div [ class "tb-sep" ] [] + , button + [ onClick (SwitchViewMode TimelineMode) + , class + (if model.viewMode == TimelineMode then + "tb-btn tb-mode-btn active" + + else + "tb-btn tb-mode-btn" + ) + ] + [ text "Timeline" ] + , button + [ onClick (SwitchViewMode FlowMode) + , class + (if model.viewMode == FlowMode then + "tb-btn tb-mode-btn active" + + else + "tb-btn tb-mode-btn" + ) + ] + [ text "Flow" ] + , div [ class "tb-spacer" ] [] + , if model.viewMode == FlowMode then + FlowView.viewPlaybackControls model.events model.playbackIndex model.isPlaying + + else if eventCount > 0 then + span [ class "event-count" ] [ text (String.fromInt eventCount ++ " events") ] + + else + text "" + , case model.flowId of + Just id -> + span [ class "flow-chip" ] + [ text "flow " + , span [ class "flow-chip-id" ] [ text (Helpers.truncateId id) ] + ] + + Nothing -> + span [ class "no-flow" ] [ text "No active flow" ] + ] diff --git a/packages/devtools-extension/tsconfig.json b/packages/devtools-extension/tsconfig.json new file mode 100644 index 0000000000..329ef5038f --- /dev/null +++ b/packages/devtools-extension/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.spec.json" } + ], + "nx": { "addTypecheckTarget": false } +} diff --git a/packages/devtools-extension/tsconfig.lib.json b/packages/devtools-extension/tsconfig.lib.json new file mode 100644 index 0000000000..849b5ce40b --- /dev/null +++ b/packages/devtools-extension/tsconfig.lib.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "nodenext", + "module": "NodeNext", + "target": "ES2022", + "outDir": "./dist", + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "strict": true, + "noImplicitOverride": true, + "skipLibCheck": true, + "sourceMap": true, + "lib": ["es2022", "dom", "dom.iterable"], + "types": ["chrome"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], + "references": [{ "path": "../devtools-types/tsconfig.lib.json" }] +} diff --git a/packages/devtools-extension/tsconfig.spec.json b/packages/devtools-extension/tsconfig.spec.json new file mode 100644 index 0000000000..4f89b32da3 --- /dev/null +++ b/packages/devtools-extension/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "NodeNext", + "moduleResolution": "nodenext", + "strict": true, + "noImplicitOverride": true, + "lib": ["es2022", "dom", "dom.iterable"] + }, + "include": ["vite.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"], + "references": [{ "path": "./tsconfig.lib.json" }] +} diff --git a/packages/devtools-extension/vite.config.ts b/packages/devtools-extension/vite.config.ts new file mode 100644 index 0000000000..c9b53d8990 --- /dev/null +++ b/packages/devtools-extension/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/devtools-extension', + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,ts}'], + reporters: ['default'], + }, +})); diff --git a/packages/devtools-types/README.md b/packages/devtools-types/README.md new file mode 100644 index 0000000000..ec583f661a --- /dev/null +++ b/packages/devtools-types/README.md @@ -0,0 +1,237 @@ +# @forgerock/devtools-types + +Shared [Effect Schema](https://effect.website/docs/schema/introduction/) definitions and TypeScript types for Ping DevTools. This package is the single source of truth for the shape of every event that flows between the SDK bridges and the DevTools extension. + +## Contents + +- [Installation](#installation) +- [AuthEvent](#authevent) +- [Data variants](#data-variants) + - [NetworkData](#networkdata) + - [SdkData — DaVinci](#sdkdata--davinci) + - [JourneyData — AM trees](#journeydata--am-trees) + - [OidcData — OIDC/OAuth](#oidcdata--oidcoauth) + - [SessionData](#sessiondata) + - [SdkConfigData](#sdkconfigdata) + - [DomData](#domdata) +- [Runtime validation](#runtime-validation) +- [Exported symbols](#exported-symbols) + +--- + +## Installation + +```bash +pnpm add @forgerock/devtools-types +``` + +`effect` is a peer dependency — add it to your project if it isn't already present. + +--- + +## AuthEvent + +Every event, regardless of source, conforms to the `AuthEvent` envelope: + +```ts +import type { AuthEvent } from '@forgerock/devtools-types'; + +// Shape +{ + id: string; // crypto.randomUUID() + timestamp: number; // performance.now() + type: AuthEventType; // e.g. 'sdk:node-change', 'network:response' + source: 'network' | 'sdk' | 'dom' | 'session'; + flowId: string | null; + causedBy: string | null; + data: NetworkData | SdkData | JourneyData | OidcData | SessionData | SdkConfigData | DomData; + flags: { + isCors: boolean; + isError: boolean; + isAuthRelated: boolean; + } +} +``` + +### Event types + +| `type` | `source` | Description | +| ------------------- | --------- | ------------------------------------- | +| `network:request` | `network` | Outbound HTTP request captured by HAR | +| `network:response` | `network` | HTTP response with status + headers | +| `network:cors-flag` | `network` | Detected CORS policy violation | +| `sdk:node-change` | `sdk` | DaVinci node status transition | +| `sdk:config` | `sdk` | SDK client configuration snapshot | +| `sdk:journey-step` | `sdk` | AM authentication tree step result | +| `sdk:oidc-state` | `sdk` | OIDC/OAuth endpoint outcome | +| `dom:form-submit` | `dom` | Form submission detected in the page | +| `dom:redirect` | `dom` | Client-side redirect detected | +| `session:cookie` | `session` | `document.cookie` changed | +| `session:storage` | `session` | `localStorage` key changed | + +--- + +## Data variants + +The `data` field is a discriminated union — use `_tag` to narrow it. + +### NetworkData + +```ts +{ + _tag: 'network'; + url: string; + method: string; + status: number; + requestHeaders: Record; + responseHeaders: Record; + duration: number; + corsFlag?: CorsFlag; + requestBody?: unknown; + responseBody?: unknown; +} +``` + +`CorsFlag` carries the reason (`'status-zero' | 'missing-allow-origin' | 'credentials-mismatch' | 'wildcard-with-credentials' | 'preflight-failed'`) plus optional preflight details. + +### SdkData — DaVinci + +Emitted on every DaVinci node status transition by `attachDevToolsBridge`. + +```ts +{ + _tag: 'sdk'; + nodeStatus: string; // 'next' | 'error' | 'success' | ... + previousStatus?: string; + interactionId?: string; + interactionToken?: string; + nodeId?: string; + requestId?: string; // DaVinci cache key (maps to raw HTTP response) + nodeName?: string; + nodeDescription?: string; + eventName?: string; + httpStatus?: number; + collectors?: unknown[]; // Form fields / UI descriptors + error?: SdkError; + authorization?: SdkAuthorization; + session?: string; + responseBody?: unknown; // Full DaVinci server response (from cache) +} +``` + +### JourneyData — AM trees + +Emitted by `attachJourneyBridge` for each RTK Query mutation that settles. + +```ts +{ + _tag: 'journey'; + stepType: 'Step' | 'LoginSuccess' | 'LoginFailure'; + callbacks?: unknown[]; // Full AM callback objects (with input/output arrays) + authId?: string; // Present on Step + tokenId?: string; // Present on LoginSuccess (session token) + successUrl?: string; // Present on LoginSuccess + realm?: string; + stage?: string; + header?: string; + description?: string; + errorCode?: number; // Present on LoginFailure + errorMessage?: string; + errorReason?: string; +} +``` + +### OidcData — OIDC/OAuth + +Emitted by `attachOidcBridge` for each RTK Query mutation that settles. + +```ts +{ + _tag: 'oidc'; + phase: 'authorize' | 'exchange' | 'revoke' | 'userinfo' | 'logout'; + status: 'success' | 'error'; + clientId?: string; + errorCode?: string; // OAuth error code (e.g. 'invalid_grant') + errorMessage?: string; // Human-readable error description +} +``` + +### SessionData + +```ts +{ + _tag: 'session'; + key: string; // localStorage key or 'document.cookie' + before?: string; + after?: string; +} +``` + +### SdkConfigData + +```ts +{ + _tag: 'sdk-config'; + config: unknown; // The raw config object passed to attachDevToolsBridge +} +``` + +### DomData + +```ts +{ + _tag: 'dom'; + element?: string; // CSS selector of the form element + url?: string; // Redirect target URL +} +``` + +--- + +## Runtime validation + +All schemas are [Effect Schema](https://effect.website/docs/schema/introduction/) definitions — use them directly for decoding untrusted data at message boundaries. + +```ts +import { Schema } from 'effect'; +import { AuthEventSchema } from '@forgerock/devtools-types'; + +const decode = Schema.decodeUnknownEither(AuthEventSchema); + +// In a service worker or message handler: +const result = decode(rawMessage); +if (Either.isLeft(result)) { + // validation failed — result.left carries detailed parse errors +} else { + const event = result.right; // AuthEvent, fully typed +} +``` + +--- + +## Exported symbols + +| Export | Kind | Description | +| ------------------------ | ------ | ------------------------------------ | +| `AuthEventSchema` | Schema | Full envelope validator | +| `AuthEventTypeSchema` | Schema | Union of all event type literals | +| `AuthEventFlagsSchema` | Schema | `{ isCors, isError, isAuthRelated }` | +| `NetworkDataSchema` | Schema | Network event data | +| `SdkDataSchema` | Schema | DaVinci SDK node data | +| `SdkConfigDataSchema` | Schema | SDK config snapshot | +| `JourneyDataSchema` | Schema | AM journey step data | +| `OidcDataSchema` | Schema | OIDC/OAuth phase data | +| `SessionDataSchema` | Schema | Cookie / localStorage diff | +| `DomDataSchema` | Schema | DOM event data | +| `SdkErrorSchema` | Schema | Error object sub-schema | +| `SdkAuthorizationSchema` | Schema | Authorization code/state sub-schema | +| `AuthEvent` | Type | Inferred from `AuthEventSchema` | +| `AuthEventType` | Type | Inferred from `AuthEventTypeSchema` | +| `AuthEventFlags` | Type | Inferred from `AuthEventFlagsSchema` | +| `NetworkData` | Type | Inferred from `NetworkDataSchema` | +| `SdkData` | Type | Inferred from `SdkDataSchema` | +| `JourneyData` | Type | Inferred from `JourneyDataSchema` | +| `OidcData` | Type | Inferred from `OidcDataSchema` | +| `SessionData` | Type | Inferred from `SessionDataSchema` | +| `SdkConfigData` | Type | Inferred from `SdkConfigDataSchema` | +| `DomData` | Type | Inferred from `DomDataSchema` | diff --git a/packages/devtools-types/eslint.config.mjs b/packages/devtools-types/eslint.config.mjs new file mode 100644 index 0000000000..cec2c4bf81 --- /dev/null +++ b/packages/devtools-types/eslint.config.mjs @@ -0,0 +1,3 @@ +import baseConfig from '../../eslint.config.mjs'; + +export default [{ ignores: ['**/dist'] }, ...baseConfig, { files: ['**/*.ts'], rules: {} }]; diff --git a/packages/devtools-types/package.json b/packages/devtools-types/package.json new file mode 100644 index 0000000000..a45f6a30b7 --- /dev/null +++ b/packages/devtools-types/package.json @@ -0,0 +1,38 @@ +{ + "name": "@forgerock/devtools-types", + "version": "2.0.0", + "private": true, + "description": "Shared AuthEvent schema and types for Ping DevTools", + "repository": { + "type": "git", + "url": "git+https://github.com/ForgeRock/ping-javascript-sdk.git", + "directory": "packages/devtools-types" + }, + "license": "MIT", + "author": "ForgeRock", + "sideEffects": false, + "type": "module", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/src/index.js", + "module": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "pnpm nx nxBuild", + "lint": "pnpm nx nxLint", + "test": "pnpm nx nxTest" + }, + "dependencies": { + "effect": "catalog:effect" + }, + "nx": { + "tags": ["scope:devtools-types"] + } +} diff --git a/packages/devtools-types/src/index.ts b/packages/devtools-types/src/index.ts new file mode 100644 index 0000000000..7f1dd8026b --- /dev/null +++ b/packages/devtools-types/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/auth-event.schema.js'; +export * from './lib/auth-event.types.js'; +export * from './lib/flow-export.schema.js'; +export * from './lib/flow-state.schema.js'; +export * from './lib/flow-state.types.js'; +export * from './lib/cors-flag.types.js'; diff --git a/packages/devtools-types/src/lib/auth-event.schema.test.ts b/packages/devtools-types/src/lib/auth-event.schema.test.ts new file mode 100644 index 0000000000..2bf6fc8fad --- /dev/null +++ b/packages/devtools-types/src/lib/auth-event.schema.test.ts @@ -0,0 +1,125 @@ +import { Schema } from 'effect'; +import { describe, expect, it } from 'vitest'; +import { AuthEventSchema } from './auth-event.schema.js'; + +const baseEvent = { + id: 'evt-001', + timestamp: 1700000000000, + source: 'network' as const, + flowId: 'flow-abc', + causedBy: null, + flags: { + isCors: false, + isError: false, + isAuthRelated: true, + }, +}; + +describe('AuthEventSchema', () => { + it('decodes a valid network event', () => { + const input = { + ...baseEvent, + type: 'network:response', + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'POST', + status: 200, + requestHeaders: { 'content-type': 'application/json' }, + responseHeaders: { 'x-request-id': 'abc123' }, + duration: 123, + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + + expect(result.id).toBe('evt-001'); + expect(result.type).toBe('network:response'); + expect(result.data._tag).toBe('network'); + }); + + it('rejects an event with an unknown type field', () => { + const input = { + ...baseEvent, + type: 'unknown:event-type', + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'GET', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 50, + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow( + /unknown:event-type|type/i, + ); + }); + + it('rejects an event with missing required id field', () => { + const input = { + timestamp: 1700000000000, + type: 'network:request', + source: 'network', + flowId: null, + flags: { isCors: false, isError: false, isAuthRelated: false }, + data: { + _tag: 'network', + url: 'https://auth.example.com/authorize', + method: 'GET', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 10, + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow(/id/i); + }); + + it('accepts null flowId', () => { + const input = { + ...baseEvent, + type: 'network:request', + flowId: null, + data: { + _tag: 'network', + url: 'https://auth.example.com/authorize', + method: 'GET', + status: 302, + requestHeaders: {}, + responseHeaders: {}, + duration: 10, + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + + expect(result.flowId).toBeNull(); + }); + + it('decodes a valid sdk event', () => { + const input = { + id: 'evt-002', + timestamp: 1700000001000, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-xyz', + causedBy: null, + flags: { isCors: false, isError: false, isAuthRelated: true }, + data: { + _tag: 'sdk', + nodeStatus: 'next', + interactionId: 'interaction-123', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + + expect(result.id).toBe('evt-002'); + expect(result.type).toBe('sdk:node-change'); + expect(result.data._tag).toBe('sdk'); + }); +}); diff --git a/packages/devtools-types/src/lib/auth-event.schema.ts b/packages/devtools-types/src/lib/auth-event.schema.ts new file mode 100644 index 0000000000..dbd61c43ba --- /dev/null +++ b/packages/devtools-types/src/lib/auth-event.schema.ts @@ -0,0 +1,160 @@ +import { Schema } from 'effect'; + +export const AuthEventTypeSchema = Schema.Union( + Schema.Literal('network:request'), + Schema.Literal('network:response'), + Schema.Literal('network:cors-flag'), + Schema.Literal('sdk:node-change'), + Schema.Literal('sdk:action'), + Schema.Literal('sdk:config'), + Schema.Literal('sdk:journey-step'), + Schema.Literal('sdk:oidc-state'), + Schema.Literal('dom:form-submit'), + Schema.Literal('dom:redirect'), + Schema.Literal('session:cookie'), + Schema.Literal('session:storage'), +); + +export const AuthEventFlagsSchema = Schema.Struct({ + isCors: Schema.Boolean, + isError: Schema.Boolean, + isAuthRelated: Schema.Boolean, +}); + +export const CorsFlagSchema = Schema.Struct({ + url: Schema.String, + reason: Schema.Union( + Schema.Literal('status-zero'), + Schema.Literal('missing-allow-origin'), + Schema.Literal('credentials-mismatch'), + Schema.Literal('wildcard-with-credentials'), + Schema.Literal('preflight-failed'), + ), + method: Schema.String, + preflightStatus: Schema.optional(Schema.Number), + allowOrigin: Schema.optional(Schema.String), + allowCredentials: Schema.optional(Schema.String), +}); + +export type CorsFlag = Schema.Schema.Type; + +export const NetworkDataSchema = Schema.Struct({ + _tag: Schema.Literal('network'), + url: Schema.String, + method: Schema.String, + status: Schema.Number, + requestHeaders: Schema.Record({ key: Schema.String, value: Schema.String }), + responseHeaders: Schema.Record({ key: Schema.String, value: Schema.String }), + duration: Schema.Number, + corsFlag: Schema.optional(CorsFlagSchema), + requestBody: Schema.optional(Schema.Unknown), + responseBody: Schema.optional(Schema.Unknown), +}); + +export const SdkErrorSchema = Schema.Struct({ + code: Schema.String, + message: Schema.String, + type: Schema.String, + internalHttpStatus: Schema.optional(Schema.Number), +}); + +export const SdkAuthorizationSchema = Schema.Struct({ + code: Schema.optional(Schema.String), + state: Schema.optional(Schema.String), +}); + +export const SdkDataSchema = Schema.Struct({ + _tag: Schema.Literal('sdk'), + nodeStatus: Schema.String, + previousStatus: Schema.optional(Schema.String), + interactionId: Schema.optional(Schema.String), + interactionToken: Schema.optional(Schema.String), + nodeId: Schema.optional(Schema.String), + requestId: Schema.optional(Schema.String), + nodeName: Schema.optional(Schema.String), + nodeDescription: Schema.optional(Schema.String), + eventName: Schema.optional(Schema.String), + httpStatus: Schema.optional(Schema.Number), + collectors: Schema.optional(Schema.Array(Schema.Unknown)), + error: Schema.optional(SdkErrorSchema), + authorization: Schema.optional(SdkAuthorizationSchema), + session: Schema.optional(Schema.String), + responseBody: Schema.optional(Schema.Unknown), +}); + +export const SdkConfigDataSchema = Schema.Struct({ + _tag: Schema.Literal('sdk-config'), + config: Schema.Unknown, +}); + +export const DomDataSchema = Schema.Struct({ + _tag: Schema.Literal('dom'), + element: Schema.optional(Schema.String), + url: Schema.optional(Schema.String), +}); + +export const SessionDataSchema = Schema.Struct({ + _tag: Schema.Literal('session'), + key: Schema.String, + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), +}); + +export const JourneyDataSchema = Schema.Struct({ + _tag: Schema.Literal('journey'), + stepType: Schema.Union( + Schema.Literal('Step'), + Schema.Literal('LoginSuccess'), + Schema.Literal('LoginFailure'), + ), + callbacks: Schema.optional(Schema.Array(Schema.Unknown)), + authId: Schema.optional(Schema.String), + tokenId: Schema.optional(Schema.String), + successUrl: Schema.optional(Schema.String), + realm: Schema.optional(Schema.String), + stage: Schema.optional(Schema.String), + header: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + errorCode: Schema.optional(Schema.Number), + errorMessage: Schema.optional(Schema.String), + errorReason: Schema.optional(Schema.String), +}); + +export const OidcDataSchema = Schema.Struct({ + _tag: Schema.Literal('oidc'), + phase: Schema.Union( + Schema.Literal('authorize'), + Schema.Literal('exchange'), + Schema.Literal('revoke'), + Schema.Literal('userinfo'), + Schema.Literal('logout'), + ), + status: Schema.Union(Schema.Literal('success'), Schema.Literal('error')), + clientId: Schema.optional(Schema.String), + errorCode: Schema.optional(Schema.String), + errorMessage: Schema.optional(Schema.String), +}); + +export const AuthEventSchema = Schema.Struct({ + id: Schema.String, + timestamp: Schema.Number, + type: AuthEventTypeSchema, + source: Schema.Union( + Schema.Literal('network'), + Schema.Literal('sdk'), + Schema.Literal('dom'), + Schema.Literal('session'), + ), + flowId: Schema.NullOr(Schema.String), + causedBy: Schema.NullOr(Schema.String), + data: Schema.Union( + NetworkDataSchema, + SdkDataSchema, + SdkConfigDataSchema, + DomDataSchema, + SessionDataSchema, + JourneyDataSchema, + OidcDataSchema, + ), + flags: AuthEventFlagsSchema, +}); diff --git a/packages/devtools-types/src/lib/auth-event.types.ts b/packages/devtools-types/src/lib/auth-event.types.ts new file mode 100644 index 0000000000..1b5b37dbbe --- /dev/null +++ b/packages/devtools-types/src/lib/auth-event.types.ts @@ -0,0 +1,24 @@ +import type { Schema } from 'effect'; +import type { + AuthEventSchema, + AuthEventTypeSchema, + AuthEventFlagsSchema, + NetworkDataSchema, + SdkDataSchema, + SdkConfigDataSchema, + DomDataSchema, + SessionDataSchema, + JourneyDataSchema, + OidcDataSchema, +} from './auth-event.schema.js'; + +export type AuthEventType = Schema.Schema.Type; +export type AuthEventFlags = Schema.Schema.Type; +export type NetworkData = Schema.Schema.Type; +export type SdkData = Schema.Schema.Type; +export type SdkConfigData = Schema.Schema.Type; +export type DomData = Schema.Schema.Type; +export type SessionData = Schema.Schema.Type; +export type JourneyData = Schema.Schema.Type; +export type OidcData = Schema.Schema.Type; +export type AuthEvent = Schema.Schema.Type; diff --git a/packages/devtools-types/src/lib/cors-flag.types.ts b/packages/devtools-types/src/lib/cors-flag.types.ts new file mode 100644 index 0000000000..8355db2fb7 --- /dev/null +++ b/packages/devtools-types/src/lib/cors-flag.types.ts @@ -0,0 +1,2 @@ +// CorsFlag type is derived from CorsFlagSchema — single source of truth. +export type { CorsFlag } from './auth-event.schema.js'; diff --git a/packages/devtools-types/src/lib/flow-export.schema.test.ts b/packages/devtools-types/src/lib/flow-export.schema.test.ts new file mode 100644 index 0000000000..d9f3dfa183 --- /dev/null +++ b/packages/devtools-types/src/lib/flow-export.schema.test.ts @@ -0,0 +1,80 @@ +import { Schema } from 'effect'; +import { describe, expect, it } from 'vitest'; +import { FlowExportSchema } from './flow-export.schema.js'; + +const validExport = { + version: 1, + exportedAt: '2026-05-08T14:32:00.000Z', + redacted: true, + flow: { + flowId: 'flow-abc', + capturedAt: '2026-05-08T14:30:00.000Z', + events: [ + { + id: 'evt-001', + timestamp: 1700000000000, + type: 'network:response', + source: 'network', + flowId: 'flow-abc', + causedBy: null, + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'POST', + status: 200, + requestHeaders: { 'content-type': 'application/json' }, + responseHeaders: { 'x-request-id': 'abc123' }, + duration: 123, + }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + }, + ], + summary: { + nodeCount: 0, + errorCount: 0, + corsFlags: [], + duration: 0, + sdkConnected: false, + }, + }, +}; + +describe('FlowExportSchema', () => { + it('decodes a valid export envelope', () => { + const result = Schema.decodeUnknownSync(FlowExportSchema)(validExport); + expect(result.version).toBe(1); + expect(result.redacted).toBe(true); + expect(result.flow.events).toHaveLength(1); + expect(result.flow.flowId).toBe('flow-abc'); + }); + + it('rejects missing version field', () => { + const { version, ...noVersion } = validExport; + expect(() => Schema.decodeUnknownSync(FlowExportSchema)(noVersion)).toThrow(); + }); + + it('rejects wrong version number', () => { + const input = { ...validExport, version: 2 }; + expect(() => Schema.decodeUnknownSync(FlowExportSchema)(input)).toThrow(); + }); + + it('rejects invalid event inside flow.events', () => { + const input = { + ...validExport, + flow: { + ...validExport.flow, + events: [{ id: 'bad', timestamp: 0 }], + }, + }; + expect(() => Schema.decodeUnknownSync(FlowExportSchema)(input)).toThrow(); + }); + + it('accepts flow with lastSdkEventId (optional field defaults to null)', () => { + const input = { + ...validExport, + flow: { ...validExport.flow, lastSdkEventId: 'sdk-99' }, + }; + const result = Schema.decodeUnknownSync(FlowExportSchema)(input); + expect(result.flow.lastSdkEventId).toBe('sdk-99'); + }); +}); diff --git a/packages/devtools-types/src/lib/flow-export.schema.ts b/packages/devtools-types/src/lib/flow-export.schema.ts new file mode 100644 index 0000000000..3e7df35f67 --- /dev/null +++ b/packages/devtools-types/src/lib/flow-export.schema.ts @@ -0,0 +1,11 @@ +import { Schema } from 'effect'; +import { FlowStateSchema } from './flow-state.schema.js'; + +export const FlowExportSchema = Schema.Struct({ + version: Schema.Literal(1), + exportedAt: Schema.String, + redacted: Schema.Boolean, + flow: FlowStateSchema, +}); + +export type FlowExport = Schema.Schema.Type; diff --git a/packages/devtools-types/src/lib/flow-state.schema.ts b/packages/devtools-types/src/lib/flow-state.schema.ts new file mode 100644 index 0000000000..fc1b0dbfc9 --- /dev/null +++ b/packages/devtools-types/src/lib/flow-state.schema.ts @@ -0,0 +1,21 @@ +import { Schema } from 'effect'; +import { AuthEventSchema, CorsFlagSchema } from './auth-event.schema.js'; + +export const FlowSummarySchema = Schema.Struct({ + nodeCount: Schema.Number, + errorCount: Schema.Number, + corsFlags: Schema.Array(CorsFlagSchema), + duration: Schema.Number, + sdkConnected: Schema.Boolean, +}); + +export const FlowStateSchema = Schema.Struct({ + flowId: Schema.NullOr(Schema.String), + capturedAt: Schema.String, + events: Schema.Array(AuthEventSchema), + summary: FlowSummarySchema, + lastSdkEventId: Schema.optionalWith(Schema.NullOr(Schema.String), { default: () => null }), +}); + +export type FlowSummary = Schema.Schema.Type; +export type FlowState = Schema.Schema.Type; diff --git a/packages/devtools-types/src/lib/flow-state.types.ts b/packages/devtools-types/src/lib/flow-state.types.ts new file mode 100644 index 0000000000..02eeed7668 --- /dev/null +++ b/packages/devtools-types/src/lib/flow-state.types.ts @@ -0,0 +1,2 @@ +// FlowState and FlowSummary types are derived from their schemas — single source of truth. +export type { FlowSummary, FlowState } from './flow-state.schema.js'; diff --git a/packages/devtools-types/tsconfig.json b/packages/devtools-types/tsconfig.json new file mode 100644 index 0000000000..9fd2495969 --- /dev/null +++ b/packages/devtools-types/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { "path": "./tsconfig.lib.json" }, + { "path": "./tsconfig.spec.json" } + ], + "nx": { + "addTypecheckTarget": false + } +} diff --git a/packages/devtools-types/tsconfig.lib.json b/packages/devtools-types/tsconfig.lib.json new file mode 100644 index 0000000000..84f810411f --- /dev/null +++ b/packages/devtools-types/tsconfig.lib.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "moduleResolution": "nodenext", + "module": "NodeNext", + "target": "ES2022", + "outDir": "./dist", + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "strict": true, + "noImplicitOverride": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true, + "sourceMap": true, + "lib": ["es2022", "dom", "dom.iterable"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/devtools-types/tsconfig.spec.json b/packages/devtools-types/tsconfig.spec.json new file mode 100644 index 0000000000..26485106b7 --- /dev/null +++ b/packages/devtools-types/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "module": "NodeNext", + "moduleResolution": "nodenext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true + }, + "include": ["vite.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"], + "references": [{ "path": "./tsconfig.lib.json" }] +} diff --git a/packages/devtools-types/vite.config.ts b/packages/devtools-types/vite.config.ts new file mode 100644 index 0000000000..b27e786af5 --- /dev/null +++ b/packages/devtools-types/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/packages/devtools-types', + test: { + watch: false, + globals: true, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,ts}'], + reporters: ['default'], + }, +})); diff --git a/packages/journey-client/api-report/journey-client.api.md b/packages/journey-client/api-report/journey-client.api.md index 9e471c8784..797d5933ae 100644 --- a/packages/journey-client/api-report/journey-client.api.md +++ b/packages/journey-client/api-report/journey-client.api.md @@ -185,6 +185,8 @@ export function journey(input: { // @public export interface JourneyClient { + // (undocumented) + getState: () => unknown; // (undocumented) next: (step: JourneyStep, options?: NextOptions) => Promise; // (undocumented) @@ -194,6 +196,8 @@ export interface JourneyClient { // (undocumented) start: (options?: StartParam) => Promise; // (undocumented) + subscribe: (listener: () => void) => () => void; + // (undocumented) terminate: (options?: { query?: Record; }) => Promise; diff --git a/packages/journey-client/api-report/journey-client.types.api.md b/packages/journey-client/api-report/journey-client.types.api.md index c9d45ac5a5..74c2ee422a 100644 --- a/packages/journey-client/api-report/journey-client.types.api.md +++ b/packages/journey-client/api-report/journey-client.types.api.md @@ -172,6 +172,8 @@ export { isValidWellknownUrl } // @public export interface JourneyClient { + // (undocumented) + getState: () => unknown; // (undocumented) next: (step: JourneyStep, options?: NextOptions) => Promise; // (undocumented) @@ -181,6 +183,8 @@ export interface JourneyClient { // (undocumented) start: (options?: StartParam) => Promise; // (undocumented) + subscribe: (listener: () => void) => () => void; + // (undocumented) terminate: (options?: { query?: Record; }) => Promise; diff --git a/packages/journey-client/src/lib/client.store.ts b/packages/journey-client/src/lib/client.store.ts index 129386a864..240374b420 100644 --- a/packages/journey-client/src/lib/client.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -36,6 +36,8 @@ export type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFail /** The journey client instance returned by the `journey()` function. */ export interface JourneyClient { + subscribe: (listener: () => void) => () => void; + getState: () => unknown; start: (options?: StartParam) => Promise; next: (step: JourneyStep, options?: NextOptions) => Promise; redirect: (step: JourneyStep) => Promise; @@ -154,6 +156,8 @@ export async function journey({ }); const self: JourneyClient = { + subscribe: store.subscribe, + getState: store.getState, start: async (options?: StartParam) => { const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options)); if (!data) { diff --git a/packages/oidc-client/api-report/oidc-client.api.md b/packages/oidc-client/api-report/oidc-client.api.md index 283d363dc9..0230e71cb8 100644 --- a/packages/oidc-client/api-report/oidc-client.api.md +++ b/packages/oidc-client/api-report/oidc-client.api.md @@ -11,7 +11,7 @@ import { CombinedState } from '@reduxjs/toolkit/query'; import { CustomLogger } from '@forgerock/sdk-logger'; import { EnhancedStore } from '@reduxjs/toolkit'; import { FetchArgs } from '@reduxjs/toolkit/query'; -import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; import { GenericError } from '@forgerock/sdk-types'; import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; @@ -27,6 +27,7 @@ import { StoreEnhancer } from '@reduxjs/toolkit'; import { ThunkDispatch } from '@reduxjs/toolkit'; import { Tuple } from '@reduxjs/toolkit'; import { UnknownAction } from '@reduxjs/toolkit'; +import { Unsubscribe } from '@reduxjs/toolkit'; import { WellknownResponse } from '@forgerock/sdk-types'; export { ActionTypes } @@ -250,12 +251,39 @@ export function oidc(input: { }; storage?: Partial; }): Promise<{ - error: string; - type: string; - authorize?: undefined; - token?: undefined; - user?: undefined; -} | { + subscribe: (listener: () => void) => Unsubscribe; + getState: () => { + oidc: CombinedState< { + authorizeFetch: MutationDefinition< { + url: string; + }, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; + authorizeIframe: MutationDefinition< { + url: string; + }, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; + endSession: MutationDefinition< { + idToken: string; + endpoint: string; + }, BaseQueryFn, never, null, "oidc", unknown>; + exchange: MutationDefinition< { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + }, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; + revoke: MutationDefinition< { + accessToken: string; + clientId?: string; + endpoint: string; + }, BaseQueryFn, never, object, "oidc", unknown>; + userInfo: MutationDefinition< { + accessToken: string; + endpoint: string; + }, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; + }, never, "oidc">; + wellknown: CombinedState< { + configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; + }, never, "wellknown">; + }; authorize: { url: (options?: GetAuthorizationUrlOptions) => Promise; background: (options?: GetAuthorizationUrlOptions) => Promise; @@ -269,8 +297,6 @@ export function oidc(input: { info: () => Promise; logout: () => Promise; }; - error?: undefined; - type?: undefined; }>; // @public (undocumented) diff --git a/packages/oidc-client/api-report/oidc-client.types.api.md b/packages/oidc-client/api-report/oidc-client.types.api.md index 283d363dc9..0230e71cb8 100644 --- a/packages/oidc-client/api-report/oidc-client.types.api.md +++ b/packages/oidc-client/api-report/oidc-client.types.api.md @@ -11,7 +11,7 @@ import { CombinedState } from '@reduxjs/toolkit/query'; import { CustomLogger } from '@forgerock/sdk-logger'; import { EnhancedStore } from '@reduxjs/toolkit'; import { FetchArgs } from '@reduxjs/toolkit/query'; -import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'; +import { FetchBaseQueryError } from '@reduxjs/toolkit/query'; import { FetchBaseQueryMeta } from '@reduxjs/toolkit/query'; import { GenericError } from '@forgerock/sdk-types'; import { GetAuthorizationUrlOptions } from '@forgerock/sdk-types'; @@ -27,6 +27,7 @@ import { StoreEnhancer } from '@reduxjs/toolkit'; import { ThunkDispatch } from '@reduxjs/toolkit'; import { Tuple } from '@reduxjs/toolkit'; import { UnknownAction } from '@reduxjs/toolkit'; +import { Unsubscribe } from '@reduxjs/toolkit'; import { WellknownResponse } from '@forgerock/sdk-types'; export { ActionTypes } @@ -250,12 +251,39 @@ export function oidc(input: { }; storage?: Partial; }): Promise<{ - error: string; - type: string; - authorize?: undefined; - token?: undefined; - user?: undefined; -} | { + subscribe: (listener: () => void) => Unsubscribe; + getState: () => { + oidc: CombinedState< { + authorizeFetch: MutationDefinition< { + url: string; + }, BaseQueryFn, never, AuthorizeSuccessResponse, "oidc", unknown>; + authorizeIframe: MutationDefinition< { + url: string; + }, BaseQueryFn, never, AuthorizationSuccess, "oidc", unknown>; + endSession: MutationDefinition< { + idToken: string; + endpoint: string; + }, BaseQueryFn, never, null, "oidc", unknown>; + exchange: MutationDefinition< { + code: string; + config: OidcConfig; + endpoint: string; + verifier?: string; + }, BaseQueryFn, never, TokenExchangeResponse, "oidc", unknown>; + revoke: MutationDefinition< { + accessToken: string; + clientId?: string; + endpoint: string; + }, BaseQueryFn, never, object, "oidc", unknown>; + userInfo: MutationDefinition< { + accessToken: string; + endpoint: string; + }, BaseQueryFn, never, UserInfoResponse, "oidc", unknown>; + }, never, "oidc">; + wellknown: CombinedState< { + configuration: QueryDefinition, never, WellknownResponse, "wellknown", unknown>; + }, never, "wellknown">; + }; authorize: { url: (options?: GetAuthorizationUrlOptions) => Promise; background: (options?: GetAuthorizationUrlOptions) => Promise; @@ -269,8 +297,6 @@ export function oidc(input: { info: () => Promise; logout: () => Promise; }; - error?: undefined; - type?: undefined; }>; // @public (undocumented) diff --git a/packages/oidc-client/src/lib/client.store.ts b/packages/oidc-client/src/lib/client.store.ts index da6c3de99c..29c0ef2c99 100644 --- a/packages/oidc-client/src/lib/client.store.ts +++ b/packages/oidc-client/src/lib/client.store.ts @@ -73,16 +73,10 @@ export async function oidc({ const store = createClientStore({ requestMiddleware, logger: log }); if (!config?.serverConfig?.wellknown) { - return { - error: 'Requires a wellknown url initializing this factory.', - type: 'argument_error', - }; + throw new Error('Requires a wellknown url initializing this factory.'); } if (!config?.clientId) { - return { - error: 'Requires a clientId.', - type: 'argument_error', - }; + throw new Error('Requires a clientId.'); } const wellknownUrl = config.serverConfig.wellknown; @@ -95,6 +89,8 @@ export async function oidc({ } return { + subscribe: store.subscribe, + getState: store.getState, /** * An object containing methods for the creation, and background use, of the authorization URL */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index babd5d7de7..77020caf56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,10 +115,10 @@ importers: version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@9.39.4(jiti@2.6.1))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(ts-node@10.9.2(@swc/core@1.15.30(@swc/helpers@0.5.21))(@types/node@24.9.2)(typescript@5.8.3))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0)) '@nx/vite': specifier: 22.6.5 - version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@nx/vitest': specifier: 22.6.5 - version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@nx/web': specifier: 22.6.5 version: 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) @@ -262,10 +262,10 @@ importers: version: 6.5.2(typanion@3.14.0) vite: specifier: catalog:vite - version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) vitest-canvas-mock: specifier: catalog:vitest version: 1.1.3(vitest@3.2.4) @@ -300,6 +300,9 @@ importers: '@forgerock/davinci-client': specifier: workspace:* version: link:../../packages/davinci-client + '@forgerock/devtools-bridge': + specifier: workspace:* + version: link:../../packages/devtools-bridge '@forgerock/javascript-sdk': specifier: 4.7.0 version: 4.7.0 @@ -333,6 +336,9 @@ importers: '@forgerock/device-client': specifier: workspace:* version: link:../../packages/device-client + '@forgerock/devtools-bridge': + specifier: workspace:* + version: link:../../packages/devtools-bridge '@forgerock/journey-client': specifier: workspace:* version: link:../../packages/journey-client @@ -389,10 +395,13 @@ importers: version: 0.27.0(effect@3.20.0)(vitest@3.2.4) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) e2e/oidc-app: dependencies: + '@forgerock/devtools-bridge': + specifier: workspace:* + version: link:../../packages/devtools-bridge '@forgerock/oidc-client': specifier: workspace:* version: link:../../packages/oidc-client @@ -445,7 +454,7 @@ importers: version: 0.27.0(effect@3.20.0)(vitest@3.2.4) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) packages/device-client: dependencies: @@ -460,6 +469,47 @@ importers: specifier: 'catalog:' version: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) + packages/devtools-bridge: + dependencies: + '@forgerock/devtools-types': + specifier: workspace:* + version: link:../devtools-types + effect: + specifier: catalog:effect + version: 3.20.0 + devDependencies: + '@forgerock/davinci-client': + specifier: workspace:* + version: link:../davinci-client + + packages/devtools-extension: + dependencies: + '@forgerock/devtools-types': + specifier: workspace:* + version: link:../devtools-types + effect: + specifier: catalog:effect + version: 3.20.0 + devDependencies: + '@types/chrome': + specifier: ^0.1.40 + version: 0.1.40 + elm-tooling: + specifier: ^1.15.1 + version: 1.17.0 + esbuild: + specifier: ^0.28.0 + version: 0.28.0 + terser: + specifier: ^5.47.1 + version: 5.47.1 + + packages/devtools-types: + dependencies: + effect: + specifier: catalog:effect + version: 3.20.0 + packages/journey-client: dependencies: '@forgerock/sdk-logger': @@ -492,10 +542,10 @@ importers: version: 3.2.4(vitest@3.2.4) vite: specifier: catalog:vite - version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) vitest-canvas-mock: specifier: catalog:vitest version: 1.1.3(vitest@3.2.4) @@ -601,7 +651,7 @@ importers: version: 4.20.6 vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) devDependencies: '@forgerock/javascript-sdk': specifier: 4.9.0 @@ -632,7 +682,7 @@ importers: version: 3.20.0 vitest: specifier: catalog:vitest - version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) devDependencies: '@effect/language-service': specifier: catalog:effect @@ -1633,6 +1683,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1645,6 +1701,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -1657,6 +1719,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -1669,6 +1737,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -1681,6 +1755,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -1693,6 +1773,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -1705,6 +1791,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -1717,6 +1809,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -1729,6 +1827,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -1741,6 +1845,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -1753,6 +1863,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -1765,6 +1881,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -1777,6 +1899,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -1789,6 +1917,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -1801,6 +1935,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -1813,6 +1953,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -1825,6 +1971,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -1837,6 +1989,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -1849,6 +2007,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -1861,6 +2025,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -1873,6 +2043,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -1885,6 +2061,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -1897,6 +2079,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -1909,6 +2097,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -1921,6 +2115,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -1933,6 +2133,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3229,6 +3435,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/chrome@0.1.40': + resolution: {integrity: sha512-UnfyRAe8ORu9HSuTH0EqyOEUin3JrWW9Nl/gDXezNfTUrfIoxw+WRZgKOxGz0t5BnjbfXBnS2eCYfW2PxH1wcA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -3259,6 +3468,15 @@ packages: '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/filesystem@0.0.36': + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + + '@types/filewriter@0.0.33': + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + + '@types/har-format@1.2.16': + resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -3406,6 +3624,7 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + deprecated: Potential CWE-502 - Update to 1.3.1 or higher '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} @@ -4828,6 +5047,10 @@ packages: electron-to-chromium@1.5.249: resolution: {integrity: sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==} + elm-tooling@1.17.0: + resolution: {integrity: sha512-Y6umJYX7w/tV08pgmNF95nkvPq+xOvhirJz6LIt4YUj2I9V2W0V4Yb1E0IUZ/9X9yUgiEcFpKyObav4QMWCPrg==} + hasBin: true + emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} @@ -4926,6 +5149,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -7836,8 +8064,8 @@ packages: uglify-js: optional: true - terser@5.46.2: - resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==} + terser@5.47.1: + resolution: {integrity: sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==} engines: {node: '>=10'} hasBin: true @@ -8223,6 +8451,7 @@ packages: uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -9829,7 +10058,7 @@ snapshots: '@effect/vitest@0.27.0(effect@3.20.0)(vitest@3.2.4)': dependencies: effect: 3.20.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) '@effect/workflow@0.8.3(@effect/platform@0.90.10(effect@3.20.0))(@effect/rpc@0.68.4(@effect/platform@0.90.10(effect@3.20.0))(effect@3.20.0))(effect@3.20.0)': dependencies: @@ -9856,156 +10085,234 @@ snapshots: '@esbuild/aix-ppc64@0.27.2': optional: true + '@esbuild/aix-ppc64@0.28.0': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.27.2': optional: true + '@esbuild/android-arm64@0.28.0': + optional: true + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.27.2': optional: true + '@esbuild/android-arm@0.28.0': + optional: true + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.27.2': optional: true + '@esbuild/android-x64@0.28.0': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.27.2': optional: true + '@esbuild/darwin-arm64@0.28.0': + optional: true + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.27.2': optional: true + '@esbuild/darwin-x64@0.28.0': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.27.2': optional: true + '@esbuild/freebsd-arm64@0.28.0': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.27.2': optional: true + '@esbuild/freebsd-x64@0.28.0': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.27.2': optional: true + '@esbuild/linux-arm64@0.28.0': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.27.2': optional: true + '@esbuild/linux-arm@0.28.0': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.27.2': optional: true + '@esbuild/linux-ia32@0.28.0': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.27.2': optional: true + '@esbuild/linux-loong64@0.28.0': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.27.2': optional: true + '@esbuild/linux-mips64el@0.28.0': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.27.2': optional: true + '@esbuild/linux-ppc64@0.28.0': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.27.2': optional: true + '@esbuild/linux-riscv64@0.28.0': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.27.2': optional: true + '@esbuild/linux-s390x@0.28.0': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.27.2': optional: true + '@esbuild/linux-x64@0.28.0': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.27.2': optional: true + '@esbuild/netbsd-arm64@0.28.0': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.27.2': optional: true + '@esbuild/netbsd-x64@0.28.0': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.27.2': optional: true + '@esbuild/openbsd-arm64@0.28.0': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.27.2': optional: true + '@esbuild/openbsd-x64@0.28.0': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.27.2': optional: true + '@esbuild/openharmony-arm64@0.28.0': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.27.2': optional: true + '@esbuild/sunos-x64@0.28.0': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.27.2': optional: true + '@esbuild/win32-arm64@0.28.0': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.27.2': optional: true + '@esbuild/win32-ia32@0.28.0': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true '@esbuild/win32-x64@0.27.2': optional: true + '@esbuild/win32-x64@0.28.0': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -10782,11 +11089,11 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + '@nx/vite@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@nx/devkit': 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) - '@nx/vitest': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) + '@nx/vitest': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4) '@phenomnomnominal/tsquery': 6.1.4(typescript@5.8.3) ajv: 8.18.0 enquirer: 2.3.6 @@ -10794,8 +11101,8 @@ snapshots: semver: 7.7.3 tsconfig-paths: 4.2.0 tslib: 2.8.1 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10806,7 +11113,7 @@ snapshots: - typescript - verdaccio - '@nx/vitest@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': + '@nx/vitest@22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(typescript@5.8.3)(verdaccio@6.5.2(typanion@3.14.0))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@nx/devkit': 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))) '@nx/js': 22.6.5(@babel/traverse@7.28.5)(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21))(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.7.0)(@emnapi/runtime@1.7.0)(@swc/core@1.15.30(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@5.8.3))(@swc/core@1.15.30(@swc/helpers@0.5.21)))(verdaccio@6.5.2(typanion@3.14.0)) @@ -10814,8 +11121,8 @@ snapshots: semver: 7.7.3 tslib: 2.8.1 optionalDependencies: - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -11498,6 +11805,11 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/chrome@0.1.40': + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.16 + '@types/connect@3.4.38': dependencies: '@types/node': 24.9.2 @@ -11543,6 +11855,14 @@ snapshots: '@types/express-serve-static-core': 5.1.0 '@types/serve-static': 2.2.0 + '@types/filesystem@0.0.36': + dependencies: + '@types/filewriter': 0.0.33 + + '@types/filewriter@0.0.33': {} + + '@types/har-format@1.2.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -11974,7 +12294,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -11986,32 +12306,32 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.8.3) - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) - '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.1(@types/node@24.9.2)(typescript@5.9.3) - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -12042,7 +12362,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -12290,17 +12610,13 @@ snapshots: dependencies: acorn: 8.16.0 - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn-walk@8.3.4: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 acorn@8.15.0: {} @@ -13363,6 +13679,8 @@ snapshots: electron-to-chromium@1.5.249: {} + elm-tooling@1.17.0: {} + emittery@0.13.1: {} emoji-regex@8.0.0: {} @@ -13550,6 +13868,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -13720,8 +14067,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -16955,12 +17302,12 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 - terser: 5.46.2 + terser: 5.47.1 webpack: 5.102.1(@swc/core@1.15.30(@swc/helpers@0.5.21)) optionalDependencies: '@swc/core': 1.15.30(@swc/helpers@0.5.21) - terser@5.46.2: + terser@5.47.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 @@ -17442,13 +17789,13 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 - vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -17463,13 +17810,13 @@ snapshots: - tsx - yaml - vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -17484,7 +17831,7 @@ snapshots: - tsx - yaml - vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.4) @@ -17496,11 +17843,11 @@ snapshots: '@types/node': 24.9.2 fsevents: 2.3.3 jiti: 2.6.1 - terser: 5.46.2 + terser: 5.47.1 tsx: 4.20.6 yaml: 2.8.1 - vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): + vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.4) @@ -17512,7 +17859,7 @@ snapshots: '@types/node': 24.9.2 fsevents: 2.3.3 jiti: 2.6.1 - terser: 5.46.2 + terser: 5.47.1 tsx: 4.21.0 yaml: 2.8.1 @@ -17520,13 +17867,13 @@ snapshots: dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) - vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.8.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17544,8 +17891,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.2 @@ -17565,11 +17912,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17587,8 +17934,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.20.6)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.2 @@ -17608,11 +17955,11 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.9.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.4.0(@noble/hashes@1.8.0))(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.12.1(@types/node@24.9.2)(typescript@5.9.3))(vite@7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17630,8 +17977,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.46.2)(tsx@4.21.0)(yaml@2.8.1) + vite: 7.3.2(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(terser@5.47.1)(tsx@4.21.0)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.9.2 diff --git a/tsconfig.json b/tsconfig.json index c22692ddac..5d08a69485 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -84,6 +84,15 @@ }, { "path": "./tools/api-report" + }, + { + "path": "./packages/devtools-types" + }, + { + "path": "./packages/devtools-bridge" + }, + { + "path": "./packages/devtools-extension" } ] } From 623b94737063bd2ca3e6df104eb41a89e9c3b60d Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 13:30:25 -0600 Subject: [PATCH 2/8] fix(devtools): resolve TS errors and add Learn tab types - Narrow union type in message-handler tests before accessing event fields - Add `as const` to category literal in serialize-diagnosis test - Remove unknown property from oidc-bridge test config fixture - Add CardId, Vec2, CanvasState types and LearnMode to ViewMode in Elm - Add stub LearnMode case in View.elm for build compatibility --- .../src/lib/oidc-bridge.test.ts | 214 ++++++++++++++++++ .../src/background/message-handler.test.ts | 172 ++++++++++++++ .../background/serialize-diagnosis.test.ts | 106 +++++++++ .../src/panel/src/Types.elm | 76 ++++++- .../devtools-extension/src/panel/src/View.elm | 15 +- 5 files changed, 572 insertions(+), 11 deletions(-) create mode 100644 packages/devtools-extension/src/background/message-handler.test.ts create mode 100644 packages/devtools-extension/src/background/serialize-diagnosis.test.ts diff --git a/packages/devtools-bridge/src/lib/oidc-bridge.test.ts b/packages/devtools-bridge/src/lib/oidc-bridge.test.ts index 3b4088c4dc..75f7759f60 100644 --- a/packages/devtools-bridge/src/lib/oidc-bridge.test.ts +++ b/packages/devtools-bridge/src/lib/oidc-bridge.test.ts @@ -310,3 +310,217 @@ describe('attachOidcBridge', () => { expect(events).toHaveLength(0); }); }); + +// --------------------------------------------------------------------------- +// Edge-case tests for pure function paths (extractOidcError, mutationToOidcData) +// --------------------------------------------------------------------------- + +describe('extractOidcError (via integration)', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + }); + + it('extracts error_description from error.data', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': rejectedMutation('exchange', { + data: { error: 'invalid_grant', error_description: 'Code expired' }, + }), + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorCode?: string; errorMessage?: string }; + expect(data.errorCode).toBe('invalid_grant'); + expect(data.errorMessage).toBe('Code expired'); + }); + + it('falls back to data.message when error_description is absent', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': rejectedMutation('exchange', { + data: { message: 'Server error' }, + }), + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorCode?: string; errorMessage?: string }; + expect(data.errorCode).toBeUndefined(); + expect(data.errorMessage).toBe('Server error'); + }); + + it('falls back to top-level message when no data object', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': rejectedMutation('exchange', { message: 'Top level error' }), + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Top level error'); + }); + + it('extracts string error as errorMessage', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': rejectedMutation('exchange', 'plain string error'), + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string; errorCode?: string }; + expect(data.errorMessage).toBe('plain string error'); + expect(data.errorCode).toBeUndefined(); + }); + + it('returns empty error fields for null error', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': rejectedMutation('exchange', null), + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorCode?: string; errorMessage?: string }; + expect(data.errorCode).toBeUndefined(); + expect(data.errorMessage).toBeUndefined(); + }); + + it('returns empty error fields for undefined error', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': { status: 'rejected', endpointName: 'exchange' }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorCode?: string; errorMessage?: string }; + expect(data.errorCode).toBeUndefined(); + expect(data.errorMessage).toBeUndefined(); + }); +}); + +describe('mutationToOidcData edge cases (via integration)', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + }); + + it('does not emit for mutation with undefined endpointName', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + client.trigger({ + oidc: { + mutations: { + 'req-1': { status: 'fulfilled' }, + }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('trims stale requestIds from emitted set', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client); + + // First trigger: add req-1 + client.trigger({ + oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } }, + }); + + // Second trigger: req-1 removed, req-2 added + client.trigger({ + oidc: { mutations: { 'req-2': fulfilledMutation('revoke') } }, + }); + + handle.detach(); + stop(); + + const oidcEvents = events.filter((e) => e.detail.type === 'sdk:oidc-state'); + expect(oidcEvents).toHaveLength(2); + expect((oidcEvents[0].detail.data as { phase: string }).phase).toBe('exchange'); + expect((oidcEvents[1].detail.data as { phase: string }).phase).toBe('revoke'); + }); + + it('passes undefined clientId when config has no clientId', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachOidcBridge(client, {}); + client.trigger({ + oidc: { mutations: { 'req-1': fulfilledMutation('exchange') } }, + }); + + handle.detach(); + stop(); + + const oidcEvent = events.find((e) => e.detail.type === 'sdk:oidc-state'); + const data = oidcEvent?.detail.data as { clientId?: string }; + expect(data.clientId).toBeUndefined(); + }); +}); diff --git a/packages/devtools-extension/src/background/message-handler.test.ts b/packages/devtools-extension/src/background/message-handler.test.ts new file mode 100644 index 0000000000..e9f71c871c --- /dev/null +++ b/packages/devtools-extension/src/background/message-handler.test.ts @@ -0,0 +1,172 @@ +import { describe, it, expect, vi } from 'vitest'; +import { Effect, Ref, pipe } from 'effect'; +import { handleMessage } from './message-handler.js'; +import { EventStoreService, makeEmptyFlowState } from './event-store.service.js'; +import type { AuthEvent, FlowState } from '@forgerock/devtools-types'; +import { Layer } from 'effect'; + +// A test-only Layer that replaces persist/rehydrate with no-ops (no chrome.storage) +const TestStoreLive = Layer.effect( + EventStoreService, + pipe( + Ref.make(makeEmptyFlowState()), + Effect.map((stateRef) => ({ + append: (event: AuthEvent) => + Ref.update(stateRef, (s) => ({ + ...s, + events: [...s.events, event], + flowId: s.flowId ?? event.flowId, + lastSdkEventId: event.type === 'sdk:node-change' ? event.id : s.lastSdkEventId, + })), + getState: () => Ref.get(stateRef), + clear: () => Ref.set(stateRef, makeEmptyFlowState()), + persist: () => Effect.void, + rehydrate: () => Effect.void, + })), + ), +); + +function run(effect: Effect.Effect): Promise { + return Effect.runPromise(Effect.provide(effect, TestStoreLive)); +} + +const makeNetworkHarEntry = (url = 'https://auth.example.com/authorize') => ({ + request: { + url, + method: 'POST', + headers: [{ name: 'content-type', value: 'application/json' }], + }, + response: { + status: 200, + headers: [{ name: 'x-request-id', value: 'abc' }], + content: { text: '{"access_token":"tok"}' }, + }, + time: 123, +}); + +const makeSdkEvent = (overrides: Partial = {}): AuthEvent => ({ + id: 'sdk-1', + timestamp: 100, + type: 'sdk:node-change', + source: 'sdk', + flowId: 'flow-1', + causedBy: null, + data: { _tag: 'sdk', nodeStatus: 'continue' }, + flags: { isCors: false, isError: false, isAuthRelated: true }, + ...overrides, +}); + +describe('handleMessage', () => { + describe('NETWORK_EVENT', () => { + it('returns the event when URL is auth-related', async () => { + const result = await run( + handleMessage({ + type: 'NETWORK_EVENT', + payload: makeNetworkHarEntry('https://auth.example.com/authorize'), + }), + ); + + expect(result).not.toBeNull(); + const event = result as AuthEvent; + expect(event.type).toBe('network:response'); + expect(event.flags.isAuthRelated).toBe(true); + }); + + it('returns null when URL is not auth-related', async () => { + const result = await run( + handleMessage({ + type: 'NETWORK_EVENT', + payload: makeNetworkHarEntry('https://api.example.com/users'), + }), + ); + + expect(result).toBeNull(); + }); + + it('sets causedBy to the lastSdkEventId', async () => { + const program = Effect.gen(function* () { + // First, append an SDK event to set lastSdkEventId + yield* handleMessage({ type: 'SDK_EVENT', payload: makeSdkEvent({ id: 'sdk-42' }) }); + + // Then process a network event + return yield* handleMessage({ + type: 'NETWORK_EVENT', + payload: makeNetworkHarEntry('https://auth.example.com/davinci/flow'), + }); + }); + + const result = await run(program); + expect(result).not.toBeNull(); + expect((result as AuthEvent).causedBy).toBe('sdk-42'); + }); + }); + + describe('SDK_EVENT', () => { + it('accepts and stores a valid SDK event', async () => { + const event = makeSdkEvent(); + const result = await run(handleMessage({ type: 'SDK_EVENT', payload: event })); + + expect(result).not.toBeNull(); + expect((result as AuthEvent).id).toBe('sdk-1'); + }); + + it('returns null for a malformed SDK event', async () => { + const spy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + const result = await run(handleMessage({ type: 'SDK_EVENT', payload: { bad: 'data' } })); + + expect(result).toBeNull(); + expect(spy).toHaveBeenCalledOnce(); + spy.mockRestore(); + }); + + it('persists the event to the store', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* handleMessage({ type: 'SDK_EVENT', payload: makeSdkEvent() }); + return yield* store.getState(); + }); + + const state = await run(program); + expect(state.events).toHaveLength(1); + }); + }); + + describe('CLEAR', () => { + it('clears the event store', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* handleMessage({ type: 'SDK_EVENT', payload: makeSdkEvent() }); + yield* handleMessage({ type: 'CLEAR' }); + return yield* store.getState(); + }); + + const state = await run(program); + expect(state.events).toHaveLength(0); + }); + + it('returns null', async () => { + const result = await run(handleMessage({ type: 'CLEAR' })); + expect(result).toBeNull(); + }); + }); + + describe('GET_STATE', () => { + it('returns the current flow state', async () => { + const program = Effect.gen(function* () { + yield* handleMessage({ type: 'SDK_EVENT', payload: makeSdkEvent() }); + return yield* handleMessage({ type: 'GET_STATE' }); + }); + + const result = await run(program); + expect(result).toHaveProperty('events'); + expect((result as FlowState).events).toHaveLength(1); + }); + + it('returns empty state when nothing has been appended', async () => { + const result = await run(handleMessage({ type: 'GET_STATE' })); + expect(result).toHaveProperty('events'); + expect((result as FlowState).events).toHaveLength(0); + }); + }); +}); diff --git a/packages/devtools-extension/src/background/serialize-diagnosis.test.ts b/packages/devtools-extension/src/background/serialize-diagnosis.test.ts new file mode 100644 index 0000000000..e83a75f1c4 --- /dev/null +++ b/packages/devtools-extension/src/background/serialize-diagnosis.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; +import { serializeDiagnosis } from './serialize-diagnosis.js'; +import type { DiagnosisResult } from './diagnosis-engine.js'; + +describe('serializeDiagnosis', () => { + it('converts annotatedEvents Map to a plain object', () => { + const diagnosis: DiagnosisResult = { + issues: [], + annotatedEvents: new Map([ + [ + 'evt-1', + [ + { + severity: 'warning', + title: 'Expired JWT', + description: 'Token expired', + steps: ['Refresh'], + }, + ], + ], + ['evt-2', [{ severity: 'error', title: 'CORS', description: 'Blocked', steps: [] }]], + ]), + flowHealth: 'warning', + }; + + const result = serializeDiagnosis(diagnosis); + + expect(result.annotatedEvents).toEqual({ + 'evt-1': [ + { + severity: 'warning', + title: 'Expired JWT', + description: 'Token expired', + steps: ['Refresh'], + }, + ], + 'evt-2': [{ severity: 'error', title: 'CORS', description: 'Blocked', steps: [] }], + }); + }); + + it('preserves issues array as-is', () => { + const issues = [ + { + id: 'cors-1', + severity: 'error' as const, + category: 'cors' as const, + title: 'CORS Blocked', + description: 'Request blocked', + steps: ['Check headers'], + relatedEventIds: ['evt-1'], + dedupKey: 'cors:status-zero', + }, + ]; + + const diagnosis: DiagnosisResult = { + issues, + annotatedEvents: new Map(), + flowHealth: 'error', + }; + + const result = serializeDiagnosis(diagnosis); + expect(result.issues).toBe(issues); + }); + + it('preserves flowHealth value', () => { + const diagnosis: DiagnosisResult = { + issues: [], + annotatedEvents: new Map(), + flowHealth: 'healthy', + }; + + expect(serializeDiagnosis(diagnosis).flowHealth).toBe('healthy'); + }); + + it('handles empty annotatedEvents Map', () => { + const diagnosis: DiagnosisResult = { + issues: [], + annotatedEvents: new Map(), + flowHealth: 'healthy', + }; + + const result = serializeDiagnosis(diagnosis); + expect(result.annotatedEvents).toEqual({}); + }); + + it('handles multiple issues per event in annotatedEvents', () => { + const diagnosis: DiagnosisResult = { + issues: [], + annotatedEvents: new Map([ + [ + 'evt-1', + [ + { severity: 'warning', title: 'Issue 1', description: 'Desc 1', steps: [] }, + { severity: 'error', title: 'Issue 2', description: 'Desc 2', steps: ['Fix it'] }, + ], + ], + ]), + flowHealth: 'error', + }; + + const result = serializeDiagnosis(diagnosis); + expect(result.annotatedEvents['evt-1']).toHaveLength(2); + expect(result.annotatedEvents['evt-1'][0].title).toBe('Issue 1'); + expect(result.annotatedEvents['evt-1'][1].title).toBe('Issue 2'); + }); +}); diff --git a/packages/devtools-extension/src/panel/src/Types.elm b/packages/devtools-extension/src/panel/src/Types.elm index fea7e0281f..49c2e0928a 100644 --- a/packages/devtools-extension/src/panel/src/Types.elm +++ b/packages/devtools-extension/src/panel/src/Types.elm @@ -1,8 +1,12 @@ module Types exposing ( AuthEvent + , CanvasState + , CardId(..) , DiagnosisResult , EventData(..) , EventIssue + , EventKind(..) + , EventSource(..) , FlowHealth(..) , FlowIssue , ImportMeta @@ -10,17 +14,51 @@ module Types exposing , JourneyData , NetworkData , NodeData + , NodeStatus(..) , OidcData , SdkAuthorization , SdkError , SessionData + , Severity(..) , SnapshotMeta + , Vec2 , ViewMode(..) ) import Json.Decode as Decode +type Severity + = SevError + | SevWarning + | SevInfo + + +type NodeStatus + = Continue + | Success + | StatusError + | Failure + | UnknownStatus + + +type EventKind + = NodeChange + | JourneyStep + | OidcState + | SdkConfig + | NetworkEvent + | SessionEvent + | OtherKind String + + +type EventSource + = NetworkSource + | SdkSource + | SessionSource + | OtherSource String + + type alias SdkError = { code : String , message : String @@ -38,8 +76,8 @@ type alias SdkAuthorization = type alias AuthEvent = { id : String , timestamp : Float - , eventType : String - , source : String + , kind : EventKind + , source : EventSource , flowId : Maybe String , isCors : Bool , isError : Bool @@ -71,8 +109,8 @@ type alias NetworkData = type alias NodeData = - { nodeStatus : Maybe String - , previousStatus : Maybe String + { nodeStatus : Maybe NodeStatus + , previousStatus : Maybe NodeStatus , interactionId : Maybe String , interactionToken : Maybe String , nodeId : Maybe String @@ -138,7 +176,7 @@ type FlowHealth type alias EventIssue = - { severity : String + { severity : Severity , title : String , description : String , steps : List String @@ -148,7 +186,7 @@ type alias EventIssue = type alias FlowIssue = { id : String - , severity : String + , severity : Severity , category : String , title : String , description : String @@ -165,9 +203,35 @@ type alias DiagnosisResult = } +type CardId + = BrowserCard + | ServerCard + | SdkCard + | FormCard + + +type alias Vec2 = + { x : Float, y : Float } + + +type alias CanvasState = + { zoom : Float + , panX : Float + , panY : Float + , cardPositions : List ( String, Vec2 ) + , expandedCard : Maybe CardId + , dragTarget : Maybe CardId + , dragStart : Maybe Vec2 + , isPanning : Bool + , panStart : Maybe Vec2 + , learnSelectedNodeId : Maybe String + } + + type ViewMode = TimelineMode | FlowMode + | LearnMode type alias ImportMeta = diff --git a/packages/devtools-extension/src/panel/src/View.elm b/packages/devtools-extension/src/panel/src/View.elm index 869f9b78ab..5b817c9f15 100644 --- a/packages/devtools-extension/src/panel/src/View.elm +++ b/packages/devtools-extension/src/panel/src/View.elm @@ -9,7 +9,7 @@ import Html.Events exposing (onClick, onInput) import Inspector import Model exposing (Model) import Timeline -import Types exposing (AuthEvent, FlowHealth(..), FlowIssue, ImportMeta, SnapshotMeta, ViewMode(..)) +import Types exposing (AuthEvent, FlowHealth(..), FlowIssue, ImportMeta, Severity(..), SnapshotMeta, ViewMode(..)) import Update exposing (Msg(..)) @@ -57,6 +57,12 @@ view model = , div [ class "inspector-panel" ] [ Inspector.view selectedEvent model.activeTab model.diagnosis ] ] + + LearnMode -> + div [ class "fv-view" ] + [ div [ class "fv-detail fv-detail-empty" ] + [ text "Learn view coming soon" ] + ] ] @@ -239,11 +245,10 @@ viewFlowIssue issue = let ( issueClass, icon ) = case issue.severity of - "error" -> ( "fh-issue fh-issue-error", "✕ " ) - "warning" -> ( "fh-issue fh-issue-warning", "⚠ " ) - _ -> ( "fh-issue fh-issue-info", "ℹ " ) + SevError -> ( "fh-issue fh-issue-error", "✕ " ) + SevWarning -> ( "fh-issue fh-issue-warning", "⚠ " ) + SevInfo -> ( "fh-issue fh-issue-info", "ℹ " ) - -- Clicking the issue jumps to the first related event firstEventId = List.head issue.relatedEventIds in From 448f500f4de634f2de01ad0c84060617f4789cd5 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 13:35:24 -0600 Subject: [PATCH 3/8] feat(devtools-extension): add Learn tab with canvas-based request lifecycle view Add LearnView module with interactive canvas showing 4-card request lifecycle (Browser -> Server -> SDK -> Form) for each SDK node. Includes pan/zoom/drag canvas interactions, SVG icon cards with error visualization, and rail node selector. Wire into View toolbar as new Learn mode button alongside Timeline and Flow. --- .../src/panel/src/LearnView.elm | 775 +++++++++++++++ .../src/panel/src/Model.elm | 15 +- .../src/panel/src/Update.elm | 250 ++++- .../devtools-extension/src/panel/src/View.elm | 20 +- .../devtools-extension/tests/UpdateTests.elm | 916 ++++++++++++++++++ 5 files changed, 1967 insertions(+), 9 deletions(-) create mode 100644 packages/devtools-extension/src/panel/src/LearnView.elm create mode 100644 packages/devtools-extension/tests/UpdateTests.elm diff --git a/packages/devtools-extension/src/panel/src/LearnView.elm b/packages/devtools-extension/src/panel/src/LearnView.elm new file mode 100644 index 0000000000..5b637352b3 --- /dev/null +++ b/packages/devtools-extension/src/panel/src/LearnView.elm @@ -0,0 +1,775 @@ +module LearnView exposing (view) + +import Helpers +import Html exposing (Html) +import Html.Attributes exposing (..) +import Svg exposing (..) +import Svg.Attributes as SA +import Svg.Events +import Types exposing (AuthEvent, CanvasState, CardId(..), EventData(..), NetworkData, NodeData) +import Update exposing (Msg(..)) + + +view : List AuthEvent -> CanvasState -> Html Msg +view events canvas = + let + sdkNodes = + Helpers.sdkNodes events + in + Html.div [ class "lv-view" ] + [ viewRail sdkNodes canvas.learnSelectedNodeId + , viewCanvas events canvas + ] + + + +-- ── Rail ───────────────────────────────────────────────────────────────────── + + +nodeSpacing : Int +nodeSpacing = + 140 + + +nodeRadius : Int +nodeRadius = + 18 + + +railHeight : Int +railHeight = + 110 + + +viewRail : List AuthEvent -> Maybe String -> Html Msg +viewRail sdkNodes selectedNodeId = + let + count = + List.length sdkNodes + + svgWidth = + if count == 0 then + 200 + + else + count * nodeSpacing + 60 + in + Html.div [ class "lv-rail" ] + [ if List.isEmpty sdkNodes then + Html.div [ class "lv-rail-empty" ] [ Html.text "No SDK nodes recorded yet." ] + + else + Svg.svg + [ SA.width (String.fromInt svgWidth) + , SA.height (String.fromInt railHeight) + , SA.viewBox ("0 0 " ++ String.fromInt svgWidth ++ " " ++ String.fromInt railHeight) + , SA.style "display:block" + ] + (railDefs + :: List.concat (List.indexedMap (renderRailNode selectedNodeId) sdkNodes) + ++ List.concat (List.indexedMap (\i _ -> renderRailArrow (List.length sdkNodes) i) sdkNodes) + ) + ] + + +railDefs : Svg Msg +railDefs = + Svg.defs [] + [ Svg.filter [ SA.id "lv-glow" ] + [ Svg.feGaussianBlur [ SA.stdDeviation "4", SA.result "blur" ] [] + , Svg.feMerge [] + [ Svg.feMergeNode [ SA.in_ "blur" ] [] + , Svg.feMergeNode [ SA.in_ "SourceGraphic" ] [] + ] + ] + , Svg.marker + [ SA.id "lv-arrowhead" + , SA.markerWidth "8" + , SA.markerHeight "8" + , SA.refX "6" + , SA.refY "3" + , SA.orient "auto" + ] + [ Svg.polygon + [ SA.points "0 0, 8 3, 0 6" + , SA.fill "#30363D" + ] + [] + ] + ] + + +renderRailNode : Maybe String -> Int -> AuthEvent -> List (Svg Msg) +renderRailNode selectedNodeId index event = + let + cx_ = + index * nodeSpacing + 40 + + cy_ = + 44 + + color = + nodeColor event + + isSelected = + selectedNodeId == Just event.id + + label = + nodeLabel event + + glowRing = + if isSelected then + [ Svg.circle + [ SA.cx (String.fromInt cx_) + , SA.cy (String.fromInt cy_) + , SA.r (String.fromInt (nodeRadius + 6)) + , SA.fill "none" + , SA.stroke color + , SA.strokeWidth "2" + , SA.strokeOpacity "0.5" + , SA.filter "url(#lv-glow)" + ] + [] + ] + + else + [] + in + glowRing + ++ [ Svg.g + [ Svg.Events.onClick (LearnSelectNode event.id) + , SA.style "cursor:pointer" + ] + [ Svg.circle + [ SA.cx (String.fromInt cx_) + , SA.cy (String.fromInt cy_) + , SA.r (String.fromInt nodeRadius) + , SA.fill color + ] + [] + , Svg.text_ + [ SA.x (String.fromInt cx_) + , SA.y (String.fromInt (cy_ + nodeRadius + 14)) + , SA.textAnchor "middle" + , SA.fontSize "10" + , SA.fill "#8B949E" + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text (truncate_ 14 label) ] + ] + ] + + +renderRailArrow : Int -> Int -> List (Svg Msg) +renderRailArrow total index = + if index >= total - 1 then + [] + + else + let + x1_ = + index * nodeSpacing + 40 + nodeRadius + 16 + + x2_ = + (index + 1) * nodeSpacing + 40 - nodeRadius - 16 + + y_ = + 44 + in + [ Svg.line + [ SA.x1 (String.fromInt x1_) + , SA.y1 (String.fromInt y_) + , SA.x2 (String.fromInt x2_) + , SA.y2 (String.fromInt y_) + , SA.stroke "#30363D" + , SA.strokeWidth "1.5" + , SA.markerEnd "url(#lv-arrowhead)" + ] + [] + ] + + + +-- ── Canvas ─────────────────────────────────────────────────────────────────── + + +viewCanvas : List AuthEvent -> CanvasState -> Html Msg +viewCanvas events canvas = + case canvas.learnSelectedNodeId of + Nothing -> + Html.div [ class "lv-canvas lv-canvas-empty" ] + [ Html.text "Select a node above to see its request lifecycle." ] + + Just nodeId -> + let + maybeNode = + Helpers.findEventInList nodeId events + + netEvents = + List.filter (\e -> e.causedBy == Just nodeId) events + |> List.sortBy .timestamp + + requestEvent = + List.head netEvents + + responseEvent = + List.head (List.reverse netEvents) + + hasError = + case maybeNode of + Just n -> + n.isError + + Nothing -> + False + + hasCollectors = + case maybeNode of + Just n -> + case n.data of + DaVinciNode nd -> + nd.collectors /= Nothing && nd.collectors /= Just [] + + _ -> + False + + Nothing -> + False + + requestMethod = + case requestEvent of + Just re -> + case re.data of + Network nd -> + Maybe.withDefault "POST" nd.method + + _ -> + "POST" + + Nothing -> + "POST" + + responseStatus = + case responseEvent of + Just re -> + case re.data of + Network nd -> + Maybe.map String.fromInt nd.status + |> Maybe.withDefault "—" + + _ -> + "—" + + Nothing -> + "—" + + serverHasError = + case responseEvent of + Just re -> + case re.data of + Network nd -> + case nd.status of + Just s -> + s >= 400 + + Nothing -> + False + + _ -> + False + + Nothing -> + False + + noNetEvents = + List.isEmpty netEvents + + transform = + "translate(" + ++ String.fromFloat canvas.panX + ++ "," + ++ String.fromFloat canvas.panY + ++ ") scale(" + ++ String.fromFloat canvas.zoom + ++ ")" + in + Html.div [ class "lv-canvas" ] + [ Svg.svg + [ SA.class "lv-canvas-svg" + , SA.width "100%" + , SA.height "100%" + , Svg.Events.onMouseDown (LearnStartPan 0 0) + , Svg.Events.onMouseUp LearnEndDrag + ] + [ canvasDefs + , Svg.g [ SA.transform transform ] + (renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus) + ] + ] + + +canvasDefs : Svg Msg +canvasDefs = + Svg.defs [] + [ Svg.marker + [ SA.id "lv-card-arrow" + , SA.markerWidth "10" + , SA.markerHeight "7" + , SA.refX "9" + , SA.refY "3.5" + , SA.orient "auto" + ] + [ Svg.polygon + [ SA.points "0 0, 10 3.5, 0 7" + , SA.fill "#484F58" + ] + [] + ] + ] + + +renderCards : CanvasState -> Bool -> Bool -> Bool -> Bool -> String -> String -> List (Svg Msg) +renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus = + let + cardW = + 160 + + cardH = + 120 + + gap = + 80 + + startX = + 50 + + y = + 80 + + browserX = + startX + + serverX = + startX + cardW + gap + + sdkX = + startX + 2 * (cardW + gap) + + formX = + startX + 3 * (cardW + gap) + + getOffset key = + List.filter (\( k, _ ) -> k == key) canvas.cardPositions + |> List.head + |> Maybe.map Tuple.second + |> Maybe.withDefault { x = 0, y = 0 } + + browserOff = + getOffset "browser" + + serverOff = + getOffset "server" + + sdkOff = + getOffset "sdk" + + formOff = + getOffset "form" + + bx = + toFloat browserX + browserOff.x + + by = + toFloat y + browserOff.y + + sx = + toFloat serverX + serverOff.x + + sy = + toFloat y + serverOff.y + + sdx = + toFloat sdkX + sdkOff.x + + sdy = + toFloat y + sdkOff.y + + fx = + toFloat formX + formOff.x + + fy = + toFloat y + formOff.y + + fW = + toFloat cardW + + fH = + toFloat cardH + + browserBorder = + if hasError then + "#F85149" + + else + "#58A6FF" + + serverBorder = + if serverHasError then + "#F85149" + + else + "#484F58" + + sdkBorder = + if hasError then + "#F85149" + + else + "#3FB950" + + formBorder = + if not hasCollectors then + "#484F58" + + else + "#A371F7" + + formOpacity = + if hasCollectors then + "1" + + else + "0.4" + + formDash = + if hasCollectors then + "" + + else + "4,4" + in + [ -- Browser card + renderCard BrowserCard bx by fW fH browserBorder "1" "" canvas.expandedCard + (browserIcon (bx + 30) (by + 20)) + "BROWSER" + "User interaction" + + -- Arrow: Browser -> Server + , renderArrowLine (bx + fW) (by + fH / 2) sx (sy + fH / 2) (requestMethod ++ " ->") + + -- Server card + , renderCard ServerCard sx sy fW fH serverBorder "1" "" canvas.expandedCard + (serverIcon (sx + 40) (sy + 15)) + "SERVER" + ("Response " ++ responseStatus) + + -- Arrow: Server -> SDK + , renderArrowLine (sx + fW) (sy + fH / 2) sdx (sdy + fH / 2) ("-> " ++ responseStatus) + + -- SDK card + , renderCard SdkCard sdx sdy fW fH sdkBorder "1" "" canvas.expandedCard + (sdkIcon (sdx + 35) (sdy + 20)) + "SDK" + "Processes response" + + -- Arrow: SDK -> Form + , renderArrowLine (sdx + fW) (sdy + fH / 2) fx (fy + fH / 2) "renders" + + -- Form card + , renderCard FormCard fx fy fW fH formBorder formOpacity formDash canvas.expandedCard + (formIcon (fx + 35) (fy + 18)) + "FORM" + (if hasCollectors then + "Collects input" + + else + "Skipped" + ) + + -- Error pulse on source card when error + ] + ++ (if hasError then + [ Svg.circle + [ SA.cx (String.fromFloat (sdx + fW / 2)) + , SA.cy (String.fromFloat (sdy + fH / 2)) + , SA.r (String.fromFloat (fW / 2 + 8)) + , SA.fill "none" + , SA.stroke "#F85149" + , SA.strokeWidth "2" + , SA.strokeOpacity "0.6" + , SA.class "lv-pulse" + ] + [] + ] + + else + [] + ) + ++ (if noNetEvents then + [ Svg.text_ + [ SA.x (String.fromFloat (bx + fW + toFloat gap / 2)) + , SA.y (String.fromFloat (by + fH + 30)) + , SA.textAnchor "middle" + , SA.fontSize "11" + , SA.fill "#484F58" + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text "No network events captured" ] + ] + + else + [] + ) + + +renderCard : CardId -> Float -> Float -> Float -> Float -> String -> String -> String -> Maybe CardId -> Svg Msg -> String -> String -> Svg Msg +renderCard cardId x y w h borderColor opacity dashArray expandedCard icon label contextLine = + let + isExpanded = + expandedCard == Just cardId + in + Svg.g + [ Svg.Events.onClick (LearnExpandCard cardId) + , SA.style "cursor:pointer" + , SA.opacity opacity + ] + [ Svg.rect + [ SA.x (String.fromFloat x) + , SA.y (String.fromFloat y) + , SA.width (String.fromFloat w) + , SA.height (String.fromFloat h) + , SA.rx "8" + , SA.ry "8" + , SA.fill "#161B22" + , SA.stroke borderColor + , SA.strokeWidth + (if isExpanded then + "3" + + else + "1.5" + ) + , SA.strokeDasharray dashArray + ] + [] + , icon + , Svg.text_ + [ SA.x (String.fromFloat (x + w / 2)) + , SA.y (String.fromFloat (y + h - 22)) + , SA.textAnchor "middle" + , SA.fontSize "11" + , SA.fontWeight "bold" + , SA.fill "#E6EDF3" + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text label ] + , Svg.text_ + [ SA.x (String.fromFloat (x + w / 2)) + , SA.y (String.fromFloat (y + h - 8)) + , SA.textAnchor "middle" + , SA.fontSize "9" + , SA.fill "#8B949E" + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text contextLine ] + ] + + +renderArrowLine : Float -> Float -> Float -> Float -> String -> Svg Msg +renderArrowLine x1 y1 x2 y2 label = + let + midX = + (x1 + x2) / 2 + + midY = + (y1 + y2) / 2 - 10 + in + Svg.g [] + [ Svg.line + [ SA.x1 (String.fromFloat x1) + , SA.y1 (String.fromFloat y1) + , SA.x2 (String.fromFloat x2) + , SA.y2 (String.fromFloat y2) + , SA.stroke "#484F58" + , SA.strokeWidth "1.5" + , SA.markerEnd "url(#lv-card-arrow)" + ] + [] + , Svg.text_ + [ SA.x (String.fromFloat midX) + , SA.y (String.fromFloat midY) + , SA.textAnchor "middle" + , SA.fontSize "9" + , SA.fill "#8B949E" + , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + ] + [ Svg.text label ] + ] + + + +-- ── Icons ──────────────────────────────────────────────────────────────────── + + +browserIcon : Float -> Float -> Svg Msg +browserIcon x y = + Svg.g [] + [ -- Window frame + Svg.rect + [ SA.x (String.fromFloat x) + , SA.y (String.fromFloat y) + , SA.width "100" + , SA.height "60" + , SA.rx "4" + , SA.fill "none" + , SA.stroke "#58A6FF" + , SA.strokeWidth "1.5" + ] + [] + + -- Title bar + , Svg.line + [ SA.x1 (String.fromFloat x) + , SA.y1 (String.fromFloat (y + 14)) + , SA.x2 (String.fromFloat (x + 100)) + , SA.y2 (String.fromFloat (y + 14)) + , SA.stroke "#58A6FF" + , SA.strokeWidth "1" + ] + [] + + -- Traffic lights + , Svg.circle [ SA.cx (String.fromFloat (x + 8)), SA.cy (String.fromFloat (y + 7)), SA.r "2.5", SA.fill "#F85149" ] [] + , Svg.circle [ SA.cx (String.fromFloat (x + 16)), SA.cy (String.fromFloat (y + 7)), SA.r "2.5", SA.fill "#D29922" ] [] + , Svg.circle [ SA.cx (String.fromFloat (x + 24)), SA.cy (String.fromFloat (y + 7)), SA.r "2.5", SA.fill "#3FB950" ] [] + + -- Globe in content area + , Svg.circle [ SA.cx (String.fromFloat (x + 50)), SA.cy (String.fromFloat (y + 38)), SA.r "12", SA.fill "none", SA.stroke "#58A6FF", SA.strokeWidth "1" ] [] + , Svg.ellipse [ SA.cx (String.fromFloat (x + 50)), SA.cy (String.fromFloat (y + 38)), SA.rx "5", SA.ry "12", SA.fill "none", SA.stroke "#58A6FF", SA.strokeWidth "0.7" ] [] + , Svg.line [ SA.x1 (String.fromFloat (x + 38)), SA.y1 (String.fromFloat (y + 38)), SA.x2 (String.fromFloat (x + 62)), SA.y2 (String.fromFloat (y + 38)), SA.stroke "#58A6FF", SA.strokeWidth "0.7" ] [] + ] + + +serverIcon : Float -> Float -> Svg Msg +serverIcon x y = + Svg.g [] + [ -- Cloud shape (simplified) + Svg.ellipse + [ SA.cx (String.fromFloat (x + 40)) + , SA.cy (String.fromFloat (y + 30)) + , SA.rx "38" + , SA.ry "22" + , SA.fill "none" + , SA.stroke "#484F58" + , SA.strokeWidth "1.5" + ] + [] + + -- LED dots + , Svg.circle [ SA.cx (String.fromFloat (x + 28)), SA.cy (String.fromFloat (y + 30)), SA.r "3", SA.fill "#3FB950" ] [] + , Svg.circle [ SA.cx (String.fromFloat (x + 40)), SA.cy (String.fromFloat (y + 30)), SA.r "3", SA.fill "#3FB950" ] [] + , Svg.circle [ SA.cx (String.fromFloat (x + 52)), SA.cy (String.fromFloat (y + 30)), SA.r "3", SA.fill "#58A6FF" ] [] + ] + + +sdkIcon : Float -> Float -> Svg Msg +sdkIcon x y = + Svg.g [] + [ -- Window frame + Svg.rect + [ SA.x (String.fromFloat x) + , SA.y (String.fromFloat y) + , SA.width "90" + , SA.height "55" + , SA.rx "4" + , SA.fill "none" + , SA.stroke "#3FB950" + , SA.strokeWidth "1.5" + ] + [] + + -- Gear icon + , Svg.circle [ SA.cx (String.fromFloat (x + 45)), SA.cy (String.fromFloat (y + 30)), SA.r "10", SA.fill "none", SA.stroke "#3FB950", SA.strokeWidth "1.5" ] [] + , Svg.circle [ SA.cx (String.fromFloat (x + 45)), SA.cy (String.fromFloat (y + 30)), SA.r "4", SA.fill "#3FB950" ] [] + ] + + +formIcon : Float -> Float -> Svg Msg +formIcon x y = + Svg.g [] + [ -- Form outline + Svg.rect + [ SA.x (String.fromFloat x) + , SA.y (String.fromFloat y) + , SA.width "90" + , SA.height "60" + , SA.rx "4" + , SA.fill "none" + , SA.stroke "#A371F7" + , SA.strokeWidth "1.5" + ] + [] + + -- Input field 1 + , Svg.rect [ SA.x (String.fromFloat (x + 10)), SA.y (String.fromFloat (y + 10)), SA.width "70", SA.height "12", SA.rx "2", SA.fill "none", SA.stroke "#484F58", SA.strokeWidth "1" ] [] + + -- Input field 2 + , Svg.rect [ SA.x (String.fromFloat (x + 10)), SA.y (String.fromFloat (y + 28)), SA.width "70", SA.height "12", SA.rx "2", SA.fill "none", SA.stroke "#484F58", SA.strokeWidth "1" ] [] + + -- Submit button + , Svg.rect [ SA.x (String.fromFloat (x + 25)), SA.y (String.fromFloat (y + 46)), SA.width "40", SA.height "10", SA.rx "2", SA.fill "#A371F7", SA.fillOpacity "0.3", SA.stroke "#A371F7", SA.strokeWidth "1" ] [] + ] + + + +-- ── Helpers ────────────────────────────────────────────────────────────────── + + +nodeColor : AuthEvent -> String +nodeColor event = + case event.data of + DaVinciNode node -> + Helpers.nodeColor (Maybe.withDefault Types.UnknownStatus node.nodeStatus) + + _ -> + "#484F58" + + +nodeLabel : AuthEvent -> String +nodeLabel event = + case event.data of + DaVinciNode node -> + node.nodeName + |> orMaybe node.eventName + |> Maybe.withDefault "—" + + Journey journey -> + journey.stage + |> orMaybe journey.header + |> orMaybe journey.stepType + |> Maybe.withDefault "—" + + Oidc oidc -> + Maybe.withDefault "oidc" oidc.phase + + _ -> + "—" + + +orMaybe : Maybe a -> Maybe a -> Maybe a +orMaybe fallback primary = + case primary of + Just _ -> + primary + + Nothing -> + fallback + + +truncate_ : Int -> String -> String +truncate_ maxLen s = + if String.length s <= maxLen then + s + + else + String.left maxLen s ++ "…" diff --git a/packages/devtools-extension/src/panel/src/Model.elm b/packages/devtools-extension/src/panel/src/Model.elm index 6ba3df5ecc..7beefe57b1 100644 --- a/packages/devtools-extension/src/panel/src/Model.elm +++ b/packages/devtools-extension/src/panel/src/Model.elm @@ -2,7 +2,7 @@ module Model exposing (Model, init) import Dict exposing (Dict) import Set exposing (Set) -import Types exposing (AuthEvent, DiagnosisResult, ImportMeta, InspectorTab(..), SnapshotMeta, ViewMode(..)) +import Types exposing (AuthEvent, CanvasState, DiagnosisResult, ImportMeta, InspectorTab(..), SnapshotMeta, ViewMode(..)) type alias Model = @@ -27,6 +27,7 @@ type alias Model = , hoveredNodeId : Maybe String , snapshotMenuOpen : Bool , snapshots : List SnapshotMeta + , learnCanvas : CanvasState } @@ -53,6 +54,18 @@ init _ = , hoveredNodeId = Nothing , snapshotMenuOpen = False , snapshots = [] + , learnCanvas = + { zoom = 1.0 + , panX = 0.0 + , panY = 0.0 + , cardPositions = [] + , expandedCard = Nothing + , dragTarget = Nothing + , dragStart = Nothing + , isPanning = False + , panStart = Nothing + , learnSelectedNodeId = Nothing + } } , Cmd.none ) diff --git a/packages/devtools-extension/src/panel/src/Update.elm b/packages/devtools-extension/src/panel/src/Update.elm index f1a4fecd2e..5df0f0cca9 100644 --- a/packages/devtools-extension/src/panel/src/Update.elm +++ b/packages/devtools-extension/src/panel/src/Update.elm @@ -4,7 +4,7 @@ import Dict import Helpers import Model exposing (Model) import Set -import Types exposing (AuthEvent, DiagnosisResult, FlowHealth(..), ImportMeta, InspectorTab(..), SnapshotMeta, ViewMode(..)) +import Types exposing (AuthEvent, CanvasState, CardId(..), DiagnosisResult, EventData(..), EventKind(..), EventSource(..), FlowHealth(..), ImportMeta, InspectorTab(..), SnapshotMeta, Vec2, ViewMode(..)) type Msg @@ -42,6 +42,16 @@ type Msg | SnapshotsReceived (List SnapshotMeta) | LoadSnapshot String | DeleteSnapshot String + | LearnSelectNode String + | LearnExpandCard CardId + | LearnCollapseCard + | LearnStartDrag CardId Float Float + | LearnDrag Float Float + | LearnEndDrag + | LearnStartPan Float Float + | LearnPan Float Float + | LearnEndPan + | LearnZoom Float update : Msg -> Model -> ( Model, Cmd Msg ) @@ -74,19 +84,19 @@ update msg model = newTab = case ( model.activeTab, selectedEvent ) of ( CollectorsTab, Just e ) -> - if Helpers.eventType e /= Helpers.NodeChange then + if e.kind /= NodeChange then HeadersTab else CollectorsTab ( SessionTab, Just e ) -> - if Helpers.eventSource e /= Helpers.SessionSource then + if e.source /= SessionSource then HeadersTab else SessionTab ( ConfigTab, Just e ) -> - if Helpers.eventType e /= Helpers.SdkConfig then + if e.kind /= SdkConfig then HeadersTab else ConfigTab @@ -313,3 +323,235 @@ update msg model = } , Cmd.none ) + + LearnSelectNode nodeId -> + let + canvas = + model.learnCanvas + in + ( { model + | learnCanvas = + { canvas + | learnSelectedNodeId = Just nodeId + , expandedCard = Nothing + , cardPositions = [] + } + } + , Cmd.none + ) + + LearnExpandCard cardId -> + let + canvas = + model.learnCanvas + + newExpanded = + if canvas.expandedCard == Just cardId then + Nothing + + else + Just cardId + in + ( { model | learnCanvas = { canvas | expandedCard = newExpanded } } + , Cmd.none + ) + + LearnCollapseCard -> + let + canvas = + model.learnCanvas + in + ( { model | learnCanvas = { canvas | expandedCard = Nothing } } + , Cmd.none + ) + + LearnStartDrag cardId mx my -> + let + canvas = + model.learnCanvas + in + ( { model + | learnCanvas = + { canvas + | dragTarget = Just cardId + , dragStart = Just (Vec2 mx my) + } + } + , Cmd.none + ) + + LearnDrag mx my -> + let + canvas = + model.learnCanvas + in + case canvas.dragTarget of + Just cardId -> + case canvas.dragStart of + Just start -> + let + dx = + (mx - start.x) / canvas.zoom + + dy = + (my - start.y) / canvas.zoom + + key = + cardIdToString cardId + + existing = + List.filter (\( k, _ ) -> k == key) canvas.cardPositions + |> List.head + |> Maybe.map Tuple.second + |> Maybe.withDefault (Vec2 0 0) + + newPos = + Vec2 (existing.x + dx) (existing.y + dy) + + newPositions = + ( key, newPos ) + :: List.filter (\( k, _ ) -> k /= key) canvas.cardPositions + in + ( { model + | learnCanvas = + { canvas + | cardPositions = newPositions + , dragStart = Just (Vec2 mx my) + } + } + , Cmd.none + ) + + Nothing -> + ( model, Cmd.none ) + + Nothing -> + if canvas.isPanning then + case canvas.panStart of + Just start -> + let + dx = + mx - start.x + + dy = + my - start.y + in + ( { model + | learnCanvas = + { canvas + | panX = canvas.panX + dx + , panY = canvas.panY + dy + , panStart = Just (Vec2 mx my) + } + } + , Cmd.none + ) + + Nothing -> + ( model, Cmd.none ) + + else + ( model, Cmd.none ) + + LearnEndDrag -> + let + canvas = + model.learnCanvas + in + ( { model + | learnCanvas = + { canvas + | dragTarget = Nothing + , dragStart = Nothing + , isPanning = False + , panStart = Nothing + } + } + , Cmd.none + ) + + LearnStartPan mx my -> + let + canvas = + model.learnCanvas + in + ( { model + | learnCanvas = + { canvas + | isPanning = True + , panStart = Just (Vec2 mx my) + } + } + , Cmd.none + ) + + LearnPan mx my -> + let + canvas = + model.learnCanvas + in + case canvas.panStart of + Just start -> + let + dx = + mx - start.x + + dy = + my - start.y + in + ( { model + | learnCanvas = + { canvas + | panX = canvas.panX + dx + , panY = canvas.panY + dy + , panStart = Just (Vec2 mx my) + } + } + , Cmd.none + ) + + Nothing -> + ( model, Cmd.none ) + + LearnEndPan -> + let + canvas = + model.learnCanvas + in + ( { model + | learnCanvas = + { canvas + | isPanning = False + , panStart = Nothing + } + } + , Cmd.none + ) + + LearnZoom delta -> + let + canvas = + model.learnCanvas + + newZoom = + clamp 0.5 3.0 (canvas.zoom + delta * 0.001) + in + ( { model | learnCanvas = { canvas | zoom = newZoom } } + , Cmd.none + ) + + +cardIdToString : CardId -> String +cardIdToString cardId = + case cardId of + BrowserCard -> + "browser" + + ServerCard -> + "server" + + SdkCard -> + "sdk" + + FormCard -> + "form" diff --git a/packages/devtools-extension/src/panel/src/View.elm b/packages/devtools-extension/src/panel/src/View.elm index 5b817c9f15..70ce2537d7 100644 --- a/packages/devtools-extension/src/panel/src/View.elm +++ b/packages/devtools-extension/src/panel/src/View.elm @@ -3,6 +3,7 @@ module View exposing (view) import FlowView import Graph import Helpers +import LearnView import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick, onInput) @@ -59,10 +60,7 @@ view model = ] LearnMode -> - div [ class "fv-view" ] - [ div [ class "fv-detail fv-detail-empty" ] - [ text "Learn view coming soon" ] - ] + LearnView.view model.events model.learnCanvas ] @@ -316,10 +314,24 @@ viewToolbar model eventCount = ) ] [ text "Flow" ] + , button + [ onClick (SwitchViewMode LearnMode) + , class + (if model.viewMode == LearnMode then + "tb-btn tb-mode-btn active" + + else + "tb-btn tb-mode-btn" + ) + ] + [ text "Learn" ] , div [ class "tb-spacer" ] [] , if model.viewMode == FlowMode then FlowView.viewPlaybackControls model.events model.playbackIndex model.isPlaying + else if model.viewMode == LearnMode then + text "" + else if eventCount > 0 then span [ class "event-count" ] [ text (String.fromInt eventCount ++ " events") ] diff --git a/packages/devtools-extension/tests/UpdateTests.elm b/packages/devtools-extension/tests/UpdateTests.elm new file mode 100644 index 0000000000..044517342c --- /dev/null +++ b/packages/devtools-extension/tests/UpdateTests.elm @@ -0,0 +1,916 @@ +module UpdateTests exposing (suite) + +import Dict +import Expect +import Model exposing (Model, init) +import Set +import Test exposing (Test, describe, test) +import Types exposing (AuthEvent, CardId(..), DiagnosisResult, EventData(..), EventKind(..), EventSource(..), FlowHealth(..), ImportMeta, InspectorTab(..), NetworkData, NodeData, NodeStatus(..), SnapshotMeta, ViewMode(..)) +import Update exposing (Msg(..), update) + + +initModel : Model +initModel = + Tuple.first (init ()) + + +makeSdkEvent : String -> Float -> AuthEvent +makeSdkEvent id ts = + { id = id + , timestamp = ts + , kind = NodeChange + , source = SdkSource + , flowId = Just "flow-1" + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = DaVinciNode (NodeData (Just Continue) Nothing Nothing Nothing Nothing Nothing (Just "Username") Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing) + } + + +makeNetworkEvent : String -> AuthEvent +makeNetworkEvent id = + { id = id + , timestamp = 100 + , kind = NetworkEvent + , source = NetworkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Network (NetworkData (Just 200) (Just "https://x.com") (Just "GET") (Just 50) Nothing Nothing Nothing Nothing) + } + + +suite : Test +suite = + describe "Update" + [ eventReceivedTests + , selectEventTests + , selectEventTabTests + , toggleTests + , clearFlowTests + , diagnosisTests + , playbackTests + , startPlaybackEdgeTests + , snapshotTests + , viewModeTests + , importTests + , flowNodeTests + , learnSelectTests + , learnDragTests + , learnZoomTests + ] + + +eventReceivedTests : Test +eventReceivedTests = + describe "EventReceived" + [ test "appends event to events list" <| + \_ -> + let + event = + makeSdkEvent "e1" 100 + + ( model, _ ) = + update (EventReceived event) initModel + in + Expect.equal 1 (List.length model.events) + , test "inserts event into eventsById" <| + \_ -> + let + event = + makeSdkEvent "e1" 100 + + ( model, _ ) = + update (EventReceived event) initModel + in + Expect.equal (Just event) (Dict.get "e1" model.eventsById) + , test "sets flowId from first event" <| + \_ -> + let + event = + makeSdkEvent "e1" 100 + + ( model, _ ) = + update (EventReceived event) initModel + in + Expect.equal (Just "flow-1") model.flowId + , test "does not overwrite existing flowId" <| + \_ -> + let + modelWithFlow = + { initModel | flowId = Just "existing-flow" } + + event = + makeSdkEvent "e1" 100 + + ( model, _ ) = + update (EventReceived event) modelWithFlow + in + Expect.equal (Just "existing-flow") model.flowId + , test "ignores events when importedFlow is set" <| + \_ -> + let + importedModel = + { initModel | importedFlow = Just (ImportMeta (Just "flow-1") "2026-01-01" True) } + + event = + makeSdkEvent "e1" 100 + + ( model, _ ) = + update (EventReceived event) importedModel + in + Expect.equal 0 (List.length model.events) + ] + + +selectEventTests : Test +selectEventTests = + describe "SelectEvent" + [ test "sets selectedEventId" <| + \_ -> + let + ( model, _ ) = + update (SelectEvent "e1") initModel + in + Expect.equal (Just "e1") model.selectedEventId + , test "switches from CollectorsTab to HeadersTab for non-NodeChange" <| + \_ -> + let + event = + makeNetworkEvent "e1" + + modelWithCollectors = + { initModel + | activeTab = CollectorsTab + , eventsById = Dict.singleton "e1" event + } + + ( model, _ ) = + update (SelectEvent "e1") modelWithCollectors + in + Expect.equal HeadersTab model.activeTab + , test "keeps CollectorsTab for NodeChange event" <| + \_ -> + let + event = + makeSdkEvent "e1" 100 + + modelWithCollectors = + { initModel + | activeTab = CollectorsTab + , eventsById = Dict.singleton "e1" event + } + + ( model, _ ) = + update (SelectEvent "e1") modelWithCollectors + in + Expect.equal CollectorsTab model.activeTab + , test "switches from SessionTab to HeadersTab for non-session event" <| + \_ -> + let + event = + makeNetworkEvent "e1" + + modelWithSession = + { initModel + | activeTab = SessionTab + , eventsById = Dict.singleton "e1" event + } + + ( model, _ ) = + update (SelectEvent "e1") modelWithSession + in + Expect.equal HeadersTab model.activeTab + ] + + +toggleTests : Test +toggleTests = + describe "Toggle actions" + [ test "ToggleRecording flips recording flag" <| + \_ -> + let + ( model, _ ) = + update ToggleRecording initModel + in + Expect.equal False model.recording + , test "ToggleExportMenu flips export menu" <| + \_ -> + let + ( model, _ ) = + update ToggleExportMenu initModel + in + Expect.equal True model.exportMenuOpen + , test "CloseExportMenu closes export menu" <| + \_ -> + let + ( model, _ ) = + update CloseExportMenu { initModel | exportMenuOpen = True } + in + Expect.equal False model.exportMenuOpen + , test "ToggleSummary flips summaryCollapsed" <| + \_ -> + let + ( model, _ ) = + update ToggleSummary initModel + in + Expect.equal True model.summaryCollapsed + , test "SwitchTab changes active tab" <| + \_ -> + let + ( model, _ ) = + update (SwitchTab CorsTab) initModel + in + Expect.equal CorsTab model.activeTab + ] + + +clearFlowTests : Test +clearFlowTests = + describe "ClearFlow" + [ test "resets all model state" <| + \_ -> + let + dirtyModel = + { initModel + | events = [ makeSdkEvent "e1" 100 ] + , selectedEventId = Just "e1" + , flowId = Just "flow-1" + , isPlaying = True + , exportMenuOpen = True + } + + ( model, _ ) = + update ClearFlow dirtyModel + in + Expect.all + [ \m -> Expect.equal [] m.events + , \m -> Expect.equal Nothing m.selectedEventId + , \m -> Expect.equal Nothing m.flowId + , \m -> Expect.equal False m.isPlaying + , \m -> Expect.equal False m.exportMenuOpen + , \m -> Expect.equal True m.recording + , \m -> Expect.equal Dict.empty m.eventsById + ] + model + ] + + +diagnosisTests : Test +diagnosisTests = + describe "DiagnosisReceived" + [ test "stores diagnosis result" <| + \_ -> + let + diag = + DiagnosisResult Healthy [] [] + + ( model, _ ) = + update (DiagnosisReceived diag) initModel + in + Expect.equal (Just diag) model.diagnosis + , test "auto-expands summary on error when recording and collapsed" <| + \_ -> + let + diag = + DiagnosisResult Error [] [] + + collapsedModel = + { initModel | summaryCollapsed = True, recording = True } + + ( model, _ ) = + update (DiagnosisReceived diag) collapsedModel + in + Expect.equal False model.summaryCollapsed + , test "does not auto-expand when not recording" <| + \_ -> + let + diag = + DiagnosisResult Error [] [] + + notRecording = + { initModel | summaryCollapsed = True, recording = False } + + ( model, _ ) = + update (DiagnosisReceived diag) notRecording + in + Expect.equal True model.summaryCollapsed + , test "does not auto-expand for healthy diagnosis" <| + \_ -> + let + diag = + DiagnosisResult Healthy [] [] + + collapsedModel = + { initModel | summaryCollapsed = True, recording = True } + + ( model, _ ) = + update (DiagnosisReceived diag) collapsedModel + in + Expect.equal True model.summaryCollapsed + ] + + +playbackTests : Test +playbackTests = + describe "Playback" + [ test "StartPlayback sets isPlaying and playbackIndex" <| + \_ -> + let + modelWithEvents = + { initModel | events = [ makeSdkEvent "s1" 100, makeSdkEvent "s2" 200 ] } + + ( model, _ ) = + update StartPlayback modelWithEvents + in + Expect.all + [ \m -> Expect.equal True m.isPlaying + , \m -> Expect.equal (Just 0) m.playbackIndex + , \m -> Expect.equal (Just "s1") m.selectedNodeId + ] + model + , test "StopPlayback stops playing" <| + \_ -> + let + ( model, _ ) = + update StopPlayback { initModel | isPlaying = True } + in + Expect.equal False model.isPlaying + , test "PlaybackTick advances to next node" <| + \_ -> + let + modelPlaying = + { initModel + | events = [ makeSdkEvent "s1" 100, makeSdkEvent "s2" 200 ] + , isPlaying = True + , playbackIndex = Just 0 + } + + ( model, _ ) = + update PlaybackTick modelPlaying + in + Expect.all + [ \m -> Expect.equal (Just 1) m.playbackIndex + , \m -> Expect.equal (Just "s2") m.selectedNodeId + ] + model + , test "PlaybackTick stops at end of nodes" <| + \_ -> + let + modelAtEnd = + { initModel + | events = [ makeSdkEvent "s1" 100 ] + , isPlaying = True + , playbackIndex = Just 0 + } + + ( model, _ ) = + update PlaybackTick modelAtEnd + in + Expect.all + [ \m -> Expect.equal False m.isPlaying + , \m -> Expect.equal Nothing m.playbackIndex + ] + model + , test "PlaybackTick is no-op when not playing" <| + \_ -> + let + ( model, _ ) = + update PlaybackTick initModel + in + Expect.equal initModel model + , test "ResetPlayback clears playback state" <| + \_ -> + let + playing = + { initModel | isPlaying = True, playbackIndex = Just 2, selectedNodeId = Just "s3" } + + ( model, _ ) = + update ResetPlayback playing + in + Expect.all + [ \m -> Expect.equal False m.isPlaying + , \m -> Expect.equal Nothing m.playbackIndex + , \m -> Expect.equal Nothing m.selectedNodeId + ] + model + ] + + +snapshotTests : Test +snapshotTests = + describe "Snapshots" + [ test "SnapshotsReceived stores snapshots" <| + \_ -> + let + snaps = + [ SnapshotMeta "s1" "2026-01-01" (Just "f1") 5 ] + + ( model, _ ) = + update (SnapshotsReceived snaps) initModel + in + Expect.equal snaps model.snapshots + , test "DeleteSnapshot removes by id" <| + \_ -> + let + snaps = + [ SnapshotMeta "s1" "2026-01-01" Nothing 3 + , SnapshotMeta "s2" "2026-01-02" Nothing 5 + ] + + modelWithSnaps = + { initModel | snapshots = snaps } + + ( model, _ ) = + update (DeleteSnapshot "s1") modelWithSnaps + in + Expect.equal [ SnapshotMeta "s2" "2026-01-02" Nothing 5 ] model.snapshots + , test "ToggleSnapshotMenu flips snapshot menu" <| + \_ -> + let + ( model, _ ) = + update ToggleSnapshotMenu initModel + in + Expect.equal True model.snapshotMenuOpen + , test "CloseSnapshotMenu closes snapshot menu" <| + \_ -> + let + ( model, _ ) = + update CloseSnapshotMenu { initModel | snapshotMenuOpen = True } + in + Expect.equal False model.snapshotMenuOpen + ] + + +viewModeTests : Test +viewModeTests = + describe "SwitchViewMode" + [ test "switches to FlowMode and resets playback" <| + \_ -> + let + ( model, _ ) = + update (SwitchViewMode FlowMode) { initModel | isPlaying = True, playbackIndex = Just 2 } + in + Expect.all + [ \m -> Expect.equal FlowMode m.viewMode + , \m -> Expect.equal False m.isPlaying + , \m -> Expect.equal Nothing m.playbackIndex + , \m -> Expect.equal Nothing m.selectedNodeId + ] + model + ] + + +importTests : Test +importTests = + describe "Import" + [ test "ImportFlow opens paste dialog and clears error" <| + \_ -> + let + ( model, _ ) = + update ImportFlow { initModel | lastDecodeError = Just "old error" } + in + Expect.all + [ \m -> Expect.equal True m.importPasteOpen + , \m -> Expect.equal "" m.importPasteText + , \m -> Expect.equal Nothing m.lastDecodeError + ] + model + , test "UpdateImportPaste stores text" <| + \_ -> + let + ( model, _ ) = + update (UpdateImportPaste "some json") { initModel | importPasteOpen = True } + in + Expect.equal "some json" model.importPasteText + , test "SubmitImportPaste closes dialog and clears text" <| + \_ -> + let + ( model, _ ) = + update SubmitImportPaste { initModel | importPasteOpen = True, importPasteText = "data" } + in + Expect.all + [ \m -> Expect.equal False m.importPasteOpen + , \m -> Expect.equal "" m.importPasteText + ] + model + , test "CancelImportPaste closes dialog and clears text" <| + \_ -> + let + ( model, _ ) = + update CancelImportPaste { initModel | importPasteOpen = True, importPasteText = "data" } + in + Expect.all + [ \m -> Expect.equal False m.importPasteOpen + , \m -> Expect.equal "" m.importPasteText + ] + model + , test "ImportMetaReceived stores meta and stops recording" <| + \_ -> + let + meta = + ImportMeta (Just "flow-1") "2026-01-01" True + + ( model, _ ) = + update (ImportMetaReceived meta) initModel + in + Expect.all + [ \m -> Expect.equal (Just meta) m.importedFlow + , \m -> Expect.equal False m.recording + ] + model + , test "ImportError stores error message" <| + \_ -> + let + ( model, _ ) = + update (ImportError "Bad JSON") initModel + in + Expect.equal (Just "Bad JSON") model.lastDecodeError + , test "DecodeError stores error message" <| + \_ -> + let + ( model, _ ) = + update (DecodeError "Parse failed") initModel + in + Expect.equal (Just "Parse failed") model.lastDecodeError + ] + + +flowNodeTests : Test +flowNodeTests = + describe "Flow node interactions" + [ test "SelectFlowNode sets selectedNodeId and clears sub-rows" <| + \_ -> + let + ( model, _ ) = + update (SelectFlowNode "node-1") { initModel | expandedSubRows = Set.fromList [ "row-1" ] } + in + Expect.all + [ \m -> Expect.equal (Just "node-1") m.selectedNodeId + , \m -> Expect.equal Set.empty m.expandedSubRows + ] + model + , test "ToggleSubRow adds key to expanded set" <| + \_ -> + let + ( model, _ ) = + update (ToggleSubRow "row-1") initModel + in + Expect.equal True (Set.member "row-1" model.expandedSubRows) + , test "ToggleSubRow removes key if already expanded" <| + \_ -> + let + ( model, _ ) = + update (ToggleSubRow "row-1") { initModel | expandedSubRows = Set.singleton "row-1" } + in + Expect.equal False (Set.member "row-1" model.expandedSubRows) + , test "HoverNode sets hoveredNodeId" <| + \_ -> + let + ( model, _ ) = + update (HoverNode (Just "n1")) initModel + in + Expect.equal (Just "n1") model.hoveredNodeId + , test "HoverNode clears hoveredNodeId" <| + \_ -> + let + ( model, _ ) = + update (HoverNode Nothing) { initModel | hoveredNodeId = Just "n1" } + in + Expect.equal Nothing model.hoveredNodeId + , test "CopyToClipboard is a no-op on model" <| + \_ -> + let + ( model, _ ) = + update (CopyToClipboard "some text") initModel + in + Expect.equal initModel model + , test "SelectNode sets selectedEventId and switches to SdkStateTab" <| + \_ -> + let + ( model, _ ) = + update (SelectNode "e1") initModel + in + Expect.all + [ \m -> Expect.equal (Just "e1") m.selectedEventId + , \m -> Expect.equal SdkStateTab m.activeTab + ] + model + , test "ExportJson closes export menu" <| + \_ -> + let + ( model, _ ) = + update ExportJson { initModel | exportMenuOpen = True } + in + Expect.equal False model.exportMenuOpen + , test "ExportMarkdown closes export menu" <| + \_ -> + let + ( model, _ ) = + update ExportMarkdown { initModel | exportMenuOpen = True } + in + Expect.equal False model.exportMenuOpen + , test "SaveSnapshot is a no-op on model" <| + \_ -> + let + ( model, _ ) = + update SaveSnapshot initModel + in + Expect.equal initModel model + , test "LoadSnapshot closes snapshot menu" <| + \_ -> + let + ( model, _ ) = + update (LoadSnapshot "snap-1") { initModel | snapshotMenuOpen = True } + in + Expect.equal False model.snapshotMenuOpen + ] + + +selectEventTabTests : Test +selectEventTabTests = + describe "SelectEvent tab switching" + [ test "switches from ConfigTab to HeadersTab for non-config event" <| + \_ -> + let + event = + makeNetworkEvent "e1" + + modelWithConfig = + { initModel + | activeTab = ConfigTab + , eventsById = Dict.singleton "e1" event + } + + ( model, _ ) = + update (SelectEvent "e1") modelWithConfig + in + Expect.equal HeadersTab model.activeTab + , test "keeps ConfigTab for sdk:config event" <| + \_ -> + let + configEvent = + { id = "e1" + , timestamp = 100 + , kind = SdkConfig + , source = SdkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Config Nothing + } + + modelWithConfig = + { initModel + | activeTab = ConfigTab + , eventsById = Dict.singleton "e1" configEvent + } + + ( model, _ ) = + update (SelectEvent "e1") modelWithConfig + in + Expect.equal ConfigTab model.activeTab + , test "keeps HeadersTab for any event" <| + \_ -> + let + event = + makeNetworkEvent "e1" + + modelWithHeaders = + { initModel + | activeTab = HeadersTab + , eventsById = Dict.singleton "e1" event + } + + ( model, _ ) = + update (SelectEvent "e1") modelWithHeaders + in + Expect.equal HeadersTab model.activeTab + ] + + +startPlaybackEdgeTests : Test +startPlaybackEdgeTests = + describe "StartPlayback edge cases" + [ test "resets to 0 when at end of nodes" <| + \_ -> + let + modelAtEnd = + { initModel + | events = [ makeSdkEvent "s1" 100, makeSdkEvent "s2" 200 ] + , playbackIndex = Just 1 + } + + ( model, _ ) = + update StartPlayback modelAtEnd + in + Expect.all + [ \m -> Expect.equal (Just 0) m.playbackIndex + , \m -> Expect.equal (Just "s1") m.selectedNodeId + ] + model + , test "resumes from current index when not at end" <| + \_ -> + let + modelMidway = + { initModel + | events = [ makeSdkEvent "s1" 100, makeSdkEvent "s2" 200, makeSdkEvent "s3" 300 ] + , playbackIndex = Just 1 + } + + ( model, _ ) = + update StartPlayback modelMidway + in + Expect.all + [ \m -> Expect.equal (Just 1) m.playbackIndex + , \m -> Expect.equal (Just "s2") m.selectedNodeId + ] + model + ] + + +learnSelectTests : Test +learnSelectTests = + describe "Learn select interactions" + [ test "LearnSelectNode sets learnSelectedNodeId and clears expandedCard and cardPositions" <| + \_ -> + let + canvas = + initModel.learnCanvas + + modelWithCard = + { initModel + | learnCanvas = + { canvas + | expandedCard = Just BrowserCard + , cardPositions = [ ( "browser", { x = 10, y = 20 } ) ] + } + } + + ( model, _ ) = + update (LearnSelectNode "node-1") modelWithCard + in + Expect.all + [ \m -> Expect.equal (Just "node-1") m.learnCanvas.learnSelectedNodeId + , \m -> Expect.equal Nothing m.learnCanvas.expandedCard + , \m -> Expect.equal [] m.learnCanvas.cardPositions + ] + model + , test "LearnExpandCard sets expandedCard" <| + \_ -> + let + ( model, _ ) = + update (LearnExpandCard BrowserCard) initModel + in + Expect.equal (Just BrowserCard) model.learnCanvas.expandedCard + , test "LearnExpandCard toggles off when same card" <| + \_ -> + let + canvas = + initModel.learnCanvas + + modelWithExpanded = + { initModel | learnCanvas = { canvas | expandedCard = Just BrowserCard } } + + ( model, _ ) = + update (LearnExpandCard BrowserCard) modelWithExpanded + in + Expect.equal Nothing model.learnCanvas.expandedCard + , test "LearnExpandCard switches to different card" <| + \_ -> + let + canvas = + initModel.learnCanvas + + modelWithExpanded = + { initModel | learnCanvas = { canvas | expandedCard = Just BrowserCard } } + + ( model, _ ) = + update (LearnExpandCard ServerCard) modelWithExpanded + in + Expect.equal (Just ServerCard) model.learnCanvas.expandedCard + , test "LearnCollapseCard clears expandedCard" <| + \_ -> + let + canvas = + initModel.learnCanvas + + modelWithExpanded = + { initModel | learnCanvas = { canvas | expandedCard = Just SdkCard } } + + ( model, _ ) = + update LearnCollapseCard modelWithExpanded + in + Expect.equal Nothing model.learnCanvas.expandedCard + ] + + +learnDragTests : Test +learnDragTests = + describe "Learn drag and pan interactions" + [ test "LearnStartDrag sets dragTarget and dragStart" <| + \_ -> + let + ( model, _ ) = + update (LearnStartDrag BrowserCard 100 200) initModel + in + Expect.all + [ \m -> Expect.equal (Just BrowserCard) m.learnCanvas.dragTarget + , \m -> Expect.equal (Just { x = 100, y = 200 }) m.learnCanvas.dragStart + ] + model + , test "LearnEndDrag clears drag and pan state" <| + \_ -> + let + canvas = + initModel.learnCanvas + + draggingModel = + { initModel + | learnCanvas = + { canvas + | dragTarget = Just BrowserCard + , dragStart = Just { x = 100, y = 200 } + , isPanning = True + , panStart = Just { x = 50, y = 60 } + } + } + + ( model, _ ) = + update LearnEndDrag draggingModel + in + Expect.all + [ \m -> Expect.equal Nothing m.learnCanvas.dragTarget + , \m -> Expect.equal Nothing m.learnCanvas.dragStart + , \m -> Expect.equal False m.learnCanvas.isPanning + , \m -> Expect.equal Nothing m.learnCanvas.panStart + ] + model + , test "LearnStartPan sets isPanning and panStart" <| + \_ -> + let + ( model, _ ) = + update (LearnStartPan 300 400) initModel + in + Expect.all + [ \m -> Expect.equal True m.learnCanvas.isPanning + , \m -> Expect.equal (Just { x = 300, y = 400 }) m.learnCanvas.panStart + ] + model + , test "LearnEndPan clears isPanning and panStart" <| + \_ -> + let + canvas = + initModel.learnCanvas + + panningModel = + { initModel + | learnCanvas = + { canvas + | isPanning = True + , panStart = Just { x = 300, y = 400 } + } + } + + ( model, _ ) = + update LearnEndPan panningModel + in + Expect.all + [ \m -> Expect.equal False m.learnCanvas.isPanning + , \m -> Expect.equal Nothing m.learnCanvas.panStart + ] + model + ] + + +learnZoomTests : Test +learnZoomTests = + describe "Learn zoom interactions" + [ test "LearnZoom adjusts zoom level" <| + \_ -> + let + ( model, _ ) = + update (LearnZoom 100) initModel + in + Expect.within (Expect.Absolute 0.001) 1.1 model.learnCanvas.zoom + , test "LearnZoom clamps to minimum 0.5" <| + \_ -> + let + ( model, _ ) = + update (LearnZoom -10000) initModel + in + Expect.within (Expect.Absolute 0.001) 0.5 model.learnCanvas.zoom + , test "LearnZoom clamps to maximum 3.0" <| + \_ -> + let + ( model, _ ) = + update (LearnZoom 10000) initModel + in + Expect.within (Expect.Absolute 0.001) 3.0 model.learnCanvas.zoom + ] From d81e6955b2229956f2f14875d77326cd8b23cd8e Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 13:44:11 -0600 Subject: [PATCH 4/8] fix(devtools): use proper mouse event decoders in Learn canvas - Replace hardcoded (0,0) pan start with clientX/clientY JSON decoders - Add mousemove, mouseleave, and wheel event handlers to canvas SVG - Add card-level mousedown decoder for drag with clientX/clientY --- .../src/panel/src/LearnView.elm | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/devtools-extension/src/panel/src/LearnView.elm b/packages/devtools-extension/src/panel/src/LearnView.elm index 5b637352b3..787fd06fd6 100644 --- a/packages/devtools-extension/src/panel/src/LearnView.elm +++ b/packages/devtools-extension/src/panel/src/LearnView.elm @@ -3,6 +3,7 @@ module LearnView exposing (view) import Helpers import Html exposing (Html) import Html.Attributes exposing (..) +import Json.Decode as JD import Svg exposing (..) import Svg.Attributes as SA import Svg.Events @@ -298,8 +299,20 @@ viewCanvas events canvas = [ SA.class "lv-canvas-svg" , SA.width "100%" , SA.height "100%" - , Svg.Events.onMouseDown (LearnStartPan 0 0) - , Svg.Events.onMouseUp LearnEndDrag + , Svg.Events.on "mousedown" + (JD.map2 LearnStartPan + (JD.field "clientX" JD.float) + (JD.field "clientY" JD.float) + ) + , Svg.Events.on "mousemove" + (JD.map2 LearnDrag + (JD.field "clientX" JD.float) + (JD.field "clientY" JD.float) + ) + , Svg.Events.on "mouseup" (JD.succeed LearnEndDrag) + , Svg.Events.on "mouseleave" (JD.succeed LearnEndDrag) + , Svg.Events.on "wheel" + (JD.map LearnZoom (JD.field "deltaY" JD.float)) ] [ canvasDefs , Svg.g [ SA.transform transform ] @@ -522,6 +535,13 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth ) +cardDragDecoder : CardId -> JD.Decoder Msg +cardDragDecoder cardId = + JD.map2 (LearnStartDrag cardId) + (JD.field "clientX" JD.float) + (JD.field "clientY" JD.float) + + renderCard : CardId -> Float -> Float -> Float -> Float -> String -> String -> String -> Maybe CardId -> Svg Msg -> String -> String -> Svg Msg renderCard cardId x y w h borderColor opacity dashArray expandedCard icon label contextLine = let @@ -529,8 +549,9 @@ renderCard cardId x y w h borderColor opacity dashArray expandedCard icon label expandedCard == Just cardId in Svg.g - [ Svg.Events.onClick (LearnExpandCard cardId) - , SA.style "cursor:pointer" + [ Svg.Events.on "mousedown" (cardDragDecoder cardId) + , Svg.Events.onClick (LearnExpandCard cardId) + , SA.style "cursor:grab" , SA.opacity opacity ] [ Svg.rect From a530c1f751f9be6fca6ccab077e7dcb336b4d93a Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 13:47:54 -0600 Subject: [PATCH 5/8] feat(devtools-extension): add accordion card detail and Learn tab CSS Add expanded foreignObject panels to each canvas card showing contextual detail (request method/URL, response status/duration, SDK status transition, collector count). Add CSS classes for the Learn tab layout. --- .../devtools-extension/src/panel/panel.html | 48 ++++++ .../src/panel/src/LearnView.elm | 156 +++++++++++++++++- 2 files changed, 201 insertions(+), 3 deletions(-) diff --git a/packages/devtools-extension/src/panel/panel.html b/packages/devtools-extension/src/panel/panel.html index 57deab0ed2..86e24f561b 100644 --- a/packages/devtools-extension/src/panel/panel.html +++ b/packages/devtools-extension/src/panel/panel.html @@ -1252,6 +1252,54 @@ .import-paste-textarea::placeholder { color: var(--dim); } + /* ── Learn Tab ──────────────────────────────────────── */ + .lv-view { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + } + .lv-rail { + flex-shrink: 0; + height: 110px; + overflow-x: auto; + overflow-y: hidden; + border-bottom: 1px solid var(--border); + background: var(--base); + } + .lv-rail-empty { + height: 110px; + display: flex; + align-items: center; + justify-content: center; + color: var(--dim); + font-size: 12px; + } + .lv-canvas { + flex: 1; + overflow: hidden; + background: var(--base); + cursor: grab; + } + .lv-canvas:active { + cursor: grabbing; + } + .lv-canvas-empty { + display: flex; + align-items: center; + justify-content: center; + color: var(--dim); + font-size: 12px; + font-style: italic; + } + .lv-canvas-svg { + display: block; + width: 100%; + height: 100%; + } + .lv-pulse { + animation: ring-pulse 2s ease-in-out infinite; + } diff --git a/packages/devtools-extension/src/panel/src/LearnView.elm b/packages/devtools-extension/src/panel/src/LearnView.elm index 787fd06fd6..015baff7bf 100644 --- a/packages/devtools-extension/src/panel/src/LearnView.elm +++ b/packages/devtools-extension/src/panel/src/LearnView.elm @@ -316,7 +316,7 @@ viewCanvas events canvas = ] [ canvasDefs , Svg.g [ SA.transform transform ] - (renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus) + (renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus maybeNode requestEvent responseEvent) ] ] @@ -341,8 +341,8 @@ canvasDefs = ] -renderCards : CanvasState -> Bool -> Bool -> Bool -> Bool -> String -> String -> List (Svg Msg) -renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus = +renderCards : CanvasState -> Bool -> Bool -> Bool -> Bool -> String -> String -> Maybe AuthEvent -> Maybe AuthEvent -> Maybe AuthEvent -> List (Svg Msg) +renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus maybeNode requestEvent responseEvent = let cardW = 160 @@ -466,6 +466,8 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth (browserIcon (bx + 30) (by + 20)) "BROWSER" "User interaction" + , expandedPanel BrowserCard bx (by + fH + 8) fW canvas.expandedCard + (browserDetail requestEvent) -- Arrow: Browser -> Server , renderArrowLine (bx + fW) (by + fH / 2) sx (sy + fH / 2) (requestMethod ++ " ->") @@ -475,6 +477,8 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth (serverIcon (sx + 40) (sy + 15)) "SERVER" ("Response " ++ responseStatus) + , expandedPanel ServerCard sx (sy + fH + 8) fW canvas.expandedCard + (serverDetail responseEvent) -- Arrow: Server -> SDK , renderArrowLine (sx + fW) (sy + fH / 2) sdx (sdy + fH / 2) ("-> " ++ responseStatus) @@ -484,6 +488,8 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth (sdkIcon (sdx + 35) (sdy + 20)) "SDK" "Processes response" + , expandedPanel SdkCard sdx (sdy + fH + 8) fW canvas.expandedCard + (sdkDetail maybeNode) -- Arrow: SDK -> Form , renderArrowLine (sdx + fW) (sdy + fH / 2) fx (fy + fH / 2) "renders" @@ -498,6 +504,8 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth else "Skipped" ) + , expandedPanel FormCard fx (fy + fH + 8) fW canvas.expandedCard + (formDetail maybeNode) -- Error pulse on source card when error ] @@ -629,6 +637,148 @@ renderArrowLine x1 y1 x2 y2 label = +-- ── Expanded Panels ───────────────────────────────────────────────────────── + + +expandedPanel : CardId -> Float -> Float -> Float -> Maybe CardId -> List (Html Msg) -> Svg Msg +expandedPanel cardId x y w expandedCard content = + if expandedCard == Just cardId then + Svg.foreignObject + [ SA.x (String.fromFloat x) + , SA.y (String.fromFloat y) + , SA.width (String.fromFloat w) + , SA.height "80" + ] + [ Html.div + [ Html.Attributes.style "font-family" "'Segoe UI', system-ui, sans-serif" + , Html.Attributes.style "font-size" "10px" + , Html.Attributes.style "color" "#8b949e" + , Html.Attributes.style "background" "#161B22" + , Html.Attributes.style "border" "1px solid #30363d" + , Html.Attributes.style "border-radius" "6px" + , Html.Attributes.style "padding" "6px 8px" + ] + content + ] + + else + Svg.g [] [] + + +detailRow : String -> String -> Html Msg +detailRow label value = + Html.div [ Html.Attributes.style "margin-bottom" "2px" ] + [ Html.span [ Html.Attributes.style "color" "#484f58" ] [ Html.text (label ++ " ") ] + , Html.span [ Html.Attributes.style "color" "#e6edf3" ] [ Html.text value ] + ] + + +browserDetail : Maybe AuthEvent -> List (Html Msg) +browserDetail requestEvent = + case requestEvent of + Just re -> + case re.data of + Network nd -> + [ detailRow "Method" (Maybe.withDefault "POST" nd.method) + , detailRow "URL" (truncate_ 22 (Maybe.withDefault "—" nd.url)) + ] + + _ -> + [ Html.text "No request data" ] + + Nothing -> + [ Html.text "No request captured" ] + + +serverDetail : Maybe AuthEvent -> List (Html Msg) +serverDetail responseEvent = + case responseEvent of + Just re -> + case re.data of + Network nd -> + let + statusStr = + Maybe.map String.fromInt nd.status + |> Maybe.withDefault "—" + + durationStr = + case nd.duration of + Just d -> + String.fromFloat d ++ "ms" + + Nothing -> + "—" + in + [ detailRow "Status" statusStr + , detailRow "Duration" durationStr + ] + + _ -> + [ Html.text "No response data" ] + + Nothing -> + [ Html.text "No response captured" ] + + +sdkDetail : Maybe AuthEvent -> List (Html Msg) +sdkDetail maybeNode = + case maybeNode of + Just n -> + case n.data of + DaVinciNode nd -> + let + statusTransition = + (Maybe.map (Helpers.nodeStatusLabel >> (\s -> s)) nd.previousStatus + |> Maybe.withDefault "—" + ) + ++ " → " + ++ (Maybe.map Helpers.nodeStatusLabel nd.nodeStatus + |> Maybe.withDefault "—" + ) + + formName = + Maybe.withDefault "—" nd.nodeName + + interactionId = + Maybe.withDefault "—" nd.interactionId + in + [ detailRow "Status" statusTransition + , detailRow "Node" (truncate_ 18 formName) + , detailRow "Interaction" (truncate_ 14 interactionId) + ] + + _ -> + [ Html.text "Not a DaVinci node" ] + + Nothing -> + [ Html.text "No node selected" ] + + +formDetail : Maybe AuthEvent -> List (Html Msg) +formDetail maybeNode = + case maybeNode of + Just n -> + case n.data of + DaVinciNode nd -> + case nd.collectors of + Just collectors -> + if List.isEmpty collectors then + [ Html.text "No collectors" ] + + else + [ detailRow "Collectors" (String.fromInt (List.length collectors)) ] + + Nothing -> + [ Html.text "No collectors" ] + + _ -> + [ Html.text "No form data" ] + + Nothing -> + [ Html.text "No node selected" ] + + + -- ── Icons ──────────────────────────────────────────────────────────────────── From 443286d9c52414cfc8f7c9c7789e95eca98f9a9f Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 13:56:24 -0600 Subject: [PATCH 6/8] feat(devtools): broaden Learn tab network event correlation When no network events have a direct causedBy link to the selected SDK node, fall back to a time-window heuristic: find network events whose timestamp falls between this node and the next SDK node. This catches events for Journey/OIDC nodes and early flow steps where causedBy linkage may be missing. --- .../src/panel/src/LearnView.elm | 72 ++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/packages/devtools-extension/src/panel/src/LearnView.elm b/packages/devtools-extension/src/panel/src/LearnView.elm index 015baff7bf..595627f868 100644 --- a/packages/devtools-extension/src/panel/src/LearnView.elm +++ b/packages/devtools-extension/src/panel/src/LearnView.elm @@ -206,10 +206,20 @@ viewCanvas events canvas = maybeNode = Helpers.findEventInList nodeId events - netEvents = + -- Primary: direct causedBy link + directNetEvents = List.filter (\e -> e.causedBy == Just nodeId) events |> List.sortBy .timestamp + -- Fallback: network events in the time window between this + -- node and the next node (when causedBy is missing) + netEvents = + if not (List.isEmpty directNetEvents) then + directNetEvents + + else + inferNetworkEvents nodeId events + requestEvent = List.head netEvents @@ -893,6 +903,66 @@ formIcon x y = +-- ── Event correlation ──────────────────────────────────────────────────────── + + +{-| When no network events have a direct `causedBy` link to this node, +infer them by time window: find all network events whose timestamp falls +between this node's timestamp and the next SDK node's timestamp. +-} +inferNetworkEvents : String -> List AuthEvent -> List AuthEvent +inferNetworkEvents nodeId events = + let + sdkNodes = + Helpers.sdkNodes events + + nodeTimestamp = + sdkNodes + |> List.filter (\e -> e.id == nodeId) + |> List.head + |> Maybe.map .timestamp + + nextNodeTimestamp = + case nodeTimestamp of + Nothing -> + Nothing + + Just ts -> + sdkNodes + |> List.filter (\e -> e.timestamp > ts) + |> List.head + |> Maybe.map .timestamp + + isNetworkEvent e = + case e.data of + Network _ -> + True + + _ -> + False + in + case nodeTimestamp of + Nothing -> + [] + + Just startTs -> + events + |> List.filter isNetworkEvent + |> List.filter + (\e -> + e.timestamp + >= startTs + && (case nextNodeTimestamp of + Just endTs -> + e.timestamp < endTs + + Nothing -> + True + ) + ) + |> List.sortBy .timestamp + + -- ── Helpers ────────────────────────────────────────────────────────────────── From 80c408b41b1bc07e01dbe398c56ae371cc42d3e1 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 14:07:50 -0600 Subject: [PATCH 7/8] fix(devtools): correct error source attribution in Learn tab - Pulse ring now targets the actual error source (SERVER for HTTP 4xx/5xx, BROWSER for CORS, SDK for SDK-only errors) - Arrow colors show error propagation direction (red only after error source) - SDK card distinguishes "received server error" vs "SDK error" - Richer expanded panels: color-coded status, error messages, URL in server detail - Browser card does not turn red on server errors (request was fine) --- .../content/learn-error-states.html | 530 +++++++++++ .../content/learn-icons-v2.html | 635 +++++++++++++ .../content/learn-layout.html | 326 +++++++ .../694753-1778353306/content/waiting-2.html | 3 + .../694753-1778353306/content/waiting.html | 3 + .../694753-1778353306/state/server-stopped | 1 + .../694753-1778353306/state/server.log | 23 + .../694753-1778353306/state/server.pid | 1 + packages/devtools-bridge/src/lib/emit.test.ts | 148 ++- .../src/lib/journey-bridge.test.ts | 252 +++++ packages/devtools-extension/elm.json | 10 +- .../background/event-store.service.test.ts | 188 ++++ .../src/background/serialize-diagnosis.ts | 15 + .../src/background/service-worker.ts | 17 +- .../devtools-extension/src/panel/Main.elm | 16 +- .../devtools-extension/src/panel/jwt.test.ts | 94 ++ packages/devtools-extension/src/panel/jwt.ts | 28 + .../devtools-extension/src/panel/panel.ts | 24 +- .../src/panel/src/Decode.elm | 305 +++--- .../src/panel/src/FlowView.elm | 46 +- .../src/panel/src/Graph.elm | 25 +- .../src/panel/src/Helpers.elm | 92 +- .../src/panel/src/Inspector.elm | 62 +- .../src/panel/src/LearnView.elm | 414 +++++--- .../src/panel/src/Timeline.elm | 39 +- .../devtools-extension/tests/DecodeTests.elm | 898 ++++++++++++++++++ .../devtools-extension/tests/HelpersTests.elm | 277 ++++++ .../src/lib/auth-event.schema.test.ts | 367 +++++++ .../src/lib/flow-state.schema.test.ts | 123 +++ 29 files changed, 4547 insertions(+), 415 deletions(-) create mode 100644 .superpowers/brainstorm/694753-1778353306/content/learn-error-states.html create mode 100644 .superpowers/brainstorm/694753-1778353306/content/learn-icons-v2.html create mode 100644 .superpowers/brainstorm/694753-1778353306/content/learn-layout.html create mode 100644 .superpowers/brainstorm/694753-1778353306/content/waiting-2.html create mode 100644 .superpowers/brainstorm/694753-1778353306/content/waiting.html create mode 100644 .superpowers/brainstorm/694753-1778353306/state/server-stopped create mode 100644 .superpowers/brainstorm/694753-1778353306/state/server.log create mode 100644 .superpowers/brainstorm/694753-1778353306/state/server.pid create mode 100644 packages/devtools-extension/src/background/serialize-diagnosis.ts create mode 100644 packages/devtools-extension/src/panel/jwt.test.ts create mode 100644 packages/devtools-extension/src/panel/jwt.ts create mode 100644 packages/devtools-extension/tests/DecodeTests.elm create mode 100644 packages/devtools-extension/tests/HelpersTests.elm create mode 100644 packages/devtools-types/src/lib/flow-state.schema.test.ts diff --git a/.superpowers/brainstorm/694753-1778353306/content/learn-error-states.html b/.superpowers/brainstorm/694753-1778353306/content/learn-error-states.html new file mode 100644 index 0000000000..15435e557b --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/content/learn-error-states.html @@ -0,0 +1,530 @@ +

Learn Tab — Error Visualization

+

+ When a step fails, how should the lifecycle show it? Here's a scenario: Server returns 401 + Unauthorized. +

+ +
+
+
+ + + + + + + + + + + + Step 3 — Token Exchange (FAILED) + + + + + + + + + + + + + + + + BROWSER + + + + + + + + + + + + + + + SERVER + + + + + + + + + + + + 401 + + + + + + + + + + + + + + ERROR + + + Auth failed + + + + + + + + + FORM + + + skipped + + + + + + + ✕ Authentication Failed + + + 401 Unauthorized — Invalid credentials. Token exchange rejected by server. + + +
+
+

A. Broken Flow

+

+ The arrow between server and SDK shows a visible ✕ break. The SDK card turns red with a + pulsing ring. Downstream stages (collectors/form) are ghosted out with dashed borders and + "skipped" label. Error banner below explains what happened. The flow visually stops at the + failure point. +

+
+
+ +
+
+ + + + + + + + + + + + Step 3 — Token Exchange + + + + + + + + + + + + + + + + BROWSER + + + + + + + + + + + + + + + + + + + + 401 + + + + SERVER + + + 401 Unauthorized + + + + + + + + + + + + + + + SDK + + + error + + + + + + + + + FORM + + + + + + + ✕ 401 Unauthorized + + + Invalid credentials — check username/password + + + POST /davinci/connections/…/flow • 123ms + + +
+
+

B. Source Highlight

+

+ The card where the error originated (SERVER) gets a red border, pulsing glow, and a "401" + badge. All downstream cards also turn red to show error propagation, but only the source + card pulses. Downstream form is ghosted. Error detail card below shows specifics anchored to + the source. +

+
+
+
diff --git a/.superpowers/brainstorm/694753-1778353306/content/learn-icons-v2.html b/.superpowers/brainstorm/694753-1778353306/content/learn-icons-v2.html new file mode 100644 index 0000000000..15c2405c01 --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/content/learn-icons-v2.html @@ -0,0 +1,635 @@ +

Learn Tab — Icon Style for Step Detail

+

+ Hybrid layout confirmed. Now let's nail the icons and visual language for the step lifecycle. + Which icon style feels right? +

+ +
+
+
+ + + + + + + + + + + + + Step 2 — Password Form + + + + + + + + + + + app + + + Browser + + + POST /flow + + + + + + request + + + + + + + + + + + + + + + + DaVinci + + + PingOne + + + + + + 200 OK + + + + + + + + + + + + + + SDK + + + continue + + + + + + renders + + + + + + + + + + Submit + + + Collectors + + + 2 fields + + + + + + user submits → next request cycle + + + + + + 200 OK + + + + continue + + + + 2 fields + + +
+
+

A. Outlined Technical

+

+ Clean outlined icons — browser with traffic-light dots, rack server with status LEDs, gear + for SDK processing, form fields for collectors. Arrows show direction and label the action. + Loopback arrow shows the cycle repeating. Status pills below summarize each stage. +

+
+
+ +
+
+ + + + + + + + + + + + Step 2 — Password Form + + + + + + + + + + + + + + + + + BROWSER + + + POST /flow + + + + + + + + REQ → + + + + + + + + + + + + + + SERVER + + + PingOne + + + + + + + + ← 200 + + + + + + + + + + + + + + SDK + + + continue + + + + + + + + + + + + + FORM + + + password + + Submit → + + + FORM + + + 2 fields + + + + + + + next cycle → + + + + + + 123ms + + +
+
+

B. Filled Cards with Icons

+

+ Each stage is a card with a recognizable icon inside — browser window with globe, cloud + server with status dots, gear for SDK processing, form with input fields. Arrows are labeled + inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop. +

+
+
+
diff --git a/.superpowers/brainstorm/694753-1778353306/content/learn-layout.html b/.superpowers/brainstorm/694753-1778353306/content/learn-layout.html new file mode 100644 index 0000000000..3aa9ab9836 --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/content/learn-layout.html @@ -0,0 +1,326 @@ +

Learn Tab — Layout Direction

+

+ How should the flow diagram be oriented? Consider that auth flows can have 3-15+ steps. +

+ +
+
+
+ + + + + + + + + + + Start + + SDK Init + + + + + + Node 1 + + Username + + + + + + Node 2 + + Password + + + + + + Done + + Success + + + + ▼ Detail panel appears below + + POST /davinci/flow → 200 → collectors + + +
+
+

A. Horizontal (Left → Right)

+

+ Natural reading direction. Flow overview scrolls horizontally. Detail panel below selected + node. Works well with pan/zoom for long flows. +

+
+
+ +
+
+ + + + + + + + + + Start + + + + + + + Node 1 + + + + + + + Node 2 + + + + + + + ✓ + + + + + + Node 1 — Username + + + POST + /davinci/connections/flow → 200 + + NODE + continue — 2 collectors + + RESP + interactionId: abc-123... + +
+
+

B. Vertical (Top → Bottom)

+

+ Like the existing Graph panel but expanded. Flow scrolls vertically. Detail panel to the + right of selected node. Familiar pattern from current UI. +

+
+
+ +
+
+ + + + + + + + + + 1 + + + 2 + + + 3 + + + + + + + Step 2 — Password + + + + + REQUEST + + + + + + + + SERVER + + + + + + + 200 OK + + + + + + + NODE + + + + + + + COLLECTORS + + + + POST /flow + DaVinci + continue + Password + 2 inputs + + + + +
+
+

C. Hybrid — Rail Overview + Step Detail

+

+ Horizontal rail at the top (like existing FlowView) for the full flow overview. Below, an + expanded step-detail section shows the internal lifecycle: Request → Server → Response → + Node → Collectors. Click a node in the rail to drill in. Keeps context while showing detail. + The detail section is the draggable canvas. +

+
+
+
diff --git a/.superpowers/brainstorm/694753-1778353306/content/waiting-2.html b/.superpowers/brainstorm/694753-1778353306/content/waiting-2.html new file mode 100644 index 0000000000..c9f1f81455 --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/content/waiting-2.html @@ -0,0 +1,3 @@ +
+

Continuing in terminal...

+
diff --git a/.superpowers/brainstorm/694753-1778353306/content/waiting.html b/.superpowers/brainstorm/694753-1778353306/content/waiting.html new file mode 100644 index 0000000000..c9f1f81455 --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/content/waiting.html @@ -0,0 +1,3 @@ +
+

Continuing in terminal...

+
diff --git a/.superpowers/brainstorm/694753-1778353306/state/server-stopped b/.superpowers/brainstorm/694753-1778353306/state/server-stopped new file mode 100644 index 0000000000..1223f1a2da --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/state/server-stopped @@ -0,0 +1 @@ +{"reason":"idle timeout","timestamp":1778356250698} diff --git a/.superpowers/brainstorm/694753-1778353306/state/server.log b/.superpowers/brainstorm/694753-1778353306/state/server.log new file mode 100644 index 0000000000..4695c7bc6a --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/state/server.log @@ -0,0 +1,23 @@ +{"type":"server-started","port":53346,"host":"127.0.0.1","url_host":"localhost","url":"http://localhost:53346","screen_dir":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content","state_dir":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/state"} +{"type":"screen-added","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/learn-layout.html"} +{"source":"user-event","type":"click","text":"1\n \n \n 2\n \n \n 3\n \n \n ✓\n\n \n \n Step 2 — Password\n\n \n \n REQUEST\n\n \n\n \n ☁\n SERVER\n\n \n\n \n 200 OK\n\n \n\n \n NODE\n\n \n\n \n COLLECTORS\n\n \n POST /flow\n DaVinci\n continue\n Password\n 2 inputs\n\n \n \n \n \n \n C. Hybrid — Rail Overview + Step Detail\n Horizontal rail at the top (like existing FlowView) for the full flow overview. Below, an expanded step-detail section shows the internal lifecycle: Request → Server → Response → Node → Collectors. Click a node in the rail to drill in. Keeps context while showing detail. The detail section is the draggable canvas.","choice":"hybrid","id":null,"timestamp":1778353448446} +{"source":"user-event","type":"click","text":"1\n \n \n 2\n \n \n 3\n \n \n ✓\n\n \n \n Step 2 — Password\n\n \n \n REQUEST\n\n \n\n \n ☁\n SERVER\n\n \n\n \n 200 OK\n\n \n\n \n NODE\n\n \n\n \n COLLECTORS\n\n \n POST /flow\n DaVinci\n continue\n Password\n 2 inputs\n\n \n \n \n \n \n C. Hybrid — Rail Overview + Step Detail\n Horizontal rail at the top (like existing FlowView) for the full flow overview. Below, an expanded step-detail section shows the internal lifecycle: Request → Server → Response → Node → Collectors. Click a node in the rail to drill in. Keeps context while showing detail. The detail section is the draggable canvas.","choice":"hybrid","id":null,"timestamp":1778353449791} +{"source":"user-event","type":"click","text":"1\n \n \n 2\n \n \n 3\n \n \n ✓\n\n \n \n Step 2 — Password\n\n \n \n REQUEST\n\n \n\n \n ☁\n SERVER\n\n \n\n \n 200 OK\n\n \n\n \n NODE\n\n \n\n \n COLLECTORS\n\n \n POST /flow\n DaVinci\n continue\n Password\n 2 inputs\n\n \n \n \n \n \n C. Hybrid — Rail Overview + Step Detail\n Horizontal rail at the top (like existing FlowView) for the full flow overview. Below, an expanded step-detail section shows the internal lifecycle: Request → Server → Response → Node → Collectors. Click a node in the rail to drill in. Keeps context while showing detail. The detail section is the draggable canvas.","choice":"hybrid","id":null,"timestamp":1778353449972} +{"type":"screen-added","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/learn-icons-v2.html"} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353603070} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353614416} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353615062} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353615233} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353617313} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353617492} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353618039} +{"source":"user-event","type":"click","text":"Step 2 — Password Form\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n BROWSER\n POST /flow\n\n \n \n \n \n REQ →\n \n\n \n \n \n \n \n \n \n \n \n SERVER\n PingOne\n\n \n \n \n \n ← 200\n \n\n \n \n \n \n \n \n ⚙\n ✓\n \n SDK\n continue\n\n \n \n \n \n\n \n \n \n \n \n FORM\n \n \n password\n \n Submit →\n \n FORM\n 2 fields\n\n \n \n \n next cycle →\n\n \n \n 123ms\n \n \n \n B. Filled Cards with Icons\n Each stage is a card with a recognizable icon inside — browser window with globe, cloud server with status dots, gear for SDK processing, form with input fields. Arrows are labeled inline. Bolder, more visual weight. Cycle arrow wraps around to show the loop.","choice":"filled","id":null,"timestamp":1778353618773} +{"type":"screen-added","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/waiting.html"} +{"type":"screen-added","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/learn-error-states.html"} +{"type":"screen-added","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/waiting-2.html"} +{"type":"screen-updated","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/learn-error-states.html"} +{"type":"screen-updated","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/learn-icons-v2.html"} +{"type":"screen-updated","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/learn-layout.html"} +{"type":"screen-updated","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/waiting-2.html"} +{"type":"screen-updated","file":"/home/ryan/programming/ping-javascript-sdk/.superpowers/brainstorm/694753-1778353306/content/waiting.html"} +{"type":"server-stopped","reason":"idle timeout"} diff --git a/.superpowers/brainstorm/694753-1778353306/state/server.pid b/.superpowers/brainstorm/694753-1778353306/state/server.pid new file mode 100644 index 0000000000..429ccc43c5 --- /dev/null +++ b/.superpowers/brainstorm/694753-1778353306/state/server.pid @@ -0,0 +1 @@ +694761 diff --git a/packages/devtools-bridge/src/lib/emit.test.ts b/packages/devtools-bridge/src/lib/emit.test.ts index 22d8832c1b..fa6de9421c 100644 --- a/packages/devtools-bridge/src/lib/emit.test.ts +++ b/packages/devtools-bridge/src/lib/emit.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { AuthEvent } from '@forgerock/devtools-types'; -import { DEVTOOLS_EVENT_NAME, emitAuthEvent } from './emit.js'; +import { DEVTOOLS_EVENT_NAME, emitAuthEvent, emitConfigEvent, configureDevtools } from './emit.js'; // Minimal valid AuthEvent fixture — _tag: 'sdk' satisfies the SdkDataSchema discriminant. -const makeEvent = (): AuthEvent => ({ +const makeEvent = (overrides: Partial = {}): AuthEvent => ({ id: 'test-id-1', timestamp: 0, type: 'sdk:node-change', @@ -19,9 +19,16 @@ const makeEvent = (): AuthEvent => ({ isError: false, isAuthRelated: true, }, + ...overrides, }); describe('emitAuthEvent', () => { + beforeEach(() => { + // Reset options between tests by calling configureDevtools with defaults + configureDevtools({}); + delete window.__PING_DEVTOOLS_STATE__; + }); + it('dispatches a CustomEvent with DEVTOOLS_EVENT_NAME and the event as detail', () => { const captured: CustomEvent[] = []; const handler = (e: Event) => { @@ -51,4 +58,139 @@ describe('emitAuthEvent', () => { // Restore window so subsequent tests are unaffected. globalThis.window = saved; }); + + it('accumulates events in window.__PING_DEVTOOLS_STATE__', () => { + emitAuthEvent(makeEvent({ id: 'a' })); + emitAuthEvent(makeEvent({ id: 'b' })); + + expect(window.__PING_DEVTOOLS_STATE__).toHaveLength(2); + expect(window.__PING_DEVTOOLS_STATE__![0].id).toBe('a'); + expect(window.__PING_DEVTOOLS_STATE__![1].id).toBe('b'); + }); + + it('initialises __PING_DEVTOOLS_STATE__ array on first call', () => { + expect(window.__PING_DEVTOOLS_STATE__).toBeUndefined(); + emitAuthEvent(makeEvent()); + expect(Array.isArray(window.__PING_DEVTOOLS_STATE__)).toBe(true); + }); + + it('appends to existing __PING_DEVTOOLS_STATE__ array', () => { + window.__PING_DEVTOOLS_STATE__ = [makeEvent({ id: 'existing' })]; + emitAuthEvent(makeEvent({ id: 'new' })); + + expect(window.__PING_DEVTOOLS_STATE__).toHaveLength(2); + expect(window.__PING_DEVTOOLS_STATE__[1].id).toBe('new'); + }); +}); + +describe('configureDevtools', () => { + beforeEach(() => { + configureDevtools({}); + delete window.__PING_DEVTOOLS_STATE__; + }); + + it('enables console logging when consoleLog is true', () => { + const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + configureDevtools({ consoleLog: true }); + const event = makeEvent(); + emitAuthEvent(event); + + expect(spy).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenCalledWith('[ping-devtools]', event.type, event); + + spy.mockRestore(); + }); + + it('does not console.log when consoleLog is false', () => { + const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + configureDevtools({ consoleLog: false }); + emitAuthEvent(makeEvent()); + + expect(spy).not.toHaveBeenCalled(); + + spy.mockRestore(); + }); + + it('does not console.log by default (no options)', () => { + const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined); + + configureDevtools({}); + emitAuthEvent(makeEvent()); + + expect(spy).not.toHaveBeenCalled(); + + spy.mockRestore(); + }); +}); + +describe('emitConfigEvent', () => { + beforeEach(() => { + configureDevtools({}); + delete window.__PING_DEVTOOLS_STATE__; + }); + + it('emits an sdk:config event with the provided config object', () => { + const captured: CustomEvent[] = []; + const handler = (e: Event) => captured.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + + const config = { serverUrl: 'https://auth.example.com', clientId: 'my-app' }; + emitConfigEvent(config); + + window.removeEventListener(DEVTOOLS_EVENT_NAME, handler); + + expect(captured).toHaveLength(1); + const event = captured[0].detail; + expect(event.type).toBe('sdk:config'); + expect(event.source).toBe('sdk'); + expect(event.data._tag).toBe('sdk-config'); + if (event.data._tag === 'sdk-config') { + expect(event.data.config).toEqual(config); + } + }); + + it('generates a unique id and timestamp', () => { + const captured: CustomEvent[] = []; + const handler = (e: Event) => captured.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + + emitConfigEvent({}); + emitConfigEvent({}); + + window.removeEventListener(DEVTOOLS_EVENT_NAME, handler); + + expect(captured[0].detail.id).not.toBe(captured[1].detail.id); + expect(typeof captured[0].detail.timestamp).toBe('number'); + }); + + it('sets flowId and causedBy to null', () => { + const captured: CustomEvent[] = []; + const handler = (e: Event) => captured.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + + emitConfigEvent({ key: 'value' }); + + window.removeEventListener(DEVTOOLS_EVENT_NAME, handler); + + expect(captured[0].detail.flowId).toBeNull(); + expect(captured[0].detail.causedBy).toBeNull(); + }); + + it('sets flags to non-cors, non-error, auth-related', () => { + const captured: CustomEvent[] = []; + const handler = (e: Event) => captured.push(e as CustomEvent); + window.addEventListener(DEVTOOLS_EVENT_NAME, handler); + + emitConfigEvent({}); + + window.removeEventListener(DEVTOOLS_EVENT_NAME, handler); + + expect(captured[0].detail.flags).toEqual({ + isCors: false, + isError: false, + isAuthRelated: true, + }); + }); }); diff --git a/packages/devtools-bridge/src/lib/journey-bridge.test.ts b/packages/devtools-bridge/src/lib/journey-bridge.test.ts index e3f82b191d..547f90d8dd 100644 --- a/packages/devtools-bridge/src/lib/journey-bridge.test.ts +++ b/packages/devtools-bridge/src/lib/journey-bridge.test.ts @@ -373,3 +373,255 @@ describe('attachJourneyBridge', () => { expect(events).toHaveLength(0); }); }); + +// --------------------------------------------------------------------------- +// Edge-case tests for pure function paths (stepPayloadToJourneyData, extractErrorMessage) +// --------------------------------------------------------------------------- + +describe('stepPayloadToJourneyData (via integration)', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + }); + + it('preserves all optional journey fields in a Step', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'fulfilled', + data: { + authId: 'abc', + tokenId: 'tok-1', + realm: '/alpha', + stage: 'UsernamePassword', + header: 'Sign In', + description: 'Enter your credentials', + callbacks: [{ type: 'NameCallback' }, { type: 'PasswordCallback' }], + }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { + tokenId?: string; + realm?: string; + description?: string; + }; + expect(data.tokenId).toBe('tok-1'); + expect(data.realm).toBe('/alpha'); + expect(data.description).toBe('Enter your credentials'); + }); + + it('does not include error fields for Step type', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'fulfilled', + data: { + authId: 'abc', + code: 110, + message: 'some message', + reason: 'some reason', + }, + }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { + stepType: string; + errorCode?: number; + errorMessage?: string; + errorReason?: string; + }; + expect(data.stepType).toBe('Step'); + expect(data.errorCode).toBeUndefined(); + expect(data.errorMessage).toBeUndefined(); + expect(data.errorReason).toBeUndefined(); + }); + + it('does not emit for fulfilled mutation with unparseable data', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { + status: 'fulfilled', + data: 'not-an-object', + }, + }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(0); + }); + + it('trims stale requestIds from emitted set', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + + // First trigger: add req-1 + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'fulfilled', data: { authId: 'abc' } }, + }, + }, + }); + + // Second trigger: req-1 removed from mutations, req-2 added + // req-1 should be trimmed from emittedRequests + client.trigger({ + journeyReducer: { + mutations: { + 'req-2': { status: 'fulfilled', data: { successUrl: '/home' } }, + }, + }, + }); + + handle.detach(); + stop(); + + expect(events).toHaveLength(2); + expect(events[0].detail.type).toBe('sdk:journey-step'); + expect(events[1].detail.type).toBe('sdk:journey-step'); + }); +}); + +describe('extractErrorMessage (via integration)', () => { + beforeEach(() => { + (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__'] = true; + }); + + afterEach(() => { + delete (window as unknown as Record)['__PING_DEVTOOLS_EXTENSION__']; + }); + + it('extracts string error directly', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'rejected', error: 'Network timeout' }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Network timeout'); + }); + + it('extracts error.message from object', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'rejected', error: { message: 'Auth failed' } }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Auth failed'); + }); + + it('falls back to "Unknown error" for null error', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'rejected', error: null }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Unknown error'); + }); + + it('falls back to "Unknown error" for undefined error', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'rejected' }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Unknown error'); + }); + + it('falls back to "Unknown error" for number error', () => { + const client = makeClient(emptyState()); + const { events, stop } = captureDevtoolsEvents(); + + const handle = attachJourneyBridge(client); + client.trigger({ + journeyReducer: { + mutations: { + 'req-1': { status: 'rejected', error: 42 }, + }, + }, + }); + + handle.detach(); + stop(); + + const data = events[0].detail.data as { errorMessage?: string }; + expect(data.errorMessage).toBe('Unknown error'); + }); +}); diff --git a/packages/devtools-extension/elm.json b/packages/devtools-extension/elm.json index 43146d7ded..d252750e16 100644 --- a/packages/devtools-extension/elm.json +++ b/packages/devtools-extension/elm.json @@ -4,6 +4,7 @@ "elm-version": "0.19.1", "dependencies": { "direct": { + "NoRedInk/elm-json-decode-pipeline": "1.0.1", "elm/browser": "1.0.2", "elm/core": "1.0.5", "elm/html": "1.0.1", @@ -17,7 +18,12 @@ } }, "test-dependencies": { - "direct": {}, - "indirect": {} + "direct": { + "elm-explorations/test": "2.2.1" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/random": "1.0.0" + } } } diff --git a/packages/devtools-extension/src/background/event-store.service.test.ts b/packages/devtools-extension/src/background/event-store.service.test.ts index cb4746d7ce..38f5c20535 100644 --- a/packages/devtools-extension/src/background/event-store.service.test.ts +++ b/packages/devtools-extension/src/background/event-store.service.test.ts @@ -117,3 +117,191 @@ describe('lastSdkEventId tracking', () => { expect(state.lastSdkEventId).toBe('sdk-2'); }); }); + +describe('updateSummary (via append)', () => { + it('increments nodeCount for sdk:node-change events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append( + makeEvent({ + id: 'sdk-1', + type: 'sdk:node-change', + source: 'sdk', + data: { _tag: 'sdk', nodeStatus: 'continue' }, + }), + ); + yield* store.append( + makeEvent({ + id: 'sdk-2', + type: 'sdk:node-change', + source: 'sdk', + data: { _tag: 'sdk', nodeStatus: 'success' }, + }), + ); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.nodeCount).toBe(2); + }); + + it('does not increment nodeCount for non-node-change events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'net-1' })); + yield* store.append( + makeEvent({ + id: 'j-1', + type: 'sdk:journey-step', + source: 'sdk', + data: { _tag: 'journey', stepType: 'Step' }, + }), + ); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.nodeCount).toBe(0); + }); + + it('sets sdkConnected to true after an sdk:node-change event', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + const before = yield* store.getState(); + yield* store.append( + makeEvent({ + id: 'sdk-1', + type: 'sdk:node-change', + source: 'sdk', + data: { _tag: 'sdk', nodeStatus: 'continue' }, + }), + ); + const after = yield* store.getState(); + return { before: before.summary.sdkConnected, after: after.summary.sdkConnected }; + }); + const result = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(result.before).toBe(false); + expect(result.after).toBe(true); + }); + + it('accumulates corsFlags from CORS network events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append( + makeEvent({ + id: 'cors-1', + flags: { isCors: true, isError: true, isAuthRelated: true }, + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'POST', + status: 0, + requestHeaders: {}, + responseHeaders: {}, + duration: 0, + corsFlag: { + url: 'https://auth.example.com/token', + reason: 'status-zero', + method: 'POST', + }, + }, + }), + ); + yield* store.append( + makeEvent({ + id: 'cors-2', + flags: { isCors: true, isError: true, isAuthRelated: true }, + data: { + _tag: 'network', + url: 'https://auth.example.com/authorize', + method: 'GET', + status: 200, + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: {}, + duration: 50, + corsFlag: { + url: 'https://auth.example.com/authorize', + reason: 'missing-allow-origin', + method: 'GET', + }, + }, + }), + ); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.corsFlags).toHaveLength(2); + expect(state.summary.corsFlags[0].reason).toBe('status-zero'); + expect(state.summary.corsFlags[1].reason).toBe('missing-allow-origin'); + }); + + it('does not add corsFlag for non-cors events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'net-1' })); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.corsFlags).toHaveLength(0); + }); + + it('calculates duration as max - min timestamp', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'e1', timestamp: 1000 })); + yield* store.append(makeEvent({ id: 'e2', timestamp: 1500 })); + yield* store.append(makeEvent({ id: 'e3', timestamp: 3000 })); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.duration).toBe(2000); + }); + + it('duration is 0 for a single event', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'e1', timestamp: 1000 })); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.duration).toBe(0); + }); + + it('sets flowId from first event with a non-null flowId', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'e1', flowId: null })); + yield* store.append(makeEvent({ id: 'e2', flowId: 'flow-abc' })); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.flowId).toBe('flow-abc'); + }); + + it('does not overwrite flowId once set', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append(makeEvent({ id: 'e1', flowId: 'flow-1' })); + yield* store.append(makeEvent({ id: 'e2', flowId: 'flow-2' })); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.flowId).toBe('flow-1'); + }); + + it('counts multiple error events', async () => { + const program = Effect.gen(function* () { + const store = yield* EventStoreService; + yield* store.append( + makeEvent({ id: 'e1', flags: { isCors: false, isError: true, isAuthRelated: true } }), + ); + yield* store.append( + makeEvent({ id: 'e2', flags: { isCors: false, isError: true, isAuthRelated: true } }), + ); + yield* store.append( + makeEvent({ id: 'e3', flags: { isCors: false, isError: false, isAuthRelated: true } }), + ); + return yield* store.getState(); + }); + const state = await Effect.runPromise(Effect.provide(program, EventStoreLive)); + expect(state.summary.errorCount).toBe(2); + }); +}); diff --git a/packages/devtools-extension/src/background/serialize-diagnosis.ts b/packages/devtools-extension/src/background/serialize-diagnosis.ts new file mode 100644 index 0000000000..761d7daaa2 --- /dev/null +++ b/packages/devtools-extension/src/background/serialize-diagnosis.ts @@ -0,0 +1,15 @@ +import type { DiagnosisResult, FlowIssue, EventIssue } from './diagnosis-engine.js'; + +export interface SerializableDiagnosisResult { + issues: FlowIssue[]; + annotatedEvents: Record; + flowHealth: 'healthy' | 'warning' | 'error'; +} + +export function serializeDiagnosis(diagnosis: DiagnosisResult): SerializableDiagnosisResult { + return { + issues: diagnosis.issues, + annotatedEvents: Object.fromEntries(diagnosis.annotatedEvents), + flowHealth: diagnosis.flowHealth, + }; +} diff --git a/packages/devtools-extension/src/background/service-worker.ts b/packages/devtools-extension/src/background/service-worker.ts index 2eabcfeb50..f319f2c7f2 100644 --- a/packages/devtools-extension/src/background/service-worker.ts +++ b/packages/devtools-extension/src/background/service-worker.ts @@ -2,21 +2,8 @@ import { ManagedRuntime, Effect } from 'effect'; import { EventStoreLive, EventStoreService } from './event-store.service.js'; import { handleMessage } from './message-handler.js'; import { runDiagnosis } from './diagnosis-engine.js'; -import type { DiagnosisResult, FlowIssue, EventIssue } from './diagnosis-engine.js'; - -interface SerializableDiagnosisResult { - issues: FlowIssue[]; - annotatedEvents: Record; - flowHealth: 'healthy' | 'warning' | 'error'; -} - -function serializeDiagnosis(diagnosis: DiagnosisResult): SerializableDiagnosisResult { - return { - issues: diagnosis.issues, - annotatedEvents: Object.fromEntries(diagnosis.annotatedEvents), - flowHealth: diagnosis.flowHealth, - }; -} +import { serializeDiagnosis } from './serialize-diagnosis.js'; +import type { SerializableDiagnosisResult } from './serialize-diagnosis.js'; const AppLayer = EventStoreLive; let runtime = ManagedRuntime.make(AppLayer); diff --git a/packages/devtools-extension/src/panel/Main.elm b/packages/devtools-extension/src/panel/Main.elm index b83288625b..eda61ced3e 100644 --- a/packages/devtools-extension/src/panel/Main.elm +++ b/packages/devtools-extension/src/panel/Main.elm @@ -109,8 +109,8 @@ subscriptions model = Ok result -> DiagnosisReceived result - Err _ -> - DecodeError "Failed to decode diagnosis result" + Err err -> + DecodeError ("Diagnosis decode failed: " ++ JD.errorToString err) ) , receiveImportMeta (\raw -> @@ -118,8 +118,8 @@ subscriptions model = Ok meta -> ImportMetaReceived meta - Err _ -> - ImportError "Failed to decode import metadata" + Err err -> + ImportError ("Import meta decode failed: " ++ JD.errorToString err) ) , receiveImportError (\raw -> @@ -127,8 +127,8 @@ subscriptions model = Ok errMsg -> ImportError errMsg - Err _ -> - ImportError "Unknown import error" + Err err -> + ImportError ("Unknown import error: " ++ JD.errorToString err) ) , receiveSnapshots (\raw -> @@ -136,8 +136,8 @@ subscriptions model = Ok list -> SnapshotsReceived list - Err _ -> - SnapshotsReceived [] + Err err -> + DecodeError ("Snapshots decode failed: " ++ JD.errorToString err) ) , playbackSub ] diff --git a/packages/devtools-extension/src/panel/jwt.test.ts b/packages/devtools-extension/src/panel/jwt.test.ts new file mode 100644 index 0000000000..6f4dd8309b --- /dev/null +++ b/packages/devtools-extension/src/panel/jwt.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect } from 'vitest'; +import { formatUnixTime, base64UrlDecode, parseJwt } from './jwt.js'; + +describe('formatUnixTime', () => { + it('formats a Unix timestamp into ISO-like UTC string', () => { + // 2023-11-14T22:13:20.000Z + const result = formatUnixTime(1700000000); + expect(result).toBe('2023-11-14 22:13:20.000 UTC'); + }); + + it('handles zero', () => { + const result = formatUnixTime(0); + expect(result).toBe('1970-01-01 00:00:00.000 UTC'); + }); + + it('returns the number as string for invalid input', () => { + const result = formatUnixTime(NaN); + expect(result).toBe('NaN'); + }); +}); + +describe('base64UrlDecode', () => { + it('decodes standard base64url string', () => { + // "hello" in base64url = "aGVsbG8" + const result = base64UrlDecode('aGVsbG8'); + expect(result).toBe('hello'); + }); + + it('handles base64url characters (- and _)', () => { + // base64url uses - for + and _ for / + // "???" in base64 = "Pz8/" → base64url = "Pz8_" + const result = base64UrlDecode('Pz8_'); + expect(result).toBe('???'); + }); + + it('handles padding correctly for 3-char input', () => { + // "ab" in base64 = "YWI=" → base64url = "YWI" + const result = base64UrlDecode('YWI'); + expect(result).toBe('ab'); + }); +}); + +describe('parseJwt', () => { + // Build a minimal JWT with base64url-encoded header and payload + function makeJwt(header: Record, payload: Record): string { + const encode = (obj: Record) => + btoa(JSON.stringify(obj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + return `${encode(header)}.${encode(payload)}.fakesignaturedata1234`; + } + + it('parses header and payload from a valid JWT', () => { + const jwt = makeJwt({ alg: 'RS256', typ: 'JWT' }, { sub: 'user-1', exp: 1700000000 }); + const result = parseJwt(jwt); + + expect(result.header).toEqual({ alg: 'RS256', typ: 'JWT' }); + expect(result.payload.sub).toBe('user-1'); + expect(result.payload.exp).toBe(1700000000); + }); + + it('returns signature preview (first 16 chars + ellipsis)', () => { + const jwt = makeJwt({ alg: 'RS256' }, { sub: '1' }); + const result = parseJwt(jwt); + + expect(result.signaturePreview).toBe('fakesignaturedat…'); + }); + + it('throws for a string with fewer than 3 parts', () => { + expect(() => parseJwt('only.two')).toThrow('Not a 3-part JWT'); + expect(() => parseJwt('just-one')).toThrow('Not a 3-part JWT'); + }); + + it('throws for a string with more than 3 parts', () => { + expect(() => parseJwt('a.b.c.d')).toThrow('Not a 3-part JWT'); + }); + + it('throws for invalid base64 content', () => { + expect(() => parseJwt('!!!.@@@.###')).toThrow(); + }); + + it('parses JWT with URL-safe base64 characters', () => { + // Create a payload that would produce + and / in standard base64 + const jwt = makeJwt({ alg: 'RS256' }, { data: '>>>???' }); + const result = parseJwt(jwt); + expect(result.payload.data).toBe('>>>???'); + }); + + it('handles short signature (less than 16 chars)', () => { + const encode = (obj: Record) => + btoa(JSON.stringify(obj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); + const jwt = `${encode({ alg: 'RS256' })}.${encode({ sub: '1' })}.abc`; + const result = parseJwt(jwt); + expect(result.signaturePreview).toBe('abc…'); + }); +}); diff --git a/packages/devtools-extension/src/panel/jwt.ts b/packages/devtools-extension/src/panel/jwt.ts new file mode 100644 index 0000000000..4ec8b34f35 --- /dev/null +++ b/packages/devtools-extension/src/panel/jwt.ts @@ -0,0 +1,28 @@ +export function formatUnixTime(seconds: number): string { + try { + return new Date(seconds * 1000).toISOString().replace('T', ' ').replace('Z', ' UTC'); + } catch { + return String(seconds); + } +} + +export function base64UrlDecode(s: string): string { + const b64 = s.replace(/-/g, '+').replace(/_/g, '/'); + const padded = b64 + '=='.slice((b64.length + 3) & 3); + return atob(padded); +} + +export function parseJwt(jwt: string): { + header: Record; + payload: Record; + signaturePreview: string; +} { + const parts = jwt.split('.'); + if (parts.length !== 3) throw new Error('Not a 3-part JWT'); + + const header = JSON.parse(base64UrlDecode(parts[0]!)) as Record; + const payload = JSON.parse(base64UrlDecode(parts[1]!)) as Record; + const signaturePreview = parts[2]!.slice(0, 16) + '…'; + + return { header, payload, signaturePreview }; +} diff --git a/packages/devtools-extension/src/panel/panel.ts b/packages/devtools-extension/src/panel/panel.ts index 0e041fdf92..9f635a2d45 100644 --- a/packages/devtools-extension/src/panel/panel.ts +++ b/packages/devtools-extension/src/panel/panel.ts @@ -4,6 +4,7 @@ import type { FlowExport } from '@forgerock/devtools-types'; import { redactFlowState } from '../export/redact.js'; import { renderFlowMarkdown } from '../export/markdown.js'; import { runDiagnosis } from '../background/diagnosis-engine.js'; +import { formatUnixTime, parseJwt } from './jwt.js'; declare const Elm: { Main: { @@ -104,14 +105,6 @@ function initResizeHandles() { // ── JWT Decoder ─────────────────────────────────────────────────────────────── -function formatUnixTime(seconds: number): string { - try { - return new Date(seconds * 1000).toISOString().replace('T', ' ').replace('Z', ' UTC'); - } catch { - return String(seconds); - } -} - function makeEl( tag: K, classes: string[], @@ -165,27 +158,16 @@ function buildJwtSection(title: string, obj: Record): DocumentF return frag; } -function base64UrlDecode(s: string): string { - const b64 = s.replace(/-/g, '+').replace(/_/g, '/'); - const padded = b64 + '=='.slice((b64.length + 3) & 3); - return atob(padded); -} - function buildJwtBody(jwt: string): HTMLElement { const body = makeEl('div', ['jwt-body']); try { - const parts = jwt.split('.'); - if (parts.length !== 3) throw new Error('Not a 3-part JWT'); - - const header = JSON.parse(base64UrlDecode(parts[0]!)) as Record; - const payload = JSON.parse(base64UrlDecode(parts[1]!)) as Record; - const sigPreview = parts[2]!.slice(0, 16) + '…'; + const { header, payload, signaturePreview } = parseJwt(jwt); body.appendChild(buildJwtSection('Header', header)); body.appendChild(buildJwtSection('Claims', payload)); body.appendChild(makeEl('div', ['jwt-section-hdr'], 'Signature')); - body.appendChild(makeEl('span', ['jwt-sig'], `${sigPreview} (not verified)`)); + body.appendChild(makeEl('span', ['jwt-sig'], `${signaturePreview} (not verified)`)); } catch (err) { body.appendChild(makeEl('span', ['jwt-err'], `Could not decode JWT: ${String(err)}`)); } diff --git a/packages/devtools-extension/src/panel/src/Decode.elm b/packages/devtools-extension/src/panel/src/Decode.elm index 600d2ea7e5..d09c46688e 100644 --- a/packages/devtools-extension/src/panel/src/Decode.elm +++ b/packages/devtools-extension/src/panel/src/Decode.elm @@ -1,130 +1,219 @@ module Decode exposing (decodeAuthEvent, decodeDiagnosisResult, decodeImportMeta, decodeSnapshotMeta) import Json.Decode as JD +import Json.Decode.Pipeline exposing (hardcoded, optional, required) import Types exposing ( AuthEvent , DiagnosisResult , EventData(..) , EventIssue + , EventKind(..) + , EventSource(..) , FlowHealth(..) , FlowIssue , ImportMeta , JourneyData , NetworkData , NodeData + , NodeStatus(..) , OidcData , SdkAuthorization , SdkError , SessionData + , Severity(..) , SnapshotMeta ) +decodeSeverity : JD.Decoder Severity +decodeSeverity = + JD.string + |> JD.andThen + (\s -> + case s of + "error" -> + JD.succeed SevError + + "warning" -> + JD.succeed SevWarning + + _ -> + JD.succeed SevInfo + ) + + +decodeNodeStatus : JD.Decoder NodeStatus +decodeNodeStatus = + JD.string + |> JD.andThen + (\s -> + case s of + "continue" -> + JD.succeed Continue + + "success" -> + JD.succeed Success + + "error" -> + JD.succeed StatusError + + "failure" -> + JD.succeed Failure + + _ -> + JD.succeed UnknownStatus + ) + + +decodeEventKind : String -> String -> EventKind +decodeEventKind eventTypeStr sourceStr = + case eventTypeStr of + "sdk:node-change" -> + NodeChange + + "sdk:journey-step" -> + JourneyStep + + "sdk:oidc-state" -> + OidcState + + "sdk:config" -> + SdkConfig + + _ -> + if sourceStr == "session" then + SessionEvent + + else if sourceStr == "network" then + NetworkEvent + + else + OtherKind eventTypeStr + + +decodeEventSource : String -> EventSource +decodeEventSource sourceStr = + case sourceStr of + "network" -> + NetworkSource + + "sdk" -> + SdkSource + + "session" -> + SessionSource + + _ -> + OtherSource sourceStr + + decodeSdkError : JD.Decoder SdkError decodeSdkError = - JD.map4 SdkError - (JD.field "code" JD.string) - (JD.field "message" JD.string) - (JD.field "type" JD.string) - (JD.maybe (JD.field "internalHttpStatus" JD.int)) + JD.succeed SdkError + |> required "code" JD.string + |> required "message" JD.string + |> required "type" JD.string + |> optional "internalHttpStatus" (JD.nullable JD.int) Nothing decodeSdkAuthorization : JD.Decoder SdkAuthorization decodeSdkAuthorization = - JD.map2 SdkAuthorization - (JD.maybe (JD.field "code" JD.string)) - (JD.maybe (JD.field "state" JD.string)) + JD.succeed SdkAuthorization + |> optional "code" (JD.nullable JD.string) Nothing + |> optional "state" (JD.nullable JD.string) Nothing decodeNetworkData : JD.Decoder NetworkData decodeNetworkData = JD.succeed NetworkData - |> andMap (JD.maybe (JD.field "status" JD.int)) - |> andMap (JD.maybe (JD.field "url" JD.string)) - |> andMap (JD.maybe (JD.field "method" JD.string)) - |> andMap (JD.maybe (JD.field "duration" JD.float)) - |> andMap (JD.maybe (JD.field "requestHeaders" JD.value)) - |> andMap (JD.maybe (JD.field "responseHeaders" JD.value)) - |> andMap (JD.maybe (JD.field "requestBody" JD.value)) - |> andMap (JD.maybe (JD.field "responseBody" JD.value)) + |> optional "status" (JD.nullable JD.int) Nothing + |> optional "url" (JD.nullable JD.string) Nothing + |> optional "method" (JD.nullable JD.string) Nothing + |> optional "duration" (JD.nullable JD.float) Nothing + |> optional "requestHeaders" (JD.nullable JD.value) Nothing + |> optional "responseHeaders" (JD.nullable JD.value) Nothing + |> optional "requestBody" (JD.nullable JD.value) Nothing + |> optional "responseBody" (JD.nullable JD.value) Nothing decodeNodeData : JD.Decoder NodeData decodeNodeData = JD.succeed NodeData - |> andMap (JD.maybe (JD.field "nodeStatus" JD.string)) - |> andMap (JD.maybe (JD.field "previousStatus" JD.string)) - |> andMap (JD.maybe (JD.field "interactionId" JD.string)) - |> andMap (JD.maybe (JD.field "interactionToken" JD.string)) - |> andMap (JD.maybe (JD.field "nodeId" JD.string)) - |> andMap (JD.maybe (JD.field "requestId" JD.string)) - |> andMap (JD.maybe (JD.field "nodeName" JD.string)) - |> andMap (JD.maybe (JD.field "nodeDescription" JD.string)) - |> andMap (JD.maybe (JD.field "eventName" JD.string)) - |> andMap (JD.maybe (JD.field "httpStatus" JD.int)) - |> andMap (JD.maybe (JD.field "error" decodeSdkError)) - |> andMap (JD.maybe (JD.field "authorization" decodeSdkAuthorization)) - |> andMap (JD.maybe (JD.field "session" JD.string)) - |> andMap (JD.maybe (JD.field "collectors" (JD.list JD.value))) - |> andMap (JD.maybe (JD.field "responseBody" JD.value)) + |> optional "nodeStatus" (JD.nullable decodeNodeStatus) Nothing + |> optional "previousStatus" (JD.nullable decodeNodeStatus) Nothing + |> optional "interactionId" (JD.nullable JD.string) Nothing + |> optional "interactionToken" (JD.nullable JD.string) Nothing + |> optional "nodeId" (JD.nullable JD.string) Nothing + |> optional "requestId" (JD.nullable JD.string) Nothing + |> optional "nodeName" (JD.nullable JD.string) Nothing + |> optional "nodeDescription" (JD.nullable JD.string) Nothing + |> optional "eventName" (JD.nullable JD.string) Nothing + |> optional "httpStatus" (JD.nullable JD.int) Nothing + |> optional "error" (JD.nullable decodeSdkError) Nothing + |> optional "authorization" (JD.nullable decodeSdkAuthorization) Nothing + |> optional "session" (JD.nullable JD.string) Nothing + |> optional "collectors" (JD.nullable (JD.list JD.value)) Nothing + |> optional "responseBody" (JD.nullable JD.value) Nothing decodeJourneyData : JD.Decoder JourneyData decodeJourneyData = JD.succeed JourneyData - |> andMap (JD.maybe (JD.field "stepType" JD.string)) - |> andMap (JD.maybe (JD.field "stage" JD.string)) - |> andMap (JD.maybe (JD.field "header" JD.string)) - |> andMap (JD.maybe (JD.field "description" JD.string)) - |> andMap (JD.maybe (JD.field "callbacks" (JD.list JD.value))) - |> andMap (JD.maybe (JD.field "authId" JD.string)) - |> andMap (JD.maybe (JD.field "tokenId" JD.string)) - |> andMap (JD.maybe (JD.field "successUrl" JD.string)) - |> andMap (JD.maybe (JD.field "errorCode" JD.int)) - |> andMap (JD.maybe (JD.field "errorMessage" JD.string)) - |> andMap (JD.maybe (JD.field "errorReason" JD.string)) + |> optional "stepType" (JD.nullable JD.string) Nothing + |> optional "stage" (JD.nullable JD.string) Nothing + |> optional "header" (JD.nullable JD.string) Nothing + |> optional "description" (JD.nullable JD.string) Nothing + |> optional "callbacks" (JD.nullable (JD.list JD.value)) Nothing + |> optional "authId" (JD.nullable JD.string) Nothing + |> optional "tokenId" (JD.nullable JD.string) Nothing + |> optional "successUrl" (JD.nullable JD.string) Nothing + |> optional "errorCode" (JD.nullable JD.int) Nothing + |> optional "errorMessage" (JD.nullable JD.string) Nothing + |> optional "errorReason" (JD.nullable JD.string) Nothing decodeOidcData : JD.Decoder OidcData decodeOidcData = JD.succeed OidcData - |> andMap (JD.maybe (JD.field "phase" JD.string)) - |> andMap (JD.maybe (JD.field "status" JD.string)) - |> andMap (JD.maybe (JD.field "clientId" JD.string)) - |> andMap (JD.maybe (JD.field "errorCode" JD.string)) - |> andMap (JD.maybe (JD.field "errorMessage" JD.string)) + |> optional "phase" (JD.nullable JD.string) Nothing + |> optional "status" (JD.nullable JD.string) Nothing + |> optional "clientId" (JD.nullable JD.string) Nothing + |> optional "errorCode" (JD.nullable JD.string) Nothing + |> optional "errorMessage" (JD.nullable JD.string) Nothing decodeSessionData : JD.Decoder SessionData decodeSessionData = JD.succeed SessionData - |> andMap (JD.maybe (JD.field "key" JD.string)) - |> andMap (JD.maybe (JD.field "before" JD.string)) - |> andMap (JD.maybe (JD.field "after" JD.string)) + |> optional "key" (JD.nullable JD.string) Nothing + |> optional "before" (JD.nullable JD.string) Nothing + |> optional "after" (JD.nullable JD.string) Nothing -decodeEventData : String -> String -> JD.Decoder EventData -decodeEventData eventTypeStr source = - case eventTypeStr of - "sdk:node-change" -> +decodeEventData : EventKind -> JD.Decoder EventData +decodeEventData kind = + case kind of + NodeChange -> JD.field "data" (JD.map DaVinciNode decodeNodeData) - "sdk:journey-step" -> + JourneyStep -> JD.field "data" (JD.map Journey decodeJourneyData) - "sdk:oidc-state" -> + OidcState -> JD.field "data" (JD.map Oidc decodeOidcData) - "sdk:config" -> + SdkConfig -> JD.map Config (JD.maybe (JD.at [ "data", "config" ] JD.value)) - _ -> - if source == "session" then - JD.field "data" (JD.map Session decodeSessionData) + SessionEvent -> + JD.field "data" (JD.map Session decodeSessionData) - else - JD.field "data" (JD.map Network decodeNetworkData) + NetworkEvent -> + JD.field "data" (JD.map Network decodeNetworkData) + + OtherKind _ -> + JD.field "data" (JD.map Network decodeNetworkData) decodeAuthEvent : JD.Decoder AuthEvent @@ -134,27 +223,29 @@ decodeAuthEvent = (\eventTypeStr -> JD.field "source" JD.string |> JD.andThen - (\source -> + (\sourceStr -> + let + kind = + decodeEventKind eventTypeStr sourceStr + + source = + decodeEventSource sourceStr + in JD.succeed AuthEvent - |> andMap (JD.field "id" JD.string) - |> andMap (JD.field "timestamp" JD.float) - |> andMap (JD.succeed eventTypeStr) - |> andMap (JD.succeed source) - |> andMap (JD.field "flowId" (JD.nullable JD.string)) - |> andMap (JD.at [ "flags", "isCors" ] JD.bool) - |> andMap (JD.at [ "flags", "isError" ] JD.bool) - |> andMap (JD.at [ "flags", "isAuthRelated" ] JD.bool) - |> andMap (JD.field "causedBy" (JD.nullable JD.string)) - |> andMap (decodeEventData eventTypeStr source) + |> required "id" JD.string + |> required "timestamp" JD.float + |> hardcoded kind + |> hardcoded source + |> required "flowId" (JD.nullable JD.string) + |> Json.Decode.Pipeline.custom (JD.at [ "flags", "isCors" ] JD.bool) + |> Json.Decode.Pipeline.custom (JD.at [ "flags", "isError" ] JD.bool) + |> Json.Decode.Pipeline.custom (JD.at [ "flags", "isAuthRelated" ] JD.bool) + |> required "causedBy" (JD.nullable JD.string) + |> Json.Decode.Pipeline.custom (decodeEventData kind) ) ) -andMap : JD.Decoder a -> JD.Decoder (a -> b) -> JD.Decoder b -andMap = - JD.map2 (|>) - - decodeRelevantData : JD.Decoder (Maybe (List ( String, String ))) decodeRelevantData = JD.maybe @@ -165,25 +256,25 @@ decodeRelevantData = decodeEventIssue : JD.Decoder EventIssue decodeEventIssue = - JD.map5 EventIssue - (JD.field "severity" JD.string) - (JD.field "title" JD.string) - (JD.field "description" JD.string) - (JD.field "steps" (JD.list JD.string)) - decodeRelevantData + JD.succeed EventIssue + |> required "severity" decodeSeverity + |> required "title" JD.string + |> required "description" JD.string + |> required "steps" (JD.list JD.string) + |> Json.Decode.Pipeline.custom decodeRelevantData decodeFlowIssue : JD.Decoder FlowIssue decodeFlowIssue = - JD.map8 FlowIssue - (JD.field "id" JD.string) - (JD.field "severity" JD.string) - (JD.field "category" JD.string) - (JD.field "title" JD.string) - (JD.field "description" JD.string) - (JD.field "steps" (JD.list JD.string)) - (JD.field "relatedEventIds" (JD.list JD.string)) - decodeRelevantData + JD.succeed FlowIssue + |> required "id" JD.string + |> required "severity" decodeSeverity + |> required "category" JD.string + |> required "title" JD.string + |> required "description" JD.string + |> required "steps" (JD.list JD.string) + |> required "relatedEventIds" (JD.list JD.string) + |> Json.Decode.Pipeline.custom decodeRelevantData decodeFlowHealth : JD.Decoder FlowHealth @@ -205,26 +296,24 @@ decodeFlowHealth = decodeDiagnosisResult : JD.Decoder DiagnosisResult decodeDiagnosisResult = - JD.map3 DiagnosisResult - (JD.field "flowHealth" decodeFlowHealth) - (JD.field "issues" (JD.list decodeFlowIssue)) - (JD.field "annotatedEvents" - (JD.keyValuePairs (JD.list decodeEventIssue)) - ) + JD.succeed DiagnosisResult + |> required "flowHealth" decodeFlowHealth + |> required "issues" (JD.list decodeFlowIssue) + |> required "annotatedEvents" (JD.keyValuePairs (JD.list decodeEventIssue)) decodeImportMeta : JD.Decoder ImportMeta decodeImportMeta = - JD.map3 ImportMeta - (JD.field "flowId" (JD.nullable JD.string)) - (JD.field "capturedAt" JD.string) - (JD.field "redacted" JD.bool) + JD.succeed ImportMeta + |> required "flowId" (JD.nullable JD.string) + |> required "capturedAt" JD.string + |> required "redacted" JD.bool decodeSnapshotMeta : JD.Decoder SnapshotMeta decodeSnapshotMeta = - JD.map4 SnapshotMeta - (JD.field "id" JD.string) - (JD.field "savedAt" JD.string) - (JD.field "flowId" (JD.nullable JD.string)) - (JD.field "eventCount" JD.int) + JD.succeed SnapshotMeta + |> required "id" JD.string + |> required "savedAt" JD.string + |> required "flowId" (JD.nullable JD.string) + |> required "eventCount" JD.int diff --git a/packages/devtools-extension/src/panel/src/FlowView.elm b/packages/devtools-extension/src/panel/src/FlowView.elm index 435301932d..d398ed8244 100644 --- a/packages/devtools-extension/src/panel/src/FlowView.elm +++ b/packages/devtools-extension/src/panel/src/FlowView.elm @@ -10,28 +10,45 @@ import Set exposing (Set) import Svg exposing (..) import Svg.Attributes as SA import Svg.Events -import Types exposing (AuthEvent, EventData(..), JourneyData, NetworkData, NodeData, OidcData) +import Types exposing (AuthEvent, EventData(..), JourneyData, NetworkData, NodeData, NodeStatus(..), OidcData) import Update exposing (Msg(..)) --- Unified status string for a node — works for DaVinci, Journey, and OIDC events. -nodeStatusLabel : AuthEvent -> String -nodeStatusLabel event = +nodeStatusFromEvent : AuthEvent -> NodeStatus +nodeStatusFromEvent event = case event.data of Oidc oidc -> - Maybe.withDefault "unknown" oidc.status + case oidc.status of + Just "success" -> + Success + + Just "error" -> + StatusError + + _ -> + UnknownStatus Journey journey -> - Maybe.withDefault "Step" journey.stepType + case journey.stepType of + Just "LoginSuccess" -> + Success + + Just "LoginFailure" -> + Failure + + Just "Step" -> + Continue + + _ -> + UnknownStatus DaVinciNode node -> - Maybe.withDefault "unknown" node.nodeStatus + Maybe.withDefault UnknownStatus node.nodeStatus _ -> - "unknown" + UnknownStatus --- Unified display label for the rail node. nodeDisplayLabel : AuthEvent -> String nodeDisplayLabel event = case event.data of @@ -159,7 +176,7 @@ renderRailNode selectedNodeId index event = 44 status = - nodeStatusLabel event + nodeStatusFromEvent event color = Helpers.nodeColor status @@ -170,6 +187,9 @@ renderRailNode selectedNodeId index event = label = nodeDisplayLabel event + statusLabel = + Helpers.nodeStatusLabel status + glowRing = if isSelected then [ Svg.circle @@ -208,7 +228,7 @@ renderRailNode selectedNodeId index event = ] [ Svg.text (truncate_ 14 label) ] - statusLabel = + statusText = Svg.text_ [ SA.x (String.fromInt cx_) , SA.y (String.fromInt (cy_ + nodeRadius + 26)) @@ -217,14 +237,14 @@ renderRailNode selectedNodeId index event = , SA.fill color , SA.fontFamily "'Segoe UI', system-ui, sans-serif" ] - [ Svg.text status ] + [ Svg.text statusLabel ] in glowRing ++ [ Svg.g [ Svg.Events.onClick (SelectFlowNode event.id) , SA.style "cursor:pointer" ] - [ nodeBg, nameLabel, statusLabel ] + [ nodeBg, nameLabel, statusText ] ] diff --git a/packages/devtools-extension/src/panel/src/Graph.elm b/packages/devtools-extension/src/panel/src/Graph.elm index eb6a7fa09e..e60d15a2e4 100644 --- a/packages/devtools-extension/src/panel/src/Graph.elm +++ b/packages/devtools-extension/src/panel/src/Graph.elm @@ -7,7 +7,7 @@ import Json.Decode as Decode import Svg exposing (..) import Svg.Attributes as SA import Svg.Events -import Types exposing (AuthEvent, EventData(..)) +import Types exposing (AuthEvent, EventData(..), EventKind(..), NodeStatus(..)) import Update exposing (Msg(..)) @@ -15,7 +15,7 @@ view : List AuthEvent -> Maybe String -> Maybe String -> Html Msg view events selectedId hoveredId = let sdkNodes = - List.filter (\e -> e.eventType == "sdk:node-change") events + List.filter (\e -> e.kind == NodeChange) events nodeSpacing = 90 @@ -60,10 +60,10 @@ renderNode spacing selectedId hoveredId index event = ( status, maybeName, maybeCollectors ) = case event.data of DaVinciNode node -> - ( Maybe.withDefault "unknown" node.nodeStatus, node.nodeName, node.collectors ) + ( Maybe.withDefault UnknownStatus node.nodeStatus, node.nodeName, node.collectors ) _ -> - ( "unknown", Nothing, Nothing ) + ( UnknownStatus, Nothing, Nothing ) color = Helpers.nodeColor status @@ -74,6 +74,9 @@ renderNode spacing selectedId hoveredId index event = isHovered = hoveredId == Just event.id + isHighlighted = + isSelected || isHovered + connectorLine = if index > 0 then [ line @@ -84,7 +87,7 @@ renderNode spacing selectedId hoveredId index event = , SA.stroke color , SA.strokeWidth "1" , SA.strokeOpacity - (if isHovered || isSelected then + (if isHighlighted then "0.5" else @@ -134,10 +137,7 @@ renderNode spacing selectedId hoveredId index event = [] nodeFilterAttr = - if isSelected then - SA.filter "url(#glow-node)" - - else if isHovered then + if isHighlighted then SA.filter "url(#glow-node)" else @@ -159,7 +159,7 @@ renderNode spacing selectedId hoveredId index event = , SA.y (String.fromInt (cy_ + 4)) , SA.fontSize "12" , SA.fill - (if isHovered || isSelected then + (if isHighlighted then "#ffffff" else @@ -167,12 +167,12 @@ renderNode spacing selectedId hoveredId index event = ) , SA.fontFamily "'Segoe UI', system-ui, sans-serif" ] - [ Svg.text status ] + [ Svg.text (Helpers.nodeStatusLabel status) ] subLabel = let subFill = - if isHovered || isSelected then + if isHighlighted then "#c9d1d9" else @@ -210,7 +210,6 @@ renderNode spacing selectedId hoveredId index event = Nothing -> [] - -- Invisible hit area for reliable mouse events hitArea = rect [ SA.x "0" diff --git a/packages/devtools-extension/src/panel/src/Helpers.elm b/packages/devtools-extension/src/panel/src/Helpers.elm index ecbb8043d6..adb12d85ab 100644 --- a/packages/devtools-extension/src/panel/src/Helpers.elm +++ b/packages/devtools-extension/src/panel/src/Helpers.elm @@ -1,67 +1,17 @@ module Helpers exposing - ( EventSource(..) - , EventType(..) - , eventSource - , eventType - , findEvent + ( findEvent , findEventInList , isSdkNode , methodClass , nodeColor + , nodeStatusLabel , sdkNodes , statusClass , truncateId ) import Dict exposing (Dict) -import Types exposing (AuthEvent, EventData(..)) - - -type EventType - = NodeChange - | JourneyStep - | OidcState - | SdkConfig - | NetworkEvent - | OtherEvent String - - -type EventSource - = SessionSource - | OtherSource String - - -eventType : AuthEvent -> EventType -eventType event = - case event.eventType of - "sdk:node-change" -> - NodeChange - - "sdk:journey-step" -> - JourneyStep - - "sdk:oidc-state" -> - OidcState - - "sdk:config" -> - SdkConfig - - _ -> - if event.source == "network" then - NetworkEvent - - else - OtherEvent event.eventType - - -eventSource : AuthEvent -> EventSource -eventSource event = - case event.source of - "session" -> - SessionSource - - other -> - OtherSource other +import Types exposing (AuthEvent, EventData(..), EventKind(..), NodeStatus(..)) isSdkNode : AuthEvent -> Bool @@ -140,32 +90,42 @@ methodClass maybeMethod = "m-other" -nodeColor : String -> String +nodeColor : NodeStatus -> String nodeColor status = case status of - "continue" -> + Continue -> "#58A6FF" - "success" -> + Success -> "#3FB950" - "error" -> + StatusError -> "#F85149" - "failure" -> + Failure -> "#F85149" - "Step" -> - "#58A6FF" + UnknownStatus -> + "#484F58" - "LoginSuccess" -> - "#3FB950" - "LoginFailure" -> - "#F85149" +nodeStatusLabel : NodeStatus -> String +nodeStatusLabel status = + case status of + Continue -> + "continue" - _ -> - "#484F58" + Success -> + "success" + + StatusError -> + "error" + + Failure -> + "failure" + + UnknownStatus -> + "unknown" truncateId : String -> String diff --git a/packages/devtools-extension/src/panel/src/Inspector.elm b/packages/devtools-extension/src/panel/src/Inspector.elm index cab1afefa7..ccdfcc0a3d 100644 --- a/packages/devtools-extension/src/panel/src/Inspector.elm +++ b/packages/devtools-extension/src/panel/src/Inspector.elm @@ -7,7 +7,7 @@ import Html.Events exposing (onClick) import Json.Decode as Decode import Json.Encode as Encode import JsonTree -import Types exposing (AuthEvent, DiagnosisResult, EventData(..), EventIssue, InspectorTab(..), NetworkData, NodeData, SessionData, SdkAuthorization, SdkError) +import Types exposing (AuthEvent, DiagnosisResult, EventData(..), EventIssue, EventKind(..), EventSource(..), InspectorTab(..), NetworkData, NodeData, NodeStatus(..), SdkAuthorization, SdkError, SessionData, Severity(..)) import Update exposing (Msg(..)) @@ -38,12 +38,7 @@ viewTabs maybeEvent activeTab maybeDiagnosis = isSdkEvent = case maybeEvent of Just event -> - case event.data of - DaVinciNode _ -> - True - - _ -> - False + event.kind == NodeChange Nothing -> False @@ -51,12 +46,7 @@ viewTabs maybeEvent activeTab maybeDiagnosis = isSessionEvent = case maybeEvent of Just event -> - case event.data of - Session _ -> - True - - _ -> - False + event.source == SessionSource Nothing -> False @@ -64,12 +54,7 @@ viewTabs maybeEvent activeTab maybeDiagnosis = isConfigEvent = case maybeEvent of Just event -> - case event.data of - Config _ -> - True - - _ -> - False + event.kind == SdkConfig Nothing -> False @@ -81,7 +66,7 @@ viewTabs maybeEvent activeTab maybeDiagnosis = not (List.isEmpty issues) diagnosisTabLabel = - if List.any (\i -> i.severity == "error") issues then + if List.any (\i -> i.severity == SevError) issues then "Diagnosis ●" else @@ -251,6 +236,16 @@ viewSdkState node = ) +nodeStatusValClass : NodeStatus -> String +nodeStatusValClass status = + case status of + StatusError -> "kv-val kv-bold kv-err" + Failure -> "kv-val kv-bold kv-err" + Success -> "kv-val kv-bold kv-ok" + Continue -> "kv-val kv-bold kv-cont" + UnknownStatus -> "kv-val" + + viewNodeSection : NodeData -> List (Html Msg) viewNodeSection node = let @@ -258,25 +253,20 @@ viewNodeSection node = case node.nodeStatus of Just s -> let - valClass = - case s of - "error" -> "kv-val kv-bold kv-err" - "failure" -> "kv-val kv-bold kv-err" - "success" -> "kv-val kv-bold kv-ok" - "continue" -> "kv-val kv-bold kv-cont" - _ -> "kv-val" + label = + Helpers.nodeStatusLabel s arrow = case node.previousStatus of Just prev -> span [] - [ span [ class "kv-arrow" ] [ text (prev ++ " ") ] + [ span [ class "kv-arrow" ] [ text (Helpers.nodeStatusLabel prev ++ " ") ] , span [ class "kv-arrow" ] [ text "→ " ] - , span [ class valClass ] [ text s ] + , span [ class (nodeStatusValClass s) ] [ text label ] ] Nothing -> - span [ class valClass ] [ text s ] + span [ class (nodeStatusValClass s) ] [ text label ] in [ viewRow "Status" arrow ] @@ -544,15 +534,15 @@ viewEventIssue issue = let severityClass = case issue.severity of - "error" -> "diag-issue diag-issue-error" - "warning" -> "diag-issue diag-issue-warning" - _ -> "diag-issue diag-issue-info" + SevError -> "diag-issue diag-issue-error" + SevWarning -> "diag-issue diag-issue-warning" + SevInfo -> "diag-issue diag-issue-info" severityIcon = case issue.severity of - "error" -> "✕ " - "warning" -> "⚠ " - _ -> "ℹ " + SevError -> "✕ " + SevWarning -> "⚠ " + SevInfo -> "ℹ " stepItems = List.indexedMap diff --git a/packages/devtools-extension/src/panel/src/LearnView.elm b/packages/devtools-extension/src/panel/src/LearnView.elm index 595627f868..5f9aac3b3a 100644 --- a/packages/devtools-extension/src/panel/src/LearnView.elm +++ b/packages/devtools-extension/src/panel/src/LearnView.elm @@ -7,7 +7,7 @@ import Json.Decode as JD import Svg exposing (..) import Svg.Attributes as SA import Svg.Events -import Types exposing (AuthEvent, CanvasState, CardId(..), EventData(..), NetworkData, NodeData) +import Types exposing (AuthEvent, CanvasState, CardId(..), EventData(..), NetworkData, NodeData, SdkError) import Update exposing (Msg(..)) @@ -226,14 +226,67 @@ viewCanvas events canvas = responseEvent = List.head (List.reverse netEvents) - hasError = + -- Error attribution: where did the error originate? + sdkNodeError = case maybeNode of Just n -> - n.isError + case n.data of + DaVinciNode nd -> + nd.sdkError + + _ -> + Nothing + + Nothing -> + Nothing + + sdkNodeStatus = + case maybeNode of + Just n -> + case n.data of + DaVinciNode nd -> + nd.nodeStatus + + _ -> + Nothing + + Nothing -> + Nothing + + serverHasError = + case responseEvent of + Just re -> + case re.data of + Network nd -> + case nd.status of + Just s -> + s >= 400 + + Nothing -> + False + + _ -> + False + + Nothing -> + False + + sdkHasError = + sdkNodeError /= Nothing + || sdkNodeStatus == Just Types.StatusError + || sdkNodeStatus == Just Types.Failure + + corsError = + case maybeNode of + Just n -> + n.isCors Nothing -> False + anyError = + serverHasError || sdkHasError || corsError + hasCollectors = case maybeNode of Just n -> @@ -260,37 +313,22 @@ viewCanvas events canvas = Nothing -> "POST" - responseStatus = + responseStatusCode = case responseEvent of Just re -> case re.data of Network nd -> - Maybe.map String.fromInt nd.status - |> Maybe.withDefault "—" + nd.status _ -> - "—" + Nothing Nothing -> - "—" + Nothing - serverHasError = - case responseEvent of - Just re -> - case re.data of - Network nd -> - case nd.status of - Just s -> - s >= 400 - - Nothing -> - False - - _ -> - False - - Nothing -> - False + responseStatus = + Maybe.map String.fromInt responseStatusCode + |> Maybe.withDefault "—" noNetEvents = List.isEmpty netEvents @@ -326,7 +364,7 @@ viewCanvas events canvas = ] [ canvasDefs , Svg.g [ SA.transform transform ] - (renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus maybeNode requestEvent responseEvent) + (renderCards canvas serverHasError sdkHasError corsError hasCollectors noNetEvents requestMethod responseStatus maybeNode requestEvent responseEvent sdkNodeError) ] ] @@ -351,9 +389,12 @@ canvasDefs = ] -renderCards : CanvasState -> Bool -> Bool -> Bool -> Bool -> String -> String -> Maybe AuthEvent -> Maybe AuthEvent -> Maybe AuthEvent -> List (Svg Msg) -renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMethod responseStatus maybeNode requestEvent responseEvent = +renderCards : CanvasState -> Bool -> Bool -> Bool -> Bool -> Bool -> String -> String -> Maybe AuthEvent -> Maybe AuthEvent -> Maybe AuthEvent -> Maybe SdkError -> List (Svg Msg) +renderCards canvas serverHasError sdkHasError corsError hasCollectors noNetEvents requestMethod responseStatus maybeNode requestEvent responseEvent sdkNodeError = let + anyError = + serverHasError || sdkHasError || corsError + cardW = 160 @@ -429,8 +470,10 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth fH = toFloat cardH + -- Card border colors: only the error SOURCE gets red, + -- downstream cards show propagated red (dimmer) browserBorder = - if hasError then + if corsError then "#F85149" else @@ -444,97 +487,158 @@ renderCards canvas hasError serverHasError hasCollectors noNetEvents requestMeth "#484F58" sdkBorder = - if hasError then + if sdkHasError && not serverHasError then + -- SDK is the error source (not just receiving a server error) "#F85149" + else if serverHasError then + -- SDK received an error from server (propagated, not source) + "#b44a44" + else "#3FB950" formBorder = - if not hasCollectors then + if anyError then + "#484F58" + + else if not hasCollectors then "#484F58" else "#A371F7" formOpacity = - if hasCollectors then + if hasCollectors && not anyError then "1" else "0.4" formDash = - if hasCollectors then + if hasCollectors && not anyError then "" else "4,4" + + -- Arrow colors + requestArrowColor = + if corsError then + "#F85149" + + else + "#58A6FF" + + responseArrowColor = + if serverHasError then + "#F85149" + + else + "#3FB950" + + responseArrowLabel = + if serverHasError then + "✕ " ++ responseStatus + + else + "← " ++ responseStatus + + sdkContextLine = + if sdkHasError && not serverHasError then + "Error in SDK" + + else if serverHasError then + "Received error" + + else + "Processes response" + + formContextLine = + if anyError then + "Skipped" + + else if hasCollectors then + "Collects input" + + else + "No collectors" + + -- Error pulse: which card is the source? + pulseTarget = + if corsError then + Just ( bx, by ) + + else if serverHasError then + Just ( sx, sy ) + + else if sdkHasError then + Just ( sdx, sdy ) + + else + Nothing in [ -- Browser card renderCard BrowserCard bx by fW fH browserBorder "1" "" canvas.expandedCard (browserIcon (bx + 30) (by + 20)) "BROWSER" - "User interaction" + (if corsError then "CORS blocked" else "Sends request") , expandedPanel BrowserCard bx (by + fH + 8) fW canvas.expandedCard - (browserDetail requestEvent) + (browserDetail requestEvent corsError) -- Arrow: Browser -> Server - , renderArrowLine (bx + fW) (by + fH / 2) sx (sy + fH / 2) (requestMethod ++ " ->") + , renderArrowLine (bx + fW) (by + fH / 2) sx (sy + fH / 2) (requestMethod ++ " →") requestArrowColor False -- Server card , renderCard ServerCard sx sy fW fH serverBorder "1" "" canvas.expandedCard (serverIcon (sx + 40) (sy + 15)) "SERVER" - ("Response " ++ responseStatus) + (if serverHasError then "✕ " ++ responseStatus else responseStatus ++ " OK") , expandedPanel ServerCard sx (sy + fH + 8) fW canvas.expandedCard - (serverDetail responseEvent) + (serverDetail responseEvent serverHasError) -- Arrow: Server -> SDK - , renderArrowLine (sx + fW) (sy + fH / 2) sdx (sdy + fH / 2) ("-> " ++ responseStatus) + , renderArrowLine (sx + fW) (sy + fH / 2) sdx (sdy + fH / 2) responseArrowLabel responseArrowColor False -- SDK card , renderCard SdkCard sdx sdy fW fH sdkBorder "1" "" canvas.expandedCard (sdkIcon (sdx + 35) (sdy + 20)) "SDK" - "Processes response" + sdkContextLine , expandedPanel SdkCard sdx (sdy + fH + 8) fW canvas.expandedCard - (sdkDetail maybeNode) + (sdkDetail maybeNode sdkNodeError serverHasError) -- Arrow: SDK -> Form - , renderArrowLine (sdx + fW) (sdy + fH / 2) fx (fy + fH / 2) "renders" + , renderArrowLine (sdx + fW) (sdy + fH / 2) fx (fy + fH / 2) "renders" "#484F58" True -- Form card , renderCard FormCard fx fy fW fH formBorder formOpacity formDash canvas.expandedCard (formIcon (fx + 35) (fy + 18)) "FORM" - (if hasCollectors then - "Collects input" - - else - "Skipped" - ) + formContextLine , expandedPanel FormCard fx (fy + fH + 8) fW canvas.expandedCard (formDetail maybeNode) - - -- Error pulse on source card when error ] - ++ (if hasError then - [ Svg.circle - [ SA.cx (String.fromFloat (sdx + fW / 2)) - , SA.cy (String.fromFloat (sdy + fH / 2)) - , SA.r (String.fromFloat (fW / 2 + 8)) - , SA.fill "none" - , SA.stroke "#F85149" - , SA.strokeWidth "2" - , SA.strokeOpacity "0.6" - , SA.class "lv-pulse" + -- Error pulse ring on the SOURCE card + ++ (case pulseTarget of + Just ( px, py ) -> + [ Svg.rect + [ SA.x (String.fromFloat (px - 6)) + , SA.y (String.fromFloat (py - 6)) + , SA.width (String.fromFloat (fW + 12)) + , SA.height (String.fromFloat (fH + 12)) + , SA.rx "12" + , SA.fill "none" + , SA.stroke "#F85149" + , SA.strokeWidth "2" + , SA.strokeOpacity "0.6" + , SA.class "lv-pulse" + ] + [] ] - [] - ] - else - [] + Nothing -> + [] ) ++ (if noNetEvents then [ Svg.text_ @@ -614,8 +718,8 @@ renderCard cardId x y w h borderColor opacity dashArray expandedCard icon label ] -renderArrowLine : Float -> Float -> Float -> Float -> String -> Svg Msg -renderArrowLine x1 y1 x2 y2 label = +renderArrowLine : Float -> Float -> Float -> Float -> String -> String -> Bool -> Svg Msg +renderArrowLine x1 y1 x2 y2 label color isDashed = let midX = (x1 + x2) / 2 @@ -625,22 +729,30 @@ renderArrowLine x1 y1 x2 y2 label = in Svg.g [] [ Svg.line - [ SA.x1 (String.fromFloat x1) - , SA.y1 (String.fromFloat y1) - , SA.x2 (String.fromFloat x2) - , SA.y2 (String.fromFloat y2) - , SA.stroke "#484F58" - , SA.strokeWidth "1.5" - , SA.markerEnd "url(#lv-card-arrow)" - ] + ([ SA.x1 (String.fromFloat x1) + , SA.y1 (String.fromFloat y1) + , SA.x2 (String.fromFloat x2) + , SA.y2 (String.fromFloat y2) + , SA.stroke color + , SA.strokeWidth "1.5" + , SA.markerEnd "url(#lv-card-arrow)" + ] + ++ (if isDashed then + [ SA.strokeDasharray "5 3" ] + + else + [] + ) + ) [] , Svg.text_ [ SA.x (String.fromFloat midX) , SA.y (String.fromFloat midY) , SA.textAnchor "middle" , SA.fontSize "9" - , SA.fill "#8B949E" + , SA.fill color , SA.fontFamily "'Segoe UI', system-ui, sans-serif" + , SA.fontWeight "600" ] [ Svg.text label ] ] @@ -657,7 +769,7 @@ expandedPanel cardId x y w expandedCard content = [ SA.x (String.fromFloat x) , SA.y (String.fromFloat y) , SA.width (String.fromFloat w) - , SA.height "80" + , SA.height "120" ] [ Html.div [ Html.Attributes.style "font-family" "'Segoe UI', system-ui, sans-serif" @@ -677,31 +789,43 @@ expandedPanel cardId x y w expandedCard content = detailRow : String -> String -> Html Msg detailRow label value = + detailRowColored label value "#e6edf3" + + +detailRowColored : String -> String -> String -> Html Msg +detailRowColored label value color = Html.div [ Html.Attributes.style "margin-bottom" "2px" ] [ Html.span [ Html.Attributes.style "color" "#484f58" ] [ Html.text (label ++ " ") ] - , Html.span [ Html.Attributes.style "color" "#e6edf3" ] [ Html.text value ] + , Html.span [ Html.Attributes.style "color" color, Html.Attributes.style "font-weight" "600" ] [ Html.text value ] ] -browserDetail : Maybe AuthEvent -> List (Html Msg) -browserDetail requestEvent = - case requestEvent of - Just re -> - case re.data of - Network nd -> - [ detailRow "Method" (Maybe.withDefault "POST" nd.method) - , detailRow "URL" (truncate_ 22 (Maybe.withDefault "—" nd.url)) - ] +browserDetail : Maybe AuthEvent -> Bool -> List (Html Msg) +browserDetail requestEvent corsError = + (if corsError then + [ detailRowColored "CORS" "Request blocked by browser" "#F85149" ] - _ -> - [ Html.text "No request data" ] + else + [] + ) + ++ (case requestEvent of + Just re -> + case re.data of + Network nd -> + [ detailRow "Method" (Maybe.withDefault "POST" nd.method) + , detailRow "URL" (Maybe.withDefault "—" nd.url) + ] + + _ -> + [ Html.text "No request data" ] - Nothing -> - [ Html.text "No request captured" ] + Nothing -> + [ Html.text "No request captured" ] + ) -serverDetail : Maybe AuthEvent -> List (Html Msg) -serverDetail responseEvent = +serverDetail : Maybe AuthEvent -> Bool -> List (Html Msg) +serverDetail responseEvent isError = case responseEvent of Just re -> case re.data of @@ -711,16 +835,27 @@ serverDetail responseEvent = Maybe.map String.fromInt nd.status |> Maybe.withDefault "—" + statusColor = + if isError then + "#F85149" + + else + "#3FB950" + durationStr = case nd.duration of Just d -> - String.fromFloat d ++ "ms" + String.fromInt (round d) ++ "ms" Nothing -> "—" + + urlStr = + Maybe.withDefault "—" nd.url in - [ detailRow "Status" statusStr + [ detailRowColored "Status" statusStr statusColor , detailRow "Duration" durationStr + , detailRow "URL" (truncateUrl urlStr) ] _ -> @@ -730,32 +865,74 @@ serverDetail responseEvent = [ Html.text "No response captured" ] -sdkDetail : Maybe AuthEvent -> List (Html Msg) -sdkDetail maybeNode = +sdkDetail : Maybe AuthEvent -> Maybe Types.SdkError -> Bool -> List (Html Msg) +sdkDetail maybeNode sdkError serverErrored = + let + errorSection = + case sdkError of + Just err -> + [ detailRowColored "Error" err.code "#F85149" + , detailRow "Message" err.message + , detailRow "Type" err.errorType + ] + + Nothing -> + if serverErrored then + [ detailRowColored "Note" "Server returned an error" "#d29922" ] + + else + [] + in case maybeNode of Just n -> case n.data of DaVinciNode nd -> let - statusTransition = - (Maybe.map (Helpers.nodeStatusLabel >> (\s -> s)) nd.previousStatus + statusLabel = + Maybe.map Helpers.nodeStatusLabel nd.nodeStatus |> Maybe.withDefault "—" - ) - ++ " → " - ++ (Maybe.map Helpers.nodeStatusLabel nd.nodeStatus - |> Maybe.withDefault "—" - ) - formName = - Maybe.withDefault "—" nd.nodeName + statusColor = + case nd.nodeStatus of + Just Types.StatusError -> + "#F85149" - interactionId = - Maybe.withDefault "—" nd.interactionId + Just Types.Failure -> + "#F85149" + + Just Types.Success -> + "#3FB950" + + Just Types.Continue -> + "#58A6FF" + + _ -> + "#8b949e" + + transitionStr = + case nd.previousStatus of + Just prev -> + Helpers.nodeStatusLabel prev ++ " → " ++ statusLabel + + Nothing -> + statusLabel in - [ detailRow "Status" statusTransition - , detailRow "Node" (truncate_ 18 formName) - , detailRow "Interaction" (truncate_ 14 interactionId) - ] + [ detailRowColored "Status" transitionStr statusColor ] + ++ (case nd.nodeName of + Just name -> + [ detailRow "Node" name ] + + Nothing -> + [] + ) + ++ (case nd.interactionId of + Just iid -> + [ detailRow "Interaction" (truncate_ 14 iid) ] + + Nothing -> + [] + ) + ++ errorSection _ -> [ Html.text "Not a DaVinci node" ] @@ -1014,3 +1191,18 @@ truncate_ maxLen s = else String.left maxLen s ++ "…" + + +truncateUrl : String -> String +truncateUrl url = + let + stripped = + url + |> String.replace "https://" "" + |> String.replace "http://" "" + in + if String.length stripped > 28 then + String.left 28 stripped ++ "…" + + else + stripped diff --git a/packages/devtools-extension/src/panel/src/Timeline.elm b/packages/devtools-extension/src/panel/src/Timeline.elm index 5fd844a2bc..77a190c39d 100644 --- a/packages/devtools-extension/src/panel/src/Timeline.elm +++ b/packages/devtools-extension/src/panel/src/Timeline.elm @@ -4,18 +4,18 @@ import Helpers import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) -import Types exposing (AuthEvent, EventData(..)) +import Types exposing (AuthEvent, EventData(..), EventKind(..), EventSource(..), NodeStatus(..)) import Update exposing (Msg(..)) -nodeStatusClass : String -> String +nodeStatusClass : NodeStatus -> String nodeStatusClass status = case status of - "continue" -> "kv-cont" - "success" -> "kv-ok" - "error" -> "kv-err" - "failure" -> "kv-err" - _ -> "st-nil" + Continue -> "kv-cont" + Success -> "kv-ok" + StatusError -> "kv-err" + Failure -> "kv-err" + UnknownStatus -> "st-nil" view : List AuthEvent -> Maybe String -> Html Msg @@ -33,20 +33,18 @@ renderRow selectedId event = rowClass = if isSelected then "tl-row sel" else "tl-row" in - case Helpers.eventType event of - Helpers.NodeChange -> + case event.kind of + NodeChange -> renderSdkRow rowClass event - Helpers.SdkConfig -> + SdkConfig -> renderConfigRow rowClass event - _ -> - case Helpers.eventSource event of - Helpers.SessionSource -> - renderSessionRow rowClass event + SessionEvent -> + renderSessionRow rowClass event - _ -> - renderNetworkRow rowClass event + _ -> + renderNetworkRow rowClass event renderConfigRow : String -> AuthEvent -> Html Msg @@ -84,15 +82,18 @@ renderSdkRow rowClass event = DaVinciNode node -> let status = - Maybe.withDefault "unknown" node.nodeStatus + Maybe.withDefault UnknownStatus node.nodeStatus + + statusLabel = + Helpers.nodeStatusLabel status transitionLabel = case node.previousStatus of Just prev -> - prev ++ " → " ++ status + Helpers.nodeStatusLabel prev ++ " → " ++ statusLabel Nothing -> - status + statusLabel collectorTag = case node.collectors of diff --git a/packages/devtools-extension/tests/DecodeTests.elm b/packages/devtools-extension/tests/DecodeTests.elm new file mode 100644 index 0000000000..b2d877e378 --- /dev/null +++ b/packages/devtools-extension/tests/DecodeTests.elm @@ -0,0 +1,898 @@ +module DecodeTests exposing (suite) + +import Decode exposing (decodeAuthEvent, decodeDiagnosisResult, decodeImportMeta, decodeSnapshotMeta) +import Expect +import Json.Decode as JD +import Test exposing (Test, describe, test) +import Types exposing (EventData(..), EventKind(..), EventSource(..), FlowHealth(..), NodeStatus(..), Severity(..)) + + +baseEventJson : String -> String -> String -> String +baseEventJson eventType source dataJson = + """ + { "id": "evt-001" + , "timestamp": 1700000000000 + , "type": \"""" + ++ eventType + ++ """" + , "source": \"""" + ++ source + ++ """" + , "flowId": "flow-abc" + , "causedBy": null + , "flags": { "isCors": false, "isError": false, "isAuthRelated": true } + , "data": """ + ++ dataJson + ++ """ + }""" + + +suite : Test +suite = + describe "Decode" + [ decodeAuthEventTests + , decodeNodeDataSubObjectTests + , decodeDiagnosisTests + , decodeRelevantDataTests + , decodeImportMetaTests + , decodeSnapshotMetaTests + , decodeSeverityTests + ] + + +decodeAuthEventTests : Test +decodeAuthEventTests = + describe "decodeAuthEvent" + [ test "decodes a network event" <| + \_ -> + let + json = + baseEventJson "network:response" + "network" + """{ "_tag": "network", "url": "https://auth.example.com/token", "method": "POST", "status": 200, "duration": 123, "requestHeaders": {}, "responseHeaders": {} }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal "evt-001" e.id + , \e -> Expect.equal NetworkEvent e.kind + , \e -> Expect.equal NetworkSource e.source + , \e -> + case e.data of + Network nd -> + Expect.equal (Just 200) nd.status + + _ -> + Expect.fail "Expected Network data variant" + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes a sdk:node-change event" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk", "nodeStatus": "continue", "interactionId": "int-1", "nodeName": "Username" }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal NodeChange e.kind + , \e -> Expect.equal SdkSource e.source + , \e -> + case e.data of + DaVinciNode nd -> + Expect.all + [ \n -> Expect.equal (Just Continue) n.nodeStatus + , \n -> Expect.equal (Just "int-1") n.interactionId + , \n -> Expect.equal (Just "Username") n.nodeName + ] + nd + + _ -> + Expect.fail "Expected DaVinciNode data variant" + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes a sdk:journey-step event" <| + \_ -> + let + json = + baseEventJson "sdk:journey-step" + "sdk" + """{ "_tag": "journey", "stepType": "Step", "authId": "abc123", "stage": "UsernamePassword", "header": "Sign In" }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal JourneyStep e.kind + , \e -> + case e.data of + Journey jd -> + Expect.all + [ \j -> Expect.equal (Just "Step") j.stepType + , \j -> Expect.equal (Just "abc123") j.authId + , \j -> Expect.equal (Just "UsernamePassword") j.stage + ] + jd + + _ -> + Expect.fail "Expected Journey data variant" + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes a sdk:oidc-state event" <| + \_ -> + let + json = + baseEventJson "sdk:oidc-state" + "sdk" + """{ "_tag": "oidc", "phase": "authorize", "status": "success", "clientId": "my-app" }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal OidcState e.kind + , \e -> + case e.data of + Oidc od -> + Expect.all + [ \o -> Expect.equal (Just "authorize") o.phase + , \o -> Expect.equal (Just "success") o.status + , \o -> Expect.equal (Just "my-app") o.clientId + ] + od + + _ -> + Expect.fail "Expected Oidc data variant" + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes a session event" <| + \_ -> + let + json = + baseEventJson "session:storage" + "session" + """{ "_tag": "session", "key": "token", "before": "old", "after": "new" }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal SessionEvent e.kind + , \e -> Expect.equal SessionSource e.source + , \e -> + case e.data of + Session sd -> + Expect.all + [ \s -> Expect.equal (Just "token") s.key + , \s -> Expect.equal (Just "old") s.before + , \s -> Expect.equal (Just "new") s.after + ] + sd + + _ -> + Expect.fail "Expected Session data variant" + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes a sdk:config event" <| + \_ -> + let + json = + baseEventJson "sdk:config" + "sdk" + """{ "_tag": "sdk-config", "config": { "serverUrl": "https://auth.example.com" } }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal SdkConfig e.kind + , \e -> + case e.data of + Config _ -> + Expect.pass + + _ -> + Expect.fail "Expected Config data variant" + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes flags correctly" <| + \_ -> + let + json = + """ + { "id": "evt-cors" + , "timestamp": 100 + , "type": "network:response" + , "source": "network" + , "flowId": null + , "causedBy": null + , "flags": { "isCors": true, "isError": true, "isAuthRelated": false } + , "data": { "_tag": "network", "status": 0, "method": "POST", "url": "https://x.com", "duration": 0, "requestHeaders": {}, "responseHeaders": {} } + }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.all + [ \e -> Expect.equal True e.isCors + , \e -> Expect.equal True e.isError + , \e -> Expect.equal False e.isAuthRelated + , \e -> Expect.equal Nothing e.flowId + ] + event + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes causedBy string" <| + \_ -> + let + json = + """ + { "id": "evt-1" + , "timestamp": 100 + , "type": "network:response" + , "source": "network" + , "flowId": null + , "causedBy": "sdk-42" + , "flags": { "isCors": false, "isError": false, "isAuthRelated": true } + , "data": { "_tag": "network", "status": 200, "method": "GET", "url": "https://x.com", "duration": 10, "requestHeaders": {}, "responseHeaders": {} } + }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + Expect.equal (Just "sdk-42") event.causedBy + + Err err -> + Expect.fail (JD.errorToString err) + , test "falls back to Network for unknown event type with non-session source" <| + \_ -> + let + json = + baseEventJson "network:request" + "network" + """{ "_tag": "network", "status": 302, "method": "GET", "url": "https://x.com/redirect", "duration": 5, "requestHeaders": {}, "responseHeaders": {} }""" + + result = + JD.decodeString decodeAuthEvent json + in + case result of + Ok event -> + case event.data of + Network _ -> + Expect.pass + + _ -> + Expect.fail "Expected Network data variant for unknown type" + + Err err -> + Expect.fail (JD.errorToString err) + ] + + +decodeNodeDataSubObjectTests : Test +decodeNodeDataSubObjectTests = + describe "decodeAuthEvent (node sub-objects)" + [ test "decodes sdk:node-change with error sub-object" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk", "nodeStatus": "error", "error": { "code": "E001", "message": "Auth failed", "type": "authentication", "internalHttpStatus": 401 } }""" + in + case JD.decodeString decodeAuthEvent json of + Ok event -> + case event.data of + DaVinciNode nd -> + Expect.all + [ \n -> Expect.equal (Just StatusError) n.nodeStatus + , \n -> + case n.sdkError of + Just err -> + Expect.all + [ \e -> Expect.equal "E001" e.code + , \e -> Expect.equal "Auth failed" e.message + , \e -> Expect.equal "authentication" e.errorType + , \e -> Expect.equal (Just 401) e.internalHttpStatus + ] + err + + Nothing -> + Expect.fail "Expected error to be present" + ] + nd + + _ -> + Expect.fail "Expected DaVinciNode" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes sdk:node-change with error without internalHttpStatus" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk", "nodeStatus": "error", "error": { "code": "E002", "message": "Timeout", "type": "network" } }""" + in + case JD.decodeString decodeAuthEvent json of + Ok event -> + case event.data of + DaVinciNode nd -> + case nd.sdkError of + Just err -> + Expect.equal Nothing err.internalHttpStatus + + Nothing -> + Expect.fail "Expected error to be present" + + _ -> + Expect.fail "Expected DaVinciNode" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes sdk:node-change with authorization sub-object" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk", "nodeStatus": "success", "authorization": { "code": "auth-code-123", "state": "state-xyz" } }""" + in + case JD.decodeString decodeAuthEvent json of + Ok event -> + case event.data of + DaVinciNode nd -> + Expect.all + [ \n -> Expect.equal (Just Success) n.nodeStatus + , \n -> + case n.authorization of + Just auth -> + Expect.all + [ \a -> Expect.equal (Just "auth-code-123") a.code + , \a -> Expect.equal (Just "state-xyz") a.state + ] + auth + + Nothing -> + Expect.fail "Expected authorization to be present" + ] + nd + + _ -> + Expect.fail "Expected DaVinciNode" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes sdk:node-change with authorization with optional fields omitted" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk", "nodeStatus": "success", "authorization": {} }""" + in + case JD.decodeString decodeAuthEvent json of + Ok event -> + case event.data of + DaVinciNode nd -> + case nd.authorization of + Just auth -> + Expect.all + [ \a -> Expect.equal Nothing a.code + , \a -> Expect.equal Nothing a.state + ] + auth + + Nothing -> + Expect.fail "Expected authorization to be present" + + _ -> + Expect.fail "Expected DaVinciNode" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes sdk:node-change with all optional fields" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk", "nodeStatus": "continue", "previousStatus": "start", "interactionId": "int-1", "interactionToken": "tok-1", "nodeId": "node-1", "requestId": "req-1", "nodeName": "Password", "nodeDescription": "Enter password", "eventName": "click", "httpStatus": 200, "session": "sess-abc" }""" + in + case JD.decodeString decodeAuthEvent json of + Ok event -> + case event.data of + DaVinciNode nd -> + Expect.all + [ \n -> Expect.equal (Just Continue) n.nodeStatus + , \n -> Expect.equal (Just UnknownStatus) n.previousStatus + , \n -> Expect.equal (Just "int-1") n.interactionId + , \n -> Expect.equal (Just "tok-1") n.interactionToken + , \n -> Expect.equal (Just "node-1") n.nodeId + , \n -> Expect.equal (Just "req-1") n.requestId + , \n -> Expect.equal (Just "Password") n.nodeName + , \n -> Expect.equal (Just "Enter password") n.nodeDescription + , \n -> Expect.equal (Just "click") n.eventName + , \n -> Expect.equal (Just 200) n.httpStatus + , \n -> Expect.equal (Just "sess-abc") n.session + ] + nd + + _ -> + Expect.fail "Expected DaVinciNode" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes sdk:node-change with minimal fields" <| + \_ -> + let + json = + baseEventJson "sdk:node-change" + "sdk" + """{ "_tag": "sdk" }""" + in + case JD.decodeString decodeAuthEvent json of + Ok event -> + case event.data of + DaVinciNode nd -> + Expect.all + [ \n -> Expect.equal Nothing n.nodeStatus + , \n -> Expect.equal Nothing n.previousStatus + , \n -> Expect.equal Nothing n.sdkError + , \n -> Expect.equal Nothing n.authorization + , \n -> Expect.equal Nothing n.collectors + ] + nd + + _ -> + Expect.fail "Expected DaVinciNode" + + Err err -> + Expect.fail (JD.errorToString err) + ] + + +decodeRelevantDataTests : Test +decodeRelevantDataTests = + describe "relevantData in issues" + [ test "decodes flow issue with relevantData" <| + \_ -> + let + json = + """ + { "flowHealth": "warning" + , "issues": [ + { "id": "tok-1" + , "severity": "warning" + , "category": "token" + , "title": "Missing Token" + , "description": "No interaction token" + , "steps": ["Check config"] + , "relatedEventIds": ["evt-1"] + , "relevantData": { "interactionToken": "null", "nodeId": "abc" } + } + ] + , "annotatedEvents": {} + }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.issues of + [ issue ] -> + Expect.all + [ \i -> Expect.equal SevWarning i.severity + , \i -> + case i.relevantData of + Just pairs -> + Expect.equal True (List.length pairs == 2) + + Nothing -> + Expect.fail "Expected relevantData to be present" + ] + issue + + _ -> + Expect.fail "Expected exactly one issue" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes flow issue without relevantData" <| + \_ -> + let + json = + """ + { "flowHealth": "error" + , "issues": [ + { "id": "cors-1" + , "severity": "error" + , "category": "cors" + , "title": "CORS Blocked" + , "description": "Blocked" + , "steps": [] + , "relatedEventIds": [] + } + ] + , "annotatedEvents": {} + }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.issues of + [ issue ] -> + Expect.all + [ \i -> Expect.equal SevError i.severity + , \i -> Expect.equal Nothing i.relevantData + ] + issue + + _ -> + Expect.fail "Expected exactly one issue" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes event issue with relevantData" <| + \_ -> + let + json = + """ + { "flowHealth": "warning" + , "issues": [] + , "annotatedEvents": { + "evt-1": [ + { "severity": "warning" + , "title": "Expired JWT" + , "description": "Token expired" + , "steps": ["Refresh"] + , "relevantData": { "exp": "1700000000", "now": "1700000100" } + } + ] + } + }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.annotatedEvents of + [ ( _, [ issue ] ) ] -> + Expect.all + [ \i -> Expect.equal SevWarning i.severity + , \i -> + case i.relevantData of + Just pairs -> + Expect.equal 2 (List.length pairs) + + Nothing -> + Expect.fail "Expected relevantData" + ] + issue + + _ -> + Expect.fail "Expected one annotated event" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes event issue without relevantData" <| + \_ -> + let + json = + """ + { "flowHealth": "warning" + , "issues": [] + , "annotatedEvents": { + "evt-1": [ + { "severity": "info" + , "title": "Status Zero" + , "description": "Request failed" + , "steps": [] + } + ] + } + }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.annotatedEvents of + [ ( _, [ issue ] ) ] -> + Expect.all + [ \i -> Expect.equal SevInfo i.severity + , \i -> Expect.equal Nothing i.relevantData + ] + issue + + _ -> + Expect.fail "Expected one annotated event" + + Err err -> + Expect.fail (JD.errorToString err) + ] + + +decodeDiagnosisTests : Test +decodeDiagnosisTests = + describe "decodeDiagnosisResult" + [ test "decodes a healthy diagnosis" <| + \_ -> + let + json = + """ + { "flowHealth": "healthy" + , "issues": [] + , "annotatedEvents": {} + }""" + + result = + JD.decodeString decodeDiagnosisResult json + in + case result of + Ok diagnosis -> + Expect.all + [ \d -> Expect.equal Healthy d.flowHealth + , \d -> Expect.equal [] d.issues + , \d -> Expect.equal [] d.annotatedEvents + ] + diagnosis + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes error flow health" <| + \_ -> + let + json = + """{ "flowHealth": "error", "issues": [], "annotatedEvents": {} }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + Expect.equal Error d.flowHealth + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes warning flow health" <| + \_ -> + let + json = + """{ "flowHealth": "warning", "issues": [], "annotatedEvents": {} }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + Expect.equal Warning d.flowHealth + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes issues with flow issue fields" <| + \_ -> + let + json = + """ + { "flowHealth": "error" + , "issues": [ + { "id": "cors-1" + , "severity": "error" + , "category": "cors" + , "title": "CORS Blocked" + , "description": "Request blocked" + , "steps": ["Check headers", "Add allow-origin"] + , "relatedEventIds": ["evt-1", "evt-2"] + } + ] + , "annotatedEvents": {} + }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.issues of + [ issue ] -> + Expect.all + [ \i -> Expect.equal "cors-1" i.id + , \i -> Expect.equal SevError i.severity + , \i -> Expect.equal "cors" i.category + , \i -> Expect.equal [ "Check headers", "Add allow-origin" ] i.steps + , \i -> Expect.equal [ "evt-1", "evt-2" ] i.relatedEventIds + ] + issue + + _ -> + Expect.fail "Expected exactly one issue" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes annotated events with event issues" <| + \_ -> + let + json = + """ + { "flowHealth": "warning" + , "issues": [] + , "annotatedEvents": { + "evt-1": [ + { "severity": "warning" + , "title": "Expired JWT" + , "description": "Token has expired" + , "steps": ["Refresh token"] + } + ] + } + }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.annotatedEvents of + [ ( eventId, [ issue ] ) ] -> + Expect.all + [ \_ -> Expect.equal "evt-1" eventId + , \_ -> Expect.equal SevWarning issue.severity + , \_ -> Expect.equal "Expired JWT" issue.title + ] + () + + _ -> + Expect.fail "Expected one annotated event entry" + + Err err -> + Expect.fail (JD.errorToString err) + ] + + +decodeImportMetaTests : Test +decodeImportMetaTests = + describe "decodeImportMeta" + [ test "decodes import meta with flowId" <| + \_ -> + let + json = + """{ "flowId": "flow-abc", "capturedAt": "2026-05-08T14:30:00.000Z", "redacted": true }""" + in + case JD.decodeString decodeImportMeta json of + Ok meta -> + Expect.all + [ \m -> Expect.equal (Just "flow-abc") m.flowId + , \m -> Expect.equal "2026-05-08T14:30:00.000Z" m.capturedAt + , \m -> Expect.equal True m.redacted + ] + meta + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes null flowId" <| + \_ -> + let + json = + """{ "flowId": null, "capturedAt": "2026-05-08T14:30:00.000Z", "redacted": false }""" + in + case JD.decodeString decodeImportMeta json of + Ok meta -> + Expect.equal Nothing meta.flowId + + Err err -> + Expect.fail (JD.errorToString err) + ] + + +decodeSnapshotMetaTests : Test +decodeSnapshotMetaTests = + describe "decodeSnapshotMeta" + [ test "decodes a snapshot meta" <| + \_ -> + let + json = + """{ "id": "snap-1", "savedAt": "2026-05-08T15:00:00.000Z", "flowId": "flow-abc", "eventCount": 5 }""" + in + case JD.decodeString decodeSnapshotMeta json of + Ok meta -> + Expect.all + [ \m -> Expect.equal "snap-1" m.id + , \m -> Expect.equal (Just "flow-abc") m.flowId + , \m -> Expect.equal 5 m.eventCount + ] + meta + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes null flowId" <| + \_ -> + let + json = + """{ "id": "snap-2", "savedAt": "2026-05-08T15:00:00.000Z", "flowId": null, "eventCount": 0 }""" + in + case JD.decodeString decodeSnapshotMeta json of + Ok meta -> + Expect.equal Nothing meta.flowId + + Err err -> + Expect.fail (JD.errorToString err) + ] + + +decodeSeverityTests : Test +decodeSeverityTests = + describe "decodeSeverity via EventIssue" + [ test "decodes error severity" <| + \_ -> + let + json = + """{ "flowHealth": "warning", "issues": [], "annotatedEvents": { "e1": [{ "severity": "error", "title": "T", "description": "D", "steps": [] }] } }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.annotatedEvents of + [ ( _, [ issue ] ) ] -> + Expect.equal SevError issue.severity + + _ -> + Expect.fail "Expected one annotated event" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes warning severity" <| + \_ -> + let + json = + """{ "flowHealth": "warning", "issues": [], "annotatedEvents": { "e1": [{ "severity": "warning", "title": "T", "description": "D", "steps": [] }] } }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.annotatedEvents of + [ ( _, [ issue ] ) ] -> + Expect.equal SevWarning issue.severity + + _ -> + Expect.fail "Expected one annotated event" + + Err err -> + Expect.fail (JD.errorToString err) + , test "decodes unknown severity as info" <| + \_ -> + let + json = + """{ "flowHealth": "warning", "issues": [], "annotatedEvents": { "e1": [{ "severity": "info", "title": "T", "description": "D", "steps": [] }] } }""" + in + case JD.decodeString decodeDiagnosisResult json of + Ok d -> + case d.annotatedEvents of + [ ( _, [ issue ] ) ] -> + Expect.equal SevInfo issue.severity + + _ -> + Expect.fail "Expected one annotated event" + + Err err -> + Expect.fail (JD.errorToString err) + ] diff --git a/packages/devtools-extension/tests/HelpersTests.elm b/packages/devtools-extension/tests/HelpersTests.elm new file mode 100644 index 0000000000..70cd6a1a8b --- /dev/null +++ b/packages/devtools-extension/tests/HelpersTests.elm @@ -0,0 +1,277 @@ +module HelpersTests exposing (suite) + +import Dict +import Expect +import Helpers exposing (findEvent, findEventInList, isSdkNode, methodClass, nodeColor, nodeStatusLabel, sdkNodes, statusClass, truncateId) +import Test exposing (Test, describe, test) +import Types exposing (AuthEvent, EventData(..), EventKind(..), EventSource(..), JourneyData, NetworkData, NodeData, NodeStatus(..), OidcData, SessionData) + + +makeNetworkEvent : String -> AuthEvent +makeNetworkEvent id = + { id = id + , timestamp = 100 + , kind = NetworkEvent + , source = NetworkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Network (NetworkData (Just 200) (Just "https://x.com") (Just "GET") (Just 50) Nothing Nothing Nothing Nothing) + } + + +makeSdkEvent : String -> Float -> AuthEvent +makeSdkEvent id ts = + { id = id + , timestamp = ts + , kind = NodeChange + , source = SdkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = DaVinciNode (NodeData (Just Continue) Nothing Nothing Nothing Nothing Nothing (Just "Username") Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing) + } + + +makeJourneyEvent : String -> Float -> AuthEvent +makeJourneyEvent id ts = + { id = id + , timestamp = ts + , kind = JourneyStep + , source = SdkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Journey (JourneyData (Just "Step") Nothing Nothing Nothing Nothing (Just "abc") Nothing Nothing Nothing Nothing Nothing) + } + + +makeOidcEvent : String -> Float -> AuthEvent +makeOidcEvent id ts = + { id = id + , timestamp = ts + , kind = OidcState + , source = SdkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Oidc (OidcData (Just "authorize") (Just "success") Nothing Nothing Nothing) + } + + +makeSessionEvent : String -> AuthEvent +makeSessionEvent id = + { id = id + , timestamp = 100 + , kind = SessionEvent + , source = SessionSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Session (SessionData (Just "token") (Just "old") (Just "new")) + } + + +makeConfigEvent : String -> AuthEvent +makeConfigEvent id = + { id = id + , timestamp = 100 + , kind = SdkConfig + , source = SdkSource + , flowId = Nothing + , isCors = False + , isError = False + , isAuthRelated = True + , causedBy = Nothing + , data = Config Nothing + } + + +suite : Test +suite = + describe "Helpers" + [ isSdkNodeTests + , sdkNodesTests + , findEventTests + , statusClassTests + , methodClassTests + , nodeColorTests + , nodeStatusLabelTests + , truncateIdTests + ] + + +isSdkNodeTests : Test +isSdkNodeTests = + describe "isSdkNode" + [ test "DaVinciNode is SDK node" <| + \_ -> Expect.equal True (isSdkNode (makeSdkEvent "e1" 0)) + , test "Journey is SDK node" <| + \_ -> Expect.equal True (isSdkNode (makeJourneyEvent "e1" 0)) + , test "Oidc is SDK node" <| + \_ -> Expect.equal True (isSdkNode (makeOidcEvent "e1" 0)) + , test "Network is NOT SDK node" <| + \_ -> Expect.equal False (isSdkNode (makeNetworkEvent "e1")) + , test "Session is NOT SDK node" <| + \_ -> Expect.equal False (isSdkNode (makeSessionEvent "e1")) + , test "Config is NOT SDK node" <| + \_ -> Expect.equal False (isSdkNode (makeConfigEvent "e1")) + ] + + +sdkNodesTests : Test +sdkNodesTests = + describe "sdkNodes" + [ test "filters to only SDK events" <| + \_ -> + let + events = + [ makeNetworkEvent "n1" + , makeSdkEvent "s1" 200 + , makeSessionEvent "ss1" + , makeJourneyEvent "j1" 100 + ] + in + Expect.equal [ "j1", "s1" ] (List.map .id (sdkNodes events)) + , test "sorts by timestamp" <| + \_ -> + let + events = + [ makeSdkEvent "s2" 300 + , makeSdkEvent "s1" 100 + , makeSdkEvent "s3" 200 + ] + in + Expect.equal [ "s1", "s3", "s2" ] (List.map .id (sdkNodes events)) + , test "returns empty list for no SDK nodes" <| + \_ -> + Expect.equal [] (sdkNodes [ makeNetworkEvent "n1" ]) + ] + + +findEventTests : Test +findEventTests = + describe "findEvent / findEventInList" + [ test "findEvent finds event by id in Dict" <| + \_ -> + let + evts = + Dict.fromList [ ( "e1", makeNetworkEvent "e1" ), ( "e2", makeSdkEvent "e2" 0 ) ] + in + case findEvent "e2" evts of + Just e -> + Expect.equal "e2" e.id + + Nothing -> + Expect.fail "Expected to find event e2" + , test "findEvent returns Nothing for missing id" <| + \_ -> + Expect.equal Nothing (findEvent "missing" Dict.empty) + , test "findEventInList finds event by id" <| + \_ -> + let + events = + [ makeNetworkEvent "e1", makeSdkEvent "e2" 0 ] + in + case findEventInList "e2" events of + Just e -> + Expect.equal "e2" e.id + + Nothing -> + Expect.fail "Expected to find event e2" + , test "findEventInList returns Nothing for missing id" <| + \_ -> + Expect.equal Nothing (findEventInList "missing" []) + ] + + +statusClassTests : Test +statusClassTests = + describe "statusClass" + [ test "Nothing → st-nil" <| + \_ -> Expect.equal "st-nil" (statusClass Nothing) + , test "0 → st-err" <| + \_ -> Expect.equal "st-err" (statusClass (Just 0)) + , test "200 → st-ok" <| + \_ -> Expect.equal "st-ok" (statusClass (Just 200)) + , test "302 → st-ok" <| + \_ -> Expect.equal "st-ok" (statusClass (Just 302)) + , test "400 → st-warn" <| + \_ -> Expect.equal "st-warn" (statusClass (Just 400)) + , test "500 → st-warn" <| + \_ -> Expect.equal "st-warn" (statusClass (Just 500)) + ] + + +methodClassTests : Test +methodClassTests = + describe "methodClass" + [ test "Nothing → m-other" <| + \_ -> Expect.equal "m-other" (methodClass Nothing) + , test "GET → m-get" <| + \_ -> Expect.equal "m-get" (methodClass (Just "GET")) + , test "POST → m-post" <| + \_ -> Expect.equal "m-post" (methodClass (Just "POST")) + , test "PUT → m-put" <| + \_ -> Expect.equal "m-put" (methodClass (Just "PUT")) + , test "PATCH → m-patch" <| + \_ -> Expect.equal "m-patch" (methodClass (Just "PATCH")) + , test "DELETE → m-del" <| + \_ -> Expect.equal "m-del" (methodClass (Just "DELETE")) + , test "lowercase get → m-get" <| + \_ -> Expect.equal "m-get" (methodClass (Just "get")) + , test "OPTIONS → m-other" <| + \_ -> Expect.equal "m-other" (methodClass (Just "OPTIONS")) + ] + + +nodeColorTests : Test +nodeColorTests = + describe "nodeColor" + [ test "Continue → blue" <| + \_ -> Expect.equal "#58A6FF" (nodeColor Continue) + , test "Success → green" <| + \_ -> Expect.equal "#3FB950" (nodeColor Success) + , test "StatusError → red" <| + \_ -> Expect.equal "#F85149" (nodeColor StatusError) + , test "Failure → red" <| + \_ -> Expect.equal "#F85149" (nodeColor Failure) + , test "UnknownStatus → gray" <| + \_ -> Expect.equal "#484F58" (nodeColor UnknownStatus) + ] + + +nodeStatusLabelTests : Test +nodeStatusLabelTests = + describe "nodeStatusLabel" + [ test "Continue → continue" <| + \_ -> Expect.equal "continue" (nodeStatusLabel Continue) + , test "Success → success" <| + \_ -> Expect.equal "success" (nodeStatusLabel Success) + , test "StatusError → error" <| + \_ -> Expect.equal "error" (nodeStatusLabel StatusError) + , test "Failure → failure" <| + \_ -> Expect.equal "failure" (nodeStatusLabel Failure) + , test "UnknownStatus → unknown" <| + \_ -> Expect.equal "unknown" (nodeStatusLabel UnknownStatus) + ] + + +truncateIdTests : Test +truncateIdTests = + describe "truncateId" + [ test "truncates to 8 characters" <| + \_ -> Expect.equal "abcdefgh" (truncateId "abcdefghijklmnop") + , test "returns short strings unchanged" <| + \_ -> Expect.equal "abc" (truncateId "abc") + ] diff --git a/packages/devtools-types/src/lib/auth-event.schema.test.ts b/packages/devtools-types/src/lib/auth-event.schema.test.ts index 2bf6fc8fad..caf725faf3 100644 --- a/packages/devtools-types/src/lib/auth-event.schema.test.ts +++ b/packages/devtools-types/src/lib/auth-event.schema.test.ts @@ -122,4 +122,371 @@ describe('AuthEventSchema', () => { expect(result.type).toBe('sdk:node-change'); expect(result.data._tag).toBe('sdk'); }); + + it('decodes sdk:node-change with all optional sdk fields', () => { + const input = { + ...baseEvent, + type: 'sdk:node-change', + source: 'sdk', + data: { + _tag: 'sdk', + nodeStatus: 'continue', + previousStatus: 'start', + interactionId: 'int-1', + interactionToken: 'tok-1', + nodeId: 'node-1', + requestId: 'req-1', + nodeName: 'Username', + nodeDescription: 'Enter your name', + eventName: 'click', + httpStatus: 200, + collectors: [{ type: 'TextCollector' }], + error: { code: 'E001', message: 'Failed', type: 'auth' }, + authorization: { code: 'auth-code', state: 'xyz' }, + session: 'sess-abc', + responseBody: { key: 'value' }, + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('sdk'); + if (result.data._tag === 'sdk') { + expect(result.data.nodeName).toBe('Username'); + expect(result.data.error?.code).toBe('E001'); + expect(result.data.authorization?.code).toBe('auth-code'); + expect(result.data.collectors).toHaveLength(1); + } + }); + + it('decodes sdk:config event', () => { + const input = { + ...baseEvent, + type: 'sdk:config', + source: 'sdk', + data: { + _tag: 'sdk-config', + config: { serverUrl: 'https://auth.example.com', clientId: 'my-app' }, + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('sdk-config'); + }); + + it('decodes sdk:journey-step event', () => { + const input = { + ...baseEvent, + type: 'sdk:journey-step', + source: 'sdk', + data: { + _tag: 'journey', + stepType: 'Step', + authId: 'auth-123', + stage: 'UsernamePassword', + header: 'Sign In', + description: 'Enter credentials', + callbacks: [{ type: 'NameCallback' }], + realm: '/alpha', + tokenId: 'tok-1', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('journey'); + if (result.data._tag === 'journey') { + expect(result.data.stepType).toBe('Step'); + expect(result.data.authId).toBe('auth-123'); + expect(result.data.callbacks).toHaveLength(1); + } + }); + + it('decodes sdk:journey-step LoginFailure with error fields', () => { + const input = { + ...baseEvent, + type: 'sdk:journey-step', + source: 'sdk', + data: { + _tag: 'journey', + stepType: 'LoginFailure', + errorCode: 401, + errorMessage: 'Authentication Failed', + errorReason: 'InvalidCredentials', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + if (result.data._tag === 'journey') { + expect(result.data.stepType).toBe('LoginFailure'); + expect(result.data.errorCode).toBe(401); + expect(result.data.errorMessage).toBe('Authentication Failed'); + } + }); + + it('rejects journey event with invalid stepType', () => { + const input = { + ...baseEvent, + type: 'sdk:journey-step', + source: 'sdk', + data: { + _tag: 'journey', + stepType: 'InvalidStep', + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow(); + }); + + it('decodes sdk:oidc-state event', () => { + const input = { + ...baseEvent, + type: 'sdk:oidc-state', + source: 'sdk', + data: { + _tag: 'oidc', + phase: 'authorize', + status: 'success', + clientId: 'my-app', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('oidc'); + if (result.data._tag === 'oidc') { + expect(result.data.phase).toBe('authorize'); + expect(result.data.status).toBe('success'); + expect(result.data.clientId).toBe('my-app'); + } + }); + + it('decodes oidc error event with errorCode and errorMessage', () => { + const input = { + ...baseEvent, + type: 'sdk:oidc-state', + source: 'sdk', + data: { + _tag: 'oidc', + phase: 'exchange', + status: 'error', + errorCode: 'invalid_grant', + errorMessage: 'Token expired', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + if (result.data._tag === 'oidc') { + expect(result.data.errorCode).toBe('invalid_grant'); + expect(result.data.errorMessage).toBe('Token expired'); + } + }); + + it('rejects oidc event with invalid phase', () => { + const input = { + ...baseEvent, + type: 'sdk:oidc-state', + source: 'sdk', + data: { + _tag: 'oidc', + phase: 'unknown-phase', + status: 'success', + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow(); + }); + + it('rejects oidc event with invalid status', () => { + const input = { + ...baseEvent, + type: 'sdk:oidc-state', + source: 'sdk', + data: { + _tag: 'oidc', + phase: 'authorize', + status: 'pending', + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow(); + }); + + it('decodes session:storage event', () => { + const input = { + ...baseEvent, + type: 'session:storage', + source: 'session', + data: { + _tag: 'session', + key: 'am-auth-session', + before: 'old-value', + after: 'new-value', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('session'); + if (result.data._tag === 'session') { + expect(result.data.key).toBe('am-auth-session'); + expect(result.data.before).toBe('old-value'); + expect(result.data.after).toBe('new-value'); + } + }); + + it('decodes session event with optional before/after', () => { + const input = { + ...baseEvent, + type: 'session:storage', + source: 'session', + data: { + _tag: 'session', + key: 'token', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + if (result.data._tag === 'session') { + expect(result.data.before).toBeUndefined(); + expect(result.data.after).toBeUndefined(); + } + }); + + it('decodes dom:form-submit event', () => { + const input = { + ...baseEvent, + type: 'dom:form-submit', + source: 'dom', + data: { + _tag: 'dom', + element: 'form#login', + url: 'https://app.example.com/login', + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('dom'); + if (result.data._tag === 'dom') { + expect(result.data.element).toBe('form#login'); + expect(result.data.url).toBe('https://app.example.com/login'); + } + }); + + it('decodes dom event with all optional fields omitted', () => { + const input = { + ...baseEvent, + type: 'dom:redirect', + source: 'dom', + data: { _tag: 'dom' }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.data._tag).toBe('dom'); + }); + + it('decodes network event with corsFlag', () => { + const input = { + ...baseEvent, + type: 'network:cors-flag', + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'POST', + status: 0, + requestHeaders: { origin: 'https://app.example.com' }, + responseHeaders: {}, + duration: 0, + corsFlag: { + url: 'https://auth.example.com/token', + reason: 'status-zero', + method: 'POST', + }, + }, + flags: { isCors: true, isError: true, isAuthRelated: true }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + if (result.data._tag === 'network') { + expect(result.data.corsFlag?.reason).toBe('status-zero'); + } + }); + + it('decodes network event with request and response bodies', () => { + const input = { + ...baseEvent, + type: 'network:response', + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'POST', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 100, + requestBody: { grant_type: 'authorization_code' }, + responseBody: { access_token: 'abc', token_type: 'Bearer' }, + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + if (result.data._tag === 'network') { + expect(result.data.requestBody).toEqual({ grant_type: 'authorization_code' }); + expect(result.data.responseBody).toEqual({ access_token: 'abc', token_type: 'Bearer' }); + } + }); + + it('rejects event with invalid source', () => { + const input = { + ...baseEvent, + source: 'invalid-source', + type: 'network:response', + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'GET', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 10, + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow(); + }); + + it('validates flags field structure', () => { + const input = { + ...baseEvent, + type: 'network:response', + flags: { isCors: 'not-a-boolean', isError: false, isAuthRelated: true }, + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'GET', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 10, + }, + }; + + expect(() => Schema.decodeUnknownSync(AuthEventSchema)(input)).toThrow(); + }); + + it('accepts causedBy as a string', () => { + const input = { + ...baseEvent, + type: 'network:response', + causedBy: 'sdk-evt-123', + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'GET', + status: 200, + requestHeaders: {}, + responseHeaders: {}, + duration: 10, + }, + }; + + const result = Schema.decodeUnknownSync(AuthEventSchema)(input); + expect(result.causedBy).toBe('sdk-evt-123'); + }); }); diff --git a/packages/devtools-types/src/lib/flow-state.schema.test.ts b/packages/devtools-types/src/lib/flow-state.schema.test.ts new file mode 100644 index 0000000000..3d1870d164 --- /dev/null +++ b/packages/devtools-types/src/lib/flow-state.schema.test.ts @@ -0,0 +1,123 @@ +import { Schema } from 'effect'; +import { describe, expect, it } from 'vitest'; +import { FlowStateSchema, FlowSummarySchema } from './flow-state.schema.js'; + +const validSummary = { + nodeCount: 3, + errorCount: 1, + corsFlags: [], + duration: 1500, + sdkConnected: true, +}; + +const validNetworkEvent = { + id: 'evt-001', + timestamp: 1700000000000, + type: 'network:response', + source: 'network', + flowId: 'flow-abc', + causedBy: null, + data: { + _tag: 'network', + url: 'https://auth.example.com/token', + method: 'POST', + status: 200, + requestHeaders: { 'content-type': 'application/json' }, + responseHeaders: { 'x-request-id': 'abc123' }, + duration: 123, + }, + flags: { isCors: false, isError: false, isAuthRelated: true }, +}; + +describe('FlowSummarySchema', () => { + it('decodes a valid summary', () => { + const result = Schema.decodeUnknownSync(FlowSummarySchema)(validSummary); + expect(result.nodeCount).toBe(3); + expect(result.errorCount).toBe(1); + expect(result.sdkConnected).toBe(true); + }); + + it('decodes a summary with corsFlags', () => { + const input = { + ...validSummary, + corsFlags: [ + { + url: 'https://auth.example.com/token', + reason: 'status-zero', + method: 'POST', + }, + ], + }; + const result = Schema.decodeUnknownSync(FlowSummarySchema)(input); + expect(result.corsFlags).toHaveLength(1); + expect(result.corsFlags[0].reason).toBe('status-zero'); + }); + + it('rejects missing nodeCount', () => { + const { nodeCount, ...rest } = validSummary; + expect(() => Schema.decodeUnknownSync(FlowSummarySchema)(rest)).toThrow(); + }); + + it('rejects invalid corsFlag reason', () => { + const input = { + ...validSummary, + corsFlags: [{ url: 'https://x.com', reason: 'bad-reason', method: 'GET' }], + }; + expect(() => Schema.decodeUnknownSync(FlowSummarySchema)(input)).toThrow(); + }); +}); + +describe('FlowStateSchema', () => { + const validFlowState = { + flowId: 'flow-abc', + capturedAt: '2026-05-08T14:30:00.000Z', + events: [validNetworkEvent], + summary: validSummary, + }; + + it('decodes a valid flow state', () => { + const result = Schema.decodeUnknownSync(FlowStateSchema)(validFlowState); + expect(result.flowId).toBe('flow-abc'); + expect(result.events).toHaveLength(1); + expect(result.capturedAt).toBe('2026-05-08T14:30:00.000Z'); + }); + + it('accepts null flowId', () => { + const input = { ...validFlowState, flowId: null }; + const result = Schema.decodeUnknownSync(FlowStateSchema)(input); + expect(result.flowId).toBeNull(); + }); + + it('defaults lastSdkEventId to null when omitted', () => { + const result = Schema.decodeUnknownSync(FlowStateSchema)(validFlowState); + expect(result.lastSdkEventId).toBeNull(); + }); + + it('accepts explicit lastSdkEventId', () => { + const input = { ...validFlowState, lastSdkEventId: 'sdk-42' }; + const result = Schema.decodeUnknownSync(FlowStateSchema)(input); + expect(result.lastSdkEventId).toBe('sdk-42'); + }); + + it('accepts null lastSdkEventId', () => { + const input = { ...validFlowState, lastSdkEventId: null }; + const result = Schema.decodeUnknownSync(FlowStateSchema)(input); + expect(result.lastSdkEventId).toBeNull(); + }); + + it('accepts empty events array', () => { + const input = { ...validFlowState, events: [] }; + const result = Schema.decodeUnknownSync(FlowStateSchema)(input); + expect(result.events).toHaveLength(0); + }); + + it('rejects missing summary', () => { + const { summary, ...rest } = validFlowState; + expect(() => Schema.decodeUnknownSync(FlowStateSchema)(rest)).toThrow(); + }); + + it('rejects invalid event in events array', () => { + const input = { ...validFlowState, events: [{ id: 'bad' }] }; + expect(() => Schema.decodeUnknownSync(FlowStateSchema)(input)).toThrow(); + }); +}); From a2e801e5d9f7d5dff38aa8e36d9f16479a581954 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Sat, 9 May 2026 14:16:53 -0600 Subject: [PATCH 8/8] docs(devtools-extension): add screenshots and Learn tab section to README Replace TODO screenshot placeholders with actual screenshots of the Flow, Timeline, and Learn views. Add Learn section documenting the canvas-based request lifecycle visualization. --- packages/devtools-extension/README.md | 17 ++++++++++++----- .../screenshots/Flow-Screen.png | Bin 0 -> 195184 bytes .../screenshots/Learn-Tab-Error-Screen.png | Bin 0 -> 185844 bytes .../screenshots/Timeline-Screen.png | Bin 0 -> 230135 bytes 4 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 packages/devtools-extension/screenshots/Flow-Screen.png create mode 100644 packages/devtools-extension/screenshots/Learn-Tab-Error-Screen.png create mode 100644 packages/devtools-extension/screenshots/Timeline-Screen.png diff --git a/packages/devtools-extension/README.md b/packages/devtools-extension/README.md index e57076d271..62ecccbeab 100644 --- a/packages/devtools-extension/README.md +++ b/packages/devtools-extension/README.md @@ -4,8 +4,7 @@ Most auth debugging starts in the Network panel and stays there — copying tokens into jwt.io, cross-referencing timestamps, guessing which 400 was the CORS preflight and which was a bad grant. Ping DevTools replaces that with a single panel that captures both network traffic and SDK-level events, correlates them into flows, and runs an automated diagnosis engine that tells you _what went wrong and how to fix it_. - - +![Flow view with diagnosis banner and node rail](screenshots/Flow-Screen.png) --- @@ -70,8 +69,7 @@ Ping DevTools gives you: - **Flow-level structure** — the Flow view shows the authentication flow as a sequence of nodes with detail cards, not a flat list of URLs. - **Playback** — step through the flow to see exactly what the SDK saw at each point. - - +![Timeline with two-stream correlation](screenshots/Timeline-Screen.png) --- @@ -97,7 +95,8 @@ Host page │ panel/Main.elm (Elm 0.19) ├── Timeline view — chronological event table with Inspector - └── Flow view — node rail + detail card + health banner + ├── Flow view — node rail + detail card + health banner + └── Learn view — canvas-based request lifecycle visualization ``` Network events follow a parallel path: `network-observer.ts` uses `chrome.devtools.network.onRequestFinished` to capture HAR entries, filters them against auth URL patterns, and sends them to the same service worker. @@ -218,6 +217,14 @@ A chronological table of all captured events. Each row shows timestamp, event ty A visual representation of the authentication flow as a sequence of SDK nodes. The node rail draws coloured circles for each node with arrows connecting them, status and node-name labels, and a glow effect on the selected node. Selecting a node opens a detail card with contextual information — collectors for DaVinci, callbacks for Journey steps, phase and error data for OIDC — plus any causally linked network requests with expandable request/response bodies. The Flow Health banner appears above the rail when the diagnosis engine detects issues. +### Learn + +A canvas-based visualization that maps the request lifecycle across four stages: **Browser**, **Server**, **SDK**, and **Form**. Each stage is drawn as a labelled card with animated connector arrows showing the direction and outcome of each hop — method labels on outgoing edges, status codes on responses. Error states are highlighted with red borders and status annotations (e.g. `X 400`), making it immediately clear where a request failed and how the SDK interpreted the result. + +The Learn tab correlates network events with SDK state transitions to show the full round-trip: the browser sends a request, the server responds, the SDK processes the response into a node transition, and the form renders the result. When errors occur, you can see exactly which stage failed and how that failure propagated through the rest of the pipeline. + +![Learn tab showing request lifecycle with error highlighting](screenshots/Learn-Tab-Error-Screen.png) + --- ## Packages diff --git a/packages/devtools-extension/screenshots/Flow-Screen.png b/packages/devtools-extension/screenshots/Flow-Screen.png new file mode 100644 index 0000000000000000000000000000000000000000..a061a3fd8165b3b9129f7f4fff6ff5c7434f2b09 GIT binary patch literal 195184 zcmaI82RIzx7e1aYAxNTki5hM7UVFkU!9EzqPMWRAUZ30CweD(U81+OdT;+p z`Fwx>@;v{2hP5+yX7Aj&=iK+a=RJaz6r?cGiP7)exq~VF?ybt5J5MU^+KX>C8u}JRRp}ZsgR#eSB1&Kh@8Fjl6*|UEqCiW=m`{`q_ zNnkN0{jZ{8jpE92lxJg=+m7z+_89`v+_znGQU|YUWT`JIwzcgDz%hC9_+#(9S0Jkx z<@W8nChouwgwX@r9zJv8P3LQUJPY2Lhe8Pj-d}iv+|~mSzgJ zoDLTLT|bubwWF=kl6GUTVXMVQ%BV<%hZW-we;@pCE=RdHbG{cV*hF(qm2Us?nLEaV zdBOR(*DI0UX%Ek-BF&{0hFM;!5mp{9(mlLalK9@|&qWh%ZSs#OjVFXi`@DGLMLU^{ zT)&&L*=U6&>V-Z8noB%;Tb(C+Nx`Q}NX#}`$SsoLg!|{jH4eXeP2kWNB0n`#Xoxx* z+mbHiZL0AgrNe2`+>&xxpgK=Lqq4fXsRke%XeuaVb+yZ-E(1z6|n*8nLyPO=e#>OSSYA)a|&HIsXbvi5A z?A>4QXOt}UPuwaQOh%hr-$rF`%9vY!q>Pw=RLBJ8Kp(EL`TejyG=n@&ONXhe7dN57 zY)sX3b@O@X(#@m4)Nu2F8082hG+;S7IdbM%i0=zS;2!ek8KkQx@JLm0yIzt0c_?=h z?yh`hM`Xea3d(|+Go;3ZrWyYnV69I`N}_?kpB+<6PmL8`dm79*!OLHVyxmOZgGKt9 z)6}2@8Rh%^!=@EoCiNL9D$B~G73w-VI>P(7ws&`}ZEaQ3wN*@#WufX{v-AB9t7X1<$t11Ge69y3K@XIg z7A`-Q$nrv$85dr_qBu5~#X)Lmw3GyNcQgOj%080|6q3N|$={~TE;cySH`0jkNZ~u( zxg6QEQ~Ym23oq|8op~S2*WlQNB)iabAk#uFogtUBf2R9~w9k7q5fLoF_CPdjQ0n^X z-qqE;Cm*M33}@$hrH-{XBg9q3toD;?AV=b%SNBj&moGnl9pf4M@Zf7h`QE}%70tiS#-LzoHtFfpcXk`EPmxoJUup(* ze?d-=J;B!_17O!P5IVad`{6-(aF4hdd|&_*#-dA^Ll)H5G|cS5AtwGR&3oxK;i;L& zIcbgd-B>JvJN?Q!@J&Wg^YZp`x-*KYl$DT(sJPkmFh_nCMpv|jw6V9RSQZofq0_}- zesdZ3K!l>NKP4wo55^bR$K~rac_7g=+PKLtF#8at_w>=|@Nj_GvvB$2x=2@-ibN;* z;vyTOr#U|mg>`k_78Y1zSdWOxQHwfcvIks_eJ^M=?a2|rK6;|JKQ>U%6N$FAbZt(kMR`p<-P|xx{ z88Vd=nOacTmz(RZ#t=6%E0C0smn;IUH9xHg=VOS&3VDmT3B4yT_g=8+Vc)0hYM-MR zWvAn$WX$kAWxl^YoVD=D1HFul=$TmvWOa32Zrz>3bHim>Q{Tfag(xua8|Z2wJ-D`W zczAk2zqz>yFD_)E=bikiBrs~tL$~jKHI>cy z;2wyBgWSIbxw{L@w}L$So_>A~)}JCki&Lf(?D?i^!;h3FsB6Jn?}+)VmX<{~yT;x3 zTP=)^oi0fvER>2Na~s z(n?FVu#gt!Sv@((UXVt9E0aOnrqS z+d>lC*lvU9tr%Z#Cv8m%&tQPEgFQQ^2F#l0_4aSlo2F|Flk@XE-nNenA7|xvsdCbM z^UKLubI$fXBH|F(Y1YT7CMCipz>;;TM+y(?>hm%Ur#ho`9d!TtJlSF9%)lTU6!=Y^ z-_Oz5xG^tx_M?4|Q7KJ~UvTlft3DdL^_*%ZR%`Vo|D44>eP~A$c&8cI55M(OLd_CpqC=S6K+^x;dVU zOHunxa_p>{#^BfuA(7pXA&L1Kr!=*uRQ?y4^%V-ULoSNS?2*db`Ib@}^=|Ya8n8e? zxAzx0N&hgdI-&yRI6K+w?Fd1B)}PI8yX&5^Iis68SeguEDSI&csO;C%Lsh^G_|~M0 zIPB~giSL+NTz0R3xBUBiFG3fU=rAmGyAQ1=b|xXF=?O6pa9= z_-`sW^H7iu8ZgP!O*{jwxCq+^2FXPCt-gLO^8D~~Y!)M1sMa3y?##)DQnqC|?X3)P z5ym(fC;o6reSBELmkK-6gNuURgBkwgV|~`2WMEg|c`J(el*r{Ca>U6ro~B7PG!hi+ zc=VkvK!d|Q#MM6WXEHL@uk18hwAq52Z`PnZwRd%O5c~L1fB_xZefVDS-ScdbPDsdG zOzhQxJ^a34D!l1EC=ScWS~0mrG+2$Fkd~B+6av9}51!@X;hCBhSR`Z9@iY^cM|@VZ z*?(0SgnFW#O5NRWx%FJpdh?qO{ObJI=j~%1_`Z-E%gKJ(*|PsjP*Xgcmfvdr(eqR@ zf^#jWsZP4lEp6>}#Y6w|FOhIgw$bb1co}GCKZ?r{BQbJDWz&S6-&0AY(ZPk2JhItw z8SpG&SyIr{y)7X!&&m(#LtxqKG`VJPg!kAGh?0`>$e{NIg$iEC2^N~g?HRWVwcQ8N6Z=aKj3S6m)ZdI0H{FYoUhboNTwvlB`DL+Ti* zi`CdeV~8?pCpqGsNWruY=b?R#%q8#w!wm33i6IxqDL2(x}uZ{O%-XD7V8aCvW0-=#0PQ=dunnd0?v zIw{uas^$%TGt+cm-_ZPgeu<*@qS$o;@o}a3NCX#mL=8HhE%!XU=~o<7c>Pz)b=~Ny)AT-05WS?NsGk^Ez{{4` z%5HF>V$+VqS^z6Z)^$M$vSp#X?{P3374^B;Z7w1jPZWv8`%HUvgQT3S<~CZczFKT%Et3bhA&ZQD0r z|D2ieF4k;WO|YABscvkf3Nq#gmOKwL(`G&bK`fi3H7|< zlag8=8}pyp4pU<|&L&vE53XBqKTMw#!teCfr+#s*yu311(a4pqM9|Adk{Pb~UxwDO}ZJojLd7uTmc}<%z^~ViO6ol)~{crgoInj71~%#O-;=! zCj)!v?Krxi@PgA}PJMClj-1?k1cD4N&NNqnY`plE+}u3bS5k5;lmNrnSQCgxEJWO; zpFfxVB4a8qpRLDh^$Ui8LmQ)NjKX2C^u$CDA0K%Q*O_ctg3QdNd-ugi$#l`EuWQ%Y z0h{?&zp}ruZJy>E++)&ozDvL}=Nc3l3EkTN@~s^rlf6W7dsQgr$hDZIM1OnLmF&U~ zoJT=t7PKTwW{QVsn#X5R$PqGnJ(O~OaN9947Tj|&yr)!8=Eib!Zs1{?%4-)~9vb^P zS;F21HG6BVKy99G=~_!8kF}<_kmP`%jyfyJCEn%y>W1GJna{hRGk-Y?wjtUZgm!HPrB36)Q~9@ z3%h?)akDIpePA8)P)c#^$}_JuY9Z~*FZ`DkJUqrq`I177BU?#n*J!w zi|Zz#QD*{BqEYePeig~mvHy>5BF zqjDImgq5?mrzg8wac+`PK~coe&=8NP)}`wtwf^S#o=4ARU8s7Ou1r8_>ApDjO>MPe zN*p9mf#>891HiGbWBXK9v8yVpeJ?gIXR8p3S;Lhu<%WhURO5Q|{Crww<{>6#rGf(d z?%j+~0yLkSbLeqdg{6U6=i$*_bf4+#*Hkge#beR_yff2$NbfUg;_&!1-?IGLw4v&- zkg&!}R8RydpTp9(x`VKWl%3^e3Oq6x5c<2n92y*S1R$C}w&2ZH;SeTfVc)%&SUasI zZ*^5w9}QP+bMyM#AVrh*n6E#6-ZmUs>8D?8<+5^Gjpo$X*9$173Qr&qn+Ch2B=uLp zBF%flUAOsg^5wk6L+0DFZdFd=ow?euz(8#<_;PQL=5*n9!|(p(pjj9q(?jMpE_=3$ zrJ-TRYEn$eJ8Ofl`XIgW?2k1*+#oczKode6=!^EIy8afmo{PW)0`Q+pd@ERm;FY zR>hH#uWyxD{?YNie)Wl~$U+C;u9Fj7am5f1zer~7+-}Y`->g!Q*v)FIt8*)yRRFQY z{yt-0FUZ_1jhr~|g# zuh^Q6&5)0u9~%?1w%*H%uCCq-0HAnyG=ZcT#!Vp4MmbB)3xDjJp23O5_4MXsp&t5R zQX-gXEBoGWf4aR@C~|#t{pnMjogMj#tDU7SyGc5%XRK64O|7WJ{zpgX!%E2b%uqbU z+8)+pGCggXL}=WzHC{bEHTC4dJn)$y6a$|b9UU_`swWTt8G$-Gvv#w@WO$Pk6Di5b z;}a9ewY8VHl3tG+7ChF~50*dcAd|+c*$DeTw$3@Pv(F)fva*)m+njV!+y?1Bxw-a+hH&80wR2%hOZPHZ`T5S{;qZu$ ztvUxS=4*Q*ksDqYHjO4qYdeTT6Rq(7+?0g+FtrZ*B}XIP>rLCXehYdqU8L}$hCkV7uO|*H?Q-r3MmC7Nn-frT z$SAJTLvGpNlUk4v8Z=&xew3zAgWRlUEz{*J!-66<<+K@+JF3c^U7Vcc6|^U3XH|7{ z9NMwQFTh4!ostr20&TRYpSjb~!iO6T-Et0;Z{+E+{$s(?RMQ3anU07@TKhvnLYyIZ z-q!eb1NHWzbei@S@#0)14)M_P8NOgtW4>iso@ImV zQ2|FYPv72dtsLq_!@|Nwby&gO@9gB{v2)xU3&RQx#h}OS4OuM&x>8m)$~m6Bczx}& z+jI&lyR(0Fp0X7B@X3=xzzje--2k-GZ}#o`_3Hz-J3R|aUVgs&FTbwX*a_b}qB6~) zEp@>%V#19&->9qbTeVl%n!JfrpalvwnO76U%0|vVUhjjcomcdnKdhDwX21 zD)|-Q30wS7iQAjgP?N`Rb4hN`6^9-^3M3agv6hmWg}B*cG-W3zcdhnGq(k@hodUzc zQ~}o2{qhXF$J`??bPYglqtcD;;rrF_1*g8cflFV)ORJ>ta9tf86^)6+bl;nZlo+Tm z-Amuq0`+QIx`l&Tdyy}iU*2{xQ9aJg$f(MZBzvl+5g!-?Dy??2va_SKH&PN|6k!zf zeiA1`(`PFrhKb+GXo%l9lF2clSn<7losvSok z?4BQ4HvN;c)6?YS&$H@NF=y7fLJtlx+}BfeeXgm5lK_i?rZ-95)MsVcc15JVeEHnI z;e03}g8M+%ckx`eMbyxQS0-BpfQE$yy~3AQ%_i@(loAS+=;N8BA3sGO9hjGso7!!- zWH{f&AULoo+7T4;W@0L;tK%OV-b}&-jH9?(f5a|a09C`oN&SkN`W46F5yXw#s4l+? z$JVCE-p=wV`U~>2V=s0a4d~_LEW)Q|zIySEZJ*1O+nSLK3^LtKNdW;KYQ(miF%Jkd zTeeGXQ?FP-jP*zT9H4+tZ=)9TUhY3YqldZC3tP2dv z4)y_m{g{N8)_M|ZYNMXhb7?06Jo`v{_7QT2*VQ#;D~X(VeB%V{lLWq)_dZS0$vCfK zwiy_(KkqYMOy)UjxJgfeqeR(#D#}bC7FN>Z)jm@WNL#+R@l3nnoMfR3YV=4+0qt=Z#cUnM& zV-yE`Vit-SO0H0isjt)DJsuW!DNZ8X+&;k2pj)lV>=g1sz7ETQ^_&kUWW6riVbFy` zX@FJxLz=S!e2Pw~>#d?D>PLbQz?KL|%8x~G$P9iZgQ`vPW^4j9L+q^szFGUhEJ z)#TxN1x=PLPt&I!HI|EmbeJG zIY1ZiZ*g&T8Xa^vcN)IjZI-mNg5Da?#%d`=6@q!RG<<7wrB+a=4%P|#zRpF5`}f7I ztqWcBJAQrrR#Wq1Dnay-;OOqpjC573YwGByXBGv_`tH7yIEoG5A=^H6mBCx}$Kq7u zlkMe$8w%9WqhqkQcyqKEn~b7J#x)!LI}0l-4K+YtvUol8b)G`NH?=G%1 z6>V2(5@VJnKl|hb*MSd^xWLt9#ttgS6kTE z(M^`%YhMPj(WS{s6!x4@l#gv$;Y3zUJ6(O01OhN&H6^}7|CyA z?A#+c0pLKdQz`rHc>5{~crh5Pf6n^~4=%#8KNXXO&Uen5w3OukTl{^Ec6 z8=fU?g#_=|s3K*2ob2pq{ac=5D%(56uyU%rBC26M=$QWr({&skR}pcHyb!3!Y?o}4khs{d5tUMs*&XSGCcek!1XLFsQCDa6>>mN3|*fT(S zCdX$KpW^XpC1CtXNhKb)@)D*ziD5A_{9|{&g;(1PwB`>Mr3 zoL9`1%yeh@7Ut?} zzRkM`woKe2k*OHr7(bsd5@9^(`e^{pR)MhZSUoJ4kuX^=z;65T;d#)@UzdRT=87k;Wl+vt7OkGW)nWENOm`dHNaZV!XfhZR3*VR(0UPPgMtGxBqGZylt(i zSU-uqR1^_#WgM%ro@2GMw3Nsm=@u0_e!rnZxk2HPs>`Cv<2_oO@TF&bGHxSDu6wT| zhVGmyz9>qS9+!6X6H+#ReWj-@jl9Wv(k?6{f^jS?d1_2eMAe^{SUIc$Q?{h9Rm`VY zbu3GNT~uN5mYg9LkFbuTn=70#96H1mnSE2`*j6QSv!5RrIMdKjba2p@%CG)d(D|5V zc-S8spz@QF4~vRyj~g}r zjj6`(9C)?Nb-;y9{FZi|szIZvR3WTyGwlMzM1@Ui=JiTZH?*Y_SeNO5x94h~($P(BV zm53e3(~+zvB^P!HqBed?`k03JF7jLFORi)ZODMKk56eW|L;6!wNzs*xT zFDCkXz}lu~1bNB+e#O-HK}PPG*Uev7TRS2GbLOqX75KH}1-EnSVf;L z_?}J=oMzKOcdL6^+UKZoCGCO4)0Lam);Jk2NW}aoA#25Dr}^-(%$K6XWuPLp8a01C zfj^+cWg?(&6u5!)IW`$aL>oqX`C>a_)>Hgh#9ygQp8Xh*G`ByvvNDYMlaC-07D*le zXapahM&q>!e}Bz#((D}dtTgNFm_Sf`ks`Zy0N^zieC#J@xJVEPAt39lo%Y?z!Xoc% z>6`+3brBfP@w)p!OSX>@k9>LABBb=2|3&dh_t~d5Wf2(_hAyv{)6<(muZBa~xv)+m zsu5LjPsxFp$K#@(ZN~}VN%;ds^F7gFUX>xyCOP@mMMX?h9Lz$x2%d>6+Y#w(f8cCj zla!RSvr9`$6Yj&&*qLfN9M>J{F=6LU&LXN&5Rn6xwX*j%=mBC?3|tRteJW~5A(s=@ z;o4TeP}hk0p=2;|+oxfGFcucRNh+w%`SK;bms291qcAILXQhWqT6%i|KD`GpXUF8L zPUBv~z%py;u4)Q!-eB;~(h|If`7AzOBUgb!ogViXv(hffPzsP85D^hYKg{#xjUX%=oS!Jbyah~=WQJYKp8+^)!?pc`5KJ}yN4P5i=g^sAaL z$6{W19RD-C<2;e1nw+hbO*L&q0>Yxl-ARd|S!V+8dw1`A{OH`w?wV2OdSXhSJrOzg zM#=N4HT9=YHx|-fk;Rm2xu6I9aw5bHxu_1~(_swa=wvLmNBUf+rp!)wdOUXTVVsNA ztii^7jUF-s@c%Jl95LJ9k|}!VH#fe%Cy77-A{>=X0bb2rd&=}x~?$hHAEs4j-Y59Sk?+_B5{%g6a@7^R>7*et$lFe`CmKx z;Z6I@ecye{8okauE(?$mtgNojt(@Z#ioM#@WQeojmO53rDbi)i&&mpKXzE`F1$b(7u8_8tHAiH-~ zH@8<-woq%oacy(+P-|;Q8{2qsWwsJMEiF^9i9w3V*k{jFTheY+1TTTkE#}w4j!R4r|wNb^0QNR_RETT938c%=kZ&w{7_UMv*`%6Hy5*!K$Kfqsh*m;GkRjmvv{hVp_BsRCBSL48P5DsR@Nsx9~u&hVP0h;BvhV} zWA|3-Q}WwKAx{Hu0$gy`+Cjy$(+)PaQ{rtQ{PI?_3%M-KBwu<#aS@Y`uXXgCf4pKe z{ER3GVrUf`8~-%D-g#qeP;t{_$cTXtvn~Qt0fQ`njs4Dq0q-R$jvDByeolFf`rEoTxuk*HBH0eoR%9D86B-G+EP2ie+_S#H0m=# z?m3b=*D;4W785!JMJ^B5VIh`={rsAS(-jm3w@AXYlSt@R{rloOD6J*eE!cZ6eGG<& zmrX<7>H^w1p-ekBH^nkJ4iS*`5nsORoV@T*CYYnU`{>y~j)|I=E0kk>m(cm2w6DA! z&$`NHJ}seUtN{H_*#;g`AICJNGE?mEf$xgeyP@6Ul+&`)ct(xtd7taHBf~C z>IPSXVUkRhd~NMyc6LSR^}y}pkB>)n0x6{~slZxRzumBp(d;bv7=L6q|E3WOi^XNY z0In@ap3Yz9yjBq$q362L2pyYsKI**x#X{z3k6}C#N$O(m;NXy{M6cVN8_6!8tkn3@ zWPiKd_4ER11Dy#ip9O5dskW%9YU}5szCs#*s|ox4dt$*_Z}Qb{ z&Xy+`;(oLF>QrsY2TsXBLB5Q9vho6R)-TZ#acaB(k{D25^-wpJmGyUbcek~5AqL8` zv!la2XnmP1_}}wzjRTaOpv%TO09QYJ_z7^s?MUumsl=C;S0<&C$lGJgW`Yi@n)zS6 z?JFO0s;a0orbb3Q2a+&Xf==39t2UK2H4!5tHSUg|o1v7U9fJb{DTAQ;WRDUKc7^=q zo$rJhtRa$F>nZgrP2BcJpU0;oU%S2C{8o(V)oPjrD0z0=3brpnxYe(t0-0>z zY$lw#+Ud{zJuM18%L?n=z71KB&_{>p+P8;>sIO`)V-KohtG;vY+pO3yHHwZZ7jRv#43_vY3Y@rF7D*?M*irnt!)aOvF^|NTu##$cU z=TE)q9|fVj&z`Wqr0T(Q0#HBqN{VkY*_AR2?DG8KT+=>F;{z5>YNMT`aP)ws)&Ot$ z`3YrxePctzY%k2zKCa`Vqn+Jd#u9q2mC4##kE`ojy~#eKpdhY?EJ@4``$idR{J`U5 zjrD+VvID#WeyFatr{mE^jF3=fL4gx(Y^_npI2b=qZRY-|$Vp^#Gn{P0g&z|e^ZbHk zzQ(X{I?qx3K7J68j(wNyFE78v7;}C;J)`!Xk%=jg^7B&Y7P4MyZds+v9`xHAQ2LkFF^pbgJWqhsQD;DZ-qm!h~+3c3|YbF zPfTnzlqO_pYfJLHR_*$5x6)#r^QDse`-kgkoy)__TRs zR#s?zy{7y?-1D=70!Cc)+LB!(5|a9|1IA?xh2GMyY7EVDhr_?Vn(WNXmRtr77D)^C zKAm2t!{EDkt-mf)8X8((4NI@vPaU)qX-Z2&)zw*HG?|)wGNTy%9Iji+#1f25x9J|_ z_IElgtI-(+1a`n3Jq)NFJtySD8@y9j^lBx zKt=^bi|dU=`#df~6Mu1GD$qRD{U+al_JulZqGm=)C!=(3BEfYw^F z+BHn`zP6_3^``l5^TC!g)axV?)O2(Na8vQI&L`U9%3=5>mUSaqLga@Jkb6W~L?vl} z7C}*{vHUEvuGQVBiOH68Po(V0cNQL=NG@ycsM~17Vl$%gFInO_g_6;I@`x3 zFy{e*n(5NrUkcDQHWrT$*_X0%78D^>AdrqvlO|>OV0CqTV&dAJK{U^H1fVtR?FDtc z1znCV55<4cv$3=+E3eeAppP0@`<9zdX(k2+1JwaH_J=t;K)zE@umJ%1UY>8`@inTd zPJsJTjNAYMU@q>7XpY;O+wIF;FXX%zxwNdTt+jP&SygaE2+&EX$ZM-#HNgOpV&ph< z@Jao|HDa6#50L!~4x;?-W}pY8kj@m8G@q{PpUeX}^e%EAT7mVV_jAk06=ED5P;3tt z*^NiPDX<7=VhirMi(Sb8Q7*tayxcUfL84n**S7^hUm8zZ#>3K0ZEbNVnhz3lZ}s;( zTIc5(i$4@I*#dDQBkqBazK@)qAroW03!oIHk$pj4b6W}sCTexob{grac^k^hshD?o z&x@8q1Av4o&XOj>%0U`=bZM!h(?K1O90o)&yxXj-KTrEWUOculH{BFz44`J}*dA|h z{L<+;fGspR-hsI7_R&Z@dBV60q{Nq&KK2%{6Nj%MSMYIhCq}6lSklf`yQSUEy6-}7 zDM9i>V2FrR_}13Ja+fM#*4ORn`rUkPRXI5$Gc(*kjX`xD5YsbK5yZpvvCv-t$&RHR zU3G2G;G(*`Jo@8PY$Mi$xH!#w3ISqz3rZv}NJTT0WMtq~yY=VBqNB9hM?h5eyB|BV z6u#RFAgqCQl-1WB{y*j~tEXqCGZzAk13&-lCTAV2Ivtif^cw;hcPT(`e{|;XmuZ~U zo!lM`01mOqahdzJqa_o?0>Bq*>seqGMIv49yHyc=Nlq@cTJ`2|oU`NfmFYD}@OWn~1G<9S66*SlqDQstR_ zPF&~iKpC-S&n7u=7Vx?%0QSol!Lv2D>j5b~w=t2ODS7w64vGeR(U1Srj9T7s_ zZ6tgU7gOp4L6g(JeLgO=b?g;JGC191t2Otzw6Lws7(SQedVbGCznc@#7I&9Q0P(YT zTEH8J)xjzy!?6wFIzS}3HXzU~5H#i=rV$tI9}|J<6@UP8*Zs@hJWa@qFG%#+GF_H& zTJ!pvDbahSP>27ZBcnW(><3Et*5$HPEIl?ipNqsVk#6Py0Wj~^3}llKk{0g9(a zF0MGkR(eAVTv*@E_O`XXy|nBtI|vk-@R9(8&&@dolvLo5l2%Q|;e-6ZMg^z`^7WawMA znQ7|tU^q9k;NEsq%t)xRa;bE%wY;31r&kBS_^YV+s?e{jJOz00AnetaFGpZsd$LK}mLltT1 z{ltnfQ%7m=Zo_UUNSNBJ5Lo-+^+SqF{Lq7Dx5?th>j-86i$4kOuJnf4xw+pssTul{ zd7GOk;i|HNvvGe-M1x5`>t^2N6uG&{=dsnjzx>*y&syBIiikQ+CKQAp_#nvKETw5e zQ1yt+Gj{hlAoB`r-{{|BFn49uuh2}&q?xbw(yxkSB0qNUs&&FmxZi7Om^E(3UGbZ8 zxJG*+n$8$;R5yDWNo0BfSyWheb0|wy^O2|Ih?5&O&tViLSUIwyBJ;9+bEfL#`S>jt zWGO-+IF6<;v!EXQ&5qfihQjxC*0fe$0T#sfJ^gR7rY}fzg=Nn(}tO=G=j?m(j?*jQj zii|9M*nH-u))?~^u@ClcJ5cU6jv-Y-!*%@igh%)i$yjIpZMol)mxQFS2N46nith$| zyu2beQw!+m&ZnsBl=p;rYQ5kRmfanan`YRknWrxP165THCqm2jyg-tJt7WIi*f@*a z@|adJ$6YWqRbYHQ{qg0b>!zm$bDN!i$WL9S#*%+JK-Zc=K`h6xb! zb|0#$=;_fW`X6{`qLEMBBnvJJaErIBpb+*nv;~)JYU**TgF3q@(n0_8kXu{5tcEV9 zy9mhH)pH-e&fMfk4RyuVfm$4ajDfyBQjxCTMPdoQq1XOC-dDgTFFydt(|!&QbL%fW zjDEMZW4WIz^4R$xDH(i0_330j%^Prcvy0N?IyiUmivU{7?}q?Txi_C6dooOuJA@1g ze*3$k@Z}oWr>uV{Z*|o)W<7D{^~z@UyD`4C$gFe)VgA(A(BFs6MC^AZ3=N0F!y#Rf zxhAGV>!>oI79f+{(z{63B5J2ByR)&ejR53E%CCW#V-I}pLaozv8@qO+GyI z;~r)FuRZYY@USgmYu9{r%f`&oGE>E{yx;t(v6cRcE4KCNQNN7QA`9s^pn1M&l(-`zFnefjrmKh(B&c3S0V{;7}4 zkx`P~%AYUp?r* zUbABSf9~n-zeBdAC2Nk*fW}`Vxvl=zQvGOH zGLT!aGfp!Y#Agk+#O;+U8EmD|%rE%XM(6eDq~e$pvoGh!4X8WeP988}&*vmIMKx~% zviO%U+3XQNzqxP3N< zX5%q5S>-eNGA&cS)2Mu)zyA^NqIprP15diN=MNCe7`C8g!cQK+5ABHY^9!hYnDdyM ziF*(^hc_@Vker-EYrV)x9NuA6%9o;7>i)|!og61HGFAHaM``5LlFw|YeC;LBcWf86 zLR=>!c!rXt4Wpavf+?0ENuTeHm-FB$h6ku_nCwo@LdxuYd$ZgmXOQcQMS}+;bM@ zlkeo|01Z-XkBQO#*SD?9hfh8du3t0Q1qE< zbmWq;`G|-ElB%kP_G-U1t?L5Z7tAM{9d%kA4{UDnDJNRD_W%^Ql6B|{s>}G@VV$}A zbPcKl9Rvc_*piQ8dsw--%~jTnUAyl)Xlc~5axw|X0?f?J4$rhB&snN`J;Ym2{zq}o0|@vTbP=vRN_ z3>f!F_SEfUX(u6I#?*;>oIa{eaMpT*#Og5%j4`(75lw7u|FmSj8X)f`8r=j21;ZIb z?zzo-n-ndvd^IO|`6qE!{l-_cFq^j-Kf}Z6+_W7dpqs2kefYmlwF1sJ`)*4KXWu zL6T6ZRi{KbpG#P2X=!3DpJ>KY{aqohBskaKO8f+kCu>N+O1=-97XxoEE>^(?|7ijG zr(^5o0P^Y?{P?L1wH&SUOMAc9A2CD2IlOeX37e%~1b^x=kVCQm9sl^IC#f{K!5+Eg zD;3%HtqIu7KN>AkeovXJsr#67`D2_%_0N}{OaI_xiTmYyHC?F2PHjL;!k*jc?+$&shNX^Xk=A6(s@1XEEoi%HBn*$Ip@U{J*-lEYqWetZH8hr~7yk3qzM0DO z>@GY>9YBI0Tr6?U6i{Ub{2T!!c*^k7P5f^BgjZAFr+CY|DkJu_YD~P5ayMvLaXBje zZ5v|QQL1^ybk8sxK*Z)~A2xYJPTt&iD>&ab%7xamCCZVfgNO(T$;s2=k3S}J2?{b1 z5&qggpX`adWJ za<1=7ihTNiE|~@o;Q9G7*zn1BqX~&NxW760>dM~BR*pix$SV=e*&ag=meZkY+U_v&2?H2kp1PgeA7Uh z>pkm~%}JD+)tM|7X6WAjF$V|F?MWf%<45sEpKIZZU%EzhKl$Qsuzh7SMh2S`zX~9> zBX0M+CFNe?H8%TxFAq#$$$^W_=8@^F|5|K1Nh=$bH+N-RZ5ur-1cK5iwO+GwFE6i3 zod1ST?jx`&K;5b)lBbw8$FO@&AfHcNKd&;3Gfn@Lqmrrpxvk{3FxYGMk+SK~F=VA| z&Z+NnK)AIPF+RDau{BUFm8Fv3ZUof0`s@c_?AR8sJas+6a@(olIkfSVydX02eI6*) z4pMBG$fALP$OQKekKn_Cr5@Yd1e(>@^B%IVqWh2P`X$ZG)}9X!1KA!@c-^p;eMwDr zZ7ropb@6epC{H=}T$9AF&GQqI+g3v3*umM!-FPrh7>D@c`IVpFO+vQg0`wB%qr3C1 zc*-$JvP7$Ae73dqZsTJW-F}j*s5Fa9?~0D=_-rK)K}%v!a}RRE~CW ziJ6kvw)pm~#N_07n!EsFVNyztDkM6(V~J}pSn&lem00#quZsPnAX|@1m)t3d`GG2Z z&bdi8TWbrV@155ZFt1*PNmB1o-*<*#W+$_E)?RD9&#TY7_QoFT zi}G1)si~*{ZTO!JRBqiCvTHPD)j>h<K(50)2{H0ApHKsdIe34?qF+* z-<;l)lk};*O$DuLoCa&7cmaC{up{a>M0_8;y5eRmQ6zXKrj43)?S@Qs=VwsD(1WGE z7Rg|9{3t1()JjAk>~jhl8lpL!eMBnHV$Zyrw$EHZHmF;fiMCUmsY520k8j6WBn4U; z9cF4RO@Fp$1MpUsD4jPeIP*e8#Q%Zl;k@(8y>48mw-UGh(r$cmG!~MW^)MRf{6|

^;cu76?mu5*grqEb`5{B+8zv^Fa;ZdF_YR?{?R-rXsdA*eX7Xnz23?EU-s_UStdRZpFz&W_=e2CYmky{ZAim zS$&xWR9iE;x9%~eqO}THXZ{k~y3Na{IQ4DfiT$L9*4D6_e6}m!MUZp)6Qj6HzA86b z#+TM0rNvSH+r1ZVph`?b5V0%A_<{r{>#ifCo&25G&%BKR^Oq6kzs-XYiJNyw$wDCI zr@OFMwTtCiA)vO)1hFhU$gVd)D>pxdJkw{_xxcPeWPs+WTC?}7fPK5kT+2f5gt`(* z>%u&XUb;MOh`Wu}WF%G0ub1Ou{--(1ALTwBvwN=%pqIVq>?~gf^@prl4IM}P>w4e& zFS~zpFHl7{tBBD^Cl#Jb@R#@7%-GS}41?pbwEpp2M07owp}wvTd3%PWij@^!oSU;6 zpAy!2Q&Cz`0r@qVDOgb5>ZjfPivl~jx5ek}?_4K0m%W1&D07fAs{vFpPu>4)b9=6O zZY^69E%;=Fno7yIZ7Uu}TosmPbLU-I2Li+4-_OT&I6|8n8-XStHP!_e?C(@Qz;-^h znN=MQ47o=O-sNTkts!1~Mjc4$_+~^zP4e$r@iE zbWC<&E32E7WJC^DR_uTja5c3!+AzdwRn9U;mw~0?l&E4LamV91Q?}bbWp`Dl3nEqV3hJwv|(6#t=86IRd z?X0Y{Vo|?w)*W{CGR@g`N@oMp?(RdtRRHIV-ni4LD08qqfk)VpXr>YJ{1BPXQescu z1hd@a7A4$;fyB)8n+#CPt`<+MPh`NMJSK7!&jLf6{2!_U{EE!bA^~Y^9M5o#D zr-`JG-r5x#)b5D5We$vS#)JY2Hl35kOHBR!N|79$)69H$(kcsR%Zg`jkbNqPf`HsS zO}4y{gaQdN@||75z1h)O`VM97F|v#v7SdzmwESRF=s=zn=AvlqR&~ksaRLjgAd>FU zo;3Dsw|?>=nDSANq2%jY8eRTDo!_AuUrvCm$Ip_Zhb?x*{a+qL{rWulIEO;6%!h|r z|G1|C>>SgcehWej>rz6HjFeV+`uK&5yOL6ySde2@7EiZzm@jHwF2OQ+n#_>pfmSG&r9O z0)P*w=r$zM8Uc0BN$TmAk?y^}98}4pOYUc4FLwYpIIuufy*5T97(mIw^^5+e*HseX zUE;4s;7cE6nb|m4MBQn$$elTr6!GJ==Y$1Z8V$qWk1bAmb6x1l$lRrnzu{*Prjhq| z26>yEo_1|y2HXb>p*diG|L(cuAG7p6MDk>}mpZ~Xzjft6`eBztWqL{zWSJ||&Fb## zOc?%tGmlPiHZ{X|sUu6$_gU|fv9m;gu|xos;G;=~cX@F&4Fe0U2-jT4X#Y1!YX=*l zgwvAqt1%3Wkm@&l@WJ`%i6KSyl(*>(i$la0Qfvij3Y9{HfqNZB0d-59dsXu*6k`jEB$~2nZkpFAtwW!|x8MudR=)XUUd)gJ)6DeNmpge)H8T zE?@4dd}-J z2NO0ih)O}Ga-4f6&us{?)Cb(I4JM7`pDY|WLFW=IF-6$ZezUV|1Y0MWEp9pDjcxd1YJk83=>2zo)nj~YUAVd$5 zjO|66QqW+kE>G%Rax3Ubnl~XFLv1WKu*JU%Sor4#z!vDuY{d ztY@I1Hp?)K*CD=oo?O4}|42Sr8n*AzuJ}DyR}S~9lEKi10%{s-W~ZG!vrDa(tHtPQ zOGm7<$?cs@F-Zb^&3emUCjITAp@LZA?-WD5t++X(zzN7hl%0fHt?;!oA~ zb88>PJTj*Tx11!!v9Iva{`NvX8!UCI#o0Dm%W9x(B(Ctn;T2PQe(=Neub5)n%R-%J zTCf%&R)yRz71e7bM)tLQd7m=02V=nY!#aFNyt+C7r_7@3^o!2Vv)yx1!RMX>AB2Ti zRdj3~Cl~NwR)~S{;pa=Jvsr!ArsJO~-W0?2D8$Phs&4MX4KKG-t1yVp5~Uv=ZS6o~ zpC08WF>6aQRvKoL*AkyoOpPbb<%QI3vwqL)W}K4#!Tkgu#o6=g_2&CTj(7K-5C?-6 zOR3zAZUM_3Vn^wd=ixN5jg31l?XJRF+6hO?H3k(&1(&v`2}S!;j_4@g%w@0m3F?6czozF>Jgw~m!{n{M_cz>hC9@HeK8?{77qOv~>Hb@VuN zY7t&XIPYJNb~Yz%(G4D%*34?2`}RUggy$~qt`4!cVmI`nTA9YVs;gBX0>~}$N5)n^ z>$#}Zto}S*)~N>SM+bfBt;Df7go%To=Vh%d>(VuZA%WSw{i(XTrq}(>$LhL^8&?y* z%kcWDN6Etco}S~}h`7MZa{+)0&;2=|U6I`+pto3q;d|TT;XHe?D>A#gs+O6YwYW zfBF^`mBv|e^)j3ifyjD+>KAbOniUWvP5NczX42JCKuuBA>Lu!nnrhcV!ZiYXYA>%G z{W2!@=6e{NQYkvxTcey@aP+;(rrd1}qng1^eIBXO7O6^GL$a;rtYt8(`%G#1*6 z9MQRFR|tfb;l7i=cSUQ4emM)?JmF01q>yava>ww4cAt z{D$6bw0~&%R;a>K&wqNcfkU@&djIO+_7*^O>@fd_&^sv^_{tR!K0Jbg{N>bETO1wc zPA0XL+%PX{jkl9}!8>~7W7AOF(6UzD+i zgAZKi>8cjX$nu_RJPpXq@-b`>rM~Z9JG&vL_7xHKR0rJCa>{x6h{(0zqvub8On<7_ z1ReujM`&HA(wk&(DE;;`9|io^x^nw9h* zpHVG&#nb1!mdx6L^V(ATZOl*KzZ1#$bG*01BsZCZXk_=iwY6xqJuVfpe&QAJf`foI zsKXSqy`a3`A~!#KXXle+VZ}c0#~XXsExn;FV80eIaxa3`slUfTE4bkVXUT(Ksq4rBwG;wckUgeM)xwWSxyA&qZ(CvK>C)vN zxgT4~FP59(zw^rwTlcLSnZ1luOt!fgT6u~+zC!EEEjFTo1Pq|jPs7?a!>9~I4?U(g z&QWH!KXDk|;c`rQCiUG*&7Y_yobz#JGN=0+6RbW*Icy&r*?R36;0oRy$%P*c@iQ@R zqaWo9QvmyRHJgqNHW#z>$e*Z^@RD$@bZXXhi*RFm|H9r6`nkh&D^t+_dL>{_hm35G zD&SnWXm_`ccM?Lr>S9~Wq5|0|4uJ&onUb1bqS|L1WlI5c&H9fLU_w zPibQ1iU<_5@b;&(p56hHU(}cM3caM;W$&8P%n$?jy@ka@`|>R@VFR_8*UIRTNNijZ zboXMqFfK=Ru&^Fkix~f__AQu+uG>vq?|RmEMZk50P0tq*rYO3&(1mr1UH|wV4G{+3v;E`h zTAoG$Y-CqItxn%w!`xiTf7ziKuqIZZq7&^ZnIfEmjy86FesYz+B+J4brffWq=73NW zblr%Fi74WIxLm&o6$g*|?25T;&gdoLCRBbLu<-Ww_tnS7uKj`*m7(V2yXONDr}0KI z3Cl+&t#01H+ss<5oqD-$!y0slr~{9w+=on<*iLTz00>0ADlUT7duYl4RH}=ykT0!8 zi`ytFRN^)@(Y;#;+8`0;H|o1vDq^QMeth7%6+`6jZiQ|$PWSt1O&vmFVr2-Q6mxvt z{sj;uB-NW;MQuUdJ_WqmQPjv}Lnb!TQ$XVWZe&6nAt{JYr&H`8bk=0G zjJCnZIO`tdL? z5>*C=+NGuZ@Jss;>!}Z_Q6TpM4w8nAiCw?e?IyRC38b8f`2o(!S(VrQftHYNY}YMN zhkR!LH7+`;*p#zVzZpQM@h z>6V-09oI|7SfJN(6`OWdaV#<53$r8PHCf)vBrc)I!4(YJvnlDVGDO{z=nl;5RthH4 zO>FHpd8~ES%s2jra}+4~F$qABQ!x_N4!l|NXRyp6=D+N{s(;3^&mhzF-rZC zq;z@uJ?1GR^l-=FY2J&_pYlXEo3RQ7d8ac4Lmteebm+eiuN+6g8RTxZ>lE&YrpdY4 zudXSY%)H=ECXN<-s*Q>1NkEleHhx8EgTfpdF4AlY%MypcbogAyFS6hlQ3QWkOcj1u zH+RcOYQn`-#h$k11H(2CNPUrYPw8wJ_JOVAbXbL+j)qd+dx-ed6tmVIV_tCFA2sxS6DR(y9g!!5nk53fFgv(&ori4Od5#cMo3YeOboIdoe8Z@tm696fYd?r}#qB zILU(XAnzCtjF)HH#3XP53>yc*)opoz;9}Lu35tZ_fuNF|yXEH4Y{Lge8iTaDi8vOz zPJ3Ry9{n`%^6mD1e~~pHxz@X+Mb^7061uEgEOd0l81Rs3WN4b2#wx#7f#ieN?Z(M5 zQOkxwwsB@k0&8NwQJh<&Ny7U4F!(M9_{&I*F8&lwn2lxqsCRyO7}p z_o+z7$}W^o_cFxkAPkuh&d2-{(>+~F?D2#17otmzx7GTrnq2Z#sN%2QoBidnz}aG` zY249*VUA6e@$%A4l!*nJ8rlkLr>shJJ!xlq*0!V+&|%ADhT+p@yXXRbnc=xt;b4nG z`1U}yAb#HEy-t?^-$uNakrrtHx?0VuI_LcgwPM|**6L{FqE+O#9T9&V0ws8^oZhUU z6hqF(K=z()hM^Qe-yI9L)(Z<#o4_46pTpeNdhOHSkdF@PuW+rbKpeWYUF#?52n>Nv zEF{gv-9vXsase!blz?D&f z8UISla%lo14WqoVPwa&04YPLf1&UeYyIiU1ovVBk(*(3{xQ-k$2dC7=R7wItGX98i z@k{+r)3G)BBVV|qNaS&LzI3Op0`*f@=vWkQTFN%IX*FK)CsB)@nQbTXD?$_Wsn)LC zJY^4AUn2yOo0$Q_NBgmUEwG2Ky{8x`nd9vL1;U%5hNyiRwGY@~zFzj`%Ql=8Tpm4k z@gWPDz3&6c`P*)tJQk6m}+AwvnCq^(=F;%%z&oWN;SY{S~H??tivai#eFWe@l z_B=v&VV2)3r7+4B!1pua%fnpa2yue=xB^886=;taQYfR8MHFhR zDnjLWJERB5oLP9n0Qwzt1dX&a5_LO~*oj0ap;9fBF1sLw9=IW$zjsN{?>yJU}1HYV~jjS6; zS4#^*WtmA*Sd?$Bv&I~YvMheSE?K0g2d$nmnRvGyzO{GQ@8*96N04(nj z40KR>67=>Q%DKVdeni_v{S#Xx<@4!qdU+|MNxhk+-|wFg&EMF?b7_10{B2@^tJ(eD z8l90dxAz#FP|P~jM76G0#{|$iYR1kALcjHF&zCA zm(Y}79KUKU+HG$`^j?Ka>ykh%KC_Y?JS}1DBRqnknFy+&YFZT`I;By|qMuiv799Np z(!ceA9$Yf~wW0^#!{pO%BAHlWqWcf0>|dyLm8EiWE`;cB@-MuI{;MoH`E;cl$>6j#38!rzQHPBWE~4 z&}GXOZEO=|y0mG#pC_mxM!@#SAJc9!$LT+xpJN1+h@Fd z6a0J=#6X`#-(jEPA3U0G=dSvm>j91-5JlKHH~J8QCKa~4Jq$@tj-3@0o%~@ANP(xr zqYX!7Vq!eGO7MIS^1oVuOD1JFghx==bBs9CAD?U{2w%pg!e@Ez3gMMtBoN5X;8{Iu zSG;>>?6yxDa7&KfGqdd1*5+9Hf$#*UDM)Spw6J962Gmg3%S+%dHS%ijk3;7&t?gy1 z4rjheqU$zxImx#a(U1iRgs_@(qxZD2>da-@gxwKL`l&x{t0|a@|xSGu&DX@2(D@ zUda13%fT>NEk`sT|M?Do-+dd~@=JA*m|k;WSME38@@_;%_+ows8Wq>5^?M3Qzao5fbWgXizJ|!=yy)?W6$Yu>Aa< z*u7bH<&=(%@Myw^p`DM6;>=10xUntOZO7y`a_=uOj5S@>SCuqH3yV&_vbsJu$aVn> z2`@`?cn!=N&l}f*hdk@L9b8Lj9PRf*IErxa>fa|`Jvh#>JCw{KX}p!>T&0jIFijs*YufKrC(u}Aa zE5&#dvM$xP-ze-Suv$q4UO=$ehhP2~ql~+15}Rn3Ke^5CmKc3Gwsw+#>f?5({wz|X zLPDW3TXWz5@3ZBxKo_huzj-(s7)%-wDb${0#;m%MRwvz__cZ1h#g!MS+O+l`9ceBHVW2Heuoxa6=li2j- z!+`4nIhI|;`Zx3qWgEDhP^=pZOA9L@2s~yfklzhpin5TIUKcYn+Y}d<+ia8U{vBu4+xa7o{X&HE=q?=cFk?bZk(%*-- zX$vUZ4(-Mq8XER50P7KT8?J7!?$iYIBR7)(ky@HN&8aE5Dego{DY&{OsFM=rv9>OV zGIYP2!6)S43cux#=g8sIjY|;R5N?_#z##4y51QDRG`~z@@oyOMSY?5=_QoIHit11u z*;OQ3vL=Dpd9@(u?gNulfd(0Mtov10X(~_|l>5`t&9$!73*Se{MY3b>+TJC_#vfjzX8$ zqx8Q>`k3CVCj%Gq|(`6`gQ$7`sdRjG|1<^c@|Bi6I8t$~O(Zh#4b|55m!0Z5gE z4TpY?3;ZWUtUNylbF4onU~Ziqt75Yw8eV1bY!tKOeRDRWQY^&r=8B-}&m8;aZ^;ZR zp@4x}ZK9+NI_^(!jtLstri$1*9B6#cT$-7REDc@wZ5*5^Rj^1r>;HuAOLyO@tLL-8 z5)%-shZsc6Xa>R~LQ#KGvUYqIiGa_ViPe;(5@ss5>o{v$?xy-Vz;z0LQW%a;bjl7t0gV7u^8?&@k{Ha+Q!%iMA-96BN%GW%{yPkjES z^QfVrgYLxn9W0R7N zs^$^1f=zmYf`prvt2Km%;thk{=ywV2RDfzt?B6VzE?1CKh>4CgsXA5W@V3|15zsdo zw&03aSC<^8;jsm4+PFKxzhyB{B!bvsUS0$jCTa^W=~W~jgg2p&FHEBQX*pI;h3;;& z$&bDN%CBg;OA01C!{-aIM==4jLcAIJVQ0sw0VHG|B$dI zHbQP|nIO~H)EE{Y;>^Y%`2z$xOM$;pJpo{f?^UrC@8Cy+bUn|p*Efs5L*G<;5^F-`bE|lQf3PP z(*4Hpk076#dRzr#k!^b{tnzAB&B+cpNKoIZk4rXygNayNM2KO+~v;{S1yaSlxafTQG zJl2q5e8$bXrkwE=hv^Ib$?Qa`YMa-ji06e`)9UJm35k#zqyf%LV&kn7ucBR2AVfI5 z?BIM}FVb^w?Ah5L6Q3jozB^uaBlc__D!cFNDqPrXR%l7@fj84hZCab%oBcry*7IS9xz4xFD=0*QmihSk z*p=rK-li+?{Bc`7TPj@dujhnZI87}1%>g4txzX7irA_8AGOV`yAa65wO|505aF*fd zv8yk%%pyJPWSlFOSl19))LJruP!*%Kq+L?iEl55n1a+lH_;pQ^7wl>wSuDqk1qnuTCe8RZsn%W7};x4)s zM}cJE!DRLsyD0$Phr#VX;HEqC`W!^*;y%}}-1l$pYwY+KxLH_4X*H(D)V1^i4i8dG z(-)c=hD{m*I-eaum7>Ju`k8>ghb6bQomk;R!_HMFvAdn%k&Cs}FP*X~ zyYxfOhTb->+I1aW1p-wuK<=;BY|^G+d~{-_@gfrNK-K7zRluA;4Ou|jLrF*P@joybfxi&?nj<4I- zGUP+%+W|zg+}>a}@h<3Hcd)e=bvb@_UBivtAktHNV<5^WHIyEPPxMF}E$x{)zlYsE z?iHGDkgCoo6ug;$k-d&rl#6)JgATL**zpNbuMA&;}!VWju{b3pzE^?NrkBr8rm zef#m$cm{ce2thk#kHcK8lB2ov+s}AUe*H)zBVY2pgB1ahcL;xGC*qKZ8@;Cgd^1oi zMn%@J-fZ*jHan%SN6W)ZtQ2sT7zwzAd@J%?@=tHnj7gj4Fe@Q-LuCdddh!5}5g@qa z6%V}U>#Mk+e(PyjT`a@gCN*xzKHMSet;^sj)PhMW``6KKv?hL@Y zX-(zR3)pc3MbG`%1LgNZy8Cz3u7KQ^%JYF1o$wFPt0Slt7=_F_>OZaw`cMfa)m+M(c1dZz4`!+Oefei{`=}%tVgAz zIX1YuY?V$a~UWCc?paoc_C%lyy_#dhXX*ql&Qo$>1>?h*Ib&YVtVZkp?_c}{+j`KYk0-<1}KRb*0E-T!e107%cBN-VXR$E z_?rUtjvqK4%Xb@FdFJIsXL`%InZ&kT!g?2+(xeGEVMAUI>XJCU-{$sF6Y$LK`10g8 zJzB!_L@?F~Aj;cLO+6Wuw*N(HemU{Bh@R2iLuQ|w>?rXnUokg*3fH_2S(?C4fBR2U z!{0_yr2s)F@y#PVVwT!|?R>16-B>vOh~>`opP#+L{8E=vMbk8bbw^@; z;(Q+m%m9(jK}92Je$MqI2jyvG2dOxkPjpNt+qykyW7)~i zm~5R$L=Kg^G&1fGO>t1gn^cpUV?cHPu(7{M^A_=hk1-BvY4 z78AQ}mo5KM%^G>N1Q$vD;L%GjNL0}>H8v?$Q+@r6kKQ-_1ovCqqJ7%qm5??~Fg4{A zSnqvTaWX30S3$buA${Lexo?XaE7!#)gPUqGJ$>mrjv^8vz21#Jeegd&e;L|J0#Po2 zmPPlQom~3~QTESWe<&FD_7jWm9&4z>N{IURoW9QnQxZA$(S~k+%|NX*yU?dvEYRm# zC8fFik1&@#HzA6>gXIpDx=x_mIJ8r-z8wMR679j*q<8_oY-!#~WopF&QaC$j4N>i!uc)%4_$fO+xE`r)}(i@DrfBoJpLV>vTDV0kn#F-aK8n{SIL$|v1)MlJp5bx zDi7rPtxr#zq!yX;3%ug7Y|z5I+gfLUBjl6J=*qJ{qS_2r&?%4-?k_}U$dP8XDX``w zqL^MV0f9$CN1}@fZxni&s<`!ba5F5X-(rB4#!h?x(*~#4Jn0EP z1+3JOjy2$Npk7?pU?3cvshXHnbr~`jTwyzGNA(F3?)nmI`;X66o*L9aI=-rl6F45N zZ6tz*>|ro~6o82afF4lQ%1ML{Si$YX+paB!><#noyo5J9>{x##m(m^4(G~BhOA_4!NYpA#G+Yo56#;AWc`eSS_a1lp)_1$RyBeL9u?E%jW?NY zWYIkqT;6`rcdr0Z4bJRnqLcY|PoJqK$mh{EI@8DZZ=az`X3biV<;nB5BBHLzu?cCE zm#(k!@7Zq0I)UC)pO3b{nQ=X)7&P%OQ(xqO77Q*o9iPcbGyGO_)B0=Oz$fUfTfuu) zewFK&-8(Kq70f?^PoDmZ!cK$Q-_^5cOQYp?&5B;74qAxk)gC-u(hj7f{?{b3*leg?sGXrd%*?RdeG9eJ=1X2xNw3OJ_4MhHjn)`D^_>uab?f>upm88)Bo2c{${r~3nzJ2|_d9cs_ zHxE|v|KY(({&%|Tf4@fXm9(w!+v z`<~2%X`I@$j&*yd=7w4_=!pCElRs@%$y1*zY^)tm-v5~mTQ90@L6+GITUDOVp(iT` zYUZS{6XhN^;jvoP3>sOuA6WAv?D<0~JnF!d@TcdfPxKgGJpa-tAiP~Y39TwjBXOk= zLOgRWmf?+XX*oNLB+S~xBrCH}c4%*RnX>TBEwsVBRDyjJr7lTl$BQ66ceR>t(f1FU zm0sl0>w5H4AK1rgp5Ak(b@!A+TlaMy2yv+j?X+?Ag@X}+MYdDTA`j*FilwJ_&I2+L z#qcyC&)xh=`Pcq5LR?b67W4SkDoQ!)EnSL3JDU-9{WHsdw?&WopM!pX%MGU1-e=t1 zw(XwIj>~u+y+Jn{-K(3`C}=S%=Qlxit{AL8M;YIO>qz&$f6rv390twHtIsZNTrvEF z8YFu^gA*u^+ZFEb%bqTz%$8o~)E{*Nygs9d=T={NMNvo-w-uXiQ`)OrJsCH_Lvg-cLAv(FsJ5J6#{_bYU{lZ5FaD50!V-qH#li<#-lWs+eOJPXuFsVj&#?B$F6g88^}0W9PE`yTKLZS5l^Ju( zpR7EL>r@bN^@~7YFVr67l0MUYd~X2$s{`S5viuv z)_H`p9Q4t_BL!zP~D7#Cr#LHhY|2-G|Z1=0CR;Z<~wW6m6y7-T=VC< z01x%}!82|1lJ0GTydMES;?ILw-0y^ka-2Q6h|X#jHb~g-#*;3&=?yZzJyr^tK^1F} z>L1$17d;F!z+uMXR#5+!+f6f2>Kla-1)=wcEhBCXLS@ zGOU!PByZR#_pSFor>e7Z^*6FnE|<*1esD;jPh(C2dZxGuEmC7P(*(18+TZ(z=i>6X z^d-JG)R2ug?BLU$n?R#ThGbD(-v)iBQx0##{6)<2W9V&zWFGXfAB6rzpz&caE)um% zH+;Q1Sz5N1{VEH)#+K*Bo2MvM3XW-Xc=@MItE-<(D5;K~=eWD)@}v$kVH4#-XDV!Jra2OaAC%$%W?*6^NtuI6&@A$9kG^J_ zczFE)l7{rqm}jYjO{Ce<6q4ws?N-AYY$m$3=bd+^&ZvhF2@%(ip)9?H+f}; z$QKlk`W5qB2Yr0e!;6L5B3k;kXYK{@ksE85qA<_9c%twq8{yc)NcuqCj z919!lK?Kf(uu;X-{%-?Z&k?2 z2gB(T7n_4>^!|+U@4d)Bs%Ru#GS$VjSl{~kKNC_6EVafa#Em40GNHG&3P+X49$Yy| znuk0ZO-{_1kOHgBi@i;b)r{z{NO0+TXDl20YaO$o>`*cMSgnpm1{hT;FSbtBB($iC z*Uf}H;j~Vz`lVR194#}V901z)XrWwnV?(8x~DW@eLB} z>Jz8sNDVRa5&$OiB%9S==aFf1f2sv$fQ1mBH*E9t85!g-kheZG^eXJ6xWFu%&#=68 z;sXCiO-g~4RY;mqdun0|+eVpy$)%g!1VUYnBF4Xa%de#B;+>9r#TS^LATOUx-_`X) zfnvQ9R`VY>aewt#`3xqH@Ao;s6W@A{@~||wRD9<)zTa_VqZ1@1dG`-B;O#1_ep?dU z)NA2zcp0f9<(@c?;3CHa#Zct>%=ggKF<4rt+1l*e_P^Dl>aeKl%TL3@V2=Uug6)X7 zAss)zs0%^ZV`$dDZmkTMGx35kWd_+zc%E^O)-LpSDSc7KZI>H6fFzxqp|_{B^K#Q3y}Vz9$$@ zT2(At;60YKPVk6bQQFU6RZQ61^0NO3uiwmb**zBuD_Ppjz~jqkko&-w8Cd;oIL|Xf zQTJs!$%SgKsi1QoP&SPgefc=Hu1r$4`uw~>^HeGI`JXh$$<0a2n!s%{88I42_EbNF zsAUB%;ywf^eScAq-TsT~+pv}1A zZYi;u(mphVYxHhbu6Csiz)KVPaHcvzr0U{%WJFI5{eA;)#IYLL!iu@TeYB%JuZWgB zGzP+{N6k{6kA2h*>Jld)ckkZ1{_tTP5Mk+xqq?Z;XOnoH3;d3+Ui}&)%Nse46qtJYx!1w+nFKU$8WYiWf7<=;xb+Ekd@`Q@k9E-9!Q?GirZC=s1x zhHKHE3JA|dX>ywJgkkpYJ^QX{`-Q*TSx~2LcK<38g*RRrFsqt>E5p@%es=nGZ+WrN zA^~+^5?#g+m4qcPm?_clY>*$MfRb5XD~?Ags5MlezSM-D)wwNhN%Kg_%+MBa#kAQk z0zcDfRw(RN>>0+NNM^fqG%C+6e(70DI=_$sGE1c9ZP1y4a0hENAPwd(+j$}G<jnHTXoP5@BHCAFU(ucoXtqn0b?6iI|u7W4Vweas(15NhIv2U918$Azl8pM z73!e@oxS|1tS;lk|JNp3$@jQ9*kx*fAq+$S_$py#f`(%K8`b!ukw`N9>up$8SxXf| z_x?pnbm%w6JXO}igV4FDy-I+y3sonO3eDlP5Gb~_v8YaQ?q@1qi5W0`C>-1!*$+*z z;1uIUG%Z7U)Kv`hbyOC&ZiUc-V};F=0F}!`gLTVi_R`L4ttfUbA*~ZbMVjd!bDTlG znLO5MVvY#ml9Pnci?V@@^m_qNEoQ=#YS5fE&g%xt>g4FvstLFQr%(Xt3P9nIZDJKvA2SG>>eXT|6-4mOno_T@nSU#^u`;jdAsmmyH|h(*S&X^s1I2UDR`I@W}_7&VNaT&;Lzj?^>D=1 zoVzkiKiwo_OI$IopW`qPHW>BrYW1c;N=KWyDhN-S`RG)-A3I@+g=%3>U)cF2eO@*B|BkHpAL3)Klt0|l5!S{lUn zvQNUyZxGi_47JVT`JD;XsUvVAHFs#JisP(po#E?8nUmr&q0Xb?yVTsvf=w(R6;P>r zraDyq^@Eb7`6sRqToP9v1p*J*`oWjQF61sXI*CYQ%p{}BW(~CHDO+Pjl~<*=$6+$3(@X9BTyQ+S;t-QRUK4;fPHk+f<0HcFeC|Ha09rsCYjvJ{KR zq4}R-2*YuW%VaLw<)yPX0Z>MJY+%EJ2o~xx14TNq%v=|v zQ#hkvV8^rT+RqN3W(^z_BgzA|krWnn@zVPMEdJ@gi4 zA)QcC8_08`yE3mk3MUw>6_K5Y1+-%dwF)Go#ie<8piJc1w_f;Qy5y^pQ z8S-1!)nXkA&eK4r5Wp>|z3{#qM8M1C)Qh|Y@5F!PcI++v__h+W+KcZ_{CZXVI{b6T z`h%J`sB^~2?F&h+&xx6n)wn1kpPx+sN@dWCNzm$p1SuzDviR}%Y3TA8_%I?4o`g=^ z*LB6bxu0FS%vN-Q`UFe)swdk973}Sp;1{Y}DM1IsDZI*z`BgiYOe<&B|HS4A)fDm5 zBBwqz%{lb@xzo4|vV9ZzcWz_F>i;nJmQitSLAYS&^Q&^Ja#{VzFAz*=O(CwX44Rs*2@*UPiWTpL=+L zhMj28yDsCsogX*eu1_RzuykETL~m)H#M37Y7TGsv2TdvDm=r5uWqmPX69gJ9*jI-R zesYMXr{H!%!iZHi;}eKPlF4xMl7Of~U(U2E9YJ*+Pv^+eif=l!rmtQ{85p+D3rH}fQ*DF@d;&~{wW^)hKzSP*Zau)>EQ z>`5vGX)#S6k13-oYk#?Jqv3>S2#nz`N(ALx{yh3jAE_~t7KkVX3n{b2vdA$GCN3(X z?VfEVqjvax>XTCZGQW5F2I~_%{tetCkKA&E$ARnv^@;ZZq7)bDO6iLb<|6$0&O$~Se9e;P#w zfKeW5xmsjc%FMAqce9o@gbnyk^RwxXj+*$)s3<^dIKZ#H-#=)x+``9UY9t!P4@Ry% z>2z=Glx<&@s?uIf931@qVdkkrj)Cg3eq>9Db4Zw6Bc+zYp7I;k($_qbSYX79pBuJ8 z>%{5?k`M*IuK8VW?m;jk`VYw z6Mk_JTJ%{^)Xo~T=G2Z=j_*AZleWlsFnXho(bVaIPG31y|0wj!e#%KrfUvd1CGs_+ zwUeskEvqidY7H3GrDnDZgdT#DM1UzK6jP>~&lEw1w)N(0MdAr(fV7ud?guu4LVwxp zfqljXK}*>{PAY%XI|yw%3BNc=yfkAS%)Z)lz+}>^6|m&Ke%@+e#F;RpDk;8i@Cpdy zOJoo6W>vb9bPN;aYuiK)Bg4+0d}cDVF?Ci`lfsoczb`mvY=A+I;a+EC{>|z_dccOL z;`Z~uGaf3&m;4EfdE#kfphS#gTV8gnS_j)^G-Q&D`@X3;sZ-y)PH$UEU*4RwdPDkBWQW{!|0<$ zjIsFG>EQ%H%KSDR>W9EGe_?(jB&X*-PxqI^RIxm8=Y=`RNr2qz6C>(jbd$^+g`%9g zBg<(4_=PGpAv5X;AsVQUZkv01KX&JBC{$~x%yKta$*W5AZnRz|Oc}jf*tkF&XNNp0 zHeG*2!6DE0+0J%1|3&tuE$O`*SjE}dLgVe(%E|7>&eJ!_(`f5z!$$mi0L0Z&0;GP{ znaERgk3V#6^r@>b|3Le_64E>!TfJ?nF1Q%E@K5TsipTVe#Hcjd{J`WCT%AKvrc#89 z&cO}8tfj(?bm8I#tjBR)RqEfzX-y5wTxP;n9cgV!mApH+{gPRmG#&GE8usqb2qN(? zxpCaK>nmQ)=!BFGY_5EY+ZSF5uc+v-9GRJpRY8h(qGKYuJI>od{^w$MLdVokDiBF% zw3$xZ(y{-OOtp4)AglMVX#l6{%sEn+{{0ujJ)QfHm}ggc3213Bz!G;Wwo=SH|J*4) z%>*cl#YJRyslk;&x!dY#GE?>>nb#Kbfe7761-n&Jl;%bAoEYagsErL!I(0v1gI|25 z6edtIb5`fM?8Ps8Y(eY0FtPgMYF1KC^=iqo@|X z`RO76OrjOz0NSpUzig65_j!zqW&%@i*L`b#OMq*mVpMr$K_7xz_y{<~3ESwShkUh& zF$OH|s!ew2L&`;V|%b1oVUW`(J*+5=T$2TN=!BQ2K8PGvwfrRiTT;WKU+Hdav#Ln!*94Lh1GQcDi^g zP10nTP}sNJ)%_g^bBhRq>f70{uZx7ff(fR&+RG9mJEVfVDRH~5Z&=X=iTv`(A)rC= z@tJ9R9|Up9RwCK6m_B`FJ?wUIkJbFSyp8q@$WziN!u&5_z*lS;_~FQu5QaB+&ARJV zZXko4-lyf$o@`s|Cl_@GcsRLI(*a98fVEVr(Y35x-Iq%V1s{}|El=7rw0~~|(PuGZ zumA`ia`4RND6ejZJ?GJ}O{hSlFRzzyczLuJtNN_~`*qmI*GTc>2d6;5piOU)C=;EO zSF0mG?ike;;?-*fKx)7(Ul<1^68w9lR`pI17XywJ4LG`>o)1yE^g;r{=cbn*Rk_QO z?$@T7o1@(tE}9yPNV`k5xOAG9ubkv>4EsHoltdXFifUVCqzCV5se6yk466k1^%id( z?6&{W14F#`oi{77xX1b!%$W*+ev9P>7t*t4DZsXAj=&L()D*_T>dBux8<`0SF9{F8 ziP{+AW0?IW);?+#z6I<|vMSUn^DH?>6q+3wYj)G<#1Jb=d9&7LSX?#<} zw)*~4S*9GFpPm6il%5@c2-&Q-TE+}F9ouK~{5negj%{HAs2TU-aa*JWCl-~*G29EU zd29VBu4O6;s4=)9|a-9e)L^C9HY=vV2{1W6ly8eP8{KW$O+0Kl3lZLFlqu?aKGo^dMT!z6=YxnJ12VQ8 zZ<49;UIcS)z0fQlND-ObKLKA)9zXT?{`0AUA7q+I83To|KaotD;MnJXYL7iIGRcAd z7fbv7;RpCH{?bGKykPXdiB|r97W?}P^8Xpz1FXNSj=wMbpAJJB`8R{;p#uD{Ccgai zp4Rc3zu{NYzhPemNx(|+dmT1_dzz}Bi5hbuef-PA0!A45_-`@nRV%(Sy!c$8MVCb_ zcpS2{v-~%t%l?=0NR$3_`Z)~{oavsm>m_Vs{quxuZhy!}Ujl31(B#}Z^_sQ)PosGH zhcl$W7bb)%hQ9LZ)BI-B|Mq<~kiOq{>EquNF<@-We_g-7H~N1lvi+Z|#03sCj{YV^ zJupf){V$4(-$C+wYjp6oU3K_5w&+{)$0tEuNX5{zI@*fW7rFWi@R6lg4w}Z1!k*`c zkAB+M1sad-jb`y}!QK|(R2L}dwr<(`lks%G#_BDY>waX)kj{1kAZOK$Xd0t@cu|mh zS%)Y!(8iimNxJ! zrTyT)z4e z?H7;D*XA#=PS0iK{+|d3o&EHhCqXigf@He$^NtbMXEa`yvs$Xm-kW$uxL=jysP6pz z1EcQ6#G)Oi`k+~4pc(7kGtA zDN1p9KXM2AG&k1?(-NHTmr^KO+&PdrcZN@_2`rU}TtvFmR|$&@TqkwV5|d3M3L9by z8$fz;3t@L&V;KTUdZu1n%kGan{o=5j$UAaozy#-qFGZ)1DW~Ukd{V}RzRFH4oA2MM zE$RqXo*1ptT16MOe5AU#u*~udzWUrGamhq8fW$F7AUEIL_|0#s{*CHC>E|~5vh$~e`-8^7h12ScEHJ?t*Em<3>*169!f@k{l+c5sawbc#!fUgs zC7e=hxy^|*Uhvh0C{tV8LWNR*60Ito_rvM#l*vb8Z?CS$YeP-7<8r(o-l(XAb@wc* z_7wh$S6PT1)+29ka7+`gTb?8#*f6WE-q`pWi-TjbVD!fu59TJ1JImu%`U8Tjq*xxv z6ESp@X?0rEgF_n!O-~QS6hToT8Ws`e+`>}E?aNp(+CnSOz1yIh_FM2qi-?C2bUwaM zT2P8&e4?&_E11w6R)Smwgpl=Vq@R5l?*5sy?T7oBIHl{M|PxoR%#E8;``iZ{}y3!=3%fhNagB9^NQCSw9bo;E#@PZc2%g z4vQE9o)*d9%GQ42e)Wc^5A)IZ_`>PPOw8<39X%Npm&?|{TC2;G9^-%N8zO!?MduDN#X-|O-FYoy6OWS#->gyO{wA4unS==RRd`iY^OtYAk}0;6Ra40 z&Bt$J>KpXo=iFWU1YW;wIC_7^O-YqYBoivTHeTI4rkax#@yxsRJO|RoB`bn?&@iP- z@SWnwyy;2J=14Hlwn4G=^9D=GDP{W!0hi+e`#asJfNlPz>CKeTFIv3CAzD)twj(op zKru)T_S<#Bnf{w?B}JjB`5N8aJM9mZ=>@o1%s#Z_LNkhWhf=wEnSjnI& z-om~gHu|M0hb=)(3>Px~cmDa9NK!m0<%^3gD+iV33d)Uk^3m54=D8uE2FJ&cYTb*) z@Zg5EF074;h_M6_`~$&B35+!cdiIY{dn=a;MZMK^rHHf- z``r^OE#F1VeRYOxt@usXk?c?Kv&dj}tL1w`Lz!KMBDeGpmu>IVoH#UBhyP(&yP0kW zX2S+Atk-5_Hf_bmigDti&L?T|oTl!ADA6mgw6@QJkr(beTMSXl4VI(6czmU#GWd{c z%_5?1!fIurjDNly;RWu;WHr~(FlJr6s6SSWL2-dJwG{1{xkC?I(Q#^LK-{}GIV6PZ zEIEFp*-9?%XNjRL<@L`^kF!eCh%k;nA5Tz2f3SzT#fuPP;O_!C?z=n@FmTMtRf)7r zpwKyWhvwm#q}G>H7jqZ&Bl)&O8$8)04TO!pZE4T->(5XoP>>wcwLvMu7UE)h{9VBL zr@Fc{bQiw&{H3`bux0j8(Y&b2p(McJQqx5G$5PEZ=W~8>w4pPo2QTt;2<4PI?~$^$ z^NLN~H;UNgtPay$fQd++7Mc3t6}qIYX~2g_fenj{x!bX15x)!On(^1@l3K21rD*_p z=%DLqkXbEhGGyo7oA)m$XDBcrY<<3X50?a6STFD8l>d z2d`i=*I_qA8E7)vUundDw7yaFS`$x(!xIrYP}KI6gS?L-(`n<%W)T<(h>HP^&>pPa+6P$7^jHu?#9ifc>#Ax20!MVvP`kLwO*WR?8{ z^9XSV10a>z8RgEU&7??l`#KQ4By6d9l$NnD##kV;)?HtG%h}JTENiEgU4QuDfpew+(?mfDj{l zX42NNCyMoz&;Q2DZm0$a#nLrcM&gzT`uw5pPJa4k@!P6`D?$_~>O!v*N4Te;NLlZc zm#x?)^Hn01*exRq*EcC9BADZ6=ny+ON2+H{_Ht&`$>UB@v=gj$^8?ne91bzmD}#oK z)64p5I9m)J)GX(lAC1s3d=OWAGIBPQdin-oC>Su%sXRM*$1@l!XR4p>3b03Q8xpS^ zyf2+e(fWi7nuc1bb8?p-I_-WAT*P=AE@hY}SqX;;O_%%SwA#yfU8v>Xwgo<8b%Gvu zc8HJzjGQr%DEP_5LdSHytCroEbdkd;410$6j*FjRQXg&{zcn7WaPe|c-DKQvy-OlL zE>~Y5RhegQ>Ms|7ik5NHN57qVjZG8pVV6KX`XhF2`msp@FK{HI;^FHk?Y;7)ffN(lq;bvETdQwQ{3R#!KRL8lS@AyQ;W_L%Zb~VVp(P-2A%^HTH9A zJd!n1ifN|4Wo<23-(ar?!v!D{P{n7!9cDK*Z(G_SvB{@^iex1ERc-$E+26)M-xuQO zvAxUq^jc@ck)nK!!vjPN zYy{>V>B(;Iyc(V=w#Xknd4W9DJAm!;kAVzf7A_xQDc@Ag{7<_QfT5N=^_)6@63h8>vaQZmal)8P z2_9r$@h(3Kby8^KO#SFaur_*PVoXQSJ-y?2!;suicek}$J z@pjs-e@`L3uq&LeddJ{n`vxu2=kS})O}R@}xKEPG95Y;+C68<0RWK?N@6Y&H7!udd z8C}=rud|MK?jBVaxQPfyxw!!6*O{YJ`G-*83L^aUzU_nhjl`?zohk#wLFzowlUiU> zE{G5g3HlDjRgjNwwEMnWt@|>`cVdP_wNy`9`pqF9p%q&jKXQA%Og=U6Gi)<1u*`qyr+%~nvQz^uCqnAJ}aN>A{O356Ue#}WAt@Hc1 zNa1s2{N2(evxn?+58+Fl{*o|Uco)nzquuC=wj9r#U1@YU1D zcpGLn=RvtX1c@z$g~KF4r*&_TCugRtI&y}3Ds~k17BrX5xy~U&uAg<7BS)koBrD+X z*3m%BxzI+WNn9qkGJS!m+Q$(E%vu|*`COvzh*?8qPBAm+yQtvz|A5lD-A~g!9^%>A zFTml1-7po?J>hTjH8U{|!MDDCoYTr2RR@!suwz>3TY+Ij(7n)@7+uFrt!OMJ(eA`@ zDLl;8g?=h+&Afsp7{{5i^ZjUMi2scZu63M(YGcOE%nR8fbH*`C()?iKAzq~rJ*f;0-IA||(XWm{x&nbQc;yd=cj{k~!b7y~kF?h)S?BP!J z0+Fm+_6k>(XnFcpHs7QvU-5ML$(d_D9oEq3=>{Ui<#T*ThU^gsVV}>@xe1`TAA70> zi@R01TW|1sBU{A*=Go?6WOSb$DG5TCj6f-6|1;|qMxpee_{~y){zKhVPi7|$dMR|g zL#f78db?t*yW3 zzH~r5z$;eMYM-k5ZnZGAE1MD@G!|7GZt$TrtlLDV z8xf>z^<|&T6-gN4==TQ_+(rQoJ$ISw%z3HS8N|QIW=kg z9u)jIeKyS*(i@N=tPPjq4LEps1F`y2DRHB*N6clh&E)vOt&kA=)Ip|n^|fZQSAci? zX2p9JOy1^7?)^^!M%!sPDiamtx`9~FDX)a6ZFcbe^H~xL4E)0Q1{JTZhaS(nyo-4I zO|`~(Dam34dPAuY=Rki@H@rwWrM^aqt9oabNbiNhDTCg@PAWYr>=8gyFbk$x*I?Ku!4RIOCVEUqz9dE(9ALGlYa6ZQRzTe4)=w}Q86 zb$f*r0UtO;e6`7?Yq^?+OypymScnh;3s%Jw2WV z2cIEA9qI%YR!~a|TjUGgynX9|wxASereNaSb0ywuAT_%@K8wUj#%K11kHPhPGuNM- z*gfdJA^*z5iN08Mo4JUEnZOO17VSXL>vWE;piks`1xt$F>ZafuVYlm@>`5846C6lH zQh`#6Th+r53*THzj) zv-hJdi=3BYf$E$P_@`*HP|upp=&uY8VOCLtG63WL zFcai`wTAohUSkbyiyKzX^TP%N6pm^7YU`(~!Y1+`#!^4{;_RV>__DB&X?&dgi&LV- z07{C0ua}=|n!#j-;Id75ap5+f17n79Fd7g`Fa^0M-yzd`?@i8qaq`jr61&_IeXzoP z87}qspH6abb4!KvvjN(X6keyT^xcRgFtoZUx4f>Y0st^$>6a!8M(7xa@i0}}Pa|^* zhEvA2QIJ*DRfG-{ef%z4K6XJ7|zZcvX<(>1Aa%ym0$mX3;Y1zW&ST zVB{7;v#S#Fa))9*7Wd9f>x-ht=2y-1Hk{AjP~Nr=-ocQJ)22ian#&1=oHlmD*!iv6 zHiy*<;fEc#AIiDS7|g3UiVL+ip2-jnCt(LIe(J4h$BfN14y2gM1~5lfY12A=Fi-06 zt^AQG+7z)KoeF(UM%u+M1fTDmJLES6|IRk=$M7x-N>=r-Zj%7Huxi1ZB5!X?CY@%J zbz<)y)*KmvkgruP06a5dlTXLMB#?yGa6OA_2V@v%ibjRkYbMVatcCN>3+sDFj1tZ) zbJ*@WXT5vHDvIDA(y$o)I6^D?L_&tpFrbI6KK>~j<5BkMiN438e)~V~G4Yaj0`p)O zqS*K^0o=c-3=63KKc1|#=yShBAVOXf(x_SV$?wYxdp%Bv9su_2w0HKt~h~tvxsLDay}m@`mEe9BSm#(tYQ}G!`-L6FumeaCV$}= znpG%Vk8xUhN9QIRc0RPagPLDx* zY+S*@+7sB_sE^JUWrv7p+W6e}sdzRnf4XlVZ-^C~yhn*4ihp3a*$9}mmot-GFcpP< zj^p<}3dmj+eXrv42&Fc6kzbbDm1oH*O-2&~VgQ)jpRiHDO-x_UFg3_=mP+ABNpijE z(ML67E6X;d+fjg1+A`ZNr4W}QH-B>ULIaH$B|r42o;})ZN|(ZOs@=JeZS+yoe#g3a zSH_I2-Co_)M^7BMwf5@ziJV^1nJ5y{c*z3PU z=Ghzm-#sZEamrdQECcG*64Rk=hgjKd8|^^<=UR)5!sLsk&C>LXUG6k@hnu-ig#A)l zNp^NNY9Ya4)R;8`d37?u;&oPP;yGGhLtGRg+>~g+=JUdaU|r`kPj|Gjjj;H5S|yU5 z>G_rV{dhFuLn=Tiwcg#S(t=b&g3DWu#kS)U@NmtDh1kBTSEKsjCCBkNn{U&>P~MIR zxP#Qab)>y@g3lWCQ{g^_zS4T8jNU@`*f{BxoGaQ|+*T+(dR}_X)a<7K+=_CQs0bJT zGXAz8#~37iU}Ff9F$1if!`*KcKTgOF2cj~KlWyJyH#G}s=rHOz+sgM5zhCi-RO`@M zK9ExdHumTht{6I^%r<4W_$@2@?O`xnPSv6Lt6g1XVWx2q&a05-rMnQwfH9hFMwU%o z<+d%%4p06*lc5p-Nnp+j{jO+1Y$Qms6|n92(ZZp9-9ueuVxeXA_`V^R!wtn?tujc04{2bD^tQLBw% zN2i3Xi8lhZ5v>V;_cjXG>)osIHwmzo@tTz*9cx(c>af{A#hwJ-R!unV&^UXX!6}4e z5{Es=%PQYK+W1Z0!}61R(>PP%E&w{VkGlfSmV1thn+7fG1YtamePwS@yE2rf>?CEE zMLw>jZMGYcBi%M%{c7`mA@H+Zc}5$PeEi1C8{Os(QAMGdd|mWnWRrQnX&AGUXNfKd zs`a{QO9}Mc5 z$?I=;xiAz-!>=fcMWSWT<WKyPtMPqVZh&%U1fWID$`DYzX|CCNN(|A_`g>&N4P11pMGF)6|s!-=#kDjK9ZHnoJCvWMcav0*TtdftSg6kt7 z>}^YTtQH%_X!6X7c`ZX~cyK|;%#y9XTg8E9_B|jXffJzMbtQlqF;lm-_k^P#K)~+! z8Ft)P-*d^nB&eS-TUjdd=qPsRSz^@@6lAKG5%(rVgW<)SrHtPy%lAQTb$V}mu?ZDK zS&NtR;2z8@Wur@8)w+JR#^#?SbKGKYnQ1oW!-Ku=%(rtvNzV@Y7BpcdwXXI`Cv8&G zV=|BLgn0tVLQzZ0zgqd$=d9Lf++Q#QZi8XM%$)$!cK{^RinglPdJZcP1pNQa*H^yF z`T|H&a&*^f2+A2wh2n88v90EOYa<^M#=hl`x^TTb&-|D^nmSP7G!QlsHPB&n#Uz>e z2^iB&q|Iz3B3Sf%96rRTNb+9jT)0^omRXaKH~owxzdMPH3=hA~?)xQ<57-z8=u0l< zU%X`N&Fm-bT1r#`(LDS5)mgTXB^L1~;{64LH1l5$_OGwKbNU-g|9ur8IQ@SiCFuRH zOn~+Ol?i~4_t%B|bCoZR|7&h^@85X*-}|t!{M|r*Uip72yAG$R=C#p}EY5Y*FWC7du9D{kd> z|HZ7w@A0xJeR9VB_3w8Mx&)H<*~S*}^gQ&Nae~_x8A2|%R!0|w&D<|VA>Rh0+?lj% z4Av;xHr@ltMSy1>I8~c%=uy<#dUreY{H&}kiL)<;XLiy>WPU6Wh!vN|` zj2sB(q{I5K;3?(=r%a8dmFuv6jHnZ&(!NH!9QOsH)22@Q3T-xJ+NNs$sS#k+igy1V zol*L$%|D&M%|k(BM&SA~iKjp3lZ~saxZWZ#!zG%SCr#n2WgHc!6=C8#}P$O6G^oZOg}nHRT#Z#Q`^!KUUR&M9XAcEK#I?$jPx`9oK|k zu%)gZJ;K}`jkMT)_x(eu$b-^4Rw9UZ<0x2zQT8-u%%bW!J^im?eX08^i{ClAsSA0g zr;|3fM|8Zq0uyRuyOb@7`Z7d3`@4p0#pQIyQzn^FV2{MU)zaDRo$qv00U>FzO53t> ze%P`|u7LxZj0??ZDpqp303A`3tb(;2_?sJ~dC)A)7nbl%Llc;71aF7n?FedVPuz#h zUBaTHsoE?N&!Xm9Nj|x~Js!W!C||Kold8PBD)Q~Q)!@{<*u}k-(;69p#YN{DC5zGY za#R$~Js9;>6Mx^e&Se}Gzxj%P(WPWl%LJIKo@d6dVZGF#M*DIGM9k%W#Q+Sipe~>4 zm`KcV)Dkr&EKSJ0J!EjmqQ#y!Mh=RsbD{e{TM`Kp5(;9~a_njawcBKE2= zt0Xxr=LbK1b}UXyGbm*S{G(LD7a3ka4*uP^p{w!gx^+b^SO=}$)DaTJtO z!yn>i*Rj6cabV*By&Aw>#l)5pJ|89pZsL3TWhFx2Y3B>rn`U7V(dj(Ul34>b*K$3` z9BUZWs!hl#vP0aB^*$w?uC3=%Q@0pR1-w0PZ)n3KF-ck~b6}Rw)#kzVN2nvQ4(@fl zyeK!0<=%X9aLs5PR*|6J!mRn|554D$)nge+h6Iwr7?OG|Pg};p9IQCN_cX83m;Mm% zvtb16_P{Auaw7fDdj&h)c@zGv>83KbQ+s(~@#fAw(J?Eu&%~jVlUll1i62~D$Lym` zgSrB~el>sI^G!-xJ9sr@XloggbLM2<#bYsBVw01t; zr6KQNvXzHJcszGKvkw@V7{P;Vk%N8i@QKZ*?sl`bb=H#wpp=$R<;7JM)#9NiYm*$| zqgzwz35;#669oz#SD81WzU%7I>wq+r+II|rxiaTE`fREcUMsrCqPk*6M@ASnA0_oJ zqy`9fxWx|vgaj@3t!QheFtMAiy|3)pIXb|&N2z3PJLJ78aQb=tc=e$V}(M3Xb^$ zLX9`r<~8%d2D8guXT6KZ_GRTFuw~!Ud|oyx@PdC2O+a=@?IjRQ++xyANrP0^=M0q<0VUd2LCd-iG;4Z2{IPLk3uf3lJE>>r4lHm@5{qVu8o-E9fKGCrW*8iV_jsH5 zyl1Ywm$>|V*EJFyMY+UnS~Ef62}fZZ%@Q=_z(3FhKhKxho7x3*IKJ!c7n67u+=_NU z!ncM`Dydmm013A}YIfgGtAx0_A^SXvcHRRPn212=w{}qv`NQ@zs$Em%#P8Km?#BCp zv%MMIrxY3vq8k_^zC*SVY|%d|{@P(eb&d{RkKU zhHn{@h~HvYjJZM<1blqdncRS9?o0hv!NbLlZ*Hd- zz?1pTu7d)ld$?w2>$rJSBA+z1sLZ*T;kB$$qY-)3k4RJYVN}>x19O3!e!s$A44_83 zQ15cY%7FC?T$ro79rQGljnn+`Ra)j-0e~6;T$t$k*>6=)5rdfC)cW&3XO$!-yRo>U zc5%;&Pi=E;_k?YPT5qoo@|R6Z~$#h zatcdTUrQtTe?EtEM2;x(8NXqZjiY}2u-H-G{Bd9PJ-m($a5?NWN>zD{D#lm7COZ`D zZEdY?9`<_zNio(fx@j+Ev?OcVZN3-$D=uEH5#d6o<<$NGH|%!0Gr2W6c1-B>QM9){ zfSDu(KOsk8{j1~?<2J{b8tENmetn*TiHBz_f4_SiWYH9Wy`6ct*47YYrbb6uuU+o! zE~47Ol-sfPCO749&_u-EeP{Q`Iqxe+kY0gfB@>JDaAeQK;>^@eA?nN3UzH;sJkYZ( z`eT6NC(h+zr{X4VY$J&}6X|MbeE<$jns~xPu?0F=y}XP=pF8kFi7r5QUBJ;kFk(|p zUo4hV&0lm{)M#L5#VE=FLc{~~OLz0{mhu$2IOrKY5JHY#mp`Ms;om$uYO|R96eKFs zWyU3d=&#a)A|=X+AnnH-1#U8H7Z<7V87vFm8um}#kK3HYQ>#ZN8X1^L|N1a!JAOU{g&^Sg$}Mf zlh074+g66g9KQDrT?PCM`8{~xsQ%Y1#!o~l2HLtX>2^4MgI}Hxr<;bz@Q0BYtN=@r zQNNgp?OC?;Hl(0#^BeCXzZXPb&D%uWv*7H$XM41}ut0{E3U@mrLQ#%=4+g=xjmzpH zq`zxD&>O)(kG@}wy>Raz5>Z#x5Ey)@T~K-S#)u~c?t8NP$)`41$IO+>vOZ=#N6@f* zpJb)&toy$0#Cn%pc|43DUWG<+`Q+rO1mVW0))T)FpTAsyS}tEV_RC zbgb-j62zN0eQvYSCEtwwi0%gj>c)+P z@sQl&_`>Hy5txrKFbSA3BVv48O3RyMFM0SVY`)zCs$`isb*Fn{aP15jSCckv@0$lD zlQscgDGsi7%t-}rky@@CXVqDbkH!XG$Hg@_!R8tWQ9Kc!UR*CdA9))j(&lXlU3rmO zXV43&Tkeci225lLiNhBx6Kj*4Kq(WS14keA{4+mlk0xM+1FFA7WKwYpv%9GHsGCbb zaKzAAR5Z6ob4VB}#msa4<64(Keg07tD4AL?mL<#?T&S+AtP~*$aCX1O)V{9WodvNr zTQjq3K;Zclw0z|aFL8+_;>;Aakw|xDro+2U`X=Hq@vOC6BY(_;IqJB%d-LHKWyyE%d!V`d{ zml0qPy_xREwF6TrbmgI7@#WHN{o2w_l`B?hZ5Jl10O*dX>1ozC`nt+Isa?O_*9r`^ z=-(7@JxqeMgoT-@B8ZN;>YH>gx51?=ER{)M7$GD{{4S`bHAqS?m19m^24>&yZ2;LF=iPo341 zlU(Z?Ms%HW#f!NJqr$L&c3>0!lhBx}&NSrUwWXRc0xa#|;$k{VBPay{BK3BVti6LZZZoO7yBnZ>%S+2X^0_DhdYQj3 zx|@myh_@H!mP5c7f~Sy((Jjdy+R=7nDRZaT!9G^VU~R>{L1O?u?7C@4vkF<&a0JiZ zv#3>^BG@(xnS9%F1W8d1vwa=R1jT1^KIv2(Pf}P4L_gI#L!rW{Nxc9IzADG zwxN2lwOw>*8hXZuZYMB+M*%GCFiy*-K{B;GTzN_vBqUh2bvi5_J&djU<9sf+J8Rtg z8Y`Zbx+MQBBFrg%$R(PT z;5u>qlS5H)w9;##>qC}c<0n380_LedI>8)}x`vFojs`Z`MGU=Zbq%7$7C~T>sEM1n z=pTkX^h&d_T@qGT-DWib^T^1n2N-A?suN%msK>e6m&JtNIG+igX;vs@2rMhoG@Pei z_+Dwd_$((1T>_M3c&g&~Nf_3|gyItkQ$-kHMg>(ewQU{h@{H-X+gt@Na+Na&@y~N9 z-aILx$57O|%yXNaK`R_i5w*OMi)djxYyR< zW{mJV8CCvJm-o?EmvsWd3F~&=(gzHhigg zoj$&>r0Z+P+%5FZ&_~4dWWs|{bH8rkx+L78-WMN3(0K1w3h**6Mu+_9Ss9SSA`MzC z^gWElfR$}M2GI)(AO4IAAIV!b~O4w3^=`W>g#p7H4Nz|^(6Xz#|Mr+e*%;5oC+7F%8e)wA5M2n-ZK0&w)GEI?cRO;^#Q&Dn>L|GL^c(pZ2E2DsrpP{vmj z8essRWf4u7;@5jXYSq@?oD@EowrwGL?4P@Q;d}qL59psy{{I7DBkj#m@Z6OYXbD`- z7_mYjBKd9RJoo}4`IBaAB&3rdE7~lFjq}>3mMBj7_we62{rkuNjuaG;JI&toCe5hX zJh-nJMic(KYK=;N=c2I&%%M1W8$x+?fF_LlcdC5cRT6|JKC6 z707(YPx@xnMC!@(=c|r)e``TH$?Oyl<=Ff_t$|#czjE;BgAe}iVWE!GR6(i+M^rR_ zuJ4=v7fAgwEf&}Pa7glk3By?Y_xAYShvaAeTjuarf}fJW;b$Fl2kHDjv()asTBa7L z7Ak0l1OYCxDp$U>wRJ_Mu*=%V5gW+CF+;rF^|UIG04MbM_+Y>^`p_l0QttKG2H#N& zv0>syh6|Vgp7W&bsI3Dbx?RH_c^Ke#xBO`qc1H}Nx}SH*qI|Y?<_@*7F6tPKaod$_ zZVp1ZD9o8MlcMCkewfv!E<_(aaqf803SJE+;^mL_+|pndL+|h%($N{id~~wi(i3Jp zRRX;*7_eHGD8@U+aS$H%KF>Im8+Ew@`Ig_!JwaT#uxv&hOdccOTK|@4e9r$)dknzk zML`o1g4}=o&`$^0QG0v1*d**uGfTR_lCg1tH%w{AQfN0m^<5m{#*Gh2P3F*_;i8NF zYtPcXFn=!S^>Nm1j>KTVG9|gkgt~QkAacj}KB<{4pxyv~f_T(aRJ2!b+!0AUcT+@g z5L@HSdSBhWiPjHXx6{y|2hY=&au80KKd_+x(~l5DNz4((f)-q83C zNG#Agd;17+{bJk4D%GIbEnq^mhiy34!EH(-iIazhio@n8T>Uvvwrhm2{`Q76X=U{V z(Yop+?P#hzdN*(qY?)i4T+d7_-T;8x%aNKed8HA4U&+P!t1glLMcmN#5et$I*#ymj zdRONcle>48tdu}23w~4HD!%wp`WCWy7k5fd)1nDa9uZeE-nKGMLj_&li_Uvnbpnqj zTF|q68y(i%8Tg(Geg6a2)9c}3G~b$EPs29)mzROxa=y!%n91Nz2Dk&(>ataztIF7* zR^Om+Z55Mb2}Sbn*LtqF-|*Xfb(So)D|k^~$-PEgrr_IsS;Fx;$d2(xQRHr1{)uy^wwl{y4|_ z`6*a~aJ+cE8J1oUBADK1pDwrn?W0EW&*{&-iVWSFe6wANn>Q|1S7y*Z@Fbk@ zDL9?h^KD{LNs8AEKxu2Q#Z_}*ESQKX+h8qsq&8?bOh=bHyw1ED(%7G+#m`R%4gH%@ zLPd<91X9UbArna$5;+gw>;c(1b#ABr2;$*7w$T~DM)Q+&3PqUop z(4OVxj1)FY&wRZs6tU7e<^~mIT14fH;Mi%wMLRnWX`7!j+9O4i6Tj=PRAOWGwGmG5 zv*>+;@YJFps5rAX!H1ncaWYmk_U8qbX*$e%2fMe*iaCE(mm8Lw7w zqzF&06Ln?+ZimYjQ7Z0k?E+wW`UcfN=T|gT4FLaB(5`c>QuJ-|CAnGe8xvzTQ__mH_oAYVCMd1JH|6FH7+M^=A|6u>$6-R>NqU%A~Mp`GgE-MmeW@t z6?>?2G~#e=Lj=mSEY;QW1Nk=RP9=PeT527uKURPufv~Ge3&x-G#sO>vHRJn@cn95x zeW1ilsfo+rTvKsvNczEy3n$i9thZNl<;2Nb@)i>YcLWN>HpOggl;deDgKzH4t7p{Z z&o$SMdU8&sk2D>0_-=O5HZd{;Ozyy*+w=t$<;UUUHhY=LUQu`ErR-v6+7E9AXX}IA zd_!^{mnHk4Xa*4$b2=BtPUZ30j9L3iX&IB0=r;B_V2;QYTx1!S=w3>^6y!60FiKzh zHL=mW;`L;g@KwE())h?aA6wHwSFry{)bV8GZuQ;L?aPRxxB zju~K|L>8(jZ-t?Fd!%3bCNE8WShN#bh6m+~a>E{ap7ZYj6w1P4YldhH0s+{emZFIvBa0Z|+l{**STWAM5n$uuqj_8Tl55-gm>(P4WS zDUHPI3uf%QB^vMdswy-z*%>&5Rzh5u3IIW!t?MPcB9ysjl@mqJ?p=_v2E2%XL)@qY3vT!}M7yU@L zy-NCarG&EK%nOrl{Y$vr#U|?ong$VHKnw;ZzI1*4E1Dq=0I#LZH{|Khh@r@DsDVo# z>O9?*mfW{EeYd3hUQ8h2wZh-|&tOr~;mcjn<@uIJaDgr_w}a80Z>)(zd75PS*v1Jb zCIvDp#uoRxt~Fn@y6Uj19RbXS3 z?zx1(lUL@oD8~#pR_%rqX;1roM|~j{aoUV|d-!Jh1`Ws%Fm?XD3`cML{tGqq^Zw2^ zH4Zk2#!1q#9dfCIZ&!(28{lsO_FPAvS)o{)DWldXz&o+9so!G|kPw0G+hA{@0^wv+ zM)*pl+ahIYSxWBM)kJYtYI#!%3kkL`>42GrVvY(0omAK|6)|t$e6@7dM{=lQP-0?~ zG->aCh#<>m^U~~mcBX=yxEs@I%5MB*irBVUKPsaXh0Q@3duN0GnF6>kkphkB zDjd@X$PV$RFL+KmEu1|#H@1F4U9D+kkeH&lGw%aK16c942)7{hBuJ@db|etD_4ebnXiahE4}6vfmG0J)0F&~OWt*;ld8 z;r5ZA%qGenr(kE=vk1qJenFIJ2xY<31g}4mwIX z%ZzvAg?&hs8Iu0-_6boNdrIrsKMQrNgb5CE6shTiK87fF4$7V}VFZfe zZ$uzW%r@V7H$R&Mkx*QW))<^P_0kbF5m5YgQ9h+V`_I`j($GsJ#lu7#tN^H;j#eZf zEr?#@W8o;i5(i+6Z(`Qx@!uqUku6h@(&>+lfxT9Mkqm%#d$J^AAU6#eA6qZ2&ULzQ*`vD)~dcvzwmni4*wy)Q%!~LbZ_5e_dEmWc4 z#4$?tCZ*t|DPrT-NckPlsWtesaaHnclFNH~6AWsW52UAe-u+=?7XG{DcG=l!cB2AZ zX~EAk&(rJ0*EbNNw;m-QT~At;YzA;Lv%|CKTGd1c6*8cnA=B>EetYVjL&F<%#VZ){ zAp~{9tMXdTBmPP(5s|-+iCy<)5!1FpwY0vFy)grIOqWCDD5qu}Gg(Tw6k3qp%uJGjwTkhrvrMW%4PnFRgKCDhE=0RgD8la?iGkB}wuRS+- z`W)Jm*tvycl|3RFBq|JzbX{2Le_Lo4F{x5A5f=%8)AbMtYoOZqV zF_2tNG1SC*8VQ&*wHdh{+pYlAm>2_Ncb|3fOcl{4^YE-y=$;^QHD$BGousxz2i%m+ znx~mOuHiKwobEME$1tP9$X;&hkrQ#9VFGSy8$-tn4Q=0m=F$k1dRE_3%S}!p9VZ$d zx^Rzk_2V=mpA6Y#YMk6(bh8DkeTrulax>v12b)QmlgxvL6uFAuT5k(x)d$yFqr1w> zXTLUZGjk`V;sL%>lv=+nUyGne*0vpnG|-z)J#wMo)HwNcI-|4Pl@KoNM&6RoDOhp? z@|n((K4e34IKz#VNor%;7RRw2on^Hi2ti1+mC+R|JNMT^5dd2}-#k(QPqt_f0PB=2 zz*pk!uY(d2c6s{jR}1?Hgj?o(oppafCsBk?By2II?HP8dhEHWWQALiFoZb(O+YYwe z+_)H-pc~t7yR&tNgXq6bMTE^mFsL*@c}@&7PS|?~)}}L**Mkz|c2r4_j(={VouVy7 zK#oH@<$u@y7SgOF{~057#|y?LGFBFmNb>SJ?W}^bLdNc%$qRES;dle< z##--+mCu3VhR5i;wlmk8yJ$}C`+6`FQoh(oVK61hT482_i7Puc{yUpgDeF|XofDa0 za>McQ+8M7?uH=|Qk+%7ICFdWJY#4e$E_f{7rry@omy3kroE-^aleJC)ifnVBld%uV zL-594dL;iS=w{^zZ`HN;vskQA7+(dU*cuOrq_3YOa4*6s^Y-~Fz=rs1ZAuOoK zVqW`x%*dj4`&cOVJ$tf#UjfxDB8Cp@M_so28W9d1&edzq{Rf zw3Wbt>pZ0qwwyYy#M8g&pOL*Hj$ah8PPsQTbu}|fSTK42ROS{=Dx5O1(hn5B2t=RE z+7di92L0FcpZ8_gcmMVG-#_I3{r7+Mr~dqpzU=>>J`$;T{=0_yqelmiM~=uGSq7M^QM?>~+d{*BYNP7f6eb*#pbdDAh?1R{9 z{xM)>={U05`fH;@f`?sSTh7VAmit~!-8twLz(XOc8rM);4ApECx1Kq+)=_4kv~`4T zkISk7cb%v-G1J2JuX?dlYC|t74RzWR>R!j3{AJ>OW=nibvofId?sw}bZ~wP-rU5hU zWE;(bmdN3`=5N%vO{s3@|Dae~Spq-f=p+Q(6lCQ!KlU7v^@Kz?wyzY%;Gf^G8*0hglYCnKm$fb<64X_;Et9=GOjJnKB*rgxZ!O$?iUaz#DvQ8jU59 z!=j6WPk6YxDJ;l4J*outf^2plv68j-hK1I@1v^wm<1)KmoGKSh0BvgqzI^sml}*4i zummI`M~m3KKx*)@Ggek=pR{QGtSn&Q>#jsL))dg=mz)aNgWZc-rNeF`|Zq zl4Gf9fYY2VBOU!4g zLw8rU^!BW+nw$^hPg*;E8ms_K=SU>HaO^*{vb^KNsuV7c>xzI@0^9cJ=mdy8GdFof zy^<8HXiU)PoDCC3B9#k@8!IUe$Bz*eqa$xv-H}5iBK)PK47(U-jjVEzgNdK4d*Xk;#=|j6W~`d^eR|9{< z6XNXj1Bu2w{o!L2boOvg?Q3H`y1Of!{~M07#>85#3v$4${h_(U{<+%InC|9Kyjtr- zExxl;1t4AL4TUn)f0$mH+dbGJCQe_*#Ty@)jCMJCr)%OaDw{0mrPb5X8a{~wBvuEh zdK?u&C)L!^FkUJ~1nE}pN0o-3->T5eCZ~{R8eu9cm+8X0C2?t^f7qA9nwt6-;csag zv#KR>RBb&s-J8FCt6-3jxN=t<90WK-T4A|S3>G^FJJtI%A|^4V;iUUJdtX!WNwEo` zXQBq4PBA?aZ6)4JiVI#gkvY}by#{p zQXV7&+(N~K{sL~&Sv&(s?Hx-qyTStaq{xRfPN;!|%<<81Wd9(u1=1UxYa*y{3er&fsc2M=d=z;TW6osPcrcHEcS)i1x;1sVbVY!ZyPS;nrrYUi($$ViJZIcBUh zVUN1JN)90-e2Lc*E^!~(+kAbGom998xJYRLc_F7=p_-#y!^wT>?0Vvan#6tAjy!xu zrjFy(lk9TbLnpEID;V{pQDC+$%dB5so;}5((Qr(xa%^)#Lrsr2rbs!weNg5E{n_eN zJe2;&-IHf+I(+F%b7R;Xo{CE5)75gg#gNusmG@v(;G6)vV34_#=B1b0{;EGfy=iy< z6@q$#a&vRj&3vlWI%W}OuM{^k)~9SAM`sjC=v8|8Ll{&c1735dZkH| zyt`{kNqK#V!dqX5vuSF@NdU}bV7e!mK2DbxrlRRMLGzXsnClXSFo0-6{f6}}gOGq| zDkAO-#cp?OsJ^ZqGAN+!SYU5%|CWqtVW6$DaF5_Vm7%bRVAnDQ`)th66yU_eZhh=R zL{~O<=)3@4Zqt5J`aj(p34^dE<;XH{ky&1DqsPY+P+8cJAX)o5CP7$0lN1U{bIUhC z4F=C5mXE$B1<7>GQ+eG!A-Ns_Q7T&;U)>gtV&3cX-9NgxI1fdBVsGfr2W78hWcU|N z_UnGnZz!%}#nfHtn@M3KY_z@}irOLId@GZQ6mZ(=30l3?6+ zAMKT%i78{nRnTS?z)RtU%Ksi!a%YeA!oV7U^_f1z&&NBQUP0ToxTse&frFbydT0M4 z-H#eIUwP+C(D0ihghe}x9QYdEBn8Wm-MF0r&&uPpe^qpOEYUyePw!NeyXup zBIO*kjO>MP69(GkG^_kF3xMl%BZqLU)yM5V?iGB+XTzrIpvJ`psQ5$j^7Jfl^6alJ zRaM2F>&Jw|?e1_=S5tCV&Bkcj;|^?AFQ z&H=#TNg{0;nu4UVS=9jjSR4{w-O>ar7c0{P#~Bs0b@ZM7J&^Y_U+5RgERd(W#g2u+78Mt<#_q|x`q@0 zHt?ajO}XQ9E6~%fi3OowYZq5}s|f!u-@9o6>MoG^V9G4KFtKP_vu6e94CN&2rLFWM znf$4ckeZJlvzZ5JHwrs3MSpnR!7rD11m?7!lL2O!6E5QmwC4Z|iWR?WtZS-wG|bfE z!)^p*FSXv)_1shB?kh$^0*)?nq#4iBAeULM*Ss3ieBmZstmm3zep5M=$5h|E*}%-F ztbZa#6W7~$W@NJQ)5+2G_~^Vx*m=44N>EhVMbkn`E+nC>tmkh;5j}p1tNl0VX$m`s zLwH`qVZq_WaYiGsu}job5{2)*CS`m=Gzin(W8{u5w%7MgdPDN~*ZRA-gh+RqOJAC}Jp!L;FZYx{M_qLPuZ@Q*Ys;jb_ za@;q3wJA*diHRPXR1kV)wilu0~5x)6)e^gLZ z{1LF1FOQnB)fXuqwt3>0?VhxUUx2!Xx&;9tW=8@4pqMASI&=40ns5}QrV9S>_G@B( zPUbDN0Ms;Jt$?1XVS7n`dNsN@`eUo@;_}tYvc}4>nTa@&bBuVN$;lT21(OIZ zEw{cZ%6BB9VAUOLUF*;YDpoCU{)dD(w?m8lwD#m#+*(l;Zr z{iP5vHmJbm$0*I$X4+0~mVl(|TxGhBpFzRVhC!+XuZlyQPev$KK7O_;O7_$o?^un)A_Vy9$Vu^c=-0Ybn`LKR;EAJi2O3vc5t|6MY?@E-P?{xdI0!mGE z2(ZTizO*oVZ}V$4lPN&#na}1ncYVL>>h1N-Q>L3Kv3e6TFXZBBI}w|p%Nt|W_qSi; zuw}KVeR4Y9!2Y4--DpidP@9n=%kPyiC;=*6s@w4WVdA@K@5@NX>El9DQo`DT6PQ^k zL*u2}8}T(_v8=d~zbTz}4FEkU8X1p=Y&GW);iuVek8EeEi_iipEE)U7N?Tr0Q`mhi z?(em)5!?LNvKL2lBx4}SG7=j5A_PR+c3@h|-agq7W{dF#rI-&&A1+f* zp9=r=nYpwy)<+o2Br%aS*M6`4=^~@9;SbQ$X17wN_wR9e++t|_%EAzcgP&dFfuXfv zld8}hgA&FC^Z0;ju1_xJg%~Dby5}stcSx|3^%e6+}u zrhmuS^({sJ3qFv`_X=q_@ZA^p=zUvtswU8Vfkj41Z##Ln3mCDCg6ZDTb}~aBsQsLt zafLq%JSzy+wr+mCYlI075lLv>CS^?LIE06op(nYviHQYXd>0rFK64H(k`5P2LN2B) z0yQy)hjXvBK%`4Rw$IPMxcOH}886V$M$I_^Tg`~sv@#Rf=eBr)`T}s&cz6ZLjU?rr zy;|MUeasypwj?CGr0$tbIali!o>!MD#@1ds$`v_;xaqb#m^Fgf+>E~XXkB|e{W`4` zY-wUnQ`N#n4rfzY1eR7W92JwQf}iB8hpu(WiqKWA#p*CJ)_){p=eG5bw-$Gn2ZZ=m zC1?Z2Z`(&yjBZ`^_>D&;Q@Yzy@Pva*PkkVUYfbGj6Na_t_&K;ku1f&UGDaz&qFNkV zehjNd&X_oY35EaccIuZu|FFnDY3C%&?J9E_R!L-IkD2xp;gj$KPL#n|H?Y91ZYUm} z#4zgx$C#<9fk`fBXNR51E^OLhS9?dq#w<<15Wb5u_Z~xEu2X&8*Dl6J$1C&A1M~=? zIJmQ>g1aUzZuZXZ&dVf{86_2_`2{~ZORLB087D_yeJ9 zlNmfIzn4SFU*y0TCk#U$sp7O}Mau31SyT|~Q0y%sVtH4UuoaOa5RLq;eU$dkjC zA;_bvYCsjNV%h(E2ThN2i@fXlJM&Y1oK2F#hx>1MMBBAnHRCv8d(_s=Xb+M_A9eiS zG>*C?JSK{DX><(qYHsfK{xpiXo=cn{@doFG&gJj+m0PoXyrF35&&fV_PxI=MffRL3 zB$UhnO<^mg)BJ527vdf_WQYY(Ee3)oy3?|j+{=lk|kPK%j!P16B{O+3V^-wxzOf`5SzQ|{r01?K}+;&lP`E!!u<95?dj18w*ny* zB_q2mZ+sxg2u~8MVzPm`=1t#zxu+&4^YLu{4f!GmpRj}&1 zffrAiqNJsS%C%76sTFMw;ww3;>&e zFp+qJ4O%ijf3`|sZXZ$@vE<~!o}v?F(rT*7dH*wEKlOX-{9YV}t?vDHj0Ou98V35H z?sw@DbzOcd4;9}hcQogRx@W^RRiQUSFkW?SpQh^a6?mLgs*cW_b}|sQF?_S79v`1r za%!RL?UJKPog&Pk_@Sq>VfJaCI)8l~fYnV`Bp&F5(unM?9}RyKCs&Y97hxBiZ|`8~ zm`$Ex92)PC15Eq!%1RKVq4-A~tuP-KJu`eY*%r1S)MT2I?c!E7H#0M&D5s&Sapshi zrOwP&nkQOZ)TB_NEFs>7hK^Rn#glCO2$%qV*TH!O&_Uft>e{>_4rh;8yLZb-7GdJU z6D>$X)QSONS2^-BPM0pZ*=u8U0!XLjMXVy?Be}~Ov)Wi_Ke(}&4Ydy_%p6U}`15H3 zvOTKY0hnBpr34F$*4AGTr@QMq+pgYQPksSl;OaIrKGomfWq}2F`3GcWY%G)j#h0?z zJ2x_miHAu*!qt`?YzcrarROy{yF3kbjkR1{4Go_CvN8ZR6uNI^evWCQFlmY5R#oFC zAtBY-8HR=tDE^R)wQXtl3mxr65_c24aCFqn|Dh`#{#QXesOr{>%4!}1b4w!J4yBI9 zi7W=5O_#m5CX$ExAdLjOcW50PWAbHZM$J`TB03xYvY<-(@f90t?_<-eq@>(xqUj#c=Hx!??B&6rC)I)n_ z&w5v)YBoE6#B>2J#sKsD)t<`v`OBUQCTc9Os>c4!tx*yT7Fc_M2&~yhE-y^xb}o>@ z6OLQ1J1~kXD=QBV%tfkh7&f|(wnKuu`uXdi#vf_vxqUWMvwON;L z$SB=cR!s=G8R962nV29=7jBgT+Ul`b5SFwg05#dDjs|Lh^n-*yoU~jg&IE-kqVj8L zmjW28_ukRp)U2GW!!wh>tW16AipSj;ef6-z0SCZlK%s_lGs6z913&%=4ABHHw=A!* z0M5MY>xLpZQCVD6+|B333T#?-W_UsA`Du9@pyUBt7w~U0z(rDZq5W@ennRLciHY1p zBQrS#@B{Nrp4<9AyI8(xLQgP7K)^=N{$o3Uu)h!W&1za$XcYPAcAV@2JVH^962Kg8 zfQsIwnO;Bo1U8+_Zd9KvD8 zEdlEiasX1Gt)c>;%Tvs{te)7%zeM{5<$b=D9R>p9sxV!c4I~}2u=z3MG2)#<4a>eVV2{&gmi@zHmAiq24KrxBX^+{Cz=N82T`Pw0fAX5(E9o{pTNCL6$iwZ7{C z`E%;*7JDfHg2j}WpVe&f%l4%oAa2^0d|<#7_f6U9piZkoq`=d%57ig-5t-x^3Vk_` z9?2`DXax2?fqi+LI*JJtZFuR&glc@-aU|eiQE#*v7GdS;oqyBETj%uF$=OnLleT4? z&8=ptOUuroFLX@e4W8M3(Yf`A6xYmc;IR!4P%SC`|HeUrBGiG_9@RNLz#q9lQ8O{+ zt?!=exl|sNBTCHcJst0>{qPMU=rSapg%cnX-O8OR4j;bo->dU4z@$o~|MFxS-BD%h zW97*3jE-%-FiNcV#LL~*%)Ld8>$kK$9pU~Vq_(YL-NWjIb!t8e^ zSpNZ{V9U($++BUI(9xZjAE%1)I<;$EjzwZvQr+@3G#gauraLPfG(Gv>rdvLe0SO;& zF#|SNjcv4+uDAah}=l4+y8#)(Wl1efXuO%n)|^Y z$?6vp(BL$p1b#`IPFRR)hH&Cf0WeKWG5_1a z*K_orlGlC1b=k&w&H06cacrK{OaMZ*t`9rbqV1N_FVgSXbvxC7G3ehXJ+gfR%&2~J z(`l^NR?N?9BBfH>e6r>-Ua@h}r%G$nd1v1W(l&~fC~k=^)2@+8QgO8&3J>E{zYzvi zI>3RkhP(kr_-E!4MU{+D9#Lx|tfiQ#`CGl8qs-&3z^6M6)^OGa18^J~qgm-i$NfgO zYloWnum+@=fkmy`tv+M5BUO4M8lAblA!GgGPpJ>!6g}+*W=;lYugkteyu{YW%DYde zb5mX^CKbGU=bzRIYZl6Fz`uAndNF&_f8AiDspm<1%?CAdOmKYaeb5&VuM6Sd1=F13ywLLI1%_q656t#$W zx&;O%*ro^Q>dGma-IPXnoVVa5|2-9WccXrIFx~mFg%9*n{xWNB66W_xp-6OLlGTw5 zdTE)~An~(i{^yD^gJuek%X(01th=a$ZPxbifUTmVM&7r#hnzepd}L@H-wm|rgS~}( zt~N#k?CiF@{!beRuE12x3sAy;u6E~VEP$AgW9GVY>Z4$OWLRR7rJD|~$0WIRW;+8E zdnluB#`f+*i_*3S5Hg**S)66kVzx=|mN;>{mftRozfD&a{aWOkioVRQgO|Y6)~mF~ zh*s8T+9ZjmJp1+XkGuD~aM zPU@E@cgi}`Fh^hA)Qtu_8mI}A(KYuD#%WTPP zulqVo_^*t}4kII~9N73|V}rUS#a!O=dNWS&QHQa%Q&EQ%FZHDJen{r|5D#KA{>BKJ zDOFnNx2MaAxLg?N)V6oN=456Y=c8@m2}AW~Z^)TM$#Y>6zbGuCkv%Q*3aNv6&xLAv z1QOa!Ec~Cjm=F0q^JtKP+6XxgOZJ(Bi5f5QN~XaPi$Smj78%@X#=f9XH^b5nuP#l* zUdS$6Hv?gv_;CO{hso2WO?sL;MZ4f6Z?n;eL|P0E{`~8&0zZRkO0b!Chmx)bZCeT7 zMIow0h_)Z!uQ;W%GNWov=glyO2eGR^5p3Lpz>@JP*wlyDm@PA3PHFwSM)tmcI+Z%T zR$&~|4jQ~4QA$^i<+dER6$@E$K4uWxYg#|obDYZOJb9#D9m|@1?~zuhlCfvL$1+5? zqeM$Xz})oIR@AgG)8E3Ji-3~ckwfAu^qu7=XmVwYFqS1|WR3TLziX#8h+A9rUhgO= z*E-Ni&2E0#8xT+OCJfP30zBXE-#fELyk-A)y;vpQ^1iS?9IoO*OYL=DsE9#_g+*ca z{xf~b6tJ&X-!87Z9JX{iF`Pcv8)r4rbdu7D2!FmokR;1yVU1_Eq1ejW8~%@pnHbc> z$L=X11C<|bV}F2Lefxj|AP6ecXnE+wqN(bibID?C-AITJoS{l*JOj*V1cIFB1r#Cq z{rhH?C44rml0o)%!%pfxo{h5XdY~NlK^8I|x#Xom`jAt@{*L=2s079T-gDZ5^?8}L zMJQ*hNOv%VN9L|>7#4X)x2Tz5x1O1HjpKa>&cG>9f{sUakyptIR4eRiFP<&E~`OCUQ(3RZ1mZJ_%WD_n!iCJ_L$Ct<&rh*34}z$K|5K`xwRAho)ok8!`k(>vX?CrOq{D4v&diV+D73SoHcI!a;Qk z8z7xF+8W$hRk%T1N8xrDYP7r9x!{zxu>{yI`78%J+)2gJHsv5%n$G}d9=gmm_^bNc1 zNNvP{g?a6`;ie9`C0Sruli*AQ-b36WVo60Kr1%dK_vjPdKMx{v*j{Gw-6RP!BxY!Q zz1bN*wE<{4VWS66>$E=2>pTxR5s5*M=t=J|l|lS^f2NtyfCEm;pV_^h=2}F`4Oqrn{jC?bEJJEiYsAYmXf6aTF@< z2^6k_0#f%-wxFDbyanH%3s2r(x29hgrU&qaDAH`s`S)fuXjC#?^JiTX1#{zJd0|mr+0DMnAgFPR9n+F%qhx6ux-N9B|vS;}DqA&A@ZF$|C;|s}{ zMwPh{VW$3z9tjBU4+Yq=AzGIwnE_w2tUL3P3NstDva>8>5jed{S*qz_^t}v1+P_Q> zrQ3{Ip8qTRfVY-~7fHZiiwA`o=W(o~uX?*+GcuinmsR7UL0Yt^Os$u~t_s(Fa$xvo zqGp;X>G?zer^>>F4e6Nq%W4}s>vE8odOrBPq)s9hY}wsNmBsELb-`)NHYmw;YTyxw zWoGFb=1uXx?LE@}>tTc@-t|e`rmh6lUl9>n2pR2sY0GGu-h`g@IrM62e^)}$_?Xff zrU^=haoV-9I%K}V`_q~90L+p%MI!W;bQVb43fu=9T;IBq+}y5K;G@y+tIB|I*6f2c zPjK;Slv^l#s|9{RAvP`I$P}QJtu<2K!*Ic*dn|{H(1Oe7!Z&Gok~3ndrvkUHk2d(~ zg|CgA$~x&%R-c4GxGy+~#~h(f#Fn$lbvK5gdexmYmiV?p5>7_i?Z4KwE{#eYLXbLb zk1bvLOh!iqbf67577r$&eO?yr$=;Ju(y~?(i9dt34{U{2Jf2@mzCU){?n}pab=|-? zZTtbO@Wj+Bt{D#Xsn({iTGQf^-lV;jq;P+M$+PuJ3736g*U3IJN5Uhm>%sO9B#z_V z)>94VSY&N3#BHZ5&__e%Qy@r)a?GzsSAL`DW#^M<_*1iE@hm)w0<7nyZ z9l3-kflo0)GZi0BkQm9e=LL`tL!Inh5(VVKw&U}(9QSbzUZvtDMA-r^nvDWdPGVzJ z*-e30)Y>XK1CrWy4aYB$l?tOv!h#19jX{fd^$QK|(R1I~-CF;+tu3kn{eXak+_>4* ziRblrt!L_hQrHCsE{LY;uW`7aM>C|;^%~@O?Gf0WE_Nn8=Pxa9MDeW`RAlxAq+kIE z+hlrf+yu%#Uvb1VU(SpYYc;a*3895%G&}FOeQifwBlxhBec@hPOe>c(&DSMOzDAlO zIay2luP>scDT`{Y-7Zdn7?9@DVNnU=R6bY67zM|qjlGK-yet`W>cPugzAWYyAhIR5bM@*m-1 zTD$Q+qdsxWdE8VmSIHjpU$Sxs;F{YXse!x2%YJA_$}^V#MoF^dL2v+Q4*IrGUuvyMO0pCRGwhdy%w4|3C@Z*+e|*J z30tfwfL8FFg_>3%S{=lXLiSWnQ?g2p5<59yN1bSAWg@Hnyd zDg$i2<#Rc$@52jYA+H=i4ru_f!pqLx4haC6CM3O&%Bpno=&aZyg_C+40uipxS#-b1 z)i1ZdW@x&+4enUD020-`{?!)~1`MCOr%8g25PG`k#jAM74jq~MS9eIVGb>P^V=z~n zY>J}Uv#8V{5Wem zOo^dh;e+pw0u)?XQ~$9CJb*=IsdrB#uU@r)xXFEQs=2cHL&n{DqDLQj&)!Z|Q*7*H zC7UQ&=+=tpomV9%ec~Nd5eYHc6zF|zeO{y#T-nGEq`~$*XZ^#KZQRDWt6hq z;B@(!Q@QBPh=I#eLP4i~JaWA}7iUgZ!i53( zwlV`|S%_as^;5o6Gg^iZC%Y>+9Hh^&PeAC`O$!3+PAI7m6Op{eCoDq^C-BkKe=H`d zVKXB+atcsgA3*8uLs8tJG(8}6V!>7p)Fdql<2k>XK>v9D*G^v7I6c?So>9Y1(z)K| zxXC^R{wT)Fx!6zAuSLgHU879L9`~HopB`9?8E#kbMHf}iBFRFkw@5YVy?MnPms2+b ze<5CDzoE+G8BF+C7ukFOZsd=iN{>2w8U4gPIajfB3Z}&$h>V~y9y(O!c1=Y7Q!r8y zHNIq>(B!6cH+X^A*1w*b%Vx1l5W3*uZjl48gGAzcybGGlICIWs*Uhh+eT=6#TJpKL z@2D+Jj;_`Bp+31D8*G{~CFdbA$&GC?R{Oz=_bz3>VwFJGcwA8uxs~TCd@%mgYhZ$)WIjQQSMH=U z#FeEf#~NXbGG+eNm-vhn)Ey2P_le6ny~s|3u-u(-&s5%Lt(W~NG^2tJSpO;#7d!IJ z)dC&(=?%AvD<75&siww;15wvrJ8gwevz>ys`>##in8|aM`CfEXT(xZ{gxfHAFaY z=mlI#f?xcg(u{N;I34?vN>+)WpAa#Gaexcy}4At|4`j6=!XH3Rwc5G}EX3kmtYYuIY%r zGvjh8X<(3iIws=dZLE}DL%@_*%23q^S7Df)emSF2wWx!xp+Qq<7a3*_SRA$I<-bI{ z)wCD-Z@ns=!C%(RGjbb}gmN-`S7_dbivkBKkRuxqj%AA~Cvgy`Qu7Id3TicWF~tNr z=c|Mj1 z$SM0y$bIvnP#xE%_cwu{nQmG#<2gf%Pq=4@t-@R71#^@^3*k%uI~9g(uf6zJtNK#R zu~rzk-^J9p*aC;FAhY6K0V5>~JO)lc9suyF+XdWkR<){wr425-7+6B;(Cqpzahno) zj~J`i6X=xaaZn-7Lb8{Vwu1)LU}7}wr@kAF(~3-ylUa;J@|d}ggQN!KL~Ww2Kyi^> zq17R=MRy5w4QGKAc=kMHEKG>78eqBN-8Fqi`W)2$)jQK+@~s&Jq&LC3N$VOKyJ+(C z`0RX8y#WFwKr)!fK3@Oa1jLWOOJy#_4iX7q;@~r3L_`KkaWY`7pJ!T}oLcVOU-&b> znwWkFO5`MadFTw-jqz{3-GfsDaH!}&C5H4H^6gAZ?#XKQBoZPrJ`GL=70UR*ed4zq z>*E1e(PCEwRJcuj%%dGWm)#o()t+h(8zEekJ^b(L3YA$2E4~Ron9tEEg%7eD)5FA1 z9Nw=q2BLC`=<$W$=hm>-1?{}AiV@;d$$Wj^2xm~6-It?$KDOPUt}A_L=&$}Y)N#bT z4c;gyLCC=q{I^FG{CzZQ80@}0B;Ywwb3UJs!`}8=ktEVVtE+BWmS*`zU^6g$p&t{m z#ig(EGLD@RYVo^FejXl9odJ-!urx@Us`edpH^f-sA(+a!J73aVMLWs3A3Tr!bd8aAi0a+`Ry7(lZz5%}A?0QcL) zmFta#gR8>|5+i-YU-7J2)toDN?S>t=O31gZ^-#OKu%NUC;8D(N;Q0FhOdNm|0}vP% zY8?l74x7bn%LY&>B__1UVC5w#3M3m5l_6ETTQZn=W1`{9iQ&4#g4Lo8?Eb`KijoH* z38_D_QQfspBE&3dP&wLPd-t?FB(96ylsnUeItmcFjdR=cx z;wmj}gaxVyMsTi-Tmm>tR^d7TjYkgLKIVTt_A)7i;)+{}V$o`9%C{mW>aJ`y1+9;D z@9#?qQyRwPw1ALjbG(1b;MZL|oK^o*$k48EV;95S&k=0SGbS&eJY*u?6gf5`B6*T_ zqFJj)k}JW&QhQ*8?TvJo>`H1&_Zt#r&`9d!)DZPfxR!w!3@mmBHgNa91&f}A(mL+C zEo?tH9m|uuI|yf2motp`2;_e2*i^)T)gEHpvU+ui?S*4yZ>dgcS)tcu9t%G|U)gNM zU6I?OdckySykT!tl-vES6k(pj`Q^wCTRD)@GG=60;w+b{*nAxuD&O6(6oUEWOb3=< zy%PUF6hfQS&Tk1D_6@o?iz&@YEpY~;d8eZ!Dk;y|9k*}sX2KL; z{dS8Q?hQ(PTcZvurvQ=+_+!vCC8$HRkb_&eDX5g1FL^KT7Q>eOPRyBzm^~=LFr?e z%e29N5Y11I0Nkt~XQzdbIm*H{?Pt3qI@qB!58XxbYB^nLL}5R$xT9dgx4Mv2-|ROab=qHZ)^r1EL08Vf2ZC9+Y)n+ zKg9Y3VsUYw`Uljze;iDVQ*zy?7$#x2X%H0r@=DpeX?z`vtCnhe`Q3f_Azb&Odkv^j zioeTEbakFWm?UZEreQIXGtKwlG85yI)VSH}_usjy<_;rQVdXsa)eGTj(fwn|!eH{v zju%;xx(XalC3=m_x)q1TC1=DKo0T2BxHQrS>0%n3>}m?!Piq>k@m*DB7{h#t-OuP| zYN$rN&A6Vl?bYH1q_T{qu^7?B3&$X9rHoS!V}7^|Tk<1ly*LLKlAKQwT6@NnEg=MW z@GA!%AIR$KxD5KO#8l$$JqzcbF8k2Cu_GjNlFir9lY(?T+gM&_n-Jv%yEKaty`we$+ zS>vnzmWGeC^-1wR6-Z3X47)JLtRf3|22AWnbrkas;;dGh`?fwQ`(@VSBwHCku8iO? zn+)f>8JwC>T4?dj=tgw7?G|ddGLOI;=lP|iqeyjrPR=|hi>zxCtoZS|4rc>=F*yL= zi>tAt;kN)_6F zi((j~uWWl%-EAJdZIWSm;5Pn(hZk+lE#_X8Ct{}&ME>-&;cCFzaH?6(7H{bb?Tl-V zH9tCS!iTJG4Q)-&2CQAG&lr$#@dZ^{-8Wg&`{ck#*@YnenoWg$?bQ+GosslKOg&bM z?b$CkSnK18HUB%&_u3!4T7vC9^V@`P;3l2TsqnBaGd1vH=jLm8r-bnZ=Z;1R=15q@cnc75-Y z)X=m3HNx@x@+?oX;tDeg-_3aAy^3N3 zK!Vq@XImbqG*RqY-TgDB{b->D?%@rfb|kq7f+RpzeNAh$%L7HHlaOS z2dQSpFUs{^J@t`>gU3eN{-AYQCR+jQ)vzm7y65LVY7zO2JgT&1PUO&Ts`=PJ>+Zz| zNYSO>%B7}9*bj@=UEJq(!~t`Ap?yGI+{A6q`Fwg@@o>eL?<^iZ)ahP@g_o!5ua7V| z7rH|(P7ZbKzGUz?qb`2Pr?CUzu%Pu%d{+ri zIdsHlv1@8xm|c4qRH3Xp(#hWJurNtoJn!wyjB$K32YC2a!TXj?3va+3@%;G!ETWSr z@h!$U?&cOTKS&A``&sZeT|7Ml#C)GBnH{D+D)uo(iOSfapY6!+b;Rbf7TEE{ZMO%& zgk*zz^qRaARorM>aBOLVWb)&h*Y}zeAK2FR-Xrp%L_4d8_UCZ>6)fDqk}sYGvmUx7q^zIgBI>JgwKjraQ|&`T_+R5?%KV2^1$JJ6gj9*rmcO5SWh_eZ(a z8@lZTyd9!FQa3LvppIX(lBs z66kM`HW)0AgujCybjrV)rpbnOv)<3dt;x6Cw{~pY-*W?N-%mb1)?sP+Wb(;CLZq4e z%)1sc5i5#LYB-|D^+{;Juxi7TeWu9%V{$u+8W6{^UpN|{hsP>zcUo6iGl?yhI>uaw z)R9sFcO*tIUfof5yi@rUNFf*NB@ItHI_AR7cIC@TekX;d#u+bwKzbN25f2LpB_KSR zZI2|00>N5+QaCm2yX`#90kdkPS!b)Gu<>2Due}%m8a4-}1UY5by}2NI;ZnvRKtd)= zYQ7~_Ott9H!-FuHovH=9NoOUlowCh)unOh6Lxu22F5Fa025)~8?E6uV$i-Ld7jWsa z>p>X({jY4D09o0?3>7Ms)7Ul3QurM=ZC8v_=r0v(tX}z(jIx9H{$2F~;jh)q=vJ*%roiRj|IKkMlo{jy<=jfcV+IlYYvw0 zJZhI;Xs@p%k%LQ2Co@-~(N#+$?{CkcBU$7-L&}9{8Ntq!t4^Y4l!6)qD~f1nsyim| z=A2HAt%9OjOq!M%l(LHPMAVSY4yUr~)S6}I;enygBxTOtZT#|otr28TN4%tYsv~aE z;EXukxl|0NO;pk3i;IQ`@3Nc4f(@U=U&Y`9ARGz0^FdAlNgd*Sn~chQ1}U!FXXk0Hf0sEYza58sQuV zJ)15u#`JiH?SiKu2^}Jc;}joanQw*`%+_}_{f>@(S|WgSTrz}=n1$4`c~{x2AjT;% zF{m_|%^ZRQAg6(+kmdxtyKigw#N<(706ucTQ(n`g#?xy(;F%ubKKrV!U~Aw3jbmx(a>`vuFP zi70CWwC2X|s1l8@NI^tEX2GG%1!V5O`}7qC(&|$NuB-U)%CcSLd7zZ2qJsH_d3?y2 z2Smdo@)HosygZQU)fiUV=nBWYI@5u4*X|cc6CIDL@^G|7r@k>R(P5tH12(7kUh^ku zxZ9YF@V_hn{!O`jvb8$w+o$)7;%^y0cC1ZMQP_%gyAoY%sP53e(rL3I5sth16?54= z@@0v3;aoJ!SF_+Ab_U(Mhf#g~7Rt4W!%$Yv59R)>GS4?02OibDuNBswJCdAsHTuqO z>*nbd>A?G~Z1PD&7Y9LW`-HqEQM~&9NTXr5#4hv?P;6Q!^Z5P4~ z^o*RI9$@j`%D}%5XaR)lVW)4i^Y^tmkB4wwHgCbYC~e9c#N-dXp-}1oX2~L$QE=AL zu*|LBxcmE1CXd&riNT|A39ko@-%>mu?0DZ^^p(#{rFOXWxesh<}l?dR@KiU+>( zyZkSS{C=IQIB=M+cfs=*vxDJ#=Z5Z$3g&;9+rq0lNFJ%-Il%T(wZr%Fz^1WP_W38Np#Sr{9#{4!-#7GZGSG3Ekw;~tJ z;sgJrxBT9P-bqG;<+DJ@$TKDj+5`<3G2VDbT@k&BTn_y_a90>4RwRH$t29_v!X}!w zRo1199#SB)ECm>U^Il^j=lX4!@9>ko;~%4r%5#qIjeoTzY_G5Joo_Yjn5rZEoIVL? z|9NUMfiN}=R{_~v0>!|`m(gK(OB~G2V`r==u8tQ9RMH)E2B+1ERy@3$QKcSRxNvwn zFy}EAF})8jjxdHG*SpG=`|1yU|0Ie2UNk8OsPJQ6Ms=)~DlD=j_1=;4sQ-FZ;!{yE z2nC(5yd?-`m>)-$LlXSQMpmP}^&h(+NXX)=e7n`<3W)jGo{@Vz*r9tpouo(|9-RyN zN}49S9VMyW%{ONM%cMGa&CAM@Y}mrm4Vd&{d0)oFd_YvZMv_fD$6mRv8EI}*@Ug6Q z(<1tIRPR{@w~q6gJv$`wlbQ-7kR>>+JGqr2II)UMlC0CGyVh`$CzHbc|LyidlBH^% zl&}83Z1?iQ#0)46pBVfBPUv8y6F1cNieen>%mngcw6P0_qRg)Wg4#KzODTJbsawO> zFd~Ji{<(~#)wb^5WBz(6XEXiMv3nMtL%M+2EK~Kbuw}Xd?DKWK7BD^#X!`r0Vu z$;~|}{&nlh7cj4?`4#ZWy&ryYba=otCA|lh$Y6H+{gA%FKZ1MuiIUdC53ZQ_$gw3S zL}Ah=IAt)p`YGv?;Ia!5LeHB#S{DRoDJ9HITRg7c@&g_kSu~oc9I5$LGX74M*f<3w zz|x5FdHS4bck}RwuawR^>aBCGJjQukNWYDA>)$NvIxbUqk#N;EF9Hrq^y=pBV?*49 z1lmZrR0$=FLsD{gYja3Fp?F5KdUq`1J}luuk?|D!W;bwgKec}J4+LNf@JU&3Zsk-5 z$yl52dQ9Qj*gpYd$IwU}Nk50tsQ2H-wkx_07hQo~8;?%LC7g|o41V6xLCMiWCd`!d z(7bZ6&?fqU_}i2M!r6p*IiC$1ZVq38U7afVQ>8?6!jR_HI(Bn!nR*Nu#2s18%!hth zEt^Wjg0H`^85BOeJe#ujDA+&OI+53w`Mxre6HnWca>UNODqmT7%*l3c-s7PrDq>|! zaEOKH{jx5f*3I8>{|)XbK2f0F5U2nH!JvjfZ;NG7YcqMdw3QW)81@y2+xXF3^v6#x zAAgT5Yw+KD{H5bG^!%+W{lbncM5y1ZVjL6OU#&tF+2uCtnp6pj8f#ka00@zGxaqIQXozW7Sz_kWG1C+qVC{PE&DTKtO)9OJ-l^$S3+gyfP>)RL z1kzG~YQWxoED#wl?oywhzQUG$Abg7($XJQOuuW0BoSvLMeW!A7iymmuf-Wj61M>lx zV#7bC&s zBapQ;GlriU28PTn99&7%<(*>Q4<=CFl$Mef#|;Br zP^{-c`~SD5Hb1q7jIx=oHsgDzGOg~V_0SeLfVu*c=TU(8=?Ce52Wnkfqqz6LIn!p2xU zN@wF&-ZYdio|gP5x^b(7aDC3(n`onbipDkmBTc(zRD8{R4=L$<)XsO^eL^?{GgmAC zhZp&g54E#d3_Z#}HCK;J4$8_Xd=Ywp$V2n2{(Mu{x&1yt?A1Y4Tx2jnn^VW9t|E7T z8d8BGDCQgN88@!g+l_OXajYK}X#q8PKye;-e!h8qKrHwHy;ESlKs4-=oa6D=h`5>QoLi}xW&=lLRtO3xB)%6hb#S0} z-_;eW;OFM>fo|3%kW>p$Ape=e)-#K?K0ZLV9}!vEp!g}Ex5w$ZTek)alTGi*1@6Ao zwzv0h6N$JTvBI}y6LRj{>|!C%r$Rw%VQnP|YlBoTaCUTFT3(x5WE62B0m9(Q;}U^| zIPq1WFG@u3B7i#i`c{RiP^oK^7rfZ#GZ!f+S7d=f^+udGM&OOhp(~eX`7KNQom=nU zKU7t<^E}1qBU+ihw$^JmMF;J@_@I7MmYfyuYd2yiBCGb#oz%6tznK4e^TEw{vr@c$V|8P#m@TO=zs@vd;TO6U$P-5l-k(h4JFl;MMU)^ zWEM@`s?b!}?mfOxq$QQfQazzB`(qrc&u?SuJ*b>4TRGEKaE|(mOiw>lAe~t~g~8+% z)HWM7x*GV@%eNIoUL}7Lc5U{uZ!@H>XY)pEe>raC=+VTjN?gCrU|%B|b3^X(#j}$} zMzHN7f|{Bd8b-{FEYf1Uo;);hjO1T;37nkNxfXW(WRWdxfU|g1a;io92r0`?o^1)$ z;*ecoV>R_1FD%KJ(6HBzBsUaH>zWMCM<*??8I(A?LB@#nhO#Ec6&{{Fn+z4st6DSjVWg=1t>iigoiNrG}oAP1T_6&ECEs*I5o<@eFy- z-JhojKYpOnAxQ#eK^4bghYU~EH02vM*P|B-_tIVaEWmS}-7+2@?=xR4ZFZ@IIed@0 zWoy(lUy5Gu(F88mxNNG%jdhkysTHmPBF~rkczXrZzz9D+5|iq&g#`t{k8`f6fY_m) zX@v86_kVs5yRiYl7M3q^J+6O#-^(#kdt6E7YIsm0VuyU*udY{8SR5T@vB$WVcFiCy zB;7x)TSmhuDn@VhP_3&aPC(q(tTV6Y^qMzT3?F=v&Xqtl=0V8t6nwZLX`uFrytH%WtH1ZW2yOhqfg)?ovmVOgGTqxSL8LWH=pZFBscC4adB2931)86%9RN|&5n z&R?W&Z%Qf9;7h-s!F2b&nyzO{0mG_9enC;X5}btW#9pi>FV&yBw60@#N*=cDS0`$H znC4yKEZTKX<*zq{<^%pM#@>q~Y4GYfA-NCvj1w53ojDcOlj>?ys}bzEjxhc?&7eZNuPt6HO}HLpXsg;m5k3S z<&Wd$koU6$h&EB6)S=Gr){0c^nPB1jlh3blNbIjyuN1;Bsf-na2t1F8qS*mQqZaR> zFQ)2j)34G}NbS^nR(qB0pu@3h>ZA*1oUL!UHMS4|^5W%1>XED&X4|@6)_QJVxYTe+ z+M>rmamVJ5v`3s}Qk1!#*1 znxFy;HtoS@Vbr#}Fj($<@n$A@DxH%`DarIInd#r(A3xW~Fdcc>2n4B&LImfxg&miu z)|+H!U>r}=y#Cu^X#3(9Nil~?d`4*)$^cWr$HjHG`LpS&ZXCBZ%);KJ7_)A%Q*hz- z)eqc2XS#Nv`NDHWsmB)DG5n%>&P<7k)n-l3rTJq4%QKa*9YZTjclh;Wp(NYGZg+XM zYesLc(lVIbx!f22yjEngw7Vp-NIT-?!6x}GbmS2J(dnSga!X9Z_!`P1%)Y|;`w*1a$5Lifpq%v5am4l~9^rH%0MOZGXM zw!N9UapeWd0y8~r`BKcfp{1?K%UaWGbhO&yk^9!t_b{Vq0X20e>vy&G9WxQV!v?WY z5klVzDvJPdaj+iRAA7~uz#tDOFD>Dl=WT;jytMI+!aj9S*hsns*ygyxXue`4w_>}P zV}1C{W6{bAJv|P>;VM;P;q?l4@FQ(Mk(_4^6RBb9?Qs#EA(cfMjD`VTOOvDu9sS3h zilK1#<+ik?PvvgS7n7l*n~@nV2edUJqMjFM+ouGLYF`cS`rh86mPfeH!vFq@Oz?9V z9IkIZoTLsxF~yCw%LA!{!(gcHMu{`w$4`LV=HYS+iQ!wrTVY4>#Ua5n7(ha^ZPpZf zyz=%j&uWRiPq}HyD0pp(I_zXTwuI1gesOQiSiM^?y63R3k zPjeFY^6Y*nk6PVCeJ#@7v)QdYC_w2tHZnM%H4OWqY(E;`TUbl2@U9{V&l#04d_|o? z$p~WD4J;tfGm<5q#3r(ill0>(AmrnTK43sfHwxhsBOGw_B?p0dh%Tc$vv>8LT3iy4 z;y^h*2NbaFwwxw^`(B9lO?QvR+6BvT>0zS3!ETqwI8x{;Z=>IrBT9}6d$LJO&uRvG zH+LHM;_CiF>%(F&k#lqLTbFj}clQ$vz*UkKy@L0g>nEhd5QSVyw)4R}1z#*bt$WPo zGgaml941qD=_A#<3?{3}ODZ5vdlB@ln@D>HF_1>DuUUkRXjE^N7{C6^QD@un6vJMjd%*8#RAJ1^>z}#P zw3@2?T2Hg2M(#cP;fLL4p(ApNSMNO)t7AeM$≺n`M9e$|eOww!A_aypS&3ACk#+ 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

*^&Ju-?yeG`-=g>*-vpO5`|O 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

ykI;+C2i_clUh_{+bB;J7u=b7!03Ks6yS3GT7@7 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 R{-1wy-^|vu{?3yZ{|D!I&+Y&K literal 0 HcmV?d00001 diff --git a/packages/devtools-extension/screenshots/Timeline-Screen.png b/packages/devtools-extension/screenshots/Timeline-Screen.png new file mode 100644 index 0000000000000000000000000000000000000000..900257c193dea372ecbf4d16fe32d1c254a3da38 GIT binary patch literal 230135 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�Q8#2(^m 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 HdhmY$!yCCf literal 0 HcmV?d00001

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 z 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