react-native-usesense 1.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,251 +1,405 @@
1
- import { NativeModules, NativeEventEmitter } from 'react-native';
1
+ import { NativeModules, NativeEventEmitter, Platform } from 'react-native';
2
2
 
3
- const { UseSenseModule } = NativeModules;
3
+ const LINKING_ERROR =
4
+ `react-native-usesense: the native module could not be found.\n\n` +
5
+ Platform.select({
6
+ ios: `iOS: run \`cd ios && pod install\` and rebuild the app.`,
7
+ android: `Android: rebuild the app after installing the package.`,
8
+ default: '',
9
+ }) +
10
+ `\n\nIf you're using Expo, the plugin must be consumed through a development build; it does not support Expo Go because it wraps a native camera SDK.`;
11
+
12
+ const UseSenseModule = NativeModules.UseSenseModule;
4
13
 
5
14
  if (!UseSenseModule) {
6
- throw new Error(
7
- 'react-native-usesense: NativeModule not found. Make sure you have linked the library correctly.\n' +
8
- 'iOS: Run `cd ios && pod install`\n' +
9
- 'Android: Rebuild the app after installing the package.',
10
- );
15
+ throw new Error(LINKING_ERROR);
11
16
  }
12
17
 
13
- // ─── Types ───────────────────────────────────────────────────────────
18
+ // ─── Types ───────────────────────────────────────────────────────────────
14
19
 
15
- /** Environment for the UseSense API. */
16
- export type UseSenseEnvironment = 'production' | 'sandbox';
20
+ /**
21
+ * Target environment for the UseSense API. `auto` asks the native SDK
22
+ * to detect from the API key prefix (`sk_sandbox_*` → sandbox,
23
+ * `sk_prod_*` → production).
24
+ */
25
+ export type UseSenseEnvironment = 'sandbox' | 'production' | 'auto';
17
26
 
18
- /** Session type for verification. */
27
+ /** Type of verification session. */
19
28
  export type SessionType = 'enrollment' | 'authentication';
20
29
 
21
- /** Challenge policy controlling verification difficulty. */
22
- export type ChallengePolicy = 'standard' | 'enhanced' | 'adaptive';
23
-
24
- /** Decision returned by the verification engine. */
25
- export type UseSenseDecision = 'approved' | 'rejected' | 'manual_review';
26
-
27
- /** Processing stage during server-side analysis. */
28
- export type ProcessingStage = 'deepsense' | 'livesense' | 'matchsense' | 'fusion';
30
+ /**
31
+ * Decision returned by the native SDK.
32
+ *
33
+ * These values come straight from the server's verdict and are
34
+ * exposed to the integrator for UI feedback only. The definitive
35
+ * verdict arrives at your backend via an HMAC-signed webhook; never
36
+ * trust the client-side decision for access-control.
37
+ */
38
+ export type Decision = 'APPROVE' | 'REJECT' | 'MANUAL_REVIEW';
29
39
 
30
40
  /**
31
- * Configuration for initializing the UseSense plugin.
41
+ * Branding overrides applied to the native camera UI.
32
42
  *
33
- * @example
34
- * ```ts
35
- * await UseSense.initialize({
36
- * apiKey: 'your_api_key',
37
- * environment: 'sandbox',
38
- * });
39
- * ```
43
+ * - `logoUrl`, `primaryColor`, `buttonRadius`, `fontFamily` are
44
+ * honoured on both iOS and Android.
45
+ * - `displayName`, `redirectUrl` are Android-only fields. They're
46
+ * accepted on iOS for cross-platform API parity but ignored.
47
+ */
48
+ export interface BrandingConfig {
49
+ logoUrl?: string;
50
+ primaryColor?: string;
51
+ buttonRadius?: number;
52
+ fontFamily?: string;
53
+ displayName?: string;
54
+ redirectUrl?: string;
55
+ }
56
+
57
+ /**
58
+ * Configuration for `UseSense.initialize`. The minimum required
59
+ * field is the API key; every other field has a sensible default.
40
60
  */
