vesant-sdk 1.7.0-dev.fce9898 → 1.7.0-dev.fe84882

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 (75) hide show
  1. package/dist/{client-DoMSYMMR.d.ts → client-0NOPDnT0.d.ts} +29 -26
  2. package/dist/{client-DMIRx7Tu.d.mts → client-CvWNRwwg.d.mts} +29 -26
  3. package/dist/{client-ZNdnpWe7.d.mts → client-Dps40EtF.d.mts} +2 -2
  4. package/dist/{client-C3DCmGe9.d.ts → client-SMxqod4j.d.ts} +2 -2
  5. package/dist/{client-BolQlL5e.d.mts → client-q9fN8Hhf.d.mts} +103 -1
  6. package/dist/{client-BolQlL5e.d.ts → client-q9fN8Hhf.d.ts} +103 -1
  7. package/dist/compliance/index.d.mts +3 -3
  8. package/dist/compliance/index.d.ts +3 -3
  9. package/dist/compliance/index.js +133 -22
  10. package/dist/compliance/index.js.map +1 -1
  11. package/dist/compliance/index.mjs +133 -22
  12. package/dist/compliance/index.mjs.map +1 -1
  13. package/dist/decisions/index.d.mts +1 -1
  14. package/dist/decisions/index.d.ts +1 -1
  15. package/dist/decisions/index.js +68 -7
  16. package/dist/decisions/index.js.map +1 -1
  17. package/dist/decisions/index.mjs +68 -7
  18. package/dist/decisions/index.mjs.map +1 -1
  19. package/dist/geolocation/index.d.mts +59 -4
  20. package/dist/geolocation/index.d.ts +59 -4
  21. package/dist/geolocation/index.js +136 -19
  22. package/dist/geolocation/index.js.map +1 -1
  23. package/dist/geolocation/index.mjs +134 -20
  24. package/dist/geolocation/index.mjs.map +1 -1
  25. package/dist/index-C3zUKydT.d.mts +264 -0
  26. package/dist/index-DSDgS09k.d.ts +264 -0
  27. package/dist/index.d.mts +26 -9
  28. package/dist/index.d.ts +26 -9
  29. package/dist/index.js +324 -57
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +317 -58
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/kyc/core.d.mts +2 -2
  34. package/dist/kyc/core.d.ts +2 -2
  35. package/dist/kyc/core.js +94 -19
  36. package/dist/kyc/core.js.map +1 -1
  37. package/dist/kyc/core.mjs +94 -19
  38. package/dist/kyc/core.mjs.map +1 -1
  39. package/dist/kyc/index.d.mts +90 -13
  40. package/dist/kyc/index.d.ts +90 -13
  41. package/dist/kyc/index.js +94 -19
  42. package/dist/kyc/index.js.map +1 -1
  43. package/dist/kyc/index.mjs +94 -19
  44. package/dist/kyc/index.mjs.map +1 -1
  45. package/dist/react.d.mts +9 -3
  46. package/dist/react.d.ts +9 -3
  47. package/dist/react.js +180 -77
  48. package/dist/react.js.map +1 -1
  49. package/dist/react.mjs +180 -77
  50. package/dist/react.mjs.map +1 -1
  51. package/dist/risk-profile/index.d.mts +1 -1
  52. package/dist/risk-profile/index.d.ts +1 -1
  53. package/dist/risk-profile/index.js +68 -7
  54. package/dist/risk-profile/index.js.map +1 -1
  55. package/dist/risk-profile/index.mjs +68 -7
  56. package/dist/risk-profile/index.mjs.map +1 -1
  57. package/dist/scores/index.d.mts +1 -1
  58. package/dist/scores/index.d.ts +1 -1
  59. package/dist/scores/index.js +68 -7
  60. package/dist/scores/index.js.map +1 -1
  61. package/dist/scores/index.mjs +68 -7
  62. package/dist/scores/index.mjs.map +1 -1
  63. package/dist/tax/index.d.mts +2 -2
  64. package/dist/tax/index.d.ts +2 -2
  65. package/dist/tax/index.js +73 -9
  66. package/dist/tax/index.js.map +1 -1
  67. package/dist/tax/index.mjs +73 -9
  68. package/dist/tax/index.mjs.map +1 -1
  69. package/dist/webhooks/index.d.mts +2 -189
  70. package/dist/webhooks/index.d.ts +2 -189
  71. package/dist/webhooks/index.js +85 -21
  72. package/dist/webhooks/index.js.map +1 -1
  73. package/dist/webhooks/index.mjs +85 -22
  74. package/dist/webhooks/index.mjs.map +1 -1
  75. package/package.json +4 -1
