vesant-sdk 1.7.0-dev.f9faca4 → 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 +325 -58
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +318 -59
  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 +110 -13
  40. package/dist/kyc/index.d.ts +110 -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 +74 -10
  66. package/dist/tax/index.js.map +1 -1
  67. package/dist/tax/index.mjs +74 -10
  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.mjs CHANGED
@@ -87,6 +87,61 @@ function getBrowserInfo() {
87
87
  return { browser, browser_version: browserVersion, os, os_version: osVersion };
88
88
  }
89
89
 
90
+ // src/geolocation/integrity.ts
91
+ var isNil = (v) => v === null || v === void 0;
92
+ function assessGpsIntegrity(input, opts = {}) {
93
+ const flags = [];
94
+ const now = opts.now ?? (() => Date.now());
95
+ const maxAgeMs = opts.maxAgeMs ?? 6e4;
96
+ const minAccuracy = opts.minAccuracyMeters ?? 1;
97
+ const { accuracy, altitude, altitudeAccuracy, heading, speed, timestamp } = input;
98
+ if (isNil(accuracy)) {
99
+ flags.push("accuracy_missing");
100
+ } else if (accuracy === 0) {
101
+ flags.push("accuracy_zero");
102
+ } else if (accuracy < minAccuracy) {
103
+ flags.push("accuracy_implausibly_small");
104
+ }
105
+ const hasMotion = !isNil(heading) || !isNil(speed);
106
+ if (hasMotion && (isNil(accuracy) || accuracy === 0)) {
107
+ flags.push("motion_without_accuracy");
108
+ }
109
+ if (!isNil(altitude) && isNil(altitudeAccuracy)) {
110
+ flags.push("altitude_without_accuracy");
111
+ }
112
+ if (timestamp !== void 0 && now() - timestamp > maxAgeMs) {
113
+ flags.push("stale_timestamp");
114
+ }
115
+ if (opts.permissionState === "denied" || opts.permissionState === "prompt") {
116
+ flags.push(`permission_${opts.permissionState}`);
117
+ }
118
+ return { suspicious: flags.length > 0, flags };
119
+ }
120
+ function gpsIntegrityInputFromPosition(position) {
121
+ const c = position.coords;
122
+ return {
123
+ latitude: c.latitude,
124
+ longitude: c.longitude,
125
+ accuracy: c.accuracy,
126
+ altitude: c.altitude,
127
+ altitudeAccuracy: c.altitudeAccuracy,
128
+ heading: c.heading,
129
+ speed: c.speed,
130
+ timestamp: position.timestamp
131
+ };
132
+ }
133
+ async function probeGeolocationPermission() {
134
+ try {
135
+ if (typeof navigator === "undefined" || !navigator.permissions?.query) {
136
+ return "unavailable";
137
+ }
138
+ const status = await navigator.permissions.query({ name: "geolocation" });
139
+ return status.state;
140
+ } catch {
141
+ return "unavailable";
142
+ }
143
+ }
144
+
90
145
  // src/geolocation/ciphertext.ts
91
146
  var CIPHER_TEXT_EXPIRY_MINUTES = 5;
