npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vizualkei/sophid-client-sdk

v0.50.1

Published

SophID Web Client SDK - Biometric authentication SDK for webapp integration

Readme

@vizualkei/sophid-client-sdk

Browser-side TypeScript SDK that bridges a web application to the SophID Mobile native app for biometric authentication. It supports two flows:

  • Phone browser — direct sophdplk:// deep-link launch into SophID Mobile.
  • Desktop browser — HTTPS QR-code fallback that the user scans with their phone.

After the operation, the SDK long-polls the SophID server for the signed Biometric Result Token (BRT) and returns it to the caller.

Installation

pnpm add @vizualkei/sophid-client-sdk

In Next.js, transpile the package because it ships ESM + TypeScript sources from a workspace dependency:

// next.config.js
module.exports = {
    transpilePackages: ['@vizualkei/sophid-client-sdk', '@vizualkei/common-utils'],
};

This package depends on @vizualkei/common-utils for logging and declares react@^18.3.1 as a peer dependency.

Public exports

From @vizualkei/sophid-client-sdk:

| Name | Kind | Description | | ---------------------------------------------------------------------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------- | | NeumaClientHelper | class | High-level helper that handles BST fetch, mobile flow, and BRT submission | | neumaClientHelper | object | Singleton wrapper around NeumaClientHelper | | NeumaClientHelperConfig | type | Init shape for the helper | | EnrollUserOptions | type | { email?: string \| null; phoneNo?: string \| null } | | SophIDMobileFactory | class (static create()) | Low-level mobile-bridge factory | | SophIDError | class | Operation error with discriminated code | | SophIDMobileInterface | type | Public interface of the low-level client | | SophIDMobileOptions | type | Init options for the low-level client | | UserDescriptor, EnrollmentResult, AuthenticationResult, KeyRetrievalResult, PackageVersion | types | Result shapes | | PendingOperationInfo | type | Metadata for an in-flight deep-link op persisted in sessionStorage (see "Resuming pending operations") | | HandlePendingOperationOptions, PendingOperationResult | types | High-level callback-based return-flow recovery shapes | | AppLaunchPlatform, AppLaunchFailureContext | types | Phone-browser launch-failure metadata | | QrCodeData, QrCodeCleanupFn | types | Desktop QR callback contract | | NEUMA_MOBILE_PLAY_STORE_URL, NEUMA_MOBILE_TESTFLIGHT_URL | constants | Direct install URLs for Neuma Mobile | | getNeumaMobileInstallUrl(platform) | function | Returns the install URL for a given AppLaunchPlatform (Google Play on Android, TestFlight on iOS) | | SOPHID_MOBILE_PLAY_STORE_URL, SOPHID_MOBILE_TESTFLIGHT_URL, getSophIDMobileInstallUrl(platform) | legacy aliases | Backward-compatible aliases for the Neuma install URLs |

There is also an installLinks subpath export for projects that only need the install URL helpers without pulling in the full SDK:

import {
    NEUMA_MOBILE_PLAY_STORE_URL,
    NEUMA_MOBILE_TESTFLIGHT_URL,
    getNeumaMobileInstallUrl,
} from '@vizualkei/sophid-client-sdk/installLinks';

Quick start (recommended)

import { neumaClientHelper } from '@vizualkei/sophid-client-sdk';

// Initialize once at app startup
neumaClientHelper.init({
    biometricSessionUrl: '/api/biometric-session',
    biometricResultUrl: '/api/biometric-results',
    fetcher: (input, init) => fetch(input, init),
    onQrCode: ({ deepLinkUrl, onCancel }) => showMyQrModal(deepLinkUrl, onCancel),
    onAppLaunchFailed: ({ installUrl, message }) => showInstallModal({ installUrl, message }),
});

// Use anywhere
const enrollBrt = await neumaClientHelper.enrollUser(
    { userName: 'Jane Roe', email: '[email protected]', phone: '+886900000000' },
    { email: '[email protected]', phoneNo: '+886900000000' }
);
const authBrt = await neumaClientHelper.authenticateUser();

neumaClientHelper.init(...) is idempotent: subsequent calls return the first instance.

NeumaClientHelper

