@moveris/shared 2.1.1 → 2.3.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/README.md CHANGED
@@ -25,6 +25,7 @@ const client = new LivenessClient({
25
25
 
26
26
  // Perform a fast liveness check
27
27
  const result = await client.fastCheck(frames, {
28
+ sessionId: 'my-session-id', // optional — auto-generated if omitted
28
29
  model: '10',
29
30
  source: 'live',
30
31
  });
@@ -293,6 +294,8 @@ const sessionId = generateSessionId();
293
294
  // e.g., 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
294
295
  ```
295
296
 
297
+ > **Session ID injection**: Every `LivenessClient` method (`fastCheck`, `fastCheckCrops`, `streamFrame`, `verify`, `hybrid50`, `hybrid150`) accepts an optional `sessionId` in its options object. If provided, that ID is used for the request; if omitted, a new UUID is generated automatically. This is useful for debugging, testing, or correlating client requests with server logs. In `@moveris/react`, the `sessionId` can be injected via the `useLiveness` hook config or the `LivenessView` / `LivenessModal` props.
298
+
296
299
  #### `toFrameData(frames)`
297
300
 
298
301
  Convert CapturedFrame array to API format.
@@ -423,6 +426,11 @@ const status = getStatusMessage('capturing', DEFAULT_LOCALE);
423
426
  | `processing` | "Processing..." | API verification in progress |
424
427
  | `success` | "Verification complete" | Successful completion |
425
428
  | `failed` | "Verification failed" | Failed verification |
429
+ | `eyes_not_visible` | "Eyes not clearly visible" | Eye region featureless |
430
+ | `eyes_shadowed` | "Eyes are in shadow…" | Eye region too dark |
431
+ | `eyes_overexposed` | "Eye region overexposed…" | Eye region too bright |
432
+ | `glasses_glare` | "Glare detected…" | Specular highlights on eyes |
433
+ | `eye_quality_poor` | "Eye region quality is poor" | Generic eye quality failure |
426
434
 
427
435
  ---
428
436
 
@@ -449,6 +457,59 @@ const color = OVAL_GUIDE_COLORS[state];
449
457
 
450
458
  ---
451
459
 
460
+ ### Eye Region Quality Analysis
461
+
462
+ Pre-request eye-region quality gate. Platform-agnostic functions that analyze RGBA pixel data from eye regions to detect shadows, glare, occlusion, and poor visibility.
463
+
464
+ ```typescript
465
+ import {
466
+ checkEyeRegionQuality,
467
+ analyzeEyeRegionBrightness,
468
+ analyzeEyeRegionContrast,
469
+ detectSpecularHighlights,
470
+ EYE_QUALITY_THRESHOLDS,
471
+ } from '@moveris/shared';
472
+
473
+ // Combined quality check (recommended)
474
+ const quality = checkEyeRegionQuality(eyeRegionPixels);
475
+ console.log(quality.passed); // boolean
476
+ console.log(quality.brightness); // 0-255
477
+ console.log(quality.contrast); // standard deviation of luminance
478
+ console.log(quality.hasGlare); // boolean
479
+ console.log(quality.glareRatio); // 0-1
480
+ console.log(quality.message); // user-facing feedback or null
481
+
482
+ // Individual checks
483
+ const brightness = analyzeEyeRegionBrightness(pixels);
484
+ const contrast = analyzeEyeRegionContrast(pixels);
485
+ const glareRatio = detectSpecularHighlights(pixels);
486
+ ```
487
+
488
+ | Check | Threshold | Condition |
489
+ | ----------- | --------------------- | --------------------------------------- |
490
+ | Shadowed | `minBrightness: 40` | Average luminance too low |
491
+ | Overexposed | `maxBrightness: 230` | Average luminance too high |
492
+ | Glare | `maxGlareRatio: 0.15` | >15% of pixels are specular highlights |
493
+ | Occluded | `minContrast: 12` | Standard deviation of luminance too low |
494
+
495
+ Custom thresholds can be passed as a second argument to `checkEyeRegionQuality()`.
496
+
497
+ ### Eye Region Landmarks
498
+
499
+ Extract bounding boxes for left and right eye regions from MediaPipe 468-point face mesh landmarks.
500
+
501
+ ```typescript
502
+ import { getEyeRegionBounds, EYE_LANDMARK_INDICES, validateFaceLandmarks } from '@moveris/shared';
503
+
504
+ const bounds = getEyeRegionBounds(landmarks);
505
+ if (bounds) {
506
+ console.log(bounds.leftEye); // { x, y, width, height } (normalized 0-1)
507
+ console.log(bounds.rightEye); // { x, y, width, height } (normalized 0-1)
508
+ }
509
+ ```
510
+
511
+ ---
512
+
452
513
  ### Frame Analysis Utilities
453
514
 
454
515
  Utilities for assessing frame quality before submission.
@@ -524,6 +585,10 @@ import type {
524
585
  HeadPose,
525
586
  FaceLandmarkPoint,
526
587
  LandmarkValidationResult,
588
+ EyeRegionQuality,
589
+ EyeQualityThresholds,
590
+ EyeRegionBounds,
591
+ EyeRegionsBounds,
527
592
  } from '@moveris/shared';
528
593
  ```
529
594
 
package/dist/index.d.mts CHANGED
@@ -322,6 +322,21 @@ declare const LANDMARK_INDEX: {
322
322
  readonly UPPER_LIP: 13;
323
323
  readonly LOWER_LIP: 14;
324
324
  };
325
+ declare const EYE_LANDMARK_INDICES: {
326
+ readonly RIGHT_EYE: readonly [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246];
327
+ readonly LEFT_EYE: readonly [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398];
328
+ };
329
+ interface EyeRegionBounds {
330
+ x: number;
331
+ y: number;
332
+ width: number;
333
+ height: number;
334
+ }
335
+ interface EyeRegionsBounds {
336
+ leftEye: EyeRegionBounds;
337
+ rightEye: EyeRegionBounds;
338
+ }
339
+ declare function getEyeRegionBounds(landmarks: FaceLandmarkPoint[]): EyeRegionsBounds | null;
325
340
  declare const LANDMARK_MIN_BOUND = 0.1;
326
341
  declare const LANDMARK_MAX_BOUND = 0.9;
327
342
  declare const MIN_LANDMARK_COUNT = 15;
@@ -429,6 +444,10 @@ declare class LivenessClient {
429
444
  private readonly fetchFn;
430
445
  constructor(config: LivenessClientConfig);
431
446
  private request;
447
+ private parseErrorResponse;
448
+ private static unwrapErrorCode;
449
+ private static unwrapErrorMessage;
450
+ private static extractCodeFromText;
432
451
  private requestWithRetry;
433
452
  health(): Promise<HealthResponse>;
434
453
  queueStats(): Promise<QueueStatsResponse>;
@@ -565,6 +584,27 @@ declare const DEFAULT_LIVENESS_CONFIG: {
565
584
  readonly timeout: 30000;
566
585
  };
567
586
 
587
+ declare const API_ERROR_CODES: {
588
+ readonly SESSION_EXPIRED: "session_expired";
589
+ readonly INVALID_KEY: "invalid_key";
590
+ readonly INVALID_MODEL: "invalid_model";
591
+ readonly INSUFFICIENT_FRAMES: "insufficient_frames";
592
+ readonly MISSING_FIELD: "missing_field";
593
+ readonly INVALID_FRAME: "invalid_frame";
594
+ readonly INVALID_SESSION: "invalid_session";
595
+ readonly RATE_LIMITED: "rate_limited";
596
+ readonly STREAM_INCOMPLETE: "stream_incomplete";
597
+ readonly NO_VERDICT: "no_verdict";
598
+ readonly TIMEOUT: "timeout";
599
+ readonly NETWORK_ERROR: "network_error";
600
+ readonly SERVER_ERROR: "server_error";
601
+ };
602
+ type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
603
+ declare const ERROR_MESSAGES: Record<string, string>;
604
+ declare const ERROR_MESSAGES_ES: Record<string, string>;
605
+ declare function getApiErrorMessage(code: string | undefined, message?: string, customMessages?: Record<string, string>): string;
606
+ declare function isRetryableError(code: string | undefined): boolean;
607
+
568
608
  declare const ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
569
609
  declare const ALIGNMENT_THRESHOLD_POOR = 0.5;
570
610
  declare const ALIGNMENT_THRESHOLD_GOOD = 0.5;
@@ -630,6 +670,11 @@ declare const FEEDBACK_MESSAGES: {
630
670
  readonly too_dark: "Low lighting - move to a brighter area";
631
671
  readonly backlit: "Backlit - try facing the light source";
632
672
  readonly hand_detected: "Remove hand from face";
673
+ readonly eyes_not_visible: "Eyes not clearly visible";
674
+ readonly eyes_shadowed: "Eyes are in shadow - improve lighting";
675
+ readonly eyes_overexposed: "Eye region overexposed - reduce lighting";
676
+ readonly glasses_glare: "Glare detected - adjust angle or remove glasses";
677
+ readonly eye_quality_poor: "Eye region quality is poor";
633
678
  readonly capturing: "Capturing...";
634
679
  readonly almost_done: "Almost done...";
635
680
  readonly verification_complete: "Verification complete";
@@ -683,4 +728,25 @@ interface RetryOptions {
683
728
  declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
684
729
  declare function sleep(ms: number): Promise<void>;
685
730
 
686
- export { ALIGNMENT_THRESHOLD_CAPTURE, ALIGNMENT_THRESHOLD_GOOD, ALIGNMENT_THRESHOLD_PERFECT, ALIGNMENT_THRESHOLD_POOR, API_ENDPOINTS, API_PATHS, AUTH_CONFIG, BACKLIT_RATIO_THRESHOLD, BLUR_THRESHOLD_MOBILE, BaseFrameCollector, type BlurAnalysis, type CaptureQualityState, type CapturedFrame, type CropData, DEFAULT_BLUR_THRESHOLD, DEFAULT_ENDPOINT, DEFAULT_FACE_DETECTION_TIERS, DEFAULT_GAZE_THRESHOLDS, DEFAULT_HAND_OCCLUSION_CONFIG, DEFAULT_LIVENESS_CONFIG, DEFAULT_LOCALE, DEFAULT_OVAL_REGION, DEFAULT_STABILIZER_CONFIG, DEFAULT_STATUS_MESSAGES, type DetectionResult, type DetectionSummary, type DetectorConfig, ES_LOCALE, type ErrorResponse, FACE_CENTER_VERTICAL_OFFSET, FACE_CROP_OUTPUT_SIZE, FEEDBACK_MESSAGES, FRAME_BUFFER_CONFIG, FRAME_CONFIG, type FaceAlignmentResult, type FaceBoundingBox, type FaceDetectionTiers, type FaceInOvalResult, type FaceLandmarkPoint, type FaceVisibilityResult, type FastCheckCropsRequest, type FastCheckModel, type FastCheckRequest, type FastCheckResponse, type FastCheckStreamRequest, type FastCheckStreamResponse, type FeedbackLocale, type FeedbackMessageKey, type Frame, FrameBuffer, type FrameData, type FrameQualityResult, FrameQueue, type FrameSource, GOOD_ALIGNMENT, type GazeThresholds, HIGH_ALIGNMENT, HYBRID_MODEL_CONFIGS, type HandOcclusionConfig, type HeadPose, type HealthResponse, type Hybrid150CheckRequest, type Hybrid50CheckRequest, type HybridCheckRequest, type HybridCheckResponse, type HybridFrameData, type HybridModelConfig, IDEAL_CROP_MULTIPLIER, type JobStatus, type JobStatusResponse, LANDMARK_INDEX, LANDMARK_MAX_BOUND, LANDMARK_MIN_BOUND, LOW_LIGHT_THRESHOLD, type LandmarkValidationResult, type LightingAnalysis, LivenessApiError, type LivenessCallbacks, LivenessClient, type LivenessClientConfig, type LivenessConfig, type LivenessResult, type LivenessState, MAX_CROP_MULTIPLIER, MAX_FACE_PERCENTAGE_IN_CROP, MAX_FACE_RATIO, MIN_CAPTURE_ALIGNMENT, MIN_CROP_MULTIPLIER, MIN_FACE_BOTTOM_MARGIN, MIN_FACE_RATIO, MIN_FACE_SIDE_MARGIN, MIN_FACE_TOP_MARGIN, MIN_LANDMARK_COUNT, MODEL_CONFIGS, type ModelConfig, type ModelType, OVAL_GUIDE_COLORS, OVAL_GUIDE_STYLES, type OnErrorCallback, type OnFrameCapturedCallback, type OnProgressCallback, type OnResultCallback, type OnStateChangeCallback, type OvalGuideState, type OvalRegion, type QueueStatsResponse, RETRY_CONFIG, type RetryOptions, type StabilizationProgress, type StabilizationResult, type StabilizerConfig, type StatusMessageKey, type StreamingStatus, TARGET_FACE_PERCENTAGE_IN_CROP, type Verdict, type VerifyRequest, type VerifyResponse, type VideoFrameMetadata, analyzeBlur, analyzeLighting, calculateAdaptiveCropMultiplier, calculateBrightness, calculateFaceAlignment, calculateFaceCropRegion, canCaptureFrame, checkFrameQuality, decodeBase64, encodeBase64, generateSessionId, getCaptureQualityFeedback, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
731
+ interface EyeRegionQuality {
732
+ passed: boolean;
733
+ brightness: number;
734
+ contrast: number;
735
+ hasGlare: boolean;
736
+ glareRatio: number;
737
+ message: string | null;
738
+ }
739
+ interface EyeQualityThresholds {
740
+ minBrightness: number;
741
+ maxBrightness: number;
742
+ minContrast: number;
743
+ glarePixelThreshold: number;
744
+ maxGlareRatio: number;
745
+ }
746
+ declare const EYE_QUALITY_THRESHOLDS: EyeQualityThresholds;
747
+ declare function analyzeEyeRegionBrightness(pixels: Uint8Array | Uint8ClampedArray): number;
748
+ declare function analyzeEyeRegionContrast(pixels: Uint8Array | Uint8ClampedArray, meanBrightness?: number): number;
749
+ declare function detectSpecularHighlights(pixels: Uint8Array | Uint8ClampedArray, threshold?: number): number;
750
+ declare function checkEyeRegionQuality(pixels: Uint8Array | Uint8ClampedArray, thresholds?: EyeQualityThresholds): EyeRegionQuality;
751
+
752
+ export { ALIGNMENT_THRESHOLD_CAPTURE, ALIGNMENT_THRESHOLD_GOOD, ALIGNMENT_THRESHOLD_PERFECT, ALIGNMENT_THRESHOLD_POOR, API_ENDPOINTS, API_ERROR_CODES, API_PATHS, AUTH_CONFIG, type ApiErrorCode, BACKLIT_RATIO_THRESHOLD, BLUR_THRESHOLD_MOBILE, BaseFrameCollector, type BlurAnalysis, type CaptureQualityState, type CapturedFrame, type CropData, DEFAULT_BLUR_THRESHOLD, DEFAULT_ENDPOINT, DEFAULT_FACE_DETECTION_TIERS, DEFAULT_GAZE_THRESHOLDS, DEFAULT_HAND_OCCLUSION_CONFIG, DEFAULT_LIVENESS_CONFIG, DEFAULT_LOCALE, DEFAULT_OVAL_REGION, DEFAULT_STABILIZER_CONFIG, DEFAULT_STATUS_MESSAGES, type DetectionResult, type DetectionSummary, type DetectorConfig, ERROR_MESSAGES, ERROR_MESSAGES_ES, ES_LOCALE, EYE_LANDMARK_INDICES, EYE_QUALITY_THRESHOLDS, type ErrorResponse, type EyeQualityThresholds, type EyeRegionBounds, type EyeRegionQuality, type EyeRegionsBounds, FACE_CENTER_VERTICAL_OFFSET, FACE_CROP_OUTPUT_SIZE, FEEDBACK_MESSAGES, FRAME_BUFFER_CONFIG, FRAME_CONFIG, type FaceAlignmentResult, type FaceBoundingBox, type FaceDetectionTiers, type FaceInOvalResult, type FaceLandmarkPoint, type FaceVisibilityResult, type FastCheckCropsRequest, type FastCheckModel, type FastCheckRequest, type FastCheckResponse, type FastCheckStreamRequest, type FastCheckStreamResponse, type FeedbackLocale, type FeedbackMessageKey, type Frame, FrameBuffer, type FrameData, type FrameQualityResult, FrameQueue, type FrameSource, GOOD_ALIGNMENT, type GazeThresholds, HIGH_ALIGNMENT, HYBRID_MODEL_CONFIGS, type HandOcclusionConfig, type HeadPose, type HealthResponse, type Hybrid150CheckRequest, type Hybrid50CheckRequest, type HybridCheckRequest, type HybridCheckResponse, type HybridFrameData, type HybridModelConfig, IDEAL_CROP_MULTIPLIER, type JobStatus, type JobStatusResponse, LANDMARK_INDEX, LANDMARK_MAX_BOUND, LANDMARK_MIN_BOUND, LOW_LIGHT_THRESHOLD, type LandmarkValidationResult, type LightingAnalysis, LivenessApiError, type LivenessCallbacks, LivenessClient, type LivenessClientConfig, type LivenessConfig, type LivenessResult, type LivenessState, MAX_CROP_MULTIPLIER, MAX_FACE_PERCENTAGE_IN_CROP, MAX_FACE_RATIO, MIN_CAPTURE_ALIGNMENT, MIN_CROP_MULTIPLIER, MIN_FACE_BOTTOM_MARGIN, MIN_FACE_RATIO, MIN_FACE_SIDE_MARGIN, MIN_FACE_TOP_MARGIN, MIN_LANDMARK_COUNT, MODEL_CONFIGS, type ModelConfig, type ModelType, OVAL_GUIDE_COLORS, OVAL_GUIDE_STYLES, type OnErrorCallback, type OnFrameCapturedCallback, type OnProgressCallback, type OnResultCallback, type OnStateChangeCallback, type OvalGuideState, type OvalRegion, type QueueStatsResponse, RETRY_CONFIG, type RetryOptions, type StabilizationProgress, type StabilizationResult, type StabilizerConfig, type StatusMessageKey, type StreamingStatus, TARGET_FACE_PERCENTAGE_IN_CROP, type Verdict, type VerifyRequest, type VerifyResponse, type VideoFrameMetadata, analyzeBlur, analyzeEyeRegionBrightness, analyzeEyeRegionContrast, analyzeLighting, calculateAdaptiveCropMultiplier, calculateBrightness, calculateFaceAlignment, calculateFaceCropRegion, canCaptureFrame, checkEyeRegionQuality, checkFrameQuality, decodeBase64, detectSpecularHighlights, encodeBase64, generateSessionId, getApiErrorMessage, getCaptureQualityFeedback, getEyeRegionBounds, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, isRetryableError, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
package/dist/index.d.ts CHANGED
@@ -322,6 +322,21 @@ declare const LANDMARK_INDEX: {
322
322
  readonly UPPER_LIP: 13;
323
323
  readonly LOWER_LIP: 14;
324
324
  };
325
+ declare const EYE_LANDMARK_INDICES: {
326
+ readonly RIGHT_EYE: readonly [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246];
327
+ readonly LEFT_EYE: readonly [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398];
328
+ };
329
+ interface EyeRegionBounds {
330
+ x: number;
331
+ y: number;
332
+ width: number;
333
+ height: number;
334
+ }
335
+ interface EyeRegionsBounds {
336
+ leftEye: EyeRegionBounds;
337
+ rightEye: EyeRegionBounds;
338
+ }
339
+ declare function getEyeRegionBounds(landmarks: FaceLandmarkPoint[]): EyeRegionsBounds | null;
325
340
  declare const LANDMARK_MIN_BOUND = 0.1;
326
341
  declare const LANDMARK_MAX_BOUND = 0.9;
327
342
  declare const MIN_LANDMARK_COUNT = 15;
@@ -429,6 +444,10 @@ declare class LivenessClient {
429
444
  private readonly fetchFn;
430
445
  constructor(config: LivenessClientConfig);
431
446
  private request;
447
+ private parseErrorResponse;
448
+ private static unwrapErrorCode;
449
+ private static unwrapErrorMessage;
450
+ private static extractCodeFromText;
432
451
  private requestWithRetry;
433
452
  health(): Promise<HealthResponse>;
434
453
  queueStats(): Promise<QueueStatsResponse>;
@@ -565,6 +584,27 @@ declare const DEFAULT_LIVENESS_CONFIG: {
565
584
  readonly timeout: 30000;
566
585
  };
567
586
 
587
+ declare const API_ERROR_CODES: {
588
+ readonly SESSION_EXPIRED: "session_expired";
589
+ readonly INVALID_KEY: "invalid_key";
590
+ readonly INVALID_MODEL: "invalid_model";
591
+ readonly INSUFFICIENT_FRAMES: "insufficient_frames";
592
+ readonly MISSING_FIELD: "missing_field";
593
+ readonly INVALID_FRAME: "invalid_frame";
594
+ readonly INVALID_SESSION: "invalid_session";
595
+ readonly RATE_LIMITED: "rate_limited";
596
+ readonly STREAM_INCOMPLETE: "stream_incomplete";
597
+ readonly NO_VERDICT: "no_verdict";
598
+ readonly TIMEOUT: "timeout";
599
+ readonly NETWORK_ERROR: "network_error";
600
+ readonly SERVER_ERROR: "server_error";
601
+ };
602
+ type ApiErrorCode = (typeof API_ERROR_CODES)[keyof typeof API_ERROR_CODES];
603
+ declare const ERROR_MESSAGES: Record<string, string>;
604
+ declare const ERROR_MESSAGES_ES: Record<string, string>;
605
+ declare function getApiErrorMessage(code: string | undefined, message?: string, customMessages?: Record<string, string>): string;
606
+ declare function isRetryableError(code: string | undefined): boolean;
607
+
568
608
  declare const ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
569
609
  declare const ALIGNMENT_THRESHOLD_POOR = 0.5;
570
610
  declare const ALIGNMENT_THRESHOLD_GOOD = 0.5;
@@ -630,6 +670,11 @@ declare const FEEDBACK_MESSAGES: {
630
670
  readonly too_dark: "Low lighting - move to a brighter area";
631
671
  readonly backlit: "Backlit - try facing the light source";
632
672
  readonly hand_detected: "Remove hand from face";
673
+ readonly eyes_not_visible: "Eyes not clearly visible";
674
+ readonly eyes_shadowed: "Eyes are in shadow - improve lighting";
675
+ readonly eyes_overexposed: "Eye region overexposed - reduce lighting";
676
+ readonly glasses_glare: "Glare detected - adjust angle or remove glasses";
677
+ readonly eye_quality_poor: "Eye region quality is poor";
633
678
  readonly capturing: "Capturing...";
634
679
  readonly almost_done: "Almost done...";
635
680
  readonly verification_complete: "Verification complete";
@@ -683,4 +728,25 @@ interface RetryOptions {
683
728
  declare function retryWithBackoff<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
684
729
  declare function sleep(ms: number): Promise<void>;
685
730
 
686
- export { ALIGNMENT_THRESHOLD_CAPTURE, ALIGNMENT_THRESHOLD_GOOD, ALIGNMENT_THRESHOLD_PERFECT, ALIGNMENT_THRESHOLD_POOR, API_ENDPOINTS, API_PATHS, AUTH_CONFIG, BACKLIT_RATIO_THRESHOLD, BLUR_THRESHOLD_MOBILE, BaseFrameCollector, type BlurAnalysis, type CaptureQualityState, type CapturedFrame, type CropData, DEFAULT_BLUR_THRESHOLD, DEFAULT_ENDPOINT, DEFAULT_FACE_DETECTION_TIERS, DEFAULT_GAZE_THRESHOLDS, DEFAULT_HAND_OCCLUSION_CONFIG, DEFAULT_LIVENESS_CONFIG, DEFAULT_LOCALE, DEFAULT_OVAL_REGION, DEFAULT_STABILIZER_CONFIG, DEFAULT_STATUS_MESSAGES, type DetectionResult, type DetectionSummary, type DetectorConfig, ES_LOCALE, type ErrorResponse, FACE_CENTER_VERTICAL_OFFSET, FACE_CROP_OUTPUT_SIZE, FEEDBACK_MESSAGES, FRAME_BUFFER_CONFIG, FRAME_CONFIG, type FaceAlignmentResult, type FaceBoundingBox, type FaceDetectionTiers, type FaceInOvalResult, type FaceLandmarkPoint, type FaceVisibilityResult, type FastCheckCropsRequest, type FastCheckModel, type FastCheckRequest, type FastCheckResponse, type FastCheckStreamRequest, type FastCheckStreamResponse, type FeedbackLocale, type FeedbackMessageKey, type Frame, FrameBuffer, type FrameData, type FrameQualityResult, FrameQueue, type FrameSource, GOOD_ALIGNMENT, type GazeThresholds, HIGH_ALIGNMENT, HYBRID_MODEL_CONFIGS, type HandOcclusionConfig, type HeadPose, type HealthResponse, type Hybrid150CheckRequest, type Hybrid50CheckRequest, type HybridCheckRequest, type HybridCheckResponse, type HybridFrameData, type HybridModelConfig, IDEAL_CROP_MULTIPLIER, type JobStatus, type JobStatusResponse, LANDMARK_INDEX, LANDMARK_MAX_BOUND, LANDMARK_MIN_BOUND, LOW_LIGHT_THRESHOLD, type LandmarkValidationResult, type LightingAnalysis, LivenessApiError, type LivenessCallbacks, LivenessClient, type LivenessClientConfig, type LivenessConfig, type LivenessResult, type LivenessState, MAX_CROP_MULTIPLIER, MAX_FACE_PERCENTAGE_IN_CROP, MAX_FACE_RATIO, MIN_CAPTURE_ALIGNMENT, MIN_CROP_MULTIPLIER, MIN_FACE_BOTTOM_MARGIN, MIN_FACE_RATIO, MIN_FACE_SIDE_MARGIN, MIN_FACE_TOP_MARGIN, MIN_LANDMARK_COUNT, MODEL_CONFIGS, type ModelConfig, type ModelType, OVAL_GUIDE_COLORS, OVAL_GUIDE_STYLES, type OnErrorCallback, type OnFrameCapturedCallback, type OnProgressCallback, type OnResultCallback, type OnStateChangeCallback, type OvalGuideState, type OvalRegion, type QueueStatsResponse, RETRY_CONFIG, type RetryOptions, type StabilizationProgress, type StabilizationResult, type StabilizerConfig, type StatusMessageKey, type StreamingStatus, TARGET_FACE_PERCENTAGE_IN_CROP, type Verdict, type VerifyRequest, type VerifyResponse, type VideoFrameMetadata, analyzeBlur, analyzeLighting, calculateAdaptiveCropMultiplier, calculateBrightness, calculateFaceAlignment, calculateFaceCropRegion, canCaptureFrame, checkFrameQuality, decodeBase64, encodeBase64, generateSessionId, getCaptureQualityFeedback, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
731
+ interface EyeRegionQuality {
732
+ passed: boolean;
733
+ brightness: number;
734
+ contrast: number;
735
+ hasGlare: boolean;
736
+ glareRatio: number;
737
+ message: string | null;
738
+ }
739
+ interface EyeQualityThresholds {
740
+ minBrightness: number;
741
+ maxBrightness: number;
742
+ minContrast: number;
743
+ glarePixelThreshold: number;
744
+ maxGlareRatio: number;
745
+ }
746
+ declare const EYE_QUALITY_THRESHOLDS: EyeQualityThresholds;
747
+ declare function analyzeEyeRegionBrightness(pixels: Uint8Array | Uint8ClampedArray): number;
748
+ declare function analyzeEyeRegionContrast(pixels: Uint8Array | Uint8ClampedArray, meanBrightness?: number): number;
749
+ declare function detectSpecularHighlights(pixels: Uint8Array | Uint8ClampedArray, threshold?: number): number;
750
+ declare function checkEyeRegionQuality(pixels: Uint8Array | Uint8ClampedArray, thresholds?: EyeQualityThresholds): EyeRegionQuality;
751
+
752
+ export { ALIGNMENT_THRESHOLD_CAPTURE, ALIGNMENT_THRESHOLD_GOOD, ALIGNMENT_THRESHOLD_PERFECT, ALIGNMENT_THRESHOLD_POOR, API_ENDPOINTS, API_ERROR_CODES, API_PATHS, AUTH_CONFIG, type ApiErrorCode, BACKLIT_RATIO_THRESHOLD, BLUR_THRESHOLD_MOBILE, BaseFrameCollector, type BlurAnalysis, type CaptureQualityState, type CapturedFrame, type CropData, DEFAULT_BLUR_THRESHOLD, DEFAULT_ENDPOINT, DEFAULT_FACE_DETECTION_TIERS, DEFAULT_GAZE_THRESHOLDS, DEFAULT_HAND_OCCLUSION_CONFIG, DEFAULT_LIVENESS_CONFIG, DEFAULT_LOCALE, DEFAULT_OVAL_REGION, DEFAULT_STABILIZER_CONFIG, DEFAULT_STATUS_MESSAGES, type DetectionResult, type DetectionSummary, type DetectorConfig, ERROR_MESSAGES, ERROR_MESSAGES_ES, ES_LOCALE, EYE_LANDMARK_INDICES, EYE_QUALITY_THRESHOLDS, type ErrorResponse, type EyeQualityThresholds, type EyeRegionBounds, type EyeRegionQuality, type EyeRegionsBounds, FACE_CENTER_VERTICAL_OFFSET, FACE_CROP_OUTPUT_SIZE, FEEDBACK_MESSAGES, FRAME_BUFFER_CONFIG, FRAME_CONFIG, type FaceAlignmentResult, type FaceBoundingBox, type FaceDetectionTiers, type FaceInOvalResult, type FaceLandmarkPoint, type FaceVisibilityResult, type FastCheckCropsRequest, type FastCheckModel, type FastCheckRequest, type FastCheckResponse, type FastCheckStreamRequest, type FastCheckStreamResponse, type FeedbackLocale, type FeedbackMessageKey, type Frame, FrameBuffer, type FrameData, type FrameQualityResult, FrameQueue, type FrameSource, GOOD_ALIGNMENT, type GazeThresholds, HIGH_ALIGNMENT, HYBRID_MODEL_CONFIGS, type HandOcclusionConfig, type HeadPose, type HealthResponse, type Hybrid150CheckRequest, type Hybrid50CheckRequest, type HybridCheckRequest, type HybridCheckResponse, type HybridFrameData, type HybridModelConfig, IDEAL_CROP_MULTIPLIER, type JobStatus, type JobStatusResponse, LANDMARK_INDEX, LANDMARK_MAX_BOUND, LANDMARK_MIN_BOUND, LOW_LIGHT_THRESHOLD, type LandmarkValidationResult, type LightingAnalysis, LivenessApiError, type LivenessCallbacks, LivenessClient, type LivenessClientConfig, type LivenessConfig, type LivenessResult, type LivenessState, MAX_CROP_MULTIPLIER, MAX_FACE_PERCENTAGE_IN_CROP, MAX_FACE_RATIO, MIN_CAPTURE_ALIGNMENT, MIN_CROP_MULTIPLIER, MIN_FACE_BOTTOM_MARGIN, MIN_FACE_RATIO, MIN_FACE_SIDE_MARGIN, MIN_FACE_TOP_MARGIN, MIN_LANDMARK_COUNT, MODEL_CONFIGS, type ModelConfig, type ModelType, OVAL_GUIDE_COLORS, OVAL_GUIDE_STYLES, type OnErrorCallback, type OnFrameCapturedCallback, type OnProgressCallback, type OnResultCallback, type OnStateChangeCallback, type OvalGuideState, type OvalRegion, type QueueStatsResponse, RETRY_CONFIG, type RetryOptions, type StabilizationProgress, type StabilizationResult, type StabilizerConfig, type StatusMessageKey, type StreamingStatus, TARGET_FACE_PERCENTAGE_IN_CROP, type Verdict, type VerifyRequest, type VerifyResponse, type VideoFrameMetadata, analyzeBlur, analyzeEyeRegionBrightness, analyzeEyeRegionContrast, analyzeLighting, calculateAdaptiveCropMultiplier, calculateBrightness, calculateFaceAlignment, calculateFaceCropRegion, canCaptureFrame, checkEyeRegionQuality, checkFrameQuality, decodeBase64, detectSpecularHighlights, encodeBase64, generateSessionId, getApiErrorMessage, getCaptureQualityFeedback, getEyeRegionBounds, getFeedbackMessage, getMinFramesForModel, getOvalGuideState, getStatusMessage, hasEnoughFrames, isFaceCropFullyInFrame, isFaceFullyVisible, isFaceInOval, isRetryableError, retryWithBackoff, rgbaToGrayscale, sleep, toFrameData, toHybridFrameData, toLivenessResult, toLivenessResultFromStream, validateApiKey, validateFaceLandmarks, validateFrameCount, validateFrameData, validateFrameIndex, validateTimestamp, validateUUID, validateUrl };
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  ALIGNMENT_THRESHOLD_PERFECT: () => ALIGNMENT_THRESHOLD_PERFECT,
26
26
  ALIGNMENT_THRESHOLD_POOR: () => ALIGNMENT_THRESHOLD_POOR,
27
27
  API_ENDPOINTS: () => API_ENDPOINTS,
28
+ API_ERROR_CODES: () => API_ERROR_CODES,
28
29
  API_PATHS: () => API_PATHS,
29
30
  AUTH_CONFIG: () => AUTH_CONFIG,
30
31
  BACKLIT_RATIO_THRESHOLD: () => BACKLIT_RATIO_THRESHOLD,
@@ -40,7 +41,11 @@ __export(index_exports, {
40
41
  DEFAULT_OVAL_REGION: () => DEFAULT_OVAL_REGION,
41
42
  DEFAULT_STABILIZER_CONFIG: () => DEFAULT_STABILIZER_CONFIG,
42
43
  DEFAULT_STATUS_MESSAGES: () => DEFAULT_STATUS_MESSAGES,
44
+ ERROR_MESSAGES: () => ERROR_MESSAGES,
45
+ ERROR_MESSAGES_ES: () => ERROR_MESSAGES_ES,
43
46
  ES_LOCALE: () => ES_LOCALE,
47
+ EYE_LANDMARK_INDICES: () => EYE_LANDMARK_INDICES,
48
+ EYE_QUALITY_THRESHOLDS: () => EYE_QUALITY_THRESHOLDS,
44
49
  FACE_CENTER_VERTICAL_OFFSET: () => FACE_CENTER_VERTICAL_OFFSET,
45
50
  FACE_CROP_OUTPUT_SIZE: () => FACE_CROP_OUTPUT_SIZE,
46
51
  FEEDBACK_MESSAGES: () => FEEDBACK_MESSAGES,
@@ -74,17 +79,23 @@ __export(index_exports, {
74
79
  RETRY_CONFIG: () => RETRY_CONFIG,
75
80
  TARGET_FACE_PERCENTAGE_IN_CROP: () => TARGET_FACE_PERCENTAGE_IN_CROP,
76
81
  analyzeBlur: () => analyzeBlur,
82
+ analyzeEyeRegionBrightness: () => analyzeEyeRegionBrightness,
83
+ analyzeEyeRegionContrast: () => analyzeEyeRegionContrast,
77
84
  analyzeLighting: () => analyzeLighting,
78
85
  calculateAdaptiveCropMultiplier: () => calculateAdaptiveCropMultiplier,
79
86
  calculateBrightness: () => calculateBrightness,
80
87
  calculateFaceAlignment: () => calculateFaceAlignment,
81
88
  calculateFaceCropRegion: () => calculateFaceCropRegion,
82
89
  canCaptureFrame: () => canCaptureFrame,
90
+ checkEyeRegionQuality: () => checkEyeRegionQuality,
83
91
  checkFrameQuality: () => checkFrameQuality,
84
92
  decodeBase64: () => decodeBase64,
93
+ detectSpecularHighlights: () => detectSpecularHighlights,
85
94
  encodeBase64: () => encodeBase64,
86
95
  generateSessionId: () => generateSessionId,
96
+ getApiErrorMessage: () => getApiErrorMessage,
87
97
  getCaptureQualityFeedback: () => getCaptureQualityFeedback,
98
+ getEyeRegionBounds: () => getEyeRegionBounds,
88
99
  getFeedbackMessage: () => getFeedbackMessage,
89
100
  getMinFramesForModel: () => getMinFramesForModel,
90
101
  getOvalGuideState: () => getOvalGuideState,
@@ -93,6 +104,7 @@ __export(index_exports, {
93
104
  isFaceCropFullyInFrame: () => isFaceCropFullyInFrame,
94
105
  isFaceFullyVisible: () => isFaceFullyVisible,
95
106
  isFaceInOval: () => isFaceInOval,
107
+ isRetryableError: () => isRetryableError,
96
108
  retryWithBackoff: () => retryWithBackoff,
97
109
  rgbaToGrayscale: () => rgbaToGrayscale,
98
110
  sleep: () => sleep,
@@ -248,7 +260,8 @@ function toLivenessResultFromStream(response) {
248
260
  throw new LivenessApiError("Stream not complete", "stream_incomplete", 400);
249
261
  }
250
262
  if (!response.verdict) {
251
- throw new LivenessApiError(response.error ?? "No verdict received", "no_verdict", 500);
263
+ const errorCode = response.error ?? "no_verdict";
264
+ throw new LivenessApiError(response.error ?? "No verdict received", errorCode, 500);
252
265
  }
253
266
  return {
254
267
  verdict: response.verdict,
@@ -269,7 +282,7 @@ function generateSessionId() {
269
282
  return v.toString(16);
270
283
  });
271
284
  }
272
- var LivenessClient = class {
285
+ var LivenessClient = class _LivenessClient {
273
286
  constructor(config) {
274
287
  this.baseUrl = (config.baseUrl ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
275
288
  this.apiKey = config.apiKey;
@@ -296,14 +309,7 @@ var LivenessClient = class {
296
309
  });
297
310
  clearTimeout(timeoutId);
298
311
  if (!response.ok) {
299
- const errorData = await response.json();
300
- throw new LivenessApiError(
301
- errorData.message,
302
- errorData.error,
303
- response.status,
304
- errorData.required,
305
- errorData.received
306
- );
312
+ throw await this.parseErrorResponse(response);
307
313
  }
308
314
  return await response.json();
309
315
  } catch (error) {
@@ -321,6 +327,75 @@ var LivenessClient = class {
321
327
  );
322
328
  }
323
329
  }
330
+ /**
331
+ * Parse an error response body, handling both JSON and non-JSON bodies.
332
+ *
333
+ * The backend sometimes wraps the real error in a generic envelope:
334
+ * ```json
335
+ * { "error": "http_error", "message": "{'error': 'session_expired', ...}" }
336
+ * ```
337
+ * This method unwraps such envelopes to extract the real error code.
338
+ */
339
+ async parseErrorResponse(response) {
340
+ const status = response.status;
341
+ let body;
342
+ try {
343
+ body = await response.text();
344
+ } catch {
345
+ return new LivenessApiError(`Request failed with status ${status}`, "server_error", status);
346
+ }
347
+ try {
348
+ const data = JSON.parse(body);
349
+ if (data.error) {
350
+ const realCode = _LivenessClient.unwrapErrorCode(data.error, data.message);
351
+ const realMessage = _LivenessClient.unwrapErrorMessage(data.message);
352
+ return new LivenessApiError(
353
+ realMessage ?? `Request failed (${realCode})`,
354
+ realCode,
355
+ status,
356
+ data.required,
357
+ data.received
358
+ );
359
+ }
360
+ } catch {
361
+ }
362
+ const code = _LivenessClient.extractCodeFromText(body) ?? "server_error";
363
+ return new LivenessApiError(`Request failed with status ${status}`, code, status);
364
+ }
365
+ /**
366
+ * When the outer error code is a generic wrapper (e.g. "http_error"),
367
+ * try to extract the real error code from the message text.
368
+ */
369
+ static unwrapErrorCode(outerCode, message) {
370
+ const WRAPPER_CODES = /* @__PURE__ */ new Set([
371
+ "http_error",
372
+ "upstream_error",
373
+ "proxy_error",
374
+ "internal_error"
375
+ ]);
376
+ if (!WRAPPER_CODES.has(outerCode) || !message) {
377
+ return outerCode;
378
+ }
379
+ return _LivenessClient.extractCodeFromText(message) ?? outerCode;
380
+ }
381
+ /**
382
+ * Try to extract a human-readable message from a nested Python dict string.
383
+ * E.g.: "{'error': 'session_expired', 'message': \"Create a new session...\"}"
384
+ * → "Create a new session..."
385
+ */
386
+ static unwrapErrorMessage(message) {
387
+ if (!message) return void 0;
388
+ const msgMatch = /'message'\s*:\s*"([^"]+)"/.exec(message) ?? /'message'\s*:\s*'([^']+)'/.exec(message) ?? /"message"\s*:\s*"([^"]+)"/.exec(message);
389
+ return msgMatch?.[1] ?? void 0;
390
+ }
391
+ /**
392
+ * Extract an error code from raw text using regex.
393
+ * Handles both Python dict (`'error': 'code'`) and JSON (`"error": "code"`).
394
+ */
395
+ static extractCodeFromText(text) {
396
+ const match = /'error'\s*:\s*'([^']+)'/.exec(text) ?? /"error"\s*:\s*"([^"]+)"/.exec(text);
397
+ return match?.[1] ?? null;
398
+ }
324
399
  /**
325
400
  * Make a request with optional retry
326
401
  */
@@ -952,6 +1027,81 @@ var DEFAULT_STABILIZER_CONFIG = {
952
1027
  sampleSize: 64
953
1028
  };
954
1029
 
1030
+ // src/constants/errors.ts
1031
+ var API_ERROR_CODES = {
1032
+ SESSION_EXPIRED: "session_expired",
1033
+ INVALID_KEY: "invalid_key",
1034
+ INVALID_MODEL: "invalid_model",
1035
+ INSUFFICIENT_FRAMES: "insufficient_frames",
1036
+ MISSING_FIELD: "missing_field",
1037
+ INVALID_FRAME: "invalid_frame",
1038
+ INVALID_SESSION: "invalid_session",
1039
+ RATE_LIMITED: "rate_limited",
1040
+ STREAM_INCOMPLETE: "stream_incomplete",
1041
+ NO_VERDICT: "no_verdict",
1042
+ TIMEOUT: "timeout",
1043
+ NETWORK_ERROR: "network_error",
1044
+ SERVER_ERROR: "server_error"
1045
+ };
1046
+ var ERROR_MESSAGES = {
1047
+ [API_ERROR_CODES.SESSION_EXPIRED]: "Session expired \u2014 please restart the verification",
1048
+ [API_ERROR_CODES.INVALID_KEY]: "Invalid API key \u2014 contact your administrator",
1049
+ [API_ERROR_CODES.INVALID_MODEL]: "Invalid model configuration",
1050
+ [API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Not enough frames captured \u2014 try again",
1051
+ [API_ERROR_CODES.MISSING_FIELD]: "Missing required data \u2014 please try again",
1052
+ [API_ERROR_CODES.INVALID_FRAME]: "Invalid frame data \u2014 please try again",
1053
+ [API_ERROR_CODES.INVALID_SESSION]: "Invalid session \u2014 please restart",
1054
+ [API_ERROR_CODES.RATE_LIMITED]: "Too many requests \u2014 wait a moment and try again",
1055
+ [API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incomplete \u2014 please try again",
1056
+ [API_ERROR_CODES.NO_VERDICT]: "No result received \u2014 please try again",
1057
+ [API_ERROR_CODES.TIMEOUT]: "Request timed out \u2014 check your connection",
1058
+ [API_ERROR_CODES.NETWORK_ERROR]: "Network error \u2014 check your connection",
1059
+ [API_ERROR_CODES.SERVER_ERROR]: "Server error \u2014 please try again later"
1060
+ };
1061
+ var ERROR_MESSAGES_ES = {
1062
+ [API_ERROR_CODES.SESSION_EXPIRED]: "Sesi\xF3n expirada \u2014 reinicie la verificaci\xF3n",
1063
+ [API_ERROR_CODES.INVALID_KEY]: "Clave API inv\xE1lida \u2014 contacte al administrador",
1064
+ [API_ERROR_CODES.INVALID_MODEL]: "Configuraci\xF3n de modelo inv\xE1lida",
1065
+ [API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Frames insuficientes \u2014 intente de nuevo",
1066
+ [API_ERROR_CODES.MISSING_FIELD]: "Datos requeridos faltantes \u2014 intente de nuevo",
1067
+ [API_ERROR_CODES.INVALID_FRAME]: "Frame inv\xE1lido \u2014 intente de nuevo",
1068
+ [API_ERROR_CODES.INVALID_SESSION]: "Sesi\xF3n inv\xE1lida \u2014 reinicie",
1069
+ [API_ERROR_CODES.RATE_LIMITED]: "Demasiadas solicitudes \u2014 espere un momento",
1070
+ [API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incompleto \u2014 intente de nuevo",
1071
+ [API_ERROR_CODES.NO_VERDICT]: "Sin resultado \u2014 intente de nuevo",
1072
+ [API_ERROR_CODES.TIMEOUT]: "Tiempo de espera agotado \u2014 verifique su conexi\xF3n",
1073
+ [API_ERROR_CODES.NETWORK_ERROR]: "Error de red \u2014 verifique su conexi\xF3n",
1074
+ [API_ERROR_CODES.SERVER_ERROR]: "Error del servidor \u2014 intente m\xE1s tarde"
1075
+ };
1076
+ function looksLikeRawData(text) {
1077
+ const trimmed = text.trim();
1078
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return true;
1079
+ if (/'error'\s*:/.test(trimmed)) return true;
1080
+ if (/"error"\s*:/.test(trimmed)) return true;
1081
+ return false;
1082
+ }
1083
+ var GENERIC_FALLBACK = "An unexpected error occurred \u2014 please try again";
1084
+ function getApiErrorMessage(code, message, customMessages) {
1085
+ if (code) {
1086
+ const custom = customMessages?.[code];
1087
+ if (custom) return custom;
1088
+ const known = ERROR_MESSAGES[code];
1089
+ if (known) return known;
1090
+ }
1091
+ if (message && !looksLikeRawData(message)) {
1092
+ return message;
1093
+ }
1094
+ return GENERIC_FALLBACK;
1095
+ }
1096
+ function isRetryableError(code) {
1097
+ if (!code) return true;
1098
+ const nonRetryable = /* @__PURE__ */ new Set([
1099
+ API_ERROR_CODES.INVALID_KEY,
1100
+ API_ERROR_CODES.INVALID_MODEL
1101
+ ]);
1102
+ return !nonRetryable.has(code);
1103
+ }
1104
+
955
1105
  // src/constants/feedback.ts
956
1106
  var ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
957
1107
  var ALIGNMENT_THRESHOLD_POOR = 0.5;
@@ -1018,6 +1168,12 @@ var FEEDBACK_MESSAGES = {
1018
1168
  backlit: "Backlit - try facing the light source",
1019
1169
  // Hand occlusion
1020
1170
  hand_detected: "Remove hand from face",
1171
+ // Eye region quality
1172
+ eyes_not_visible: "Eyes not clearly visible",
1173
+ eyes_shadowed: "Eyes are in shadow - improve lighting",
1174
+ eyes_overexposed: "Eye region overexposed - reduce lighting",
1175
+ glasses_glare: "Glare detected - adjust angle or remove glasses",
1176
+ eye_quality_poor: "Eye region quality is poor",
1021
1177
  // Progress messages
1022
1178
  capturing: "Capturing...",
1023
1179
  almost_done: "Almost done...",
@@ -1069,6 +1225,12 @@ var ES_LOCALE = {
1069
1225
  backlit: "Contraluz - intenta mirar hacia la fuente de luz",
1070
1226
  // Hand occlusion
1071
1227
  hand_detected: "Retira la mano del rostro",
1228
+ // Eye region quality
1229
+ eyes_not_visible: "Ojos no visibles claramente",
1230
+ eyes_shadowed: "Ojos en sombra - mejora la iluminaci\xF3n",
1231
+ eyes_overexposed: "Regi\xF3n de ojos sobreexpuesta - reduce la iluminaci\xF3n",
1232
+ glasses_glare: "Reflejo detectado - ajusta el \xE1ngulo o retira los lentes",
1233
+ eye_quality_poor: "Calidad de la regi\xF3n de ojos insuficiente",
1072
1234
  // Progress messages
1073
1235
  capturing: "Capturando...",
1074
1236
  almost_done: "Casi listo...",
@@ -1502,6 +1664,44 @@ var LANDMARK_INDEX = {
1502
1664
  /** Lower lip center */
1503
1665
  LOWER_LIP: 14
1504
1666
  };
1667
+ var EYE_LANDMARK_INDICES = {
1668
+ /** Right eye contour (viewer's left) */
1669
+ RIGHT_EYE: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
1670
+ /** Left eye contour (viewer's right) */
1671
+ LEFT_EYE: [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398]
1672
+ };
1673
+ function getEyeRegionBounds(landmarks) {
1674
+ if (landmarks.length < 468) return null;
1675
+ const PADDING = 0.2;
1676
+ function computeBounds(indices) {
1677
+ let minX = 1;
1678
+ let minY = 1;
1679
+ let maxX = 0;
1680
+ let maxY = 0;
1681
+ for (const idx of indices) {
1682
+ const lm = landmarks[idx];
1683
+ if (!lm) continue;
1684
+ if (lm.x < minX) minX = lm.x;
1685
+ if (lm.y < minY) minY = lm.y;
1686
+ if (lm.x > maxX) maxX = lm.x;
1687
+ if (lm.y > maxY) maxY = lm.y;
1688
+ }
1689
+ const width = maxX - minX;
1690
+ const height = maxY - minY;
1691
+ const padX = width * PADDING;
1692
+ const padY = height * PADDING;
1693
+ return {
1694
+ x: Math.max(0, minX - padX),
1695
+ y: Math.max(0, minY - padY),
1696
+ width: Math.min(1 - Math.max(0, minX - padX), width + padX * 2),
1697
+ height: Math.min(1 - Math.max(0, minY - padY), height + padY * 2)
1698
+ };
1699
+ }
1700
+ return {
1701
+ rightEye: computeBounds(EYE_LANDMARK_INDICES.RIGHT_EYE),
1702
+ leftEye: computeBounds(EYE_LANDMARK_INDICES.LEFT_EYE)
1703
+ };
1704
+ }
1505
1705
  var LANDMARK_MIN_BOUND = 0.1;
1506
1706
  var LANDMARK_MAX_BOUND = 0.9;
1507
1707
  var MIN_LANDMARK_COUNT = 15;
@@ -1523,6 +1723,115 @@ function validateFaceLandmarks(landmarks) {
1523
1723
  }
1524
1724
  return { valid: true };
1525
1725
  }
1726
+
1727
+ // src/utils/eyeRegionAnalysis.ts
1728
+ var EYE_QUALITY_THRESHOLDS = {
1729
+ minBrightness: 40,
1730
+ maxBrightness: 230,
1731
+ minContrast: 12,
1732
+ glarePixelThreshold: 240,
1733
+ maxGlareRatio: 0.15
1734
+ };
1735
+ function rgbaToLuminance(r, g, b) {
1736
+ return 0.299 * r + 0.587 * g + 0.114 * b;
1737
+ }
1738
+ function analyzeEyeRegionBrightness(pixels) {
1739
+ const pixelCount = pixels.length / 4;
1740
+ if (pixelCount === 0) return 0;
1741
+ let totalLuminance = 0;
1742
+ for (let i = 0; i < pixels.length; i += 4) {
1743
+ totalLuminance += rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
1744
+ }
1745
+ return totalLuminance / pixelCount;
1746
+ }
1747
+ function analyzeEyeRegionContrast(pixels, meanBrightness) {
1748
+ const pixelCount = pixels.length / 4;
1749
+ if (pixelCount === 0) return 0;
1750
+ const mean = meanBrightness ?? analyzeEyeRegionBrightness(pixels);
1751
+ let sumSqDiff = 0;
1752
+ for (let i = 0; i < pixels.length; i += 4) {
1753
+ const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
1754
+ const diff = lum - mean;
1755
+ sumSqDiff += diff * diff;
1756
+ }
1757
+ return Math.sqrt(sumSqDiff / pixelCount);
1758
+ }
1759
+ function detectSpecularHighlights(pixels, threshold = EYE_QUALITY_THRESHOLDS.glarePixelThreshold) {
1760
+ const pixelCount = pixels.length / 4;
1761
+ if (pixelCount === 0) return 0;
1762
+ let glareCount = 0;
1763
+ for (let i = 0; i < pixels.length; i += 4) {
1764
+ const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
1765
+ if (lum >= threshold) {
1766
+ glareCount++;
1767
+ }
1768
+ }
1769
+ return glareCount / pixelCount;
1770
+ }
1771
+ function checkEyeRegionQuality(pixels, thresholds = EYE_QUALITY_THRESHOLDS) {
1772
+ if (pixels.length < 4) {
1773
+ return {
1774
+ passed: false,
1775
+ brightness: 0,
1776
+ contrast: 0,
1777
+ hasGlare: false,
1778
+ glareRatio: 0,
1779
+ message: "Eyes not clearly visible"
1780
+ };
1781
+ }
1782
+ const brightness = analyzeEyeRegionBrightness(pixels);
1783
+ const contrast = analyzeEyeRegionContrast(pixels, brightness);
1784
+ const glareRatio = detectSpecularHighlights(pixels, thresholds.glarePixelThreshold);
1785
+ const hasGlare = glareRatio > thresholds.maxGlareRatio;
1786
+ if (brightness < thresholds.minBrightness) {
1787
+ return {
1788
+ passed: false,
1789
+ brightness,
1790
+ contrast,
1791
+ hasGlare,
1792
+ glareRatio,
1793
+ message: "Eyes are in shadow - improve lighting"
1794
+ };
1795
+ }
1796
+ if (brightness > thresholds.maxBrightness) {
1797
+ return {
1798
+ passed: false,
1799
+ brightness,
1800
+ contrast,
1801
+ hasGlare,
1802
+ glareRatio,
1803
+ message: "Eye region overexposed - reduce lighting"
1804
+ };
1805
+ }
1806
+ if (hasGlare) {
1807
+ return {
1808
+ passed: false,
1809
+ brightness,
1810
+ contrast,
1811
+ hasGlare,
1812
+ glareRatio,
1813
+ message: "Glare detected - adjust angle or remove glasses"
1814
+ };
1815
+ }
1816
+ if (contrast < thresholds.minContrast) {
1817
+ return {
1818
+ passed: false,
1819
+ brightness,
1820
+ contrast,
1821
+ hasGlare,
1822
+ glareRatio,
1823
+ message: "Eyes not clearly visible"
1824
+ };
1825
+ }
1826
+ return {
1827
+ passed: true,
1828
+ brightness,
1829
+ contrast,
1830
+ hasGlare,
1831
+ glareRatio,
1832
+ message: null
1833
+ };
1834
+ }
1526
1835
  // Annotate the CommonJS export names for ESM import in node:
1527
1836
  0 && (module.exports = {
1528
1837
  ALIGNMENT_THRESHOLD_CAPTURE,
@@ -1530,6 +1839,7 @@ function validateFaceLandmarks(landmarks) {
1530
1839
  ALIGNMENT_THRESHOLD_PERFECT,
1531
1840
  ALIGNMENT_THRESHOLD_POOR,
1532
1841
  API_ENDPOINTS,
1842
+ API_ERROR_CODES,
1533
1843
  API_PATHS,
1534
1844
  AUTH_CONFIG,
1535
1845
  BACKLIT_RATIO_THRESHOLD,
@@ -1545,7 +1855,11 @@ function validateFaceLandmarks(landmarks) {
1545
1855
  DEFAULT_OVAL_REGION,
1546
1856
  DEFAULT_STABILIZER_CONFIG,
1547
1857
  DEFAULT_STATUS_MESSAGES,
1858
+ ERROR_MESSAGES,
1859
+ ERROR_MESSAGES_ES,
1548
1860
  ES_LOCALE,
1861
+ EYE_LANDMARK_INDICES,
1862
+ EYE_QUALITY_THRESHOLDS,
1549
1863
  FACE_CENTER_VERTICAL_OFFSET,
1550
1864
  FACE_CROP_OUTPUT_SIZE,
1551
1865
  FEEDBACK_MESSAGES,
@@ -1579,17 +1893,23 @@ function validateFaceLandmarks(landmarks) {
1579
1893
  RETRY_CONFIG,
1580
1894
  TARGET_FACE_PERCENTAGE_IN_CROP,
1581
1895
  analyzeBlur,
1896
+ analyzeEyeRegionBrightness,
1897
+ analyzeEyeRegionContrast,
1582
1898
  analyzeLighting,
1583
1899
  calculateAdaptiveCropMultiplier,
1584
1900
  calculateBrightness,
1585
1901
  calculateFaceAlignment,
1586
1902
  calculateFaceCropRegion,
1587
1903
  canCaptureFrame,
1904
+ checkEyeRegionQuality,
1588
1905
  checkFrameQuality,
1589
1906
  decodeBase64,
1907
+ detectSpecularHighlights,
1590
1908
  encodeBase64,
1591
1909
  generateSessionId,
1910
+ getApiErrorMessage,
1592
1911
  getCaptureQualityFeedback,
1912
+ getEyeRegionBounds,
1593
1913
  getFeedbackMessage,
1594
1914
  getMinFramesForModel,
1595
1915
  getOvalGuideState,
@@ -1598,6 +1918,7 @@ function validateFaceLandmarks(landmarks) {
1598
1918
  isFaceCropFullyInFrame,
1599
1919
  isFaceFullyVisible,
1600
1920
  isFaceInOval,
1921
+ isRetryableError,
1601
1922
  retryWithBackoff,
1602
1923
  rgbaToGrayscale,
1603
1924
  sleep,
package/dist/index.mjs CHANGED
@@ -135,7 +135,8 @@ function toLivenessResultFromStream(response) {
135
135
  throw new LivenessApiError("Stream not complete", "stream_incomplete", 400);
136
136
  }
137
137
  if (!response.verdict) {
138
- throw new LivenessApiError(response.error ?? "No verdict received", "no_verdict", 500);
138
+ const errorCode = response.error ?? "no_verdict";
139
+ throw new LivenessApiError(response.error ?? "No verdict received", errorCode, 500);
139
140
  }
140
141
  return {
141
142
  verdict: response.verdict,
@@ -156,7 +157,7 @@ function generateSessionId() {
156
157
  return v.toString(16);
157
158
  });
158
159
  }
159
- var LivenessClient = class {
160
+ var LivenessClient = class _LivenessClient {
160
161
  constructor(config) {
161
162
  this.baseUrl = (config.baseUrl ?? DEFAULT_ENDPOINT).replace(/\/$/, "");
162
163
  this.apiKey = config.apiKey;
@@ -183,14 +184,7 @@ var LivenessClient = class {
183
184
  });
184
185
  clearTimeout(timeoutId);
185
186
  if (!response.ok) {
186
- const errorData = await response.json();
187
- throw new LivenessApiError(
188
- errorData.message,
189
- errorData.error,
190
- response.status,
191
- errorData.required,
192
- errorData.received
193
- );
187
+ throw await this.parseErrorResponse(response);
194
188
  }
195
189
  return await response.json();
196
190
  } catch (error) {
@@ -208,6 +202,75 @@ var LivenessClient = class {
208
202
  );
209
203
  }
210
204
  }
205
+ /**
206
+ * Parse an error response body, handling both JSON and non-JSON bodies.
207
+ *
208
+ * The backend sometimes wraps the real error in a generic envelope:
209
+ * ```json
210
+ * { "error": "http_error", "message": "{'error': 'session_expired', ...}" }
211
+ * ```
212
+ * This method unwraps such envelopes to extract the real error code.
213
+ */
214
+ async parseErrorResponse(response) {
215
+ const status = response.status;
216
+ let body;
217
+ try {
218
+ body = await response.text();
219
+ } catch {
220
+ return new LivenessApiError(`Request failed with status ${status}`, "server_error", status);
221
+ }
222
+ try {
223
+ const data = JSON.parse(body);
224
+ if (data.error) {
225
+ const realCode = _LivenessClient.unwrapErrorCode(data.error, data.message);
226
+ const realMessage = _LivenessClient.unwrapErrorMessage(data.message);
227
+ return new LivenessApiError(
228
+ realMessage ?? `Request failed (${realCode})`,
229
+ realCode,
230
+ status,
231
+ data.required,
232
+ data.received
233
+ );
234
+ }
235
+ } catch {
236
+ }
237
+ const code = _LivenessClient.extractCodeFromText(body) ?? "server_error";
238
+ return new LivenessApiError(`Request failed with status ${status}`, code, status);
239
+ }
240
+ /**
241
+ * When the outer error code is a generic wrapper (e.g. "http_error"),
242
+ * try to extract the real error code from the message text.
243
+ */
244
+ static unwrapErrorCode(outerCode, message) {
245
+ const WRAPPER_CODES = /* @__PURE__ */ new Set([
246
+ "http_error",
247
+ "upstream_error",
248
+ "proxy_error",
249
+ "internal_error"
250
+ ]);
251
+ if (!WRAPPER_CODES.has(outerCode) || !message) {
252
+ return outerCode;
253
+ }
254
+ return _LivenessClient.extractCodeFromText(message) ?? outerCode;
255
+ }
256
+ /**
257
+ * Try to extract a human-readable message from a nested Python dict string.
258
+ * E.g.: "{'error': 'session_expired', 'message': \"Create a new session...\"}"
259
+ * → "Create a new session..."
260
+ */
261
+ static unwrapErrorMessage(message) {
262
+ if (!message) return void 0;
263
+ const msgMatch = /'message'\s*:\s*"([^"]+)"/.exec(message) ?? /'message'\s*:\s*'([^']+)'/.exec(message) ?? /"message"\s*:\s*"([^"]+)"/.exec(message);
264
+ return msgMatch?.[1] ?? void 0;
265
+ }
266
+ /**
267
+ * Extract an error code from raw text using regex.
268
+ * Handles both Python dict (`'error': 'code'`) and JSON (`"error": "code"`).
269
+ */
270
+ static extractCodeFromText(text) {
271
+ const match = /'error'\s*:\s*'([^']+)'/.exec(text) ?? /"error"\s*:\s*"([^"]+)"/.exec(text);
272
+ return match?.[1] ?? null;
273
+ }
211
274
  /**
212
275
  * Make a request with optional retry
213
276
  */
@@ -839,6 +902,81 @@ var DEFAULT_STABILIZER_CONFIG = {
839
902
  sampleSize: 64
840
903
  };
841
904
 
905
+ // src/constants/errors.ts
906
+ var API_ERROR_CODES = {
907
+ SESSION_EXPIRED: "session_expired",
908
+ INVALID_KEY: "invalid_key",
909
+ INVALID_MODEL: "invalid_model",
910
+ INSUFFICIENT_FRAMES: "insufficient_frames",
911
+ MISSING_FIELD: "missing_field",
912
+ INVALID_FRAME: "invalid_frame",
913
+ INVALID_SESSION: "invalid_session",
914
+ RATE_LIMITED: "rate_limited",
915
+ STREAM_INCOMPLETE: "stream_incomplete",
916
+ NO_VERDICT: "no_verdict",
917
+ TIMEOUT: "timeout",
918
+ NETWORK_ERROR: "network_error",
919
+ SERVER_ERROR: "server_error"
920
+ };
921
+ var ERROR_MESSAGES = {
922
+ [API_ERROR_CODES.SESSION_EXPIRED]: "Session expired \u2014 please restart the verification",
923
+ [API_ERROR_CODES.INVALID_KEY]: "Invalid API key \u2014 contact your administrator",
924
+ [API_ERROR_CODES.INVALID_MODEL]: "Invalid model configuration",
925
+ [API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Not enough frames captured \u2014 try again",
926
+ [API_ERROR_CODES.MISSING_FIELD]: "Missing required data \u2014 please try again",
927
+ [API_ERROR_CODES.INVALID_FRAME]: "Invalid frame data \u2014 please try again",
928
+ [API_ERROR_CODES.INVALID_SESSION]: "Invalid session \u2014 please restart",
929
+ [API_ERROR_CODES.RATE_LIMITED]: "Too many requests \u2014 wait a moment and try again",
930
+ [API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incomplete \u2014 please try again",
931
+ [API_ERROR_CODES.NO_VERDICT]: "No result received \u2014 please try again",
932
+ [API_ERROR_CODES.TIMEOUT]: "Request timed out \u2014 check your connection",
933
+ [API_ERROR_CODES.NETWORK_ERROR]: "Network error \u2014 check your connection",
934
+ [API_ERROR_CODES.SERVER_ERROR]: "Server error \u2014 please try again later"
935
+ };
936
+ var ERROR_MESSAGES_ES = {
937
+ [API_ERROR_CODES.SESSION_EXPIRED]: "Sesi\xF3n expirada \u2014 reinicie la verificaci\xF3n",
938
+ [API_ERROR_CODES.INVALID_KEY]: "Clave API inv\xE1lida \u2014 contacte al administrador",
939
+ [API_ERROR_CODES.INVALID_MODEL]: "Configuraci\xF3n de modelo inv\xE1lida",
940
+ [API_ERROR_CODES.INSUFFICIENT_FRAMES]: "Frames insuficientes \u2014 intente de nuevo",
941
+ [API_ERROR_CODES.MISSING_FIELD]: "Datos requeridos faltantes \u2014 intente de nuevo",
942
+ [API_ERROR_CODES.INVALID_FRAME]: "Frame inv\xE1lido \u2014 intente de nuevo",
943
+ [API_ERROR_CODES.INVALID_SESSION]: "Sesi\xF3n inv\xE1lida \u2014 reinicie",
944
+ [API_ERROR_CODES.RATE_LIMITED]: "Demasiadas solicitudes \u2014 espere un momento",
945
+ [API_ERROR_CODES.STREAM_INCOMPLETE]: "Streaming incompleto \u2014 intente de nuevo",
946
+ [API_ERROR_CODES.NO_VERDICT]: "Sin resultado \u2014 intente de nuevo",
947
+ [API_ERROR_CODES.TIMEOUT]: "Tiempo de espera agotado \u2014 verifique su conexi\xF3n",
948
+ [API_ERROR_CODES.NETWORK_ERROR]: "Error de red \u2014 verifique su conexi\xF3n",
949
+ [API_ERROR_CODES.SERVER_ERROR]: "Error del servidor \u2014 intente m\xE1s tarde"
950
+ };
951
+ function looksLikeRawData(text) {
952
+ const trimmed = text.trim();
953
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) return true;
954
+ if (/'error'\s*:/.test(trimmed)) return true;
955
+ if (/"error"\s*:/.test(trimmed)) return true;
956
+ return false;
957
+ }
958
+ var GENERIC_FALLBACK = "An unexpected error occurred \u2014 please try again";
959
+ function getApiErrorMessage(code, message, customMessages) {
960
+ if (code) {
961
+ const custom = customMessages?.[code];
962
+ if (custom) return custom;
963
+ const known = ERROR_MESSAGES[code];
964
+ if (known) return known;
965
+ }
966
+ if (message && !looksLikeRawData(message)) {
967
+ return message;
968
+ }
969
+ return GENERIC_FALLBACK;
970
+ }
971
+ function isRetryableError(code) {
972
+ if (!code) return true;
973
+ const nonRetryable = /* @__PURE__ */ new Set([
974
+ API_ERROR_CODES.INVALID_KEY,
975
+ API_ERROR_CODES.INVALID_MODEL
976
+ ]);
977
+ return !nonRetryable.has(code);
978
+ }
979
+
842
980
  // src/constants/feedback.ts
843
981
  var ALIGNMENT_THRESHOLD_CAPTURE = 0.6;
844
982
  var ALIGNMENT_THRESHOLD_POOR = 0.5;
@@ -905,6 +1043,12 @@ var FEEDBACK_MESSAGES = {
905
1043
  backlit: "Backlit - try facing the light source",
906
1044
  // Hand occlusion
907
1045
  hand_detected: "Remove hand from face",
1046
+ // Eye region quality
1047
+ eyes_not_visible: "Eyes not clearly visible",
1048
+ eyes_shadowed: "Eyes are in shadow - improve lighting",
1049
+ eyes_overexposed: "Eye region overexposed - reduce lighting",
1050
+ glasses_glare: "Glare detected - adjust angle or remove glasses",
1051
+ eye_quality_poor: "Eye region quality is poor",
908
1052
  // Progress messages
909
1053
  capturing: "Capturing...",
910
1054
  almost_done: "Almost done...",
@@ -956,6 +1100,12 @@ var ES_LOCALE = {
956
1100
  backlit: "Contraluz - intenta mirar hacia la fuente de luz",
957
1101
  // Hand occlusion
958
1102
  hand_detected: "Retira la mano del rostro",
1103
+ // Eye region quality
1104
+ eyes_not_visible: "Ojos no visibles claramente",
1105
+ eyes_shadowed: "Ojos en sombra - mejora la iluminaci\xF3n",
1106
+ eyes_overexposed: "Regi\xF3n de ojos sobreexpuesta - reduce la iluminaci\xF3n",
1107
+ glasses_glare: "Reflejo detectado - ajusta el \xE1ngulo o retira los lentes",
1108
+ eye_quality_poor: "Calidad de la regi\xF3n de ojos insuficiente",
959
1109
  // Progress messages
960
1110
  capturing: "Capturando...",
961
1111
  almost_done: "Casi listo...",
@@ -1389,6 +1539,44 @@ var LANDMARK_INDEX = {
1389
1539
  /** Lower lip center */
1390
1540
  LOWER_LIP: 14
1391
1541
  };
1542
+ var EYE_LANDMARK_INDICES = {
1543
+ /** Right eye contour (viewer's left) */
1544
+ RIGHT_EYE: [33, 7, 163, 144, 145, 153, 154, 155, 133, 173, 157, 158, 159, 160, 161, 246],
1545
+ /** Left eye contour (viewer's right) */
1546
+ LEFT_EYE: [362, 382, 381, 380, 374, 373, 390, 249, 263, 466, 388, 387, 386, 385, 384, 398]
1547
+ };
1548
+ function getEyeRegionBounds(landmarks) {
1549
+ if (landmarks.length < 468) return null;
1550
+ const PADDING = 0.2;
1551
+ function computeBounds(indices) {
1552
+ let minX = 1;
1553
+ let minY = 1;
1554
+ let maxX = 0;
1555
+ let maxY = 0;
1556
+ for (const idx of indices) {
1557
+ const lm = landmarks[idx];
1558
+ if (!lm) continue;
1559
+ if (lm.x < minX) minX = lm.x;
1560
+ if (lm.y < minY) minY = lm.y;
1561
+ if (lm.x > maxX) maxX = lm.x;
1562
+ if (lm.y > maxY) maxY = lm.y;
1563
+ }
1564
+ const width = maxX - minX;
1565
+ const height = maxY - minY;
1566
+ const padX = width * PADDING;
1567
+ const padY = height * PADDING;
1568
+ return {
1569
+ x: Math.max(0, minX - padX),
1570
+ y: Math.max(0, minY - padY),
1571
+ width: Math.min(1 - Math.max(0, minX - padX), width + padX * 2),
1572
+ height: Math.min(1 - Math.max(0, minY - padY), height + padY * 2)
1573
+ };
1574
+ }
1575
+ return {
1576
+ rightEye: computeBounds(EYE_LANDMARK_INDICES.RIGHT_EYE),
1577
+ leftEye: computeBounds(EYE_LANDMARK_INDICES.LEFT_EYE)
1578
+ };
1579
+ }
1392
1580
  var LANDMARK_MIN_BOUND = 0.1;
1393
1581
  var LANDMARK_MAX_BOUND = 0.9;
1394
1582
  var MIN_LANDMARK_COUNT = 15;
@@ -1410,12 +1598,122 @@ function validateFaceLandmarks(landmarks) {
1410
1598
  }
1411
1599
  return { valid: true };
1412
1600
  }
1601
+
1602
+ // src/utils/eyeRegionAnalysis.ts
1603
+ var EYE_QUALITY_THRESHOLDS = {
1604
+ minBrightness: 40,
1605
+ maxBrightness: 230,
1606
+ minContrast: 12,
1607
+ glarePixelThreshold: 240,
1608
+ maxGlareRatio: 0.15
1609
+ };
1610
+ function rgbaToLuminance(r, g, b) {
1611
+ return 0.299 * r + 0.587 * g + 0.114 * b;
1612
+ }
1613
+ function analyzeEyeRegionBrightness(pixels) {
1614
+ const pixelCount = pixels.length / 4;
1615
+ if (pixelCount === 0) return 0;
1616
+ let totalLuminance = 0;
1617
+ for (let i = 0; i < pixels.length; i += 4) {
1618
+ totalLuminance += rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
1619
+ }
1620
+ return totalLuminance / pixelCount;
1621
+ }
1622
+ function analyzeEyeRegionContrast(pixels, meanBrightness) {
1623
+ const pixelCount = pixels.length / 4;
1624
+ if (pixelCount === 0) return 0;
1625
+ const mean = meanBrightness ?? analyzeEyeRegionBrightness(pixels);
1626
+ let sumSqDiff = 0;
1627
+ for (let i = 0; i < pixels.length; i += 4) {
1628
+ const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
1629
+ const diff = lum - mean;
1630
+ sumSqDiff += diff * diff;
1631
+ }
1632
+ return Math.sqrt(sumSqDiff / pixelCount);
1633
+ }
1634
+ function detectSpecularHighlights(pixels, threshold = EYE_QUALITY_THRESHOLDS.glarePixelThreshold) {
1635
+ const pixelCount = pixels.length / 4;
1636
+ if (pixelCount === 0) return 0;
1637
+ let glareCount = 0;
1638
+ for (let i = 0; i < pixels.length; i += 4) {
1639
+ const lum = rgbaToLuminance(pixels[i], pixels[i + 1], pixels[i + 2]);
1640
+ if (lum >= threshold) {
1641
+ glareCount++;
1642
+ }
1643
+ }
1644
+ return glareCount / pixelCount;
1645
+ }
1646
+ function checkEyeRegionQuality(pixels, thresholds = EYE_QUALITY_THRESHOLDS) {
1647
+ if (pixels.length < 4) {
1648
+ return {
1649
+ passed: false,
1650
+ brightness: 0,
1651
+ contrast: 0,
1652
+ hasGlare: false,
1653
+ glareRatio: 0,
1654
+ message: "Eyes not clearly visible"
1655
+ };
1656
+ }
1657
+ const brightness = analyzeEyeRegionBrightness(pixels);
1658
+ const contrast = analyzeEyeRegionContrast(pixels, brightness);
1659
+ const glareRatio = detectSpecularHighlights(pixels, thresholds.glarePixelThreshold);
1660
+ const hasGlare = glareRatio > thresholds.maxGlareRatio;
1661
+ if (brightness < thresholds.minBrightness) {
1662
+ return {
1663
+ passed: false,
1664
+ brightness,
1665
+ contrast,
1666
+ hasGlare,
1667
+ glareRatio,
1668
+ message: "Eyes are in shadow - improve lighting"
1669
+ };
1670
+ }
1671
+ if (brightness > thresholds.maxBrightness) {
1672
+ return {
1673
+ passed: false,
1674
+ brightness,
1675
+ contrast,
1676
+ hasGlare,
1677
+ glareRatio,
1678
+ message: "Eye region overexposed - reduce lighting"
1679
+ };
1680
+ }
1681
+ if (hasGlare) {
1682
+ return {
1683
+ passed: false,
1684
+ brightness,
1685
+ contrast,
1686
+ hasGlare,
1687
+ glareRatio,
1688
+ message: "Glare detected - adjust angle or remove glasses"
1689
+ };
1690
+ }
1691
+ if (contrast < thresholds.minContrast) {
1692
+ return {
1693
+ passed: false,
1694
+ brightness,
1695
+ contrast,
1696
+ hasGlare,
1697
+ glareRatio,
1698
+ message: "Eyes not clearly visible"
1699
+ };
1700
+ }
1701
+ return {
1702
+ passed: true,
1703
+ brightness,
1704
+ contrast,
1705
+ hasGlare,
1706
+ glareRatio,
1707
+ message: null
1708
+ };
1709
+ }
1413
1710
  export {
1414
1711
  ALIGNMENT_THRESHOLD_CAPTURE,
1415
1712
  ALIGNMENT_THRESHOLD_GOOD,
1416
1713
  ALIGNMENT_THRESHOLD_PERFECT,
1417
1714
  ALIGNMENT_THRESHOLD_POOR,
1418
1715
  API_ENDPOINTS,
1716
+ API_ERROR_CODES,
1419
1717
  API_PATHS,
1420
1718
  AUTH_CONFIG,
1421
1719
  BACKLIT_RATIO_THRESHOLD,
@@ -1431,7 +1729,11 @@ export {
1431
1729
  DEFAULT_OVAL_REGION,
1432
1730
  DEFAULT_STABILIZER_CONFIG,
1433
1731
  DEFAULT_STATUS_MESSAGES,
1732
+ ERROR_MESSAGES,
1733
+ ERROR_MESSAGES_ES,
1434
1734
  ES_LOCALE,
1735
+ EYE_LANDMARK_INDICES,
1736
+ EYE_QUALITY_THRESHOLDS,
1435
1737
  FACE_CENTER_VERTICAL_OFFSET,
1436
1738
  FACE_CROP_OUTPUT_SIZE,
1437
1739
  FEEDBACK_MESSAGES,
@@ -1465,17 +1767,23 @@ export {
1465
1767
  RETRY_CONFIG,
1466
1768
  TARGET_FACE_PERCENTAGE_IN_CROP,
1467
1769
  analyzeBlur,
1770
+ analyzeEyeRegionBrightness,
1771
+ analyzeEyeRegionContrast,
1468
1772
  analyzeLighting,
1469
1773
  calculateAdaptiveCropMultiplier,
1470
1774
  calculateBrightness,
1471
1775
  calculateFaceAlignment,
1472
1776
  calculateFaceCropRegion,
1473
1777
  canCaptureFrame,
1778
+ checkEyeRegionQuality,
1474
1779
  checkFrameQuality,
1475
1780
  decodeBase64,
1781
+ detectSpecularHighlights,
1476
1782
  encodeBase64,
1477
1783
  generateSessionId,
1784
+ getApiErrorMessage,
1478
1785
  getCaptureQualityFeedback,
1786
+ getEyeRegionBounds,
1479
1787
  getFeedbackMessage,
1480
1788
  getMinFramesForModel,
1481
1789
  getOvalGuideState,
@@ -1484,6 +1792,7 @@ export {
1484
1792
  isFaceCropFullyInFrame,
1485
1793
  isFaceFullyVisible,
1486
1794
  isFaceInOval,
1795
+ isRetryableError,
1487
1796
  retryWithBackoff,
1488
1797
  rgbaToGrayscale,
1489
1798
  sleep,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moveris/shared",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "Core business logic for Moveris Live SDK",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",