vesant-sdk 1.7.0-dev.9b89b96 → 1.7.0-dev.a85efc5

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 (41) hide show
  1. package/dist/{client-ZNdnpWe7.d.mts → client-B0qhE2kr.d.mts} +1 -1
  2. package/dist/{client-DoMSYMMR.d.ts → client-DF7hlMEz.d.ts} +13 -0
  3. package/dist/{client-DMIRx7Tu.d.mts → client-DrjgZoH_.d.mts} +13 -0
  4. package/dist/{client-C3DCmGe9.d.ts → client-DtH2RLuy.d.ts} +1 -1
  5. package/dist/compliance/index.d.mts +2 -2
  6. package/dist/compliance/index.d.ts +2 -2
  7. package/dist/compliance/index.js +0 -3
  8. package/dist/compliance/index.js.map +1 -1
  9. package/dist/compliance/index.mjs +0 -3
  10. package/dist/compliance/index.mjs.map +1 -1
  11. package/dist/geolocation/index.d.mts +2 -2
  12. package/dist/geolocation/index.d.ts +2 -2
  13. package/dist/index.d.mts +3 -3
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +8 -8
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +8 -8
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/kyc/core.js +4 -4
  20. package/dist/kyc/core.js.map +1 -1
  21. package/dist/kyc/core.mjs +4 -4
  22. package/dist/kyc/core.mjs.map +1 -1
  23. package/dist/kyc/index.d.mts +56 -5
  24. package/dist/kyc/index.d.ts +56 -5
  25. package/dist/kyc/index.js +4 -4
  26. package/dist/kyc/index.js.map +1 -1
  27. package/dist/kyc/index.mjs +4 -4
  28. package/dist/kyc/index.mjs.map +1 -1
  29. package/dist/react.d.mts +4 -2
  30. package/dist/react.d.ts +4 -2
  31. package/dist/react.js +125 -77
  32. package/dist/react.js.map +1 -1
  33. package/dist/react.mjs +125 -77
  34. package/dist/react.mjs.map +1 -1
  35. package/dist/tax/index.d.mts +18 -1
  36. package/dist/tax/index.d.ts +18 -1
  37. package/dist/tax/index.js +4 -1
  38. package/dist/tax/index.js.map +1 -1
  39. package/dist/tax/index.mjs +4 -1
  40. package/dist/tax/index.mjs.map +1 -1
  41. package/package.json +1 -1