NeumaClientHelper orchestrates the full BST-then-operation-then-BRT flow:

  1. POST to biometricSessionUrl to obtain a BST.
  2. Initialize a SophIDMobile client with that BST.
  3. Run the requested operation (deep-link or QR).
  4. POST the resulting BRT to biometricResultUrl (skipped by authenticateUserDirect()).

NeumaClientHelperConfig

| Field | Type | Required | Default | Description | | ---------------------- | ----------------------------------------------------------------------------------------- | -------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------- | | biometricSessionUrl | string | yes | — | Endpoint that returns { bst } (POST) | | biometricResultUrl | string | yes | — | Endpoint that receives the BRT (POST) | | fetcher | (input, init?) => Promise<Response> | yes | — | Custom fetch (inject auth headers here) | | biometricService | string | no | https://api.neuma.me:443 | SophID server as host, host:port, or https://host[:port]; passed through normalizeBiometricService | | onQrCode | SophIDMobileOptions['onQrCode'] | no | — | Desktop QR callback | | onAppLaunchFailed | SophIDMobileOptions['onAppLaunchFailed'] | no | — | Phone-browser deep-link failure callback | | sdkOptions | Omit<SophIDMobileOptions, 'biometricSessionToken' \| 'onQrCode' \| 'onAppLaunchFailed'> | no | Neuma app naming/install URLs | Extra options forwarded to SophIDMobile.initialize(); override installUrlResolver only for custom app wrappers | | resultPayloadBuilder | ({ brt, enrollOptions }) => Record<string, unknown> | no | { brt } | Custom shape for the BRT submission body |

biometricService accepts a host, host:port, or https://.... Other schemes, paths, query strings, credentials, and IPv6 literals are rejected.

Methods

All operation methods except retrieveKey/clearUser return the BRT JWT string.

| Method | Signature | Notes | | ------------------------------------ | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | ensureInitialized() | void | Optional warm-up; primes the underlying SophIDMobile | | enrollUser(user, enrollOptions?) | (UserDescriptor, EnrollUserOptions?) => Promise<string> | Mints BST → enroll → submit BRT | | restoreUser() | () => Promise<string> | Mints BST → restore → submit BRT | | authenticateUser() | () => Promise<string> | Mints BST → authenticate → submit BRT | | authenticateUserDirect() | () => Promise<string> | Mints BST → authenticate; does not submit the BRT (caller forwards it to a business endpoint) | | unenrollUser() | () => Promise<string> | Mints BST → unenroll → submit BRT | | retrieveKey() | () => Promise<null> | Phone-browser only; works offline (no polling). Returns null; user pastes the JSON result back from the mobile app | | clearUser() | () => Promise<void> | Phone-browser only; works offline (no polling) | | parseRetrieveKeyResult(jsonString) | (string) => KeyRetrievalResult | Validate and decode the JSON the user pasted from the mobile app | | handlePendingOperation(options?) | (HandlePendingOperationOptions?) => Promise<PendingOperationResult \| null> | Resume iOS Safari return-flow results, submit recovered BRTs by default, and call app-provided UI/business callbacks |

Singleton wrapper

neumaClientHelper exposes the same surface as a singleton:

neumaClientHelper.init(config);            // returns NeumaClientHelper
neumaClientHelper.get();                   // throws if not initialized
neumaClientHelper.ensureInitialized();
neumaClientHelper.enrollUser(user, options?);
neumaClientHelper.restoreUser();
neumaClientHelper.authenticateUser();
neumaClientHelper.authenticateUserDirect();
neumaClientHelper.unenrollUser();
neumaClientHelper.retrieveKey();
neumaClientHelper.clearUser();
neumaClientHelper.parseRetrieveKeyResult(jsonString);
neumaClientHelper.handlePendingOperation({
    operation: 'authenticate',
    onSuccess: result => showResultUi(result),
    onError: error => showErrorUi(error),
});
neumaClientHelper.getPendingOperation();
neumaClientHelper.resumePendingResult();
neumaClientHelper.clearPendingResult();

Low-level API: SophIDMobile

Use SophIDMobileFactory.create() directly when you need full control of BST fetching and BRT submission.