package/dist/react.d.mts CHANGED
@@ -1,9 +1,9 @@
1
- import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-DMIRx7Tu.mjs';
2
- import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-ZNdnpWe7.mjs';
1
+ import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-CvWNRwwg.mjs';
2
+ import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-Dps40EtF.mjs';
3
3
  import { RiskProfileClient } from './risk-profile/index.mjs';
4
4
  import { C as CustomerProfile } from './types-BOFaMQxI.mjs';
5
5
  import { KycClient, CreateEventBasedFaceVerificationSessionRequest, CreateEventBasedFaceVerificationSessionResponse, EventBasedFaceVerificationCallback, UseKycAlertsOptions, UseKycAlertsResult, UseKycOverviewOptions, UseKycOverviewResult, UseKycPreferencesResult, UseKycRequestsOptions, UseKycRequestsResult, UseKycSubmissionOptions, UseKycSubmissionResult } from './kyc/index.mjs';
6
- import './client-BolQlL5e.mjs';
6
+ import './client-q9fN8Hhf.mjs';
7
7
  import './types-CBQRNL-l.mjs';
8
8
 
9
9
  /**
@@ -21,6 +21,10 @@ declare function getBrowserInfo(): {
21
21
  * React Hooks for Vesant Geolocation SDK
22
22
  *
23
23
  * Provides convenient React hooks for using the geolocation service in Next.js applications.
24
+ *
25
+ * Scope: the web SDK collects GPS + IP only (no WiFi/cell positioning — no browser
26
+ * API exists). The client never asserts its own IP; the server derives it. GPS
27
+ * fixes are run through advisory in-browser integrity heuristics (see ./integrity).
24
28
  */
25
29
 
26
30
  /**
@@ -530,11 +534,13 @@ declare function useEventBasedFaceVerificationSubmission(client: KycClient): {
530
534
  defaultDevice?: "ask" | "this" | "mobile";
531
535
  renderQR?: (payload: string) => React.ReactNode;
532
536
  onCancel?: () => void;
537
+ mediapipeAssetBasePath?: string;
533
538
  }) => Promise<EventBasedFaceVerificationCallback | null>;
534
539
  runFlow: (request: CreateEventBasedFaceVerificationSessionRequest, opts?: {
535
540
  defaultDevice?: "ask" | "this" | "mobile";
536
541
  renderQR?: (payload: string) => React.ReactNode;
537
542
  onCancel?: () => void;
543
+ mediapipeAssetBasePath?: string;
538
544
  }) => Promise<{
539
545
  kind: "not_required";
540
546
  session: CreateEventBasedFaceVerificationSessionResponse;
package/dist/react.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-DoMSYMMR.js';
2
- import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-C3DCmGe9.js';
1
+ import { D as DeviceFingerprintRequest, G as GeolocationClient, i as CipherTextOptions, l as CipherTextResult, W as UseGeolocationOptions, X as UseGeolocationResult, Y as UseLocationCaptureOptions, Z as UseLocationCaptureResult, _ as UseLocationRequestsOptions, $ as UseLocationRequestsResult } from './client-0NOPDnT0.js';
2
+ import { e as LoginVerificationResponse, L as LoginVerificationRequest, f as RegistrationVerificationResponse, R as RegistrationVerificationRequest, h as TransactionVerificationResponse, g as TransactionVerificationRequest, C as ComplianceClient } from './client-SMxqod4j.js';
3
3
  import { RiskProfileClient } from './risk-profile/index.js';
4
4
  import { C as CustomerProfile } from './types-UGyDl1fd.js';
5
5
  import { KycClient, CreateEventBasedFaceVerificationSessionRequest, CreateEventBasedFaceVerificationSessionResponse, EventBasedFaceVerificationCallback, UseKycAlertsOptions, UseKycAlertsResult, UseKycOverviewOptions, UseKycOverviewResult, UseKycPreferencesResult, UseKycRequestsOptions, UseKycRequestsResult, UseKycSubmissionOptions, UseKycSubmissionResult } from './kyc/index.js';
6
- import './client-BolQlL5e.js';
6
+ import './client-q9fN8Hhf.js';
7
7
  import './types-CBQRNL-l.js';
8
8
 
9
9
  /**
@@ -21,6 +21,10 @@ declare function getBrowserInfo(): {
21
21
  * React Hooks for Vesant Geolocation SDK
22
22
  *
23
23
  * Provides convenient React hooks for using the geolocation service in Next.js applications.
24
+ *
25
+ * Scope: the web SDK collects GPS + IP only (no WiFi/cell positioning — no browser
26
+ * API exists). The client never asserts its own IP; the server derives it. GPS
27
+ * fixes are run through advisory in-browser integrity heuristics (see ./integrity).
24
28
  */