package/dist/react.mjs CHANGED
@@ -883,8 +883,90 @@ 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;
969
+ var SESSION_EXPIRY_MS = 15 * 60 * 1e3;
888
970
  var LIVENESS_MESSAGES = [
889
971
  "Position your face inside the circle",
890
972
  "Make sure your face is well lit",
@@ -915,7 +997,8 @@ function FaceCaptureModal({
915
997
  onComplete,
916
998
  onCancel,
917
999
  defaultDevice,
918
- renderQR
1000
+ renderQR,
1001
+ mediapipeAssetBasePath
919
1002
  }) {
920
1003
  const isMobile = typeof navigator !== "undefined" && MOBILE_UA.test(navigator.userAgent);
921
1004
  const initialChoice = defaultDevice ?? (isMobile ? "this" : "ask");
@@ -932,15 +1015,24 @@ function FaceCaptureModal({
932
1015
  const [captureMode, setCaptureMode] = useState("idle");
933
1016
  const [capturedPreview, setCapturedPreview] = useState(null);
934
1017
  const [capturedBase64, setCapturedBase64] = useState(null);
1018
+ const [capturedArtifact, setCapturedArtifact] = useState(null);
935
1019
  const [livenessMessage, setLivenessMessage] = useState(LIVENESS_MESSAGES[0]);
936
1020
  const qrPayload = session.link || `vesant://reuse-kyc?token=${encodeURIComponent(session.token)}`;
937
1021
  const cancelledRef = useRef(false);
938
1022
  const submittingRef = useRef(false);
1023
+ useEffect(() => {
1024
+ const timer = setTimeout(() => {
1025
+ setStage(
1026
+ (prev) => prev.kind === "choose" || prev.kind === "qr" || prev.kind === "capture" ? { kind: "error", message: "Session expired" } : prev
1027
+ );
1028
+ }, SESSION_EXPIRY_MS);
1029
+ return () => clearTimeout(timer);
1030
+ }, [session.token]);
939
1031
  useEffect(() => {
940
1032
  if (stage.kind !== "qr") return;
941
1033
  let stopped = false;
942
1034
  const intervalMs = 2e3;
943
- const deadline = Date.now() + 15 * 60 * 1e3;
1035
+ const deadline = Date.now() + SESSION_EXPIRY_MS;
944
1036
  let mobileSeen = stage.mobileConnected;
945
1037
  const tick = async () => {
946
1038
  if (stopped || cancelledRef.current) return;
@@ -1021,7 +1113,10 @@ function FaceCaptureModal({
1021
1113
  const data = await client.submitEventBasedFaceVerificationSession({
1022
1114
  token: session.token,
1023
1115
  reference: ref,
1024
- 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
1025
1120
  });
1026
1121
  return { ok: true, data };
1027
1122
  } catch (err2) {
@@ -1067,16 +1162,18 @@ function FaceCaptureModal({
1067
1162
  submittingRef.current = false;
1068
1163
  }
1069
1164
  };
1070
- const handleCameraCapture = (dataUrl) => {
1165
+ const handleCameraCapture = (dataUrl, livenessArtifact) => {
1071
1166
  const raw = dataUrl.split(",")[1] ?? "";
1072
1167
  setCapturedPreview(dataUrl);
1073
1168
  setCapturedBase64(raw);
1169
+ setCapturedArtifact(livenessArtifact ?? null);
1074
1170
  setCaptureMode("preview");
1075
1171
  };
1076
1172
  const handleCameraCancel = () => setCaptureMode("idle");
1077
1173
  const handleRetake = () => {
1078
1174
  setCapturedPreview(null);
1079
1175
  setCapturedBase64(null);
1176
+ setCapturedArtifact(null);
1080
1177
  setCaptureMode("live");
1081
1178
  };
1082
1179
  const handleConfirmSubmit = () => {
@@ -1148,14 +1245,15 @@ function FaceCaptureModal({
1148
1245
  {
1149
1246
  onCapture: handleCameraCapture,
1150
1247
  onCancel: handleCameraCancel,
1151
- message: livenessMessage
1248
+ message: livenessMessage,
1249
+ assetBasePath: mediapipeAssetBasePath
1152
1250
  }
1153
1251
  ), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1154
1252
  }
1155
1253
  if (captureMode === "preview" && capturedPreview) {
1156
1254
  return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Looks good? Submit this selfie or retake it."), /* @__PURE__ */ React.createElement("div", { style: previewBoxStyle }, /* @__PURE__ */ React.createElement("img", { src: capturedPreview, alt: "Captured selfie preview", style: previewImgStyle })), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleConfirmSubmit }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, "Submit")), /* @__PURE__ */ React.createElement("button", { style: secondaryButtonStyle, onClick: handleRetake }, "Retake"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", attempts + 1, " of ", maxAttempts));
1157
1255
  }
1158
- return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React.createElement("div", { style: circleStyle }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React.createElement("div", { style: badgeStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, declinedReason ? "Try again" : "Open Camera")), hasMethodChoice && /* @__PURE__ */ React.createElement(
1256
+ return /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideContainerStyle }, /* @__PURE__ */ React.createElement("div", { style: visualGuideStyle }, /* @__PURE__ */ React.createElement("div", { style: circleStyle }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React.createElement("div", { style: badgeStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 16, height: 16 })))), declinedReason && /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("strong", null, "Verification declined."), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, declinedReason)), /* @__PURE__ */ React.createElement("div", { style: instructionsBoxStyle }, /* @__PURE__ */ React.createElement("h3", { style: instructionsTitleStyle }, "Tips for best results:"), /* @__PURE__ */ React.createElement("ul", { style: instructionsListStyle }, /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Ensure good lighting on your face")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Remove glasses or accessories if possible")), /* @__PURE__ */ React.createElement("li", { style: instructionItemStyle }, /* @__PURE__ */ React.createElement("span", { style: bulletStyle }, "\u2022"), /* @__PURE__ */ React.createElement("span", null, "Look directly at the camera")))), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleOpenCamera }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, declinedReason ? "Try Again" : "Open Camera")), hasMethodChoice && /* @__PURE__ */ React.createElement(
1159
1257
  "button",
1160
1258
  {
1161
1259
  style: secondaryButtonStyle,
@@ -1167,10 +1265,10 @@ function FaceCaptureModal({
1167
1265
  const renderSubmitting = () => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: { ...spinnerStyle, margin: "24px auto", width: 32, height: 32 } }), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Verifying your photo\u2026"), /* @__PURE__ */ React.createElement("p", { style: { ...attemptsTextStyle, marginTop: 8 } }, "This usually takes a few seconds."));
1168
1266
  const renderAccepted = (result) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: successCircleStyle }, /* @__PURE__ */ React.createElement("img", { src: Done, alt: "", width: 48, height: 48 })), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Verified"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, "Your identity has been confirmed. You can continue."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Continue")));