41
61
  export interface UseSenseConfig {
42
- /** API key from the UseSense dashboard. */
62
+ /** API key from watchtower.usesense.ai. */
43
63
  apiKey: string;
44
- /** Target environment. Defaults to `'sandbox'`. */
64
+ /** Target environment. Defaults to `auto` (detected from key prefix). */
45
65
  environment?: UseSenseEnvironment;
46
- /** Your organization ID (optional, inferred from API key). */
47
- organizationId?: string;
48
- /** Default session type. Defaults to `'enrollment'`. */
49
- sessionType?: SessionType;
50
- /** Identity ID for authentication sessions. */
66
+ /**
67
+ * Override the API endpoint. Defaults to `https://api.usesense.ai/v1`.
68
+ * Only set this if you're pointing at a staging or self-hosted
69
+ * proxy. Maps to `apiEndpoint` on iOS and `baseUrl` on Android
70
+ * under the hood.
71
+ */
72
+ apiEndpoint?: string;
73
+ /** SDK-level branding overrides. */
74
+ branding?: BrandingConfig;
75
+ /**
76
+ * Opt in to the on-device antispoof classifier (v4.2). When enabled the
77
+ * native layer runs the bundled CelebA-Spoof model during capture and
78
+ * attaches per-frame spoof probabilities to the uploaded metadata. When
79
+ * disabled (default), the watchtower backend runs the classifier server-side.
80
+ * See `docs/sdk-specs/antispoof-classifier-sdk-spec.md` in the watchtower
81
+ * repo for the rollout plan and crosscheck policy.
82
+ */
83
+ antispoofOnDeviceEnabled?: boolean;
84
+
85
+ /**
86
+ * Opt the session into the LiveSense v4 capture flow. When true the native
87
+ * layer:
88
+ * - sends `x-usesense-sdk-version: v4` on session creation
89
+ * - inserts a constitutive zoom-motion phase between baseline and the
90
+ * active challenge (additive, not a replacement)
91
+ * - tags every captured frame with its capture phase
92
+ * The org must also have `livesense_v4_enabled` set in its features map
93
+ * server-side.
94
+ */
95
+ liveSenseV4Enabled?: boolean;
96
+ }
97
+
98
+ /**
99
+ * Request to start a verification session.
100
+ */
101
+ export interface VerificationRequest {
102
+ sessionType: SessionType;
103
+ /** Your internal user identifier (optional, stored in session metadata). */
104
+ externalUserId?: string;
105
+ /**
106
+ * Required for `authentication` sessions. The previously-enrolled
107
+ * identity you're verifying against.
108
+ */
51
109
  identityId?: string;
52
- /** Challenge difficulty policy. Defaults to `'standard'`. */
53
- challengePolicy?: ChallengePolicy;
54
- /** Enable audio capture for voice deepfake detection. Defaults to `false`. */
55
- enableAudio?: boolean;
56
- /** Maximum session duration in milliseconds. Defaults to `60000`. */
57
- timeout?: number;
58
110
  /** Custom key-value pairs attached to the session. */
59
- metadata?: Record<string, string>;
111
+ metadata?: Record<string, string | number | boolean>;
60
112
  }
61
113
 
62
114
  /**
63
- * Result returned when a verification session completes.
115
+ * Redacted decision object returned when a session completes.
64
116
  *
65
- * **Important:** This result is for UI feedback only.
66
- * The definitive verdict arrives at your backend via HMAC-signed webhook.
117
+ * This is the ONLY shape the plugin exposes to React Native. Pillar
118
+ * scores (channel trust, liveness, matchsense risk) are intentionally
119
+ * not included — the native SDKs strip those fields before returning
120
+ * across the bridge to prevent reverse-engineering of the server-side
121
+ * scoring logic. If you need full scoring details, consume the signed
122
+ * webhook delivered to your backend.
67
123
  */
