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.mjs CHANGED
@@ -883,6 +883,87 @@ var Camera = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i
883
883
  var Done = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik04LjUgMTIuNUwxMC41IDE0LjVMMTUuNSA5LjUiIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPHBhdGggZD0iTTcgMy4zMzc4MkM4LjQ3MDg3IDIuNDg2OTcgMTAuMTc4NiAyIDEyIDJDMTcuNTIyOCAyIDIyIDYuNDc3MTUgMjIgMTJDMjIgMTcuNTIyOCAxNy41MjI4IDIyIDEyIDIyQzYuNDc3MTUgMjIgMiAxNy41MjI4IDIgMTJDMiAxMC4xNzg2IDIuNDg2OTcgOC40NzA4NyAzLjMzNzgyIDciIHN0cm9rZT0iIzFDMjc0QyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPg0KPC9zdmc+";
884
884
  var Close = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xNC41IDkuNTAwMDJMOS41IDE0LjVNOS40OTk5OCA5LjVMMTQuNSAxNC41IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjxwYXRoIGQ9Ik03IDMuMzM3ODJDOC40NzA4NyAyLjQ4Njk3IDEwLjE3ODYgMiAxMiAyQzE3LjUyMjggMiAyMiA2LjQ3NzE1IDIyIDEyQzIyIDE3LjUyMjggMTcuNTIyOCAyMiAxMiAyMkM2LjQ3NzE1IDIyIDIgMTcuNTIyOCAyIDEyQzIgMTAuMTc4NiAyLjQ4Njk3IDguNDcwODcgMy4zMzc4MiA3IiBzdHJva2U9IiMxQzI3NEMiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4NCjwvc3ZnPg==";
885
885
 
886
+ // src/kyc/faceDetection.ts
887
+ var FACE_MESSAGES = {
888
+ idle: "Initializing camera...",
889
+ loading: "Loading face detection...",
890
+ scanning: "Position your face in the circle",
891
+ too_far: "Face too far. Please move closer to the camera",
892
+ too_close: "Too close. Please move back a little",
893
+ off_center: "Center your face in the circle",
894
+ ok: "Looking great! Tap the button to capture"
895
+ };
896
+ var FACE_COLORS = {
897
+ idle: "#9ca3af",
898
+ loading: "#3b82f6",
899
+ scanning: "#3b82f6",
900
+ too_far: "#ef4444",
901
+ too_close: "#f59e0b",
902
+ off_center: "#f59e0b",
903
+ ok: "#10b981"
904
+ };
905
+ var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
906
+ function mapDetectionsToFaceStatus(detections) {
907
+ if (detections.length === 0) return "scanning";
908
+ const bb = detections[0].boundingBox;
909
+ if (bb.width < 0.18) return "too_far";
910
+ if (bb.width > 0.72) return "too_close";
911
+ if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
912
+ return "off_center";
913
+ }
914
+ return "ok";
915
+ }
916
+ function isCaptureDisabled(ready, detectionActive, faceStatus) {
917
+ return !ready || detectionActive && faceStatus !== "ok";
918
+ }
919
+ function resolveMediaPipeBase(base) {
920
+ if (!base) return MEDIAPIPE_BASE;
921
+ return base.endsWith("/") ? base.slice(0, -1) : base;
922
+ }
923
+ var LIVENESS_SAMPLE_LIMIT = 20;
924
+ function buildLivenessArtifact(samples) {
925
+ if (samples.length === 0) return void 0;
926
+ return JSON.stringify({
927
+ version: 1,
928
+ source: "mediapipe-face-detection",
929
+ samples: samples.slice(-LIVENESS_SAMPLE_LIMIT)
930
+ });
931
+ }
932
+ var mediapipeLoaderPromises = /* @__PURE__ */ new Map();
933
+ function loadMediaPipeFaceDetection(assetBasePath) {
934
+ if (typeof window === "undefined" || typeof document === "undefined") {
935
+ return Promise.reject(new Error("MediaPipe requires a browser environment"));
936
+ }
937
+ const w = window;
938
+ if (w.FaceDetection) {
939
+ return Promise.resolve(w.FaceDetection);
940
+ }
941
+ const src = `${resolveMediaPipeBase(assetBasePath)}/face_detection.js`;
942
+ const existing = mediapipeLoaderPromises.get(src);
943
+ if (existing) return existing;
944
+ const loader = new Promise((resolve, reject) => {
945
+ const script = document.createElement("script");
946
+ script.src = src;
947
+ script.async = true;
948
+ script.crossOrigin = "anonymous";
949
+ script.onload = () => {
950
+ if (w.FaceDetection) {
951
+ resolve(w.FaceDetection);
952
+ } else {
953
+ mediapipeLoaderPromises.delete(src);
954
+ reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
955
+ }
956
+ };
957
+ script.onerror = () => {
958
+ mediapipeLoaderPromises.delete(src);
959
+ reject(new Error("Failed to load MediaPipe face_detection script"));
960
+ };
961
+ document.head.appendChild(script);
962
+ });
963
+ mediapipeLoaderPromises.set(src, loader);
964
+ return loader;
965
+ }
966
+
886
967
  // src/kyc/FaceCaptureModal.tsx
887
968
  var MOBILE_UA = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
888
969
  var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