25
29
 
26
30
  /**
@@ -530,11 +534,13 @@ declare function useEventBasedFaceVerificationSubmission(client: KycClient): {
530
534
  defaultDevice?: "ask" | "this" | "mobile";
531
535
  renderQR?: (payload: string) => React.ReactNode;
532
536
  onCancel?: () => void;
537
+ mediapipeAssetBasePath?: string;
533
538
  }) => Promise<EventBasedFaceVerificationCallback | null>;
534
539
  runFlow: (request: CreateEventBasedFaceVerificationSessionRequest, opts?: {
535
540
  defaultDevice?: "ask" | "this" | "mobile";
536
541
  renderQR?: (payload: string) => React.ReactNode;
537
542
  onCancel?: () => void;
543
+ mediapipeAssetBasePath?: string;
538
544
  }) => Promise<{
539
545
  kind: "not_required";
540
546
  session: CreateEventBasedFaceVerificationSessionResponse;
package/dist/react.js CHANGED
@@ -93,6 +93,61 @@ function getBrowserInfo() {
93
93
  return { browser, browser_version: browserVersion, os, os_version: osVersion };
94
94
  }
95
95
 
96
+ // src/geolocation/integrity.ts
97
+ var isNil = (v) => v === null || v === void 0;
98
+ function assessGpsIntegrity(input, opts = {}) {
99
+ const flags = [];
100
+ const now = opts.now ?? (() => Date.now());
101
+ const maxAgeMs = opts.maxAgeMs ?? 6e4;
102
+ const minAccuracy = opts.minAccuracyMeters ?? 1;
103
+ const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
104
+ if (isNil(accuracy)) {
105
+ flags.push("accuracy_missing");
106
+ } else if (accuracy === 0) {
107
+ flags.push("accuracy_zero");
108
+ } else if (accuracy < minAccuracy) {
109
+ flags.push("accuracy_implausibly_small");
110
+ }
111
+ const hasMotion = !isNil(heading) || !isNil(speed);
112
+ if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
113
+ flags.push("motion_without_accuracy");
114
+ }
115
+ if (!isNil(altitude) && isNil(altitudeAccuracy)) {
116
+ flags.push("altitude_without_accuracy");
117
+ }
118
+ if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
119
+ flags.push("stale_timestamp");
120
+ }
121
+ if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
122
+ flags.push(`permission_${opts.permissionState}`);
123
+ }
124
+ return { suspicious: flags.length > 0, flags };
125
+ }
126
+ function gpsIntegrityInputFromPosition(position) {
127
+ const c = position.coords;
128
+ return {
129
+ latitude: c.latitude,
130
+ longitude: c.longitude,
131
+ accuracy: c.accuracy,
132
+ altitude: c.altitude,
133
+ altitudeAccuracy: c.altitudeAccuracy,
134
+ heading: c.heading,
135
+ speed: c.speed,
136
+ timestamp: position.timestamp
137
+ };
138
+ }
139
+ async function probeGeolocationPermission() {
140
+ try {
141
+ if (typeof navigator === "undefined" || !navigator.permissions?.query) {
142
+ return "unavailable";
143
+ }
144
+ const status = await navigator.permissions.query({ name: "geolocation" });
145
+ return status.state;
146
+ } catch {
147
+ return "unavailable";
148
+ }
149
+ }
150
+
96
151
  // src/geolocation/ciphertext.ts
97
152
  var CIPHER_TEXT_EXPIRY_MINUTES = 5;
98
153
  async function computeHMAC(key, message) {
@@ -145,6 +200,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
145
200
  if (typeof navigator === "undefined" || !navigator.geolocation) {
146
201
  return null;
147
202
  }
203
+ const permissionState = await probeGeolocationPermission();
148
204
  return new Promise((resolve) => {
149
205
  navigator.geolocation.getCurrentPosition(
150
206
  (position) => {
@@ -161,6 +217,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
161
217
  resolve(null);
162
218
  return;
163
219
  }
220
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
164
221
  resolve({
165
222
  latitude,
166
223
  longitude,
@@ -169,7 +226,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
169
226
  altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
170
227
  heading: position.coords.heading ?? void 0,
171
228
  speed: position.coords.speed ?? void 0,
172
- timestamp: position.timestamp
229
+ timestamp: position.timestamp,
230
+ integrity_flags: flags.length ? flags : void 0
173
231
  });
174
232
  },
175
233
  () => {
@@ -236,6 +294,9 @@ async function generateCipherText(options, config) {
236
294
  );
237
295
  if (location) {
238
296
  locationData = location;
297
+ if (location.integrity_flags?.length) {
298
+ warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
299
+ }
239
300
  } else if (gpsRequiredByConfig) {
240
301
  throw new VesantError(
241
302
  `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
@@ -338,8 +399,6 @@ function useGeolocation(client, options = {}) {
338
399
  if (optionsRef.current.autoVerify) {
339
400
  const detectAndVerify = async () => {
340
401
  try {
341
- const ipResponse = await fetch("https://api.ipify.org?format=json");
342
- const { ip } = await ipResponse.json();
343
402
  let deviceFingerprint;
344
403
  if (optionsRef.current.includeDeviceFingerprint && typeof window !== "undefined") {
345
404
  const deviceId = generateDeviceId();
@@ -353,7 +412,6 @@ function useGeolocation(client, options = {}) {
353
412
  };
354
413
  }
355
414
  await verifyIP({
356
- ip_address: ip,
357
415
  user_id: optionsRef.current.userId || "",
358
416
  event_type: optionsRef.current.eventType || "page_view",
359
417
  device_fingerprint: deviceFingerprint
@@ -667,6 +725,12 @@ function useLocationCapture(client, options) {
667
725
  if (accuracy !== null && accuracy !== void 0 && accuracy > 0) {
668
726
  capture.accuracy = accuracy;
669
727
  }
728
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), {
729
+ permissionState: await probeGeolocationPermission()
730
+ });
731
+ if (flags.length) {
732
+ capture.integrity_flags = flags;
733
+ }
670
734
  }
671
735
  }
672
736
  if (gpsError) {
@@ -889,6 +953,87 @@ var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i
889
953
  var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
890
954
  var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
891
955
 
956
+ // src/kyc/faceDetection.ts
957
+ var FACE_MESSAGES = {
958
+ idle: "Initializing camera...",
959
+ loading: "Loading face detection...",
960
+ scanning: "Position your face in the circle",
961
+ too_far: "Face too far. Please move closer to the camera",
962
+ too_close: "Too close. Please move back a little",
963
+ off_center: "Center your face in the circle",
964
+ ok: "Looking great! Tap the button to capture"
965
+ };
966
+ var FACE_COLORS = {
967
+ idle: "#9ca3af",
968
+ loading: "#3b82f6",
969
+ scanning: "#3b82f6",
970
+ too_far: "#ef4444",
971
+ too_close: "#f59e0b",
972
+ off_center: "#f59e0b",
973
+ ok: "#10b981"
974
+ };
975
+ var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
976
+ function mapDetectionsToFaceStatus(detections) {
977
+ if (detections.length === 0) return "scanning";
978
+ const bb = detections[0].boundingBox;
979
+ if (bb.width < 0.18) return "too_far";
980
+ if (bb.width > 0.72) return "too_close";
981
+ if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
982
+ return "off_center";
983
+ }
984
+ return "ok";
985
+ }
986
+ function isCaptureDisabled(ready, detectionActive, faceStatus) {
987
+ return !ready || detectionActive && faceStatus !== "ok";
988
+ }
989
+ function resolveMediaPipeBase(base) {
990
+ if (!base) return MEDIAPIPE_BASE;
991
+ return base.endsWith("/") ? base.slice(0, -1) : base;
992
+ }
993
+ var LIVENESS_SAMPLE_LIMIT = 20;
994
+ function buildLivenessArtifact(samples) {
995
+ if (samples.length === 0) return void 0;
996
+ return JSON.stringify({
997
+ version: 1,
998
+ source: "mediapipe-face-detection",
999
+ samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
1000
+ });
1001
+ }
1002
+ var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
1003
+ function loadMediaPipeFaceDetection(assetBasePath) {
1004
+ if (typeof window === "undefined" || typeof document === "undefined") {
1005
+ return Promise.reject(new Error("MediaPipe requires a browser environment"));
1006
+ }
1007
+ const w = window;
1008
+ if (w.FaceDetection) {
1009
+ return Promise.resolve(w.FaceDetection);
1010
+ }
1011
+ const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
1012
+ const existing = mediapipeLoaderPromises.get(src);
1013
+ if (existing) return existing;
1014
+ const loader = new Promise((resolve, reject) => {
1015
+ const script = document.createElement("script");
1016
+ script.src = src;
1017
+ script.async = true;
1018
+ script.crossOrigin = "anonymous";
1019
+ script.onload = () => {
1020
+ if (w.FaceDetection) {
1021
+ resolve(w.FaceDetection);
1022
+ } else {
1023
+ mediapipeLoaderPromises.delete(src);
1024
+ reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1025
+ }
1026
+ };
1027
+ script.onerror = () => {
1028
+ mediapipeLoaderPromises.delete(src);
1029
+ reject(new Error("Failed to load MediaPipe face_detection script"));
1030
+ };
1031
+ document.head.appendChild(script);
1032
+ });
1033
+ mediapipeLoaderPromises.set(src, loader);
1034
+ return loader;
1035
+ }
1036
+
892
1037
  // src/kyc/FaceCaptureModal.tsx
893
1038
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
894
1039
  var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
@@ -922,7 +1067,8 @@ function FaceCaptureModal({
922
1067
  onComplete,
923
1068
  onCancel,
924
1069
  defaultDevice,
925
- renderQR
1070
+ renderQR,
1071
+ mediapipeAssetBasePath
926
1072
  }) {
927
1073
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
928
1074
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
@@ -939,6 +1085,7 @@ function FaceCaptureModal({
939
1085
  const [captureMode, setCaptureMode] = React.useState("idle");
940
1086
  const [capturedPreview, setCapturedPreview] = React.useState(null);
941
1087
  const [capturedBase64, setCapturedBase64] = React.useState(null);
1088
+ const [capturedArtifact, setCapturedArtifact] = React.useState(null);
942
1089
  const [livenessMessage, setLivenessMessage] = React.useState(LIVENESS_MESSAGES[0]);
943
1090
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
944
1091
  const cancelledRef = React.useRef(false);
@@ -1036,7 +1183,10 @@ function FaceCaptureModal({
1036
1183
  const data = await client.submitEventBasedFaceVerificationSession({
1037
1184
  token: session.token,
1038
1185
  reference: ref,
1039
- proof
1186
+ proof,
1187
+ // undefined when detection wasn't running for this capture —
1188
+ // JSON.stringify drops it, so the field is absent on the wire.
1189
+ liveness_artifact: capturedArtifact ?? void 0
1040
1190
  });
1041
1191
  return { ok: true, data };
1042
1192
  } catch (err2) {
@@ -1082,16 +1232,18 @@ function FaceCaptureModal({
1082
1232
  submittingRef.current = false;
1083
1233
  }
1084
1234
  };
1085
- const handleCameraCapture = (dataUrl) => {
1235
+ const handleCameraCapture = (dataUrl, livenessArtifact) => {
1086
1236
  const raw = dataUrl.split(",")[1] ?? "";
1087
1237
  setCapturedPreview(dataUrl);
1088
1238
  setCapturedBase64(raw);
1239
+ setCapturedArtifact(livenessArtifact ?? null);
1089
1240
  setCaptureMode("preview");
1090
1241
  };
1091
1242
  const handleCameraCancel = () => setCaptureMode("idle");
1092
1243
  const handleRetake = () => {
1093
1244
  setCapturedPreview(null);
1094
1245
  setCapturedBase64(null);
1246
+ setCapturedArtifact(null);
1095
1247
  setCaptureMode("live");
1096
1248
  };
1097
1249
  const handleConfirmSubmit = () => {
@@ -1163,7 +1315,8 @@ function FaceCaptureModal({
1163
1315
  {
1164
1316
  onCapture: handleCameraCapture,
1165
1317
  onCancel: handleCameraCancel,
1166
- message: livenessMessage
1318
+ message: livenessMessage,
1319
+ assetBasePath: mediapipeAssetBasePath
1167
1320
  }
1168
1321
  ), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1169
1322
  }
@@ -1394,59 +1547,10 @@ var attemptsTextStyle = {
1394
1547
  };
1395
1548
  var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1396
1549
  var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1397
- var FACE_MESSAGES = {
1398
- idle: "Initializing camera...",
1399
- loading: "Loading face detection...",
1400
- scanning: "Position your face in the circle",
1401
- too_far: "Face too far. Please move closer to the camera",
1402
- too_close: "Too close. Please move back a little",
1403
- off_center: "Center your face in the circle",
1404
- ok: "Looking great! Tap the button to capture"
1405
- };
1406
- var FACE_COLORS = {
1407
- idle: "#9ca3af",
1408
- loading: "#3b82f6",
1409
- scanning: "#3b82f6",
1410
- too_far: "#ef4444",
1411
- too_close: "#f59e0b",
1412
- off_center: "#f59e0b",
1413
- ok: "#10b981"
1414
- };
1415
- var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
1416
- var mediapipeLoaderPromise = null;
1417
- function loadMediaPipeFaceDetection() {
1418
- if (typeof window === "undefined" || typeof document === "undefined") {
1419
- return Promise.reject(new Error("MediaPipe requires a browser environment"));
1420
- }
1421
- const w = window;
1422
- if (w.FaceDetection) {
1423
- return Promise.resolve(w.FaceDetection);
1424
- }
1425
- if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
1426
- mediapipeLoaderPromise = new Promise((resolve, reject) => {
1427
- const script = document.createElement("script");
1428
- script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
1429
- script.async = true;
1430
- script.crossOrigin = "anonymous";
1431
- script.onload = () => {
1432
- if (w.FaceDetection) {
1433
- resolve(w.FaceDetection);
1434
- } else {
1435
- mediapipeLoaderPromise = null;
1436
- reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1437
- }
1438
- };
1439
- script.onerror = () => {
1440
- mediapipeLoaderPromise = null;
1441
- reject(new Error("Failed to load MediaPipe face_detection script"));
1442
- };
1443
- document.head.appendChild(script);
1444
- });
1445
- return mediapipeLoaderPromise;
1446
- }
1447
- function LiveCamera({ onCapture, onCancel, message }) {
1550
+ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1448
1551
  const videoRef = React.useRef(null);
1449
1552
  const streamRef = React.useRef(null);
1553
+ const samplesRef = React.useRef([]);
1450
1554
  const [ready, setReady] = React.useState(false);
1451
1555
  const [error, setError] = React.useState(null);
1452
1556
  const [faceStatus, setFaceStatus] = React.useState("idle");
@@ -1501,31 +1605,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
1501
1605
  let detector = null;
1502
1606
  setFaceStatus("loading");
1503
1607
  setFaceDetectionFailed(false);
1608
+ samplesRef.current = [];
1504
1609
  (async () => {
1505
1610
  try {
1506
- const FaceDetection = await loadMediaPipeFaceDetection();
1611
+ const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
1507
1612
  if (cancelled) return;
1613
+ const assetBase = resolveMediaPipeBase(assetBasePath);
1508
1614
  const d = new FaceDetection({
1509
- locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1615
+ locateFile: (f) => `${assetBase}/${f}`
1510
1616
  });
1511
1617
  d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1512
1618
  d.onResults((results) => {
1513
1619
  if (cancelled) return;
1514
1620
  const detections = results.detections ?? [];
1515
- let next;
1516
- if (detections.length === 0) {
1517
- next = "scanning";
1518
- } else {
1519
- const bb = detections[0].boundingBox;
1520
- if (bb.width < 0.18) next = "too_far";
1521
- else if (bb.width > 0.72) next = "too_close";
1522
- else if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
1523
- next = "off_center";
1524
- } else {
1525
- next = "ok";
1526
- }
1621
+ const status = mapDetectionsToFaceStatus(detections);
1622
+ samplesRef.current.push({
1623
+ t: Date.now(),
1624
+ status,
1625
+ ...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
1626
+ });
1627
+ if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
1628
+ samplesRef.current.shift();
1527
1629
  }
1528
- setFaceStatus(next);
1630
+ setFaceStatus(status);
1529
1631
  });
1530
1632
  await d.initialize();
1531
1633
  if (cancelled) return;
@@ -1561,7 +1663,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1561
1663
  } catch {
1562
1664
  }
1563
1665
  };
1564
- }, [ready]);
1666
+ }, [ready, assetBasePath]);
1565
1667
  const capture = () => {
1566
1668
  const video = videoRef.current;
1567
1669
  if (!video || !video.videoWidth) return;
@@ -1578,7 +1680,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1578
1680
  ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1579
1681
  streamRef.current?.getTracks().forEach((t) => t.stop());
1580
1682
  streamRef.current = null;
1581
- onCapture(canvas.toDataURL("image/jpeg", 0.9));
1683
+ onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
1582
1684
  };
1583
1685
  if (error) {
1584
1686
  return /* @__PURE__ */ React__default.default.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React__default.default.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React__default.default.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React__default.default.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React__default.default.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React__default.default.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
@@ -1586,7 +1688,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1586
1688
  const detectionActive = ready && !faceDetectionFailed;
1587
1689
  const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1588
1690
  const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1589
- const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1691
+ const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1590
1692
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1591
1693
  const dynamicCircleStyle = {
1592
1694
  ...faceCircleStyle,
@@ -2113,6 +2215,7 @@ function useEventBasedFaceVerificationSubmission(client$1) {
2113
2215
  defaultDevice: opts.defaultDevice,
2114
2216
  renderQR: opts.renderQR,
2115
2217
  onCancel: opts.onCancel,
2218
+ mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
2116
2219
  onComplete: (result) => {
2117
2220
  cleanup();
2118
2221
  resolve(result);