68
124
  export interface UseSenseResult {
69
- /** Unique session identifier. */
70
125
  sessionId: string;
71
- /** Verification decision. */
72
- decision: UseSenseDecision;
73
- /** DeepSense channel trust score (0-100). */
74
- channelTrustScore: number;
75
- /** LiveSense liveness score (0-100). */
76
- livenessScore: number;
77
- /** MatchSense risk score (0-100). Lower is better. */
78
- matchSenseRiskScore: number;
79
- /** Fused presence confidence score (0-100). */
80
- presenceConfidence: number;
81
- /** Reasons contributing to the decision. */
82
- reasons: string[];
83
- /** Rule that triggered the decision, if any. */
84
- ruleTriggered?: string;
85
- /** Recommended action for the integrator. */
86
- recommendedAction?: string;
87
- /** Cryptographic signature for result verification. */
88
- sessionSignature: string;
126
+ sessionType: string | null;
127
+ identityId: string | null;
128
+ decision: Decision;
129
+ timestamp: string;
130
+ /** Convenience: `decision === 'APPROVE'`. */
131
+ isApproved: boolean;
132
+ /** Convenience: `decision === 'REJECT'`. */
133
+ isRejected: boolean;
134
+ /** Convenience: `decision === 'MANUAL_REVIEW'`. */
135
+ isPendingReview: boolean;
89
136
  }
90
137
 
91
138
  /**
92
- * Error returned when a verification session fails.
139
+ * Error thrown when a verification session fails.
140
+ *
141
+ * Matches the native SDKs' error shape: machine-readable code,
142
+ * human-readable message, retry hint.
93
143
  */
94
144
  export interface UseSenseError {
95
- /** Machine-readable error code. */
145
+ /**
146
+ * Machine-readable error code. One of
147
+ * `CAMERA_UNAVAILABLE`, `CAMERA_PERMISSION_DENIED`,
148
+ * `MIC_PERMISSION_DENIED`, `NETWORK_ERROR`, `NETWORK_TIMEOUT`,
149
+ * `SESSION_EXPIRED`, `UNAUTHORIZED`, `INVALID_TOKEN`,
150
+ * `SESSION_NOT_FOUND`, `IDENTITY_NOT_FOUND`, `INVALID_REQUEST`,
151
+ * `INVALID_CONFIG`, `QUOTA_EXCEEDED`, `USER_CANCELLED`,
152
+ * `session_cancelled`, `sdk_not_initialized`, `no_view_controller`,
153
+ * `CAPTURE_FAILED`, `ENCODING_FAILED`, `UPLOAD_FAILED`,
154
+ * `FACE_NOT_DETECTED`, `LOW_LIGHT`, `TIMEOUT`, `SERVER_ERROR`,
155
+ * `SERVICE_UNAVAILABLE`, `TOKEN_EXPIRED`, `TOKEN_ALREADY_USED`,
156
+ * `INSUFFICIENT_CREDITS`, `NONCE_MISMATCH`, `UNKNOWN_ERROR`.
157
+ */
96
158
  code: string;
97
159
  /** Human-readable error message. */
98
160
  message: string;
99
- /** Additional error details. */
100
- details?: Record<string, any>;
161
+ /** Whether the operation can be retried. */
162
+ isRetryable?: boolean;
163
+ /** Server-specific error code, if any. */
164
+ serverCode?: string;
101
165
  }
102
166
 
103
167
  /**
104
- * Events emitted during the verification session lifecycle.
168
+ * Lifecycle event types emitted by the native SDK during a
169
+ * verification session.
105
170
  */
106
- export type UseSenseEvent =
107
- | { type: 'session_started'; sessionId: string }
108
- | { type: 'challenge_presented'; challengeType: string }
109
- | { type: 'challenge_completed'; challengeType: string }
110
- | { type: 'processing'; stage: ProcessingStage }
111
- | { type: 'session_completed'; result: UseSenseResult }
112
- | { type: 'session_error'; error: UseSenseError };
113
-
114
- /** Options for starting a verification session. */
115
- export interface StartSessionOptions {
116
- /** Session type. Overrides the value set in {@link UseSenseConfig}. */
117
- sessionType?: SessionType;
118
- /** Identity ID for authentication sessions. */
119
- identityId?: string;
120
- }
171
+ export type UseSenseEventType =
172
+ | 'SESSION_CREATED'
173
+ | 'PERMISSIONS_REQUESTED'
174
+ | 'PERMISSIONS_GRANTED'
175
+ | 'PERMISSIONS_DENIED'
176
+ | 'CAPTURE_STARTED'
177
+ | 'FRAME_CAPTURED'
178
+ | 'CAPTURE_COMPLETED'
179
+ | 'AUDIO_RECORD_STARTED'
180
+ | 'AUDIO_RECORD_COMPLETED'
181
+ | 'CHALLENGE_STARTED'
182
+ | 'CHALLENGE_COMPLETED'
183
+ | 'UPLOAD_STARTED'
184
+ | 'UPLOAD_PROGRESS'
185
+ | 'UPLOAD_COMPLETED'
186
+ | 'COMPLETE_STARTED'
187
+ | 'DECISION_RECEIVED'
188
+ | 'IMAGE_QUALITY_CHECK'
189
+ | 'ERROR'
190
+ | 'UNKNOWN';
121
191
 
