npm package: @rdhiq/rdh-fp
Description: React SDK for fingerprint capture, validation and submission via Neurotechnology Device Server
Latest version: 1.1.0

# @rdhiq/rdh-fp

A React SDK for fingerprint capture, enrollment, verification, and identification using a **Neurotechnology Device Server** + **RD.FingerPrint middleware** backend.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Architecture Overview](#architecture-overview)
- [Provider Setup](#provider-setup)
- [Triggering Capture](#triggering-capture)
  - [Convenience Buttons](#convenience-buttons)
  - [Programmatic API](#programmatic-api)
- [Handling Results](#handling-results)
- [Settings Page](#settings-page)
- [Configuration Reference](#configuration-reference)
  - [FingerprintConfig](#fingerprintconfig)
  - [ValidationRules](#validationrules)
  - [CaptureOperationParams](#captureoperationparams)
- [CaptureResult Reference](#captureresult-reference)
- [Validation and Capture Modes](#validation-and-capture-modes)
- [TypeScript Types](#typescript-types)
- [Error Handling](#error-handling)
- [Advanced: Low-Level API](#advanced-low-level-api)
  - [MiddlewareClient](#middlewareclient)
  - [DeviceServerClient](#deviceserverclient)
- [Configuration Utilities](#configuration-utilities)

---

## Installation

Published package name: `@rdhiq/rdh-fp`

```bash
npm install @rdhiq/rdh-fp
# or
bun add @rdhiq/rdh-fp
# or
yarn add @rdhiq/rdh-fp
```

> **Peer dependencies:** `react >= 18`, `react-dom >= 18`

Minimal import usage:

```tsx
import "@rdhiq/rdh-fp/style.css";
import { FingerprintProvider, FingerprintModal } from "@rdhiq/rdh-fp";
```

---

## Quick Start

```tsx
import "@rdhiq/rdh-fp/style.css";
import {
  FingerprintProvider,
  FingerprintModal,
  FingerprintButtonEnroll,
  FingerprintButtonVerify,
  FingerprintButtonIdentify,
} from "@rdhiq/rdh-fp";

export default function App() {
  return (
    <FingerprintProvider
      initialConfig={{
        deviceServer: { baseUrl: "http://127.0.0.1", port: 8040 },
        middleware: { baseUrl: "https://your-api.example.com" },
      }}
      onSuccess={(result) => console.log("Capture succeeded", result)}
      onError={(msg, result) => console.error("Capture failed", msg, result)}
    >
      <FingerprintButtonEnroll params={{ personId: "p-001", fullName: "Jane Doe" }}>
        Enroll
      </FingerprintButtonEnroll>

      <FingerprintButtonVerify params={{ personId: "p-001" }}>
        Verify
      </FingerprintButtonVerify>

      <FingerprintButtonIdentify>
        Identify
      </FingerprintButtonIdentify>

      {/* Mount once — rendered globally inside the provider */}
      <FingerprintModal />
    </FingerprintProvider>
  );
}
```

---

## Architecture Overview

```
Host App
  └─ <FingerprintProvider>          ← config, state, and context
       ├─ <FingerprintButtonEnroll> ← opens modal in enrollment mode
       ├─ <FingerprintButtonVerify> ← opens modal in verification mode
       ├─ <FingerprintButtonIdentify> ← opens modal in identification mode
       ├─ <SettingsPage />          ← config editor (optional)
       └─ <FingerprintModal />      ← capture UI, mount once
```

**Capture lifecycle (per operation):**

```
register → initialize → lock → capture → download
  → validate (client-side) → submit to middleware → unlock → unregister
```

The session cleanup (`unlock` + `unregister`) runs in a `finally` block — it always executes even on error or abort.

---

## Provider Setup

Wrap your application (or the relevant subtree) with `FingerprintProvider`:

```tsx
import { FingerprintProvider } from "@rdhiq/rdh-fp";

<FingerprintProvider
  initialConfig={partialConfig}
  onSuccess={(result) => { /* ... */ }}
  onError={(msg, result) => { /* ... */ }}
>
  {children}
</FingerprintProvider>
```

| Prop | Type | Description |
|------|------|-------------|
| `initialConfig` | `Partial<FingerprintConfig>` | Merged over `localStorage` config + defaults. Useful for injecting server-side config. |
| `projectId` | `string` | Optional project identifier. When provided, the provider fetches the project from the middleware on mount and stores it in `config.projectConfig`. |
| `onSuccess` | `(result: CaptureResult) => void` | Called once after a successful capture and backend submission. |
| `onError` | `(msg: string, result: Partial<CaptureResult>) => void` | Called on terminal failure (device error, quality rejection, middleware error, etc.) together with the current partial result. |

```tsx
<FingerprintProvider
  projectId="6f6d2b9d-1111-2222-3333-444444444444"
  initialConfig={{ middleware: { baseUrl: "https://your-api.example.com" } }}
  onSuccess={(result) => console.log(result)}
  onError={(message, result) => console.error(message, result)}
>
  {children}
</FingerprintProvider>
```

Config is loaded from `localStorage` automatically. The merge order is:

```
DEFAULT_CONFIG ← localStorage ← initialConfig prop
```

---

## Triggering Capture

### Convenience Buttons

Each button calls `useFingerprint()` internally and opens the modal in the matching operation mode. They extend `React.ButtonHTMLAttributes<HTMLButtonElement>` so all standard button props (`className`, `disabled`, `style`, `onClick`, etc.) pass through.

#### `FingerprintButtonEnroll`

```tsx
<FingerprintButtonEnroll
  className="my-btn"
  params={{
    personId: "p-001",
    nationalId: "123456789",
    fullName: "Jane Doe",
    fingerIndex: 0,
    imageFormat: 1,
    debug: true,
  }}
  onSuccess={(result) => console.log("enrolled", result)}
  onError={(msg, result) => console.error("enroll failed", msg, result)}
>
  ✋ Enroll Fingerprint
</FingerprintButtonEnroll>
```

#### `FingerprintButtonVerify`

```tsx
<FingerprintButtonVerify
  params={{
    personId: "p-001",
    nationalId: "123456789",
    fingerIndex: 0,
    threshold: 60,
  }}
  onSuccess={(result) => console.log("verified", result)}
  onError={(msg, result) => console.error("verify failed", msg, result)}
>
  🔐 Verify Fingerprint
</FingerprintButtonVerify>
```

#### `FingerprintButtonIdentify`

```tsx
<FingerprintButtonIdentify
  params={{
    fingerIndex: 0,
    topK: 5,
    threshold: 50,
  }}
  onSuccess={(result) => console.log("identified", result)}
  onError={(msg, result) => console.error("identify failed", msg, result)}
>
  🔍 Identify
</FingerprintButtonIdentify>
```

All three buttons accept `onSuccess` and `onError` props that override the provider-level and modal-level callbacks for that specific operation. They are automatically **disabled** while the modal is open to prevent concurrent operations.

---

### Programmatic API

Use `useFingerprint()` to open the modal imperatively:

```tsx
import { useFingerprint } from "@rdhiq/rdh-fp";

function MyComponent() {
  const { openEnroll, openVerify, openIdentify, isModalOpen } = useFingerprint();

  return (
    <>
      <button
        disabled={isModalOpen}
        onClick={() => openEnroll({ personId: "p-001", fullName: "Jane Doe" })}
      >
        Enroll
      </button>

      <button
        disabled={isModalOpen}
        onClick={() => openVerify({ personId: "p-001", threshold: 60 })}
      >
        Verify
      </button>

      <button
        disabled={isModalOpen}
        onClick={() => openIdentify({ topK: 5 })}
      >
        Identify
      </button>
    </>
  );
}
```

#### `useFingerprint()` return value

| Property | Type | Description |
|----------|------|-------------|
| `openEnroll` | `(params?, callbacks?) => void` | Opens modal in enrollment mode |
| `openVerify` | `(params?, callbacks?) => void` | Opens modal in verification mode |
| `openIdentify` | `(params?, callbacks?) => void` | Opens modal in identification mode |
| `isModalOpen` | `boolean` | `true` while a capture operation is in progress |
| `modalState` | `ModalState` | Current state of the modal |
| `lastResult` | `CaptureResult \| null` | Most recent completed result (success or failure) |
| `config` | `FingerprintConfig` | Active configuration |
| `updateConfig` | `(cfg: FingerprintConfig) => void` | Replaces and persists the config |

`callbacks` is `{ onSuccess?: (result: CaptureResult) => void; onError?: (error: string, result: Partial<CaptureResult>) => void }` and overrides both modal-level and provider-level callbacks for that call.

Callbacks can also be embedded directly in `params` as `onSuccess`/`onError` fields.

---

## Handling Results

React to outcomes via the provider callbacks or by observing `lastResult`:

```tsx
// Via provider props (recommended)
<FingerprintProvider
  onSuccess={(result) => {
    if (result.backendResponse?.matched) {
      console.log("Identity verified, score:", result.backendResponse.score);
    }
    if (result.backendResponse?.fingerprintRecordId) {
      console.log("Enrolled, record ID:", result.backendResponse.fingerprintRecordId);
    }
    if (result.backendResponse?.candidates) {
      console.log("Top candidates:", result.backendResponse.candidates);
    }
  }}
  onError={(msg) => alert(`Capture failed: ${msg}`)}
>
```

```tsx
// Via hook (e.g. to keep a local history)
function ResultLog() {
  const { lastResult } = useFingerprint();
  const [history, setHistory] = useState<CaptureResult[]>([]);

  useEffect(() => {
    if (lastResult) setHistory(prev => [lastResult, ...prev].slice(0, 10));
  }, [lastResult]);

  return <pre>{JSON.stringify(history, null, 2)}</pre>;
}
```

---

## Settings Page

`SettingsPage` is a ready-made configuration editor. Render it inside `FingerprintProvider`:

```tsx
import { SettingsPage } from "@rdhiq/rdh-fp";

function SettingsRoute() {
  return <SettingsPage />;
}
```

The page covers:
- **Theme** — toggle between dark and light mode
- **Device Selection** — load available devices and pick one
- **Project Settings** — load by project ID and display allowed hand, mandatory fingers, optional fingers, and whether all mandatory fingers are required
- **Validation Rules** — display current version and fetch latest rules from middleware
- **Save** — persists everything to `localStorage`

Device Server and middleware settings are configured in host app code through `FingerprintProvider.initialConfig`; they are not edited in `SettingsPage`.

---

## Configuration Reference

### FingerprintConfig

```ts
interface FingerprintConfig {
  deviceServer: DeviceServerConfig;
  middleware: MiddlewareConfig;
  validationRules: ValidationRules;
  projectConfig: ProjectConfig | null;
}
```

#### DeviceServerConfig

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `baseUrl` | `string` | `"http://127.0.0.1"` | Device Server host address |
| `port` | `number` | `8040` | Device Server port |
| `deviceId` | `string \| null` | `null` | Active device (selected via SettingsPage) |
| `timeoutMs` | `number` | `15000` | Per-request timeout in milliseconds |
| `captureParams` | `Record<string, unknown> \| undefined` | `undefined` | Optional session-scoped configure parameters sent to the Device Server |

#### MiddlewareConfig

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `baseUrl` | `string` | `"https://api.example.com"` | RD.FingerPrint.API host |
| `validationEndpoint` | `string` | `"/api/_conf_validation"` | Endpoint to fetch `ValidationRules` |
| `StationId` | `string \| null` | `null` | Optional station identifier |

#### ProjectConfig

| Field | Type | Description |
|-------|------|-------------|
| `projectId` | `string` | Loaded project identifier |
| `projectName` | `string \| null` | Project display name |
| `allowedHand` | `"Unknown" \| "Left" \| "Right" \| "Both" \| null` | Which hand is allowed for capture |
| `mandatoryFingerPositions` | `string[] \| null` | Required finger positions defined by the project |
| `optionalFingerPositions` | `string[] \| null` | Optional finger positions defined by the project |
| `requireAllMandatoryFingers` | `boolean \| null` | Whether all mandatory fingers must be captured |

---

### ValidationRules

Client-side capture quality thresholds, fetched from the middleware and cached in config.

| Field | Type | Description |
|-------|------|-------------|
| `rulesVersion` | `string \| null` | Semver of the rule set |
| `MaxCaptureAttempts` | `number \| null` | Max retries before aborting (default: 3) |
| `DefaultVerificationThreshold` | `number \| null` | Default match threshold for verify/identify |
| `DefaultTopK` | `number \| null` | Default top-K for identification |
| `MaxTopKResults` | `number \| null` | Upper bound on topK |
| `StandardQualityMin` | `number \| null` | Minimum quality score (standard mode) |
| `StandardTooWetMax` | `number \| null` | Max "too wet" metric (standard mode) |
| `StandardPressedTooHardMax` | `number \| null` | Max pressure metric (standard mode) |
| `StandardLivenessMin` | `number \| null` | Min liveness confidence (standard mode) |
| `StandardFingerTipMax` | `number \| null` | Max finger-tip ratio (standard mode) |
| `ReducedQualityMin` | `number \| null` | Quality floor for reduced acceptance mode |
| *(and parallel Reduced* fields)* | | Same metrics with relaxed thresholds |

---

### CaptureOperationParams

Passed to `openEnroll()`, `openVerify()`, `openIdentify()`, or the button `params` prop:

| Field | Type | Modes | Description |
|-------|------|-------|-------------|
| `personId` | `string` | enroll, verify | Person to enroll or verify against |
| `nationalId` | `string` | enroll, verify | National ID to associate |
| `fullName` | `string` | enroll | Person's full name |
| `employeeName` | `string` | all | Display name of the operator / employee performing the capture |
| `transactionName` | `string` | all | Display name of the transaction |
| `citizenName` | `string` | all | Display name of the citizen being captured |
| `fingerIndex` | `number` | all | Finger position 0–9 (default: 0) |
| `fingerPositions` | `FingerPosition[]` | all | One or two positions selected in the FingerSelector UI |
| `imageFormat` | `number` | enroll | Image format code (default: 1) |
| `threshold` | `number` | verify, identify | Match score threshold 0–100 |
| `topK` | `number` | identify | Number of top candidates to return |
| `requestId` | `string` | all | Host-assigned correlation identifier |
| `userId` | `string` | all | User performing the operation |
| `projectId` | `string` | all | Project context |
| `transactionId` | `string` | all | Reference for audit trails |
| `allowReducedAcceptance` | `boolean` | all | Re-run with reduced thresholds when standard validation fails and backend policy permits |
| `overrideToken` | `string` | all | Backend token enabling reduced acceptance |
| `debug` | `boolean` | all | Show debug panel inside the modal |
| `fingers` | `FingerConfig[]` | all | Optional custom finger list shown in the selector UI |
| `onSuccess` | `(result: CaptureResult) => void` | all | Per-operation callback invoked on success. Overrides modal-level and provider-level `onSuccess`. |
| `onError` | `(error: string, result: Partial<CaptureResult>) => void` | all | Per-operation callback invoked on terminal failure. Overrides modal-level and provider-level `onError`. |

---

## CaptureResult Reference

Returned via `onSuccess`/`onError` and available via `lastResult`:

```ts
interface CaptureResult {
  success: boolean;
  operationMode: "enrollment" | "verification" | "identification";
  captureMode: "standard" | "reduced";
  sensorData: null;               // Never stored, privacy by design
  captureId: string | null;       // Device Server capture ID
  metadata: Record<string, string | number | boolean | null>;
  deviceId: string;
  dsVersion: string | null;
  rulesVersion: string | null;
  sessionId: string | null;
  correlationId: string;
  backendResponse?: BackendSubmitResponse;
  error?: string;
}
```

#### BackendSubmitResponse fields by operation

| Operation | Field | Type | Description |
|-----------|-------|------|-------------|
| **enroll** | `fingerprintRecordId` | `string` | Stored record ID |
| | `qualityScore` | `number` | Enrollment quality (0–100) |
| | `personId` | `string` | Confirmed person ID from backend |
| **verify** | `matched` | `boolean` | `true` if the probe matched |
| | `score` | `number` | Match score (0–100) |
| | `candidatePersonId` | `string` | Person who matched |
| **identify** | `candidates` | `IdentifyCandidate[]` | Top-K match candidates |
| | `requestId` | `string` | Async job request ID |

```ts
interface IdentifyCandidate {
  personId: string;
  nationalId?: string;
  fullName?: string;
  score: number;
  matchingFingerPosition?: number;
}
```

---

## Validation and Capture Modes

The SDK runs **two tiers** of client-side capture quality checks before submitting to the backend:

### Standard mode (default)

Uses `Standard*` thresholds from `ValidationRules`. These are strict — a captured image must pass all metrics (quality score, liveness, wetness, pressure, finger-tip ratio, image size) to proceed.

### Reduced mode

Uses the `Reduced*` thresholds, which are more lenient. Activated when `overrideToken` is present in `CaptureOperationParams`. If `RequireOverrideForReducedAcceptance` is `true`, a backend-issued token is required.

### Metrics evaluated

| Metric | Description |
|--------|-------------|
| `Quality` | Overall fingerprint quality score |
| `TooWet` | Excessive moisture detection |
| `PressedTooHard` | Excessive finger pressure |
| `LivenessConfidence` | Liveness / anti-spoofing confidence |
| `FingerTip` | Proportion of image occupied by finger tip |
| `ImageSizeBytes` | Raw image file size |

Client-side validation is a **client-side gate only**. The middleware backend is authoritative.

---

## TypeScript Types

All types are exported from the package root:

```ts
import type {
  // Config
  FingerprintConfig,
  DeviceServerConfig,
  MiddlewareConfig,
  ValidationRules,

  // Operation
  OperationMode,
  CaptureMode,
  ModalState,
  CaptureOperationParams,
  CaptureResult,
  BackendSubmitResponse,
  IdentifyCandidate,
  ValidationResult,
  FailedMetric,

  // Middleware API
  EnrollApiRequest,
  EnrollApiResponse,
  VerifyApiRequest,
  VerifyApiResponse,
  IdentifyApiRequest,
  IdentifyJobResponse,
  IdentifyStatusResponse,
  ValidationRulesResponse,
  MiddlewareErrorCode,

  // Device Server
  DeviceInfo,
  MetadataEntry,
  DSErrorCode,

  // Validation utils
  MetricValues,

  // Connectivity
  ConnectivityTestResult,
} from "@rdhiq/rdh-fp";

import { DSError, MiddlewareError } from "@rdhiq/rdh-fp";
```

---

## Error Handling

Both API clients throw typed errors with machine-readable codes:

```ts
import { DSError, MiddlewareError } from "@rdhiq/rdh-fp";

try {
  // ... capture operation
} catch (err) {
  if (err instanceof DSError) {
    switch (err.code) {
      case "DS_OFFLINE":        // Device Server unreachable
      case "DEVICE_NOT_FOUND":  // Configured deviceId not returned by DS
      case "SENSOR_BUSY":       // Another session already has the sensor locked
      case "CAPTURE_FAILED":    // Sensor returned no data
      case "DOWNLOAD_FAILED":   // Could not fetch image
      case "TIMEOUT":           // Request exceeded timeoutMs
    }
  }

  if (err instanceof MiddlewareError) {
    switch (err.code) {
      case "MIDDLEWARE_OFFLINE":    // Cannot reach the middleware host
      case "UNAUTHORIZED_DEVICE":  // 401 / 403 from middleware
      case "SUBMIT_FAILED":        // Submission or identify polling failed
      case "RULES_FETCH_FAILED":   // Could not fetch validation rules
    }
  }
}
```

In normal usage you do not need to catch these directly — the SDK surfaces errors through `onError` on the provider.

---

## Advanced: Low-Level API

> For integration testing, custom UIs, or scripted workflows. Not needed for typical SDK usage.

### MiddlewareClient

```ts
import { MiddlewareClient } from "@rdhiq/rdh-fp";

const mw = new MiddlewareClient("https://your-api.example.com");

// Fetch current validation rules
const rules = await mw.fetchValidationRules();

// Enroll
const enrolled = await mw.enroll({
  personId: "p-001",
  nationalId: "123456789",
  fullName: "Jane Doe",
  fingerPosition: 0,
  imageBase64: "<base64-encoded-image>",
  imageFormat: 1,
});
console.log(enrolled.fingerprintRecordId, enrolled.qualityScore);

// Verify
const verified = await mw.verify({
  personId: "p-001",
  probeImageBase64: "<base64-encoded-image>",
  probeFingerPosition: 0,
  threshold: 60,
});
console.log(verified.matched, verified.score);

// Identify (async — polls internally until done)
const result = await mw.identify({
  probeImageBase64: "<base64-encoded-image>",
  probeFingerPosition: 0,
  topK: 5,
  threshold: 50,
});
console.log(result.candidates);
```

**`identify()` polls `GET /api/v1/fingerprints/identify/{requestId}` every 1 second, up to 60 seconds, until `status === 3` (Done).**

---

### DeviceServerClient

```ts
import { DeviceServerClient } from "@rdhiq/rdh-fp";

const ds = new DeviceServerClient("http://127.0.0.1", 8040, 15000);

// Discover
const { version } = await ds.getVersion();
const devices = await ds.getDevices();

// Session lifecycle
const { sessionId } = await ds.register(deviceId);
await ds.initialize(deviceId, sessionId);
await ds.lock(deviceId, sessionId);

// Capture
const { captureIds } = await ds.capture(deviceId, sessionId);
const download = await ds.download(deviceId, captureIds[0]);
// download.sensorData — base64 fingerprint image
// download.metadata   — quality metrics array

// Always clean up
await ds.unlock(deviceId, sessionId);
await ds.unregister(deviceId, sessionId);
```

---

## Configuration Utilities

```ts
import {
  loadConfig,
  saveConfig,
  clearConfig,
  STORAGE_KEY,
  DEFAULT_CONFIG,
} from "@rdhiq/rdh-fp";

// Load config (merges localStorage with defaults, SSR-safe)
const config = loadConfig();

// Persist the full config
saveConfig({ ...config, deviceServer: { ...config.deviceServer, port: 9001 } });

// Reset to defaults
clearConfig();

// The localStorage key ("finger-prints-config")
console.log(STORAGE_KEY);

// Inspect the baked-in defaults
console.log(DEFAULT_CONFIG);
```