@incodetech/core 2.1.0-rc.1 → 2.1.0-rc.3
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/3dAvatarProvider-ChciZmjE.esm.js +28214 -0
- package/dist/{BrowserStorageProvider-BpJM-gIl.esm.js → BrowserStorageProvider-CrrN540a.esm.js} +1 -8
- package/dist/BrowserVisibilityObserverProvider-rim9Jrz-.esm.js +25 -0
- package/dist/Face2DProvider-BeLlYBbB.esm.js +824 -0
- package/dist/FaceDetectionProvider-B78455D4.d.ts +119 -0
- package/dist/IVisibilityObserverCapability-BbWjgpiF.d.ts +12 -0
- package/dist/IWasmUtilCapability-d2XvzaCk.d.ts +145 -0
- package/dist/IpifyProvider-BRW14_uo.esm.js +51 -0
- package/dist/{OpenViduRecordingProvider-O4GjBseO.esm.js → OpenViduRecordingProvider-B_0GKkzc.esm.js} +1 -1
- package/dist/PrivacyLensProvider-hDzaDzZ6.esm.js +339 -0
- package/dist/{StreamCanvasCapture-ImiDQdVA.esm.js → StreamCanvasCapture-Bz17q4FP.esm.js} +1 -1
- package/dist/StreamCanvasCapture-DS1TOoiH.d.ts +42 -0
- package/dist/{WasmUtilProvider-BKWm8rSA.esm.js → WasmUtilProvider-ADG54T6M.esm.js} +26 -2
- package/dist/{addressSearch-DvmWXKZg.esm.js → addressSearch-DJyNTg5S.esm.js} +4 -4
- package/dist/{ae-signature-BFZta3TZ.esm.js → ae-signature-DlElqFrZ.esm.js} +1 -1
- package/dist/ae-signature.d.ts +4 -3
- package/dist/ae-signature.esm.js +10 -7
- package/dist/antifraud.d.ts +4 -3
- package/dist/antifraud.esm.js +7 -6
- package/dist/antifraudManager-BKdQoMFf.esm.js +84 -0
- package/dist/{antifraudManager-CznnhyvD.d.ts → antifraudManager-DpIvbrZP.d.ts} +2 -2
- package/dist/{antifraudStateMachine-Ccrb-Jxc.esm.js → antifraudStateMachine-C0NGnP9W.esm.js} +3 -3
- package/dist/{api-CESGtpbH.esm.js → api-DsbUi7TO.esm.js} +21 -2
- package/dist/authentication.d.ts +19 -13
- package/dist/authentication.esm.js +33 -27
- package/dist/{authenticationManager-CaiQbp-q.d.ts → authenticationManager-CdPbNS2H.d.ts} +13 -3
- package/dist/{authenticationManager-BlR6mSaG.esm.js → authenticationManager-Dtl1vcET.esm.js} +17 -8
- package/dist/{authenticationStateMachine-DkcGUMw5.esm.js → authenticationStateMachine-D-hOlg8p.esm.js} +20 -23
- package/dist/avatar.d.ts +3 -0
- package/dist/avatar.esm.js +3 -0
- package/dist/{backCameraStream-D7Wo4Nbx.esm.js → backCameraStream-ZyqrFyjO.esm.js} +3 -3
- package/dist/browser-DlFgHpoC-qLKs75Qp.esm.js +1425 -0
- package/dist/{browserSimulation-B1dWiXp7.esm.js → browserSimulation-TsSoavQv.esm.js} +153 -1216
- package/dist/camera-DUkgmYMo.esm.js +79 -0
- package/dist/camera.d.ts +7 -1
- package/dist/camera.esm.js +4 -4
- package/dist/{StreamCanvasCapture-yyl20qd9.d.ts → canvas-CJUmoXSD.d.ts} +1 -41
- package/dist/certificate-issuance.d.ts +312 -0
- package/dist/certificate-issuance.esm.js +74 -0
- package/dist/certificateIssuanceStateMachine-C07m7qtP.esm.js +127 -0
- package/dist/clientLifecycle-Ct7h9uxx.esm.js +534 -0
- package/dist/consent.d.ts +4 -3
- package/dist/consent.esm.js +7 -6
- package/dist/{consentManager-BYo9Nu1r.esm.js → consentManager-BtnPZUW4.esm.js} +30 -5
- package/dist/{consentManager-Cg5Sk2tv.d.ts → consentManager-DufVo9D9.d.ts} +55 -55
- package/dist/{consentStateMachine-BG3yL8aC.esm.js → consentStateMachine-DO2JqoZW.esm.js} +3 -3
- package/dist/cpf-ocr.d.ts +4 -3
- package/dist/cpf-ocr.esm.js +7 -6
- package/dist/{cpfOcrManager-sSKegxox.esm.js → cpfOcrManager-C4rLgKsu.esm.js} +6 -5
- package/dist/{cpfOcrManager-Dg1s_TY5.d.ts → cpfOcrManager-DqZZ94Gt.d.ts} +19 -19
- package/dist/createFaceAvatar-B8pFpy6o.d.ts +401 -0
- package/dist/createFaceAvatar-BOHDKQRq.esm.js +46 -0
- package/dist/cross-document-data-match.d.ts +2 -1
- package/dist/cross-document-data-match.esm.js +6 -5
- package/dist/curp-validation.d.ts +10 -7
- package/dist/curp-validation.esm.js +7 -6
- package/dist/{curpValidationManager-CFem6zP9.esm.js → curpValidationManager-BafobPXC.esm.js} +52 -5
- package/dist/{curpValidationManager-RttixpIc.d.ts → curpValidationManager-Co6LZAHT.d.ts} +4 -3
- package/dist/{curpValidationStateMachine-B7V_qp66.esm.js → curpValidationStateMachine-DXTPK6vI.esm.js} +3 -3
- package/dist/custom-fields.d.ts +4 -2
- package/dist/custom-fields.esm.js +49 -6
- package/dist/custom-watchlist.d.ts +2 -1
- package/dist/custom-watchlist.esm.js +27 -6
- package/dist/{customWatchlistStateMachine-HmFybXLX.esm.js → customWatchlistStateMachine-B4G3Chkx.esm.js} +3 -3
- package/dist/{deepsightLoader-8tnddNx-.esm.js → deepsightLoader-DHSVD776.esm.js} +14 -13
- package/dist/{deepsightService-CrHmvx8X.esm.js → deepsightService-CDahSjGP.esm.js} +9 -38
- package/dist/deepsightService-CMB9XjES.d.ts +69 -0
- package/dist/device.d.ts +9 -1
- package/dist/device.esm.js +5 -5
- package/dist/document-capture.d.ts +178 -177
- package/dist/document-capture.esm.js +78 -14
- package/dist/document-upload.d.ts +51 -50
- package/dist/document-upload.esm.js +10 -9
- package/dist/{documentCaptureStateMachine-CUTUzUw_.esm.js → documentCaptureStateMachine-DSOX2epf.esm.js} +7 -7
- package/dist/dynamic-forms.d.ts +4 -192
- package/dist/dynamic-forms.esm.js +8 -410
- package/dist/dynamicFormsManager-BOefZbyF.esm.js +168 -0
- package/dist/dynamicFormsManager-CUYLOj9g.d.ts +239 -0
- package/dist/dynamicFormsStateMachine-BXHcaocI.esm.js +284 -0
- package/dist/ekyb.d.ts +10 -6
- package/dist/ekyb.esm.js +62 -17
- package/dist/{ekybStateMachine-klc-AImJ.esm.js → ekybStateMachine-CCSeqjCt.esm.js} +5 -5
- package/dist/ekyc.d.ts +5 -3
- package/dist/ekyc.esm.js +55 -9
- package/dist/{ekycStateMachine-CXbpaJJn.esm.js → ekycStateMachine-DkLVTOyA.esm.js} +5 -5
- package/dist/electronic-signature.d.ts +4 -3
- package/dist/electronic-signature.esm.js +9 -6
- package/dist/{electronicSignatureManager-BaECdJ1u.esm.js → electronicSignatureManager-DjzUzqZj.esm.js} +135 -92
- package/dist/email.d.ts +5 -4
- package/dist/email.esm.js +9 -8
- package/dist/{emailManager--D5G3ChB.esm.js → emailManager-B_XzEsXH.esm.js} +47 -6
- package/dist/{emailManager-lAzDoQOs.d.ts → emailManager-fHASH6uo.d.ts} +4 -3
- package/dist/{emailStateMachine-CxTOMAjC.esm.js → emailStateMachine-fqJ42b19.esm.js} +5 -5
- package/dist/{endpoints-BeTK0Mlt.esm.js → endpoints-BzMJFAPV.esm.js} +15 -0
- package/dist/{events-Dvvriq9l.esm.js → events-CqGY1yMq.esm.js} +87 -206
- package/dist/events.d.ts +23 -16
- package/dist/events.esm.js +3 -2
- package/dist/extensibility.d.ts +37 -27
- package/dist/extensibility.esm.js +74 -57
- package/dist/externalFetch-CyhBfD-n.esm.js +22 -0
- package/dist/face-match.d.ts +2 -2
- package/dist/face-match.esm.js +6 -5
- package/dist/{faceCaptureManagerFactory-ej2j1LMr.esm.js → faceCaptureManagerFactory-2brKTzMb.esm.js} +115 -25
- package/dist/faceCaptureManagerFactory-CGKbf7zt.d.ts +400 -0
- package/dist/{faceCaptureSetup-B9t6bYze.esm.js → faceCaptureSetup-CDUVU1nl.esm.js} +97 -13
- package/dist/{faceMatchStateMachine-DeXXzSuz.esm.js → faceMatchStateMachine-DUeUjEPx.esm.js} +3 -3
- package/dist/field-comparison.d.ts +4 -3
- package/dist/field-comparison.esm.js +6 -5
- package/dist/{fieldComparisonManager-COGI2ARD.esm.js → fieldComparisonManager-B9W0a9A5.esm.js} +5 -4
- package/dist/{fieldComparisonManager-Bu5TaSr3.d.ts → fieldComparisonManager-DdLqDeB2.d.ts} +2 -2
- package/dist/fiscal-qr.d.ts +5 -3
- package/dist/fiscal-qr.esm.js +13 -12
- package/dist/flow-events.d.ts +8 -6
- package/dist/flow.d.ts +34 -16
- package/dist/flow.esm.js +53 -30
- package/dist/flowCompletionService-BIqSTZcd.d.ts +28 -0
- package/dist/flowCompletionService-qEqh_g14.esm.js +42 -0
- package/dist/{flowServices-ChgTNggJ.esm.js → flowServices-C4piblgx.esm.js} +24 -9
- package/dist/geolocation.d.ts +4 -126
- package/dist/geolocation.esm.js +8 -87
- package/dist/geolocationManager-BP2Do9O-.esm.js +123 -0
- package/dist/geolocationManager-Cs50eRlC.d.ts +156 -0
- package/dist/{geolocationStateMachine-Dvh7X0wF.esm.js → geolocationStateMachine-BYfOOOG0.esm.js} +4 -4
- package/dist/{getDeviceClass-C8Do2qYu.esm.js → getDeviceClass-DGGzzfqm.esm.js} +1 -1
- package/dist/getDeviceData-TrKM8NPh.esm.js +101 -0
- package/dist/government-validation.d.ts +4 -3
- package/dist/government-validation.esm.js +43 -8
- package/dist/{governmentValidationStateMachine-DcJ-BfsC.esm.js → governmentValidationStateMachine-CwoPPO8b.esm.js} +5 -5
- package/dist/home.d.ts +18 -15
- package/dist/home.esm.js +5 -4
- package/dist/http.d.ts +1 -0
- package/dist/http.esm.js +2 -2
- package/dist/id-ocr.d.ts +4 -625
- package/dist/id-ocr.esm.js +8 -84
- package/dist/id-verification.d.ts +28 -27
- package/dist/id-verification.esm.js +6 -5
- package/dist/id.d.ts +24 -13
- package/dist/id.esm.js +48 -33
- package/dist/idCaptureDefaults-DNJON8Xo.esm.js +188 -0
- package/dist/{idCaptureManager-ChCNmgBj.d.ts → idCaptureManager-BVdvfJys.d.ts} +47 -12
- package/dist/{idCaptureManager-D3pwWutw.esm.js → idCaptureManager-Bo9DCQSU.esm.js} +100 -15
- package/dist/{idCaptureStateMachine-B3AiDIsT.esm.js → idCaptureStateMachine-BeDidMLU.esm.js} +525 -439
- package/dist/idOcrManager-BkRDrpZu.esm.js +92 -0
- package/dist/idOcrManager-CJzrmM91.d.ts +641 -0
- package/dist/{idOcrStateMachine-CDQ5d_VM.esm.js → idOcrStateMachine-DRZc4FBl.esm.js} +5 -5
- package/dist/{idVerificationStateMachine-kRxwImzO.esm.js → idVerificationStateMachine-oqiERKpg.esm.js} +3 -3
- package/dist/identity-reuse.d.ts +4 -3
- package/dist/identity-reuse.esm.js +7 -6
- package/dist/{identityReuseManager-C6n_97dw.esm.js → identityReuseManager-Bu62LToE.esm.js} +4 -3
- package/dist/{identityReuseManager-Cww-NDtd.d.ts → identityReuseManager-TWKbXuvY.d.ts} +33 -33
- package/dist/{identityReuseStateMachine-BfE5YiEr.esm.js → identityReuseStateMachine-BmONeF6B.esm.js} +3 -3
- package/dist/{index-B5hPA0Bg.d.ts → index-DtUlD-bz.d.ts} +2 -2
- package/dist/{index-B9NysVDB.d.ts → index-Icld-cKW.d.ts} +175 -295
- package/dist/index-XTtxaahe.d.ts +93 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +16 -12
- package/dist/{invokeOnCaptureCallback-ygByVdnn.esm.js → invokeOnCaptureCallback-v_PV_IJz.esm.js} +1 -1
- package/dist/{lib-BY67lgbq.esm.js → lib-B050WTth.esm.js} +1 -1
- package/dist/mandatory-consent.d.ts +10 -7
- package/dist/mandatory-consent.esm.js +7 -6
- package/dist/{mandatoryConsentManager-Dq6WiOZh.d.ts → mandatoryConsentManager-Bmdjo14S.d.ts} +50 -50
- package/dist/{mandatoryConsentManager-H6D18cZB.esm.js → mandatoryConsentManager-CQ3jirlZ.esm.js} +4 -3
- package/dist/{mandatoryConsentStateMachine-DtQNW1ji.esm.js → mandatoryConsentStateMachine-DkJJLHqC.esm.js} +3 -3
- package/dist/mediaRecorderOptions-C3NAkVgK.esm.js +42 -0
- package/dist/mediapipeShared-CRAKVElv.esm.js +67 -0
- package/dist/{recordingService-yRw7hfzU.esm.js → onDeviceFaceResults-DVBggeq8.esm.js} +71 -389
- package/dist/{openviduLazy-Dh14JNJc.esm.js → openviduLazy-BTOIauGP.esm.js} +2 -2
- package/dist/openviduLazy-Dov8oKfL.esm.js +3 -0
- package/dist/{permissionGuards-D0wGddy7.esm.js → permissionGuards-BR2oN573.esm.js} +1 -1
- package/dist/{permissionServices-Doec4X5L.esm.js → permissionServices-BJhGL13R.esm.js} +7 -4
- package/dist/personhood.d.ts +534 -0
- package/dist/personhood.esm.js +1494 -0
- package/dist/phone.d.ts +5 -4
- package/dist/phone.esm.js +9 -8
- package/dist/{phoneManager-fPmIBYQK.d.ts → phoneManager-BOTcjXgs.d.ts} +4 -3
- package/dist/{phoneManager-BmF-0Ez4.esm.js → phoneManager-BWlxqbyD.esm.js} +48 -6
- package/dist/{phoneStateMachine-BiV0yoEx.esm.js → phoneStateMachine-B_V9nODs.esm.js} +5 -5
- package/dist/{qe-signature-CUYPcHVo.esm.js → qe-signature-BhFnI9pT.esm.js} +1 -1
- package/dist/qe-signature.d.ts +4 -3
- package/dist/qe-signature.esm.js +10 -7
- package/dist/recordingService-m_1xCthr.esm.js +347 -0
- package/dist/redirect-to-mobile.d.ts +4 -3
- package/dist/redirect-to-mobile.esm.js +9 -7
- package/dist/{redirectToMobileManager-Dy3t7o0C.esm.js → redirectToMobileManager-BaAo9rgi.esm.js} +5 -4
- package/dist/{redirectToMobileManager-BNe3IzC_.d.ts → redirectToMobileManager-Br0KZK1G.d.ts} +2 -2
- package/dist/{redirectToMobileStateMachine-DyAdRxfP.esm.js → redirectToMobileStateMachine-DoAnf2ql.esm.js} +16 -28
- package/dist/{runChildModule-CuoHZ1cx.esm.js → runChildModule-0nQfjVVo.esm.js} +14 -24
- package/dist/selfie.d.ts +27 -14
- package/dist/selfie.esm.js +43 -30
- package/dist/{selfieManager-BNuTIGAz.esm.js → selfieManager-D_ZFERsE.esm.js} +17 -8
- package/dist/{selfieManager-C2y_t6DG.d.ts → selfieManager-R0Jw5sjb.d.ts} +13 -3
- package/dist/{selfieStateMachine-Bv99bZJE.esm.js → selfieStateMachine-CIgeeiN9.esm.js} +4 -4
- package/dist/{session-DoVb-OcB.esm.js → session-9jIfKFCc.esm.js} +3 -3
- package/dist/session.d.ts +11 -8
- package/dist/session.esm.js +16 -12
- package/dist/{sessionInitializer-DRB-hgbV.esm.js → sessionInitializer-C_TYIgyk.esm.js} +16 -109
- package/dist/{setup-BwCluiw3.d.ts → setup-CpRnXxn8.d.ts} +61 -2
- package/dist/setup-DHc6OaHm.esm.js +562 -0
- package/dist/signature.d.ts +4 -2
- package/dist/signature.esm.js +49 -6
- package/dist/{signatureStateMachine-C5qqYLRz.esm.js → signatureStateMachine-DtoiPzys.esm.js} +21 -39
- package/dist/{stats-BMNUG1AU.esm.js → stats-CYcScXcO.esm.js} +2 -2
- package/dist/stats.esm.js +3 -3
- package/dist/thumbmarkAdapter-fHBUDlAP.esm.js +1194 -0
- package/dist/tri-BG1iJsr4.esm.js +399 -0
- package/dist/tri.d.ts +2 -0
- package/dist/tri.esm-DUC01NgO.esm.js +2593 -0
- package/dist/tri.esm.js +4 -0
- package/dist/trust-graph.d.ts +4 -3
- package/dist/trust-graph.esm.js +4 -3
- package/dist/{types-NuT8ftBV.d.ts → types-BklZ_Wne.d.ts} +2 -1
- package/dist/{types-DOUhndhT.d.ts → types-BsYXnC5s.d.ts} +39 -3
- package/dist/{types-DsnEVMhr.esm.js → types-CMcxlSmH.esm.js} +2 -1
- package/dist/types-CxW2e0Ar.d.ts +132 -0
- package/dist/{types-Bj9hdFjU.d.ts → types-DgGlgyZT.d.ts} +1 -1
- package/dist/types-ElOJAGaM.esm.js +189 -0
- package/dist/urlUtils-BZqFU9rR.esm.js +16 -0
- package/dist/video-C2WIj6TS.esm.js +36 -0
- package/dist/video-selfie.d.ts +351 -0
- package/dist/video-selfie.esm.js +1509 -0
- package/dist/videoSelfieSteps-7M5vxKn-.esm.js +58 -0
- package/dist/{warmup-Dg8Lh-50.d.ts → warmup-BJZPQIiX.d.ts} +5 -1
- package/dist/wasm.d.ts +4 -3
- package/dist/wasm.esm.js +17 -13
- package/dist/watchlist-for-business.d.ts +2 -1
- package/dist/watchlist-for-business.esm.js +30 -7
- package/dist/watchlist.d.ts +2 -1
- package/dist/watchlist.esm.js +28 -6
- package/dist/{watchlistForBusinessStateMachine-DMl8j2Ov.esm.js → watchlistForBusinessStateMachine-DvEunDxs.esm.js} +3 -3
- package/dist/{watchlistStateMachine-DmQlqI6L.esm.js → watchlistStateMachine-AHXVf-TB.esm.js} +3 -3
- package/dist/workflow.d.ts +296 -103
- package/dist/workflow.esm.js +137 -74
- package/dist/{xstate.esm-C9wncMQa.esm.js → xstate.esm-Bomq1a0z.esm.js} +54 -5
- package/package.json +26 -1
- package/dist/IpifyProvider-D4LWD15E.esm.js +0 -167
- package/dist/antifraudManager-CkV4u-LE.esm.js +0 -55
- package/dist/camera-DJWm3V4g.esm.js +0 -22
- package/dist/deepsightService-Dh5P-hLf.d.ts +0 -265
- package/dist/faceCaptureManagerFactory-CjdBUS6s.d.ts +0 -721
- package/dist/flowCompletionService-BdR2cGgB.d.ts +0 -19
- package/dist/flowCompletionService-DdGojV9K.esm.js +0 -20
- package/dist/getBrowser-C8DP7oTB.esm.js +0 -8
- package/dist/openviduLazy-B8L--0oe.esm.js +0 -3
- package/dist/setup-CUO4mErT.esm.js +0 -1037
- /package/dist/{Actor-Y0_Fj-KL.d.ts → Actor-CGEelYtQ.d.ts} +0 -0
- /package/dist/{BrowserTimerProvider-DhNc_x02.esm.js → BrowserTimerProvider-CHloo0eQ.esm.js} +0 -0
- /package/dist/{ITimerCapability-CB0I1Uf2.esm.js → ITimerCapability-pltuG1E8.esm.js} +0 -0
- /package/dist/{Manager-BHn8wH8K.d.ts → Manager-CJ3G2zqi.d.ts} +0 -0
- /package/dist/{MotionSensorProvider-Bx7Mpzt0.esm.js → MotionSensorProvider-DCbId4De.esm.js} +0 -0
- /package/dist/{StateMachine-BC_nGvrc.d.ts → StateMachine-C_AY4cs7.d.ts} +0 -0
- /package/dist/{apiError-B-j-gyDx.esm.js → apiError-DNS2OCEX.esm.js} +0 -0
- /package/dist/{camera-SRBpPq2X.d.ts → camera-CNVUo-bC.d.ts} +0 -0
- /package/dist/{canvas-SKcRBxsk.esm.js → canvas-_I95hkPg.esm.js} +0 -0
- /package/dist/{chunk-CMUKZ2uL.esm.js → chunk-CIKcdghV.esm.js} +0 -0
- /package/dist/{cpf-BRzggV8G.esm.js → cpf-BKaho0j_.esm.js} +0 -0
- /package/dist/{dateUtils-AksLQmgV.esm.js → dateUtils-7-tQYRNN.esm.js} +0 -0
- /package/dist/{getDeviceClass-C0olyNFS.esm.js → getDeviceClass-DavNdcxh.esm.js} +0 -0
- /package/dist/{http-Cai3IoLS.esm.js → http-BkiUT7EB.esm.js} +0 -0
- /package/dist/{otp-CGMdUzBC.esm.js → otp-Bfnzf7I3.esm.js} +0 -0
- /package/dist/{otp-DF5A0sFx.d.ts → otp-M4iy57XY.d.ts} +0 -0
- /package/dist/{platform-SKvEfCBh.esm.js → platform-uIVQUnJP.esm.js} +0 -0
- /package/dist/{types-DvGZI7BF.d.ts → types-D_IceWxT.d.ts} +0 -0
- /package/dist/{types-ya0LN_MX.d.ts → types-Duy2rDxF.d.ts} +0 -0
|
@@ -0,0 +1,1494 @@
|
|
|
1
|
+
import { t as WasmUtilProvider } from "./WasmUtilProvider-ADG54T6M.esm.js";
|
|
2
|
+
import { t as BrowserTimerProvider } from "./BrowserTimerProvider-CHloo0eQ.esm.js";
|
|
3
|
+
import { t as api } from "./api-DsbUi7TO.esm.js";
|
|
4
|
+
import { a as createApi_default, t as getStoredHttpConfig } from "./clientLifecycle-Ct7h9uxx.esm.js";
|
|
5
|
+
import { n as eventModuleNames } from "./types-ElOJAGaM.esm.js";
|
|
6
|
+
import { t as endpoints } from "./endpoints-BzMJFAPV.esm.js";
|
|
7
|
+
import "./stats-CYcScXcO.esm.js";
|
|
8
|
+
import { n as isBrowserSimulation, o as BrowserEnvironmentProvider } from "./browserSimulation-TsSoavQv.esm.js";
|
|
9
|
+
import { t as getDeviceFingerprintInfo } from "./getDeviceData-TrKM8NPh.esm.js";
|
|
10
|
+
import { t as getThumbmarkId } from "./thumbmarkAdapter-fHBUDlAP.esm.js";
|
|
11
|
+
import "./platform-uIVQUnJP.esm.js";
|
|
12
|
+
import { a as fromPromise, i as fromCallback, l as createManager, r as assign, s as createActor, t as setup } from "./xstate.esm-Bomq1a0z.esm.js";
|
|
13
|
+
import "./MotionSensorProvider-DCbId4De.esm.js";
|
|
14
|
+
import { i as requestPermission, t as checkPermission } from "./permissionServices-BJhGL13R.esm.js";
|
|
15
|
+
import { a as stopCameraStream, r as requestCameraAccess } from "./camera-DUkgmYMo.esm.js";
|
|
16
|
+
import { t as BrowserStorageProvider } from "./BrowserStorageProvider-CrrN540a.esm.js";
|
|
17
|
+
import { b as FaceDetectionProvider, f as stopStream, l as startDetection, s as initializeDeepsightSession } from "./onDeviceFaceResults-DVBggeq8.esm.js";
|
|
18
|
+
import { t as sleep } from "./ITimerCapability-pltuG1E8.esm.js";
|
|
19
|
+
import "./canvas-_I95hkPg.esm.js";
|
|
20
|
+
import "./backCameraStream-ZyqrFyjO.esm.js";
|
|
21
|
+
import "./video-C2WIj6TS.esm.js";
|
|
22
|
+
import "./getDeviceClass-DavNdcxh.esm.js";
|
|
23
|
+
import "./deepsightService-CDahSjGP.esm.js";
|
|
24
|
+
import "./getDeviceClass-DGGzzfqm.esm.js";
|
|
25
|
+
import "./types-CMcxlSmH.esm.js";
|
|
26
|
+
import { t as StreamCanvasCapture } from "./StreamCanvasCapture-Bz17q4FP.esm.js";
|
|
27
|
+
|
|
28
|
+
//#region ../infra/src/crypto/randomUUID.ts
|
|
29
|
+
/**
|
|
30
|
+
* Cryptographically-strong UUID v4 (RFC 4122).
|
|
31
|
+
*
|
|
32
|
+
* Lives in infra (L0) so Core (L1) can mint secure identifiers without touching
|
|
33
|
+
* the `crypto` browser API directly — Core delegates here, keeping it free of
|
|
34
|
+
* browser globals (enforced by the `no-browser-api-in-core` lint rule). Backed
|
|
35
|
+
* by `crypto.randomUUID`, available in every browser the SDK targets.
|
|
36
|
+
*/
|
|
37
|
+
function randomUUID() {
|
|
38
|
+
return crypto.randomUUID();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/modules/personhood/personhoodServices.ts
|
|
43
|
+
/**
|
|
44
|
+
* Config used for the passive liveness pass. Deliberately stripped down from
|
|
45
|
+
* selfie: no recording, no validation rituals, no headwear/mask checks — we
|
|
46
|
+
* want the lowest-friction path to a single head-pose-tracked liveness shot.
|
|
47
|
+
*/
|
|
48
|
+
const PERSONHOOD_FACE_CAPTURE_CONFIG = {
|
|
49
|
+
showTutorial: false,
|
|
50
|
+
showPreview: false,
|
|
51
|
+
assistedOnboarding: false,
|
|
52
|
+
enableFaceRecording: false,
|
|
53
|
+
autoCaptureTimeout: 8e3,
|
|
54
|
+
captureAttempts: 2,
|
|
55
|
+
validateLenses: false,
|
|
56
|
+
validateFaceMask: false,
|
|
57
|
+
validateHeadCover: false,
|
|
58
|
+
validateClosedEyes: false,
|
|
59
|
+
validateBrightness: false,
|
|
60
|
+
deepsightLiveness: "SINGLE_FRAME",
|
|
61
|
+
ds: false
|
|
62
|
+
};
|
|
63
|
+
const PERSONHOOD_CAMERA_CONSTRAINTS = {
|
|
64
|
+
video: {
|
|
65
|
+
facingMode: "user",
|
|
66
|
+
height: { ideal: 480 },
|
|
67
|
+
width: { ideal: 640 }
|
|
68
|
+
},
|
|
69
|
+
audio: false
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Boots the Deepsight session used to back-stop the on-device liveness check.
|
|
73
|
+
* Independent of `initializePersonhoodCamera` so the two can proceed in
|
|
74
|
+
* parallel during the permission / collect-signals phase.
|
|
75
|
+
*/
|
|
76
|
+
async function initializePersonhoodDeepsight(params) {
|
|
77
|
+
return initializeDeepsightSession({
|
|
78
|
+
storage: params.storage,
|
|
79
|
+
ds: false
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Boots the camera stream and WASM face-detection pipeline as fast as
|
|
84
|
+
* possible after the user grants permission. Optimized for low latency
|
|
85
|
+
* between "permission granted" and "avatar begins tracking":
|
|
86
|
+
*
|
|
87
|
+
* 1. WASM model loading and `getUserMedia` run **in parallel** — they're
|
|
88
|
+
* independent I/O and were previously sequential, which stacked
|
|
89
|
+
* 50-500ms of avoidable latency.
|
|
90
|
+
* 2. Deepsight PRC warmup (anti-spoof pipeline priming) is fired as a
|
|
91
|
+
* **background task** after the real stream is open. It's not needed
|
|
92
|
+
* for face detection or pose tracking — only for the post-capture
|
|
93
|
+
* `analyzeFrame` step. `analyzeLiveness` degrades gracefully if PRC
|
|
94
|
+
* hasn't finished in time.
|
|
95
|
+
*
|
|
96
|
+
* Earlier versions opened a second hidden camera stream for PRC warmup
|
|
97
|
+
* BEFORE opening the real one, which on Safari/Firefox serialized the
|
|
98
|
+
* two `getUserMedia` calls and added visible lag before the avatar came
|
|
99
|
+
* alive.
|
|
100
|
+
*/
|
|
101
|
+
async function initializePersonhoodCamera(params) {
|
|
102
|
+
const provider = new FaceDetectionProvider();
|
|
103
|
+
const [initResult, streamResult] = await Promise.allSettled([provider.initialize({ autocaptureInterval: PERSONHOOD_FACE_CAPTURE_CONFIG.autoCaptureTimeout * 1e3 }), requestCameraAccess(PERSONHOOD_CAMERA_CONSTRAINTS)]);
|
|
104
|
+
if (streamResult.status === "rejected") {
|
|
105
|
+
if (initResult.status === "fulfilled") provider.dispose();
|
|
106
|
+
throw streamResult.reason;
|
|
107
|
+
}
|
|
108
|
+
const stream = streamResult.value;
|
|
109
|
+
if (initResult.status === "rejected") {
|
|
110
|
+
stopCameraStream(stream);
|
|
111
|
+
throw initResult.reason;
|
|
112
|
+
}
|
|
113
|
+
provider.setChecksEnabled({
|
|
114
|
+
lenses: false,
|
|
115
|
+
mask: false,
|
|
116
|
+
closedEyes: false,
|
|
117
|
+
headWear: false,
|
|
118
|
+
occlusion: false
|
|
119
|
+
});
|
|
120
|
+
if (params.deepsightService) params.deepsightService.performPrcCheck({ constraints: PERSONHOOD_CAMERA_CONSTRAINTS }).catch(() => {});
|
|
121
|
+
return {
|
|
122
|
+
stream,
|
|
123
|
+
provider
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Wraps the existing face-capture detection loop for the Personhood flow.
|
|
128
|
+
*
|
|
129
|
+
* - Drops the captured canvas (we never upload the raw frame).
|
|
130
|
+
* - Forwards per-frame `onFaceData` into `onPose` so the avatar can mirror
|
|
131
|
+
* the user's head in real time.
|
|
132
|
+
*/
|
|
133
|
+
const AUTO_COMPLETE_DEADLINE_MS = 4e3;
|
|
134
|
+
function runPersonhoodDetection(params) {
|
|
135
|
+
let forcedCaptureFired = false;
|
|
136
|
+
const timer = BrowserTimerProvider.getInstance();
|
|
137
|
+
let autoCompleteTimer = null;
|
|
138
|
+
let autoCompleteArmed = false;
|
|
139
|
+
const cancelAutoComplete = () => {
|
|
140
|
+
if (autoCompleteTimer !== null) {
|
|
141
|
+
timer.clearTimeout(autoCompleteTimer);
|
|
142
|
+
autoCompleteTimer = null;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const fireForceCapture = () => {
|
|
146
|
+
if (forcedCaptureFired) return;
|
|
147
|
+
forcedCaptureFired = true;
|
|
148
|
+
cancelAutoComplete();
|
|
149
|
+
params.provider.forceCapture();
|
|
150
|
+
};
|
|
151
|
+
const armAutoComplete = () => {
|
|
152
|
+
if (autoCompleteArmed) return;
|
|
153
|
+
autoCompleteArmed = true;
|
|
154
|
+
if (!params.autoComplete) return;
|
|
155
|
+
autoCompleteTimer = timer.setTimeout(() => {
|
|
156
|
+
autoCompleteTimer = null;
|
|
157
|
+
fireForceCapture();
|
|
158
|
+
}, AUTO_COMPLETE_DEADLINE_MS);
|
|
159
|
+
};
|
|
160
|
+
const innerResult = startDetection({
|
|
161
|
+
config: PERSONHOOD_FACE_CAPTURE_CONFIG,
|
|
162
|
+
capturer: params.capturer,
|
|
163
|
+
provider: params.provider,
|
|
164
|
+
onUpdate: params.onStatus,
|
|
165
|
+
onFrame: () => armAutoComplete(),
|
|
166
|
+
onPose: (face, frameSize) => {
|
|
167
|
+
let faceSizeRatio;
|
|
168
|
+
if (frameSize && frameSize.width > 0 && frameSize.height > 0 && face.rect.width > 0 && face.rect.height > 0) faceSizeRatio = Math.sqrt(face.rect.width * face.rect.height) / Math.min(frameSize.width, frameSize.height);
|
|
169
|
+
params.onPose({
|
|
170
|
+
pitch: face.pitch,
|
|
171
|
+
yaw: face.yaw,
|
|
172
|
+
roll: face.roll,
|
|
173
|
+
faceSizeRatio
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
onSuccess: (canvas) => {
|
|
177
|
+
cancelAutoComplete();
|
|
178
|
+
params.onSuccess(canvas);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
cleanup: () => {
|
|
183
|
+
cancelAutoComplete();
|
|
184
|
+
innerResult.cleanup();
|
|
185
|
+
},
|
|
186
|
+
reset: innerResult.reset
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Runs the Deepsight pipeline against the stream + captured frame and pairs
|
|
191
|
+
* the result with the locally-observed pose motion. All work happens on the
|
|
192
|
+
* device — no network round-trip.
|
|
193
|
+
*
|
|
194
|
+
* The returned `LivenessOutcome` is fed into {@link produceLocalVerdict}:
|
|
195
|
+
* - `virtualCamera: true` → hard reject (OBS/ManyCam)
|
|
196
|
+
* - `analysisStatus !== 'PASS'` → reject with `spoof_suspected`
|
|
197
|
+
* - `yawRange + pitchRange < MIN_POSE_MOTION_DEG` → reject with `no_motion`
|
|
198
|
+
*/
|
|
199
|
+
async function analyzeLiveness(params) {
|
|
200
|
+
let virtualCamera = false;
|
|
201
|
+
let analysisStatus = "";
|
|
202
|
+
let motionStatus = "";
|
|
203
|
+
let metadata = "";
|
|
204
|
+
let pipelineState = "";
|
|
205
|
+
const deepsight = params.deepsightService;
|
|
206
|
+
if (deepsight) {
|
|
207
|
+
try {
|
|
208
|
+
const videoTrack = params.stream?.getVideoTracks()[0];
|
|
209
|
+
if (videoTrack) virtualCamera = await deepsight.checkVirtualCamera(videoTrack);
|
|
210
|
+
} catch {}
|
|
211
|
+
try {
|
|
212
|
+
await deepsight.performVirtualCameraCheck(params.token ?? null, "SELFIE");
|
|
213
|
+
} catch {}
|
|
214
|
+
try {
|
|
215
|
+
const imageData = params.capturedImage?.getImageData();
|
|
216
|
+
if (imageData) await deepsight.analyzeFrame(imageData);
|
|
217
|
+
} catch {}
|
|
218
|
+
try {
|
|
219
|
+
analysisStatus = deepsight.getAnalysisStatus();
|
|
220
|
+
motionStatus = deepsight.getMotionStatus();
|
|
221
|
+
pipelineState = deepsight.getPipelineState();
|
|
222
|
+
metadata = deepsight.getMetadata();
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
virtualCamera,
|
|
227
|
+
analysisStatus,
|
|
228
|
+
motionStatus,
|
|
229
|
+
yawRange: params.yawRange,
|
|
230
|
+
pitchRange: params.pitchRange,
|
|
231
|
+
metadata,
|
|
232
|
+
pipelineState
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Minimum combined yaw+pitch range (in degrees) required across the
|
|
237
|
+
* detection window to clear the no-motion guard. A static photo or a
|
|
238
|
+
* face with no head movement will fall below this.
|
|
239
|
+
*/
|
|
240
|
+
const MIN_POSE_MOTION_DEG = 4;
|
|
241
|
+
/**
|
|
242
|
+
* Upper bound of motion (degrees) that adds confidence — beyond this,
|
|
243
|
+
* additional head movement doesn't increase our belief that there's a
|
|
244
|
+
* human (and may just be noise from a moving phone).
|
|
245
|
+
*/
|
|
246
|
+
const MAX_POSE_MOTION_FOR_BONUS_DEG = 50;
|
|
247
|
+
/**
|
|
248
|
+
* Ceiling for on-device verdicts. Intentionally below 1.0 — nothing short
|
|
249
|
+
* of a signed server round-trip should claim perfect confidence, and the
|
|
250
|
+
* ceiling is strictly below what a server verdict would return so the two
|
|
251
|
+
* paths are distinguishable to integrators.
|
|
252
|
+
*/
|
|
253
|
+
const LOCAL_CONFIDENCE_CEILING = .95;
|
|
254
|
+
/**
|
|
255
|
+
* Default pass threshold when `PersonhoodConfig.threshold` is omitted. Single
|
|
256
|
+
* source of truth — keep `PersonhoodConfig.threshold`'s JSDoc and PERSONHOOD.md
|
|
257
|
+
* in sync with this value. (A clean local pass without strong head motion lands
|
|
258
|
+
* around here, so this is the effective on-device gate.)
|
|
259
|
+
*/
|
|
260
|
+
const DEFAULT_PERSONHOOD_THRESHOLD = .75;
|
|
261
|
+
/**
|
|
262
|
+
* Produces a verdict entirely on-device using an **additive** confidence
|
|
263
|
+
* model. Each positive signal contributes a measurable amount to the score;
|
|
264
|
+
* penalties subtract. This means clean passes vary — a user who swings
|
|
265
|
+
* through a full head turn scores higher than one who barely moves — which
|
|
266
|
+
* is useful for integrators that want to tune their own thresholds.
|
|
267
|
+
*
|
|
268
|
+
* Positive signals (contribute up to +0.95):
|
|
269
|
+
* - Successful detection → +0.25
|
|
270
|
+
* - Pose motion (linear from min to max) → up to +0.25
|
|
271
|
+
* - Clean device signals (no automation) → +0.05
|
|
272
|
+
* - Deepsight analysis: no explicit spoof → +0.05
|
|
273
|
+
* - Deepsight motion status == PASS → +0.05
|
|
274
|
+
* - Baseline presence → +0.30
|
|
275
|
+
*
|
|
276
|
+
* Penalties (hard rejects or large deductions):
|
|
277
|
+
* - `navigator.webdriver` true → -0.9
|
|
278
|
+
* - Automation markers → -0.9
|
|
279
|
+
* - Headless hints → -0.3
|
|
280
|
+
* - Virtual camera detected → -0.9
|
|
281
|
+
* - Explicit spoof/injection in analysis → -0.4
|
|
282
|
+
* - No head motion (below MIN_POSE_MOTION) → -0.5
|
|
283
|
+
* - Detection didn't reach 'success' → -0.2
|
|
284
|
+
*/
|
|
285
|
+
function produceLocalVerdict(params) {
|
|
286
|
+
const threshold = params.threshold ?? DEFAULT_PERSONHOOD_THRESHOLD;
|
|
287
|
+
const reasons = [];
|
|
288
|
+
let confidence = .4;
|
|
289
|
+
if (params.detectionOutcome === "success") confidence += .25;
|
|
290
|
+
else {
|
|
291
|
+
reasons.push(`detection:${params.detectionOutcome}`);
|
|
292
|
+
confidence -= .2;
|
|
293
|
+
}
|
|
294
|
+
if (params.liveness) {
|
|
295
|
+
const motion = params.liveness.yawRange + params.liveness.pitchRange;
|
|
296
|
+
if (motion < MIN_POSE_MOTION_DEG) {
|
|
297
|
+
reasons.push("no_motion");
|
|
298
|
+
confidence -= .5;
|
|
299
|
+
} else {
|
|
300
|
+
const span = MAX_POSE_MOTION_FOR_BONUS_DEG - MIN_POSE_MOTION_DEG;
|
|
301
|
+
const motionRatio = Math.min(1, Math.max(0, (motion - MIN_POSE_MOTION_DEG) / span));
|
|
302
|
+
confidence += .25 * motionRatio;
|
|
303
|
+
}
|
|
304
|
+
if (!hasExplicitSpoofSignal(params.liveness.analysisStatus)) confidence += .05;
|
|
305
|
+
else {
|
|
306
|
+
reasons.push("spoof_suspected");
|
|
307
|
+
confidence -= .4;
|
|
308
|
+
}
|
|
309
|
+
if (isMotionStatusPass(params.liveness.motionStatus)) confidence += .05;
|
|
310
|
+
}
|
|
311
|
+
if (!(params.signals.webdriver || params.signals.emulator || params.signals.headlessHints.length > 0)) confidence += .05;
|
|
312
|
+
if (params.signals.webdriver) {
|
|
313
|
+
reasons.push("webdriver");
|
|
314
|
+
confidence -= .9;
|
|
315
|
+
}
|
|
316
|
+
if (params.signals.emulator) {
|
|
317
|
+
reasons.push("emulator");
|
|
318
|
+
confidence -= .9;
|
|
319
|
+
}
|
|
320
|
+
const SUBSUMED_HINTS = new Set(["navigator.webdriver", "automation-marker"]);
|
|
321
|
+
if (params.signals.headlessHints.filter((hint) => !SUBSUMED_HINTS.has(hint)).length > 0) {
|
|
322
|
+
reasons.push("headless");
|
|
323
|
+
confidence -= .3;
|
|
324
|
+
}
|
|
325
|
+
if (params.liveness?.virtualCamera) {
|
|
326
|
+
reasons.push("virtual_camera");
|
|
327
|
+
confidence -= .9;
|
|
328
|
+
}
|
|
329
|
+
confidence = Math.min(LOCAL_CONFIDENCE_CEILING, Math.max(0, confidence));
|
|
330
|
+
confidence = Math.round(confidence * 1e3) / 1e3;
|
|
331
|
+
if (confidence < threshold && reasons.length === 0) reasons.push("low_confidence");
|
|
332
|
+
const human = confidence >= threshold && reasons.length === 0;
|
|
333
|
+
return {
|
|
334
|
+
human,
|
|
335
|
+
confidence,
|
|
336
|
+
evidenceId: generateEvidenceId(params.siteKey),
|
|
337
|
+
reason: human ? void 0 : reasons.join(","),
|
|
338
|
+
signals: params.signals,
|
|
339
|
+
liveness: params.liveness,
|
|
340
|
+
spoofCheckResults: buildSpoofCheckResults({
|
|
341
|
+
signals: params.signals,
|
|
342
|
+
liveness: params.liveness,
|
|
343
|
+
detectionOutcome: params.detectionOutcome
|
|
344
|
+
})
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function isMotionStatusPass(status) {
|
|
348
|
+
return status.trim().toUpperCase() === "PASS";
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Tests whether the Deepsight analysis status contains an explicit spoof /
|
|
352
|
+
* injection / fail signal.
|
|
353
|
+
*
|
|
354
|
+
* The WASM's `getCheck()` returns a pipeline-state blob intended primarily
|
|
355
|
+
* for backend consumption — the server is what turns the blob into a
|
|
356
|
+
* pass/fail. In phase 0 (no session, no backend round-trip) we intentionally
|
|
357
|
+
* only treat the output as a negative signal when it contains an explicit
|
|
358
|
+
* failure token; everything else is indeterminate and we defer to the
|
|
359
|
+
* other local checks (virtual camera + pose motion + device signals).
|
|
360
|
+
*
|
|
361
|
+
* Without this, every clean capture in a session-less dev demo was
|
|
362
|
+
* erroneously flagged as spoof_suspected because the WASM didn't emit
|
|
363
|
+
* literally "PASS".
|
|
364
|
+
*/
|
|
365
|
+
function hasExplicitSpoofSignal(status) {
|
|
366
|
+
const normalized = status.toUpperCase();
|
|
367
|
+
return normalized.includes("FAIL") || normalized.includes("SPOOF") || normalized.includes("INJECTION");
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Requests a server-side verdict when the browser can't run the on-device
|
|
371
|
+
* liveness pass (no camera, blocked permission, incompatible browser). The
|
|
372
|
+
* same API surface is returned to the widget so integrators see a single
|
|
373
|
+
* universal `{ human, confidence, evidence_id }` shape.
|
|
374
|
+
*
|
|
375
|
+
* The confidence returned by this path is intentionally capped below the
|
|
376
|
+
* on-device ceiling — with no camera we can only score device / behavioural
|
|
377
|
+
* signals, which are weaker evidence of humanness.
|
|
378
|
+
*/
|
|
379
|
+
async function requestServerFallbackVerdict(params) {
|
|
380
|
+
if (params.mockFallback) return mockServerFallback(params);
|
|
381
|
+
try {
|
|
382
|
+
const res = await api.post(endpoints.personhoodServerFallback, {
|
|
383
|
+
siteKey: params.siteKey,
|
|
384
|
+
signals: params.signals,
|
|
385
|
+
reason: params.reason
|
|
386
|
+
}, { signal: params.signal });
|
|
387
|
+
if (!res.ok) throw new Error(`POST ${endpoints.personhoodServerFallback} failed: ${res.status} ${res.statusText}`);
|
|
388
|
+
return res.data;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (isApiNotConfigured(error)) return mockServerFallback(params);
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function mockServerFallback(params) {
|
|
395
|
+
await sleep(200);
|
|
396
|
+
const blocked = params.signals.webdriver || params.signals.emulator || params.signals.headlessHints.length > 0;
|
|
397
|
+
const spoofCheckResults = buildSpoofCheckResults({
|
|
398
|
+
signals: params.signals,
|
|
399
|
+
liveness: void 0,
|
|
400
|
+
detectionOutcome: "error"
|
|
401
|
+
});
|
|
402
|
+
if (blocked) return {
|
|
403
|
+
human: false,
|
|
404
|
+
confidence: .1,
|
|
405
|
+
evidenceId: generateEvidenceId(params.siteKey),
|
|
406
|
+
reason: "automation_markers",
|
|
407
|
+
signals: params.signals,
|
|
408
|
+
spoofCheckResults
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
human: true,
|
|
412
|
+
confidence: .72,
|
|
413
|
+
evidenceId: generateEvidenceId(params.siteKey),
|
|
414
|
+
reason: `fallback:${params.reason}`,
|
|
415
|
+
signals: params.signals,
|
|
416
|
+
spoofCheckResults
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Pulls a token string out of `/omni/personhood/token`'s response,
|
|
421
|
+
* regardless of which envelope the backend uses. The Incode service
|
|
422
|
+
* surface has historically returned tokens under several keys
|
|
423
|
+
* (`token`, `interviewToken`, `sessionToken`, `access_token`, raw
|
|
424
|
+
* `id`) and a few endpoints return the bare string in the body.
|
|
425
|
+
* Trying each in order and returning the first non-empty value
|
|
426
|
+
* means we don't have to second-guess the contract — when the
|
|
427
|
+
* backend changes shape, the diagnostic log of `res.data` in the
|
|
428
|
+
* caller still surfaces the raw response.
|
|
429
|
+
*/
|
|
430
|
+
function extractTokenFromResponse(data) {
|
|
431
|
+
if (typeof data === "string" && data.length > 0) return data;
|
|
432
|
+
if (data && typeof data === "object") {
|
|
433
|
+
const obj = data;
|
|
434
|
+
const candidates = [
|
|
435
|
+
obj.token,
|
|
436
|
+
obj.interviewToken,
|
|
437
|
+
obj.sessionToken,
|
|
438
|
+
obj.access_token,
|
|
439
|
+
obj.accessToken,
|
|
440
|
+
obj.id,
|
|
441
|
+
obj.data?.token,
|
|
442
|
+
obj.data?.interviewToken
|
|
443
|
+
];
|
|
444
|
+
for (const candidate of candidates) if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Pulls the verify endpoint's authoritative pass/fail boolean out
|
|
449
|
+
* of its response, regardless of envelope. Current backend
|
|
450
|
+
* contract: `{ "isHuman": boolean }`. Older / forward-compat
|
|
451
|
+
* shapes also tried so a server-side rename doesn't silently break
|
|
452
|
+
* the client. Returns `undefined` only when no recognizable field
|
|
453
|
+
* is present — caller treats that as "trust the local verdict".
|
|
454
|
+
*/
|
|
455
|
+
function extractIsHumanFromResponse(data) {
|
|
456
|
+
if (typeof data === "boolean") return data;
|
|
457
|
+
if (data && typeof data === "object") {
|
|
458
|
+
const obj = data;
|
|
459
|
+
const candidates = [
|
|
460
|
+
obj.isHuman,
|
|
461
|
+
obj.human,
|
|
462
|
+
obj.passed,
|
|
463
|
+
obj.data?.isHuman,
|
|
464
|
+
obj.data?.human,
|
|
465
|
+
obj.result?.isHuman
|
|
466
|
+
];
|
|
467
|
+
for (const candidate of candidates) if (typeof candidate === "boolean") return candidate;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Mints the personhood `Authorization: Bearer` token via the OAuth2
|
|
472
|
+
* client-credentials grant against {@link endpoints.personhoodOAuthToken}
|
|
473
|
+
* (`POST /omni/oauth2/token`, `application/x-www-form-urlencoded`,
|
|
474
|
+
* `grant_type=client_credentials&scope=openid`). Returns the
|
|
475
|
+
* `access_token` (a signed JWT); the caller (state machine `fetchToken`
|
|
476
|
+
* actor) lands it in actor context where downstream services receive it
|
|
477
|
+
* as an explicit input. No module-level slot — concurrent personhood
|
|
478
|
+
* instances stay isolated, and `actor.stop()` cleans up implicitly.
|
|
479
|
+
*
|
|
480
|
+
* Mock path returns a `mock-token-*` string after a short delay so
|
|
481
|
+
* `mockVerify=true` flows complete without a configured backend.
|
|
482
|
+
*
|
|
483
|
+
* Replaces the removed `GET /omni/personhood/token` (ENG-45783).
|
|
484
|
+
*/
|
|
485
|
+
async function fetchPersonhoodAuthToken(params = {}) {
|
|
486
|
+
if (params.mockVerify) {
|
|
487
|
+
await sleep(50);
|
|
488
|
+
return `mock-token-${randomUUID()}`;
|
|
489
|
+
}
|
|
490
|
+
const form = {
|
|
491
|
+
grant_type: "client_credentials",
|
|
492
|
+
scope: "openid"
|
|
493
|
+
};
|
|
494
|
+
if (params.clientId) form.client_id = params.clientId;
|
|
495
|
+
if (params.clientSecret) form.client_secret = params.clientSecret;
|
|
496
|
+
const body = new URLSearchParams(form).toString();
|
|
497
|
+
const authPath = params.authBaseUrl ? endpoints.personhoodOAuthTokenDirect : endpoints.personhoodOAuthToken;
|
|
498
|
+
const reqConfig = {
|
|
499
|
+
signal: params.signal,
|
|
500
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
501
|
+
};
|
|
502
|
+
try {
|
|
503
|
+
let res;
|
|
504
|
+
if (params.authBaseUrl) res = await createApi_default({
|
|
505
|
+
apiURL: params.authBaseUrl,
|
|
506
|
+
customHeaders: getStoredHttpConfig()?.customHeaders
|
|
507
|
+
}).post(authPath, body, reqConfig);
|
|
508
|
+
else res = await api.post(authPath, body, reqConfig);
|
|
509
|
+
if (!res.ok) throw new Error(`POST ${authPath} failed: ${res.status} ${res.statusText}`);
|
|
510
|
+
const token = extractTokenFromResponse(res.data);
|
|
511
|
+
if (!token) throw new Error(`POST ${authPath} returned no access_token`);
|
|
512
|
+
return token;
|
|
513
|
+
} catch (error) {
|
|
514
|
+
if (isApiNotConfigured(error)) return fetchPersonhoodAuthToken({
|
|
515
|
+
...params,
|
|
516
|
+
mockVerify: true
|
|
517
|
+
});
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Builds the full per-check breakdown that lands on
|
|
523
|
+
* {@link PersonhoodVerdict.spoofCheckResults} and in the verify
|
|
524
|
+
* POST body. One entry per check the SDK ran, with a stable id, a
|
|
525
|
+
* `passed | failed | unknown` outcome, and (on failure) a short
|
|
526
|
+
* tag the server can pattern-match on.
|
|
527
|
+
*
|
|
528
|
+
* This is the single source of truth for "what did the on-device
|
|
529
|
+
* pipeline check?" — both {@link produceLocalVerdict} and the
|
|
530
|
+
* verify POST consume it. Adding a new check here automatically
|
|
531
|
+
* surfaces it to integrators and to the backend without touching
|
|
532
|
+
* the state machine or the verify wire format.
|
|
533
|
+
*
|
|
534
|
+
* Conservative defaults: when a pipeline didn't run (no liveness
|
|
535
|
+
* outcome / Deepsight not initialized), the dependent checks are
|
|
536
|
+
* reported as `unknown`. This lets the server distinguish "we
|
|
537
|
+
* tested and the user passed" from "we couldn't test", which is a
|
|
538
|
+
* meaningful gradient for risk scoring.
|
|
539
|
+
*/
|
|
540
|
+
function buildSpoofCheckResults(params) {
|
|
541
|
+
const results = [];
|
|
542
|
+
const { signals, liveness, detectionOutcome } = params;
|
|
543
|
+
if (detectionOutcome === "success") results.push({
|
|
544
|
+
check: "faceDetection",
|
|
545
|
+
result: "passed"
|
|
546
|
+
});
|
|
547
|
+
else results.push({
|
|
548
|
+
check: "faceDetection",
|
|
549
|
+
result: "failed",
|
|
550
|
+
reason: `detection:${detectionOutcome}`
|
|
551
|
+
});
|
|
552
|
+
if (liveness) {
|
|
553
|
+
if (liveness.analysisStatus === "PASS") results.push({
|
|
554
|
+
check: "physicalAttack",
|
|
555
|
+
result: "passed"
|
|
556
|
+
});
|
|
557
|
+
else if (hasExplicitSpoofSignal(liveness.analysisStatus)) results.push({
|
|
558
|
+
check: "physicalAttack",
|
|
559
|
+
result: "failed",
|
|
560
|
+
reason: liveness.analysisStatus || "spoof_suspected"
|
|
561
|
+
});
|
|
562
|
+
else results.push({
|
|
563
|
+
check: "physicalAttack",
|
|
564
|
+
result: "unknown"
|
|
565
|
+
});
|
|
566
|
+
results.push({
|
|
567
|
+
check: "virtualCamera",
|
|
568
|
+
result: liveness.virtualCamera ? "failed" : "passed",
|
|
569
|
+
...liveness.virtualCamera ? { reason: "virtual_camera" } : {}
|
|
570
|
+
});
|
|
571
|
+
if (liveness.motionStatus === "PASS") results.push({
|
|
572
|
+
check: "motionLiveness",
|
|
573
|
+
result: "passed"
|
|
574
|
+
});
|
|
575
|
+
else if (liveness.motionStatus === "FAIL") results.push({
|
|
576
|
+
check: "motionLiveness",
|
|
577
|
+
result: "failed",
|
|
578
|
+
reason: "no_motion"
|
|
579
|
+
});
|
|
580
|
+
else results.push({
|
|
581
|
+
check: "motionLiveness",
|
|
582
|
+
result: "unknown"
|
|
583
|
+
});
|
|
584
|
+
if (liveness.yawRange + liveness.pitchRange >= MIN_POSE_MOTION_DEG) results.push({
|
|
585
|
+
check: "headMotion",
|
|
586
|
+
result: "passed"
|
|
587
|
+
});
|
|
588
|
+
else results.push({
|
|
589
|
+
check: "headMotion",
|
|
590
|
+
result: "failed",
|
|
591
|
+
reason: "no_motion"
|
|
592
|
+
});
|
|
593
|
+
} else {
|
|
594
|
+
results.push({
|
|
595
|
+
check: "physicalAttack",
|
|
596
|
+
result: "unknown"
|
|
597
|
+
});
|
|
598
|
+
results.push({
|
|
599
|
+
check: "virtualCamera",
|
|
600
|
+
result: "unknown"
|
|
601
|
+
});
|
|
602
|
+
results.push({
|
|
603
|
+
check: "motionLiveness",
|
|
604
|
+
result: "unknown"
|
|
605
|
+
});
|
|
606
|
+
results.push({
|
|
607
|
+
check: "headMotion",
|
|
608
|
+
result: "unknown"
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
results.push({
|
|
612
|
+
check: "webdriver",
|
|
613
|
+
result: signals.webdriver ? "failed" : "passed",
|
|
614
|
+
...signals.webdriver ? { reason: "webdriver" } : {}
|
|
615
|
+
});
|
|
616
|
+
results.push({
|
|
617
|
+
check: "emulator",
|
|
618
|
+
result: signals.emulator ? "failed" : "passed",
|
|
619
|
+
...signals.emulator ? { reason: "emulator" } : {}
|
|
620
|
+
});
|
|
621
|
+
const independentHeadlessHints = signals.headlessHints.filter((hint) => hint !== "navigator.webdriver" && hint !== "automation-marker");
|
|
622
|
+
results.push({
|
|
623
|
+
check: "headlessBrowser",
|
|
624
|
+
result: independentHeadlessHints.length > 0 ? "failed" : "passed",
|
|
625
|
+
...independentHeadlessHints.length > 0 ? { reason: independentHeadlessHints.join(",") } : {}
|
|
626
|
+
});
|
|
627
|
+
return results;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Trims the rich client-side {@link SpoofCheckResult} array down to the wire
|
|
631
|
+
* shape the verify POST expects: `check` + `result` only (the `reason` field is
|
|
632
|
+
* dropped — not part of the contract). Entry order is preserved so the server
|
|
633
|
+
* sees them in detection-pipeline order (face → physical → virtual → motion →
|
|
634
|
+
* head → automation). `unknown` results are passed through as-is —
|
|
635
|
+
* `SpoofCheckResultWire.result` is `'passed' | 'failed' | 'unknown'`. (Whether
|
|
636
|
+
* the backend should instead receive only `passed | failed` is an open
|
|
637
|
+
* server-contract question, tracked with the wider verify-contract work.)
|
|
638
|
+
*/
|
|
639
|
+
function toWireSpoofCheckResults(results) {
|
|
640
|
+
const wire = [];
|
|
641
|
+
for (const entry of results) wire.push({
|
|
642
|
+
check: entry.check,
|
|
643
|
+
result: entry.result
|
|
644
|
+
});
|
|
645
|
+
return wire;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* On-device verify hop — POST `/omni/personhood/verify` with the
|
|
649
|
+
* Deepsight metadata + derived `spoofCheckResults`. The personhood
|
|
650
|
+
* Bearer token (passed in via `params.token`, minted by the OAuth2
|
|
651
|
+
* client-credentials hop and threaded through actor context) is
|
|
652
|
+
* attached as `Authorization: Bearer <token>`.
|
|
653
|
+
*
|
|
654
|
+
* When the server returns a recognizable `isHuman` boolean, that
|
|
655
|
+
* value becomes the authoritative pass/fail on the returned
|
|
656
|
+
* verdict. Otherwise (HTTP error, missing field, network blip,
|
|
657
|
+
* SDK not configured) we fall back to the local on-device verdict
|
|
658
|
+
* the caller passed in — keeping the widget functional in flaky
|
|
659
|
+
* conditions. `mockVerify: true` short-circuits to the fallback
|
|
660
|
+
* verdict without hitting the network.
|
|
661
|
+
*/
|
|
662
|
+
async function verifyPersonhoodOnDevice(params) {
|
|
663
|
+
if (params.mockVerify) {
|
|
664
|
+
await sleep(50);
|
|
665
|
+
return params.fallbackVerdict;
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const personhoodToken = params.token;
|
|
669
|
+
const res = await api.post(endpoints.personhoodOnDeviceVerify, {
|
|
670
|
+
metadata: params.metadata,
|
|
671
|
+
spoofCheckResults: toWireSpoofCheckResults(params.spoofCheckResults)
|
|
672
|
+
}, {
|
|
673
|
+
signal: params.signal,
|
|
674
|
+
...personhoodToken ? { headers: { Authorization: `Bearer ${personhoodToken}` } } : {}
|
|
675
|
+
});
|
|
676
|
+
if (!res.ok) throw new Error(`POST ${endpoints.personhoodOnDeviceVerify} failed: ${res.status} ${res.statusText}`);
|
|
677
|
+
const serverData = res.data;
|
|
678
|
+
const serverIsHuman = extractIsHumanFromResponse(serverData);
|
|
679
|
+
if (typeof serverIsHuman !== "boolean") return params.fallbackVerdict;
|
|
680
|
+
const finalReason = serverIsHuman ? void 0 : params.fallbackVerdict.reason ?? "server_rejected";
|
|
681
|
+
return {
|
|
682
|
+
...params.fallbackVerdict,
|
|
683
|
+
human: serverIsHuman,
|
|
684
|
+
confidence: serverIsHuman ? .95 : .05,
|
|
685
|
+
reason: finalReason
|
|
686
|
+
};
|
|
687
|
+
} catch {
|
|
688
|
+
return params.fallbackVerdict;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function isApiNotConfigured(error) {
|
|
692
|
+
return error instanceof Error && error.message.includes("SDK not configured");
|
|
693
|
+
}
|
|
694
|
+
function generateEvidenceId(siteKey) {
|
|
695
|
+
return `ph_${(siteKey || "site").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 12)}_${randomUUID().replace(/-/g, "")}`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/modules/personhood/personhoodSignals.ts
|
|
700
|
+
/**
|
|
701
|
+
* Returns a placeholder {@link DeviceBehavioralSignals} for the
|
|
702
|
+
* narrow set of code paths that need to operate before the real
|
|
703
|
+
* `collectDeviceBehavioralSignals()` has resolved (or where it
|
|
704
|
+
* errored out and the parallel `signals` branch landed in `done`
|
|
705
|
+
* with `context.signals` still undefined). Single source of truth
|
|
706
|
+
* — every call site that previously inlined this literal now
|
|
707
|
+
* imports from here.
|
|
708
|
+
*/
|
|
709
|
+
function emptySignals() {
|
|
710
|
+
return {
|
|
711
|
+
deviceFingerprint: "",
|
|
712
|
+
userAgentClass: "unknown",
|
|
713
|
+
emulator: false,
|
|
714
|
+
webdriver: false,
|
|
715
|
+
headlessHints: []
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Collects the device + behavioural bundle returned alongside the human
|
|
720
|
+
* verdict. Intentionally offline-only (no IP geolookup) so a Personhood check
|
|
721
|
+
* does not pay the latency of an external call in the hot path.
|
|
722
|
+
*
|
|
723
|
+
* The bundle is **opaque to the SDK** — the integrator uses these signals for
|
|
724
|
+
* their own dedup/risk logic. Personhood itself does NOT re-identify the human.
|
|
725
|
+
*/
|
|
726
|
+
async function collectDeviceBehavioralSignals() {
|
|
727
|
+
const browserEnv = new BrowserEnvironmentProvider();
|
|
728
|
+
const [deviceFingerprint, deviceInfo] = await Promise.all([safeGetFingerprint(), safeGetDeviceInfo()]);
|
|
729
|
+
const webdriver = Boolean(browserEnv.getNavigatorProperty("webdriver"));
|
|
730
|
+
const emulator = isBrowserSimulation(browserEnv);
|
|
731
|
+
const headlessHints = [];
|
|
732
|
+
if (webdriver) headlessHints.push("navigator.webdriver");
|
|
733
|
+
if (emulator) headlessHints.push("automation-marker");
|
|
734
|
+
const plugins = browserEnv.getNavigatorProperty("plugins");
|
|
735
|
+
const userAgent = browserEnv.getNavigatorProperty("userAgent")?.toLowerCase();
|
|
736
|
+
if (plugins && typeof plugins.length === "number" && plugins.length === 0 && userAgent?.includes("headless")) headlessHints.push("headless-user-agent");
|
|
737
|
+
return {
|
|
738
|
+
deviceFingerprint,
|
|
739
|
+
userAgentClass: formatUserAgentClass(deviceInfo),
|
|
740
|
+
emulator,
|
|
741
|
+
webdriver,
|
|
742
|
+
headlessHints
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function formatUserAgentClass(info) {
|
|
746
|
+
if (!info) return "unknown";
|
|
747
|
+
const parts = [
|
|
748
|
+
info.browser?.name,
|
|
749
|
+
info.browser?.version,
|
|
750
|
+
info.os?.name,
|
|
751
|
+
info.os?.version,
|
|
752
|
+
info.device?.model
|
|
753
|
+
].filter((part) => typeof part === "string" && part.length > 0);
|
|
754
|
+
return parts.length > 0 ? parts.join(" / ") : "unknown";
|
|
755
|
+
}
|
|
756
|
+
async function safeGetFingerprint() {
|
|
757
|
+
try {
|
|
758
|
+
return await getThumbmarkId();
|
|
759
|
+
} catch {
|
|
760
|
+
return "";
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function safeGetDeviceInfo() {
|
|
764
|
+
try {
|
|
765
|
+
return await getDeviceFingerprintInfo();
|
|
766
|
+
} catch {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
//#endregion
|
|
772
|
+
//#region src/modules/personhood/types.ts
|
|
773
|
+
const PERSONHOOD_ERROR_CODES = {
|
|
774
|
+
CAMERA_DENIED: "CAMERA_DENIED",
|
|
775
|
+
DETECTION_FAILED: "DETECTION_FAILED",
|
|
776
|
+
AUTH_TOKEN_FAILED: "AUTH_TOKEN_FAILED",
|
|
777
|
+
VERIFY_FAILED: "VERIFY_FAILED",
|
|
778
|
+
INJECTION_SUSPECTED: "INJECTION_SUSPECTED",
|
|
779
|
+
EMULATOR_DETECTED: "EMULATOR_DETECTED",
|
|
780
|
+
TIMEOUT: "TIMEOUT",
|
|
781
|
+
UNKNOWN: "UNKNOWN"
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
//#endregion
|
|
785
|
+
//#region src/modules/personhood/personhoodStateMachine.ts
|
|
786
|
+
const _personhoodMachine = setup({
|
|
787
|
+
types: {
|
|
788
|
+
context: {},
|
|
789
|
+
events: {},
|
|
790
|
+
input: {}
|
|
791
|
+
},
|
|
792
|
+
actors: {
|
|
793
|
+
checkPermission: fromPromise(async () => {
|
|
794
|
+
return checkPermission();
|
|
795
|
+
}),
|
|
796
|
+
requestPermission: fromPromise(async () => {
|
|
797
|
+
return requestPermission({ requestMotion: false });
|
|
798
|
+
}),
|
|
799
|
+
initializeCamera: fromPromise(async ({ input }) => {
|
|
800
|
+
return initializePersonhoodCamera({ deepsightService: input.deepsightService });
|
|
801
|
+
}),
|
|
802
|
+
initializeDeepsight: fromPromise(async ({ input }) => {
|
|
803
|
+
return initializePersonhoodDeepsight({ storage: input.dependencies.storage });
|
|
804
|
+
}),
|
|
805
|
+
collectSignals: fromPromise(async () => {
|
|
806
|
+
return collectDeviceBehavioralSignals();
|
|
807
|
+
}),
|
|
808
|
+
analyzeLiveness: fromPromise(async ({ input }) => {
|
|
809
|
+
return analyzeLiveness(input);
|
|
810
|
+
}),
|
|
811
|
+
runDetection: fromCallback(({ input, sendBack }) => {
|
|
812
|
+
let yawMin = Infinity;
|
|
813
|
+
let yawMax = -Infinity;
|
|
814
|
+
let pitchMin = Infinity;
|
|
815
|
+
let pitchMax = -Infinity;
|
|
816
|
+
const { cleanup, reset } = runPersonhoodDetection({
|
|
817
|
+
provider: input.provider,
|
|
818
|
+
capturer: input.frameCapturer,
|
|
819
|
+
autoComplete: input.autoComplete,
|
|
820
|
+
onStatus: (status) => sendBack({
|
|
821
|
+
type: "DETECTION_UPDATE",
|
|
822
|
+
status
|
|
823
|
+
}),
|
|
824
|
+
onPose: (pose) => {
|
|
825
|
+
if (pose.yaw < yawMin) yawMin = pose.yaw;
|
|
826
|
+
if (pose.yaw > yawMax) yawMax = pose.yaw;
|
|
827
|
+
if (pose.pitch < pitchMin) pitchMin = pose.pitch;
|
|
828
|
+
if (pose.pitch > pitchMax) pitchMax = pose.pitch;
|
|
829
|
+
},
|
|
830
|
+
onSuccess: (canvas) => {
|
|
831
|
+
sendBack({
|
|
832
|
+
type: "DETECTION_SUCCESS",
|
|
833
|
+
canvas,
|
|
834
|
+
yawRange: yawMax > yawMin ? yawMax - yawMin : 0,
|
|
835
|
+
pitchRange: pitchMax > pitchMin ? pitchMax - pitchMin : 0
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
sendBack({
|
|
840
|
+
type: "DETECTION_RESET_READY",
|
|
841
|
+
reset
|
|
842
|
+
});
|
|
843
|
+
return cleanup;
|
|
844
|
+
}),
|
|
845
|
+
produceLocalVerdict: fromPromise(async ({ input }) => {
|
|
846
|
+
return produceLocalVerdict({
|
|
847
|
+
siteKey: input.config.siteKey,
|
|
848
|
+
threshold: input.config.threshold,
|
|
849
|
+
signals: input.signals,
|
|
850
|
+
detectionOutcome: input.detectionStatus,
|
|
851
|
+
liveness: input.liveness
|
|
852
|
+
});
|
|
853
|
+
}),
|
|
854
|
+
requestServerFallback: fromPromise(async ({ input, signal }) => {
|
|
855
|
+
return requestServerFallbackVerdict({
|
|
856
|
+
siteKey: input.config.siteKey,
|
|
857
|
+
signals: input.signals,
|
|
858
|
+
reason: input.reason,
|
|
859
|
+
mockFallback: input.config.mockVerify,
|
|
860
|
+
signal
|
|
861
|
+
});
|
|
862
|
+
}),
|
|
863
|
+
fetchToken: fromPromise(async ({ input, signal }) => {
|
|
864
|
+
return fetchPersonhoodAuthToken({
|
|
865
|
+
clientId: input.config.clientId,
|
|
866
|
+
clientSecret: input.config.clientSecret,
|
|
867
|
+
authBaseUrl: input.config.authBaseUrl,
|
|
868
|
+
mockVerify: input.config.mockVerify,
|
|
869
|
+
signal
|
|
870
|
+
});
|
|
871
|
+
}),
|
|
872
|
+
verifyOnDevice: fromPromise(async ({ input, signal }) => {
|
|
873
|
+
return verifyPersonhoodOnDevice({
|
|
874
|
+
metadata: input.liveness?.metadata ?? "",
|
|
875
|
+
spoofCheckResults: input.localVerdict.spoofCheckResults,
|
|
876
|
+
fallbackVerdict: input.localVerdict,
|
|
877
|
+
mockVerify: input.config.mockVerify,
|
|
878
|
+
token: input.token,
|
|
879
|
+
signal
|
|
880
|
+
});
|
|
881
|
+
})
|
|
882
|
+
},
|
|
883
|
+
actions: {
|
|
884
|
+
setStreamAndProvider: assign(({ event }) => {
|
|
885
|
+
const output = event.output;
|
|
886
|
+
return {
|
|
887
|
+
stream: output.stream,
|
|
888
|
+
provider: output.provider,
|
|
889
|
+
frameCapturer: new StreamCanvasCapture(output.stream)
|
|
890
|
+
};
|
|
891
|
+
}),
|
|
892
|
+
setDetectionStatus: assign({ detectionStatus: ({ event }) => event.status }),
|
|
893
|
+
setCapturedImageAndRangeFromEvent: assign(({ event }) => {
|
|
894
|
+
const e = event;
|
|
895
|
+
return {
|
|
896
|
+
capturedImage: e.canvas,
|
|
897
|
+
yawRange: e.yawRange,
|
|
898
|
+
pitchRange: e.pitchRange
|
|
899
|
+
};
|
|
900
|
+
}),
|
|
901
|
+
setDeepsightServiceFromEvent: assign({ deepsightService: ({ event }) => event.output }),
|
|
902
|
+
setLivenessOutcomeFromEvent: assign({ livenessOutcome: ({ event }) => event.output }),
|
|
903
|
+
setResetDetection: assign({ resetDetection: ({ event }) => event.reset }),
|
|
904
|
+
setSignalsFromEvent: assign({ signals: ({ event }) => event.output }),
|
|
905
|
+
setTokenFromEvent: assign({ token: ({ event }) => event.output }),
|
|
906
|
+
setLocalVerdictFromEvent: assign({ localVerdict: ({ event }) => event.output }),
|
|
907
|
+
setVerdictFromEvent: assign({ verdict: ({ event }) => event.output }),
|
|
908
|
+
setPermissionResultFromEvent: assign({ permissionResult: ({ event }) => event.output }),
|
|
909
|
+
setPermissionDenied: assign({
|
|
910
|
+
permissionResult: () => "denied",
|
|
911
|
+
error: () => ({ code: PERSONHOOD_ERROR_CODES.CAMERA_DENIED })
|
|
912
|
+
}),
|
|
913
|
+
setDetectionError: assign({ error: () => ({ code: PERSONHOOD_ERROR_CODES.DETECTION_FAILED }) }),
|
|
914
|
+
setFallbackReasonNoCamera: assign({ fallbackReason: () => "no_camera" }),
|
|
915
|
+
setTokenError: assign({ error: ({ event }) => ({
|
|
916
|
+
code: PERSONHOOD_ERROR_CODES.AUTH_TOKEN_FAILED,
|
|
917
|
+
message: String(event.error ?? "auth token fetch failed")
|
|
918
|
+
}) }),
|
|
919
|
+
setVerifyError: assign({ error: ({ event }) => ({
|
|
920
|
+
code: PERSONHOOD_ERROR_CODES.VERIFY_FAILED,
|
|
921
|
+
message: String(event.error ?? "verify failed")
|
|
922
|
+
}) }),
|
|
923
|
+
setLocalVerdictError: assign({ error: ({ event }) => ({
|
|
924
|
+
code: PERSONHOOD_ERROR_CODES.UNKNOWN,
|
|
925
|
+
message: String(event.error ?? "local verdict synthesis failed")
|
|
926
|
+
}) }),
|
|
927
|
+
setVerdictToLocal: assign({ verdict: ({ context }) => context.localVerdict }),
|
|
928
|
+
stopMediaStream: assign(({ context }) => {
|
|
929
|
+
context.frameCapturer?.dispose();
|
|
930
|
+
if (context.stream) stopStream(context.stream);
|
|
931
|
+
context.provider?.dispose();
|
|
932
|
+
context.deepsightService?.cleanup();
|
|
933
|
+
return {
|
|
934
|
+
stream: void 0,
|
|
935
|
+
provider: void 0,
|
|
936
|
+
frameCapturer: void 0
|
|
937
|
+
};
|
|
938
|
+
}),
|
|
939
|
+
resetContext: assign({
|
|
940
|
+
permissionResult: () => void 0,
|
|
941
|
+
stream: () => void 0,
|
|
942
|
+
provider: () => void 0,
|
|
943
|
+
frameCapturer: () => void 0,
|
|
944
|
+
deepsightService: () => void 0,
|
|
945
|
+
detectionStatus: () => "idle",
|
|
946
|
+
yawRange: () => 0,
|
|
947
|
+
pitchRange: () => 0,
|
|
948
|
+
capturedImage: () => void 0,
|
|
949
|
+
livenessOutcome: () => void 0,
|
|
950
|
+
signals: () => void 0,
|
|
951
|
+
fallbackReason: () => void 0,
|
|
952
|
+
token: () => void 0,
|
|
953
|
+
localVerdict: () => void 0,
|
|
954
|
+
verdict: () => void 0,
|
|
955
|
+
error: () => void 0,
|
|
956
|
+
resetDetection: () => void 0
|
|
957
|
+
}),
|
|
958
|
+
emitSignal: ({ context, event }) => {
|
|
959
|
+
const listener = context.config.onSignal;
|
|
960
|
+
if (!listener) return;
|
|
961
|
+
if (event.type === "DETECTION_UPDATE") listener({
|
|
962
|
+
type: "detection",
|
|
963
|
+
payload: event.status
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
},
|
|
967
|
+
guards: {
|
|
968
|
+
hasShowTutorial: ({ context }) => context.config.showTutorial === true,
|
|
969
|
+
isPermissionGranted: ({ event }) => {
|
|
970
|
+
if ("output" in event) return event.output === "granted";
|
|
971
|
+
return false;
|
|
972
|
+
},
|
|
973
|
+
isPermissionDenied: ({ event }) => {
|
|
974
|
+
if ("output" in event) return event.output === "denied";
|
|
975
|
+
return false;
|
|
976
|
+
},
|
|
977
|
+
hasTokenError: ({ context }) => context.error?.code === PERSONHOOD_ERROR_CODES.AUTH_TOKEN_FAILED,
|
|
978
|
+
isDetectionError: ({ event }) => event.status === "error"
|
|
979
|
+
}
|
|
980
|
+
}).createMachine({
|
|
981
|
+
id: "personhood",
|
|
982
|
+
initial: "idle",
|
|
983
|
+
context: ({ input }) => ({
|
|
984
|
+
config: input.config,
|
|
985
|
+
dependencies: input.dependencies,
|
|
986
|
+
permissionResult: void 0,
|
|
987
|
+
fallbackReason: void 0,
|
|
988
|
+
stream: void 0,
|
|
989
|
+
provider: void 0,
|
|
990
|
+
frameCapturer: void 0,
|
|
991
|
+
deepsightService: void 0,
|
|
992
|
+
detectionStatus: "idle",
|
|
993
|
+
yawRange: 0,
|
|
994
|
+
pitchRange: 0,
|
|
995
|
+
capturedImage: void 0,
|
|
996
|
+
livenessOutcome: void 0,
|
|
997
|
+
signals: void 0,
|
|
998
|
+
token: void 0,
|
|
999
|
+
localVerdict: void 0,
|
|
1000
|
+
verdict: void 0,
|
|
1001
|
+
error: void 0,
|
|
1002
|
+
resetDetection: void 0
|
|
1003
|
+
}),
|
|
1004
|
+
on: { QUIT: { target: "#personhood.closed" } },
|
|
1005
|
+
states: {
|
|
1006
|
+
idle: { on: { LOAD: [{
|
|
1007
|
+
target: "tutorial",
|
|
1008
|
+
guard: "hasShowTutorial"
|
|
1009
|
+
}, { target: "collectingSignals" }] } },
|
|
1010
|
+
tutorial: { on: { NEXT_STEP: { target: "collectingSignals" } } },
|
|
1011
|
+
collectingSignals: {
|
|
1012
|
+
type: "parallel",
|
|
1013
|
+
states: {
|
|
1014
|
+
signals: {
|
|
1015
|
+
initial: "running",
|
|
1016
|
+
states: {
|
|
1017
|
+
running: { invoke: {
|
|
1018
|
+
id: "collectSignals",
|
|
1019
|
+
src: "collectSignals",
|
|
1020
|
+
onDone: {
|
|
1021
|
+
target: "done",
|
|
1022
|
+
actions: "setSignalsFromEvent"
|
|
1023
|
+
},
|
|
1024
|
+
onError: { target: "done" }
|
|
1025
|
+
} },
|
|
1026
|
+
done: { type: "final" }
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
deepsight: {
|
|
1030
|
+
initial: "running",
|
|
1031
|
+
states: {
|
|
1032
|
+
running: { invoke: {
|
|
1033
|
+
id: "initializeDeepsight",
|
|
1034
|
+
src: "initializeDeepsight",
|
|
1035
|
+
input: ({ context }) => ({ dependencies: context.dependencies }),
|
|
1036
|
+
onDone: {
|
|
1037
|
+
target: "done",
|
|
1038
|
+
actions: "setDeepsightServiceFromEvent"
|
|
1039
|
+
},
|
|
1040
|
+
onError: { target: "done" }
|
|
1041
|
+
} },
|
|
1042
|
+
done: { type: "final" }
|
|
1043
|
+
}
|
|
1044
|
+
},
|
|
1045
|
+
token: {
|
|
1046
|
+
initial: "running",
|
|
1047
|
+
states: {
|
|
1048
|
+
running: { invoke: {
|
|
1049
|
+
id: "fetchToken",
|
|
1050
|
+
src: "fetchToken",
|
|
1051
|
+
input: ({ context }) => ({ config: context.config }),
|
|
1052
|
+
onDone: {
|
|
1053
|
+
target: "done",
|
|
1054
|
+
actions: "setTokenFromEvent"
|
|
1055
|
+
},
|
|
1056
|
+
onError: {
|
|
1057
|
+
target: "done",
|
|
1058
|
+
actions: "setTokenError"
|
|
1059
|
+
}
|
|
1060
|
+
} },
|
|
1061
|
+
done: { type: "final" }
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
onDone: [{
|
|
1066
|
+
target: "error",
|
|
1067
|
+
guard: "hasTokenError"
|
|
1068
|
+
}, { target: "checkingPermission" }]
|
|
1069
|
+
},
|
|
1070
|
+
checkingPermission: { invoke: {
|
|
1071
|
+
id: "checkPermission",
|
|
1072
|
+
src: "checkPermission",
|
|
1073
|
+
onDone: [
|
|
1074
|
+
{
|
|
1075
|
+
target: "capture",
|
|
1076
|
+
guard: "isPermissionGranted",
|
|
1077
|
+
actions: "setPermissionResultFromEvent"
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
target: "permissions.denied",
|
|
1081
|
+
guard: "isPermissionDenied",
|
|
1082
|
+
actions: "setPermissionResultFromEvent"
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
target: "permissions",
|
|
1086
|
+
actions: "setPermissionResultFromEvent"
|
|
1087
|
+
}
|
|
1088
|
+
]
|
|
1089
|
+
} },
|
|
1090
|
+
permissions: {
|
|
1091
|
+
initial: "idle",
|
|
1092
|
+
states: {
|
|
1093
|
+
idle: { on: {
|
|
1094
|
+
REQUEST_PERMISSION: "requesting",
|
|
1095
|
+
GO_TO_LEARN_MORE: "learnMore"
|
|
1096
|
+
} },
|
|
1097
|
+
learnMore: { on: {
|
|
1098
|
+
BACK: "idle",
|
|
1099
|
+
REQUEST_PERMISSION: "requesting"
|
|
1100
|
+
} },
|
|
1101
|
+
requesting: { invoke: {
|
|
1102
|
+
id: "requestPermission",
|
|
1103
|
+
src: "requestPermission",
|
|
1104
|
+
onDone: [
|
|
1105
|
+
{
|
|
1106
|
+
target: "#personhood.capture",
|
|
1107
|
+
guard: "isPermissionGranted",
|
|
1108
|
+
actions: "setPermissionResultFromEvent"
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
target: "denied",
|
|
1112
|
+
guard: "isPermissionDenied",
|
|
1113
|
+
actions: "setPermissionResultFromEvent"
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
target: "idle",
|
|
1117
|
+
actions: "setPermissionResultFromEvent"
|
|
1118
|
+
}
|
|
1119
|
+
],
|
|
1120
|
+
onError: {
|
|
1121
|
+
target: "denied",
|
|
1122
|
+
actions: "setPermissionDenied"
|
|
1123
|
+
}
|
|
1124
|
+
} },
|
|
1125
|
+
denied: {
|
|
1126
|
+
entry: "setPermissionDenied",
|
|
1127
|
+
on: { USE_FALLBACK: { target: "#personhood.serverFallback" } }
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
on: { USE_FALLBACK: { target: "#personhood.serverFallback" } }
|
|
1131
|
+
},
|
|
1132
|
+
capture: {
|
|
1133
|
+
initial: "initializing",
|
|
1134
|
+
states: {
|
|
1135
|
+
initializing: { invoke: {
|
|
1136
|
+
id: "initializeCamera",
|
|
1137
|
+
src: "initializeCamera",
|
|
1138
|
+
input: ({ context }) => ({ deepsightService: context.deepsightService }),
|
|
1139
|
+
onDone: {
|
|
1140
|
+
target: "detecting",
|
|
1141
|
+
actions: "setStreamAndProvider"
|
|
1142
|
+
},
|
|
1143
|
+
onError: {
|
|
1144
|
+
target: "#personhood.serverFallback",
|
|
1145
|
+
actions: "setFallbackReasonNoCamera"
|
|
1146
|
+
}
|
|
1147
|
+
} },
|
|
1148
|
+
detecting: {
|
|
1149
|
+
invoke: {
|
|
1150
|
+
id: "runDetection",
|
|
1151
|
+
src: "runDetection",
|
|
1152
|
+
input: ({ context }) => ({
|
|
1153
|
+
provider: context.provider,
|
|
1154
|
+
frameCapturer: context.frameCapturer,
|
|
1155
|
+
autoComplete: context.config.autoComplete
|
|
1156
|
+
}),
|
|
1157
|
+
onError: {
|
|
1158
|
+
target: "#personhood.error",
|
|
1159
|
+
actions: "setDetectionError"
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
on: {
|
|
1163
|
+
DETECTION_UPDATE: [{
|
|
1164
|
+
guard: "isDetectionError",
|
|
1165
|
+
target: "#personhood.error",
|
|
1166
|
+
actions: [
|
|
1167
|
+
"setDetectionStatus",
|
|
1168
|
+
"setDetectionError",
|
|
1169
|
+
"emitSignal"
|
|
1170
|
+
]
|
|
1171
|
+
}, { actions: ["setDetectionStatus", "emitSignal"] }],
|
|
1172
|
+
DETECTION_RESET_READY: { actions: "setResetDetection" },
|
|
1173
|
+
DETECTION_SUCCESS: {
|
|
1174
|
+
target: "#personhood.analyzing",
|
|
1175
|
+
actions: "setCapturedImageAndRangeFromEvent"
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
},
|
|
1181
|
+
analyzing: {
|
|
1182
|
+
exit: "stopMediaStream",
|
|
1183
|
+
invoke: {
|
|
1184
|
+
id: "analyzeLiveness",
|
|
1185
|
+
src: "analyzeLiveness",
|
|
1186
|
+
input: ({ context }) => ({
|
|
1187
|
+
deepsightService: context.deepsightService,
|
|
1188
|
+
stream: context.stream,
|
|
1189
|
+
capturedImage: context.capturedImage,
|
|
1190
|
+
yawRange: context.yawRange,
|
|
1191
|
+
pitchRange: context.pitchRange,
|
|
1192
|
+
token: context.token
|
|
1193
|
+
}),
|
|
1194
|
+
onDone: {
|
|
1195
|
+
target: "processing",
|
|
1196
|
+
actions: "setLivenessOutcomeFromEvent"
|
|
1197
|
+
},
|
|
1198
|
+
onError: { target: "processing" }
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
processing: { invoke: {
|
|
1202
|
+
id: "produceLocalVerdict",
|
|
1203
|
+
src: "produceLocalVerdict",
|
|
1204
|
+
input: ({ context }) => ({
|
|
1205
|
+
config: context.config,
|
|
1206
|
+
signals: context.signals ?? emptySignals(),
|
|
1207
|
+
detectionStatus: context.detectionStatus,
|
|
1208
|
+
liveness: context.livenessOutcome
|
|
1209
|
+
}),
|
|
1210
|
+
onDone: {
|
|
1211
|
+
target: "verifyingOnServer",
|
|
1212
|
+
actions: "setLocalVerdictFromEvent"
|
|
1213
|
+
},
|
|
1214
|
+
onError: {
|
|
1215
|
+
target: "error",
|
|
1216
|
+
actions: "setLocalVerdictError"
|
|
1217
|
+
}
|
|
1218
|
+
} },
|
|
1219
|
+
verifyingOnServer: { invoke: {
|
|
1220
|
+
id: "verifyOnDevice",
|
|
1221
|
+
src: "verifyOnDevice",
|
|
1222
|
+
input: ({ context }) => ({
|
|
1223
|
+
config: context.config,
|
|
1224
|
+
liveness: context.livenessOutcome,
|
|
1225
|
+
localVerdict: context.localVerdict,
|
|
1226
|
+
token: context.token
|
|
1227
|
+
}),
|
|
1228
|
+
onDone: {
|
|
1229
|
+
target: "finished",
|
|
1230
|
+
actions: "setVerdictFromEvent"
|
|
1231
|
+
},
|
|
1232
|
+
onError: {
|
|
1233
|
+
target: "finished",
|
|
1234
|
+
actions: "setVerdictToLocal"
|
|
1235
|
+
}
|
|
1236
|
+
} },
|
|
1237
|
+
serverFallback: { invoke: {
|
|
1238
|
+
id: "requestServerFallback",
|
|
1239
|
+
src: "requestServerFallback",
|
|
1240
|
+
input: ({ context }) => ({
|
|
1241
|
+
config: context.config,
|
|
1242
|
+
signals: context.signals ?? emptySignals(),
|
|
1243
|
+
reason: context.fallbackReason ?? "permission_denied"
|
|
1244
|
+
}),
|
|
1245
|
+
onDone: {
|
|
1246
|
+
target: "finished",
|
|
1247
|
+
actions: "setVerdictFromEvent"
|
|
1248
|
+
},
|
|
1249
|
+
onError: {
|
|
1250
|
+
target: "error",
|
|
1251
|
+
actions: "setVerifyError"
|
|
1252
|
+
}
|
|
1253
|
+
} },
|
|
1254
|
+
finished: { type: "final" },
|
|
1255
|
+
closed: {
|
|
1256
|
+
entry: "stopMediaStream",
|
|
1257
|
+
type: "final"
|
|
1258
|
+
},
|
|
1259
|
+
error: {
|
|
1260
|
+
entry: "stopMediaStream",
|
|
1261
|
+
on: { RESET: {
|
|
1262
|
+
target: "idle",
|
|
1263
|
+
actions: "resetContext"
|
|
1264
|
+
} }
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
/**
|
|
1269
|
+
* Public machine value. Typed as `AnyStateMachine` for declaration file
|
|
1270
|
+
* portability — mirrors the convention used by `selfieMachine` and the
|
|
1271
|
+
* other Core modules. Type safety is enforced by the `setup()` config
|
|
1272
|
+
* above, not by the exported binding.
|
|
1273
|
+
*/
|
|
1274
|
+
const personhoodMachine = _personhoodMachine;
|
|
1275
|
+
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region src/modules/personhood/personhoodActor.ts
|
|
1278
|
+
/**
|
|
1279
|
+
* Production-default {@link PersonhoodDependencies}. Browser storage +
|
|
1280
|
+
* the singleton WASM util. Single source of truth for what the actor
|
|
1281
|
+
* and the manager wire up when the caller doesn't provide their own;
|
|
1282
|
+
* tests and the extensibility layer pass `dependencies` explicitly.
|
|
1283
|
+
*/
|
|
1284
|
+
function getDefaultPersonhoodDependencies() {
|
|
1285
|
+
return {
|
|
1286
|
+
storage: new BrowserStorageProvider(),
|
|
1287
|
+
getWasmUtil: () => WasmUtilProvider.getInstance()
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Creates and starts a Personhood XState actor. Defaults to the production
|
|
1292
|
+
* browser storage + WASM providers; passing `dependencies` lets tests and the
|
|
1293
|
+
* extensibility layer swap them for fakes.
|
|
1294
|
+
*/
|
|
1295
|
+
function createPersonhoodActor(options) {
|
|
1296
|
+
const dependencies = options.dependencies ?? getDefaultPersonhoodDependencies();
|
|
1297
|
+
return createActor(personhoodMachine, { input: {
|
|
1298
|
+
config: options.config,
|
|
1299
|
+
dependencies
|
|
1300
|
+
} }).start();
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
//#endregion
|
|
1304
|
+
//#region src/modules/personhood/personhoodManager.ts
|
|
1305
|
+
/**
|
|
1306
|
+
* Personhood deliberately opts out of the SDK's manager-instrumentation
|
|
1307
|
+
* analytics pipeline. The widget runs as a self-contained modal/inline
|
|
1308
|
+
* captcha replacement, not as part of the broader onboarding funnel —
|
|
1309
|
+
* it doesn't share an onboarding session token (`/omni/start`) with
|
|
1310
|
+
* the rest of an integrator's flow, so the per-transition events the
|
|
1311
|
+
* default instrumentation would emit can't authenticate against
|
|
1312
|
+
* `/omni/interview-events`. Wiring them up only produced a cascade of
|
|
1313
|
+
* 403s in integrators' devtools (one preflight + one POST per
|
|
1314
|
+
* `screenOpened` / `elementClicked` / `captureAttemptFinished` /
|
|
1315
|
+
* `errorTriggered` transition), with no usable analytics signal on
|
|
1316
|
+
* the receiving end.
|
|
1317
|
+
*
|
|
1318
|
+
* `createManager()` requires the `instrumentation` option to be
|
|
1319
|
+
* present, so we pass a no-op that satisfies the contract without
|
|
1320
|
+
* firing any of the helpers. If a future use case calls for
|
|
1321
|
+
* personhood analytics (integrator opt-in, internal QA dashboard,
|
|
1322
|
+
* etc.) the right move is to re-enable `createManagerInstrumentation`
|
|
1323
|
+
* here gated on a config flag — not to remove this no-op.
|
|
1324
|
+
*/
|
|
1325
|
+
function createNoopInstrumentation() {
|
|
1326
|
+
return {
|
|
1327
|
+
moduleName: eventModuleNames.personhood,
|
|
1328
|
+
onModuleOpened: () => void 0,
|
|
1329
|
+
onModuleClosed: () => void 0,
|
|
1330
|
+
getScreenName: () => void 0,
|
|
1331
|
+
onScreenOpened: () => void 0,
|
|
1332
|
+
onScreenClosed: () => void 0,
|
|
1333
|
+
getErrorName: () => void 0,
|
|
1334
|
+
getErrorPayload: () => void 0,
|
|
1335
|
+
onErrorTriggered: () => void 0,
|
|
1336
|
+
onElementClicked: () => void 0,
|
|
1337
|
+
onCaptureAttemptFinished: () => void 0
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
function getPermissionSubState(snapshot) {
|
|
1341
|
+
if (snapshot.matches({ permissions: "learnMore" })) return "learnMore";
|
|
1342
|
+
if (snapshot.matches({ permissions: "requesting" })) return "requesting";
|
|
1343
|
+
if (snapshot.matches({ permissions: "denied" })) return "denied";
|
|
1344
|
+
return "idle";
|
|
1345
|
+
}
|
|
1346
|
+
function mapState(snapshot) {
|
|
1347
|
+
const context = snapshot.context;
|
|
1348
|
+
if (snapshot.matches("idle")) return { status: "idle" };
|
|
1349
|
+
if (snapshot.matches("tutorial")) return { status: "tutorial" };
|
|
1350
|
+
if (snapshot.matches("collectingSignals")) return {
|
|
1351
|
+
status: "loading",
|
|
1352
|
+
phase: "collectingSignals"
|
|
1353
|
+
};
|
|
1354
|
+
if (snapshot.matches("checkingPermission")) return {
|
|
1355
|
+
status: "loading",
|
|
1356
|
+
phase: "checkingPermission"
|
|
1357
|
+
};
|
|
1358
|
+
if (snapshot.matches("permissions")) return {
|
|
1359
|
+
status: "permissions",
|
|
1360
|
+
permissionStatus: getPermissionSubState(snapshot)
|
|
1361
|
+
};
|
|
1362
|
+
if (snapshot.matches({ capture: "initializing" })) return {
|
|
1363
|
+
status: "capture",
|
|
1364
|
+
captureStatus: "initializing",
|
|
1365
|
+
detectionStatus: context.detectionStatus,
|
|
1366
|
+
stream: context.stream
|
|
1367
|
+
};
|
|
1368
|
+
if (snapshot.matches("capture")) return {
|
|
1369
|
+
status: "capture",
|
|
1370
|
+
captureStatus: "detecting",
|
|
1371
|
+
detectionStatus: context.detectionStatus,
|
|
1372
|
+
stream: context.stream
|
|
1373
|
+
};
|
|
1374
|
+
if (snapshot.matches("analyzing")) return {
|
|
1375
|
+
status: "processing",
|
|
1376
|
+
source: "analyzing"
|
|
1377
|
+
};
|
|
1378
|
+
if (snapshot.matches("processing")) return {
|
|
1379
|
+
status: "processing",
|
|
1380
|
+
source: "local"
|
|
1381
|
+
};
|
|
1382
|
+
if (snapshot.matches("serverFallback")) return {
|
|
1383
|
+
status: "processing",
|
|
1384
|
+
source: "fallback"
|
|
1385
|
+
};
|
|
1386
|
+
if (snapshot.matches("verifyingOnServer")) return {
|
|
1387
|
+
status: "processing",
|
|
1388
|
+
source: "local"
|
|
1389
|
+
};
|
|
1390
|
+
if (snapshot.matches("finished")) return {
|
|
1391
|
+
status: "finished",
|
|
1392
|
+
verdict: context.verdict ?? {
|
|
1393
|
+
human: false,
|
|
1394
|
+
confidence: 0,
|
|
1395
|
+
evidenceId: "missing",
|
|
1396
|
+
signals: emptySignals(),
|
|
1397
|
+
spoofCheckResults: []
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
if (snapshot.matches("closed")) return { status: "closed" };
|
|
1401
|
+
if (snapshot.matches("error")) return {
|
|
1402
|
+
status: "error",
|
|
1403
|
+
error: context.error ?? { code: "UNKNOWN" }
|
|
1404
|
+
};
|
|
1405
|
+
return { status: "idle" };
|
|
1406
|
+
}
|
|
1407
|
+
function createApi({ actor, trackElementClicked }) {
|
|
1408
|
+
return {
|
|
1409
|
+
load() {
|
|
1410
|
+
actor.send({ type: "LOAD" });
|
|
1411
|
+
},
|
|
1412
|
+
nextStep() {
|
|
1413
|
+
trackElementClicked?.("nextStep");
|
|
1414
|
+
actor.send({ type: "NEXT_STEP" });
|
|
1415
|
+
},
|
|
1416
|
+
requestPermission() {
|
|
1417
|
+
trackElementClicked?.("requestPermission");
|
|
1418
|
+
actor.send({ type: "REQUEST_PERMISSION" });
|
|
1419
|
+
},
|
|
1420
|
+
goToLearnMore() {
|
|
1421
|
+
trackElementClicked?.("goToLearnMore");
|
|
1422
|
+
actor.send({ type: "GO_TO_LEARN_MORE" });
|
|
1423
|
+
},
|
|
1424
|
+
back() {
|
|
1425
|
+
trackElementClicked?.("back");
|
|
1426
|
+
actor.send({ type: "BACK" });
|
|
1427
|
+
},
|
|
1428
|
+
close() {
|
|
1429
|
+
trackElementClicked?.("close");
|
|
1430
|
+
actor.send({ type: "QUIT" });
|
|
1431
|
+
},
|
|
1432
|
+
reset() {
|
|
1433
|
+
actor.send({ type: "RESET" });
|
|
1434
|
+
},
|
|
1435
|
+
useFallback() {
|
|
1436
|
+
trackElementClicked?.("useFallback");
|
|
1437
|
+
actor.send({ type: "USE_FALLBACK" });
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Wraps a pre-built personhood actor in a manager. Internal helper
|
|
1443
|
+
* shared by {@link createPersonhoodManager} (drop-in widget path) and
|
|
1444
|
+
* {@link createPersonhoodManagerFromActor} (extensibility surface).
|
|
1445
|
+
* Centralizing the `createManager(...)` + instrumentation call avoids
|
|
1446
|
+
* two near-identical wrap blocks drifting apart.
|
|
1447
|
+
*/
|
|
1448
|
+
function wrapActor(actor) {
|
|
1449
|
+
return createManager({
|
|
1450
|
+
actor,
|
|
1451
|
+
mapState,
|
|
1452
|
+
createApi,
|
|
1453
|
+
instrumentation: createNoopInstrumentation()
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Manager built from an externally-owned actor (extensibility path).
|
|
1458
|
+
*/
|
|
1459
|
+
function createPersonhoodManagerFromActor(actor) {
|
|
1460
|
+
return wrapActor(actor);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Creates a Personhood manager for the drop-in widget.
|
|
1464
|
+
*
|
|
1465
|
+
* The manager exposes:
|
|
1466
|
+
* - `state: PersonhoodState` via `getState()` / `subscribe()`
|
|
1467
|
+
* - Lifecycle controls: `load`, `nextStep`, `requestPermission`,
|
|
1468
|
+
* `goToLearnMore`, `back`, `close`, `reset`, `useFallback`, `stop`
|
|
1469
|
+
*
|
|
1470
|
+
* Head-pose is intentionally **not** exposed as a stream. The SDK
|
|
1471
|
+
* extracts pose internally for the server-side `headMotion` spoof
|
|
1472
|
+
* check; UI consumers that need a frame-rate pose signal (avatar
|
|
1473
|
+
* mirroring, etc.) should run their own face-tracking pipeline
|
|
1474
|
+
* (e.g. MediaPipe FaceLandmarker) against `state.stream` instead.
|
|
1475
|
+
*
|
|
1476
|
+
* @example
|
|
1477
|
+
* ```ts
|
|
1478
|
+
* const manager = createPersonhoodManager({
|
|
1479
|
+
* config: { siteKey: 'demo', mockVerify: true },
|
|
1480
|
+
* });
|
|
1481
|
+
* manager.subscribe((state) => {
|
|
1482
|
+
* if (state.status === 'finished') {
|
|
1483
|
+
* console.log('human?', state.verdict.human);
|
|
1484
|
+
* }
|
|
1485
|
+
* });
|
|
1486
|
+
* manager.load();
|
|
1487
|
+
* ```
|
|
1488
|
+
*/
|
|
1489
|
+
function createPersonhoodManager(options) {
|
|
1490
|
+
return wrapActor(createPersonhoodActor(options));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
//#endregion
|
|
1494
|
+
export { PERSONHOOD_ERROR_CODES, createPersonhoodActor, createPersonhoodManager, createPersonhoodManagerFromActor, personhoodMachine };
|