-
Notifications
You must be signed in to change notification settings - Fork 3
Recognize callback #745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Recognize callback #745
Changes from all commits
7ff6a89
09613d4
3049a50
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Recognize Callback Test | Ping Identity JavaScript SDK</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="index-callback-test.ts"></script> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| import { | ||
| callbackType, | ||
| journey, | ||
| NameCallback, | ||
| PasswordCallback, | ||
| PingOneRecognizeCallback, | ||
| } from '@forgerock/journey-client'; | ||
| import { recognize } from '@forgerock/recognize'; | ||
| import './styles.css'; | ||
|
|
||
| const appEl = document.getElementById('app') as HTMLDivElement; | ||
| appEl.style.cssText = 'display:flex;gap:1.5rem;align-items:flex-start;'; | ||
|
|
||
| const leftEl = document.createElement('div'); | ||
| leftEl.style.cssText = 'flex:0 0 400px;min-width:400px;'; | ||
| appEl.appendChild(leftEl); | ||
|
|
||
| const rightEl = document.createElement('div'); | ||
| rightEl.style.cssText = 'flex:1;height:calc(100vh - 4rem);overflow-y:auto;'; | ||
| appEl.appendChild(rightEl); | ||
|
|
||
| console.log('[build] recognize-app loaded'); | ||
|
|
||
| function promptConfig(): Promise<{ wellknown: string; journeyName: string }> { | ||
| return new Promise((resolve) => { | ||
| const form = document.createElement('form'); | ||
| form.style.cssText = 'display:flex;flex-direction:column;gap:0.5rem;'; | ||
| form.innerHTML = ` | ||
| <label style="display:flex;flex-direction:column;gap:2px;font-size:0.85rem;">Well-known URL <input id="wellknown" type="text" style="width:100%;box-sizing:border-box;" /></label> | ||
| <label style="display:flex;flex-direction:column;gap:2px;font-size:0.85rem;">Journey name <input id="journeyName" type="text" /></label> | ||
| <button type="submit">Connect</button> | ||
| `; | ||
| leftEl.appendChild(form); | ||
| form.addEventListener('submit', (e) => { | ||
| e.preventDefault(); | ||
| const wellknown = (form.querySelector('#wellknown') as HTMLInputElement).value.trim(); | ||
| const journeyName = (form.querySelector('#journeyName') as HTMLInputElement).value.trim(); | ||
| form.remove(); | ||
| resolve({ wellknown, journeyName }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| function log(msg: string) { | ||
| console.log(msg); | ||
| const p = document.createElement('p'); | ||
| p.style.cssText = 'font-family:monospace;font-size:0.85rem;margin:2px 0;'; | ||
| if (msg.startsWith('[error]')) p.style.color = 'crimson'; | ||
| else if (msg.startsWith('[done]')) p.style.color = 'green'; | ||
| else if (msg.startsWith('[recognize]')) p.style.color = '#2563eb'; | ||
| else if (msg.startsWith('[step]')) p.style.color = '#7c3aed'; | ||
| p.textContent = msg; | ||
| rightEl.appendChild(p); | ||
| } | ||
|
|
||
| function promptCredentials(): Promise<{ username: string; password: string }> { | ||
| return new Promise((resolve) => { | ||
| const form = document.createElement('form'); | ||
| form.style.cssText = 'display:flex;flex-direction:column;gap:0.5rem;'; | ||
| form.innerHTML = ` | ||
| <label style="display:flex;flex-direction:column;gap:2px;font-size:0.85rem;">Username <input id="username" type="text" autocomplete="username" /></label> | ||
| <label style="display:flex;flex-direction:column;gap:2px;font-size:0.85rem;">Password <input id="password" type="password" autocomplete="current-password" /></label> | ||
| <button type="submit">Submit</button> | ||
| `; | ||
| leftEl.appendChild(form); | ||
| form.addEventListener('submit', (e) => { | ||
| e.preventDefault(); | ||
| const username = (form.querySelector('#username') as HTMLInputElement).value; | ||
| const password = (form.querySelector('#password') as HTMLInputElement).value; | ||
| form.remove(); | ||
| resolve({ username, password }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| (async () => { | ||
| const { wellknown, journeyName } = await promptConfig(); | ||
| log('[init] starting journey client...'); | ||
| let journeyClient; | ||
| try { | ||
| journeyClient = await journey({ config: { serverConfig: { wellknown } } }); | ||
| } catch (err) { | ||
| log(`[error] failed to init journey client: ${err}`); | ||
| return; | ||
| } | ||
|
|
||
| log('[init] starting journey...'); | ||
| let step; | ||
| try { | ||
| step = await journeyClient.start({ journey: journeyName }); | ||
| } catch (err) { | ||
| log(`[error] failed to start journey: ${err}`); | ||
| return; | ||
| } | ||
|
|
||
| while (step.type === 'Step') { | ||
| const recognizeCallback = step.callbacks.find( | ||
| (cb) => cb.getType() === callbackType.PingOneRecognizeCallback, | ||
| ) as PingOneRecognizeCallback | undefined; | ||
|
|
||
| if (recognizeCallback) { | ||
| log(`[step] got PingOneRecognizeCallback — op: ${recognizeCallback.getOperationType()}`); | ||
| log(`[config] ${JSON.stringify(recognizeCallback.getWebSDKConfig())}`); | ||
|
|
||
| const config = recognizeCallback.getWebSDKConfig(); | ||
| const operationType = recognizeCallback.getOperationType(); | ||
|
|
||
| const serviceURL = config.ws.url | ||
| .replace(/^wss:\/\//, 'https://') | ||
| .replace(/^ws:\/\//, 'http://'); | ||
|
|
||
| log(`[options] webSDKOptions from server: ${JSON.stringify(recognizeCallback.getOptions())}`); | ||
|
|
||
| const client = recognize({ | ||
| customer: recognizeCallback.getCustomerName(), | ||
| serviceURL, | ||
| ...(recognizeCallback.getTransactionData() | ||
| ? { transactionData: recognizeCallback.getTransactionData() } | ||
| : {}), | ||
| ...(recognizeCallback.getOptions() as Record<string, unknown>), | ||
| }); | ||
|
|
||
| await new Promise<void>((resolve, reject) => { | ||
| client.subscribe({ | ||
| next: (event) => { | ||
| log( | ||
| `[recognize] ${event.type}${'detail' in event ? ': ' + JSON.stringify(event.detail) : ''}`, | ||
| ); | ||
| }, | ||
| error: (err) => { | ||
| console.error( | ||
| '[recognize] raw error:', | ||
| err, | ||
| 'constructor:', | ||
| err?.constructor?.name, | ||
| 'instanceof RecognizeError:', | ||
| err instanceof Error, | ||
| ); | ||
| log( | ||
| `[recognize] error: ${JSON.stringify(err)} — code:${err.error.code} — msg:${err.error.message} — constructor:${err?.constructor?.name}`, | ||
| ); | ||
| recognizeCallback.setClientError(err.error.message); | ||
| recognizeCallback.setClientErrorCode(String(err.error.code)); | ||
| resolve(); | ||
| }, | ||
| complete: (data) => { | ||
| log(`[recognize] complete — data: ${JSON.stringify(data)}`); | ||
| if (data.jwt) { | ||
| recognizeCallback.setSignedJwt(data.jwt); | ||
| try { | ||
| const payload = JSON.parse(atob(data.jwt.split('.')[1])); | ||
| if (payload.sub) { | ||
| log(`[recognize] recognizeId from JWT sub: ${payload.sub}`); | ||
| recognizeCallback.setRecognizeId(payload.sub); | ||
|
Comment on lines
+151
to
+154
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find existing Base64url or JWT decoding helpers before adding another implementation.
rg -n -C 4 --glob '*.{ts,tsx}' 'base64url|base64Url|atob\(|TextDecoder|split\('\''\.'\''\)' packages e2eRepository: ForgeRock/ping-javascript-sdk Length of output: 7404 Decode the JWT payload as Base64url. Normalize 🤖 Prompt for AI Agents |
||
| } | ||
| } catch (e) { | ||
| log(`[recognize] could not parse JWT sub: ${e}`); | ||
| } | ||
| } | ||
| resolve(); | ||
| }, | ||
| }); | ||
|
|
||
| const container = document.createElement('div'); | ||
| leftEl.appendChild(container); | ||
|
|
||
| client | ||
| .init({ | ||
| mode: 'mount', | ||
| container, | ||
| type: operationType === 'ENROLL' ? 'enroll' : 'auth', | ||
| username: recognizeCallback.getUsername(), | ||
| }) | ||
| .then((err) => { | ||
| if (err) { | ||
| log(`[recognize] init error: ${err}`); | ||
| reject(err); | ||
| } | ||
| }) | ||
| .catch((err) => { | ||
| log(`[recognize] init threw: ${err}`); | ||
| console.error('[recognize] init threw:', err); | ||
| reject(err); | ||
| }); | ||
| }); | ||
|
|
||
| client.dispose(); | ||
|
Comment on lines
+174
to
+187
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In the repository version of Citations: 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- target file outline ---'
ast-grep outline e2e/recognize-app/src/index-callback-test.ts 2>/dev/null || true
printf '%s\n' '--- target lines ---'
sed -n '130,240p' e2e/recognize-app/src/index-callback-test.ts
printf '%s\n' '--- recognize imports and API usage ---'
rg -n -C 3 'recognize|client\.init|client\.dispose|journeyClient\.(next|fail)|callback|client-error' e2e/recognize-app/src package.json e2e/recognize-app 2>/dev/null
printf '%s\n' '--- package manifests and lockfile references ---'
rg -n -C 2 '"`@forgerock/recognize`"|`@forgerock/recognize`|ping-javascript-sdk' --glob 'package.json' --glob '*lock*' --glob '*.yaml' --glob '*.yml' .Repository: ForgeRock/ping-javascript-sdk Length of output: 42673 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- recognize package files ---'
git ls-files packages/recognize
printf '%s\n' '--- recognize package outline ---'
ast-grep outline packages/recognize 2>/dev/null || true
printf '%s\n' '--- init/dispose definitions and references ---'
rg -n -C 5 'class .*Recognize|function recognize|const recognize|init\s*[:=]|dispose\s*[:=]|dispose\(|RecognizeError|errorCode|client-error|setClientError' packages/recognize packages/journey-clientRepository: ForgeRock/ping-javascript-sdk Length of output: 50387 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- recognize implementation ---'
sed -n '34,185p' packages/recognize/src/lib/recognize.ts
printf '%s\n' '--- recognize types and error codes ---'
sed -n '32,145p' packages/recognize/src/lib/recognize.types.ts
cat packages/recognize/src/lib/defs/recognize-error-code.ts
printf '%s\n' '--- initialization-related tests ---'
sed -n '1,180p' packages/recognize/src/lib/recognize.test.tsRepository: ForgeRock/ping-javascript-sdk Length of output: 15708 🏁 Script executed: #!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
impl = Path("packages/recognize/src/lib/recognize.ts").read_text()
app = Path("e2e/recognize-app/src/index-callback-test.ts").read_text()
checks = {
"init_declares_error_union": "Promise<RecognizeError | void>" in impl,
"init_returns_import_error": "return createRecognizeError(RECOGNIZE_ERROR_CODE.SDK_WEB_ASSEMBLY_IMPORT_FAILED" in impl,
"init_returns_attach_error": "return createRecognizeError(RECOGNIZE_ERROR_CODE.SDK_ERROR" in impl
and "invalid element <${tag.toLowerCase()}" in impl,
"element_created_before_mount_append": impl.index("element = document.createElement(tag);")
< impl.index("options.container.appendChild(element);"),
"app_rejects_returned_init_error": ".then((err) =>" in app
and "if (err)" in app
and "reject(err);" in app,
"app_disposes_after_await_only": app.index("client.dispose();")
> app.index("await new Promise<void>"),
"app_advances_after_recognize_branch": "step = await journeyClient.next(step);" in app,
}
for name, result in checks.items():
print(f"{name}: {'PASS' if result else 'FAIL'}")
if not all(checks.values()):
raise SystemExit(1)
PYRepository: ForgeRock/ping-javascript-sdk Length of output: 421 Handle initialization errors without aborting the Journey flow.
🤖 Prompt for AI Agents |
||
| } else { | ||
| const hasName = step.callbacks.some((cb) => cb.getType() === callbackType.NameCallback); | ||
| const hasPassword = step.callbacks.some( | ||
| (cb) => cb.getType() === callbackType.PasswordCallback, | ||
| ); | ||
|
|
||
| if (hasName || hasPassword) { | ||
| log('[step] credentials required'); | ||
| const { username, password } = await promptCredentials(); | ||
|
|
||
| if (hasName) { | ||
| const cb = step.callbacks.find( | ||
| (cb) => cb.getType() === callbackType.NameCallback, | ||
| ) as NameCallback; | ||
| cb.setName(username); | ||
| } | ||
| if (hasPassword) { | ||
| const cb = step.callbacks.find( | ||
| (cb) => cb.getType() === callbackType.PasswordCallback, | ||
| ) as PasswordCallback; | ||
| cb.setPassword(password); | ||
| } | ||
| } else { | ||
| const types = step.callbacks.map((cb) => cb.getType()).join(', '); | ||
| log(`[step] unhandled callbacks: [${types}]`); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| step = await journeyClient.next(step); | ||
| } | ||
|
|
||
| if (step.type === 'LoginSuccess') { | ||
| log(`[done] Login successful — session: ${step.getSessionToken() ?? 'none'}`); | ||
| } else if (step.type === 'LoginFailure') { | ||
| log(`[done] Login failed — ${step.payload.message}`); | ||
| } | ||
| })(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /* | ||
| * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. | ||
| * | ||
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| */ | ||
|
|
||
| import type { Callback } from '@forgerock/sdk-types'; | ||
|
|
||
| import { BaseCallback } from './base-callback.js'; | ||
|
|
||
| export type PingOneRecognizeOperationType = 'ENROLL' | 'AUTHENTICATE'; | ||
|
|
||
| export interface PingOneRecognizeWebSDKConfig { | ||
| customer: { name: string }; | ||
| transaction: { data: string }; | ||
| username: string; | ||
| ws: { url: string }; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| /** | ||
| * @class - Represents a callback used to perform PingOne Recognize (Keyless) biometric operations. | ||
| */ | ||
| export class PingOneRecognizeCallback extends BaseCallback { | ||
| constructor(public override payload: Callback) { | ||
| super(payload); | ||
| } | ||
|
|
||
| public getOperationType(): PingOneRecognizeOperationType { | ||
| return this.getOutputByName<PingOneRecognizeOperationType>('operationType', 'AUTHENTICATE'); | ||
| } | ||
|
|
||
| public getServiceURL(): string { | ||
| return this.getOutputByName<string>('websocketURL', ''); | ||
| } | ||
|
|
||
| public getCustomerName(): string { | ||
| return this.getOutputByName<string>('customerName', ''); | ||
| } | ||
|
|
||
| public getUsername(): string { | ||
| return this.getOutputByName<string>('username', ''); | ||
| } | ||
|
|
||
| public getTransactionData(): string { | ||
| return this.getOutputByName<string>('transactionData', ''); | ||
| } | ||
|
|
||
| public getOptions(): Record<string, unknown> { | ||
| return this.getOutputByName<Record<string, unknown>>('webSDKOptions', {}); | ||
| } | ||
|
|
||
| public getWebSDKConfig(): PingOneRecognizeWebSDKConfig { | ||
| return { | ||
| customer: { name: this.getCustomerName() }, | ||
| transaction: { data: this.getTransactionData() }, | ||
| username: this.getUsername(), | ||
| ws: { url: this.getServiceURL() }, | ||
| ...this.getOptions(), | ||
| }; | ||
| } | ||
|
|
||
| public setSignedJwt(jwt: string): void { | ||
| this.setInputValue(jwt, 'IDToken1signedJwt'); | ||
| } | ||
|
|
||
| public setRecognizeId(recognizeId: string): void { | ||
| this.setInputValue(recognizeId, 'IDToken1recognizeId'); | ||
| } | ||
|
|
||
| public setClientError(errorMessage: string): void { | ||
| this.setInputValue(errorMessage, 'IDToken1clientError'); | ||
| } | ||
|
|
||
| public setClientErrorCode(errorCode: string): void { | ||
| this.setInputValue(errorCode, 'IDToken1clientErrorCode'); | ||
| } | ||
| } | ||
|
Comment on lines
+25
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift Use a factory-created callback implementation instead of a class. This new client-package implementation introduces As per coding guidelines: " 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ export const callbackType = { | |
| PasswordCallback: 'PasswordCallback', | ||
| PingOneProtectEvaluationCallback: 'PingOneProtectEvaluationCallback', | ||
| PingOneProtectInitializeCallback: 'PingOneProtectInitializeCallback', | ||
| PingOneRecognizeCallback: 'PingOneRecognizeCallback', | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift Move Line 23 adds runtime code to a As per coding guidelines: " 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| PollingWaitCallback: 'PollingWaitCallback', | ||
| ReCaptchaCallback: 'ReCaptchaCallback', | ||
| ReCaptchaEnterpriseCallback: 'ReCaptchaEnterpriseCallback', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Remove authentication data from logs.
The test writes callback configuration, Recognize result data, JWTs, and session tokens to the browser console and page. Log only non-sensitive status and error identifiers.
e2e/recognize-app/src/index-callback-test.ts#L147-L147: Do not logdata, because it can containjwt.e2e/recognize-app/src/index-callback-test.ts#L103-L103: Do not log the full Web SDK configuration, because it includes username and transaction data.e2e/recognize-app/src/index-callback-test.ts#L112-L112: Do not log unfilteredwebSDKOptions.e2e/recognize-app/src/index-callback-test.ts#L221-L221: Do not log the session token.📍 Affects 1 file
e2e/recognize-app/src/index-callback-test.ts#L147-L147(this comment)e2e/recognize-app/src/index-callback-test.ts#L103-L103e2e/recognize-app/src/index-callback-test.ts#L112-L112e2e/recognize-app/src/index-callback-test.ts#L221-L221🤖 Prompt for AI Agents