import { SophIDMobileFactory } from '@vizualkei/sophid-client-sdk';

const client = SophIDMobileFactory.create();

client.initialize({
    biometricSessionToken: bstFromYourServer,
    timeoutMs: 90_000,
    qrAppLinkBase: '/my-app/f',
    onQrCode: qrData => showQrModal(qrData),
    onAppLaunchFailed: ctx => showInstallGuidance(ctx),
});

const brt = await client.authenticateUser();

SophIDMobileOptions

| Field | Type | Default | Description | | ----------------------- | ------------------------------------------------- | ------------------------------ | ------------------------------------------------- | | timeoutMs | number | 90000 | Long-poll timeout | | overlay | { appName?: string } | { appName: 'SophID Mobile' } | App-name string used in AppLaunchFailureContext | | biometricService | string | https://api.sophid.xyz:443 | SophID server host[:port] or full https URL | | installUrlResolver | (platform: AppLaunchPlatform) => string \| undefined | — | App-specific install URL resolver used only when phone-browser launch fails | | qrAppLinkBase | string | ${origin}/f | HTTPS app-link base for desktop QR handoff. Apps mounted below / should pass their mounted handoff route, e.g. /neuma-wellness/f or https://apps.example.com/neuma-wellness/f | | biometricSessionToken | string | — | BST issued by your server | | onQrCode | (qrData: QrCodeData) => QrCodeCleanupFn \| void | — | Desktop QR callback | | onAppLaunchFailed | (context: AppLaunchFailureContext) => void | — | Phone-browser fallback callback |

SophIDMobileInterface methods

| Method | Returns | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- | | initialize(opts) | void | | getVersion() | Promise<PackageVersion> | | getEnrolledUser() | Promise<EnrollmentResult \| null> (read from localStorage key sophid_enrolled_user) | | enrollUser(user) | Promise<string> (BRT) | | restoreUser() | Promise<string> (BRT) | | authenticateUser() | Promise<string> (BRT) | | unenrollUser() | Promise<string> (BRT) | | retrieveKey() | Promise<null> — phone-browser only | | clearUser() | Promise<void> — phone-browser only | | parseRetrieveKeyResult(jsonString) | KeyRetrievalResult | | getPendingOperation() | PendingOperationInfo \| null (synchronous; reads SDK recovery state) | | resumePendingResult() | Promise<unknown \| null> (raw biometric server payload, cached payload, or null for no/offline pending) | | clearPendingResult() | void (clears SDK recovery state after app-level result handling is durable) |

Resuming pending operations

iOS Safari only. The pending-op persistence and the returnUrl emission that drives it are gated on iOS Safari (navigator.userAgent matching iOS and not matching CriOS/FxiOS/EdgiOS/OPiOS/DuckDuckGo/Brave/etc.). On every other browser — Android Chrome, iOS Chrome, iOS Firefox, in-app webviews, desktop QR — the SDK does not write the recovery entries, the deep link does not carry returnUrl, and getPendingOperation() normally returns null. Those browsers handle the mobile app's openURL by opening a new tab, leaving the originating tab (with its in-flight polling) untouched, so resume isn't needed and would be incorrect anyway.

When the SDK fires the sophdplk:// deep-link from the phone-browser flow on iOS Safari it persists pending-operation metadata in sessionStorage under the key __sophid_pending_op__ immediately before navigating, injects sophid_cb, sophid_op, and sophid_ts into the return URL, then long-polls GET /api/biometric-results/{id} for the result. If polling completes before the webapp has durably handled the result, the SDK stores the raw payload under __sophid_completed_result__ for a short recovery window.

For most webapps, use handlePendingOperation() from a page-load effect. It is UI-agnostic: the SDK detects and resumes the returned operation, decodes failed BRT claims into SophIDError, optionally submits successful BRTs to biometricResultUrl, and clears SDK recovery state after your callback completes. Your app decides whether the callback opens a modal, navigates, writes state, shows a toast, or does something else.

