@incodetech/core 2.3.2 → 2.4.0-rc.1
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/THIRD_PARTY_NOTICES.txt +1243 -3
- package/dist/Actor-DpTj5PFb.d.ts +1 -0
- package/dist/{BaseWasmProvider-Dyi6dKp1.d.ts → BaseWasmProvider-Bq3OHo7P.d.ts} +2 -2
- package/dist/BrowserDiagnosticsEnvelopeProvider-BQca6KPG.esm.js +43 -0
- package/dist/{FaceDetectionProvider-DtmtCKa9.esm.js → FaceDetectionProvider-BIImDZrc.esm.js} +125 -10
- package/dist/HttpRequestError-Bh0pNGSC.esm.js +35 -0
- package/dist/{IIdCaptureCapability-RNiFhvXR.d.ts → IIdCaptureCapability-CGcEt2Cw.d.ts} +2 -2
- package/dist/{IWasmUtilCapability-CNWRl7jr.d.ts → IWasmUtilCapability-GF2n7sfm.d.ts} +28 -5
- package/dist/{Manager-CQQ99QOI.d.ts → Manager-C9T5amUN.d.ts} +7 -1
- package/dist/{MotionSensorProvider-DyhbM4Si.esm.js → MotionSensorProvider-DYxh8TjR.esm.js} +1 -1
- package/dist/{ekycStateMachine-UeJRO8_p.esm.js → PhoneValidationProvider-SZiOS0jB.esm.js} +232 -827
- package/dist/{StreamCanvasCapture-_8gdW5Lk.d.ts → StreamCanvasCapture-7JRf-2ck.d.ts} +2 -2
- package/dist/{safeStorage-BdU9_m2E.esm.js → WasmUtilProvider-DtJuBdt-.esm.js} +111 -49
- package/dist/{addressSearch-DyHGgeX8.esm.js → addressSearch-DPjIKJrX.esm.js} +142 -4
- package/dist/{advanceableErrors-CpF2cBy6.esm.js → advanceableErrors-q_pfU3Cn.esm.js} +5 -4
- package/dist/ae-signature.d.ts +2 -2
- package/dist/ae-signature.esm.js +1 -1
- package/dist/antifraud.d.ts +1 -1
- package/dist/antifraud.esm.js +2 -2
- package/dist/{antifraudManager-DNkw66oW.esm.js → antifraudManager-CaweCYnY.esm.js} +3 -3
- package/dist/{antifraudManager-BY36bS12.d.ts → antifraudManager-DbGCvlJs.d.ts} +2 -2
- package/dist/{antifraudStateMachine-DBeq47D2.esm.js → antifraudStateMachine-DRl0UVyA.esm.js} +3 -4
- package/dist/{endpoints-B-3Q-LUw.esm.js → api-Cz90EHyu.esm.js} +151 -1
- package/dist/{apiError-CmGEmaGU.esm.js → apiError-D5u6HIDG.esm.js} +2 -1
- package/dist/authentication.d.ts +3 -3
- package/dist/authentication.esm.js +2 -2
- package/dist/{authenticationManager-CboRCl_N.d.ts → authenticationManager-BIZC55Bi.d.ts} +2 -8
- package/dist/{authenticationManager-BFjSjtQ_.esm.js → authenticationManager-CB3YW6EK.esm.js} +6 -6
- package/dist/{authenticationStateMachine-Dfo34ZiI.esm.js → authenticationStateMachine-BJJhLmdH.esm.js} +5 -6
- package/dist/avatar.d.ts +1 -1
- package/dist/{backCameraStream-B0-YgP_y.esm.js → backCameraStream-CMvXj4X4.esm.js} +1 -1
- package/dist/camera.esm.js +1 -1
- package/dist/certificate-issuance.d.ts +1 -1
- package/dist/certificate-issuance.esm.js +2 -2
- package/dist/{certificateIssuanceManager-C3-CI0_s.esm.js → certificateIssuanceManager-BfeMv2PU.esm.js} +3 -3
- package/dist/{certificateIssuanceManager-DCgEidHz.d.ts → certificateIssuanceManager-Dr8A4Lgr.d.ts} +2 -3
- package/dist/{certificateIssuanceStateMachine-CxqS_nD-.esm.js → certificateIssuanceStateMachine-Frgjbjw8.esm.js} +4 -5
- package/dist/consent.d.ts +1 -1
- package/dist/consent.esm.js +2 -2
- package/dist/{consentManager-B6E1XJDw.esm.js → consentManager-CvEu8h_W.esm.js} +3 -3
- package/dist/{consentManager-2h_w6wFc.d.ts → consentManager-Cv_0QKLD.d.ts} +7 -8
- package/dist/{consentStateMachine-DTSfdGiW.esm.js → consentStateMachine-DJTgN_-1.esm.js} +3 -4
- package/dist/{createCameraCycleOwnership-DnwTl1ad.esm.js → createCameraCycleOwnership-Dj_LHUvN.esm.js} +71 -66
- package/dist/{createFaceAvatar-BlcTYgJv.d.ts → createFaceAvatar-lYX0VTYs.d.ts} +1 -1
- package/dist/cross-document-data-match.d.ts +1 -1
- package/dist/cross-document-data-match.esm.js +1 -1
- package/dist/{crossDocumentDataMatchManager-9XhFVZ5S.esm.js → crossDocumentDataMatchManager-Cu141CE8.esm.js} +4 -5
- package/dist/{crossDocumentDataMatchManager-Dq8Z3SOg.d.ts → crossDocumentDataMatchManager-yCWCOzcU.d.ts} +2 -2
- package/dist/curp-validation.d.ts +1 -1
- package/dist/curp-validation.esm.js +2 -2
- package/dist/{curpValidationManager-DbIMYFcJ.esm.js → curpValidationManager-B-z9q5V3.esm.js} +15 -22
- package/dist/{curpValidationManager-DNuZn9fn.d.ts → curpValidationManager-C9sqoh9n.d.ts} +3 -3
- package/dist/{curpValidationStateMachine-g5kMfuLj.esm.js → curpValidationStateMachine-vDEFLMPw.esm.js} +4 -5
- package/dist/custom-fields.d.ts +1 -1
- package/dist/custom-fields.esm.js +1 -1
- package/dist/custom-watchlist.d.ts +1 -1
- package/dist/custom-watchlist.esm.js +2 -2
- package/dist/{customFieldsManager-8-W2KY4n.d.ts → customFieldsManager-CF-cO8ap.d.ts} +2 -2
- package/dist/{customFieldsManager-CzWZTkW-.esm.js → customFieldsManager-D1ex2cFM.esm.js} +4 -5
- package/dist/{customWatchlistManager-DWTED5Ur.esm.js → customWatchlistManager-CTI-SGk0.esm.js} +3 -3
- package/dist/{customWatchlistManager-PBWHr_oM.d.ts → customWatchlistManager-DolUswyR.d.ts} +2 -2
- package/dist/{customWatchlistStateMachine-DOQou6TW.esm.js → customWatchlistStateMachine-hmRd0f2I.esm.js} +3 -4
- package/dist/deepsightLoader-DZMdYSl-.esm.js +55 -0
- package/dist/{deepsightLoader-v-dLejS-.esm.js → deepsightService-tGP53cSS.esm.js} +51 -56
- package/dist/device.esm.js +1 -1
- package/dist/document-capture.d.ts +2 -2
- package/dist/document-capture.esm.js +2 -2
- package/dist/{documentCaptureManager-BRPxqYD2.esm.js → documentCaptureManager-DUJwpKC5.esm.js} +17 -18
- package/dist/{documentCaptureManager-lEtCM320.d.ts → documentCaptureManager-DkyDtm3Y.d.ts} +11 -34
- package/dist/{documentCaptureStateMachine-D312sSIu.esm.js → documentCaptureStateMachine-B8i7GnLh.esm.js} +55 -59
- package/dist/dynamic-forms.d.ts +2 -2
- package/dist/dynamic-forms.esm.js +2 -2
- package/dist/{dynamicFormsManager-C-9Jhnd3.esm.js → dynamicFormsManager-Bxmv_9PB.esm.js} +3 -3
- package/dist/{dynamicFormsManager-D5R_uxLQ.d.ts → dynamicFormsManager-C0dHs1ty.d.ts} +3 -3
- package/dist/{dynamicFormsStateMachine-DGI_F2PP.esm.js → dynamicFormsStateMachine-CoOWTzyM.esm.js} +4 -5
- package/dist/ekyb.d.ts +5 -5
- package/dist/ekyb.esm.js +3 -3
- package/dist/{ekybManager-DeoMcZEz.esm.js → ekybManager-BBkdPxUY.esm.js} +23 -27
- package/dist/{ekybManager-BwRuGAwt.d.ts → ekybManager-BaZ_7CEu.d.ts} +4 -4
- package/dist/{ekybStateMachine-D1IJ1Z72.esm.js → ekybStateMachine-n4Iz9iLc.esm.js} +338 -14
- package/dist/ekyc.d.ts +2 -2
- package/dist/ekyc.esm.js +3 -3
- package/dist/{ekycManager-BX2d46FY.esm.js → ekycManager-BO-Lui3t.esm.js} +29 -36
- package/dist/{ekycManager-DLzDuBzI.d.ts → ekycManager-DW-ynodd.d.ts} +3 -3
- package/dist/ekycStateMachine-CDnI_NRq.esm.js +1443 -0
- package/dist/{ekycUtils-D6wfSpPN.esm.js → ekycUtils-zY8CdKJL.esm.js} +53 -3
- package/dist/electronic-signature.d.ts +1 -1
- package/dist/electronic-signature.esm.js +1 -1
- package/dist/{electronicSignatureManager-yYUc30na.esm.js → electronicSignatureManager-BtWCrz5_.esm.js} +10 -11
- package/dist/{electronicSignatureManager-Ds-KEVTq.d.ts → electronicSignatureManager-DTSLOvKR.d.ts} +2 -3
- package/dist/email.d.ts +1 -1
- package/dist/email.esm.js +2 -2
- package/dist/{emailManager-bR1N5rV_.esm.js → emailManager-ButFktRm.esm.js} +20 -26
- package/dist/{emailManager-CADXW_9X.d.ts → emailManager-Dy6omJVp.d.ts} +2 -2
- package/dist/{emailStateMachine-Bc_6oIYw.esm.js → emailStateMachine-BXtlC0NV.esm.js} +3 -4
- package/dist/{events-ijlbX_Wx.esm.js → events-Du2A4-Qr.esm.js} +203 -8
- package/dist/events.d.ts +3 -2
- package/dist/events.esm.js +1 -1
- package/dist/extensibility.d.ts +38 -38
- package/dist/extensibility.esm.js +36 -36
- package/dist/{externalFetch-BKBThAUC.esm.js → externalFetch-DrR9xshk.esm.js} +3 -2
- package/dist/face-match.d.ts +1 -1
- package/dist/face-match.esm.js +2 -2
- package/dist/{faceCaptureManagerFactory-m-7ae67q.d.ts → faceCaptureManagerFactory-1hzeS_rB.d.ts} +11 -51
- package/dist/{faceCaptureManagerFactory-B6i9QcAk.esm.js → faceCaptureManagerFactory-CwZUeYfq.esm.js} +19 -54
- package/dist/{faceCaptureSetup-DU1mjRr6.esm.js → faceCaptureSetup-CYbdKNlI.esm.js} +104 -396
- package/dist/{faceMatchManager-BnvrZM2F.d.ts → faceMatchManager-DIgG-UFS.d.ts} +1 -2
- package/dist/{faceMatchManager-Bmv0HzuY.esm.js → faceMatchManager-DWFUQjMQ.esm.js} +3 -3
- package/dist/{faceMatchStateMachine-mRIHQ3dA.esm.js → faceMatchStateMachine-CUCvlydD.esm.js} +6 -5
- package/dist/{feature-management-C8CuN0os.esm.js → feature-management-rTfjzdDa.esm.js} +3 -3
- package/dist/feature-management.esm.js +1 -1
- package/dist/field-comparison.d.ts +2 -2
- package/dist/field-comparison.esm.js +1 -1
- package/dist/{fieldComparisonManager-CBvEGxFx.d.ts → fieldComparisonManager-CUX4gza2.d.ts} +6 -3
- package/dist/{fieldComparisonManager-CC-klG0h.esm.js → fieldComparisonManager-v3knTYV_.esm.js} +35 -18
- package/dist/fiscal-qr.d.ts +1 -1
- package/dist/fiscal-qr.esm.js +1 -1
- package/dist/{fiscalQrManager-B1STmT6z.d.ts → fiscalQrManager-D0Iy7EKR.d.ts} +2 -2
- package/dist/fiscalQrManager-Dt9fHG7h.esm.js +6798 -0
- package/dist/flow-events.d.ts +1 -1
- package/dist/flow.d.ts +14 -13
- package/dist/flow.esm.js +14 -14
- package/dist/{flowCompletionService-DccgtDnm.esm.js → flowCompletionService-oiK1PjPr.esm.js} +2 -3
- package/dist/{flowServices-CdsWsppG.esm.js → flowServices-CTc-zqfN.esm.js} +11 -8
- package/dist/geolocation.d.ts +1 -1
- package/dist/geolocation.esm.js +2 -2
- package/dist/{geolocationManager-CGHBxtMb.esm.js → geolocationManager-BOdtVuR8.esm.js} +6 -7
- package/dist/{geolocationManager-Cz744E5S.d.ts → geolocationManager-Dn24g1fl.d.ts} +2 -3
- package/dist/{geolocationStateMachine-CdEb1Gn0.esm.js → geolocationStateMachine-BnGuZ3hn.esm.js} +4 -5
- package/dist/{getDeviceClass-5NYZg9s6.esm.js → getDeviceClass-bsWdEDxP.esm.js} +1 -1
- package/dist/{getParsedUserAgent-B2iFDql1.esm.js → getParsedUserAgent-DhQPeKqw.esm.js} +1 -1
- package/dist/government-validation.d.ts +1 -1
- package/dist/government-validation.esm.js +2 -2
- package/dist/{governmentValidationManager-D02vT4yY.d.ts → governmentValidationManager-B82zOut8.d.ts} +3 -3
- package/dist/{governmentValidationManager-CacFOtKO.esm.js → governmentValidationManager-BtXWHICu.esm.js} +3 -3
- package/dist/{governmentValidationStateMachine-CoaZOSTP.esm.js → governmentValidationStateMachine-COL-Vs48.esm.js} +3 -4
- package/dist/home.d.ts +3 -26
- package/dist/home.esm.js +2 -2
- package/dist/http.esm.js +1 -1
- package/dist/id-ocr.d.ts +1 -1
- package/dist/id-ocr.esm.js +2 -2
- package/dist/id-selfie-flow.d.ts +6 -9
- package/dist/id-selfie-flow.esm.js +23 -28
- package/dist/id-verification.d.ts +1 -1
- package/dist/id-verification.esm.js +2 -2
- package/dist/id.d.ts +4 -4
- package/dist/id.esm.js +4 -205
- package/dist/{idCaptureDefaults-Dl64Trv5.esm.js → idCaptureDefaults-BGsG2OAs.esm.js} +7 -5
- package/dist/{idCaptureManager-BHwL2Z0V.d.ts → idCaptureManager-0M02hSuC.d.ts} +242 -98
- package/dist/{idCaptureManager-BNmVJQkd.esm.js → idCaptureManager-BKedt48Y.esm.js} +276 -210
- package/dist/{idCaptureServices-dtDG2HXS.esm.js → idCaptureServices-D0p_RFqk.esm.js} +21 -40
- package/dist/{idCaptureStateMachine-BbiZD3uf.esm.js → idCaptureStateMachine-CxwoN5ff.esm.js} +992 -872
- package/dist/{idOcrManager-ByWuQ4ej.esm.js → idOcrManager-D3-HNMXP.esm.js} +3 -3
- package/dist/{idOcrManager-CFlDwUvN.d.ts → idOcrManager-DT3kPIB3.d.ts} +3 -4
- package/dist/{idOcrStateMachine-Bt_abXQQ.esm.js → idOcrStateMachine-DyENX6Ki.esm.js} +3 -4
- package/dist/{idVerificationManager-BB4Pj_N6.d.ts → idVerificationManager-BJapZfpy.d.ts} +2 -3
- package/dist/{idVerificationManager-DT42c77H.esm.js → idVerificationManager-iWz7Avkr.esm.js} +3 -3
- package/dist/{idVerificationStateMachine-DbDVIh9t.esm.js → idVerificationStateMachine-CL1kUdBe.esm.js} +3 -4
- package/dist/identity-reuse.d.ts +2 -2
- package/dist/identity-reuse.esm.js +2 -2
- package/dist/{identityReuseManager-BXEBBI3N.d.ts → identityReuseManager-B1birg5X.d.ts} +162 -44
- package/dist/{identityReuseManager-yG3acUrQ.esm.js → identityReuseManager-CD1ucp1d.esm.js} +26 -13
- package/dist/{identityReuseStateMachine-BhAkuIra.esm.js → identityReuseStateMachine-DfV81wQC.esm.js} +89 -24
- package/dist/{index-CeaC32qI.d.ts → index-CKQ9QBZP.d.ts} +2 -2
- package/dist/index-DpTj5PFb.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/{invokeOnCaptureCallback-BjzPjJ_p.esm.js → invokeOnCaptureCallback-DYrkOXO6.esm.js} +1 -1
- package/dist/lamejs-B6TzhU5q.esm.js +10 -0
- package/dist/mandatory-consent.d.ts +2 -2
- package/dist/mandatory-consent.esm.js +2 -2
- package/dist/{mandatoryConsentManager-7dI0D08q.esm.js → mandatoryConsentManager-Cwqx1nK7.esm.js} +3 -3
- package/dist/{mandatoryConsentManager-DFVm_i2B.d.ts → mandatoryConsentManager-okPILEru.d.ts} +8 -9
- package/dist/{mandatoryConsentStateMachine-DC5Mo6M1.esm.js → mandatoryConsentStateMachine-pXWqAHo_.esm.js} +3 -4
- package/dist/{multipartRecordingService-mwKFTlhR.esm.js → multipartRecordingService-CLwrF5wD.esm.js} +1 -2
- package/dist/{permissionGuards-D6TfVop2.esm.js → permissionGuards-gcy3U0SL.esm.js} +1 -1
- package/dist/{permissionServices-EY3_Vx6c.esm.js → permissionServices-Ca-tSNhT.esm.js} +34 -3
- package/dist/phone-number.d.ts +24 -0
- package/dist/phone-number.esm.js +1922 -0
- package/dist/phone.d.ts +1 -1
- package/dist/phone.esm.js +2 -2
- package/dist/{phoneManager-BBU8Lq0O.d.ts → phoneManager-BO-LTih5.d.ts} +16 -3
- package/dist/{phoneManager-IjhSv5XS.esm.js → phoneManager-KKUq-Vgo.esm.js} +31 -22
- package/dist/{phoneStateMachine-CXdXufKg.esm.js → phoneStateMachine-D2QWS2cu.esm.js} +38 -20
- package/dist/phoneValidation-DnBcNUNh.esm.js +48 -0
- package/dist/qe-signature.d.ts +2 -2
- package/dist/qe-signature.esm.js +1 -1
- package/dist/redirect-to-mobile.d.ts +1 -1
- package/dist/redirect-to-mobile.esm.js +2 -2
- package/dist/{redirectToMobileManager-BD2PvT5B.d.ts → redirectToMobileManager-CVsvpVbQ.d.ts} +2 -2
- package/dist/{redirectToMobileManager-FLEWJtv8.esm.js → redirectToMobileManager-uHW3dyuQ.esm.js} +4 -4
- package/dist/{redirectToMobileStateMachine-CSWWgUTO.esm.js → redirectToMobileStateMachine-b-H-ipGM.esm.js} +4 -5
- package/dist/{rolldown-runtime-FDOR9p9I.esm.js → rolldown-runtime-B-1-B7_t.esm.js} +10 -1
- package/dist/{runChildModule-B2Y_hlix.esm.js → runChildModule-CPa0i2_C.esm.js} +13 -12
- package/dist/safeStorage-UxDrnjwa.esm.js +35 -0
- package/dist/{sdkVersion-N1UiyFiY.esm.js → sdkVersion-BtROs6OA.esm.js} +1 -1
- package/dist/selfie.d.ts +4 -4
- package/dist/selfie.esm.js +12 -12
- package/dist/selfieErrorUtils-sMc0MaNh.esm.js +21 -0
- package/dist/{selfieManager-BSZG891S.d.ts → selfieManager-BJdNu_nd.d.ts} +2 -8
- package/dist/{selfieManager-AS6wfUvZ.esm.js → selfieManager-Ch9xJbWJ.esm.js} +6 -6
- package/dist/selfieStateMachine-DT8QCfKR.esm.js +41 -0
- package/dist/{session-BESZc05k.esm.js → session-CNNTckUS.esm.js} +5 -6
- package/dist/session.d.ts +15 -3
- package/dist/session.esm.js +30 -5
- package/dist/sessionInitializer-DVWMJh25.esm.js +973 -0
- package/dist/{setup-BuPbxpwF.esm.js → setup-B7ehQ-lf.esm.js} +155 -140
- package/dist/{setup-Cyko6cAn.d.ts → setup-YPKTUCY5.d.ts} +42 -15
- package/dist/signature.d.ts +1 -1
- package/dist/signature.esm.js +2 -2
- package/dist/{signatureManager-DGBSWz2_.d.ts → signatureManager-BpVt_Jv7.d.ts} +2 -2
- package/dist/{signatureManager-B72np4ZO.esm.js → signatureManager-f_dPAGXn.esm.js} +7 -11
- package/dist/{signatureStateMachine-ce_XMA3l.esm.js → signatureStateMachine-C-nUq_oq.esm.js} +3 -4
- package/dist/splitTrackedName-3zJRd2RD.esm.js +19 -0
- package/dist/{src-35ekGRzK.esm.js → src-DJAJjRYQ.esm.js} +1 -1
- package/dist/{stats-CZcy2J53.esm.js → stats-BEvexnKC.esm.js} +1 -2
- package/dist/stats.esm.js +1 -1
- package/dist/{sessionInitializer-CnhAkMUN.esm.js → thumbmark.esm-CRc0cYvz.esm.js} +33 -776
- package/dist/{trueSightService-M5VpCo96.esm.js → trueSightService-BzLdXpmJ.esm.js} +1391 -709
- package/dist/trust-graph.d.ts +1 -2
- package/dist/trust-graph.esm.js +2 -2
- package/dist/{types-C0oWGUzg.d.ts → types-0L-eFDmr.d.ts} +1 -1
- package/dist/{types-C1g9itHE.d.ts → types-B6HsSzpk.d.ts} +6 -7
- package/dist/{types-DEGVXwsk2.d.ts → types-BAxn7MpY.d.ts} +110 -62
- package/dist/{types-BfgmV7s7.d.ts → types-BQA7lRDV.d.ts} +1 -1
- package/dist/{types-Cx-1zo-i.d.ts → types-BvLo3U4u.d.ts} +1 -1
- package/dist/{types-Bx-BWveD.d.ts → types-CK8Jbjkp.d.ts} +1 -1
- package/dist/{types-CxGdmzxa.d.ts → types-Ckir_BEs.d.ts} +1 -1
- package/dist/{types-BjSly8cp.d.ts → types-DUnFZ08M.d.ts} +1 -1
- package/dist/{types-ESdtnHtg.d.ts → types-yPH8lAH-.d.ts} +3 -4
- package/dist/types-zOgZqMni.d.ts +6 -0
- package/dist/{urlUtils-D6phjq1-.esm.js → urlUtils-6jsE6rKp.esm.js} +9 -1
- package/dist/video-selfie.d.ts +6 -9
- package/dist/video-selfie.esm.js +1390 -1349
- package/dist/{videoSelfieCameraWarmer-q9ZjvXu9.esm.js → videoSelfieCameraWarmer-DWoFBJAn.esm.js} +10 -11
- package/dist/{videoSelfieFlowModuleMachine-BUe2uACW.esm.js → videoSelfieFlowModuleMachine-BCFVHBQ1.esm.js} +1 -1
- package/dist/{videoSelfieStepDetector-ChYgFM15.d.ts → videoSelfieStepDetector-CGsTltqB.d.ts} +4 -4
- package/dist/videoTrimByCopy-BFfAiNpv.esm.js +15901 -0
- package/dist/{warmup-ChDwHSel.esm.js → warmup-BElYWRh7.esm.js} +348 -11
- package/dist/{warmup-Cis2p6O0.d.ts → warmup-BO57syF6.d.ts} +8 -0
- package/dist/wasm.d.ts +2 -2
- package/dist/wasm.esm.js +3 -4
- package/dist/{wasmDsFlagSync-DYrLUEII.esm.js → wasmDsFlagSync-CYj5LL9z.esm.js} +1 -1
- package/dist/{wasmWebClient-B7baE-Wf.esm.js → wasmWebClient-BjZw_7OB.esm.js} +570 -33
- package/dist/watchlist-for-business.d.ts +1 -1
- package/dist/watchlist-for-business.esm.js +2 -2
- package/dist/watchlist.d.ts +1 -1
- package/dist/watchlist.esm.js +2 -2
- package/dist/{watchlistForBusinessManager-CsnJnG-C.d.ts → watchlistForBusinessManager-B3GDPZrr.d.ts} +2 -2
- package/dist/{watchlistForBusinessManager-CIMkpEhu.esm.js → watchlistForBusinessManager-CZZOFkGl.esm.js} +7 -5
- package/dist/{watchlistForBusinessStateMachine-CJ-MvUre.esm.js → watchlistForBusinessStateMachine-B66E7jud.esm.js} +3 -4
- package/dist/{watchlistManager-DLXef90v.d.ts → watchlistManager-DJQOd8Pw.d.ts} +2 -2
- package/dist/{watchlistManager-CixcLNDT.esm.js → watchlistManager-DXtX94SN.esm.js} +3 -3
- package/dist/{watchlistStateMachine-CBqk2w-h.esm.js → watchlistStateMachine-McALifag.esm.js} +3 -4
- package/dist/workflow.d.ts +34 -5
- package/dist/workflow.esm.js +64 -47
- package/dist/{xstate-actors.esm-BXeQ-Ucz.esm.js → xstate-actors.esm-8Gzepa_A.esm.js} +1 -1
- package/dist/{xstate.esm-BuFLFuzz.esm.js → xstate.esm-DSajW7CJ.esm.js} +24 -10
- package/package.json +13 -5
- package/dist/Actor-iTq7YY48.d.ts +0 -1
- package/dist/BrowserDiagnosticsCryptoProvider-BuzqvbhN.esm.js +0 -77
- package/dist/StateMachine-EDpYWy0i.d.ts +0 -6
- package/dist/api-Bx3gKW_y.esm.js +0 -129
- package/dist/fiscalQrManager-DbCVMIBx.esm.js +0 -34143
- package/dist/index-B8weJxmL.d.ts +0 -1
- package/dist/selfieErrorUtils-UiQ46EBa.esm.js +0 -116
- package/dist/selfieStateMachine-DY7vJVNY.esm.js +0 -71
- package/dist/types-BN5RPPIZ.esm.js +0 -34
- package/dist/types-Bt9RuRqt.d.ts +0 -6
- package/dist/virtualCameraCheck-Cjab7FTI.esm.js +0 -48
- /package/dist/{types-BSBzhHRH.d.ts → types-BSBzhHRH2.d.ts} +0 -0
|
@@ -1,177 +1,719 @@
|
|
|
1
|
-
import { a as setWebApiHttpTransport,
|
|
1
|
+
import { a as setWebApiHttpTransport, g as emitWasmDiagnostic, i as initializeWasmWebClient, o as setupWasmConnection, r as executeWasmRequest, t as WasmWebClientError, w as resolveWasmConfig } from "./wasmWebClient-BjZw_7OB.esm.js";
|
|
2
2
|
import { t as getTimestamp } from "./IBrowserEnvironmentCapability-DEaRRk-R.esm.js";
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import { t as SDK_VERSION } from "./sdkVersion-
|
|
6
|
-
import { t as
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
3
|
+
import { C as subscribeEvent, n as eventModuleNames } from "./events-Du2A4-Qr.esm.js";
|
|
4
|
+
import { i as isApiConfigured, l as withTiming, n as getApi, o as setClient, r as getToken, t as api, u as endpoints } from "./api-Cz90EHyu.esm.js";
|
|
5
|
+
import { t as SDK_VERSION } from "./sdkVersion-BtROs6OA.esm.js";
|
|
6
|
+
import { n as reportHttpError, t as HttpRequestError } from "./HttpRequestError-Bh0pNGSC.esm.js";
|
|
7
|
+
/** Dirty-timer half of the interrupted-snapshot rebuild debounce (module boundaries + timer). */
|
|
8
|
+
const SNAPSHOT_DIRTY_INTERVAL_MS = 15e3;
|
|
9
|
+
/** Hard cap per log message, UTF-8 bytes (contract). */
|
|
10
|
+
const MESSAGE_MAX_BYTES = 8 * 1024;
|
|
11
|
+
const TRUESIGHT_SDK_ID = "IncdOnboarding";
|
|
12
|
+
/** localStorage key prefix for persisted snapshot envelopes (A3: one slot per session). */
|
|
13
|
+
const PENDING_STORAGE_KEY_PREFIX = "incode.truesight.pending.";
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/internal/trueSight/sanitize.ts
|
|
16
|
+
/**
|
|
17
|
+
* Deny-by-default message scrubber — runs at INGESTION (inside the log buffer's
|
|
18
|
+
* `append`), so the buffer, every persisted snapshot, and every upload are
|
|
19
|
+
* already clean; there is no code path around it (ENG-48677 plan §1.2).
|
|
20
|
+
*
|
|
21
|
+
* Ordered pipeline. Two deliberate deviations from the plan's original rule
|
|
22
|
+
* order, both from the P1-A adversarial review (2026-07-23, recorded in the
|
|
23
|
+
* plan's Operating-decisions section):
|
|
24
|
+
* - Inputs are pre-clamped to 2× the message cap BEFORE any regex runs — the
|
|
25
|
+
* unanchored patterns are quadratic on pathological non-matching runs
|
|
26
|
+
* (measured ~4.5 s on a 100 KB run), so regex cost must be bounded first.
|
|
27
|
+
* The final 8 KB cap still runs last.
|
|
28
|
+
* - The base64-blob rule runs BEFORE the digit-run rule — a digit run inside a
|
|
29
|
+
* blob would otherwise split it into sub-64-char fragments that escape
|
|
30
|
+
* redaction.
|
|
31
|
+
*
|
|
32
|
+
* ENG-58122 additions, from the pre-restore review:
|
|
33
|
+
* - JSON bodies run FIRST, on the raw message BEFORE the pre-clamp: error
|
|
34
|
+
* text can embed a whole response body (the WASM HTTP client's error
|
|
35
|
+
* text, kit messages), no field-level rule can vouch for an arbitrary
|
|
36
|
+
* body, and a clamp that cut a body's closing bracket would otherwise let
|
|
37
|
+
* its head through. Only the JSON span itself is replaced — the text
|
|
38
|
+
* around it, and the body's size, are kept (`[json:<chars>]`).
|
|
39
|
+
* - Relative Incode paths: per-request ids in the path
|
|
40
|
+
* (`/digital-id/verifications/<id>/…`) and query strings on relative URLs
|
|
41
|
+
* (`/omni/get/ekyc?id=…`); the absolute-URL rules never matched these.
|
|
42
|
+
*
|
|
43
|
+
* The regex table ships in the PR description for privacy sign-off (see
|
|
44
|
+
* .agents/guides/privacy.md). Known hardening backlog (D4, Phase 4): formatted
|
|
45
|
+
* phone numbers, unicode digits/emails, fuzz/property tests.
|
|
46
|
+
*/
|
|
47
|
+
/** Three dot-joined base64url segments — JWT/JWS shape. */
|
|
48
|
+
const JWT_PATTERN = /\b[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g;
|
|
49
|
+
/**
|
|
50
|
+
* Credential-ish key with a `:`/`=` value (quoted, `Bearer …`, or bare token).
|
|
51
|
+
* The key may carry prefixes (`access_token`, `x-api-key`, `sessionToken`) —
|
|
52
|
+
* a plain \b boundary would miss `_token` since `_` is a word character —
|
|
53
|
+
* and a JSON closing quote (`"token":"…"` in a fragment the JSON rule could
|
|
54
|
+
* not see as a body, e.g. one sliced mid-object by its source).
|
|
55
|
+
*/
|
|
56
|
+
const CREDENTIAL_KV_PATTERN = /([\w-]*(?:token|api[-_]?key|authorization|auth|secret|password|passwd))"?\s*[:=]\s*("[^"]*"|'[^']*'|(?:Bearer\s+)?[^\s,;&]+)/gi;
|
|
57
|
+
/** Userinfo credentials in an absolute URL (`https://user:pass@host/…`). */
|
|
58
|
+
const URL_USERINFO_PATTERN = /(\bhttps?:\/\/)[^\s/@]*(?::[^\s/@]*)?@/gi;
|
|
59
|
+
/** Query string / fragment of an absolute URL — kills signed URLs. */
|
|
60
|
+
const URL_TAIL_PATTERN = /(https?:\/\/[^\s?#]+)[?#][^\s]*/g;
|
|
61
|
+
/** Per-request id segment of a verifications path (`…/verifications/<id>/status`). */
|
|
62
|
+
const VERIFICATION_ID_SEGMENT_PATTERN = /(\/verifications\/)[^\s/?#]+/g;
|
|
63
|
+
/**
|
|
64
|
+
* Query string / fragment of a RELATIVE path (`/omni/get/ekyc?id=…`). Runs
|
|
65
|
+
* after the absolute-URL rule, so only relative paths still carry one.
|
|
66
|
+
*/
|
|
67
|
+
const RELATIVE_PATH_TAIL_PATTERN = /((?:^|[\s"'(=])\/[^\s?#"'(=]*)[?#][^\s"')]*/g;
|
|
68
|
+
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
69
|
+
/** base64-ish blobs (media, keys) — 64+ chars of the base64/base64url alphabet. */
|
|
70
|
+
const BASE64_BLOB_PATTERN = /[-A-Za-z0-9+/_]{64,}={0,2}/g;
|
|
71
|
+
/** Runs of 8+ digits — phones, CURP, document numbers. */
|
|
72
|
+
const DIGIT_RUN_PATTERN = /\d{8,}/g;
|
|
73
|
+
const TRUNCATION_SUFFIX = "…[truncated]";
|
|
74
|
+
/** Regex-cost bound: clamp raw input before any pattern runs (see header note). */
|
|
75
|
+
const REGEX_INPUT_MAX_BYTES = MESSAGE_MAX_BYTES * 2;
|
|
76
|
+
function sanitizeMessage(message) {
|
|
77
|
+
return truncateUtf8(truncateUtf8(redactJsonBodies(message), REGEX_INPUT_MAX_BYTES).replace(JWT_PATTERN, "[jwt]").replace(CREDENTIAL_KV_PATTERN, "$1=[redacted]").replace(URL_USERINFO_PATTERN, "$1[redacted]@").replace(URL_TAIL_PATTERN, "$1").replace(RELATIVE_PATH_TAIL_PATTERN, "$1").replace(VERIFICATION_ID_SEGMENT_PATTERN, "$1:id").replace(EMAIL_PATTERN, "[email]").replace(BASE64_BLOB_PATTERN, (match) => `[blob:${match.length}]`).replace(DIGIT_RUN_PATTERN, "[digits]"), MESSAGE_MAX_BYTES);
|
|
39
78
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Replaces every serialized JSON object or array with `[json:<chars>]`. A
|
|
81
|
+
* span opens where JSON does and diagnostic text does not — `{"`, `["` or
|
|
82
|
+
* `[{"` (whitespace allowed), never `pipeline={face}` or
|
|
83
|
+
* `pipelines=[selfie,idCapture]` — and closes at its matching bracket,
|
|
84
|
+
* skipping brackets inside strings. Text outside the spans is kept as is.
|
|
85
|
+
* An unclosed span is a body its source cut short, so it runs to the end.
|
|
86
|
+
* A scan, not a regex: one linear pass, safe on an unbounded message.
|
|
87
|
+
*/
|
|
88
|
+
function redactJsonBodies(text) {
|
|
89
|
+
let redacted = "";
|
|
90
|
+
let keptFrom = 0;
|
|
91
|
+
let index = 0;
|
|
92
|
+
while (index < text.length) if (opensJsonSpan(text, index)) {
|
|
93
|
+
const end = jsonSpanEnd(text, index);
|
|
94
|
+
redacted += `${text.slice(keptFrom, index)}[json:${end - index}]`;
|
|
95
|
+
keptFrom = end;
|
|
96
|
+
index = end;
|
|
97
|
+
} else index += 1;
|
|
98
|
+
return keptFrom === 0 ? text : redacted + text.slice(keptFrom);
|
|
99
|
+
}
|
|
100
|
+
function opensJsonSpan(text, index) {
|
|
101
|
+
const char = text[index];
|
|
102
|
+
if (char !== "{" && char !== "[") return false;
|
|
103
|
+
let next = skipWhitespace(text, index + 1);
|
|
104
|
+
if (char === "[" && text[next] === "{") next = skipWhitespace(text, next + 1);
|
|
105
|
+
return text[next] === "\"";
|
|
106
|
+
}
|
|
107
|
+
function skipWhitespace(text, index) {
|
|
108
|
+
let next = index;
|
|
109
|
+
while (next < text.length && /\s/.test(text[next])) next += 1;
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
/** Exclusive end of the span opening at `start`; the text's end when unclosed. */
|
|
113
|
+
function jsonSpanEnd(text, start) {
|
|
114
|
+
let depth = 0;
|
|
115
|
+
let inString = false;
|
|
116
|
+
for (let index = start; index < text.length; index += 1) {
|
|
117
|
+
const char = text[index];
|
|
118
|
+
if (inString) {
|
|
119
|
+
if (char === "\\") index += 1;
|
|
120
|
+
else if (char === "\"") inString = false;
|
|
121
|
+
} else if (char === "\"") inString = true;
|
|
122
|
+
else if (char === "{" || char === "[") depth += 1;
|
|
123
|
+
else if (char === "}" || char === "]") {
|
|
124
|
+
depth -= 1;
|
|
125
|
+
if (depth === 0) return index + 1;
|
|
126
|
+
}
|
|
44
127
|
}
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
function warnMalformedWasmPath(field) {
|
|
48
|
-
console.warn(`[Incode SDK] wasm.${field} appears malformed. Check the URL or path.`);
|
|
128
|
+
return text.length;
|
|
49
129
|
}
|
|
50
130
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
131
|
+
* UTF-8 byte length without TextEncoder (banned browser global in L1 —
|
|
132
|
+
* see the no-browser-api-in-core lint rule). Counts by code point.
|
|
53
133
|
*/
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
134
|
+
function utf8ByteLength(value) {
|
|
135
|
+
let bytes = 0;
|
|
136
|
+
for (const char of value) bytes += codePointByteLength(char);
|
|
137
|
+
return bytes;
|
|
138
|
+
}
|
|
139
|
+
function codePointByteLength(char) {
|
|
140
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
141
|
+
if (codePoint < 128) return 1;
|
|
142
|
+
if (codePoint < 2048) return 2;
|
|
143
|
+
if (codePoint < 65536) return 3;
|
|
144
|
+
return 4;
|
|
145
|
+
}
|
|
146
|
+
/** Cuts on a code-point boundary so no surrogate half ever reaches the wire. */
|
|
147
|
+
function truncateUtf8(value, maxBytes) {
|
|
148
|
+
if (utf8ByteLength(value) <= maxBytes) return value;
|
|
149
|
+
const budget = maxBytes - utf8ByteLength(TRUNCATION_SUFFIX);
|
|
150
|
+
let bytes = 0;
|
|
151
|
+
let result = "";
|
|
152
|
+
for (const char of value) {
|
|
153
|
+
const charBytes = codePointByteLength(char);
|
|
154
|
+
if (bytes + charBytes > budget) break;
|
|
155
|
+
bytes += charBytes;
|
|
156
|
+
result += char;
|
|
58
157
|
}
|
|
158
|
+
return result + TRUNCATION_SUFFIX;
|
|
59
159
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
160
|
+
const EVICTION_ORDER = [
|
|
161
|
+
"verbose",
|
|
162
|
+
"debug",
|
|
163
|
+
"info"
|
|
164
|
+
];
|
|
165
|
+
/** Stored in place of a message that is blank after sanitizing. */
|
|
166
|
+
const EMPTY_MESSAGE = "[empty]";
|
|
167
|
+
function createLogBuffer(options = {}) {
|
|
168
|
+
const flushThresholdBytes = options.flushThresholdBytes ?? 6e5;
|
|
169
|
+
const maxEntries = options.maxEntries ?? 5e3;
|
|
170
|
+
const { onOverflow } = options;
|
|
171
|
+
const entries = [];
|
|
172
|
+
let mode = onOverflow ? "flush" : "evict";
|
|
173
|
+
let nextIndex = 0;
|
|
174
|
+
let bytes = 0;
|
|
175
|
+
let dropped = 0;
|
|
176
|
+
let overflowPending = false;
|
|
177
|
+
function entryCost(entry) {
|
|
178
|
+
return utf8ByteLength(entry.message) + utf8ByteLength(entry.subsystem) + 120;
|
|
179
|
+
}
|
|
180
|
+
function removeAt(position) {
|
|
181
|
+
const [removed] = entries.splice(position, 1);
|
|
182
|
+
bytes -= entryCost(removed);
|
|
183
|
+
dropped += 1;
|
|
184
|
+
}
|
|
185
|
+
/** Lowest level first (oldest within it); warning+ only as the last resort. */
|
|
186
|
+
function evictOne() {
|
|
187
|
+
for (const level of EVICTION_ORDER) {
|
|
188
|
+
const position = entries.findIndex((entry) => entry.level === level);
|
|
189
|
+
if (position !== -1) {
|
|
190
|
+
removeAt(position);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
removeAt(0);
|
|
68
195
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return defaultValue;
|
|
196
|
+
function copyOf(entry) {
|
|
197
|
+
return { ...entry };
|
|
72
198
|
}
|
|
73
|
-
return raw.trim();
|
|
74
|
-
}
|
|
75
|
-
function pickSource(key, overrides, defaultValue) {
|
|
76
|
-
if (!(key in overrides) || overrides[key] === void 0) return defaultValue;
|
|
77
|
-
const source = overrides[key];
|
|
78
|
-
if (typeof source !== "string") return source;
|
|
79
|
-
return pickPath(key, overrides, String(defaultValue));
|
|
80
|
-
}
|
|
81
|
-
function resolveBasePath(overrides) {
|
|
82
|
-
const defaultValue = `${WASM_CDN_BASE_URL}/${DEFAULT_WASM_VERSION}`;
|
|
83
|
-
if (!("basePath" in overrides) || overrides.basePath === void 0) return defaultValue;
|
|
84
|
-
const basePath = overrides.basePath;
|
|
85
|
-
if (typeof basePath !== "string" || isBlank(basePath) || isMalformedPath(basePath)) throw new Error("[Incode SDK] wasm.basePath is invalid.");
|
|
86
|
-
return basePath.trim();
|
|
87
|
-
}
|
|
88
|
-
/** Builds a WarmupConfig from a directory that mirrors the ml-wasm-kit CDN layout. */
|
|
89
|
-
function buildWasmConfigFromBasePath(basePath) {
|
|
90
|
-
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
91
199
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
200
|
+
append(level, subsystem, category, message, timestampMs) {
|
|
201
|
+
const sanitized = sanitizeMessage(message);
|
|
202
|
+
const entry = {
|
|
203
|
+
index: nextIndex,
|
|
204
|
+
timestampMs: timestampMs !== void 0 && Number.isFinite(timestampMs) ? timestampMs : getTimestamp(),
|
|
205
|
+
level,
|
|
206
|
+
subsystem,
|
|
207
|
+
category,
|
|
208
|
+
message: sanitized.trim() === "" ? EMPTY_MESSAGE : sanitized
|
|
209
|
+
};
|
|
210
|
+
nextIndex += 1;
|
|
211
|
+
entries.push(entry);
|
|
212
|
+
bytes += entryCost(entry);
|
|
213
|
+
if (mode === "flush" && onOverflow) {
|
|
214
|
+
if (!overflowPending && bytes >= flushThresholdBytes) {
|
|
215
|
+
overflowPending = true;
|
|
216
|
+
onOverflow(entries.map(copyOf));
|
|
217
|
+
}
|
|
218
|
+
} else while (bytes > flushThresholdBytes && entries.length > 1) evictOne();
|
|
219
|
+
while (entries.length > maxEntries) evictOne();
|
|
220
|
+
},
|
|
221
|
+
snapshot() {
|
|
222
|
+
return entries.map(copyOf);
|
|
223
|
+
},
|
|
224
|
+
sliceSince(index) {
|
|
225
|
+
return entries.filter((entry) => entry.index > index).map(copyOf);
|
|
226
|
+
},
|
|
227
|
+
markFlushed(uptoIndex) {
|
|
228
|
+
let removedBytes = 0;
|
|
229
|
+
let keepFrom = 0;
|
|
230
|
+
while (keepFrom < entries.length && entries[keepFrom].index <= uptoIndex) {
|
|
231
|
+
removedBytes += entryCost(entries[keepFrom]);
|
|
232
|
+
keepFrom += 1;
|
|
233
|
+
}
|
|
234
|
+
entries.splice(0, keepFrom);
|
|
235
|
+
bytes -= removedBytes;
|
|
236
|
+
overflowPending = false;
|
|
237
|
+
},
|
|
238
|
+
reserveIndex() {
|
|
239
|
+
const reserved = nextIndex;
|
|
240
|
+
nextIndex += 1;
|
|
241
|
+
return reserved;
|
|
242
|
+
},
|
|
243
|
+
setOverflowMode(next) {
|
|
244
|
+
mode = next;
|
|
245
|
+
},
|
|
246
|
+
get droppedCount() {
|
|
247
|
+
return dropped;
|
|
248
|
+
},
|
|
249
|
+
get byteEstimate() {
|
|
250
|
+
return bytes;
|
|
251
|
+
}
|
|
97
252
|
};
|
|
98
253
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region ../infra/src/crypto/randomUUID.ts
|
|
256
|
+
/**
|
|
257
|
+
* Cryptographically-strong UUID v4 (RFC 4122).
|
|
258
|
+
*
|
|
259
|
+
* Lives in infra (L0) so Core (L1) can mint secure identifiers without touching
|
|
260
|
+
* the `crypto` browser API directly — Core delegates here, keeping it free of
|
|
261
|
+
* browser globals (enforced by the `no-browser-api-in-core` lint rule). Backed
|
|
262
|
+
* by `crypto.randomUUID`, which browsers expose only in secure contexts — check
|
|
263
|
+
* `isRandomUUIDAvailable()` where a plain-`http` page must not throw.
|
|
264
|
+
*/
|
|
265
|
+
function randomUUID() {
|
|
266
|
+
return crypto.randomUUID();
|
|
267
|
+
}
|
|
268
|
+
/** `false` on insecure (plain-`http`) pages, where `randomUUID()` throws. */
|
|
269
|
+
function isRandomUUIDAvailable() {
|
|
270
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function";
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region ../infra/src/time/epochMsToIsoString.ts
|
|
274
|
+
/**
|
|
275
|
+
* Converts an epoch-milliseconds timestamp to a full ISO-8601 UTC string
|
|
276
|
+
* (e.g. `2026-07-23T09:41:00.000Z`).
|
|
277
|
+
*
|
|
278
|
+
* Lives in Infra (L0) so Core can format wire timestamps without touching the
|
|
279
|
+
* `Date` global directly — banned in Core by the `no-browser-api-in-core` lint
|
|
280
|
+
* rule — mirroring the `crypto/randomUUID.ts` free-function precedent.
|
|
281
|
+
* (`DateFormattingProvider.utcToIsoDateString` is date-only and not suitable.)
|
|
282
|
+
*/
|
|
283
|
+
function epochMsToIsoString(epochMs) {
|
|
284
|
+
return new Date(epochMs).toISOString();
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/internal/trueSight/recordBuilder.ts
|
|
288
|
+
/**
|
|
289
|
+
* Assembles the exact plaintext contract record — the ONLY place a diagnostics
|
|
290
|
+
* payload is shaped. Pure: same function serves final records, interrupted
|
|
291
|
+
* snapshots, and future incremental slices (ENG-48677 plan §1.1).
|
|
292
|
+
*
|
|
293
|
+
* Deny-by-default is structural here: every field is an explicit literal —
|
|
294
|
+
* nothing is spread from internal state, so no unexpected field can ever leak
|
|
295
|
+
* into an upload.
|
|
296
|
+
*
|
|
297
|
+
* Hard gates enforced:
|
|
298
|
+
* - empty/missing `sessionId` or `startedAt` → returns `null` (upload skipped —
|
|
299
|
+
* expected for sessions that die before `/omni/start`).
|
|
300
|
+
* - `failureReason` present if-and-only-if `outcome === 'failure'` (defaults to
|
|
301
|
+
* `'unknown'` when a failure arrives without one, or with a blank one, rather
|
|
302
|
+
* than dropping the record — TrueSight answers 400 to a blank reason).
|
|
303
|
+
* - fresh record `id` per build; `endedAt` stamped at build time.
|
|
304
|
+
*/
|
|
305
|
+
function buildDiagnosticsRecord(meta, logs, outcome, failureReason, recordId) {
|
|
306
|
+
if (!meta.sessionId || meta.startedAtMs === null) return null;
|
|
307
|
+
const flowType = meta.flowType ?? deriveFlowTypeFromSurface(meta.integrationType);
|
|
308
|
+
if (flowType === null) return null;
|
|
309
|
+
return {
|
|
310
|
+
schemaVersion: "1.0",
|
|
311
|
+
id: recordId ?? randomUUID(),
|
|
312
|
+
session: {
|
|
313
|
+
sessionId: meta.sessionId,
|
|
314
|
+
sdkId: TRUESIGHT_SDK_ID,
|
|
315
|
+
sdkVersion: SDK_VERSION,
|
|
316
|
+
platform: "web",
|
|
317
|
+
sdkMode: null,
|
|
318
|
+
browser: meta.browser,
|
|
319
|
+
osVersion: meta.osVersion,
|
|
320
|
+
osLanguage: meta.osLanguage,
|
|
321
|
+
sdkLanguage: meta.sdkLanguage,
|
|
322
|
+
integrationType: meta.integrationType,
|
|
323
|
+
flowType,
|
|
324
|
+
flowId: meta.flowId,
|
|
325
|
+
workflowId: meta.workflowId,
|
|
326
|
+
outcome,
|
|
327
|
+
...outcome === "failure" ? { failureReason: failureReason?.trim() || "unknown" } : {},
|
|
328
|
+
startedAt: epochMsToIsoString(meta.startedAtMs),
|
|
329
|
+
endedAt: epochMsToIsoString(getTimestamp()),
|
|
330
|
+
testing: meta.testing,
|
|
331
|
+
e2eEncryptionEnabled: meta.e2eEncryptionEnabled,
|
|
332
|
+
configurationId: meta.hasConfigurationId,
|
|
333
|
+
externalCustomerId: meta.hasExternalCustomerId,
|
|
334
|
+
hasProvidedSessionId: meta.hasProvidedSessionId,
|
|
335
|
+
token: meta.hasToken,
|
|
336
|
+
externalToken: meta.hasExternalToken,
|
|
337
|
+
...meta.wasmProfile ? { webProfile: toWebProfile(meta.wasmProfile) } : {}
|
|
338
|
+
},
|
|
339
|
+
logs: logs.map(toContractEntry)
|
|
340
|
+
};
|
|
102
341
|
}
|
|
103
|
-
const BARCODE_ENHANCE_MODEL_FILE = WASM_MODEL_FILES.barcode;
|
|
104
342
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* deployments resolve it correctly, falling back to the CDN default. Loaded on
|
|
108
|
-
* demand via `IIdCaptureCapability.setBarcodeEnhancementModel` — deliberately
|
|
109
|
-
* NOT part of any warmup model bundle.
|
|
343
|
+
* `session.webProfile`, field by field: TrueSight decodes it strictly
|
|
344
|
+
* (unknown keys reject the record), so nothing is spread from the source.
|
|
110
345
|
*/
|
|
111
|
-
function
|
|
112
|
-
|
|
113
|
-
|
|
346
|
+
function toWebProfile(wasm) {
|
|
347
|
+
return { wasm: {
|
|
348
|
+
version: wasm.version,
|
|
349
|
+
basePathPinned: wasm.basePathPinned,
|
|
350
|
+
simd: wasm.simd,
|
|
351
|
+
...wasm.diagnosticsMode !== void 0 ? { diagnosticsMode: wasm.diagnosticsMode } : {},
|
|
352
|
+
warmupDurationMs: wasm.warmupDurationMs
|
|
353
|
+
} };
|
|
114
354
|
}
|
|
115
355
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
356
|
+
* Wire `flowType` when no orchestrator was OBSERVED (an observed one — set by
|
|
357
|
+
* the bridge on any flow event — always wins). Stage ingest requires a
|
|
358
|
+
* non-null enum (probe 2026-07-23: null/omitted/off-enum all 400), so the
|
|
359
|
+
* entry surface decides, and the mapping is deliberately exhaustive:
|
|
360
|
+
*
|
|
361
|
+
* - `startWorkflow` → 'workflow' (Webflows engine entered, died pre-ready)
|
|
362
|
+
* - `startFlow` → 'flow' (flow/orchestrated manager, died pre-ready)
|
|
363
|
+
* - `startOnboarding`→ 'flow' (self-loading UI wraps the flow engine)
|
|
364
|
+
* - `startOnboardingSection` → null → the BUILD ABORTS (record withheld):
|
|
365
|
+
* a standalone session has no truthful flowType, and mislabeling it as a
|
|
366
|
+
* flow session is worse than skipping the upload. Withheld until the
|
|
367
|
+
* DECIDED BE ask ships (`flowType:"standalone"`, not nullable) — then this
|
|
368
|
+
* branch returns 'standalone' and standalone diagnostics start flowing,
|
|
369
|
+
* coordinated with the BE deploy since off-enum values 400 today.
|
|
118
370
|
*/
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
modelUrls: overrides.modelUrls,
|
|
137
|
-
legacyModelUrls: overrides.legacyModelUrls,
|
|
138
|
-
loadGlue: overrides.loadGlue,
|
|
139
|
-
loadGlueSimd: overrides.loadGlueSimd,
|
|
140
|
-
selfHosted: overrides.selfHosted
|
|
371
|
+
function deriveFlowTypeFromSurface(integrationType) {
|
|
372
|
+
switch (integrationType) {
|
|
373
|
+
case "startWorkflow": return "workflow";
|
|
374
|
+
case "startFlow":
|
|
375
|
+
case "startOnboarding": return "flow";
|
|
376
|
+
case "startOnboardingSection": return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function toContractEntry(entry) {
|
|
380
|
+
return {
|
|
381
|
+
index: entry.index,
|
|
382
|
+
timestamp: epochMsToIsoString(entry.timestampMs),
|
|
383
|
+
level: entry.level,
|
|
384
|
+
thread: "main",
|
|
385
|
+
message: entry.message,
|
|
386
|
+
subsystem: entry.subsystem,
|
|
387
|
+
category: entry.category
|
|
141
388
|
};
|
|
142
|
-
validateWasmPaths(merged);
|
|
143
|
-
return merged;
|
|
144
389
|
}
|
|
145
390
|
//#endregion
|
|
146
|
-
//#region
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
391
|
+
//#region src/internal/trueSight/sessionMetadata.ts
|
|
392
|
+
/**
|
|
393
|
+
* Hosts that match this pattern are non-prod by heuristic. Known failure modes
|
|
394
|
+
* (customer CNAMEs, devMode left on in prod) are documented for the planned
|
|
395
|
+
* P1 team review — see plan §Operating decisions (B6).
|
|
396
|
+
*/
|
|
397
|
+
const NON_PROD_HOST_PATTERN = /stage|demo|saas-dev|localhost/i;
|
|
398
|
+
function createInitialState() {
|
|
399
|
+
return {
|
|
400
|
+
sessionId: null,
|
|
401
|
+
startedAtMs: null,
|
|
402
|
+
browser: null,
|
|
403
|
+
osVersion: null,
|
|
404
|
+
osLanguage: null,
|
|
405
|
+
sdkLanguage: "en-US",
|
|
406
|
+
integrationType: "startOnboardingSection",
|
|
407
|
+
flowType: null,
|
|
408
|
+
flowId: null,
|
|
409
|
+
workflowId: null,
|
|
410
|
+
testing: false,
|
|
411
|
+
devModeSignal: void 0,
|
|
412
|
+
apiUrlSignal: void 0,
|
|
413
|
+
hasConfigurationId: false,
|
|
414
|
+
hasExternalCustomerId: false,
|
|
415
|
+
hasProvidedSessionId: false,
|
|
416
|
+
hasExternalToken: false
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
let state = createInitialState();
|
|
420
|
+
let sources = {};
|
|
421
|
+
function stampStartedAtOnce() {
|
|
422
|
+
if (state.startedAtMs === null) state.startedAtMs = getTimestamp();
|
|
423
|
+
}
|
|
424
|
+
/** Tap for `createSession` — sessionId comes from the returned `interviewId`. */
|
|
425
|
+
function noteSessionCreated(session, options = {}) {
|
|
426
|
+
if (session.interviewId) state.sessionId = session.interviewId;
|
|
427
|
+
stampStartedAtOnce();
|
|
428
|
+
state.sdkLanguage = options.language ?? state.sdkLanguage;
|
|
429
|
+
state.hasConfigurationId = state.hasConfigurationId || options.configurationId !== void 0;
|
|
430
|
+
state.hasExternalCustomerId = state.hasExternalCustomerId || options.externalCustomerId !== void 0;
|
|
431
|
+
state.hasProvidedSessionId = state.hasProvidedSessionId || options.interviewId !== void 0;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Fallback sessionId for host-minted-token integrations that never called
|
|
435
|
+
* `createSession`: `Features.sessionIdentifier` — used only when no
|
|
436
|
+
* `interviewId` was observed.
|
|
437
|
+
*/
|
|
438
|
+
function noteFeatureConfig(features) {
|
|
439
|
+
if (!state.sessionId && features.sessionIdentifier) {
|
|
440
|
+
state.sessionId = features.sessionIdentifier;
|
|
441
|
+
stampStartedAtOnce();
|
|
158
442
|
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Entry-surface discriminator. `startOnboardingSection` is the default (weakest
|
|
446
|
+
* signal — standalone module managers); richer surfaces upgrade it and are never
|
|
447
|
+
* downgraded back.
|
|
448
|
+
*/
|
|
449
|
+
function noteIntegrationSurface(integrationType) {
|
|
450
|
+
if (integrationType === "startOnboardingSection") return;
|
|
451
|
+
state.integrationType = integrationType;
|
|
452
|
+
}
|
|
453
|
+
function noteEnvironment(environment) {
|
|
454
|
+
state.browser = environment.browser ?? state.browser;
|
|
455
|
+
state.osVersion = environment.osVersion ?? state.osVersion;
|
|
456
|
+
state.osLanguage = environment.osLanguage ?? state.osLanguage;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* WRITE-ONCE orchestrator identity (determinism guarantee for debugging):
|
|
460
|
+
* a session is a flow or a workflow from its first orchestrator observation
|
|
461
|
+
* and can never flip mid-session — a record labeled `flow` must always point
|
|
462
|
+
* at a real flow. First non-null value wins for each of flowType/flowId/
|
|
463
|
+
* workflowId; later calls are ignored (the service logs a self-health line
|
|
464
|
+
* when a CONFLICTING orchestrator kind shows up).
|
|
465
|
+
*/
|
|
466
|
+
function noteFlowContext(context) {
|
|
467
|
+
state.flowType = state.flowType ?? context.flowType;
|
|
468
|
+
state.flowId = state.flowId ?? context.flowId ?? null;
|
|
469
|
+
state.workflowId = state.workflowId ?? context.workflowId ?? null;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* `testing` heuristic: devMode OR non-prod API host pattern. Always a boolean —
|
|
473
|
+
* the contract requires the field present on every record. Signals MERGE across
|
|
474
|
+
* calls (a later apiUrl-only call must not erase an earlier devMode signal);
|
|
475
|
+
* `testing` is recomputed from the merged pair.
|
|
476
|
+
*/
|
|
477
|
+
function noteTestingSignals(signals) {
|
|
478
|
+
state.devModeSignal = signals.devMode ?? state.devModeSignal;
|
|
479
|
+
state.apiUrlSignal = signals.apiUrl ?? state.apiUrlSignal;
|
|
480
|
+
state.testing = state.devModeSignal === true || state.apiUrlSignal !== void 0 && NON_PROD_HOST_PATTERN.test(state.apiUrlSignal);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Live read of the merged testing signal (devMode OR non-prod host). Used to
|
|
484
|
+
* gate host-console self-health noise so production host pages stay quiet while
|
|
485
|
+
* staging/dev keeps the console signal (the record always captures it either
|
|
486
|
+
* way via `trueSightLog`). Same signal the overflow-flush staging gate uses.
|
|
487
|
+
*/
|
|
488
|
+
function isTrueSightTesting() {
|
|
489
|
+
return state.testing;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Tap for `initializeSession({ token })` with a host-minted token — records
|
|
493
|
+
* only the presence fact ("this token came from outside the SDK"), never the
|
|
494
|
+
* token itself.
|
|
495
|
+
*/
|
|
496
|
+
function noteExternalTokenProvided() {
|
|
497
|
+
state.hasExternalToken = true;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Wiring tap for every `initializeSession({ token })`: a token activated
|
|
501
|
+
* before any `createSession` was observed on this page is host-minted, i.e.
|
|
502
|
+
* external. (The exact `token`/`externalToken` web-semantics are a pending
|
|
503
|
+
* Phase 0 contract ask — this inference is the interim mapping.)
|
|
504
|
+
*/
|
|
505
|
+
function noteTokenActivated() {
|
|
506
|
+
if (state.sessionId === null) noteExternalTokenProvided();
|
|
507
|
+
}
|
|
508
|
+
function configureTrueSightMetadataSources(next) {
|
|
509
|
+
sources = next;
|
|
510
|
+
}
|
|
511
|
+
/** Explicit literal — the metadata half handed to the record builder. */
|
|
512
|
+
function buildSessionMetadata() {
|
|
513
|
+
return {
|
|
514
|
+
sessionId: state.sessionId,
|
|
515
|
+
startedAtMs: state.startedAtMs,
|
|
516
|
+
browser: state.browser,
|
|
517
|
+
osVersion: state.osVersion,
|
|
518
|
+
osLanguage: state.osLanguage,
|
|
519
|
+
sdkLanguage: state.sdkLanguage,
|
|
520
|
+
integrationType: state.integrationType,
|
|
521
|
+
flowType: state.flowType,
|
|
522
|
+
flowId: state.flowId,
|
|
523
|
+
workflowId: state.workflowId,
|
|
524
|
+
testing: state.testing,
|
|
525
|
+
e2eEncryptionEnabled: readE2eEncryptionEnabled(),
|
|
526
|
+
hasConfigurationId: state.hasConfigurationId,
|
|
527
|
+
hasExternalCustomerId: state.hasExternalCustomerId,
|
|
528
|
+
hasProvidedSessionId: state.hasProvidedSessionId,
|
|
529
|
+
hasToken: readHasToken(),
|
|
530
|
+
hasExternalToken: state.hasExternalToken,
|
|
531
|
+
wasmProfile: readWasmProfile()
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** Fail-open: diagnostics must never throw into the host flow. */
|
|
535
|
+
function readE2eEncryptionEnabled() {
|
|
536
|
+
try {
|
|
537
|
+
return sources.getE2eEncryptionEnabled?.() ?? false;
|
|
538
|
+
} catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** Fail-open, like readE2eEncryptionEnabled. */
|
|
543
|
+
function readWasmProfile() {
|
|
544
|
+
try {
|
|
545
|
+
return sources.getWasmProfile?.() ?? null;
|
|
546
|
+
} catch {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/** Fail-open, like readE2eEncryptionEnabled. */
|
|
551
|
+
function readHasToken() {
|
|
552
|
+
try {
|
|
553
|
+
return sources.getHasToken?.() ?? false;
|
|
554
|
+
} catch {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function resetTrueSightMetadata() {
|
|
559
|
+
state = createInitialState();
|
|
560
|
+
sources = {};
|
|
561
|
+
}
|
|
562
|
+
//#endregion
|
|
563
|
+
//#region src/internal/trueSight/pendingEnvelopeStore.ts
|
|
564
|
+
/** Index slot listing sessionIds with pending entries (no enumeration API). */
|
|
565
|
+
const PENDING_INDEX_KEY = `${PENDING_STORAGE_KEY_PREFIX}index`;
|
|
566
|
+
function slotKey(sessionId) {
|
|
567
|
+
return `${PENDING_STORAGE_KEY_PREFIX}${sessionId}`;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Adds the session to the pending index. Called once at collection start —
|
|
571
|
+
* long before any page-hide — so the hide path never needs the index
|
|
572
|
+
* read-modify-write and stays a single synchronous-effective write.
|
|
573
|
+
*/
|
|
574
|
+
async function registerPendingSlot(sessionId, deps) {
|
|
575
|
+
try {
|
|
576
|
+
const index = await readIndex(deps.storage);
|
|
577
|
+
if (index.includes(sessionId)) return;
|
|
578
|
+
await deps.storage.set(PENDING_INDEX_KEY, [...index, sessionId]);
|
|
579
|
+
} catch {}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Writes the sealed snapshot envelope into the session's slot. `BrowserStorageProvider`
|
|
583
|
+
* executes the underlying `localStorage.setItem` synchronously in the first
|
|
584
|
+
* slice of the async call, which is what makes this safe to fire (void) from
|
|
585
|
+
* a page-hide handler.
|
|
586
|
+
*/
|
|
587
|
+
async function writePendingEnvelope(sessionId, entry, deps) {
|
|
588
|
+
try {
|
|
589
|
+
await deps.storage.set(slotKey(sessionId), entry);
|
|
590
|
+
} catch {}
|
|
591
|
+
}
|
|
592
|
+
/** Removes the entry and its index membership (terminal upload dispatched). */
|
|
593
|
+
async function clearPending(sessionId, deps) {
|
|
594
|
+
try {
|
|
595
|
+
await deps.storage.remove(slotKey(sessionId));
|
|
596
|
+
const index = await readIndex(deps.storage);
|
|
597
|
+
if (index.includes(sessionId)) await deps.storage.set(PENDING_INDEX_KEY, index.filter((candidate) => candidate !== sessionId));
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Next-load recovery, invoked from the first-token-observed hook (A1 — never
|
|
602
|
+
* at `setup()`, which has no auth). For each foreign, old-enough entry: the
|
|
603
|
+
* slot is cleared AT DISPATCH, then the envelope is posted fire-and-forget —
|
|
604
|
+
* no 200-tracking, no retries (A2; a crash between clear and fire loses that
|
|
605
|
+
* record, consistent with no-retry). Entries at/past the retention ceiling
|
|
606
|
+
* are deleted unsent (A4). Never rejects; returns the A5 loss counters.
|
|
607
|
+
*/
|
|
608
|
+
async function recoverPendingEnvelopes(options) {
|
|
609
|
+
const { storage, post, currentSessionId } = options;
|
|
610
|
+
const now = options.now ?? getTimestamp;
|
|
611
|
+
const counters = {
|
|
612
|
+
recovered: 0,
|
|
613
|
+
gcExpired: 0
|
|
614
|
+
};
|
|
615
|
+
try {
|
|
616
|
+
const index = await readIndex(storage);
|
|
617
|
+
if (index.length === 0) return counters;
|
|
618
|
+
const keptInIndex = [];
|
|
619
|
+
for (const sessionId of index) {
|
|
620
|
+
if (sessionId === currentSessionId) {
|
|
621
|
+
keptInIndex.push(sessionId);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const entry = await readEntry(storage, sessionId);
|
|
625
|
+
if (entry === "missing") continue;
|
|
626
|
+
if (entry === "corrupt") {
|
|
627
|
+
counters.gcExpired += 1;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const age = now() - entry.persistedAt;
|
|
631
|
+
if (age >= 6048e5) {
|
|
632
|
+
counters.gcExpired += 1;
|
|
633
|
+
await storage.remove(slotKey(sessionId));
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (age < 12e4) {
|
|
637
|
+
keptInIndex.push(sessionId);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
await storage.remove(slotKey(sessionId));
|
|
641
|
+
counters.recovered += 1;
|
|
642
|
+
post(endpoints.diagnosticsData, entry.envelope, {}).catch(() => {});
|
|
643
|
+
}
|
|
644
|
+
await storage.set(PENDING_INDEX_KEY, keptInIndex);
|
|
645
|
+
} catch {}
|
|
646
|
+
return counters;
|
|
647
|
+
}
|
|
648
|
+
async function readIndex(storage) {
|
|
649
|
+
const index = await storage.get(PENDING_INDEX_KEY);
|
|
650
|
+
if (!Array.isArray(index)) return [];
|
|
651
|
+
return index.filter((value) => typeof value === "string");
|
|
652
|
+
}
|
|
653
|
+
/** Reads one entry; malformed slots are pruned and reported as corrupt. */
|
|
654
|
+
async function readEntry(storage, sessionId) {
|
|
655
|
+
try {
|
|
656
|
+
const value = await storage.get(slotKey(sessionId));
|
|
657
|
+
if (value === null) return "missing";
|
|
658
|
+
if (typeof value === "object" && value.v === 1 && typeof value.persistedAt === "number" && typeof value.envelope === "object") return value;
|
|
659
|
+
await storage.remove(slotKey(sessionId));
|
|
660
|
+
return "corrupt";
|
|
661
|
+
} catch {
|
|
662
|
+
try {
|
|
663
|
+
await storage.remove(slotKey(sessionId));
|
|
664
|
+
} catch {}
|
|
665
|
+
return "corrupt";
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/internal/featureConfig/featureConfigService.ts
|
|
670
|
+
let cachedFeatures = null;
|
|
671
|
+
/**
|
|
672
|
+
* Checks if a feature is enabled in the feature config.
|
|
673
|
+
*/
|
|
674
|
+
function isFeatureEnabled(feature, features) {
|
|
675
|
+
return features?.find((f) => f.feature === feature)?.enabled ?? false;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Fetches feature configuration from the backend.
|
|
679
|
+
* Results are cached for the session lifetime.
|
|
680
|
+
*/
|
|
681
|
+
async function fetchFeatureConfig(signal) {
|
|
682
|
+
if (cachedFeatures) return cachedFeatures;
|
|
683
|
+
const response = await api.get(endpoints.featureConfig, { signal });
|
|
684
|
+
if (!response.ok) throw new Error(`Failed to fetch feature config: ${response.status} ${response.statusText}`);
|
|
685
|
+
cachedFeatures = response.data;
|
|
686
|
+
return cachedFeatures;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Resets the cached feature config (useful for testing).
|
|
690
|
+
*/
|
|
691
|
+
function resetFeatureConfigCache() {
|
|
692
|
+
cachedFeatures = null;
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region ../infra/src/http/createApi.ts
|
|
696
|
+
var FetchHttpError = class extends HttpRequestError {
|
|
697
|
+
constructor(..._args) {
|
|
698
|
+
super(..._args);
|
|
699
|
+
this.name = "FetchHttpError";
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
const buildQueryString$1 = (query) => {
|
|
703
|
+
if (!query) return "";
|
|
704
|
+
const params = new URLSearchParams();
|
|
705
|
+
Object.entries(query).forEach(([key, value]) => {
|
|
706
|
+
if (value !== void 0 && value !== null) params.append(key, String(value));
|
|
707
|
+
});
|
|
708
|
+
const str = params.toString();
|
|
709
|
+
return str ? `?${str}` : "";
|
|
710
|
+
};
|
|
711
|
+
const buildUrl$1 = (baseURL, url, query) => {
|
|
712
|
+
const queryString = buildQueryString$1(query);
|
|
713
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return `${url}${queryString}`;
|
|
714
|
+
return `${baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL}${url.startsWith("/") ? url : `/${url}`}${queryString}`;
|
|
715
|
+
};
|
|
716
|
+
const prepareBody$1 = (body) => {
|
|
175
717
|
if (body === null || body === void 0) return null;
|
|
176
718
|
if (body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer) return body;
|
|
177
719
|
if (typeof body === "object") return JSON.stringify(body);
|
|
@@ -229,7 +771,7 @@ const requestWithXHR = (fullUrl, method, mergedHeaders, requestBody, timeout, si
|
|
|
229
771
|
headers: responseHeaders,
|
|
230
772
|
data
|
|
231
773
|
});
|
|
232
|
-
else reject(new FetchHttpError(xhr.status, xhr.statusText, fullUrl, method, responseHeaders, data));
|
|
774
|
+
else reject(reportHttpError(new FetchHttpError(xhr.status, xhr.statusText, fullUrl, method, responseHeaders, data), mergedHeaders));
|
|
233
775
|
};
|
|
234
776
|
xhr.onerror = () => {
|
|
235
777
|
reject(new FetchHttpError(0, "Network Error", fullUrl, method, {}, null));
|
|
@@ -294,7 +836,7 @@ const createApi = (config) => {
|
|
|
294
836
|
responseHeaders[key] = value;
|
|
295
837
|
});
|
|
296
838
|
const data = await parseResponse(response, parse);
|
|
297
|
-
if (!response.ok) throw new FetchHttpError(response.status, response.statusText, fullUrl, method, responseHeaders, data);
|
|
839
|
+
if (!response.ok) throw reportHttpError(new FetchHttpError(response.status, response.statusText, fullUrl, method, responseHeaders, data), mergedHeaders);
|
|
298
840
|
return {
|
|
299
841
|
ok: true,
|
|
300
842
|
status: response.status,
|
|
@@ -385,16 +927,9 @@ function buildWasmDefaultHeaders(customHeaders) {
|
|
|
385
927
|
...customHeaders ?? {}
|
|
386
928
|
};
|
|
387
929
|
}
|
|
388
|
-
var WasmHttpError = class extends
|
|
389
|
-
constructor(
|
|
390
|
-
super(
|
|
391
|
-
this.ok = false;
|
|
392
|
-
this.status = status;
|
|
393
|
-
this.statusText = statusText;
|
|
394
|
-
this.url = url;
|
|
395
|
-
this.method = method;
|
|
396
|
-
this.headers = headers;
|
|
397
|
-
this.data = data;
|
|
930
|
+
var WasmHttpError = class extends HttpRequestError {
|
|
931
|
+
constructor(..._args) {
|
|
932
|
+
super(..._args);
|
|
398
933
|
this.name = "WasmHttpError";
|
|
399
934
|
}
|
|
400
935
|
};
|
|
@@ -449,9 +984,9 @@ const getErrorMessage = (error) => {
|
|
|
449
984
|
};
|
|
450
985
|
const withAbortSignal = async (promise, signal, error) => {
|
|
451
986
|
if (!signal) return await promise;
|
|
452
|
-
if (signal.aborted) throw error;
|
|
987
|
+
if (signal.aborted) throw error();
|
|
453
988
|
return await new Promise((resolve, reject) => {
|
|
454
|
-
const onAbort = () => reject(error);
|
|
989
|
+
const onAbort = () => reject(error());
|
|
455
990
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
456
991
|
promise.then(resolve, reject).finally(() => {
|
|
457
992
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -463,7 +998,12 @@ const createWasmApi = async (config) => {
|
|
|
463
998
|
const isEncryptionEnabled = config.isEncryptionEnabled ?? (() => false);
|
|
464
999
|
const useSha256 = config.mgf1 === "sha256";
|
|
465
1000
|
await initializeWasmWebClient(config.wasm);
|
|
466
|
-
|
|
1001
|
+
try {
|
|
1002
|
+
await setupWasmConnection(config.apiURL, "", config.encryptionEnabled ?? false, headers, useSha256);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
if (!(error instanceof WasmWebClientError)) throw error;
|
|
1005
|
+
throw reportHttpError(new WasmHttpError(error.statusCode, "", config.apiURL, "POST", {}, getErrorData(error)), headers);
|
|
1006
|
+
}
|
|
467
1007
|
const defaults = {
|
|
468
1008
|
baseURL: config.apiURL,
|
|
469
1009
|
headers
|
|
@@ -486,11 +1026,11 @@ const createWasmApi = async (config) => {
|
|
|
486
1026
|
const wasmProgress = onUploadProgress ? (progress) => {
|
|
487
1027
|
if (progress.total > 0) onUploadProgress(Math.round(progress.loaded / progress.total * 100));
|
|
488
1028
|
} : void 0;
|
|
489
|
-
const abortError = new WasmHttpError(0, "Request aborted", fullUrl, upperMethod, {}, null);
|
|
490
|
-
if (signal?.aborted) throw abortError;
|
|
1029
|
+
const abortError = () => new WasmHttpError(0, "Request aborted", fullUrl, upperMethod, {}, null);
|
|
1030
|
+
if (signal?.aborted) throw abortError();
|
|
491
1031
|
try {
|
|
492
1032
|
const response = await withAbortSignal(executeWasmRequest(upperMethod, url, requestBody, mergedHeaders, requestParams, timeout, wasmProgress, ie), signal, abortError);
|
|
493
|
-
if (response.status < 200 || response.status >= 300) throw new WasmHttpError(response.status, "", fullUrl, upperMethod, {}, response.data);
|
|
1033
|
+
if (response.status < 200 || response.status >= 300) throw reportHttpError(new WasmHttpError(response.status, "", fullUrl, upperMethod, {}, response.data), mergedHeaders);
|
|
494
1034
|
return {
|
|
495
1035
|
ok: true,
|
|
496
1036
|
status: response.status,
|
|
@@ -501,7 +1041,7 @@ const createWasmApi = async (config) => {
|
|
|
501
1041
|
};
|
|
502
1042
|
} catch (error) {
|
|
503
1043
|
if (error instanceof WasmHttpError) throw error;
|
|
504
|
-
throw new WasmHttpError(getErrorStatus(error), getErrorMessage(error), fullUrl, upperMethod, {}, getErrorData(error));
|
|
1044
|
+
throw reportHttpError(new WasmHttpError(getErrorStatus(error), getErrorMessage(error), fullUrl, upperMethod, {}, getErrorData(error)), mergedHeaders);
|
|
505
1045
|
}
|
|
506
1046
|
},
|
|
507
1047
|
get(url, requestConfig) {
|
|
@@ -570,10 +1110,15 @@ const createWasmApi = async (config) => {
|
|
|
570
1110
|
* host called `setup()` without `wasm` and warmed WASM separately), swap to
|
|
571
1111
|
* the WASM client first. The module is already loaded — the payload came
|
|
572
1112
|
* out of it — so the swap only creates the `WebClient`.
|
|
1113
|
+
*
|
|
1114
|
+
* These requests bypass the `api.*` helpers, so the transport reports them
|
|
1115
|
+
* to the same timing observer itself (TrueSight row 23b): the protected
|
|
1116
|
+
* image uploads are otherwise the only requests a record never shows.
|
|
573
1117
|
*/
|
|
574
1118
|
function bindWebApiTransport(client) {
|
|
575
1119
|
return async (url, body, config) => {
|
|
576
|
-
const
|
|
1120
|
+
const activeClient = config.ie === true && storedHttpConfig && !storedHttpConfig.isWasm ? await ensureWasmHttpClient() : client;
|
|
1121
|
+
const response = await withTiming("POST", url, () => activeClient.request({
|
|
577
1122
|
method: "POST",
|
|
578
1123
|
url,
|
|
579
1124
|
body,
|
|
@@ -581,7 +1126,7 @@ function bindWebApiTransport(client) {
|
|
|
581
1126
|
params: config.params,
|
|
582
1127
|
timeout: config.timeout,
|
|
583
1128
|
ie: config.ie
|
|
584
|
-
});
|
|
1129
|
+
}));
|
|
585
1130
|
return {
|
|
586
1131
|
data: response.data,
|
|
587
1132
|
status: response.status
|
|
@@ -606,511 +1151,225 @@ function getStoredHttpConfig() {
|
|
|
606
1151
|
function isHttpEncrypted() {
|
|
607
1152
|
return storedHttpConfig?.encryption.enabled ?? false;
|
|
608
1153
|
}
|
|
609
|
-
/** Clears all HTTP-client state. Called by `reset()`. */
|
|
610
|
-
function resetHttpClientState() {
|
|
611
|
-
storedHttpConfig = void 0;
|
|
612
|
-
setWebApiHttpTransport(null);
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Builds (or replaces) the HTTP client when `setup()` receives an `apiURL`.
|
|
616
|
-
* Picks `createWasmApi` when `effectiveWasm` is set, otherwise plain
|
|
617
|
-
* `createApi`. Wraps the call in handshake-failure context when encryption
|
|
618
|
-
* is requested.
|
|
619
|
-
*/
|
|
620
|
-
async function installFreshHttpClient(apiURL, options, effectiveWasm, encryption) {
|
|
621
|
-
installClient(effectiveWasm ? await withHandshakeFailureContext(encryption.enabled, () => createWasmApi({
|
|
622
|
-
apiURL,
|
|
623
|
-
customHeaders: options.customHeaders,
|
|
624
|
-
timeout: options.timeout,
|
|
625
|
-
wasm: effectiveWasm,
|
|
626
|
-
encryptionEnabled: encryption.enabled,
|
|
627
|
-
mgf1: encryption.mgf1,
|
|
628
|
-
isEncryptionEnabled: isHttpEncrypted
|
|
629
|
-
})) : createApi({
|
|
630
|
-
apiURL,
|
|
631
|
-
customHeaders: options.customHeaders,
|
|
632
|
-
timeout: options.timeout
|
|
633
|
-
}));
|
|
634
|
-
storedHttpConfig = {
|
|
635
|
-
apiURL,
|
|
636
|
-
customHeaders: options.customHeaders,
|
|
637
|
-
timeout: options.timeout,
|
|
638
|
-
isWasm: Boolean(effectiveWasm),
|
|
639
|
-
encryption
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Replaces the active HTTP client with the WASM-backed one. Used internally
|
|
644
|
-
* by lazy-loading UI components (`<incode-flow>`, `<incode-workflow>`) when
|
|
645
|
-
* WASM finishes warming up.
|
|
646
|
-
*
|
|
647
|
-
* No-op when no apiURL was previously configured, or when the active client
|
|
648
|
-
* is already WASM-backed. The encryption state (enabled + MGF1 scheme) is
|
|
649
|
-
* preserved across the swap — this is *not* a user-facing encryption change.
|
|
650
|
-
*
|
|
651
|
-
* @internal NOT a public API. Application code should use
|
|
652
|
-
* `setup({ wasm: {...} })` instead.
|
|
653
|
-
*/
|
|
654
|
-
async function upgradeToWasmHttpClient(resolved) {
|
|
655
|
-
const stored = storedHttpConfig;
|
|
656
|
-
if (!stored || stored.isWasm) return;
|
|
657
|
-
pendingUpgrade ??= (async () => {
|
|
658
|
-
installClient(await createWasmApi({
|
|
659
|
-
apiURL: stored.apiURL,
|
|
660
|
-
customHeaders: stored.customHeaders,
|
|
661
|
-
timeout: stored.timeout,
|
|
662
|
-
wasm: resolved,
|
|
663
|
-
encryptionEnabled: stored.encryption.enabled,
|
|
664
|
-
mgf1: stored.encryption.mgf1,
|
|
665
|
-
isEncryptionEnabled: isHttpEncrypted
|
|
666
|
-
}));
|
|
667
|
-
storedHttpConfig = {
|
|
668
|
-
...stored,
|
|
669
|
-
isWasm: true
|
|
670
|
-
};
|
|
671
|
-
})().finally(() => {
|
|
672
|
-
pendingUpgrade = void 0;
|
|
673
|
-
});
|
|
674
|
-
return pendingUpgrade;
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* When `wantsEncryption` is true, wraps a call that triggers the in-binary
|
|
678
|
-
* RSA/AES handshake so its rejection surfaces as a single, descriptive error
|
|
679
|
-
* with the original error preserved as `cause`. Pass-through otherwise.
|
|
680
|
-
*/
|
|
681
|
-
async function withHandshakeFailureContext(wantsEncryption, fn) {
|
|
682
|
-
if (!wantsEncryption) return fn();
|
|
683
|
-
try {
|
|
684
|
-
return await fn();
|
|
685
|
-
} catch (cause) {
|
|
686
|
-
const error = /* @__PURE__ */ new Error("Failed to establish encrypted transport. The /e2ee/key handshake did not complete. Verify the apiURL is reachable and that the environment supports E2EE.");
|
|
687
|
-
error.cause = cause;
|
|
688
|
-
throw error;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
/** Hard cap per log message, UTF-8 bytes (contract). */
|
|
692
|
-
const MESSAGE_MAX_BYTES = 8 * 1024;
|
|
693
|
-
const TRUESIGHT_SDK_ID = "IncdOnboarding";
|
|
694
|
-
/** localStorage key prefix for persisted sealed snapshots (A3: one slot per session). */
|
|
695
|
-
const PENDING_STORAGE_KEY_PREFIX = "incode.truesight.pending.";
|
|
696
|
-
//#endregion
|
|
697
|
-
//#region src/internal/trueSight/sanitize.ts
|
|
698
|
-
/**
|
|
699
|
-
* Deny-by-default message scrubber — runs at INGESTION (inside the log buffer's
|
|
700
|
-
* `append`), so the buffer, every persisted snapshot, and every upload are
|
|
701
|
-
* already clean; there is no code path around it (ENG-48677 plan §1.2).
|
|
702
|
-
*
|
|
703
|
-
* Ordered pipeline. Two deliberate deviations from the plan's original rule
|
|
704
|
-
* order, both from the P1-A adversarial review (2026-07-23, recorded in the
|
|
705
|
-
* plan's Operating-decisions section):
|
|
706
|
-
* - Inputs are pre-clamped to 2× the message cap BEFORE any regex runs — the
|
|
707
|
-
* unanchored patterns are quadratic on pathological non-matching runs
|
|
708
|
-
* (measured ~4.5 s on a 100 KB run), so regex cost must be bounded first.
|
|
709
|
-
* The final 8 KB cap still runs last.
|
|
710
|
-
* - The base64-blob rule runs BEFORE the digit-run rule — a digit run inside a
|
|
711
|
-
* blob would otherwise split it into sub-64-char fragments that escape
|
|
712
|
-
* redaction.
|
|
713
|
-
*
|
|
714
|
-
* The regex table ships in the P1-A PR description for privacy sign-off (see
|
|
715
|
-
* .agents/guides/privacy.md). Known hardening backlog (D4, Phase 4): formatted
|
|
716
|
-
* phone numbers, unicode digits/emails, fuzz/property tests.
|
|
717
|
-
*/
|
|
718
|
-
/** Three dot-joined base64url segments — JWT/JWS shape. */
|
|
719
|
-
const JWT_PATTERN = /\b[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g;
|
|
720
|
-
/**
|
|
721
|
-
* Credential-ish key with a `:`/`=` value (quoted, `Bearer …`, or bare token).
|
|
722
|
-
* The key may carry prefixes (`access_token`, `x-api-key`, `sessionToken`) —
|
|
723
|
-
* a plain \b boundary would miss `_token` since `_` is a word character.
|
|
724
|
-
*/
|
|
725
|
-
const CREDENTIAL_KV_PATTERN = /([\w-]*(?:token|api[-_]?key|authorization|auth|secret|password|passwd))\s*[:=]\s*("[^"]*"|'[^']*'|(?:Bearer\s+)?[^\s,;&]+)/gi;
|
|
726
|
-
/** Userinfo credentials in an absolute URL (`https://user:pass@host/…`). */
|
|
727
|
-
const URL_USERINFO_PATTERN = /(\bhttps?:\/\/)[^\s/@]*(?::[^\s/@]*)?@/gi;
|
|
728
|
-
/** Query string / fragment of an absolute URL — kills signed URLs. */
|
|
729
|
-
const URL_TAIL_PATTERN = /(https?:\/\/[^\s?#]+)[?#][^\s]*/g;
|
|
730
|
-
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
731
|
-
/** base64-ish blobs (media, keys) — 64+ chars of the base64/base64url alphabet. */
|
|
732
|
-
const BASE64_BLOB_PATTERN = /[-A-Za-z0-9+/_]{64,}={0,2}/g;
|
|
733
|
-
/** Runs of 8+ digits — phones, CURP, document numbers. */
|
|
734
|
-
const DIGIT_RUN_PATTERN = /\d{8,}/g;
|
|
735
|
-
const TRUNCATION_SUFFIX = "…[truncated]";
|
|
736
|
-
/** Regex-cost bound: clamp raw input before any pattern runs (see header note). */
|
|
737
|
-
const REGEX_INPUT_MAX_BYTES = MESSAGE_MAX_BYTES * 2;
|
|
738
|
-
function sanitizeMessage(message) {
|
|
739
|
-
return truncateUtf8(truncateUtf8(message, REGEX_INPUT_MAX_BYTES).replace(JWT_PATTERN, "[jwt]").replace(CREDENTIAL_KV_PATTERN, "$1=[redacted]").replace(URL_USERINFO_PATTERN, "$1[redacted]@").replace(URL_TAIL_PATTERN, "$1").replace(EMAIL_PATTERN, "[email]").replace(BASE64_BLOB_PATTERN, (match) => `[blob:${match.length}]`).replace(DIGIT_RUN_PATTERN, "[digits]"), MESSAGE_MAX_BYTES);
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* UTF-8 byte length without TextEncoder (banned browser global in L1 —
|
|
743
|
-
* see the no-browser-api-in-core lint rule). Counts by code point.
|
|
744
|
-
*/
|
|
745
|
-
function utf8ByteLength(value) {
|
|
746
|
-
let bytes = 0;
|
|
747
|
-
for (const char of value) bytes += codePointByteLength(char);
|
|
748
|
-
return bytes;
|
|
749
|
-
}
|
|
750
|
-
function codePointByteLength(char) {
|
|
751
|
-
const codePoint = char.codePointAt(0) ?? 0;
|
|
752
|
-
if (codePoint < 128) return 1;
|
|
753
|
-
if (codePoint < 2048) return 2;
|
|
754
|
-
if (codePoint < 65536) return 3;
|
|
755
|
-
return 4;
|
|
756
|
-
}
|
|
757
|
-
/** Cuts on a code-point boundary so no surrogate half ever reaches the wire. */
|
|
758
|
-
function truncateUtf8(value, maxBytes) {
|
|
759
|
-
if (utf8ByteLength(value) <= maxBytes) return value;
|
|
760
|
-
const budget = maxBytes - utf8ByteLength(TRUNCATION_SUFFIX);
|
|
761
|
-
let bytes = 0;
|
|
762
|
-
let result = "";
|
|
763
|
-
for (const char of value) {
|
|
764
|
-
const charBytes = codePointByteLength(char);
|
|
765
|
-
if (bytes + charBytes > budget) break;
|
|
766
|
-
bytes += charBytes;
|
|
767
|
-
result += char;
|
|
768
|
-
}
|
|
769
|
-
return result + TRUNCATION_SUFFIX;
|
|
770
|
-
}
|
|
771
|
-
//#endregion
|
|
772
|
-
//#region ../infra/src/crypto/randomUUID.ts
|
|
773
|
-
/**
|
|
774
|
-
* Cryptographically-strong UUID v4 (RFC 4122).
|
|
775
|
-
*
|
|
776
|
-
* Lives in infra (L0) so Core (L1) can mint secure identifiers without touching
|
|
777
|
-
* the `crypto` browser API directly — Core delegates here, keeping it free of
|
|
778
|
-
* browser globals (enforced by the `no-browser-api-in-core` lint rule). Backed
|
|
779
|
-
* by `crypto.randomUUID`, available in every browser the SDK targets.
|
|
780
|
-
*/
|
|
781
|
-
function randomUUID() {
|
|
782
|
-
return crypto.randomUUID();
|
|
783
|
-
}
|
|
784
|
-
//#endregion
|
|
785
|
-
//#region ../infra/src/time/epochMsToIsoString.ts
|
|
786
|
-
/**
|
|
787
|
-
* Converts an epoch-milliseconds timestamp to a full ISO-8601 UTC string
|
|
788
|
-
* (e.g. `2026-07-23T09:41:00.000Z`).
|
|
789
|
-
*
|
|
790
|
-
* Lives in Infra (L0) so Core can format wire timestamps without touching the
|
|
791
|
-
* `Date` global directly — banned in Core by the `no-browser-api-in-core` lint
|
|
792
|
-
* rule — mirroring the `crypto/randomUUID.ts` free-function precedent.
|
|
793
|
-
* (`DateFormattingProvider.utcToIsoDateString` is date-only and not suitable.)
|
|
794
|
-
*/
|
|
795
|
-
function epochMsToIsoString(epochMs) {
|
|
796
|
-
return new Date(epochMs).toISOString();
|
|
797
|
-
}
|
|
798
|
-
//#endregion
|
|
799
|
-
//#region src/internal/trueSight/recordBuilder.ts
|
|
800
|
-
/**
|
|
801
|
-
* Assembles the exact plaintext contract record — the ONLY place a diagnostics
|
|
802
|
-
* payload is shaped. Pure: same function serves final records, interrupted
|
|
803
|
-
* snapshots, and future incremental slices (ENG-48677 plan §1.1).
|
|
804
|
-
*
|
|
805
|
-
* Deny-by-default is structural here: every field is an explicit literal —
|
|
806
|
-
* nothing is spread from internal state, so no unexpected field can ever leak
|
|
807
|
-
* into an upload.
|
|
808
|
-
*
|
|
809
|
-
* Hard gates enforced:
|
|
810
|
-
* - empty/missing `sessionId` or `startedAt` → returns `null` (upload skipped —
|
|
811
|
-
* expected for sessions that die before `/omni/start`).
|
|
812
|
-
* - `failureReason` present if-and-only-if `outcome === 'failure'` (defaults to
|
|
813
|
-
* `'unknown'` when a failure arrives without one, rather than dropping the record).
|
|
814
|
-
* - fresh record `id` per build; `endedAt` stamped at build time.
|
|
815
|
-
*/
|
|
816
|
-
function buildDiagnosticsRecord(meta, logs, outcome, failureReason, recordId) {
|
|
817
|
-
if (!meta.sessionId || meta.startedAtMs === null) return null;
|
|
818
|
-
const flowType = meta.flowType ?? deriveFlowTypeFromSurface(meta.integrationType);
|
|
819
|
-
if (flowType === null) return null;
|
|
820
|
-
return {
|
|
821
|
-
schemaVersion: "1.0",
|
|
822
|
-
id: recordId ?? randomUUID(),
|
|
823
|
-
session: {
|
|
824
|
-
sessionId: meta.sessionId,
|
|
825
|
-
sdkId: TRUESIGHT_SDK_ID,
|
|
826
|
-
sdkVersion: SDK_VERSION,
|
|
827
|
-
platform: "web",
|
|
828
|
-
sdkMode: null,
|
|
829
|
-
browser: meta.browser,
|
|
830
|
-
osVersion: meta.osVersion,
|
|
831
|
-
osLanguage: meta.osLanguage,
|
|
832
|
-
sdkLanguage: meta.sdkLanguage,
|
|
833
|
-
integrationType: meta.integrationType,
|
|
834
|
-
flowType,
|
|
835
|
-
flowId: meta.flowId,
|
|
836
|
-
workflowId: meta.workflowId,
|
|
837
|
-
outcome,
|
|
838
|
-
...outcome === "failure" ? { failureReason: failureReason ?? "unknown" } : {},
|
|
839
|
-
startedAt: epochMsToIsoString(meta.startedAtMs),
|
|
840
|
-
endedAt: epochMsToIsoString(getTimestamp()),
|
|
841
|
-
testing: meta.testing,
|
|
842
|
-
e2eEncryptionEnabled: meta.e2eEncryptionEnabled,
|
|
843
|
-
configurationId: meta.hasConfigurationId,
|
|
844
|
-
externalCustomerId: meta.hasExternalCustomerId,
|
|
845
|
-
hasProvidedSessionId: meta.hasProvidedSessionId,
|
|
846
|
-
token: meta.hasToken,
|
|
847
|
-
externalToken: meta.hasExternalToken
|
|
848
|
-
},
|
|
849
|
-
logs: logs.map(toContractEntry)
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* Wire `flowType` when no orchestrator was OBSERVED (an observed one — set by
|
|
854
|
-
* the bridge on any flow event — always wins). Stage ingest requires a
|
|
855
|
-
* non-null enum (probe 2026-07-23: null/omitted/off-enum all 400), so the
|
|
856
|
-
* entry surface decides, and the mapping is deliberately exhaustive:
|
|
857
|
-
*
|
|
858
|
-
* - `startWorkflow` → 'workflow' (Webflows engine entered, died pre-ready)
|
|
859
|
-
* - `startFlow` → 'flow' (flow/orchestrated manager, died pre-ready)
|
|
860
|
-
* - `startOnboarding`→ 'flow' (self-loading UI wraps the flow engine)
|
|
861
|
-
* - `startOnboardingSection` → null → the BUILD ABORTS (record withheld):
|
|
862
|
-
* a standalone session has no truthful flowType, and mislabeling it as a
|
|
863
|
-
* flow session is worse than skipping the upload. Withheld until the
|
|
864
|
-
* DECIDED BE ask ships (`flowType:"standalone"`, not nullable) — then this
|
|
865
|
-
* branch returns 'standalone' and standalone diagnostics start flowing,
|
|
866
|
-
* coordinated with the BE deploy since off-enum values 400 today.
|
|
867
|
-
*/
|
|
868
|
-
function deriveFlowTypeFromSurface(integrationType) {
|
|
869
|
-
switch (integrationType) {
|
|
870
|
-
case "startWorkflow": return "workflow";
|
|
871
|
-
case "startFlow":
|
|
872
|
-
case "startOnboarding": return "flow";
|
|
873
|
-
case "startOnboardingSection": return null;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
function toContractEntry(entry) {
|
|
877
|
-
return {
|
|
878
|
-
index: entry.index,
|
|
879
|
-
timestamp: epochMsToIsoString(entry.timestampMs),
|
|
880
|
-
level: entry.level,
|
|
881
|
-
thread: "main",
|
|
882
|
-
message: entry.message,
|
|
883
|
-
subsystem: entry.subsystem,
|
|
884
|
-
category: entry.category
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
function createInitialState() {
|
|
888
|
-
return {
|
|
889
|
-
sessionId: null,
|
|
890
|
-
startedAtMs: null,
|
|
891
|
-
browser: null,
|
|
892
|
-
osVersion: null,
|
|
893
|
-
osLanguage: null,
|
|
894
|
-
sdkLanguage: "en-US",
|
|
895
|
-
integrationType: "startOnboardingSection",
|
|
896
|
-
flowType: null,
|
|
897
|
-
flowId: null,
|
|
898
|
-
workflowId: null,
|
|
899
|
-
testing: false,
|
|
900
|
-
devModeSignal: void 0,
|
|
901
|
-
apiUrlSignal: void 0,
|
|
902
|
-
hasConfigurationId: false,
|
|
903
|
-
hasExternalCustomerId: false,
|
|
904
|
-
hasProvidedSessionId: false,
|
|
905
|
-
hasExternalToken: false
|
|
906
|
-
};
|
|
907
|
-
}
|
|
908
|
-
let state = createInitialState();
|
|
909
|
-
let sources = {};
|
|
910
|
-
/** Tap for `createSession` — sessionId comes from the returned `interviewId`. */
|
|
911
|
-
function noteSessionCreated(session, options = {}) {}
|
|
912
|
-
function noteEnvironment(environment) {}
|
|
913
|
-
/**
|
|
914
|
-
* `testing` heuristic: devMode OR non-prod API host pattern. Always a boolean —
|
|
915
|
-
* the contract requires the field present on every record. Signals MERGE across
|
|
916
|
-
* calls (a later apiUrl-only call must not erase an earlier devMode signal);
|
|
917
|
-
* `testing` is recomputed from the merged pair.
|
|
918
|
-
*/
|
|
919
|
-
function noteTestingSignals(signals) {}
|
|
920
|
-
/**
|
|
921
|
-
* Live read of the merged testing signal (devMode OR non-prod host). Used to
|
|
922
|
-
* gate host-console self-health noise so production host pages stay quiet while
|
|
923
|
-
* staging/dev keeps the console signal (the record always captures it either
|
|
924
|
-
* way via `trueSightLog`). Same signal the overflow-flush staging gate uses.
|
|
925
|
-
*/
|
|
926
|
-
function isTrueSightTesting() {
|
|
927
|
-
return state.testing;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Wiring tap for every `initializeSession({ token })`: a token activated
|
|
931
|
-
* before any `createSession` was observed on this page is host-minted, i.e.
|
|
932
|
-
* external. (The exact `token`/`externalToken` web-semantics are a pending
|
|
933
|
-
* Phase 0 contract ask — this inference is the interim mapping.)
|
|
934
|
-
*/
|
|
935
|
-
function noteTokenActivated() {}
|
|
936
|
-
/** Explicit literal — the metadata half handed to the record builder. */
|
|
937
|
-
function buildSessionMetadata() {
|
|
938
|
-
return {
|
|
939
|
-
sessionId: state.sessionId,
|
|
940
|
-
startedAtMs: state.startedAtMs,
|
|
941
|
-
browser: state.browser,
|
|
942
|
-
osVersion: state.osVersion,
|
|
943
|
-
osLanguage: state.osLanguage,
|
|
944
|
-
sdkLanguage: state.sdkLanguage,
|
|
945
|
-
integrationType: state.integrationType,
|
|
946
|
-
flowType: state.flowType,
|
|
947
|
-
flowId: state.flowId,
|
|
948
|
-
workflowId: state.workflowId,
|
|
949
|
-
testing: state.testing,
|
|
950
|
-
e2eEncryptionEnabled: readE2eEncryptionEnabled(),
|
|
951
|
-
hasConfigurationId: state.hasConfigurationId,
|
|
952
|
-
hasExternalCustomerId: state.hasExternalCustomerId,
|
|
953
|
-
hasProvidedSessionId: state.hasProvidedSessionId,
|
|
954
|
-
hasToken: readHasToken(),
|
|
955
|
-
hasExternalToken: state.hasExternalToken
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
/** Fail-open: diagnostics must never throw into the host flow. */
|
|
959
|
-
function readE2eEncryptionEnabled() {
|
|
960
|
-
try {
|
|
961
|
-
return sources.getE2eEncryptionEnabled?.() ?? false;
|
|
962
|
-
} catch {
|
|
963
|
-
return false;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
/** Fail-open, like readE2eEncryptionEnabled. */
|
|
967
|
-
function readHasToken() {
|
|
968
|
-
try {
|
|
969
|
-
return sources.getHasToken?.() ?? false;
|
|
970
|
-
} catch {
|
|
971
|
-
return false;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
function resetTrueSightMetadata() {
|
|
975
|
-
state = createInitialState();
|
|
976
|
-
sources = {};
|
|
977
|
-
}
|
|
978
|
-
//#endregion
|
|
979
|
-
//#region src/internal/trueSight/pendingEnvelopeStore.ts
|
|
980
|
-
/** Index slot listing sessionIds with pending entries (no enumeration API). */
|
|
981
|
-
const PENDING_INDEX_KEY = `${PENDING_STORAGE_KEY_PREFIX}index`;
|
|
982
|
-
function slotKey(sessionId) {
|
|
983
|
-
return `${PENDING_STORAGE_KEY_PREFIX}${sessionId}`;
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* Adds the session to the pending index. Called once at collection start —
|
|
987
|
-
* long before any page-hide — so the hide path never needs the index
|
|
988
|
-
* read-modify-write and stays a single synchronous-effective write.
|
|
989
|
-
*/
|
|
990
|
-
async function registerPendingSlot(sessionId, deps) {
|
|
991
|
-
try {
|
|
992
|
-
const index = await readIndex(deps.storage);
|
|
993
|
-
if (index.includes(sessionId)) return;
|
|
994
|
-
await deps.storage.set(PENDING_INDEX_KEY, [...index, sessionId]);
|
|
995
|
-
} catch {}
|
|
996
|
-
}
|
|
997
|
-
/**
|
|
998
|
-
* Writes the sealed snapshot into the session's slot. `BrowserStorageProvider`
|
|
999
|
-
* executes the underlying `localStorage.setItem` synchronously in the first
|
|
1000
|
-
* slice of the async call, which is what makes this safe to fire (void) from
|
|
1001
|
-
* a page-hide handler.
|
|
1002
|
-
*/
|
|
1003
|
-
async function writePendingEnvelope(sessionId, entry, deps) {
|
|
1004
|
-
try {
|
|
1005
|
-
await deps.storage.set(slotKey(sessionId), entry);
|
|
1006
|
-
} catch {}
|
|
1007
|
-
}
|
|
1008
|
-
/** Removes the entry and its index membership (terminal upload dispatched). */
|
|
1009
|
-
async function clearPending(sessionId, deps) {
|
|
1010
|
-
try {
|
|
1011
|
-
await deps.storage.remove(slotKey(sessionId));
|
|
1012
|
-
const index = await readIndex(deps.storage);
|
|
1013
|
-
if (index.includes(sessionId)) await deps.storage.set(PENDING_INDEX_KEY, index.filter((candidate) => candidate !== sessionId));
|
|
1014
|
-
} catch {}
|
|
1015
|
-
}
|
|
1154
|
+
/** Clears all HTTP-client state. Called by `reset()`. */
|
|
1155
|
+
function resetHttpClientState() {
|
|
1156
|
+
storedHttpConfig = void 0;
|
|
1157
|
+
setWebApiHttpTransport(null);
|
|
1158
|
+
}
|
|
1016
1159
|
/**
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1020
|
-
*
|
|
1021
|
-
* record, consistent with no-retry). Entries at/past the retention ceiling
|
|
1022
|
-
* are deleted unsent (A4). Never rejects; returns the A5 loss counters.
|
|
1160
|
+
* Builds (or replaces) the HTTP client when `setup()` receives an `apiURL`.
|
|
1161
|
+
* Picks `createWasmApi` when `effectiveWasm` is set, otherwise plain
|
|
1162
|
+
* `createApi`. Wraps the call in handshake-failure context when encryption
|
|
1163
|
+
* is requested.
|
|
1023
1164
|
*/
|
|
1024
|
-
async function
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1165
|
+
async function installFreshHttpClient(apiURL, options, effectiveWasm, encryption) {
|
|
1166
|
+
installClient(effectiveWasm ? await withHandshakeFailureContext(encryption.enabled, () => createWasmApi({
|
|
1167
|
+
apiURL,
|
|
1168
|
+
customHeaders: options.customHeaders,
|
|
1169
|
+
timeout: options.timeout,
|
|
1170
|
+
wasm: effectiveWasm,
|
|
1171
|
+
encryptionEnabled: encryption.enabled,
|
|
1172
|
+
mgf1: encryption.mgf1,
|
|
1173
|
+
isEncryptionEnabled: isHttpEncrypted
|
|
1174
|
+
})) : createApi({
|
|
1175
|
+
apiURL,
|
|
1176
|
+
customHeaders: options.customHeaders,
|
|
1177
|
+
timeout: options.timeout
|
|
1178
|
+
}));
|
|
1179
|
+
storedHttpConfig = {
|
|
1180
|
+
apiURL,
|
|
1181
|
+
customHeaders: options.customHeaders,
|
|
1182
|
+
timeout: options.timeout,
|
|
1183
|
+
isWasm: Boolean(effectiveWasm),
|
|
1184
|
+
encryption
|
|
1030
1185
|
};
|
|
1031
|
-
try {
|
|
1032
|
-
const index = await readIndex(storage);
|
|
1033
|
-
if (index.length === 0) return counters;
|
|
1034
|
-
const keptInIndex = [];
|
|
1035
|
-
for (const sessionId of index) {
|
|
1036
|
-
if (sessionId === currentSessionId) {
|
|
1037
|
-
keptInIndex.push(sessionId);
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
const entry = await readEntry(storage, sessionId);
|
|
1041
|
-
if (entry === "missing") continue;
|
|
1042
|
-
if (entry === "corrupt") {
|
|
1043
|
-
counters.gcExpired += 1;
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
const age = now() - entry.persistedAt;
|
|
1047
|
-
if (age >= 6048e5) {
|
|
1048
|
-
counters.gcExpired += 1;
|
|
1049
|
-
await storage.remove(slotKey(sessionId));
|
|
1050
|
-
continue;
|
|
1051
|
-
}
|
|
1052
|
-
if (age < 12e4) {
|
|
1053
|
-
keptInIndex.push(sessionId);
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
await storage.remove(slotKey(sessionId));
|
|
1057
|
-
counters.recovered += 1;
|
|
1058
|
-
post(endpoints.diagnosticsData, entry.envelope, {}).catch(() => {});
|
|
1059
|
-
}
|
|
1060
|
-
await storage.set(PENDING_INDEX_KEY, keptInIndex);
|
|
1061
|
-
} catch {}
|
|
1062
|
-
return counters;
|
|
1063
1186
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1187
|
+
/**
|
|
1188
|
+
* Replaces the active HTTP client with the WASM-backed one. Used internally
|
|
1189
|
+
* by lazy-loading UI components (`<incode-flow>`, `<incode-workflow>`) when
|
|
1190
|
+
* WASM finishes warming up.
|
|
1191
|
+
*
|
|
1192
|
+
* No-op when no apiURL was previously configured, or when the active client
|
|
1193
|
+
* is already WASM-backed. The encryption state (enabled + MGF1 scheme) is
|
|
1194
|
+
* preserved across the swap — this is *not* a user-facing encryption change.
|
|
1195
|
+
*
|
|
1196
|
+
* @internal NOT a public API. Application code should use
|
|
1197
|
+
* `setup({ wasm: {...} })` instead.
|
|
1198
|
+
*/
|
|
1199
|
+
async function upgradeToWasmHttpClient(resolved) {
|
|
1200
|
+
const stored = storedHttpConfig;
|
|
1201
|
+
if (!stored || stored.isWasm) return;
|
|
1202
|
+
pendingUpgrade ??= (async () => {
|
|
1203
|
+
try {
|
|
1204
|
+
installClient(await createWasmApi({
|
|
1205
|
+
apiURL: stored.apiURL,
|
|
1206
|
+
customHeaders: stored.customHeaders,
|
|
1207
|
+
timeout: stored.timeout,
|
|
1208
|
+
wasm: resolved,
|
|
1209
|
+
encryptionEnabled: stored.encryption.enabled,
|
|
1210
|
+
mgf1: stored.encryption.mgf1,
|
|
1211
|
+
isEncryptionEnabled: isHttpEncrypted
|
|
1212
|
+
}));
|
|
1213
|
+
storedHttpConfig = {
|
|
1214
|
+
...stored,
|
|
1215
|
+
isWasm: true
|
|
1216
|
+
};
|
|
1217
|
+
emitWasmDiagnostic({
|
|
1218
|
+
level: "info",
|
|
1219
|
+
subsystem: "client",
|
|
1220
|
+
category: "other",
|
|
1221
|
+
message: `http client upgraded to wasm encrypted=${stored.encryption.enabled}`
|
|
1222
|
+
});
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
emitWasmDiagnostic({
|
|
1225
|
+
level: "error",
|
|
1226
|
+
subsystem: "client",
|
|
1227
|
+
category: "other",
|
|
1228
|
+
message: `http client upgrade failed error=${String(error)}`
|
|
1229
|
+
});
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
})().finally(() => {
|
|
1233
|
+
pendingUpgrade = void 0;
|
|
1234
|
+
});
|
|
1235
|
+
return pendingUpgrade;
|
|
1068
1236
|
}
|
|
1069
|
-
/**
|
|
1070
|
-
|
|
1237
|
+
/**
|
|
1238
|
+
* When `wantsEncryption` is true, wraps a call that triggers the in-binary
|
|
1239
|
+
* RSA/AES handshake so its rejection surfaces as a single, descriptive error
|
|
1240
|
+
* with the original error preserved as `cause`. Pass-through otherwise.
|
|
1241
|
+
*/
|
|
1242
|
+
async function withHandshakeFailureContext(wantsEncryption, fn) {
|
|
1243
|
+
if (!wantsEncryption) return fn();
|
|
1071
1244
|
try {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
} catch {
|
|
1078
|
-
try {
|
|
1079
|
-
await storage.remove(slotKey(sessionId));
|
|
1080
|
-
} catch {}
|
|
1081
|
-
return "corrupt";
|
|
1245
|
+
return await fn();
|
|
1246
|
+
} catch (cause) {
|
|
1247
|
+
const error = /* @__PURE__ */ new Error("Failed to establish encrypted transport. The /e2ee/key handshake did not complete. Verify the apiURL is reachable and that the environment supports E2EE.");
|
|
1248
|
+
error.cause = cause;
|
|
1249
|
+
throw error;
|
|
1082
1250
|
}
|
|
1083
1251
|
}
|
|
1084
1252
|
//#endregion
|
|
1085
|
-
//#region src/internal/
|
|
1086
|
-
let cachedFeatures = null;
|
|
1253
|
+
//#region src/internal/trueSight/eventBridge.ts
|
|
1087
1254
|
/**
|
|
1088
|
-
*
|
|
1255
|
+
* Canonical subsystem registry (plan §Subsystem registry): one stable
|
|
1256
|
+
* lowercase dotted string per SDK area. The raw `eventModuleNames` wire
|
|
1257
|
+
* values are inconsistent (`SELFIE`, `datasharingconsent`, `AdSign`…), four
|
|
1258
|
+
* keys collapse into `ID`, and `workflow` doubles as a module name — this
|
|
1259
|
+
* table is the single normalization point, snapshot-frozen in tests so the
|
|
1260
|
+
* dashboard facet stays bounded (~70 strings) forever.
|
|
1089
1261
|
*/
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1262
|
+
const MODULE_SUBSYSTEMS = {
|
|
1263
|
+
[eventModuleNames.selfie]: "module.selfie",
|
|
1264
|
+
[eventModuleNames.faceCapture]: "module.face-capture",
|
|
1265
|
+
[eventModuleNames.authentication]: "module.authentication",
|
|
1266
|
+
[eventModuleNames.authFace]: "module.auth-face",
|
|
1267
|
+
[eventModuleNames.mlConsent]: "module.ml-consent",
|
|
1268
|
+
[eventModuleNames.combinedConsent]: "module.combined-consent",
|
|
1269
|
+
[eventModuleNames.curpValidation]: "module.curp",
|
|
1270
|
+
[eventModuleNames.faceMatch]: "module.face-match",
|
|
1271
|
+
[eventModuleNames.qr]: "module.qr",
|
|
1272
|
+
[eventModuleNames.videoSelfie]: "module.video-selfie",
|
|
1273
|
+
[eventModuleNames.id]: "module.id",
|
|
1274
|
+
[eventModuleNames.idCapture]: "module.id-capture",
|
|
1275
|
+
[eventModuleNames.idVerification]: "module.id-verification",
|
|
1276
|
+
[eventModuleNames.idOcr]: "module.id-ocr",
|
|
1277
|
+
[eventModuleNames.document]: "module.document-capture",
|
|
1278
|
+
[eventModuleNames.creditCard]: "module.credit-card-front",
|
|
1279
|
+
[eventModuleNames.creditCardBack]: "module.credit-card-back",
|
|
1280
|
+
[eventModuleNames.conference]: "module.conference",
|
|
1281
|
+
[eventModuleNames.otp]: "module.otp",
|
|
1282
|
+
[eventModuleNames.signature]: "module.e-sign",
|
|
1283
|
+
[eventModuleNames.ekyc]: "module.e-kyc",
|
|
1284
|
+
[eventModuleNames.watchlist]: "module.watchlist",
|
|
1285
|
+
[eventModuleNames.globalWatchList]: "module.global-watchlist",
|
|
1286
|
+
[eventModuleNames.customWatchList]: "module.custom-watchlist",
|
|
1287
|
+
[eventModuleNames.email]: "module.email",
|
|
1288
|
+
[eventModuleNames.phone]: "module.phone-number",
|
|
1289
|
+
[eventModuleNames.instantVerifyEmail]: "module.instant-verify-email",
|
|
1290
|
+
[eventModuleNames.instantVerifyConsent]: "module.instant-verify-consent",
|
|
1291
|
+
[eventModuleNames.forms]: "module.forms",
|
|
1292
|
+
[eventModuleNames.customModule]: "module.custom-module",
|
|
1293
|
+
[eventModuleNames.geolocation]: "module.geolocation",
|
|
1294
|
+
[eventModuleNames.ekyb]: "module.e-kyb",
|
|
1295
|
+
[eventModuleNames.identityReuse]: "module.identity-reuse",
|
|
1296
|
+
[eventModuleNames.antifraud]: "module.antifraud",
|
|
1297
|
+
[eventModuleNames.aeSignature]: "module.ae-signature",
|
|
1298
|
+
[eventModuleNames.qeSignature]: "module.qe-signature",
|
|
1299
|
+
[eventModuleNames.crossDocumentDataMatch]: "module.cross-document-data-match",
|
|
1300
|
+
[eventModuleNames.customFields]: "module.custom-fields",
|
|
1301
|
+
[eventModuleNames.fieldComparison]: "module.field-comparison",
|
|
1302
|
+
[eventModuleNames.personhood]: "module.personhood",
|
|
1303
|
+
[eventModuleNames.watchlistForBusiness]: "module.watchlist-for-business",
|
|
1304
|
+
[eventModuleNames.trustGraph]: "module.trust-graph",
|
|
1305
|
+
[eventModuleNames.certificateIssuance]: "module.certificate-issuance",
|
|
1306
|
+
[eventModuleNames.governmentValidation]: "module.government-validation",
|
|
1307
|
+
[eventModuleNames.documentUpload]: "module.document-upload",
|
|
1308
|
+
[eventModuleNames.home]: "module.home",
|
|
1309
|
+
[eventModuleNames.workflow]: "module.workflow",
|
|
1310
|
+
[eventModuleNames.fiscalQr]: "module.fiscal-qr"
|
|
1311
|
+
};
|
|
1093
1312
|
/**
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1313
|
+
* Raw orchestrator module keys (flow `currentStep`, workflow
|
|
1314
|
+
* `currentNode.moduleKey`) whose kebab fallback would NOT land on the
|
|
1315
|
+
* module's canonical subsystem. Advanceable-error events carry these keys
|
|
1316
|
+
* instead of `eventModuleNames`, so they alias here; keys whose fallback
|
|
1317
|
+
* already matches (`DOCUMENT_CAPTURE`, `COMBINED_CONSENT`, …) or that name a
|
|
1318
|
+
* distinct step (`ADDRESS`, `INE_VALIDATION`, `SECOND_ID`) need no entry.
|
|
1319
|
+
* Snapshot-frozen in tests with the table above.
|
|
1096
1320
|
*/
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1321
|
+
const ORCHESTRATOR_KEY_SUBSYSTEMS = {
|
|
1322
|
+
PHONE: "module.phone-number",
|
|
1323
|
+
EXTERNAL_VERIFICATION: "module.e-kyc",
|
|
1324
|
+
EKYB: "module.e-kyb",
|
|
1325
|
+
SIGNATURE: "module.e-sign",
|
|
1326
|
+
DYNAMIC_FORMS: "module.forms",
|
|
1327
|
+
CURP_VALIDATION: "module.curp",
|
|
1328
|
+
GOVT_VALIDATION_PROVISIONING: "module.government-validation",
|
|
1329
|
+
WATCHLIST_BUSINESS: "module.watchlist-for-business",
|
|
1330
|
+
INCODE_WATCHLIST: "module.custom-watchlist",
|
|
1331
|
+
REDIRECT_TO_MOBILE: "module.qr"
|
|
1332
|
+
};
|
|
1333
|
+
const CUSTOM_SUBSYSTEM_MAX_CHARS = 40;
|
|
1104
1334
|
/**
|
|
1105
|
-
*
|
|
1335
|
+
* Canonical `module.<kebab>` for a raw wire module name. Unknown/integrator-
|
|
1336
|
+
* custom names get a sanitized lowercase fallback (capped) — custom SDK 2
|
|
1337
|
+
* integrations get real per-module detail, not a lumped `other`.
|
|
1106
1338
|
*/
|
|
1107
|
-
function
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1339
|
+
function normalizeSubsystem(rawModule) {
|
|
1340
|
+
if (rawModule === void 0) return "module.unknown";
|
|
1341
|
+
const canonical = MODULE_SUBSYSTEMS[rawModule] ?? ORCHESTRATOR_KEY_SUBSYSTEMS[rawModule];
|
|
1342
|
+
if (canonical !== void 0) return canonical;
|
|
1343
|
+
const sanitized = rawModule.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, CUSTOM_SUBSYSTEM_MAX_CHARS).replace(/-+$/g, "");
|
|
1344
|
+
return `module.${sanitized === "" ? "unknown" : sanitized}`;
|
|
1345
|
+
}
|
|
1346
|
+
const ID_SCAN_MODULES = /* @__PURE__ */ new Set([
|
|
1347
|
+
eventModuleNames.id,
|
|
1348
|
+
eventModuleNames.idCapture,
|
|
1349
|
+
eventModuleNames.idVerification,
|
|
1350
|
+
eventModuleNames.idOcr,
|
|
1351
|
+
eventModuleNames.document,
|
|
1352
|
+
eventModuleNames.documentUpload,
|
|
1353
|
+
eventModuleNames.creditCard,
|
|
1354
|
+
eventModuleNames.creditCardBack,
|
|
1355
|
+
eventModuleNames.curpValidation,
|
|
1356
|
+
"ID_CAPTURE",
|
|
1357
|
+
"TUTORIAL_ID",
|
|
1358
|
+
"SECOND_ID",
|
|
1359
|
+
"DOCUMENT_CAPTURE",
|
|
1360
|
+
"ADDRESS",
|
|
1361
|
+
"CURP_VALIDATION"
|
|
1362
|
+
]);
|
|
1363
|
+
const FACE_SCAN_MODULES = /* @__PURE__ */ new Set([
|
|
1364
|
+
eventModuleNames.selfie,
|
|
1365
|
+
eventModuleNames.faceCapture,
|
|
1366
|
+
eventModuleNames.authentication,
|
|
1367
|
+
eventModuleNames.authFace,
|
|
1368
|
+
eventModuleNames.faceMatch,
|
|
1369
|
+
eventModuleNames.videoSelfie,
|
|
1370
|
+
eventModuleNames.personhood
|
|
1371
|
+
]);
|
|
1372
|
+
const ORCHESTRATION_MODULES = /* @__PURE__ */ new Set([eventModuleNames.home, eventModuleNames.workflow]);
|
|
1114
1373
|
/**
|
|
1115
1374
|
* Coarse category for HTTP-failure entries, from the endpoint path (event
|
|
1116
1375
|
* map row 23: "by endpoint"). A conservative token heuristic — the category
|
|
@@ -1118,36 +1377,220 @@ eventModuleNames.home, eventModuleNames.workflow;
|
|
|
1118
1377
|
* scan bucket.
|
|
1119
1378
|
*/
|
|
1120
1379
|
function categoryForEndpoint(endpoint) {
|
|
1380
|
+
if (/\/omni\/es\//.test(endpoint)) return "other";
|
|
1121
1381
|
if (/face|selfie|video|speech|personhood|authenticate|recordings/.test(endpoint)) return "faceScan";
|
|
1122
|
-
if (/front-id|back-id|second-id|process\/id|images|barcode|ocr|document|curp|cfdi|address-statement/.test(endpoint)) return "idScan";
|
|
1382
|
+
if (/front-id|back-id|second-id|process\/id|images|barcode|ocr|document|curp|cfdi|address-statement|digital-id|device\/wallet/.test(endpoint)) return "idScan";
|
|
1123
1383
|
if (/start|flow|workflow|feature-config|onboarding|finish-status/.test(endpoint)) return "orchestration";
|
|
1124
1384
|
return "other";
|
|
1125
1385
|
}
|
|
1386
|
+
/** Coarse contract category from the RAW `event.module` value (plan rule). */
|
|
1387
|
+
function categoryForModule(rawModule) {
|
|
1388
|
+
if (rawModule === void 0) return "other";
|
|
1389
|
+
if (ID_SCAN_MODULES.has(rawModule)) return "idScan";
|
|
1390
|
+
if (FACE_SCAN_MODULES.has(rawModule)) return "faceScan";
|
|
1391
|
+
if (ORCHESTRATION_MODULES.has(rawModule)) return "orchestration";
|
|
1392
|
+
return "other";
|
|
1393
|
+
}
|
|
1394
|
+
/** `verb key=value` line: undefined values drop their pair entirely. */
|
|
1395
|
+
function formatLine(verb, pairs = []) {
|
|
1396
|
+
const parts = [verb];
|
|
1397
|
+
for (const [key, value] of pairs) if (value !== void 0 && value !== null && value !== "") parts.push(`${key}=${String(value)}`);
|
|
1398
|
+
return parts.join(" ");
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Analytics event → contract log tuple, or `null` for codes the taxonomy
|
|
1402
|
+
* does not cover (deny-by-default: nothing rides into the record unreviewed).
|
|
1403
|
+
* Levels/subsystems/categories per the plan's event map rows 5–6 and 16–20.
|
|
1404
|
+
*/
|
|
1405
|
+
function mapAnalyticsEventToLog(event) {
|
|
1406
|
+
const payload = event.payload ?? {};
|
|
1407
|
+
const subsystem = normalizeSubsystem(event.module);
|
|
1408
|
+
const category = categoryForModule(event.module);
|
|
1409
|
+
switch (event.code) {
|
|
1410
|
+
case "moduleOpened":
|
|
1411
|
+
case "moduleClosed": return {
|
|
1412
|
+
level: "info",
|
|
1413
|
+
subsystem,
|
|
1414
|
+
category,
|
|
1415
|
+
message: formatLine(event.code === "moduleOpened" ? "module opened" : "module closed", [["screen", event.screen]])
|
|
1416
|
+
};
|
|
1417
|
+
case "screenOpened":
|
|
1418
|
+
case "screenClosed": return {
|
|
1419
|
+
level: "debug",
|
|
1420
|
+
subsystem,
|
|
1421
|
+
category,
|
|
1422
|
+
message: formatLine(event.code === "screenOpened" ? "screen opened" : "screen closed", [["screen", event.screen]])
|
|
1423
|
+
};
|
|
1424
|
+
case "errorTriggered": return {
|
|
1425
|
+
level: "error",
|
|
1426
|
+
subsystem,
|
|
1427
|
+
category,
|
|
1428
|
+
message: formatLine("errorTriggered", [
|
|
1429
|
+
["errorName", stringOrUndefined(payload.errorName) ?? "unknown"],
|
|
1430
|
+
["errorCode", stringOrUndefined(payload.errorCode)],
|
|
1431
|
+
["reason", stringOrUndefined(payload.reason)]
|
|
1432
|
+
])
|
|
1433
|
+
};
|
|
1434
|
+
case "captureAttemptFinished": {
|
|
1435
|
+
const logs = stringOrUndefined(payload.logs);
|
|
1436
|
+
return {
|
|
1437
|
+
level: "debug",
|
|
1438
|
+
subsystem,
|
|
1439
|
+
category,
|
|
1440
|
+
message: formatLine("capture attempt finished", [["logsBytes", logs === void 0 ? void 0 : utf8ByteLength(logs)]])
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
case "background":
|
|
1444
|
+
case "foreground": return {
|
|
1445
|
+
level: "debug",
|
|
1446
|
+
subsystem,
|
|
1447
|
+
category,
|
|
1448
|
+
message: `visibility ${event.code}`
|
|
1449
|
+
};
|
|
1450
|
+
case "switchToAutoCaptureFrontId":
|
|
1451
|
+
case "switchToAutoCaptureBackId": return {
|
|
1452
|
+
level: "debug",
|
|
1453
|
+
subsystem,
|
|
1454
|
+
category,
|
|
1455
|
+
message: formatLine("capture mode switched to auto", [["side", event.code === "switchToAutoCaptureBackId" ? "back" : "front"]])
|
|
1456
|
+
};
|
|
1457
|
+
case "experimentAssigned": return {
|
|
1458
|
+
level: "info",
|
|
1459
|
+
subsystem,
|
|
1460
|
+
category,
|
|
1461
|
+
message: formatLine("experiment assigned", [["screen", event.screen], ["fields", Object.keys(payload).filter((key) => key !== "module").length]])
|
|
1462
|
+
};
|
|
1463
|
+
case "elementClicked": return {
|
|
1464
|
+
level: "debug",
|
|
1465
|
+
subsystem,
|
|
1466
|
+
category,
|
|
1467
|
+
message: formatLine("element clicked", [["element", stringOrUndefined(payload.element)], ["action", stringOrUndefined(payload.action)]])
|
|
1468
|
+
};
|
|
1469
|
+
case "browser": {
|
|
1470
|
+
const browser = nameVersion(payload.browser);
|
|
1471
|
+
const os = nameVersion(payload.os);
|
|
1472
|
+
const device = typeof payload.device === "object" && payload.device !== null ? stringOrUndefined(payload.device.type) : void 0;
|
|
1473
|
+
return {
|
|
1474
|
+
level: "info",
|
|
1475
|
+
subsystem: "session",
|
|
1476
|
+
category: "other",
|
|
1477
|
+
message: formatLine("browser info", [
|
|
1478
|
+
["browser", browser],
|
|
1479
|
+
["os", os],
|
|
1480
|
+
["device", device],
|
|
1481
|
+
["webrtc", typeof payload.isWebRtcSupported === "boolean" ? payload.isWebRtcSupported : void 0]
|
|
1482
|
+
])
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
default: return null;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function stringOrUndefined(value) {
|
|
1489
|
+
return typeof value === "string" ? value : void 0;
|
|
1490
|
+
}
|
|
1491
|
+
function nameVersion(value) {
|
|
1492
|
+
if (typeof value !== "object" || value === null) return;
|
|
1493
|
+
const { name, version } = value;
|
|
1494
|
+
const joined = [name, version].filter((part) => typeof part === "string" && part !== "").join(" ");
|
|
1495
|
+
return joined === "" ? void 0 : joined;
|
|
1496
|
+
}
|
|
1126
1497
|
//#endregion
|
|
1127
1498
|
//#region src/internal/trueSight/uploader.ts
|
|
1128
|
-
let
|
|
1499
|
+
let sealerProviderPromise = null;
|
|
1129
1500
|
/**
|
|
1130
|
-
* Loads the browser sealer as a LAZY chunk, once —
|
|
1501
|
+
* Loads the browser sealer as a LAZY chunk, once — it stays off the host
|
|
1131
1502
|
* page's critical path until diagnostics actually need it (first upload or
|
|
1132
|
-
* the service's prewarm).
|
|
1503
|
+
* the service's prewarm). A future sealing scheme swaps in here.
|
|
1504
|
+
*/
|
|
1505
|
+
function loadDiagnosticsEnvelopeProvider() {
|
|
1506
|
+
sealerProviderPromise ??= import("./BrowserDiagnosticsEnvelopeProvider-BQca6KPG.esm.js").then((module) => new module.BrowserDiagnosticsEnvelopeProvider());
|
|
1507
|
+
return sealerProviderPromise;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Seals a record inside the contract limits: the record JSON at or under
|
|
1511
|
+
* `MAX_RECORD_JSON_BYTES` (decompressed) and the serialized envelope at or
|
|
1512
|
+
* under `MAX_ENVELOPE_BYTES`. Over either cap, rebuilds ONCE with the oldest
|
|
1513
|
+
* half of the logs dropped; still over → `null` rather than an invalid
|
|
1514
|
+
* envelope. Rejects only when sealing itself fails.
|
|
1133
1515
|
*/
|
|
1134
|
-
function
|
|
1135
|
-
|
|
1136
|
-
|
|
1516
|
+
async function sealWithinLimits(record, sealer, warn) {
|
|
1517
|
+
const first = await sealMeasured(record, sealer);
|
|
1518
|
+
if (withinLimits(first)) {
|
|
1519
|
+
if (first.envelopeBytes > 9e5) warn(`[TrueSight] envelope ${first.envelopeBytes}B is approaching the 1MB cap`);
|
|
1520
|
+
return first;
|
|
1521
|
+
}
|
|
1522
|
+
const trimmed = {
|
|
1523
|
+
...record,
|
|
1524
|
+
logs: record.logs.slice(Math.floor(record.logs.length / 2))
|
|
1525
|
+
};
|
|
1526
|
+
warn(`[TrueSight] envelope ${first.envelopeBytes}B (record ${first.recordBytes}B) over the cap — rebuilt with the oldest ${record.logs.length - trimmed.logs.length} logs dropped`);
|
|
1527
|
+
const second = await sealMeasured(trimmed, sealer);
|
|
1528
|
+
if (!withinLimits(second)) {
|
|
1529
|
+
warn("[TrueSight] envelope still over the cap after trim — upload dropped");
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
return second;
|
|
1137
1533
|
}
|
|
1138
1534
|
/**
|
|
1139
|
-
* Seals and posts one diagnostics record. Never rejects (fail-open)
|
|
1140
|
-
* guards per contract: warn approaching the
|
|
1141
|
-
* rebuild ONCE with the oldest half of the
|
|
1142
|
-
* the upload entirely rather than send an
|
|
1535
|
+
* Seals and posts one diagnostics record. Never rejects (fail-open); resolves
|
|
1536
|
+
* with how the attempt ended. Size guards per contract: warn approaching the
|
|
1537
|
+
* 1 MB envelope cap; over a cap, rebuild ONCE with the oldest half of the
|
|
1538
|
+
* logs dropped; still over → drop the upload entirely rather than send an
|
|
1539
|
+
* invalid envelope.
|
|
1143
1540
|
*/
|
|
1144
|
-
async function uploadDiagnostics(record, options) {
|
|
1541
|
+
async function uploadDiagnostics(record, options) {
|
|
1542
|
+
const warn = options.warn ?? defaultWarn;
|
|
1543
|
+
let sealed;
|
|
1544
|
+
try {
|
|
1545
|
+
sealed = await sealWithinLimits(record, await (options.loadSealer ?? loadDiagnosticsEnvelopeProvider)(), warn);
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
warn(`[TrueSight] diagnostics upload failed (no retry): ${String(error)}`);
|
|
1548
|
+
return "unsent";
|
|
1549
|
+
}
|
|
1550
|
+
if (sealed === null) return "dropped";
|
|
1551
|
+
if (options.onSealed) try {
|
|
1552
|
+
await options.onSealed(sealed);
|
|
1553
|
+
} catch {}
|
|
1554
|
+
try {
|
|
1555
|
+
const keepalive = options.keepalive === true && sealed.envelopeBytes <= 6e4;
|
|
1556
|
+
const response = await options.post(endpoints.diagnosticsData, sealed.envelope, keepalive ? { keepalive: true } : {});
|
|
1557
|
+
if (isForwardedFalse(response.data)) warn("[TrueSight] proxy accepted the record but did not forward it (forwarded:false)");
|
|
1558
|
+
return response.ok ? "delivered" : "rejected";
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
warn(`[TrueSight] diagnostics upload failed (no retry): ${String(error)}`);
|
|
1561
|
+
return hasHttpStatus(error) ? "rejected" : "unsent";
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
/** An HTTP answer arrived (the client threw for a non-2xx status). */
|
|
1565
|
+
function hasHttpStatus(error) {
|
|
1566
|
+
if (typeof error !== "object" || error === null || !("status" in error)) return false;
|
|
1567
|
+
const { status } = error;
|
|
1568
|
+
return typeof status === "number" && status > 0;
|
|
1569
|
+
}
|
|
1570
|
+
async function sealMeasured(record, sealer) {
|
|
1571
|
+
const json = JSON.stringify(record);
|
|
1572
|
+
const envelope = await sealer.seal(json);
|
|
1573
|
+
return {
|
|
1574
|
+
envelope,
|
|
1575
|
+
envelopeBytes: utf8ByteLength(JSON.stringify(envelope)),
|
|
1576
|
+
recordBytes: utf8ByteLength(json)
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
function withinLimits(measured) {
|
|
1580
|
+
return measured.envelopeBytes <= 1e6 && measured.recordBytes <= 2097152;
|
|
1581
|
+
}
|
|
1582
|
+
function isForwardedFalse(data) {
|
|
1583
|
+
return typeof data === "object" && data !== null && "forwarded" in data && data.forwarded === false;
|
|
1584
|
+
}
|
|
1585
|
+
function defaultWarn() {}
|
|
1145
1586
|
//#endregion
|
|
1146
1587
|
//#region src/internal/trueSight/trueSightService.ts
|
|
1147
1588
|
let active = false;
|
|
1589
|
+
/** The lifecycle's first `initialize` opted out — later calls cannot re-enable. */
|
|
1590
|
+
let optedOut = false;
|
|
1148
1591
|
let remoteKilled = false;
|
|
1149
1592
|
let finalized = false;
|
|
1150
|
-
let
|
|
1593
|
+
let sealerUnsupported = false;
|
|
1151
1594
|
let collectionStarted = false;
|
|
1152
1595
|
let flagResolutionStarted = false;
|
|
1153
1596
|
let lastError = null;
|
|
@@ -1158,15 +1601,26 @@ let options = defaultOptions();
|
|
|
1158
1601
|
let observedManagerKind = null;
|
|
1159
1602
|
let orchestratorConflictLogged = false;
|
|
1160
1603
|
let snapshotRecordId = null;
|
|
1604
|
+
let cachedPendingEnvelope = null;
|
|
1161
1605
|
let snapshotDirty = false;
|
|
1162
1606
|
let captureActive = false;
|
|
1163
1607
|
let rebuildInFlight = false;
|
|
1608
|
+
/**
|
|
1609
|
+
* Bumped by `resetTrueSight()`: an async continuation started under an older
|
|
1610
|
+
* value belongs to a torn-down lifecycle and must not touch this one.
|
|
1611
|
+
*/
|
|
1612
|
+
let lifecycleGeneration = 0;
|
|
1164
1613
|
let sealFailureLogged = false;
|
|
1165
1614
|
let pendingSlotRegistered = false;
|
|
1166
1615
|
let recoveryStarted = false;
|
|
1616
|
+
let bridgeFailureCount = 0;
|
|
1617
|
+
let envSnapshotLogged = false;
|
|
1167
1618
|
let hintTransitionCount = 0;
|
|
1168
1619
|
let hintSuppressedCount = 0;
|
|
1169
1620
|
let lastHintStatus = null;
|
|
1621
|
+
let overflowBatchCount = 0;
|
|
1622
|
+
let lastFlushRecordId = null;
|
|
1623
|
+
let overflowDormant = false;
|
|
1170
1624
|
let overflowFlushInFlightUpTo = null;
|
|
1171
1625
|
let dirtyTimerHandle = null;
|
|
1172
1626
|
let unsubscribePageHide = null;
|
|
@@ -1175,20 +1629,45 @@ function defaultOptions() {
|
|
|
1175
1629
|
fetchFeatures: fetchFeatureConfig,
|
|
1176
1630
|
captureTransport: captureLiveTransport,
|
|
1177
1631
|
upload: uploadDiagnostics,
|
|
1178
|
-
|
|
1632
|
+
loadSealer: loadDiagnosticsEnvelopeProvider
|
|
1179
1633
|
};
|
|
1180
1634
|
}
|
|
1181
1635
|
/**
|
|
1182
|
-
* Boot-time entry, called from `setup()`. Idempotent — the first
|
|
1183
|
-
*
|
|
1184
|
-
* encryption lock); `reset()` → `resetTrueSight()` starts a new one.
|
|
1185
|
-
* `enabled:false`
|
|
1636
|
+
* Boot-time entry, called from `setup()`. Idempotent — the first call decides
|
|
1637
|
+
* for the SDK lifecycle, an opt-out included (like the analytics batcher and
|
|
1638
|
+
* the encryption lock); `reset()` → `resetTrueSight()` starts a new one.
|
|
1639
|
+
* `enabled:false` collects nothing by design — zero subscriptions, zero
|
|
1640
|
+
* state — and holds even when a later `setup()` omits the option (the web
|
|
1641
|
+
* wrapper's `setup()` never forwards it).
|
|
1186
1642
|
*/
|
|
1187
|
-
function initializeTrueSightDiagnostics(serviceOptions) {
|
|
1643
|
+
function initializeTrueSightDiagnostics(serviceOptions) {
|
|
1644
|
+
if (active || optedOut) return;
|
|
1645
|
+
if (!serviceOptions.enabled) {
|
|
1646
|
+
optedOut = true;
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
active = true;
|
|
1650
|
+
options = {
|
|
1651
|
+
...defaultOptions(),
|
|
1652
|
+
...pickDefined(serviceOptions)
|
|
1653
|
+
};
|
|
1654
|
+
buffer = createLogBuffer({ onOverflow: handleBufferOverflow });
|
|
1655
|
+
configureTrueSightMetadataSources({
|
|
1656
|
+
getHasToken: () => getToken() !== null,
|
|
1657
|
+
getE2eEncryptionEnabled: isHttpEncrypted,
|
|
1658
|
+
getWasmProfile: options.getWasmProfile
|
|
1659
|
+
});
|
|
1660
|
+
unsubscribeEvents = subscribeEvent(handleAnalyticsEvent);
|
|
1661
|
+
trueSightLog("debug", "diagnostics", "other", "diagnostics initialized");
|
|
1662
|
+
if (options.pageLifecycle && options.storage) unsubscribePageHide = options.pageLifecycle.onPageHide(handlePageHide);
|
|
1663
|
+
if (options.timer && options.storage) dirtyTimerHandle = options.timer.setInterval(() => {
|
|
1664
|
+
maybeRebuildSnapshot();
|
|
1665
|
+
}, SNAPSHOT_DIRTY_INTERVAL_MS);
|
|
1666
|
+
}
|
|
1188
1667
|
/**
|
|
1189
1668
|
* Session-time entry, called from `initializeSession` once feature config is
|
|
1190
1669
|
* known. Collection is already running by then (buffering starts at
|
|
1191
|
-
* initialize); this resolves the remote flag and prewarms the
|
|
1670
|
+
* initialize); this resolves the remote flag and prewarms the sealer chunk.
|
|
1192
1671
|
* When the host's fetch failed, call with no argument — the service resolves
|
|
1193
1672
|
* the cached `fetchFeatureConfig()` itself (a token is present at this point).
|
|
1194
1673
|
*/
|
|
@@ -1196,9 +1675,10 @@ function startTrueSightCollection(features) {
|
|
|
1196
1675
|
if (!active || remoteKilled || finalized) return;
|
|
1197
1676
|
if (!collectionStarted) {
|
|
1198
1677
|
collectionStarted = true;
|
|
1199
|
-
|
|
1678
|
+
prewarmSealer();
|
|
1200
1679
|
}
|
|
1201
1680
|
if (features) {
|
|
1681
|
+
noteFeatureConfig(features);
|
|
1202
1682
|
if (!flagResolutionStarted) {
|
|
1203
1683
|
flagResolutionStarted = true;
|
|
1204
1684
|
applyFeatureResolution(features, false);
|
|
@@ -1206,6 +1686,7 @@ function startTrueSightCollection(features) {
|
|
|
1206
1686
|
} else if (!flagResolutionStarted) {
|
|
1207
1687
|
flagResolutionStarted = true;
|
|
1208
1688
|
options.fetchFeatures().then((fetched) => {
|
|
1689
|
+
noteFeatureConfig(fetched);
|
|
1209
1690
|
applyFeatureResolution(fetched, false);
|
|
1210
1691
|
}, () => {
|
|
1211
1692
|
applyFeatureResolution(null, true);
|
|
@@ -1218,38 +1699,64 @@ function startTrueSightCollection(features) {
|
|
|
1218
1699
|
* snapshot rebuilds are suppressed — the collector must never compete with
|
|
1219
1700
|
* 30 fps capture. Deactivation flushes the deferred rebuild.
|
|
1220
1701
|
*/
|
|
1221
|
-
function noteTrueSightCaptureActivity(activityActive) {
|
|
1702
|
+
function noteTrueSightCaptureActivity(activityActive) {
|
|
1703
|
+
if (activityActive && !captureActive) resetHintBudget();
|
|
1704
|
+
captureActive = activityActive;
|
|
1705
|
+
if (!activityActive) maybeRebuildSnapshot();
|
|
1706
|
+
}
|
|
1222
1707
|
/**
|
|
1223
1708
|
* Seals and uploads the one final record per session. The upload-relevant
|
|
1224
1709
|
* state — transport, metadata (live token/E2EE getters read NOW), buffered
|
|
1225
1710
|
* logs — is captured SYNCHRONOUSLY before the first await, because `reset()`
|
|
1226
1711
|
* calls `resetApi()` immediately after this returns. Finalize-once: later
|
|
1227
|
-
* terminals and late events are no-ops. Never rejects
|
|
1712
|
+
* terminals and late events are no-ops. Never rejects and never throws:
|
|
1713
|
+
* `reset()` and the flow bridge call it synchronously.
|
|
1228
1714
|
*/
|
|
1229
1715
|
function finalizeTrueSight(outcome, failureReason) {
|
|
1230
1716
|
if (!active || remoteKilled || finalized) return Promise.resolve();
|
|
1231
1717
|
finalized = true;
|
|
1718
|
+
try {
|
|
1719
|
+
return uploadTerminalRecord(outcome, failureReason);
|
|
1720
|
+
} catch {
|
|
1721
|
+
return Promise.resolve();
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
function uploadTerminalRecord(outcome, failureReason) {
|
|
1725
|
+
drainExternalDiagnostics();
|
|
1232
1726
|
trueSightLog("info", "session", "orchestration", `finalizing outcome=${outcome}${failureReason !== void 0 ? ` reason=${failureReason}` : ""}`);
|
|
1233
1727
|
const metadata = buildSessionMetadata();
|
|
1234
1728
|
const logs = (buffer?.snapshot() ?? []).filter((entry) => overflowFlushInFlightUpTo === null || entry.index > overflowFlushInFlightUpTo);
|
|
1235
1729
|
const transport = safeCaptureTransport();
|
|
1236
|
-
if (transport === null ||
|
|
1730
|
+
if (transport === null || sealerUnsupported) return Promise.resolve();
|
|
1237
1731
|
const record = buildDiagnosticsRecord(metadata, logs, outcome, failureReason);
|
|
1238
1732
|
if (record === null) return Promise.resolve();
|
|
1239
|
-
|
|
1733
|
+
cachedPendingEnvelope = null;
|
|
1734
|
+
const storage = options.storage;
|
|
1735
|
+
const { sessionId } = record.session;
|
|
1240
1736
|
return options.upload(record, {
|
|
1241
1737
|
post: transport,
|
|
1242
1738
|
keepalive: true,
|
|
1243
|
-
|
|
1244
|
-
warn: selfHealthWarn
|
|
1739
|
+
loadSealer: options.loadSealer,
|
|
1740
|
+
warn: selfHealthWarn,
|
|
1741
|
+
onSealed: storage ? (sealed) => persistTerminalEnvelope(sessionId, sealed, storage) : void 0
|
|
1742
|
+
}).then((uploadOutcome) => {
|
|
1743
|
+
if (storage && uploadOutcome !== "unsent") clearPending(sessionId, { storage });
|
|
1245
1744
|
});
|
|
1246
1745
|
}
|
|
1746
|
+
async function persistTerminalEnvelope(sessionId, sealed, storage) {
|
|
1747
|
+
await registerPendingSlot(sessionId, { storage });
|
|
1748
|
+
await writePendingEnvelope(sessionId, {
|
|
1749
|
+
v: 1,
|
|
1750
|
+
persistedAt: getTimestamp(),
|
|
1751
|
+
envelope: sealed.envelope
|
|
1752
|
+
}, { storage });
|
|
1753
|
+
}
|
|
1247
1754
|
/**
|
|
1248
1755
|
* Capture guidance-hint transition tap (event map 19b/19c): the moment the
|
|
1249
1756
|
* hint the user sees CHANGES, as the status CODE — never the localized text.
|
|
1250
1757
|
* Consecutive dedupe on top of the source dedupe, plus a per-attempt budget
|
|
1251
1758
|
* (~60, then suppress-with-counter) so a struggling capture can never flood
|
|
1252
|
-
* the buffer. The budget resets when a capture attempt finishes. Level is
|
|
1759
|
+
* the buffer. The budget resets when a capture attempt starts or finishes. Level is
|
|
1253
1760
|
* debug so the eviction fallback drops hints before warnings/errors.
|
|
1254
1761
|
*/
|
|
1255
1762
|
function trueSightHintTransition(subsystem, category, status) {
|
|
@@ -1267,10 +1774,33 @@ function trueSightHintTransition(subsystem, category, status) {
|
|
|
1267
1774
|
}
|
|
1268
1775
|
trueSightLog("debug", subsystem, category, `hint changed status=${status}`);
|
|
1269
1776
|
}
|
|
1777
|
+
function resetHintBudget() {
|
|
1778
|
+
if (hintSuppressedCount > 0) trueSightLog("debug", "diagnostics", "other", `hint transitions suppressed count=${hintSuppressedCount}`);
|
|
1779
|
+
hintTransitionCount = 0;
|
|
1780
|
+
hintSuppressedCount = 0;
|
|
1781
|
+
lastHintStatus = null;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* External drain (ENG-48677 WS2): finalize and every snapshot rebuild call it
|
|
1785
|
+
* synchronously before building a record, so diagnostics buffered outside
|
|
1786
|
+
* TrueSight (the WASM kit's sink tail, or its `getAppLogs` text) land in the
|
|
1787
|
+
* terminal record and in an interrupted one alike. Injected by `setup()` —
|
|
1788
|
+
* same decoupled shape as the observers there.
|
|
1789
|
+
*/
|
|
1790
|
+
let externalDrain = null;
|
|
1791
|
+
function setTrueSightExternalDrain(drain) {
|
|
1792
|
+
externalDrain = drain;
|
|
1793
|
+
}
|
|
1794
|
+
/** Synchronous by contract; best-effort — a broken drain never breaks a build. */
|
|
1795
|
+
function drainExternalDiagnostics() {
|
|
1796
|
+
try {
|
|
1797
|
+
externalDrain?.();
|
|
1798
|
+
} catch {}
|
|
1799
|
+
}
|
|
1270
1800
|
/** Buffer tap for wiring points and self-health lines. No-op when inactive. */
|
|
1271
|
-
function trueSightLog(level, subsystem, category, message) {
|
|
1801
|
+
function trueSightLog(level, subsystem, category, message, timestampMs) {
|
|
1272
1802
|
if (buffer === null) return;
|
|
1273
|
-
buffer.append(level, subsystem, category, message);
|
|
1803
|
+
buffer.append(level, subsystem, category, message, timestampMs);
|
|
1274
1804
|
snapshotDirty = true;
|
|
1275
1805
|
}
|
|
1276
1806
|
/**
|
|
@@ -1289,7 +1819,11 @@ function notifyTrueSightFlowEvent(event, managerKind) {
|
|
|
1289
1819
|
trueSightLog("info", managerKind, "orchestration", "flow.started");
|
|
1290
1820
|
break;
|
|
1291
1821
|
case "flow.ready":
|
|
1292
|
-
if (identityAccepted)
|
|
1822
|
+
if (identityAccepted) noteFlowContext({
|
|
1823
|
+
flowType: managerKind,
|
|
1824
|
+
flowId: managerKind === "flow" ? event.flowId ?? null : null,
|
|
1825
|
+
workflowId: managerKind === "workflow" ? event.flowId ?? null : null
|
|
1826
|
+
});
|
|
1293
1827
|
trueSightLog("info", managerKind, "orchestration", `flow.ready ${managerKind === "workflow" ? "workflowId" : "flowId"}=${event.flowId ?? "unknown"}`);
|
|
1294
1828
|
break;
|
|
1295
1829
|
case "flow.module.started":
|
|
@@ -1300,8 +1834,8 @@ function notifyTrueSightFlowEvent(event, managerKind) {
|
|
|
1300
1834
|
break;
|
|
1301
1835
|
}
|
|
1302
1836
|
case "flow.error":
|
|
1303
|
-
lastError = sanitizeMessage(event.error
|
|
1304
|
-
trueSightLog("error", managerKind, "orchestration", `flow.error message=${event.error
|
|
1837
|
+
lastError = sanitizeMessage(event.error || "unknown");
|
|
1838
|
+
trueSightLog("error", managerKind, "orchestration", `flow.error message=${event.error || "unknown"}`);
|
|
1305
1839
|
break;
|
|
1306
1840
|
case "flow.completed":
|
|
1307
1841
|
trueSightLog("info", managerKind, "orchestration", "flow.completed");
|
|
@@ -1347,6 +1881,8 @@ function stampOrchestratorIdentity(managerKind) {
|
|
|
1347
1881
|
}
|
|
1348
1882
|
return false;
|
|
1349
1883
|
}
|
|
1884
|
+
noteIntegrationSurface(managerKind === "workflow" ? "startWorkflow" : "startFlow");
|
|
1885
|
+
noteFlowContext({ flowType: managerKind });
|
|
1350
1886
|
return true;
|
|
1351
1887
|
}
|
|
1352
1888
|
/** Full teardown, called from `reset()` AFTER finalize captured its snapshot. */
|
|
@@ -1361,28 +1897,63 @@ function resetTrueSight() {
|
|
|
1361
1897
|
}
|
|
1362
1898
|
buffer = null;
|
|
1363
1899
|
active = false;
|
|
1900
|
+
lifecycleGeneration += 1;
|
|
1901
|
+
optedOut = false;
|
|
1364
1902
|
remoteKilled = false;
|
|
1365
1903
|
finalized = false;
|
|
1366
|
-
|
|
1904
|
+
sealerUnsupported = false;
|
|
1367
1905
|
collectionStarted = false;
|
|
1368
1906
|
flagResolutionStarted = false;
|
|
1369
1907
|
lastError = null;
|
|
1370
1908
|
observedManagerKind = null;
|
|
1371
1909
|
orchestratorConflictLogged = false;
|
|
1372
1910
|
snapshotRecordId = null;
|
|
1911
|
+
cachedPendingEnvelope = null;
|
|
1373
1912
|
snapshotDirty = false;
|
|
1374
1913
|
captureActive = false;
|
|
1375
1914
|
rebuildInFlight = false;
|
|
1376
1915
|
sealFailureLogged = false;
|
|
1377
1916
|
pendingSlotRegistered = false;
|
|
1378
1917
|
recoveryStarted = false;
|
|
1918
|
+
bridgeFailureCount = 0;
|
|
1919
|
+
envSnapshotLogged = false;
|
|
1379
1920
|
hintTransitionCount = 0;
|
|
1380
1921
|
hintSuppressedCount = 0;
|
|
1381
1922
|
lastHintStatus = null;
|
|
1923
|
+
overflowBatchCount = 0;
|
|
1924
|
+
lastFlushRecordId = null;
|
|
1925
|
+
overflowDormant = false;
|
|
1382
1926
|
overflowFlushInFlightUpTo = null;
|
|
1927
|
+
externalDrain = null;
|
|
1383
1928
|
options = defaultOptions();
|
|
1384
1929
|
resetTrueSightMetadata();
|
|
1385
1930
|
}
|
|
1931
|
+
function handleAnalyticsEvent(event) {
|
|
1932
|
+
if (finalized) return;
|
|
1933
|
+
try {
|
|
1934
|
+
const mapped = mapAnalyticsEventToLog(event);
|
|
1935
|
+
if (mapped === null) return;
|
|
1936
|
+
trueSightLog(mapped.level, mapped.subsystem, mapped.category, mapped.message);
|
|
1937
|
+
if (mapped.level === "error" || mapped.level === "severe") logEnvironmentSnapshotOnce();
|
|
1938
|
+
if (event.code === "captureAttemptFinished") resetHintBudget();
|
|
1939
|
+
} catch {
|
|
1940
|
+
bridgeFailureCount += 1;
|
|
1941
|
+
if (bridgeFailureCount === 1) trueSightLog("warning", "diagnostics", "other", "bridge mapping failed");
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Environment-snapshot-on-first-error (event map 19d): the "conditions at
|
|
1946
|
+
* the moment of failure" line — logged once per session, right after the
|
|
1947
|
+
* first error-level entry. The snapshot closure is injected (navigator
|
|
1948
|
+
* facts live behind the L0 environment provider).
|
|
1949
|
+
*/
|
|
1950
|
+
function logEnvironmentSnapshotOnce() {
|
|
1951
|
+
if (envSnapshotLogged || options.getEnvironmentSnapshot === void 0) return;
|
|
1952
|
+
envSnapshotLogged = true;
|
|
1953
|
+
try {
|
|
1954
|
+
trueSightLog("info", "session", "other", `environment at first error ${options.getEnvironmentSnapshot()}`);
|
|
1955
|
+
} catch {}
|
|
1956
|
+
}
|
|
1386
1957
|
function applyFeatureResolution(features, unresolvable) {
|
|
1387
1958
|
if (!active || remoteKilled || finalized) return;
|
|
1388
1959
|
const entry = features?.features?.find((candidate) => candidate.feature === "TRUESIGHT_DIAGNOSTICS");
|
|
@@ -1399,6 +1970,70 @@ function remoteKill() {
|
|
|
1399
1970
|
unsubscribeEvents = null;
|
|
1400
1971
|
}
|
|
1401
1972
|
/**
|
|
1973
|
+
* Size-limit overflow flush (plan §1.3, event map 10b): the buffer crossed
|
|
1974
|
+
* ~600 KB mid-session — ship the slice as its OWN contract-valid record
|
|
1975
|
+
* (fresh id, same sessionId, index continuity ties the records together)
|
|
1976
|
+
* and keep logging. **STAGING-ONLY until BE ships `inProgress`/`flushReason`
|
|
1977
|
+
* (hard gate B2)**: outside staging (`testing !== true`) the machinery goes
|
|
1978
|
+
* dormant and the buffer falls back to eviction. Interim outcome marker is
|
|
1979
|
+
* the sanctioned `failure/interrupted` pair; the terminal record supersedes.
|
|
1980
|
+
*/
|
|
1981
|
+
function handleBufferOverflow(slice) {
|
|
1982
|
+
dispatchOverflowSlice(slice).catch(() => {
|
|
1983
|
+
overflowFlushInFlightUpTo = null;
|
|
1984
|
+
goOverflowDormant("overflowFlush dormant reason=failed");
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
async function dispatchOverflowSlice(slice) {
|
|
1988
|
+
if (!active || remoteKilled || finalized || overflowDormant || slice.length === 0) return;
|
|
1989
|
+
const metadata = buildSessionMetadata();
|
|
1990
|
+
if (metadata.testing !== true) {
|
|
1991
|
+
goOverflowDormant("overflowFlush dormant reason=prod");
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (sealerUnsupported) {
|
|
1995
|
+
goOverflowDormant("overflowFlush dormant reason=sealerUnsupported");
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
const transport = safeCaptureTransport();
|
|
1999
|
+
if (transport === null) {
|
|
2000
|
+
goOverflowDormant("overflowFlush dormant reason=noTransport");
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
overflowBatchCount += 1;
|
|
2004
|
+
const record = buildDiagnosticsRecord(metadata, [{
|
|
2005
|
+
index: buffer?.reserveIndex() ?? slice[slice.length - 1].index + 1,
|
|
2006
|
+
timestampMs: getTimestamp(),
|
|
2007
|
+
level: "info",
|
|
2008
|
+
subsystem: "diagnostics",
|
|
2009
|
+
category: "other",
|
|
2010
|
+
message: `sizeLimitFlush batch=${overflowBatchCount} prevRecordId=${lastFlushRecordId ?? "none"}`
|
|
2011
|
+
}, ...slice], "failure", "interrupted");
|
|
2012
|
+
if (record === null) {
|
|
2013
|
+
goOverflowDormant("overflowFlush dormant reason=unbuildable");
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
lastFlushRecordId = record.id;
|
|
2017
|
+
const upToIndex = slice[slice.length - 1].index;
|
|
2018
|
+
overflowFlushInFlightUpTo = upToIndex;
|
|
2019
|
+
await options.upload(record, {
|
|
2020
|
+
post: transport,
|
|
2021
|
+
loadSealer: options.loadSealer,
|
|
2022
|
+
warn: selfHealthWarn
|
|
2023
|
+
});
|
|
2024
|
+
buffer?.markFlushed(upToIndex);
|
|
2025
|
+
overflowFlushInFlightUpTo = null;
|
|
2026
|
+
snapshotDirty = true;
|
|
2027
|
+
trueSightLog("info", "diagnostics", "other", `sizeLimitFlush dispatched batch=${overflowBatchCount} recordId=${record.id} entries=${slice.length}`);
|
|
2028
|
+
}
|
|
2029
|
+
/** B2 fallback: machinery dormant, eviction takes over — logged exactly once. */
|
|
2030
|
+
function goOverflowDormant(message) {
|
|
2031
|
+
if (overflowDormant) return;
|
|
2032
|
+
overflowDormant = true;
|
|
2033
|
+
trueSightLog("warning", "diagnostics", "other", message);
|
|
2034
|
+
buffer?.setOverflowMode("evict");
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
1402
2037
|
* Debounced interrupted-snapshot rebuild (plan §1.8): triggered by module
|
|
1403
2038
|
* boundaries and the dirty-interval tick, suppressed while a capture loop is
|
|
1404
2039
|
* active (C7). Seals the CURRENT state as a `failure/interrupted` record
|
|
@@ -1406,7 +2041,17 @@ function remoteKill() {
|
|
|
1406
2041
|
* handler writes only the cache — it never builds), and persists it.
|
|
1407
2042
|
*/
|
|
1408
2043
|
function maybeRebuildSnapshot() {
|
|
1409
|
-
|
|
2044
|
+
try {
|
|
2045
|
+
rebuildSnapshotIfDue();
|
|
2046
|
+
} catch {
|
|
2047
|
+
rebuildInFlight = false;
|
|
2048
|
+
snapshotDirty = true;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
function rebuildSnapshotIfDue() {
|
|
2052
|
+
if (!active || remoteKilled || finalized || sealerUnsupported || captureActive || rebuildInFlight || options.storage === void 0) return;
|
|
2053
|
+
drainExternalDiagnostics();
|
|
2054
|
+
if (!snapshotDirty) return;
|
|
1410
2055
|
const metadata = buildSessionMetadata();
|
|
1411
2056
|
if (!metadata.sessionId) return;
|
|
1412
2057
|
const storage = options.storage;
|
|
@@ -1416,10 +2061,16 @@ function maybeRebuildSnapshot() {
|
|
|
1416
2061
|
if (record === null) return;
|
|
1417
2062
|
snapshotDirty = false;
|
|
1418
2063
|
rebuildInFlight = true;
|
|
1419
|
-
|
|
2064
|
+
const generation = lifecycleGeneration;
|
|
2065
|
+
const isStale = () => generation !== lifecycleGeneration || finalized;
|
|
2066
|
+
options.loadSealer().then((sealer) => sealWithinLimits(record, sealer, () => {})).then(async (sealed) => {
|
|
2067
|
+
if (sealed === null || isStale()) return;
|
|
2068
|
+
const envelope = sealed.envelope;
|
|
2069
|
+
cachedPendingEnvelope = envelope;
|
|
1420
2070
|
if (!pendingSlotRegistered) {
|
|
1421
2071
|
pendingSlotRegistered = true;
|
|
1422
2072
|
await registerPendingSlot(sessionId, { storage });
|
|
2073
|
+
if (isStale()) return;
|
|
1423
2074
|
}
|
|
1424
2075
|
await writePendingEnvelope(sessionId, {
|
|
1425
2076
|
v: 1,
|
|
@@ -1427,16 +2078,33 @@ function maybeRebuildSnapshot() {
|
|
|
1427
2078
|
envelope
|
|
1428
2079
|
}, { storage });
|
|
1429
2080
|
}).catch(() => {
|
|
2081
|
+
if (generation !== lifecycleGeneration) return;
|
|
1430
2082
|
snapshotDirty = true;
|
|
1431
2083
|
if (!sealFailureLogged) {
|
|
1432
2084
|
sealFailureLogged = true;
|
|
1433
2085
|
trueSightLog("warning", "diagnostics", "other", "snapshot seal failed persistDisabled=false");
|
|
1434
2086
|
}
|
|
1435
2087
|
}).finally(() => {
|
|
1436
|
-
rebuildInFlight = false;
|
|
2088
|
+
if (generation === lifecycleGeneration) rebuildInFlight = false;
|
|
1437
2089
|
});
|
|
1438
2090
|
}
|
|
1439
2091
|
/**
|
|
2092
|
+
* Page-hide = persist only, NEVER send (locked decision): one synchronous-
|
|
2093
|
+
* effective write of the freshest pre-sealed snapshot. Building or posting
|
|
2094
|
+
* here is forbidden — a hidden tab is frequently a user who returns.
|
|
2095
|
+
*/
|
|
2096
|
+
function handlePageHide() {
|
|
2097
|
+
if (!active || remoteKilled || finalized || cachedPendingEnvelope === null || options.storage === void 0) return;
|
|
2098
|
+
const sessionId = buildSessionMetadata().sessionId;
|
|
2099
|
+
if (!sessionId) return;
|
|
2100
|
+
writePendingEnvelope(sessionId, {
|
|
2101
|
+
v: 1,
|
|
2102
|
+
persistedAt: getTimestamp(),
|
|
2103
|
+
envelope: cachedPendingEnvelope
|
|
2104
|
+
}, { storage: options.storage });
|
|
2105
|
+
trueSightLog("debug", "diagnostics", "other", "snapshot persisted on pagehide");
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
1440
2108
|
* Next-load recovery off the first-token-observed hook (A1) — runs once per
|
|
1441
2109
|
* lifecycle, fire-and-forget, and reports the A5 loss counters as a
|
|
1442
2110
|
* self-health line when there was anything to report.
|
|
@@ -1455,21 +2123,21 @@ function kickRecoveryOnce() {
|
|
|
1455
2123
|
});
|
|
1456
2124
|
}
|
|
1457
2125
|
/**
|
|
1458
|
-
* Resolves
|
|
2126
|
+
* Resolves sealer support ahead of finalize (and warms the lazy chunk so the
|
|
1459
2127
|
* terminal path never pays the import). Unsupported → collect-only mode with
|
|
1460
2128
|
* exactly one self-health warning.
|
|
1461
2129
|
*/
|
|
1462
|
-
function
|
|
1463
|
-
options.
|
|
1464
|
-
if (!provider.isSupported())
|
|
2130
|
+
function prewarmSealer() {
|
|
2131
|
+
options.loadSealer().then((provider) => {
|
|
2132
|
+
if (!provider.isSupported() || !isRandomUUIDAvailable()) markSealerUnsupported();
|
|
1465
2133
|
}, () => {
|
|
1466
|
-
|
|
2134
|
+
markSealerUnsupported();
|
|
1467
2135
|
});
|
|
1468
2136
|
}
|
|
1469
|
-
function
|
|
1470
|
-
if (
|
|
1471
|
-
|
|
1472
|
-
trueSightLog("warning", "diagnostics", "other", "
|
|
2137
|
+
function markSealerUnsupported() {
|
|
2138
|
+
if (sealerUnsupported) return;
|
|
2139
|
+
sealerUnsupported = true;
|
|
2140
|
+
trueSightLog("warning", "diagnostics", "other", "sealer unsupported collectOnly=true");
|
|
1473
2141
|
}
|
|
1474
2142
|
/**
|
|
1475
2143
|
* Default transport capture: a closure over the live client + token AS OF
|
|
@@ -1501,6 +2169,20 @@ function selfHealthWarn(message) {
|
|
|
1501
2169
|
if (isTrueSightTesting()) console.warn(message);
|
|
1502
2170
|
trueSightLog("warning", "diagnostics", "other", message);
|
|
1503
2171
|
}
|
|
2172
|
+
/** Spread-safe subset: injected seams only, `undefined` values dropped. */
|
|
2173
|
+
function pickDefined(serviceOptions) {
|
|
2174
|
+
const picked = {};
|
|
2175
|
+
if (serviceOptions.fetchFeatures) picked.fetchFeatures = serviceOptions.fetchFeatures;
|
|
2176
|
+
if (serviceOptions.captureTransport) picked.captureTransport = serviceOptions.captureTransport;
|
|
2177
|
+
if (serviceOptions.upload) picked.upload = serviceOptions.upload;
|
|
2178
|
+
if (serviceOptions.loadSealer) picked.loadSealer = serviceOptions.loadSealer;
|
|
2179
|
+
if (serviceOptions.pageLifecycle) picked.pageLifecycle = serviceOptions.pageLifecycle;
|
|
2180
|
+
if (serviceOptions.timer) picked.timer = serviceOptions.timer;
|
|
2181
|
+
if (serviceOptions.storage) picked.storage = serviceOptions.storage;
|
|
2182
|
+
if (serviceOptions.getWasmProfile) picked.getWasmProfile = serviceOptions.getWasmProfile;
|
|
2183
|
+
if (serviceOptions.getEnvironmentSnapshot) picked.getEnvironmentSnapshot = serviceOptions.getEnvironmentSnapshot;
|
|
2184
|
+
return picked;
|
|
2185
|
+
}
|
|
1504
2186
|
/** ` index=i total=n` suffix when the flow event carries module positioning. */
|
|
1505
2187
|
function modulePosition(payload) {
|
|
1506
2188
|
if (typeof payload !== "object" || payload === null) return "";
|
|
@@ -1508,4 +2190,4 @@ function modulePosition(payload) {
|
|
|
1508
2190
|
return `${typeof moduleIndex === "number" ? ` index=${moduleIndex}` : ""}${typeof totalModules === "number" ? ` total=${totalModules}` : ""}`;
|
|
1509
2191
|
}
|
|
1510
2192
|
//#endregion
|
|
1511
|
-
export {
|
|
2193
|
+
export { noteTokenActivated as C, noteTestingSignals as S, isFeatureEnabled as _, notifyTrueSightManagerStopped as a, noteEnvironment as b, startTrueSightCollection as c, categoryForEndpoint as d, getStoredHttpConfig as f, fetchFeatureConfig as g, upgradeToWasmHttpClient as h, notifyTrueSightFlowEvent as i, trueSightHintTransition as l, resetHttpClientState as m, initializeTrueSightDiagnostics as n, resetTrueSight as o, installFreshHttpClient as p, noteTrueSightCaptureActivity as r, setTrueSightExternalDrain as s, finalizeTrueSight as t, trueSightLog as u, resetFeatureConfigCache as v, noteSessionCreated as x, isTrueSightTesting as y };
|