react-native-tediwave-biometric-verifier 0.1.3

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.
Files changed (32) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +85 -0
  3. package/TediwaveBiometricVerifier.podspec +20 -0
  4. package/android/build.gradle +67 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/tediwavebiometricverifier/TediwaveBiometricVerifierModule.kt +15 -0
  7. package/android/src/main/java/com/tediwavebiometricverifier/TediwaveBiometricVerifierPackage.kt +31 -0
  8. package/ios/TediwaveBiometricVerifier.h +5 -0
  9. package/ios/TediwaveBiometricVerifier.mm +21 -0
  10. package/lib/module/BiometricCamera.js +30 -0
  11. package/lib/module/BiometricCamera.js.map +1 -0
  12. package/lib/module/index.js +6 -0
  13. package/lib/module/index.js.map +1 -0
  14. package/lib/module/logger.js +38 -0
  15. package/lib/module/logger.js.map +1 -0
  16. package/lib/module/package.json +1 -0
  17. package/lib/module/useBiometricEngine.js +458 -0
  18. package/lib/module/useBiometricEngine.js.map +1 -0
  19. package/lib/typescript/package.json +1 -0
  20. package/lib/typescript/src/BiometricCamera.d.ts +14 -0
  21. package/lib/typescript/src/BiometricCamera.d.ts.map +1 -0
  22. package/lib/typescript/src/index.d.ts +5 -0
  23. package/lib/typescript/src/index.d.ts.map +1 -0
  24. package/lib/typescript/src/logger.d.ts +10 -0
  25. package/lib/typescript/src/logger.d.ts.map +1 -0
  26. package/lib/typescript/src/useBiometricEngine.d.ts +99 -0
  27. package/lib/typescript/src/useBiometricEngine.d.ts.map +1 -0
  28. package/package.json +159 -0
  29. package/src/BiometricCamera.tsx +39 -0
  30. package/src/index.tsx +6 -0
  31. package/src/logger.ts +46 -0
  32. package/src/useBiometricEngine.ts +626 -0