122
- /** Status of a verification session. */
123
- export interface SessionStatus {
124
- /** Current session status. */
125
- status: string;
126
- /** Result, if the session has completed. */
127
- result?: UseSenseResult;
192
+ /** Single event emitted during a session. */
193
+ export interface UseSenseEvent {
194
+ type: UseSenseEventType;
195
+ /** Unix epoch milliseconds. */
196
+ timestamp: number;
197
+ /** Event-specific payload. Opaque strings; never contains scoring data. */
198
+ data?: Record<string, string>;
128
199
  }
129
200
 
130
- /** Subscription handle returned by {@link UseSense.addListener}. */
201
+ /** Handle returned by `UseSense.addListener`. */
131
202
  export interface UseSenseSubscription {
132
- /** Remove the event listener. */
133
- remove: () => void;
203
+ remove(): void;
134
204
  }
135
205
 
136
- // ─── Implementation ──────────────────────────────────────────────────
206
+ // ─── Implementation ──────────────────────────────────────────────────────
137
207
 
138
208
  const eventEmitter = new NativeEventEmitter(UseSenseModule);
139
209
 
140
210
  /**
141
211
  * UseSense React Native plugin.
142
212
  *
143
- * Provides human presence verification by wrapping the native UseSense
144
- * iOS and Android SDKs into a single cross-platform API.
213
+ * Human presence verification wrapping the native iOS and Android
214
+ * UseSense SDKs. Every session runs inside a full-screen native
215
+ * camera activity; the plugin is a thin bridge that forwards
216
+ * initialization, verification requests, and lifecycle events to
217
+ * the platform SDK and returns a redacted decision object.
145
218
  *
146
219
  * @example
147
220
  * ```ts
148
221
  * import { UseSense } from 'react-native-usesense';
149
222
  *
150
- * await UseSense.initialize({ apiKey: 'your_key', environment: 'sandbox' });
151
- * const result = await UseSense.startSession({ sessionType: 'enrollment' });
152
- * console.log(result.decision);
223
+ * await UseSense.initialize({
224
+ * apiKey: 'sk_sandbox_...',
225
+ * environment: 'sandbox',
226
+ * });
227
+ *
228
+ * const unsubscribe = UseSense.addListener((event) => {
229
+ * console.log(event.type, event.data);
230
+ * });
231
+ *
232
+ * try {
233
+ * const result = await UseSense.startVerification({
234
+ * sessionType: 'enrollment',
235
+ * });
236
+ * console.log(result.decision); // 'APPROVE' | 'REJECT' | 'MANUAL_REVIEW'
237
+ * } finally {
238
+ * unsubscribe.remove();
239
+ * }
153
240
  * ```
154
241
  */
155
242
  export class UseSense {
156
243
  /**
157
- * Initialize the UseSense plugin. Must be called once before any other method.
244
+ * Initialize the UseSense plugin. Must be called before any other
245
+ * method. Calling again with a different API key or environment
246
+ * replaces the previous configuration.
158
247
  *
159
- * @param config - Plugin configuration including API key and environment.
160
- * @throws {UseSenseError} If the API key is invalid or configuration is malformed.
248
+ * @throws {UseSenseError} If the API key is empty or initialization
249
+ * fails on the native side.
161
250
  */
162
251
  static async initialize(config: UseSenseConfig): Promise<void> {
163
252
  if (!config.apiKey) {
164
- throw { code: 'invalid_config', message: 'apiKey is required' } as UseSenseError;
253
+ const err: UseSenseError = {
254
+ code: 'invalid_config',
255
+ message: 'apiKey is required',
256
+ isRetryable: false,
257
+ };
258
+ throw err;
165
259
  }
166
260
  return UseSenseModule.initialize({
167
261
  apiKey: config.apiKey,
168
- environment: config.environment ?? 'sandbox',
169
- organizationId: config.organizationId,
170
- sessionType: config.sessionType ?? 'enrollment',
171
- identityId: config.identityId,
172
- challengePolicy: config.challengePolicy ?? 'standard',
173
- enableAudio: config.enableAudio ?? false,
174
- timeout: config.timeout ?? 60000,
175
- metadata: config.metadata,
262
+ environment: config.environment ?? 'auto',
263
+ apiEndpoint: config.apiEndpoint,
264
+ branding: config.branding,
265
+ antispoofOnDeviceEnabled: config.antispoofOnDeviceEnabled ?? false,
266
+ liveSenseV4Enabled: config.liveSenseV4Enabled ?? false,
176
267
  });
177
268
  }
178
269
 
179
270
  /**
180
- * Start a verification session. Presents a full-screen native camera UI.
181
- *
182
- * The returned promise resolves when the session completes and the modal
183
- * dismisses, or rejects if an error occurs.
184
- *
185
- * @param options - Optional overrides for session type and identity ID.
186
- * @returns The verification result (for UI feedback only).
187
- * @throws {UseSenseError} On failure or cancellation.
271
+ * Start a verification session. Presents a full-screen native
272
+ * camera activity. The returned promise resolves when the session
273
+ * completes or rejects with a `UseSenseError` (including the
274
+ * `USER_CANCELLED` / `session_cancelled` cases when the user
275
+ * dismisses the camera screen).
188
276
  */
189
- static async startSession(options?: StartSessionOptions): Promise<UseSenseResult> {
190
- return UseSenseModule.startSession(options ?? {});
277
+ static async startVerification(
278
+ request: VerificationRequest,
279
+ ): Promise<UseSenseResult> {
280
+ return UseSenseModule.startVerification({
281
+ sessionType: request.sessionType,
282
+ externalUserId: request.externalUserId,
283
+ identityId: request.identityId,
284
+ metadata: request.metadata,
285
+ });
191
286
  }
192
287
 
193
288
  /**
194
- * Cancel an in-progress verification session.
289
+ * Subscribe to real-time lifecycle events emitted by the native
290
+ * SDK during a verification session. Call this **before**
291
+ * `startVerification` so the subscription captures the earliest
292
+ * events (`SESSION_CREATED`, `PERMISSIONS_REQUESTED`).
195
293
  *
196
- * The camera UI is dismissed and the `startSession` promise rejects
197
- * with a `session_cancelled` error.
294
+ * Returns a subscription handle; call `.remove()` to stop
295
+ * receiving events.
198
296
  */
199
- static async cancelSession(): Promise<void> {
200
- return UseSenseModule.cancelSession();
297
+ static addListener(
298
+ callback: (event: UseSenseEvent) => void,
299
+ ): UseSenseSubscription {
300
+ const subscription = eventEmitter.addListener(
301
+ 'UseSenseEvent',
302
+ (raw: UseSenseEvent) => {
303
+ callback(raw);
304
+ },
305
+ );
306
+ return {
307
+ remove: () => subscription.remove(),
308
+ };
201
309
  }
202
310
 
203
311
  /**
204
- * Get the status of a verification session by ID.
205
- *
206
- * @param sessionId - The session ID to query.
207
- * @returns The current status and optional result.
312
+ * Clear all event listeners and release native resources. Does not
313
+ * need to be called explicitly during normal usage; the plugin
314
+ * cleans up on process teardown. Useful when you want to re-
315
+ * initialize against a different API key or during test teardown.
208
316
  */
209
- static async getSessionStatus(sessionId: string): Promise<SessionStatus> {
210
- return UseSenseModule.getSessionStatus(sessionId);
317
+ static async reset(): Promise<void> {
318
+ return UseSenseModule.reset();
211
319
  }
212
320
 
213
321
  /**
214
- * Subscribe to real-time events during the verification lifecycle.
215
- *
216
- * Call this before `startSession()` to receive all events.
217
- *
218
- * @param callback - Function invoked for each event.
219
- * @returns A subscription handle with a `remove()` method.
220
- *
221
- * @example
222
- * ```ts
223
- * const listener = UseSense.addListener((event) => {
224
- * console.log(event.type);
225
- * });
226
- * // Later:
227
- * listener.remove();
228
- * ```
322
+ * Whether the native plugin has an active `UseSense` client. Set
323
+ * to `true` after a successful `initialize` and `false` after
324
+ * `reset`. Useful for feature-flagging UI that should only show
325
+ * when the SDK is ready.
229
326
  */
230
- static addListener(callback: (event: UseSenseEvent) => void): UseSenseSubscription {
231
- UseSenseModule.subscribeToEvents();
232
- const subscription = eventEmitter.addListener('UseSenseEvent', callback);
233
- return {
234
- remove: () => {
235
- subscription.remove();
236
- UseSenseModule.unsubscribeFromEvents();
237
- },
238
- };
327
+ static async isInitialized(): Promise<boolean> {
328
+ return UseSenseModule.isInitialized();
239
329
  }
240
330
 
331
+ // ─── LiveSense v4 ─────────────────────────────────────────────────────
332
+ // Phase 1 ticket R-1.
333
+ //
334
+ // v4 runs the perspective-distortion zoom-motion capture. Passthrough
335
+ // to the native SDK's startV4Session (iOS) / startV4Verification
336
+ // (Android). The returned promise resolves with the opaque verdict;
337
+ // sub-scores and pillar verdicts are never exposed.
338
+
241
339
  /**
242
- * Get the native SDK version string.
340
+ * Start a LiveSense v4 session.
243
341
  *
244
- * @returns Version string in semver format (e.g. `"1.0.0"`).
342
+ * The session must already have been created on your backend and the
343
+ * session_token + nonce forwarded to the client. The native SDK drives
344
+ * the camera, signs the frame hash chain with the platform-attested
345
+ * key, uploads, and returns the opaque verdict.
245
346
  */
246
- static getSdkVersion(): string {
247
- return UseSenseModule.getSdkVersion();
347
+ static async startV4Verification(
348
+ request: V4VerificationRequest,
349
+ ): Promise<V4Verdict> {
350
+ return UseSenseModule.startV4Verification({
351
+ sessionId: request.sessionId,
352
+ sessionToken: request.sessionToken,
353
+ nonce: request.nonce,
354
+ apiBaseUrl: request.apiBaseUrl,
355
+ environment: request.environment ?? 'production',
356
+ displayName: request.displayName,
357
+ brandPrimaryColor: request.brandPrimaryColor,
358
+ });
248
359
  }
249
360
  }
250
361
 
362
+ // ─── LiveSense v4 types (R-1) ─────────────────────────────────────────────
363
+
364
+ export interface V4VerificationRequest {
365
+ sessionId: string;
366
+ sessionToken: string;
367
+ nonce: string;
368
+ apiBaseUrl: string;
369
+ environment?: UseSenseEnvironment;
370
+ displayName?: string;
371
+ brandPrimaryColor?: string;
372
+ }
373
+
374
+ export type V4Decision = 'pass' | 'fail' | 'review';
375
+ export type V4Confidence = 'high' | 'medium' | 'low';
376
+ export type V4AssuranceLevel = 'mobile_hardware' | 'web_attested' | 'web_unattested';
377
+ export type V4CaptureChannel = 'ios' | 'android' | 'rn' | 'flutter' | 'web';
378
+
379
+ /**
380
+ * Opaque verdict from POST /v1/sessions/:id/result. Matches the server
381
+ * contract exactly; no sub-scores leak through.
382
+ */
383
+ export interface V4Verdict {
384
+ session_id: string;
385
+ verdict: V4Decision;
386
+ confidence: V4Confidence;
387
+ assurance_level_achieved: V4AssuranceLevel;
388
+ capture_channel: V4CaptureChannel;
389
+ match_sense_embedding_id: string | null;
390
+ timestamp: string;
391
+ }
392
+
251
393
  export default UseSense;
394
+
395
+ // Slice 5d: Flows. A parallel surface to Sessions — host apps choose one
396
+ // per call. See guides/flows/sessions-vs-flows in the API docs.
397
+ export {
398
+ UseSenseFlows,
399
+ FlowError,
400
+ type FlowErrorCode,
401
+ type FlowOutcome,
402
+ type FlowRunResult,
403
+ type FlowRunState,
404
+ type RunFlowOptions,
405
+ } from './flows';