useEffect(() => {
    let cancelled = false;

    void neumaClientHelper
        .handlePendingOperation({
            operation: 'authenticate',
            onStart: () => setLoading(true),
            onSuccess: async result => {
                // Do durable app-level handling here: persist result state,
                // open your modal, navigate, or update your store.
                await showAuthResultPopup(result);
            },
            onError: error => {
                showAuthError(error);
            },
        })
        .finally(() => {
            if (!cancelled) setLoading(false);
        });

    return () => {
        cancelled = true;
    };
}, []);

Useful options:

  • operation: string, string array, or matcher function for SDK operation names such as authenticate, enroll, restore, or unenroll-user.
  • enrollOptions: metadata included in the recovered BRT submission for enroll flows.
  • submitResult: defaults to true; set false when the recovered BRT must be sent to a custom business endpoint instead of biometricResultUrl.
  • requireSuccess: defaults to true; failed BRT claims are routed to onError.
  • onStart, onSuccess, onError: app-provided UI/business callbacks.
  • Do not skip durable result handling inside onSuccess/onError just because a component is unmounting; the SDK clears recovery state after the callback resolves.

The lower-level methods remain available for custom integrations:

client.getPendingOperation(): PendingOperationInfo | null;
client.resumePendingResult(): Promise<unknown | null>;
client.clearPendingResult(): void;

These methods are also exposed on neumaClientHelper and the neumaClientHelper.init(...) instance.

interface PendingOperationInfo {
    callbackId: string; // matches the deep-link `id` and the polling URL path segment
    operation: string; // 'enroll' | 'restore' | 'authenticate' | 'unenroll-user' | 'retrieve-key' | 'clear-user'
    ts: number; // epoch ms when the deep-link was issued
}

Why this exists

On iOS Safari, when the mobile app refocuses the originating browser tab via openURL(returnUrl), the page is not always restored from the back-forward cache. Instead the tab reloads, the React tree remounts, and the in-progress polling promise from the original await neumaClient.authenticateUser() (or the equivalent) is gone — even though the biometric server has the result waiting. Without resume support, the user closes the popup in the mobile app, lands back in the browser, and the webapp UI is stuck in its idle state forever.

resumePendingResult() re-attaches polling to the same callback ID, or returns the cached completed payload if polling already finished in a prior page lifetime. Stale entries (>5 minutes old) are auto-cleared by getPendingOperation() and surfaced as null. A cached completed payload that has already been returned to app code is kept briefly so a second iOS Safari remount during page-level handling can still recover it.

Low-level page-load pattern

useEffect(() => {
    const pending = neumaClient.getPendingOperation();
    if (!pending || pending.operation !== 'authenticate') return;
    let cancelled = false;
    setLoading(true); // same UI state as a foreground call
    (async () => {
        try {
            const payload = await neumaClient.resumePendingResult();
            if (cancelled || payload === null) return;
            // Replicate whatever your foreground success handler does:
            // decode the BRT, refresh the session, show the success popup, etc.
            await onAuthSuccess(payload);
            neumaClient.clearPendingResult();
        } catch (err) {
            if (cancelled) return;
            // SophIDError code === 'USER_CANCELLED' is a no-op; otherwise show the
            // same error UI a foreground failure would.
            onAuthError(err);
        } finally {
            if (!cancelled) setLoading(false);
        }
    })();
    return () => {
        cancelled = true;
    };
}, []);

Notes:

  • Prefer handlePendingOperation() unless you specifically need to own polling, BRT submission, and cleanup yourself.
  • getPendingOperation() is synchronous; safe to call during render or in a layout effect.
  • resumePendingResult() clears pending metadata, but may keep a completed-result payload briefly. Call clearPendingResult() after your page has durably handled the payload (for example after persisting or showing your result popup).
  • The pending operation name reflects the SDK-internal op (authenticate, enroll, etc.), not your page-level intent. If the same SDK op (e.g. authenticate) is invoked from multiple page contexts (e.g. authenticateUser() on a sign-in page vs authenticateUserDirect() on a password-update page), each page should only resume when the pending.operation matches its own intent.
  • The desktop QR flow (invokeMobileAppViaQr) does not persist pending state — the desktop tab stays alive while the phone is doing its biometric step, so resume is unnecessary.
  • Non-iOS-Safari mobile browsers (Android Chrome, iOS Chrome / Firefox / Edge / Opera / DuckDuckGo / Brave, in-app webviews, …) also do not persist pending state. The mobile app's openURL opens a new tab on those browsers, the originating tab and its polling promise stay alive, and the foreground await neumaClient.authenticateUser() (or equivalent) returns normally when the user manually switches back. Calling getPendingOperation() on these browsers is safe but always returns null.
  • The offline operations retrieve-key and clear-user do not write a server-side result; resumePendingResult() returns null for them after clearing the entry.

