@vizualkei/sophid-client-sdk
v0.33.1
Published
SophID Web Client SDK - Biometric authentication SDK for webapp integration
Maintainers
Readme
SophID Client SDK
TypeScript SDK that bridges web applications to the SophID Mobile app for biometric authentication. Supports phone browsers (direct deep-linking) and desktop browsers (QR code flow). Returns signed Biometric Result Tokens (BRT, ES256 JWT) from the SophID server.
Installation
pnpm add @vizualkei/sophid-client-sdkIn Next.js, add to next.config.js:
module.exports = {
transpilePackages: ['@vizualkei/sophid-client-sdk'],
};Quick Start (Recommended)
Use the SophidClientHelper singleton for the simplest integration. It handles BST fetching, SDK initialization, and BRT submission automatically.
import { sophidClientHelper } from '@vizualkei/sophid-client-sdk';
// Initialize once at app startup
const sophidClient = sophidClientHelper.init({
biometricSessionUrl: '/api/biometric-session',
biometricResultUrl: '/api/biometric-result',
fetcher: (input, init) => fetch(input, init),
onQrCode: (qrData) => showMyQrModal(qrData.deepLinkUrl, qrData.onCancel),
onAppLaunchFailed: (context) => showInstallModal(context),
});
// Enroll a new user
const brt = await sophidClient.enrollUser({ userName: 'Jane Roe' });
// Authenticate
const authBrt = await sophidClient.authenticateUser();Architecture
End-to-End Flow
PWA Server Web SDK Mobile App SophID Server
│ │ │ │
│◄── POST /biometric-session ──│ │ │
│── { bst } ──────────────►│ │ │
│ │── deep-link (bst) ──────►│ │
│ │ │── POST /api/{op} ─────►│
│ │── long-poll ─────────────────────────────────────►│
│ │◄─────────────────────────────────── { brt } ──────│
│◄── POST /biometric-result ──│ │ │
│── validate BRT ──────────│ │ │Security Tokens
| Token | Format | Purpose |
|-------|--------|---------|
| BST | JSON string ({"sid","ts","ebuid","sig"}) | Partner-issued session token, HMAC-signed |
| BRT | ES256 JWT | Server-signed operation result |
| ebuid | AES-256-GCM encrypted string | Binds session to a specific user |
SophidClientHelper (Recommended)
The SophidClientHelper is the recommended integration surface. It wraps the low-level SDK and automates the full BST→operation→BRT flow in a single method call.
Initialization
import { sophidClientHelper } from '@vizualkei/sophid-client-sdk';
const sophidClient = sophidClientHelper.init(config);SophidClientHelperConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| biometricSessionUrl | string | Yes | Server endpoint to POST for BST (e.g., /api/biometric-session) |
| biometricResultUrl | string | Yes | Server endpoint to POST BRT after operation (e.g., /api/biometric-result) |
| fetcher | (input, init?) => Promise<Response> | Yes | Fetch function (inject auth headers here) |
| biometricService | string | No | SophID server as host, host:port, or https://host[:port] (default: https://api.sophid.xyz:443) |
| onQrCode | (qrData: QrCodeData) => QrCodeCleanupFn \| void | No | Callback for desktop browser QR flow |
| onAppLaunchFailed | (context: AppLaunchFailureContext) => void | No | Called when a phone-browser deep-link likely failed to open the native app |
| sdkOptions | SophIDMobileOptions | No | Additional SDK options (timeout, overlay) |
| resultPayloadBuilder | (params) => Record<string, unknown> | No | Custom BRT submission payload builder |
Methods
enrollUser(user, enrollOptions?): Promise<string>
Enroll a new user. Fetches BST, launches mobile enrollment, submits BRT to server.
const brt = await sophidClient.enrollUser(
{ userName: 'Jane Roe', email: '[email protected]', phone: '+886900000000' },
{ email: '[email protected]', phoneNo: '+886900000000' } // optional, included in BRT submission
);| Parameter | Type | Description |
|-----------|------|-------------|
| user | UserDescriptor | { userName (required), email?, phone? } |
| enrollOptions | EnrollUserOptions | { email?, phoneNo? } — extra fields sent with BRT |
restoreUser(): Promise<string>
Restore an existing user to a new device.
const brt = await sophidClient.restoreUser();authenticateUser(): Promise<string>
Authenticate the enrolled user. Fetches BST, runs authentication, submits BRT.
const brt = await sophidClient.authenticateUser();authenticateUserDirect(): Promise<string>
Authenticate without submitting the BRT to the server. Use this when a business route validates the BRT itself (e.g., biometric-gated password update).
const brt = await sophidClient.authenticateUserDirect();
// Pass brt to your own endpoint for validation
await fetch('/api/update-password', {
method: 'POST',
body: JSON.stringify({ brt, newPassword }),
});unenrollUser(): Promise<string>
Remove the user's enrollment on the server.
const brt = await sophidClient.unenrollUser();retrieveKey(): Promise<null>
Offline key retrieval (phone browsers only). Returns null; the user must manually paste the result from the mobile app.
await sophidClient.retrieveKey();
// Later, when user pastes JSON from mobile app:
const keyResult = sophidClient.parseRetrieveKeyResult(pastedJson);clearUser(): Promise<void>
Clear local enrollment data on the device (phone browsers only, offline).
await sophidClient.clearUser();parseRetrieveKeyResult(jsonString): KeyRetrievalResult
Parse the JSON string pasted from the mobile app after retrieveKey().
const result = sophidClient.parseRetrieveKeyResult('{"key":"...","userId":"..."}');
// result.key, result.userId, result.userDescriptorIntegration Examples
Example: No-Auth Demo Server
import { sophidClientHelper } from '@vizualkei/sophid-client-sdk';
import { showBiometricQrModal } from './utils/BiometricQrModal';
const sophidClient = sophidClientHelper.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,
}),
onAppLaunchFailed: ({ installUrl, message }) => {
showInstallModal({ installUrl, message });
},
onQrCode: ({ deepLinkUrl, onCancel }) =>
showBiometricQrModal(deepLinkUrl, onCancel),
});Example: Authenticated Server (Bearer Token)
import { sophidClientHelper } from '@vizualkei/sophid-client-sdk';
import { showBiometricQrModal } from '@/utils/biometricQrModal';
import { getAccessToken } from '@/lib/auth';
const fetchWithAuth = async (input: RequestInfo, init: RequestInit = {}) => {
const token = await getAccessToken();
return fetch(input, {
...init,
headers: { ...(init.headers || {}), Authorization: `Bearer ${token}` },
});
};
const sophidClient = sophidClientHelper.init({
biometricSessionUrl: '/api/biometric-session',
biometricResultUrl: '/api/biometric-result',
fetcher: fetchWithAuth,
onQrCode: ({ deepLinkUrl, onCancel }) =>
showBiometricQrModal(deepLinkUrl, onCancel),
});Singleton Access
After calling sophidClientHelper.init(), you can access the instance from anywhere:
import { sophidClientHelper } from '@vizualkei/sophid-client-sdk';
// Direct singleton methods (delegates to the initialized instance)
const brt = await sophidClientHelper.authenticateUser();
// Or get the instance explicitly
const client = sophidClientHelper.get();Low-Level API: SophIDMobile
For advanced use cases, you can use the factory directly. You are responsible for BST fetching and BRT submission.
Create a Client
import { SophIDMobileFactory } from '@vizualkei/sophid-client-sdk';
const client = SophIDMobileFactory.create();Initialize
client.initialize({
timeoutMs: 90000,
overlay: { appName: 'SophID Mobile' },
biometricSessionToken: bst, // from your server
onQrCode: (qrData) => { /* show QR modal */ },
});SophIDMobileOptions
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| timeoutMs | number | 90000 | Polling timeout in milliseconds |
| overlay | { appName? } | { appName: 'SophID Mobile' } | App naming config used in onAppLaunchFailed guidance |
| biometricService | string | https://api.sophid.xyz:443 | SophID server as host, host:port, or https://host[:port] |
| biometricSessionToken | string | — | BST from your server |
| onQrCode | (qrData) => cleanup \| void | — | Desktop QR flow callback |
| onAppLaunchFailed | (context) => void | — | Phone-browser fallback callback when the app likely failed to open |
Operations
// All operations return BRT (JWT string) except retrieveKey and clearUser
const enrollBrt = await client.enrollUser({ userName: 'Jane', email: '[email protected]' });
const restoreBrt = await client.restoreUser();
const authBrt = await client.authenticateUser();
const unenrollBrt = await client.unenrollUser();
// Offline operations (phone browser only)
await client.retrieveKey(); // returns null
const key = client.parseRetrieveKeyResult(pastedJson);
await client.clearUser();
// Utility
const version = await client.getVersion();
const enrolled = await client.getEnrolledUser();Manual BST Flow
// 1. Get BST from your server
const { bst } = await fetch('/api/biometric-session', { method: 'POST' }).then(r => r.json());
// 2. Initialize with BST
client.initialize({ biometricSessionToken: bst });
// 3. Run operation
const brt = await client.authenticateUser();
// 4. Submit BRT to your server
await fetch('/api/biometric-result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ brt }),
});Desktop QR Flow
When running on a desktop browser, the SDK generates an HTTPS URL for QR code display instead of direct deep-linking.
How It Works
- SDK detects desktop browser via user-agent analysis.
- Generates a short HTTPS URL:
https://{host}/f/{opCode}/{callbackId}?b={bioServer}&w={webServer}&u={userName}&... - Calls
onQrCode(qrData)— your app displays the QR code. - User scans QR with phone camera → phone browser opens → redirects to mobile app.
- Mobile app completes biometric operation → SophID server signs and caches result.
- SDK receives result via long-polling → calls cleanup function → returns BRT.
QrCodeData
interface QrCodeData {
deepLinkUrl: string; // HTTPS URL to encode in QR code
callbackId: string; // Polling callback ID
onCancel: () => void; // Call when user cancels the QR modal
}QR Callback Example
client.initialize({
onQrCode: (qrData) => {
const modal = showQrModal(qrData.deepLinkUrl, qrData.onCancel);
return () => modal.dismiss(); // cleanup function called when operation completes
},
});Restrictions
retrieveKey()is not supported on desktop browsers (requires same-device clipboard). Throws:"This operation is supported only on mobile browsers".clearUser()is not supported on desktop browsers.
App Launch Failure Detection
On phone browsers, the SDK cannot directly prove whether the native app is installed. Instead it:
- Attempts to open the deep link.
- Waits briefly for the page to go hidden.
- If the page stays visible, it infers that the app likely failed to open.
When that heuristic triggers, the SDK calls onAppLaunchFailed(context). Client apps should use that callback to show install/open guidance UI. On Android, the context includes the direct Google Play URL for SophID Mobile.
Phone vs Desktop Comparison
| Aspect | Phone Browser | Desktop Browser |
|--------|---------------|-----------------|
| Invocation | Direct deep-link (sophdplk://...) | QR code scan → HTTPS URL → redirect |
| User Flow | Tap → app opens → complete biometric | Tap → scan QR → app opens → complete biometric |
| Polling | Same server long-polling | Same server long-polling |
| retrieveKey | Supported | Not supported |
| clearUser | Supported | Not supported |
| Fallback UI | Client-owned onAppLaunchFailed guidance | QR modal with cancel button |
Types
UserDescriptor
interface UserDescriptor {
readonly userName: string;
readonly email?: string;
readonly phone?: string;
}EnrollmentResult
interface EnrollmentResult {
readonly userId: string;
readonly enrollmentId: string;
readonly enrolledAt: Date;
readonly userDescriptor: UserDescriptor;
}AuthenticationResult
interface AuthenticationResult {
readonly userId: string;
readonly userDescriptor: UserDescriptor;
readonly authenticatedAt: Date;
}KeyRetrievalResult
interface KeyRetrievalResult {
readonly key: string;
readonly userId: string;
readonly userDescriptor: UserDescriptor;
}PackageVersion
interface PackageVersion {
readonly major: number;
readonly minor: number;
readonly patch: number;
readonly build: number;
}EnrollUserOptions
type EnrollUserOptions = {
email?: string | null;
phoneNo?: string | null;
};SophIDError
class SophIDError extends Error {
readonly code:
| 'USER_EXISTS'
| 'AUTH_FAILED'
| 'USER_NOT_FOUND'
| 'INVALID_BIOMETRIC'
| 'USER_CANCELLED'
| 'PARSE_ERROR';
}Error Handling
All biometric operations throw SophIDError on failure or cancellation.
import { SophIDError } from '@vizualkei/sophid-client-sdk';
try {
const brt = await sophidClient.authenticateUser();
} catch (err) {
if (err instanceof SophIDError) {
switch (err.code) {
case 'USER_CANCELLED': /* user dismissed the operation */ break;
case 'AUTH_FAILED': /* biometric mismatch */ break;
case 'USER_NOT_FOUND': /* no enrollment found */ break;
case 'USER_EXISTS': /* duplicate enrollment */ break;
case 'INVALID_BIOMETRIC': /* biometric quality issue */ break;
case 'PARSE_ERROR': /* malformed response */ break;
}
}
}BST Format
BST is a JSON string issued by your server, not a JWT.
{"sid":"<uuid>","ts":1700000000000,"ebuid":"<base64url>","sig":"<base64url>"}| Field | Type | Description |
|-------|------|-------------|
| sid | string | Session UUID |
| ts | number | Timestamp (ms since epoch) |
| ebuid | string | Encrypted biometric user ID (empty for enrollment) |
| sig | string | HMAC-SHA256 of sid.ts.ebuid using partner API key |
TTL: 5 minutes. Single-use (consumed on validation).
BRT Claims
BRT is an ES256 JWT signed by the SophID server.
Common Claims
| Claim | Type | Description |
|-------|------|-------------|
| success | boolean | Operation outcome |
| op | string | enroll \| authenticate \| unenroll \| checkin \| restore \| update-pin |
| callbackId | string | Polling callback ID |
| bst | string | Original BST (for partner validation) |
| iss | string | JWT issuer |
| aud | string | JWT audience |
| jti | string | Unique token ID |
| iat | number | Issued at (unix seconds) |
| exp | number | Expiration (unix seconds) |
Success Claims by Operation
| Operation | Additional Claims |
|-----------|-------------------|
| enroll | userId, enrollmentId, userName, enrolledAt |
| authenticate | userId, enrollmentId, userName, authenticatedAt, message |
| restore | userId, enrollmentId, userName, enrolledAt |
| unenroll | userId, enrollmentId, userName |
| checkin | userId, eventId, ts |
| update-pin | userId, enrollmentId, message |
Failure Claims
| Claim | Type | Description |
|-------|------|-------------|
| error | string | Error message |
| reason | string | Failure reason |
| code | string | Error code |
| cancelled | boolean | true if user cancelled |
