vesant-sdk 1.7.0-dev.059da4f → 1.7.0-dev.117915b

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/dist/react.js CHANGED
@@ -889,6 +889,87 @@ var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i
889
889
  var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
890
890
  var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
891
891
 
892
+ // src/kyc/faceDetection.ts
893
+ var FACE_MESSAGES = {
894
+ idle: "Initializing camera...",
895
+ loading: "Loading face detection...",
896
+ scanning: "Position your face in the circle",
897
+ too_far: "Face too far. Please move closer to the camera",
898
+ too_close: "Too close. Please move back a little",
899
+ off_center: "Center your face in the circle",
900
+ ok: "Looking great! Tap the button to capture"
901
+ };
902
+ var FACE_COLORS = {
903
+ idle: "#9ca3af",
904
+ loading: "#3b82f6",
905
+ scanning: "#3b82f6",
906
+ too_far: "#ef4444",
907
+ too_close: "#f59e0b",
908
+ off_center: "#f59e0b",
909
+ ok: "#10b981"
910
+ };
911
+ var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
912
+ function mapDetectionsToFaceStatus(detections) {
913
+ if (detections.length === 0) return "scanning";
914
+ const bb = detections[0].boundingBox;
915
+ if (bb.width < 0.18) return "too_far";
916
+ if (bb.width > 0.72) return "too_close";
917
+ if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
918
+ return "off_center";
919
+ }
920
+ return "ok";
921
+ }
922
+ function isCaptureDisabled(ready, detectionActive, faceStatus) {
923
+ return !ready || detectionActive && faceStatus !== "ok";
924
+ }
925
+ function resolveMediaPipeBase(base) {
926
+ if (!base) return MEDIAPIPE_BASE;
927
+ return base.endsWith("/") ? base.slice(0, -1) : base;
928
+ }
929
+ var LIVENESS_SAMPLE_LIMIT = 20;
930
+ function buildLivenessArtifact(samples) {
931
+ if (samples.length === 0) return void 0;
932
+ return JSON.stringify({
933
+ version: 1,
934
+ source: "mediapipe-face-detection",
935
+ samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
936
+ });
937
+ }
938
+ var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
939
+ function loadMediaPipeFaceDetection(assetBasePath) {
940
+ if (typeof window === "undefined" || typeof document === "undefined") {
941
+ return Promise.reject(new Error("MediaPipe requires a browser environment"));
942
+ }
943
+ const w = window;
944
+ if (w.FaceDetection) {
945
+ return Promise.resolve(w.FaceDetection);
946
+ }
947
+ const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
948
+ const existing = mediapipeLoaderPromises.get(src);
949
+ if (existing) return existing;
950
+ const loader = new Promise((resolve, reject) => {
951
+ const script = document.createElement("script");
952
+ script.src = src;
953
+ script.async = true;
954
+ script.crossOrigin = "anonymous";
955
+ script.onload = () => {
956
+ if (w.FaceDetection) {
957
+ resolve(w.FaceDetection);
958
+ } else {
959
+ mediapipeLoaderPromises.delete(src);
960
+ reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
961
+ }
962
+ };
963
+ script.onerror = () => {
964
+ mediapipeLoaderPromises.delete(src);
965
+ reject(new Error("Failed to load MediaPipe face_detection script"));
966
+ };
967
+ document.head.appendChild(script);
968
+ });
969
+ mediapipeLoaderPromises.set(src, loader);
970
+ return loader;
971
+ }
972
+
892
973
  // src/kyc/FaceCaptureModal.tsx
893
974
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
894
975
  var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