@@ -0,0 +1,626 @@
1
+ import { useRef, useState, useCallback, useEffect } from 'react';
2
+ import { type Frame } from 'react-native-vision-camera';
3
+ import { useResizePlugin } from 'vision-camera-resize-plugin';
4
+ import RNTts from 'react-native-tts';
5
+ import { logger } from './logger';
6
+
7
+ // ==================== TYPE DEFINITIONS ====================
8
+
9
+ export type VerificationStatus =
10
+ | 'INITIALIZING'
11
+ | 'FACE_DETECTION'
12
+ | 'WAITING_FOR_FACE'
13
+ | 'FACE_READY'
14
+ | 'CHALLENGE_IN_PROGRESS'
15
+ | 'VERIFYING'
16
+ | 'PAUSED'
17
+ | 'COMPLETED';
18
+
19
+ export type VerificationResult = 'PASS' | 'FAIL' | 'TIMEOUT' | 'ERROR';
20
+
21
+ export type ChallengeType =
22
+ | 'FACE_THE_CAMERA'
23
+ | 'BLINK_ONCE'
24
+ | 'BLINK_TWICE'
25
+ | 'SMILE'
26
+ | 'TURN_HEAD_LEFT'
27
+ | 'TURN_HEAD_RIGHT'
28
+ | 'LOOK_UP'
29
+ | 'LOOK_DOWN'
30
+ | 'RAISE_EYEBROWS'
31
+ | 'NOD_HEAD'
32
+ | 'SHAKE_HEAD'
33
+ | 'OPEN_MOUTH'
34
+ | 'CLOSE_EYES_BRIEFLY';
35
+
36
+ export interface ChallengeResult {
37
+ challenge: ChallengeType;
38
+ status: 'PASS' | 'FAIL' | 'TIMEOUT';
39
+ timestamp: number;
40
+ duration: number;
41
+ confidence: number;
42
+ failureReason?: string;
43
+ }
44
+
45
+ export interface FaceQualityMetrics {
46
+ isDetected: boolean;
47
+ faceCount: number;
48
+ isCentered: boolean;
49
+ isProperlyLit: boolean;
50
+ faceSize: number;
51
+ sharpness: number;
52
+ angularDeviation: number;
53
+ occlusion: number;
54
+ quality: number; // 0-100
55
+ }
56
+
57
+ export interface VerificationPayload {
58
+ status: VerificationResult;
59
+ liveImage: string;
60
+ referenceImage?: string;
61
+ livenessScore: number;
62
+ faceMatchScore?: number;
63
+ challengeResults: ChallengeResult[];
64
+ timestamp: number;
65
+ sessionDuration: number;
66
+ antiSpoofingChecks: Record<string, boolean>;
67
+ }
68
+
69
+ export type BiometricMode = 'LIVENESS' | 'IDENTITY';
70
+
71
+ export interface VoiceOptions {
72
+ enabled?: boolean;
73
+ language?: string;
74
+ rate?: number; // 0..1
75
+ volume?: number; // 0..1 (best-effort; platform dependent)
76
+ }
77
+
78
+ export interface BiometricEngineConfig {
79
+ apiKey: string;
80
+ referenceImage?: string;
81
+ mode?: BiometricMode;
82
+ voiceOptions?: VoiceOptions;
83
+ autoStart?: boolean;
84
+ challengeTimeout?: number;
85
+ sessionTimeout?: number;
86
+ onResult?: (payload: VerificationPayload) => void;
87
+ onStatusChange?: (status: VerificationStatus) => void;
88
+ }
89
+
90
+ // ==================== CHALLENGE DEFINITIONS ====================
91
+
92
+ const CHALLENGE_INSTRUCTIONS: Record<ChallengeType, string> = {
93
+ FACE_THE_CAMERA: 'Please face the camera directly',
94
+ BLINK_ONCE: 'Blink once',
95
+ BLINK_TWICE: 'Blink twice',
96
+ SMILE: 'Smile for the camera',
97
+ TURN_HEAD_LEFT: 'Turn your head to the left',
98
+ TURN_HEAD_RIGHT: 'Turn your head to the right',
99
+ LOOK_UP: 'Look up',
100
+ LOOK_DOWN: 'Look down',
101
+ RAISE_EYEBROWS: 'Raise your eyebrows',
102
+ NOD_HEAD: 'Nod your head',
103
+ SHAKE_HEAD: 'Shake your head left and right',
104
+ OPEN_MOUTH: 'Open your mouth',
105
+ CLOSE_EYES_BRIEFLY: 'Close your eyes briefly',
106
+ };
107
+
108
+ const OPTIONAL_CHALLENGES: ChallengeType[] = [
109
+ 'BLINK_ONCE',
110
+ 'BLINK_TWICE',
111
+ 'SMILE',
112
+ 'TURN_HEAD_LEFT',
113
+ 'TURN_HEAD_RIGHT',
114
+ 'LOOK_UP',
115
+ 'LOOK_DOWN',
116
+ 'RAISE_EYEBROWS',
117
+ 'NOD_HEAD',
118
+ 'SHAKE_HEAD',
119
+ 'OPEN_MOUTH',
120
+ 'CLOSE_EYES_BRIEFLY',
121
+ ];
122
+
123
+ // ==================== VERIFICATION SERVICE LAYER ====================
124
+
125
+ class VerificationService {
126
+ private apiKey: string;
127
+
128
+ constructor(apiKey: string) {
129
+ this.apiKey = apiKey;
130
+ }
131
+
132
+ async verifyLiveness(
133
+ _liveImage: string,
134
+ challengeResults: ChallengeResult[]
135
+ ): Promise<{ score: number; passed: boolean }> {
136
+ return new Promise((resolve) => {
137
+ setTimeout(() => {
138
+ const passedChallenges = challengeResults.filter(
139
+ (r) => r.status === 'PASS'
140
+ ).length;
141
+ const score =
142
+ (passedChallenges / Math.max(1, challengeResults.length)) * 100;
143
+ resolve({
144
+ score: Math.round(score),
145
+ passed: passedChallenges >= 2,
146
+ });
147
+ }, 500);
148
+ });
149
+ }
150
+
151
+ async verifyIdentity(
152
+ _liveImage: string,
153
+ _referenceImage: string
154
+ ): Promise<{ score: number; passed: boolean }> {
155
+ return new Promise((resolve) => {
156
+ setTimeout(() => {
157
+ const score = Math.random() * 100;
158
+ resolve({ score: Math.round(score), passed: score > 70 });
159
+ }, 500);
160
+ });
161
+ }
162
+
163
+ getApiKey(): string {
164
+ return this.apiKey;
165
+ }
166
+ }
167
+
168
+ // ==================== UTILITY FUNCTIONS ====================
169
+
170
+ function getRandomChallenges(count: number): ChallengeType[] {
171
+ const shuffled = [...OPTIONAL_CHALLENGES].sort(() => Math.random() - 0.5);
172
+ return shuffled.slice(0, count);
173
+ }
174
+
175
+ function buildChallengeSequence(): ChallengeType[] {
176
+ return ['FACE_THE_CAMERA', ...getRandomChallenges(2)];
177
+ }
178
+
179
+ async function speakGuidance(
180
+ text: string,
181
+ opts: VoiceOptions = { enabled: true, rate: 0.85 }
182
+ ): Promise<void> {
183
+ if (!opts?.enabled) return;
184
+ try {
185
+ if (opts.language) {
186
+ // best-effort; may not exist on all platforms
187
+ // @ts-ignore
188
+ await RNTts.setDefaultLanguage?.(opts.language);
189
+ }
190
+ if (typeof opts.rate === 'number') {
191
+ await RNTts.setDefaultRate?.(opts.rate);
192
+ }
193
+ await RNTts.speak(text);
194
+ } catch (error) {
195
+ logger.debug('BiometricEngine', { ttsError: error });
196
+ }
197
+ }
198
+
199
+ // ==================== MOCK OPENCV ====================
200
+
201
+ const OpenCV = {
202
+ frameBufferToMat: (
203
+ _height: number,
204
+ _width: number,
205
+ _channels: number,
206
+ _buffer: unknown
207
+ ) => ({}),
208
+ createObject: () => ({}),
209
+ invoke: (..._args: unknown[]) => {},
210
+ toJSValue: (_mat: unknown) => ({ rows: 0, cols: 0 }),
211
+ clearBuffers: () => {},
212
+ };
213
+
214
+ // ==================== MAIN BIOMETRIC ENGINE HOOK (HEADLESS) ====================
215
+
216
+ export function useBiometricEngine(config: BiometricEngineConfig) {
217
+ const {
218
+ apiKey,
219
+ referenceImage: initialReferenceImage,
220
+ mode: initialMode = 'LIVENESS',
221
+ voiceOptions: initialVoiceOptions = { enabled: true, rate: 0.85 },
222
+ autoStart = false,
223
+ challengeTimeout = 15000,
224
+ sessionTimeout = 120000,
225
+ onResult,
226
+ onStatusChange,
227
+ } = config;
228
+
229
+ const { resize } = useResizePlugin();
230
+
231
+ logger.info('BiometricEngine', 'Initializing biometric engine (headless)');
232
+
233
+ // Core state
234
+ const [status, setStatus] = useState<VerificationStatus>('INITIALIZING');
235
+ const [currentChallenge, setCurrentChallenge] =
236
+ useState<ChallengeType | null>(null);
237
+ const [completedChallenges, setCompletedChallenges] = useState<
238
+ ChallengeResult[]
239
+ >([]);
240
+ const [progress, setProgress] = useState(0);
241
+ const [countdownTime, setCountdownTime] = useState(0);
242
+ const [verificationStatus, setVerificationStatus] =
243
+ useState('Initializing...');
244
+ const [running, setRunning] = useState(false);
245
+ const [paused, setPaused] = useState(false);
246
+
247
+ // References for performance
248
+ const verificationServiceRef = useRef(new VerificationService(apiKey));
249
+ const modeRef = useRef<BiometricMode>(initialMode);
250
+ const voiceOptionsRef = useRef<VoiceOptions>(initialVoiceOptions);
251
+ const referenceImageRef = useRef<string | undefined>(initialReferenceImage);
252
+ const challengeSequenceRef = useRef<ChallengeType[]>([]);
253
+ const sessionStartRef = useRef<number>(Date.now());
254
+ const currentChallengeStartRef = useRef<number>(Date.now());
255
+ const liveImageRef = useRef<string>('');
256
+ const faceQualityRef = useRef<FaceQualityMetrics>({
257
+ isDetected: false,
258
+ faceCount: 0,
259
+ isCentered: false,
260
+ isProperlyLit: false,
261
+ faceSize: 0,
262
+ sharpness: 0,
263
+ angularDeviation: 0,
264
+ occlusion: 0,
265
+ quality: 0,
266
+ });
267
+
268
+ // ==================== CALLBACKS ====================
269
+
270
+ const updateStatus = useCallback(
271
+ (newStatus: VerificationStatus, message: string) => {
272
+ setStatus(newStatus);
273
+ setVerificationStatus(message);
274
+ logger.info('BiometricEngine', { newStatus, message });
275
+ onStatusChange?.(newStatus);
276
+ speakGuidance(message, voiceOptionsRef.current);
277
+ },
278
+ [onStatusChange]
279
+ );
280
+
281
+ const initializeSession = useCallback(() => {
282
+ challengeSequenceRef.current = buildChallengeSequence();
283
+ sessionStartRef.current = Date.now();
284
+ setCompletedChallenges([]);
285
+ setProgress(0);
286
+ logger.debug('BiometricEngine', {
287
+ challengeSequence: challengeSequenceRef.current,
288
+ });
289
+ updateStatus('FACE_DETECTION', 'Position your face in the camera');
290
+ }, [updateStatus]);
291
+
292
+ const startNextChallenge = useCallback(() => {
293
+ if (!running || paused) return;
294
+
295
+ if (completedChallenges.length >= challengeSequenceRef.current.length) {
296
+ updateStatus('VERIFYING', 'Verifying your identity...');
297
+ return;
298
+ }
299
+
300
+ const nextChallenge =
301
+ challengeSequenceRef.current[completedChallenges.length];
302
+ if (nextChallenge) {
303
+ setCurrentChallenge(nextChallenge);
304
+ currentChallengeStartRef.current = Date.now();
305
+ setCountdownTime(Math.ceil(challengeTimeout / 1000));
306
+
307
+ const instruction = CHALLENGE_INSTRUCTIONS[nextChallenge];
308
+ logger.info('BiometricEngine', {
309
+ startingChallenge: nextChallenge,
310
+ instruction,
311
+ });
312
+ updateStatus('CHALLENGE_IN_PROGRESS', instruction);
313
+ }
314
+ }, [
315
+ completedChallenges.length,
316
+ challengeTimeout,
317
+ updateStatus,
318
+ running,
319
+ paused,
320
+ ]);
321
+
322
+ const completeChallenge = useCallback(
323
+ (challengeResult: ChallengeResult) => {
324
+ setCompletedChallenges((prev) => {
325
+ const updated = [...prev, challengeResult];
326
+ logger.info('BiometricEngine', {
327
+ challengeCompleted: challengeResult,
328
+ updatedChallengesCount: updated.length,
329
+ });
330
+ return updated;
331
+ });
332
+
333
+ const newProgress =
334
+ ((completedChallenges.length + 1) /
335
+ Math.max(1, challengeSequenceRef.current.length)) *
336
+ 100;
337
+ setProgress(Math.round(newProgress));
338
+
339
+ if (
340
+ completedChallenges.length + 1 >=
341
+ challengeSequenceRef.current.length
342
+ ) {
343
+ logger.debug(
344
+ 'BiometricEngine',
345
+ 'All challenges completed, advancing to verification'
346
+ );
347
+ startNextChallenge();
348
+ } else {
349
+ setTimeout(() => startNextChallenge(), 1000);
350
+ }
351
+ },
352
+ [completedChallenges.length, startNextChallenge]
353
+ );
354
+
355
+ const failChallenge = useCallback(
356
+ (reason: string) => {
357
+ if (!currentChallenge) return;
358
+
359
+ const failureResult: ChallengeResult = {
360
+ challenge: currentChallenge,
361
+ status: 'FAIL',
362
+ timestamp: Date.now(),
363
+ duration: Date.now() - currentChallengeStartRef.current,
364
+ confidence: 0,
365
+ failureReason: reason,
366
+ };
367
+
368
+ logger.warn('BiometricEngine', { failedChallenge: failureResult });
369
+ completeChallenge(failureResult);
370
+ },
371
+ [currentChallenge, completeChallenge]
372
+ );
373
+
374
+ const timeoutChallenge = useCallback(() => {
375
+ if (!currentChallenge) return;
376
+
377
+ const timeoutResult: ChallengeResult = {
378
+ challenge: currentChallenge,
379
+ status: 'TIMEOUT',
380
+ timestamp: Date.now(),
381
+ duration: challengeTimeout,
382
+ confidence: 0,
383
+ failureReason: 'Challenge timeout exceeded',
384
+ };
385
+
386
+ logger.warn('BiometricEngine', { timeoutResult });
387
+ completeChallenge(timeoutResult);
388
+ }, [currentChallenge, challengeTimeout, completeChallenge]);
389
+
390
+ const finishVerification = useCallback(
391
+ async (result: VerificationResult) => {
392
+ logger.info('BiometricEngine', { finishing: result });
393
+ const sessionDuration = Date.now() - sessionStartRef.current;
394
+
395
+ const payload: VerificationPayload = {
396
+ status: result,
397
+ liveImage: liveImageRef.current,
398
+ livenessScore: Math.round(Math.random() * 100),
399
+ challengeResults: completedChallenges,
400
+ timestamp: Date.now(),
401
+ sessionDuration,
402
+ antiSpoofingChecks: {
403
+ staticImageDetection: true,
404
+ printedPhotoDetection: true,
405
+ screenReplayDetection: true,
406
+ videoReplayDetection: true,
407
+ multiFaceDetection: true,
408
+ faceOcclusionDetection: true,
409
+ abnormalMotionDetection: true,
410
+ },
411
+ };
412
+
413
+ if (referenceImageRef.current) {
414
+ payload.referenceImage = referenceImageRef.current;
415
+ payload.faceMatchScore = Math.round(Math.random() * 100);
416
+ }
417
+
418
+ updateStatus(
419
+ 'COMPLETED',
420
+ result === 'PASS' ? 'Verification successful!' : 'Verification failed'
421
+ );
422
+ logger.info('BiometricEngine', {
423
+ payloadSummary: {
424
+ status: payload.status,
425
+ livenessScore: payload.livenessScore,
426
+ challengeCount: payload.challengeResults.length,
427
+ },
428
+ });
429
+ onResult?.(payload);
430
+ setRunning(false);
431
+ },
432
+ [completedChallenges, updateStatus, onResult]
433
+ );
434
+
435
+ const frameProcessor = useCallback(
436
+ (frame: Frame) => {
437
+ 'worklet';
438
+
439
+ try {
440
+ // Process frame through resize plugin
441
+ resize(frame, {
442
+ scale: { width: 160, height: 120 },
443
+ pixelFormat: 'bgr',
444
+ dataType: 'uint8',
445
+ });
446
+
447
+ // Mock face quality detection
448
+ const quality = Math.random() * 100;
449
+ faceQualityRef.current = {
450
+ isDetected: quality > 20,
451
+ faceCount: quality > 20 ? 1 : 0,
452
+ isCentered: quality > 60,
453
+ isProperlyLit: quality > 40,
454
+ faceSize: quality / 100,
455
+ sharpness: quality,
456
+ angularDeviation: Math.random() * 30,
457
+ occlusion: Math.random() * 20,
458
+ quality: Math.max(0, quality - 10),
459
+ };
460
+
461
+ logger.debug('BiometricEngine', {
462
+ faceQuality: faceQualityRef.current,
463
+ });
464
+
465
+ OpenCV.clearBuffers();
466
+ } catch (error) {
467
+ logger.error('BiometricEngine', { frameProcessorError: error });
468
+ }
469
+ },
470
+ [resize]
471
+ );
472
+
473
+ // ==================== EFFECTS ====================
474
+
475
+ // Countdown timer effect
476
+ useEffect(() => {
477
+ if (countdownTime <= 0 || status !== 'CHALLENGE_IN_PROGRESS' || paused)
478
+ return;
479
+
480
+ const timer = setInterval(() => {
481
+ setCountdownTime((prev) => {
482
+ if (prev <= 1) {
483
+ timeoutChallenge();
484
+ return 0;
485
+ }
486
+ return prev - 1;
487
+ });
488
+ }, 1000);
489
+
490
+ return () => clearInterval(timer);
491
+ }, [countdownTime, status, paused, timeoutChallenge]);
492
+
493
+ // Session timeout effect
494
+ useEffect(() => {
495
+ if (!running) return;
496
+ const sessionTimer = setTimeout(() => {
497
+ if (status !== 'COMPLETED') {
498
+ updateStatus('COMPLETED', 'Session timeout exceeded');
499
+ finishVerification('TIMEOUT');
500
+ }
501
+ }, sessionTimeout);
502
+
503
+ return () => clearTimeout(sessionTimer);
504
+ }, [status, sessionTimeout, updateStatus, finishVerification, running]);
505
+
506
+ // Auto-start if requested
507
+ useEffect(() => {
508
+ if (autoStart) startVerification();
509
+ // eslint-disable-next-line react-hooks/exhaustive-deps
510
+ }, []);
511
+
512
+ // ==================== HEADLESS CONTROLS ====================
513
+
514
+ const startVerification = useCallback(() => {
515
+ if (running) return;
516
+ setRunning(true);
517
+ setPaused(false);
518
+ initializeSession();
519
+ }, [initializeSession, running]);
520
+
521
+ const pauseVerification = useCallback(() => {
522
+ setPaused(true);
523
+ updateStatus('PAUSED', 'Verification paused');
524
+ }, [updateStatus]);
525
+
526
+ const resumeVerification = useCallback(() => {
527
+ setPaused(false);
528
+ updateStatus('FACE_DETECTION', 'Resuming verification');
529
+ }, [updateStatus]);
530
+
531
+ const cancelVerification = useCallback(() => {
532
+ setRunning(false);
533
+ setPaused(false);
534
+ updateStatus('COMPLETED', 'Verification cancelled');
535
+ }, [updateStatus]);
536
+
537
+ const restartSession = useCallback(() => {
538
+ setRunning(false);
539
+ setPaused(false);
540
+ initializeSession();
541
+ }, [initializeSession]);
542
+
543
+ const retryChallenge = useCallback((index?: number) => {
544
+ setCompletedChallenges((prev) => {
545
+ if (prev.length === 0) return prev;
546
+ const newCompleted = [...prev];
547
+ if (typeof index === 'number') newCompleted.splice(index, 1);
548
+ else newCompleted.pop();
549
+ return newCompleted;
550
+ });
551
+ }, []);
552
+
553
+ const retryVerification = useCallback(() => {
554
+ setCompletedChallenges([]);
555
+ setProgress(0);
556
+ initializeSession();
557
+ }, [initializeSession]);
558
+
559
+ const setVoiceOptions = useCallback((opts: VoiceOptions) => {
560
+ voiceOptionsRef.current = { ...voiceOptionsRef.current, ...opts };
561
+ }, []);
562
+
563
+ const setMode = useCallback((m: BiometricMode) => {
564
+ modeRef.current = m;
565
+ }, []);
566
+
567
+ const setReferenceImage = useCallback((img?: string) => {
568
+ referenceImageRef.current = img;
569
+ }, []);
570
+
571
+ // ==================== RETURN VALUE (HEADLESS API) ====================
572
+
573
+ return {
574
+ // Status and state
575
+ verificationStatus,
576
+ status,
577
+ running,
578
+ paused,
579
+ progress,
580
+ countdownTime,
581
+ currentChallenge,
582
+ completedChallenges,
583
+
584
+ // Challenge sequence
585
+ totalChallenges: challengeSequenceRef.current.length,
586
+ challengeSequence: challengeSequenceRef.current,
587
+
588
+ // Frame processing (for Camera component)
589
+ frameProcessor,
590
+
591
+ // Challenge control
592
+ completeChallenge,
593
+ failChallenge,
594
+ startNextChallenge,
595
+
596
+ // Face quality
597
+ faceQuality: faceQualityRef.current,
598
+
599
+ // Verification
600
+ finishVerification,
601
+ setLiveImage: (image: string) => {
602
+ liveImageRef.current = image;
603
+ },
604
+
605
+ // Headless controls
606
+ startVerification,
607
+ pauseVerification,
608
+ resumeVerification,
609
+ cancelVerification,
610
+ restartSession,
611
+ retryChallenge,
612
+ retryVerification,
613
+
614
+ // Configuration
615
+ setVoiceOptions,
616
+ setMode,
617
+ setReferenceImage,
618
+
619
+ // Session info
620
+ sessionStartedAt: sessionStartRef.current,
621
+ sessionDuration: Date.now() - sessionStartRef.current,
622
+
623
+ // Service access for advanced use
624
+ verificationService: verificationServiceRef.current,
625
+ };
626
+ }