1169
1267
  const renderDeclined = (result) => /* @__PURE__ */ React.createElement("div", { style: contentStyle }, /* @__PURE__ */ React.createElement("div", { style: alertBoxStyle }, /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, result.declined_reason ?? "We couldn't match your selfie. Please take a new one.")), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: handleRetakeAfterDecline }, /* @__PURE__ */ React.createElement("img", { src: Camera, alt: "", width: 20, height: 20 }), /* @__PURE__ */ React.createElement("span", null, "Retake")), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(result) }, "Cancel")), /* @__PURE__ */ React.createElement("p", { style: attemptsTextStyle }, "Attempt ", Math.min(attempts + 1, maxAttempts), " of ", maxAttempts));
1170
- const renderMaxAttempts = (result) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "For your security, your account has been temporarily restricted", result.data.freeze_duration_minutes ? ` for ${result.data.freeze_duration_minutes} minutes` : "", ". Please contact support if you need immediate help."), result.data?.enforce_logout && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1268
+ const renderMaxAttempts = (result) => /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, "Maximum Attempts Reached"), /* @__PURE__ */ React.createElement("p", { style: subtitleStyle }, result.declined_reason ?? "We couldn't verify your identity after several attempts."), result.data?.freeze_account && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "For your security, your account has been temporarily restricted", result.data.freeze_duration_minutes ? ` for ${result.data.freeze_duration_minutes} minutes` : "", ". Please contact support if you need immediate help."), result.data?.enforce_logout && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "You will be signed out of your session."), result.data?.block && /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, "This action has been blocked for security reasons."), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => close(result) }, "Close")));
1171
1269
  const renderError = (message) => {
1172
1270
  const sessionExpired = /session\s+expired/i.test(message);
1173
- return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something went wrong"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try again"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1271
+ return /* @__PURE__ */ React.createElement("div", { style: { ...contentStyle, alignItems: "center", textAlign: "center" } }, /* @__PURE__ */ React.createElement("div", { style: errorCircleStyle }, "!"), /* @__PURE__ */ React.createElement("h3", { style: titleStyle }, sessionExpired ? "Session expired" : "Something Went Wrong"), /* @__PURE__ */ React.createElement("p", { style: alertTextStyle }, sessionExpired ? "Your verification session is no longer valid. Please start a new verification from the beginning." : message), /* @__PURE__ */ React.createElement("div", { style: buttonsContainerStyle }, !sessionExpired && /* @__PURE__ */ React.createElement("button", { style: primaryButtonStyle, onClick: () => setStage({ kind: "capture" }) }, "Try Again"), /* @__PURE__ */ React.createElement("button", { style: cancelButtonStyle, onClick: () => close(null) }, sessionExpired ? "Close" : "Cancel")));
1174
1272
  };