@@ -922,7 +1003,8 @@ function FaceCaptureModal({
922
1003
  onComplete,
923
1004
  onCancel,
924
1005
  defaultDevice,
925
- renderQR
1006
+ renderQR,
1007
+ mediapipeAssetBasePath
926
1008
  }) {
927
1009
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
928
1010
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
@@ -939,6 +1021,7 @@ function FaceCaptureModal({
939
1021
  const [captureMode, setCaptureMode] = React.useState("idle");
940
1022
  const [capturedPreview, setCapturedPreview] = React.useState(null);
941
1023
  const [capturedBase64, setCapturedBase64] = React.useState(null);
1024
+ const [capturedArtifact, setCapturedArtifact] = React.useState(null);
942
1025
  const [livenessMessage, setLivenessMessage] = React.useState(LIVENESS_MESSAGES[0]);
943
1026
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
944
1027
  const cancelledRef = React.useRef(false);
@@ -1036,7 +1119,10 @@ function FaceCaptureModal({
1036
1119
  const data = await client.submitEventBasedFaceVerificationSession({
1037
1120
  token: session.token,
1038
1121
  reference: ref,
1039
- proof
1122
+ proof,
1123
+ // undefined when detection wasn't running for this capture —
1124
+ // JSON.stringify drops it, so the field is absent on the wire.
1125
+ liveness_artifact: capturedArtifact ?? void 0
1040
1126
  });
1041
1127
  return { ok: true, data };
1042
1128
  } catch (err2) {
@@ -1082,16 +1168,18 @@ function FaceCaptureModal({
1082
1168
  submittingRef.current = false;
1083
1169
  }
1084
1170
  };
1085
- const handleCameraCapture = (dataUrl) => {
1171
+ const handleCameraCapture = (dataUrl, livenessArtifact) => {
1086
1172
  const raw = dataUrl.split(",")[1] ?? "";
1087
1173
  setCapturedPreview(dataUrl);
1088
1174
  setCapturedBase64(raw);
1175
+ setCapturedArtifact(livenessArtifact ?? null);
1089
1176
  setCaptureMode("preview");
1090
1177
  };
1091
1178
  const handleCameraCancel = () => setCaptureMode("idle");
1092
1179
  const handleRetake = () => {
1093
1180
  setCapturedPreview(null);
1094
1181
  setCapturedBase64(null);
1182
+ setCapturedArtifact(null);
1095
1183
  setCaptureMode("live");
1096
1184
  };
1097
1185
  const handleConfirmSubmit = () => {
@@ -1163,7 +1251,8 @@ function FaceCaptureModal({
1163
1251
  {
1164
1252
  onCapture: handleCameraCapture,
1165
1253
  onCancel: handleCameraCancel,
1166
- message: livenessMessage
1254
+ message: livenessMessage,
1255
+ assetBasePath: mediapipeAssetBasePath
1167
1256
  }
1168
1257
  ), /* @__PURE__ */ React__default.default.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1169
1258
  }
@@ -1394,59 +1483,10 @@ var attemptsTextStyle = {
1394
1483
  };
1395
1484
  var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1396
1485
  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 }) {
1486
+ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1448
1487
  const videoRef = React.useRef(null);
1449
1488
  const streamRef = React.useRef(null);
1489
+ const samplesRef = React.useRef([]);
1450
1490
  const [ready, setReady] = React.useState(false);
1451
1491
  const [error, setError] = React.useState(null);
1452
1492
  const [faceStatus, setFaceStatus] = React.useState("idle");
@@ -1501,31 +1541,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
1501
1541
  let detector = null;
1502
1542
  setFaceStatus("loading");
1503
1543
  setFaceDetectionFailed(false);
1544
+ samplesRef.current = [];
1504
1545
  (async () => {
1505
1546
  try {
1506
- const FaceDetection = await loadMediaPipeFaceDetection();
1547
+ const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
1507
1548
  if (cancelled) return;
1549
+ const assetBase = resolveMediaPipeBase(assetBasePath);
1508
1550
  const d = new FaceDetection({
1509
- locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1551
+ locateFile: (f) => `${assetBase}/${f}`
1510
1552
  });
1511
1553
  d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1512
1554
  d.onResults((results) => {
1513
1555
  if (cancelled) return;
1514
1556
  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
- }
1557
+ const status = mapDetectionsToFaceStatus(detections);
1558
+ samplesRef.current.push({
1559
+ t: Date.now(),
1560
+ status,
1561
+ ...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
1562
+ });
1563
+ if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
1564
+ samplesRef.current.shift();
1527
1565
  }
1528
- setFaceStatus(next);
1566
+ setFaceStatus(status);
1529
1567
  });
1530
1568
  await d.initialize();
1531
1569
  if (cancelled) return;
@@ -1561,7 +1599,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1561
1599
  } catch {
1562
1600
  }
1563
1601
  };
1564
- }, [ready]);
1602
+ }, [ready, assetBasePath]);
1565
1603
  const capture = () => {
1566
1604
  const video = videoRef.current;
1567
1605
  if (!video || !video.videoWidth) return;
@@ -1578,7 +1616,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1578
1616
  ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1579
1617
  streamRef.current?.getTracks().forEach((t) => t.stop());
1580
1618
  streamRef.current = null;
1581
- onCapture(canvas.toDataURL("image/jpeg", 0.9));
1619
+ onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
1582
1620
  };
1583
1621
  if (error) {
1584
1622
  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 +1624,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1586
1624
  const detectionActive = ready && !faceDetectionFailed;
1587
1625
  const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1588
1626
  const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1589
- const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1627
+ const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1590
1628
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1591
1629
  const dynamicCircleStyle = {
1592
1630
  ...faceCircleStyle,
@@ -2113,6 +2151,7 @@ function useEventBasedFaceVerificationSubmission(client$1) {
2113
2151
  defaultDevice: opts.defaultDevice,
2114
2152
  renderQR: opts.renderQR,
2115
2153
  onCancel: opts.onCancel,
2154
+ mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
2116
2155
  onComplete: (result) => {
2117
2156
  cleanup();
2118
2157
  resolve(result);