Phone vs desktop

Detection is user-agent based (isDesktopBrowser() returns false if any of android, webos, iphone, ipad, ipod, blackberry, iemobile, opera mini, mobile, tablet, touch is present in the UA).

| Aspect | Phone browser | Desktop browser (with onQrCode) | | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | Invocation | sophdplk://operation?operation=...&id=...&bioServer=...&webServer=...&...&returnUrl=... via window.location.href | HTTPS app-link ${qrAppLinkBase}/{opCode}/{callbackId}?b=...&w=...&... rendered as a QR code | | Polling | GET {biometricService}/api/biometric-results/{callbackId} (5 retries, 2s delay, 90s default budget) | Same long-poll endpoint | | Launch failure detection | 2-second visibility heuristic; if the page stays visible the SDK fires onAppLaunchFailed | Not applicable | | retrieveKey | Supported (offline, returns null; user pastes JSON) | Throws SophIDError('This operation is supported only on mobile browsers', 'AUTH_FAILED') | | clearUser | Supported (offline) | Throws SophIDError('This operation is supported only on mobile browsers', 'AUTH_FAILED') | | Cancellation | Built-in error mapping (USER_CANCELLED) | qrData.onCancel() rejects the in-flight operation |

The QR app-link op codes are: enroll → e, authenticate → a, clear-user → c, unenroll-user → u, retrieve-key → r, restore → rs. Unknown operations fall back to the first character.

For root-mounted apps, the default QR app-link base is ${window.location.origin}/f. Apps mounted below a path must configure qrAppLinkBase so scanned QR codes land back inside that app's handoff route. For example:

client.initialize({
    qrAppLinkBase: '/neuma-wellness/f',
    onQrCode: ({ deepLinkUrl, onCancel }) => showQrModal(deepLinkUrl, onCancel),
});

qrAppLinkBase may be a full HTTPS URL or a root-relative path. Query strings and fragments are ignored because the SDK appends the handoff operation, callback ID, and biometric query parameters itself.

Deep-link query parameters

| Param | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | operation | Operation name (enroll, authenticate, restore, unenroll-user, retrieve-key, clear-user). | | id | Callback ID (random UUID-like string). The mobile app posts its result to {biometricService}/api/biometric-results/{id}; the SDK long-polls the same endpoint. | | bioServer | Biometric service host[:port] or full https URL. | | webServer | Web app host (window.location.host). | | userName, email, phone | Optional user descriptor fields. | | bst | Biometric session token, when supplied. | | returnUrl | URL-encoded https:// URL for the originating browser tab. The mobile app launches this URL after the user closes the result popup so the originating tab is foregrounded. The SDK builds it by injecting sophid_cb=<id>, sophid_op=<operation>, and sophid_ts=<issuedAtMs> into the current page URL via history.replaceState(history.state, '', urlWithMarkers) (preserving app state) and then capturing window.location.href. sophid_cb disambiguates the originating tab when multiple tabs are open to the same page; sophid_op and sophid_ts let the SDK recover from a Safari remount even if storage state was already cleared. The SDK omits returnUrl only when the page is not loaded over https:// (e.g. localhost dev). The mobile app validates that returnUrl is https:// and launches it byte-for-byte — callers must not mutate it. |

QR-code flow contract

interface QrCodeData {
    deepLinkUrl: string; // HTTPS app link to encode in the QR code
    callbackId: string; // Polling callback ID
    onCancel: () => void; // Call when the user dismisses the modal
}
type QrCodeCleanupFn = () => void;

onQrCode may return a QrCodeCleanupFn that the SDK calls automatically when the operation completes (success, failure, or cancellation).