@@ -916,7 +997,8 @@ function FaceCaptureModal({
916
997
  onComplete,
917
998
  onCancel,
918
999
  defaultDevice,
919
- renderQR
1000
+ renderQR,
1001
+ mediapipeAssetBasePath
920
1002
  }) {
921
1003
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
922
1004
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
@@ -933,6 +1015,7 @@ function FaceCaptureModal({
933
1015
  const [captureMode, setCaptureMode] = useState("idle");
934
1016
  const [capturedPreview, setCapturedPreview] = useState(null);
935
1017
  const [capturedBase64, setCapturedBase64] = useState(null);
1018
+ const [capturedArtifact, setCapturedArtifact] = useState(null);
936
1019
  const [livenessMessage, setLivenessMessage] = useState(LIVENESS_MESSAGES[0]);
937
1020
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
938
1021
  const cancelledRef = useRef(false);
@@ -1030,7 +1113,10 @@ function FaceCaptureModal({
1030
1113
  const data = await client.submitEventBasedFaceVerificationSession({
1031
1114
  token: session.token,
1032
1115
  reference: ref,
1033
- proof
1116
+ proof,
1117
+ // undefined when detection wasn't running for this capture —
1118
+ // JSON.stringify drops it, so the field is absent on the wire.
1119
+ liveness_artifact: capturedArtifact ?? void 0
1034
1120
  });
1035
1121
  return { ok: true, data };
1036
1122
  } catch (err2) {
@@ -1076,16 +1162,18 @@ function FaceCaptureModal({
1076
1162
  submittingRef.current = false;
1077
1163
  }
1078
1164
  };
1079
- const handleCameraCapture = (dataUrl) => {
1165
+ const handleCameraCapture = (dataUrl, livenessArtifact) => {
1080
1166
  const raw = dataUrl.split(",")[1] ?? "";
1081
1167
  setCapturedPreview(dataUrl);
1082
1168
  setCapturedBase64(raw);
1169
+ setCapturedArtifact(livenessArtifact ?? null);
1083
1170
  setCaptureMode("preview");
1084
1171
  };
1085
1172
  const handleCameraCancel = () => setCaptureMode("idle");
1086
1173
  const handleRetake = () => {
1087
1174
  setCapturedPreview(null);
1088
1175
  setCapturedBase64(null);
1176
+ setCapturedArtifact(null);
1089
1177
  setCaptureMode("live");
1090
1178
  };
1091
1179
  const handleConfirmSubmit = () => {
@@ -1157,7 +1245,8 @@ function FaceCaptureModal({
1157
1245
  {
1158
1246
  onCapture: handleCameraCapture,
1159
1247
  onCancel: handleCameraCancel,
1160
- message: livenessMessage
1248
+ message: livenessMessage,
1249
+ assetBasePath: mediapipeAssetBasePath
1161
1250
  }
1162
1251
  ), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1163
1252
  }
@@ -1388,59 +1477,10 @@ var attemptsTextStyle = {
1388
1477
  };
1389
1478
  var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1390
1479
  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 }) {
1480
+ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1442
1481
  const videoRef = useRef(null);
1443
1482
  const streamRef = useRef(null);
1483
+ const samplesRef = useRef([]);
1444
1484
  const [ready, setReady] = useState(false);
1445
1485
  const [error, setError] = useState(null);
1446
1486
  const [faceStatus, setFaceStatus] = useState("idle");
@@ -1495,31 +1535,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
1495
1535
  let detector = null;
1496
1536
  setFaceStatus("loading");
1497
1537
  setFaceDetectionFailed(false);
1538
+ samplesRef.current = [];
1498
1539
  (async () => {
1499
1540
  try {
1500
- const FaceDetection = await loadMediaPipeFaceDetection();
1541
+ const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
1501
1542
  if (cancelled) return;
1543
+ const assetBase = resolveMediaPipeBase(assetBasePath);
1502
1544
  const d = new FaceDetection({
1503
- locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1545
+ locateFile: (f) => `${assetBase}/${f}`
1504
1546
  });
1505
1547
  d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1506
1548
  d.onResults((results) => {
1507
1549
  if (cancelled) return;
1508
1550
  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
- }
1551
+ const status = mapDetectionsToFaceStatus(detections);
1552
+ samplesRef.current.push({
1553
+ t: Date.now(),
1554
+ status,
1555
+ ...detections.length > 0 ? { boundingBox: detections[0].boundingBox } : {}
1556
+ });
1557
+ if (samplesRef.current.length > LIVENESS_SAMPLE_LIMIT) {
1558
+ samplesRef.current.shift();
1521
1559
  }
1522
- setFaceStatus(next);
1560
+ setFaceStatus(status);
1523
1561
  });
1524
1562
  await d.initialize();
1525
1563
  if (cancelled) return;
@@ -1555,7 +1593,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1555
1593
  } catch {
1556
1594
  }
1557
1595
  };
1558
- }, [ready]);
1596
+ }, [ready, assetBasePath]);
1559
1597
  const capture = () => {
1560
1598
  const video = videoRef.current;
1561
1599
  if (!video || !video.videoWidth) return;
@@ -1572,7 +1610,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1572
1610
  ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1573
1611
  streamRef.current?.getTracks().forEach((t) => t.stop());
1574
1612
  streamRef.current = null;
1575
- onCapture(canvas.toDataURL("image/jpeg", 0.9));
1613
+ onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
1576
1614
  };
1577
1615
  if (error) {
1578
1616
  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 +1618,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1580
1618
  const detectionActive = ready && !faceDetectionFailed;
1581
1619
  const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1582
1620
  const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1583
- const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1621
+ const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1584
1622
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1585
1623
  const dynamicCircleStyle = {
1586
1624
  ...faceCircleStyle,
@@ -2107,6 +2145,7 @@ function useEventBasedFaceVerificationSubmission(client) {
2107
2145
  defaultDevice: opts.defaultDevice,
2108
2146
  renderQR: opts.renderQR,
2109
2147
  onCancel: opts.onCancel,
2148
+ mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
2110
2149
  onComplete: (result) => {
2111
2150
  cleanup();
2112
2151
  resolve(result);