1175
1273
  const body = (() => {
1176
1274
  switch (stage.kind) {
@@ -1379,59 +1477,10 @@ var attemptsTextStyle = {
1379
1477
  };
1380
1478
  var footerStyle = { padding: "12px 24px 16px", borderTop: "1px solid #f3f4f6" };
1381
1479
  var footerTextStyle = { margin: 0, fontSize: 12, color: "#9ca3af", textAlign: "center" };
1382
- var FACE_MESSAGES = {
1383
- idle: "Initializing camera...",
1384
- loading: "Loading face detection...",
1385
- scanning: "Position your face in the circle",
1386
- too_far: "Face too far. Please move closer to the camera",
1387
- too_close: "Too close. Please move back a little",
1388
- off_center: "Center your face in the circle",
1389
- ok: "Looking great! Tap the button to capture"
1390
- };
1391
- var FACE_COLORS = {
1392
- idle: "#9ca3af",
1393
- loading: "#3b82f6",
1394
- scanning: "#3b82f6",
1395
- too_far: "#ef4444",
1396
- too_close: "#f59e0b",
1397
- off_center: "#f59e0b",
1398
- ok: "#10b981"
1399
- };
1400
- var MEDIAPIPE_BASE = "https://cdn.jsdelivr.net/npm/@mediapipe/face_detection";
1401
- var mediapipeLoaderPromise = null;
1402
- function loadMediaPipeFaceDetection() {
1403
- if (typeof window === "undefined" || typeof document === "undefined") {
1404
- return Promise.reject(new Error("MediaPipe requires a browser environment"));
1405
- }
1406
- const w = window;
1407
- if (w.FaceDetection) {
1408
- return Promise.resolve(w.FaceDetection);
1409
- }
1410
- if (mediapipeLoaderPromise) return mediapipeLoaderPromise;
1411
- mediapipeLoaderPromise = new Promise((resolve, reject) => {
1412
- const script = document.createElement("script");
1413
- script.src = `${MEDIAPIPE_BASE}/face_detection.js`;
1414
- script.async = true;
1415
- script.crossOrigin = "anonymous";
1416
- script.onload = () => {
1417
- if (w.FaceDetection) {
1418
- resolve(w.FaceDetection);
1419
- } else {
1420
- mediapipeLoaderPromise = null;
1421
- reject(new Error("MediaPipe loaded but FaceDetection global is missing"));
1422
- }
1423
- };
1424
- script.onerror = () => {
1425
- mediapipeLoaderPromise = null;
1426
- reject(new Error("Failed to load MediaPipe face_detection script"));
1427
- };
1428
- document.head.appendChild(script);
1429
- });
1430
- return mediapipeLoaderPromise;
1431
- }
1432
- function LiveCamera({ onCapture, onCancel, message }) {
1480
+ function LiveCamera({ onCapture, onCancel, message, assetBasePath }) {
1433
1481
  const videoRef = useRef(null);
1434
1482
  const streamRef = useRef(null);
1483
+ const samplesRef = useRef([]);
1435
1484
  const [ready, setReady] = useState(false);
1436
1485
  const [error, setError] = useState(null);
1437
1486
  const [faceStatus, setFaceStatus] = useState("idle");
@@ -1486,31 +1535,29 @@ function LiveCamera({ onCapture, onCancel, message }) {
1486
1535
  let detector = null;
1487
1536
  setFaceStatus("loading");
1488
1537
  setFaceDetectionFailed(false);
1538
+ samplesRef.current = [];
1489
1539
  (async () => {
1490
1540
  try {
1491
- const FaceDetection = await loadMediaPipeFaceDetection();
1541
+ const FaceDetection = await loadMediaPipeFaceDetection(assetBasePath);
1492
1542
  if (cancelled) return;
1543
+ const assetBase = resolveMediaPipeBase(assetBasePath);
1493
1544
  const d = new FaceDetection({
1494
- locateFile: (f) => `${MEDIAPIPE_BASE}/${f}`
1545
+ locateFile: (f) => `${assetBase}/${f}`
1495
1546
  });
1496
1547
  d.setOptions({ model: "short", minDetectionConfidence: 0.5 });
1497
1548
  d.onResults((results) => {
1498
1549
  if (cancelled) return;
1499
1550
  const detections = results.detections ?? [];
1500
- let next;
1501
- if (detections.length === 0) {
1502
- next = "scanning";
1503
- } else {
1504
- const bb = detections[0].boundingBox;
1505
- if (bb.width < 0.18) next = "too_far";
1506
- else if (bb.width > 0.72) next = "too_close";
1507
- else if (bb.xCenter < 0.28 || bb.xCenter > 0.72 || bb.yCenter < 0.28 || bb.yCenter > 0.72) {
1508
- next = "off_center";
1509
- } else {
1510
- next = "ok";
1511
- }
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();
1512
1559
  }
1513
- setFaceStatus(next);
1560
+ setFaceStatus(status);
1514
1561
  });
1515
1562
  await d.initialize();
1516
1563
  if (cancelled) return;
@@ -1546,7 +1593,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1546
1593
  } catch {
1547
1594
  }
1548
1595
  };
1549
- }, [ready]);
1596
+ }, [ready, assetBasePath]);
1550
1597
  const capture = () => {
1551
1598
  const video = videoRef.current;
1552
1599
  if (!video || !video.videoWidth) return;
@@ -1563,7 +1610,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1563
1610
  ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size);
1564
1611
  streamRef.current?.getTracks().forEach((t) => t.stop());
1565
1612
  streamRef.current = null;
1566
- onCapture(canvas.toDataURL("image/jpeg", 0.9));
1613
+ onCapture(canvas.toDataURL("image/jpeg", 0.9), buildLivenessArtifact(samplesRef.current));
1567
1614
  };
1568
1615
  if (error) {
1569
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")));
@@ -1571,7 +1618,7 @@ function LiveCamera({ onCapture, onCancel, message }) {
1571
1618
  const detectionActive = ready && !faceDetectionFailed;
1572
1619
  const liveMessage = detectionActive ? FACE_MESSAGES[faceStatus] : message;
1573
1620
  const liveFaceColor = detectionActive ? FACE_COLORS[faceStatus] : void 0;
1574
- const captureDisabled = !ready || detectionActive && faceStatus !== "ok";
1621
+ const captureDisabled = isCaptureDisabled(ready, detectionActive, faceStatus);
1575
1622
  const ringPulse = detectionActive && (faceStatus === "too_far" || faceStatus === "too_close") ? "pulse 1.5s ease-in-out infinite" : void 0;
1576
1623
  const dynamicCircleStyle = {
1577
1624
  ...faceCircleStyle,
@@ -2098,6 +2145,7 @@ function useEventBasedFaceVerificationSubmission(client) {
2098
2145
  defaultDevice: opts.defaultDevice,
2099
2146
  renderQR: opts.renderQR,
2100
2147
  onCancel: opts.onCancel,
2148
+ mediapipeAssetBasePath: opts.mediapipeAssetBasePath,
2101
2149
  onComplete: (result) => {
2102
2150
  cleanup();
2103
2151
  resolve(result);