App-launch failure contract

type AppLaunchPlatform = 'android' | 'ios' | 'unknown';

interface AppLaunchFailureContext {
    operation: string; // 'enroll' | 'authenticate' | 'restore' | 'unenroll-user' | 'retrieve-key' | 'clear-user'
    platform: AppLaunchPlatform;
    appName: string; // from overlay.appName
    installUrl?: string; // present for Android (Google Play) and iOS (TestFlight)
    message: string; // human-readable suggestion
}

SophIDMobile itself does not choose an app-store destination. NeumaClientHelper configures installUrlResolver with getNeumaMobileInstallUrl, so Neuma launch failures receive NEUMA_MOBILE_PLAY_STORE_URL on Android and NEUMA_MOBILE_TESTFLIGHT_URL on iOS. Other branded helper packages should provide their own resolver at this layer. The legacy SOPHID_* exports alias the Neuma URLs for compatibility.

Error model

class SophIDError extends Error {
    readonly code:
        | 'USER_EXISTS'
        | 'AUTH_FAILED'
        | 'USER_NOT_FOUND'
        | 'INVALID_BIOMETRIC'
        | 'USER_CANCELLED'
        | 'PARSE_ERROR';
}

Mapping per operation when a failure or cancellation is detected:

| Operation | Default error code | | --------------------------------------------- | -------------------------------------------------------------------- | | enrollUser | USER_EXISTS | | restoreUser | AUTH_FAILED | | authenticateUser / authenticateUserDirect | AUTH_FAILED | | unenrollUser | USER_NOT_FOUND | | clearUser | USER_NOT_FOUND | | retrieveKey (desktop) | AUTH_FAILED | | parseRetrieveKeyResult | AUTH_FAILED for failure payloads, PARSE_ERROR for malformed JSON | | any | USER_CANCELLED if the underlying payload signals cancellation |

The SDK considers a payload cancelled when any of cancelled === true, reason ∈ {back, cancel, user_cancelled}, code ∈ {CANCELLED, USER_CANCELLED}, or an error message containing cancel/canceled/cancelled is present.

Type reference

interface UserDescriptor {
    readonly userName: string;
    readonly email?: string;
    readonly phone?: string;
}

interface EnrollmentResult {
    readonly userId: string;
    readonly enrollmentId: string;
    readonly enrolledAt: Date;
    readonly userDescriptor: UserDescriptor;
}

interface AuthenticationResult {
    readonly userId: string;
    readonly userDescriptor: UserDescriptor;
    readonly authenticatedAt: Date;
}

interface KeyRetrievalResult {
    readonly key: string;
    readonly userId: string;
    readonly userDescriptor: UserDescriptor;
}

interface PackageVersion {
    readonly major: number;
    readonly minor: number;
    readonly patch: number;
    readonly build: number;
}

type EnrollUserOptions = {
    email?: string | null;
    phoneNo?: string | null;
};

End-to-end example

import { neumaClientHelper, SophIDError } from '@vizualkei/sophid-client-sdk';

neumaClientHelper.init({
    biometricSessionUrl: '/api/biometric-session',
    biometricResultUrl: '/api/biometric-results',
    fetcher: (input, init) => fetch(input, init),
    resultPayloadBuilder: ({ brt, enrollOptions }) => ({
        brt,
        email: enrollOptions?.email ?? null,
        phoneNo: enrollOptions?.phoneNo ?? null,
    }),
    onQrCode: ({ deepLinkUrl, onCancel }) => showQrModal(deepLinkUrl, onCancel),
    onAppLaunchFailed: ({ installUrl, message }) => showInstallModal({ installUrl, message }),
});

try {
    const brt = await neumaClientHelper.authenticateUser();
    // BRT was already submitted to /api/biometric-results by the helper
    console.log('Authenticated; BRT length =', brt.length);
} catch (err) {
    if (err instanceof SophIDError && err.code === 'USER_CANCELLED') {
        // user backed out of the flow
    } else {
        throw err;
    }
}

For business endpoints that consume the BRT directly (e.g. biometric-gated password change), use authenticateUserDirect() and POST the BRT yourself.

License

Apache-2.0