92
147
  async function computeHMAC(key, message) {
@@ -139,6 +194,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
139
194
  if (typeof navigator === "undefined" || !navigator.geolocation) {
140
195
  return null;
141
196
  }
197
+ const permissionState = await probeGeolocationPermission();
142
198
  return new Promise((resolve) => {
143
199
  navigator.geolocation.getCurrentPosition(
144
200
  (position) => {
@@ -155,6 +211,7 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
155
211
  resolve(null);
156
212
  return;
157
213
  }
214
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), { permissionState });
158
215
  resolve({
159
216
  latitude,
160
217
  longitude,
@@ -163,7 +220,8 @@ async function requestGPSLocation(timeout = 1e4, highAccuracy = true) {
163
220
  altitude_accuracy: position.coords.altitudeAccuracy ?? void 0,
164
221
  heading: position.coords.heading ?? void 0,
165
222
  speed: position.coords.speed ?? void 0,
166
- timestamp: position.timestamp
223
+ timestamp: position.timestamp,
224
+ integrity_flags: flags.length ? flags : void 0
167
225
  });
168
226
  },
169
227
  () => {
@@ -230,6 +288,9 @@ async function generateCipherText(options, config) {
230
288
  );
231
289
  if (location) {
232
290
  locationData = location;
291
+ if (location.integrity_flags?.length) {
292
+ warnings.push(`GPS integrity flags (advisory): ${location.integrity_flags.join(", ")}`);
293
+ }
233
294
  } else if (gpsRequiredByConfig) {
234
295
  throw new VesantError(
235
296
  `GPS location is required for ${options.reason} by tenant configuration, but GPS was not available or permission was denied`,
@@ -332,8 +393,6 @@ function useGeolocation(client, options = {}) {
332
393
  if (optionsRef.current.autoVerify) {
333
394
  const detectAndVerify = async () => {
334
395
  try {
335
- const ipResponse = await fetch("https://api.ipify.org?format=json");
336
- const { ip } = await ipResponse.json();
337
396
  let deviceFingerprint;
338
397
  if (optionsRef.current.includeDeviceFingerprint && typeof window !== "undefined") {
339
398
  const deviceId = generateDeviceId();
@@ -347,7 +406,6 @@ function useGeolocation(client, options = {}) {
347
406
  };
348
407
  }
349
408
  await verifyIP({
350
- ip_address: ip,
351
409
  user_id: optionsRef.current.userId || "",
352
410
  event_type: optionsRef.current.eventType || "page_view",
353
411
  device_fingerprint: deviceFingerprint
@@ -661,6 +719,12 @@ function useLocationCapture(client, options) {
661
719
  if (accuracy !== null && accuracy !== void 0 && accuracy > 0) {
662
720
  capture.accuracy = accuracy;
663
721
  }
722
+ const { flags } = assessGpsIntegrity(gpsIntegrityInputFromPosition(position), {
723
+ permissionState: await probeGeolocationPermission()
724
+ });
725
+ if (flags.length) {
726
+ capture.integrity_flags = flags;
727
+ }
664
728
  }
665
729
  }
666
730
  if (gpsError) {
@@ -883,6 +947,87 @@ var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i
883
947
  var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
884
948
  var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
885
949
 
950
+ // src/kyc/faceDetection.ts
951
+ var FACE_MESSAGES = {
952
+ idle: "Initializing camera...",
953
+ loading: "Loading face detection...",
954
+ scanning: "Position your face in the circle",
955
+ too_far: "Face too far. Please move closer to the camera",
956
+ too_close: "Too close. Please move back a little",
957
+ off_center: "Center your face in the circle",
958
+ ok: "Looking great! Tap the button to capture"
959
+ };
960
+ var FACE_COLORS = {
961
+ idle: "#9ca3af",
962
+ loading: "#3b82f6",
963
+ scanning: "#3b82f6",
964
+ too_far: "#ef4444",
965
+ too_close: "#f59e0b",
966
+ off_center: "#f59e0b",
967
+ ok: "#10b981"
968
+ };
969
+ var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
970
+ function mapDetectionsToFaceStatus(detections) {
971
+ if (detections.length === 0) return "scanning";
972
+ const bb = detections[0].boundingBox;
973
+ if (bb.width < 0.18) return "too_far";
974
+ if (bb.width > 0.72) return "too_close";
975
+ if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
976
+ return "off_center";
977
+ }
978
+ return "ok";
979
+ }
980
+ function isCaptureDisabled(ready, detectionActive, faceStatus) {
981
+ return !ready || detectionActive && faceStatus !== "ok";
982
+ }
983
+ function resolveMediaPipeBase(base) {
984
+ if (!base) return MEDIAPIPE_BASE;
985
+ return base.endsWith("/") ? base.slice(0, -1) : base;
986
+ }
987
+ var LIVENESS_SAMPLE_LIMIT = 20;
988
+ function buildLivenessArtifact(samples) {
989
+ if (samples.length === 0) return void 0;
990
+ return JSON.stringify({
991
+ version: 1,
992
+ source: "mediapipe-face-detection",
993
+ samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
994
+ });
995
+ }
996
+ var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
997
+ function loadMediaPipeFaceDetection(assetBasePath) {
998
+ if (typeof window === "undefined" || typeof document === "undefined") {
999
+ return Promise.reject(new Error("MediaPipe requires a browser environment"));
1000
+ }
1001
+ const w = window;
1002
+ if (w.FaceDetection) {
1003
+ return Promise.resolve(w.FaceDetection);
1004
+ }
1005
+ const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
1006
+ const existing = mediapipeLoaderPromises.get(src);
1007
+ if (existing) return existing;
1008
+ const loader = new Promise((resolve, reject) => {
1009
+ const script = document.createElement("script");
1010
+ script.src = src;
1011
+ script.async = true;
1012
+ script.crossOrigin = "anonymous";
1013
+ script.onload = () => {
1014
+ if (w.FaceDetection) {
1015
+ resolve(w.FaceDetection);
1016
+ } else {
1017
+ mediapipeLoaderPromises.delete(src);
1018
+ reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1019
+ }
1020
+ };
1021
+ script.onerror = () => {
1022
+ mediapipeLoaderPromises.delete(src);
1023
+ reject(new Error("Failed to load MediaPipe face_detection script"));
1024
+ };
1025
+ document.head.appendChild(script);
1026
+ });
1027
+ mediapipeLoaderPromises.set(src, loader);
1028
+ return loader;
1029
+ }
1030
+
886
1031
  // src/kyc/FaceCaptureModal.tsx
887
1032
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
888
1033
  var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
@@ -916,7 +1061,8 @@ function FaceCaptureModal({
916
1061
  onComplete,
917
1062
  onCancel,
918
1063
  defaultDevice,
919
- renderQR
1064
+ renderQR,
1065
+ mediapipeAssetBasePath
920
1066
  }) {
921
1067
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
922
1068
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
@@ -933,6 +1079,7 @@ function FaceCaptureModal({
933
1079
  const [captureMode, setCaptureMode] = useState("idle");
934
1080
  const [capturedPreview, setCapturedPreview] = useState(null);
935
1081
  const [capturedBase64, setCapturedBase64] = useState(null);
1082
+ const [capturedArtifact, setCapturedArtifact] = useState(null);
936
1083
  const [livenessMessage, setLivenessMessage] = useState(LIVENESS_MESSAGES[0]);
937
1084
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
938
1085
  const cancelledRef = useRef(false);
@@ -1030,7 +1177,10 @@ function FaceCaptureModal({
1030
1177
  const data = await client.submitEventBasedFaceVerificationSession({
1031
1178
  token: session.token,
1032
1179
  reference: ref,
1033
- proof
1180
+ proof,
1181
+ // undefined when detection wasn't running for this capture —
1182
+ // JSON.stringify drops it, so the field is absent on the wire.
1183
+ liveness_artifact: capturedArtifact ?? void 0
1034
1184
  });
1035
1185
  return { ok: true, data };
1036
1186
  } catch (err2) {
@@ -1076,16 +1226,18 @@ function FaceCaptureModal({
1076
1226
  submittingRef.current = false;
1077
1227
  }
1078
1228
  };
1079
- const handleCameraCapture = (dataUrl) => {
1229
+ const handleCameraCapture = (dataUrl, livenessArtifact) => {
1080
1230
  const raw = dataUrl.split(",")[1] ?? "";
1081
1231
  setCapturedPreview(dataUrl);
1082
1232
  setCapturedBase64(raw);
1233
+ setCapturedArtifact(livenessArtifact ?? null);
1083
1234
  setCaptureMode("preview");
1084
1235
  };
1085
1236
  const handleCameraCancel = () => setCaptureMode("idle");
1086
1237
  const handleRetake = () => {
1087
1238
  setCapturedPreview(null);
1088
1239
  setCapturedBase64(null);
1240
+ setCapturedArtifact(null);
1089
1241
  setCaptureMode("live");
1090
1242
  };
1091
1243
  const handleConfirmSubmit = () => {
@@ -1157,7 +1309,8 @@ function FaceCaptureModal({
1157
1309
  {
1158
1310
  onCapture: handleCameraCapture,
1159
1311
  onCancel: handleCameraCancel,
1160
- message: livenessMessage
1312
+ message: livenessMessage,
1313
+ assetBasePath: mediapipeAssetBasePath
1161
1314
  }
1162
1315
  ), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1163
1316
  }
@@ -1388,59 +1541,10 @@ var attemptsTextStyle = {
1388
1541
  };
1389
1542
  var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1390
1543
  var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1391
- var FACE_MESSAGES = {
1392
- idle: "Initializing camera...",
1393
- loading: "Loading face detection...",
1394
- scanning: "Position your face in the circle",
1395
- too_far: "Face too far. Please move closer to the camera",
1396
- too_close: "Too close. Please move back a little",
1397
- off_center: "Center your face in the circle",
1398
- ok: "Looking great! Tap the button to capture"
1399
- };
1400
- var FACE_COLORS = {
1401
- idle: "#9ca3af",
1402
- loading: "#3b82f6",
1403
- scanning: "#3b82f6",
1404
- too_far: "#ef4444",
1405
- too_close: "#f59e0b",
1406
- off_center: "#f59e0b",
1407
- ok: "#10b981"
1408
- };
1409
- var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
1410
- var mediapipeLoaderPromise = null;
1411
- function loadMediaPipeFaceDetection() {
1412
- if (typeof window === "undefined" || typeof document === "undefined") {
1413
- return Promise.reject(new Error("MediaPipe requires a browser environment"));
1414
- }
1415
- const w = window;
1416
- if (w.FaceDetection) {
1417
- return Promise.resolve(w.FaceDetection);
1418
- }
1419
- if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
1420
- mediapipeLoaderPromise = new Promise((resolve, reject) => {
1421
- const script = document.createElement("script");
1422
- script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
1423
- script.async = true;
1424
- script.crossOrigin = "anonymous";
1425
- script.onload = () => {
1426
- if (w.FaceDetection) {
1427
- resolve(w.FaceDetection);
1428
- } else {
1429
- mediapipeLoaderPromise = null;
1430
- reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1431
- }
1432
- };
1433
- script.onerror = () => {
1434
- mediapipeLoaderPromise = null;
1435
- reject(new Error("Failed to load MediaPipe face_detection script"));
1436
- };
1437
- document.head.appendChild(script);
1438
- });
1439
- return mediapipeLoaderPromise;
1440
- }
1441
- function LiveCamera({ onCapture, onCancel, message }) {
1544
+ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1442
1545
  const videoRef = useRef(null);
1443
1546
  const streamRef = useRef(null);
1547
+ const samplesRef = useRef([]);
1444
1548
  const [ready, setReady] = useState(false);
1445
1549
  const [error, setError] = useState(null);
1446
1550
  const [faceStatus, setFaceStatus] = useState("idle");
@@ -1495,31 +1599,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
1495
1599
  let detector = null;
1496
1600
  setFaceStatus("loading");
1497
1601
  setFaceDetectionFailed(false);
1602
+ samplesRef.current = [];
1498
1603
  (async () => {
1499
1604
  try {
1500
- const FaceDetection = await loadMediaPipeFaceDetection();
1605
+ const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
1501
1606
  if (cancelled) return;
1607
+ const assetBase = resolveMediaPipeBase(assetBasePath);
1502
1608
  const d = new FaceDetection({
1503
- locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1609
+ locateFile: (f) => `${assetBase}/${f}`
1504
1610
  });
1505
1611
  d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1506
1612
  d.onResults((results) => {
1507
1613
  if (cancelled) return;
1508
1614
  const detections = results.detections ?? [];
1509
- let next;
1510
- if (detections.length === 0) {
1511
- next = "scanning";
1512
- } else {
1513
- const bb = detections[0].boundingBox;
1514
- if (bb.width < 0.18) next = "too_far";
1515
- else if (bb.width > 0.72) next = "too_close";
1516
- else if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
1517
- next = "off_center";
1518
- } else {
1519
- next = "ok";
1520
- }
1615
+ const status = mapDetectionsToFaceStatus(detections);
1616
+ samplesRef.current.push({
1617
+ t: Date.now(),
1618
+ status,
1619
+ ...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
1620
+ });
1621
+ if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
1622
+ samplesRef.current.shift();
1521
1623
  }
1522
- setFaceStatus(next);
1624
+ setFaceStatus(status);
1523
1625
  });
1524
1626
  await d.initialize();
1525
1627
  if (cancelled) return;
@@ -1555,7 +1657,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1555
1657
  } catch {
1556
1658
  }
1557
1659
  };
1558
- }, [ready]);
1660
+ }, [ready, assetBasePath]);
1559
1661
  const capture = () => {
1560
1662
  const video = videoRef.current;
1561
1663
  if (!video || !video.videoWidth) return;
@@ -1572,7 +1674,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1572
1674
  ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1573
1675
  streamRef.current?.getTracks().forEach((t) => t.stop());
1574
1676
  streamRef.current = null;
1575
- onCapture(canvas.toDataURL("image/jpeg", 0.9));
1677
+ onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
1576
1678
  };
1577
1679
  if (error) {
1578
1680
  return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center", padding: 0 } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Camera unavailable"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, error), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: onCancel }, "Back")));
@@ -1580,7 +1682,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1580
1682
  const detectionActive = ready && !faceDetectionFailed;
1581
1683
  const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1582
1684
  const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1583
- const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1685
+ const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1584
1686
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1585
1687
  const dynamicCircleStyle = {
1586
1688
  ...faceCircleStyle,
@@ -2107,6 +2209,7 @@ function useEventBasedFaceVerificationSubmission(client) {
2107
2209
  defaultDevice: opts.defaultDevice,
2108
2210
  renderQR: opts.renderQR,
2109
2211
  onCancel: opts.onCancel,
2212
+ mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
2110
2213
  onComplete: (result) => {
2111
2214
  cleanup();
2112
2215
  resolve(result);