@incodetech/core 2.4.0-rc.0 → 2.4.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseWasmProvider-BL9wIRnN.d.ts → BaseWasmProvider-Bq3OHo7P.d.ts} +2 -2
- package/dist/BrowserDiagnosticsEnvelopeProvider-BQca6KPG.esm.js +43 -0
- package/dist/{FaceDetectionProvider-BjTer9h3.esm.js → FaceDetectionProvider-DSWxnrS3.esm.js} +10 -11
- package/dist/{IIdCaptureCapability-DBsv9-7M.d.ts → IIdCaptureCapability-CGcEt2Cw.d.ts} +2 -2
- package/dist/{IWasmUtilCapability-CPX0RGdQ.d.ts → IWasmUtilCapability-GF2n7sfm.d.ts} +2 -2
- package/dist/{StreamCanvasCapture-DVmzJux5.d.ts → StreamCanvasCapture-7JRf-2ck.d.ts} +2 -2
- package/dist/{WasmUtilProvider-CIdnE7Fx.esm.js → WasmUtilProvider-DtJuBdt-.esm.js} +18 -5
- package/dist/{addressSearch-BiYZjvp3.esm.js → addressSearch-DPjIKJrX.esm.js} +1 -2
- package/dist/ae-signature.esm.js +1 -1
- package/dist/antifraud.esm.js +2 -2
- package/dist/{antifraudManager-HL08jeDX.esm.js → antifraudManager-CaweCYnY.esm.js} +2 -2
- package/dist/{antifraudStateMachine-DoJVzblM.esm.js → antifraudStateMachine-DRl0UVyA.esm.js} +1 -2
- package/dist/{endpoints-DHzlkQNe.esm.js → api-Cz90EHyu.esm.js} +149 -1
- package/dist/{apiError-DcSWedC6.esm.js → apiError-u5fRJruc.esm.js} +1 -1
- package/dist/authentication.d.ts +3 -3
- package/dist/authentication.esm.js +2 -2
- package/dist/{authenticationManager-CANv5-W5.d.ts → authenticationManager-BIZC55Bi.d.ts} +1 -1
- package/dist/{authenticationManager-SaiAxqvS.esm.js → authenticationManager-CHQ3a8M3.esm.js} +5 -5
- package/dist/{authenticationStateMachine-CHSxmV7z.esm.js → authenticationStateMachine-C3poykXk.esm.js} +3 -3
- package/dist/avatar.d.ts +1 -1
- package/dist/camera.esm.js +1 -1
- package/dist/certificate-issuance.esm.js +2 -2
- package/dist/{certificateIssuanceManager-CMg4SgL4.esm.js → certificateIssuanceManager-BfeMv2PU.esm.js} +2 -2
- package/dist/{certificateIssuanceStateMachine-Cs64QmYH.esm.js → certificateIssuanceStateMachine-Frgjbjw8.esm.js} +1 -2
- package/dist/consent.d.ts +1 -1
- package/dist/consent.esm.js +2 -2
- package/dist/{consentManager-DgzWQpAR.d.ts → consentManager-Ca8FHlC4.d.ts} +1 -1
- package/dist/{consentManager-CjrxY737.esm.js → consentManager-CvEu8h_W.esm.js} +2 -2
- package/dist/{consentStateMachine-BCj5Jb3g.esm.js → consentStateMachine-DJTgN_-1.esm.js} +1 -2
- package/dist/{createCameraCycleOwnership-kdlhkmmY.esm.js → createCameraCycleOwnership-Dj_LHUvN.esm.js} +2 -3
- package/dist/{createFaceAvatar-DXgmQmQy.d.ts → createFaceAvatar-lYX0VTYs.d.ts} +1 -1
- package/dist/cross-document-data-match.esm.js +1 -1
- package/dist/{crossDocumentDataMatchManager-BHnGRsto.esm.js → crossDocumentDataMatchManager-Cu141CE8.esm.js} +2 -3
- package/dist/curp-validation.d.ts +1 -1
- package/dist/curp-validation.esm.js +2 -2
- package/dist/{curpValidationManager-DBNIn8BF.esm.js → curpValidationManager-B-z9q5V3.esm.js} +2 -2
- package/dist/{curpValidationManager-B-1EZbP_.d.ts → curpValidationManager-C9sqoh9n.d.ts} +1 -1
- package/dist/{curpValidationStateMachine-DAxImy8p.esm.js → curpValidationStateMachine-vDEFLMPw.esm.js} +1 -2
- package/dist/custom-fields.esm.js +1 -1
- package/dist/custom-watchlist.esm.js +2 -2
- package/dist/{customFieldsManager-BV6UyLFV.esm.js → customFieldsManager-D1ex2cFM.esm.js} +2 -3
- package/dist/{customWatchlistManager-DML0UPzf.esm.js → customWatchlistManager-CTI-SGk0.esm.js} +2 -2
- package/dist/{customWatchlistStateMachine-D9gI5uu7.esm.js → customWatchlistStateMachine-hmRd0f2I.esm.js} +1 -2
- package/dist/{deepsightLoader-DvfAYnzg.esm.js → deepsightLoader-BgGxliJ0.esm.js} +6 -6
- package/dist/{deepsightService-DLGO9yv1.esm.js → deepsightService-ty5dU0LC.esm.js} +3 -3
- package/dist/document-capture.esm.js +2 -2
- package/dist/{documentCaptureManager-C9XUnYU6.esm.js → documentCaptureManager-DUJwpKC5.esm.js} +2 -2
- package/dist/{documentCaptureStateMachine-BHDsCfhL.esm.js → documentCaptureStateMachine-B8i7GnLh.esm.js} +3 -4
- package/dist/dynamic-forms.esm.js +2 -2
- package/dist/{dynamicFormsManager-5uIXobxi.esm.js → dynamicFormsManager-jtJpRLz9.esm.js} +2 -2
- package/dist/{dynamicFormsStateMachine-DQmok084.esm.js → dynamicFormsStateMachine-D20Fh6BI.esm.js} +2 -3
- package/dist/ekyb.d.ts +1 -1
- package/dist/ekyb.esm.js +3 -3
- package/dist/{ekybManager-CazShiQz.esm.js → ekybManager-CaTOF46q.esm.js} +2 -2
- package/dist/{ekybStateMachine-hstiCNmW.esm.js → ekybStateMachine-BcJtiQRv.esm.js} +3 -4
- package/dist/ekyc.esm.js +2 -2
- package/dist/{ekycManager-BdJRUsz0.esm.js → ekycManager-BO-Lui3t.esm.js} +2 -2
- package/dist/{ekycStateMachine-x6Klot8g.esm.js → ekycStateMachine-CDnI_NRq.esm.js} +2 -3
- package/dist/electronic-signature.esm.js +1 -1
- package/dist/{electronicSignatureManager-Bc8spG8p.esm.js → electronicSignatureManager-CoeuiyX4.esm.js} +3 -4
- package/dist/email.esm.js +2 -2
- package/dist/{emailManager-CtlKRRaR.esm.js → emailManager-ButFktRm.esm.js} +2 -2
- package/dist/{emailStateMachine-5Neq3rE5.esm.js → emailStateMachine-BXtlC0NV.esm.js} +1 -2
- package/dist/{events-CAgFE-k3.esm.js → events-Du2A4-Qr.esm.js} +184 -1
- package/dist/events.esm.js +1 -1
- package/dist/extensibility.d.ts +13 -13
- package/dist/extensibility.esm.js +34 -34
- package/dist/face-match.esm.js +2 -2
- package/dist/{faceCaptureManagerFactory-jKQ2QtY-.d.ts → faceCaptureManagerFactory-1hzeS_rB.d.ts} +7 -7
- package/dist/{faceCaptureManagerFactory-uFm1AcaY.esm.js → faceCaptureManagerFactory-D5hz8P8_.esm.js} +4 -4
- package/dist/{faceCaptureSetup-Cu2wabzo.esm.js → faceCaptureSetup-aZ7IJflJ.esm.js} +15 -11
- package/dist/{faceMatchManager-9Nj9aPci.esm.js → faceMatchManager-DWFUQjMQ.esm.js} +2 -2
- package/dist/{faceMatchStateMachine-B9ZUnlyU.esm.js → faceMatchStateMachine-CUCvlydD.esm.js} +1 -2
- package/dist/{feature-management-BTbYruNe.esm.js → feature-management-D7SnNua4.esm.js} +1 -1
- package/dist/feature-management.esm.js +1 -1
- package/dist/field-comparison.esm.js +1 -1
- package/dist/{fieldComparisonManager-Dk38hXZP.esm.js → fieldComparisonManager-v3knTYV_.esm.js} +2 -3
- package/dist/fiscal-qr.esm.js +1 -1
- package/dist/{fiscalQrManager-DmBkQfs0.esm.js → fiscalQrManager-Dt9fHG7h.esm.js} +3 -3
- package/dist/flow-events.d.ts +1 -1
- package/dist/flow.d.ts +6 -6
- package/dist/flow.esm.js +4 -4
- package/dist/{flowCompletionService-xnn_SlJU.esm.js → flowCompletionService-D7le3leY.esm.js} +2 -3
- package/dist/{flowServices-DA64AEZC.esm.js → flowServices-BnHYS8ez.esm.js} +6 -7
- package/dist/geolocation.esm.js +2 -2
- package/dist/{geolocationManager-CqNLY45e.esm.js → geolocationManager-CEi7S-bI.esm.js} +2 -2
- package/dist/{geolocationStateMachine-D-cQi7ar.esm.js → geolocationStateMachine-BJsZ4RyH.esm.js} +2 -3
- package/dist/government-validation.esm.js +2 -2
- package/dist/{governmentValidationManager-CP0SiOOe.esm.js → governmentValidationManager-BtXWHICu.esm.js} +2 -2
- package/dist/{governmentValidationStateMachine-C3YQ8yCZ.esm.js → governmentValidationStateMachine-COL-Vs48.esm.js} +1 -2
- package/dist/home.d.ts +1 -1
- package/dist/home.esm.js +1 -1
- 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 +3 -3
- package/dist/id-selfie-flow.esm.js +9 -9
- package/dist/id-verification.esm.js +2 -2
- package/dist/id.d.ts +3 -3
- package/dist/id.esm.js +3 -3
- package/dist/{idCaptureDefaults-CBg73RnV.esm.js → idCaptureDefaults-BGsG2OAs.esm.js} +3 -4
- package/dist/{idCaptureManager-BY97SRCV.d.ts → idCaptureManager-0M02hSuC.d.ts} +7 -7
- package/dist/{idCaptureManager-D4V8vXtY.esm.js → idCaptureManager-Bbt7x3xN.esm.js} +7 -7
- package/dist/{idCaptureServices-DJ33k3-p.esm.js → idCaptureServices-t3zj66lr.esm.js} +6 -7
- package/dist/{idCaptureStateMachine-PJt0yzom.esm.js → idCaptureStateMachine-e56glzRH.esm.js} +23 -20
- package/dist/{idOcrManager-DxoCesYP.esm.js → idOcrManager-D3-HNMXP.esm.js} +2 -2
- package/dist/{idOcrManager-BTdHlJ9D.d.ts → idOcrManager-DT3kPIB3.d.ts} +1 -1
- package/dist/{idOcrStateMachine-kPn7DB2I.esm.js → idOcrStateMachine-DyENX6Ki.esm.js} +1 -2
- package/dist/{idVerificationManager-CDhRUU5n.esm.js → idVerificationManager-iWz7Avkr.esm.js} +2 -2
- package/dist/{idVerificationStateMachine-0HTVVxse.esm.js → idVerificationStateMachine-CL1kUdBe.esm.js} +1 -2
- package/dist/identity-reuse.esm.js +2 -2
- package/dist/{identityReuseManager-BleUi6TK.esm.js → identityReuseManager-CD1ucp1d.esm.js} +2 -2
- package/dist/{identityReuseStateMachine-BrBUpfn5.esm.js → identityReuseStateMachine-DfV81wQC.esm.js} +1 -2
- package/dist/{index-DfEuu-Ew.d.ts → index-CKQ9QBZP.d.ts} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/{invokeOnCaptureCallback-BW7V3vub.esm.js → invokeOnCaptureCallback-DYrkOXO6.esm.js} +1 -1
- package/dist/mandatory-consent.d.ts +2 -2
- package/dist/mandatory-consent.esm.js +2 -2
- package/dist/{mandatoryConsentManager-CX_Dyht7.esm.js → mandatoryConsentManager-Cwqx1nK7.esm.js} +2 -2
- package/dist/{mandatoryConsentManager-TkX57U6f.d.ts → mandatoryConsentManager-qYojnZYg.d.ts} +2 -2
- package/dist/{mandatoryConsentStateMachine-CoaSB7P7.esm.js → mandatoryConsentStateMachine-pXWqAHo_.esm.js} +1 -2
- package/dist/{multipartRecordingService-DbeQpXdU.esm.js → multipartRecordingService-CLwrF5wD.esm.js} +1 -2
- package/dist/{permissionGuards-Bf4LbHjC.esm.js → permissionGuards-gcy3U0SL.esm.js} +1 -1
- package/dist/{permissionServices-d2UW0r_L.esm.js → permissionServices-Ca-tSNhT.esm.js} +1 -1
- package/dist/phone.esm.js +2 -2
- package/dist/{phoneManager-BGBcpqsk.esm.js → phoneManager-KKUq-Vgo.esm.js} +2 -2
- package/dist/{phoneStateMachine-CEm2y7yG.esm.js → phoneStateMachine-D2QWS2cu.esm.js} +1 -2
- package/dist/qe-signature.esm.js +1 -1
- package/dist/redirect-to-mobile.esm.js +2 -2
- package/dist/{redirectToMobileManager-B1yQHFLu.esm.js → redirectToMobileManager-uHW3dyuQ.esm.js} +2 -2
- package/dist/{redirectToMobileStateMachine--Sr9NEVT.esm.js → redirectToMobileStateMachine-b-H-ipGM.esm.js} +1 -2
- package/dist/{runChildModule-vdxQMCmO.esm.js → runChildModule-BMlIz7Hc.esm.js} +2 -2
- package/dist/{sdkVersion-F5YZbjip.esm.js → sdkVersion-DnWdXHbH.esm.js} +1 -1
- package/dist/selfie.d.ts +4 -4
- package/dist/selfie.esm.js +10 -10
- package/dist/{selfieErrorUtils-D-tsgEuX.esm.js → selfieErrorUtils-BmKZD7uF.esm.js} +1 -1
- package/dist/{selfieManager-1J0IEyFq.d.ts → selfieManager-BJdNu_nd.d.ts} +1 -1
- package/dist/{selfieManager-26pENXxw.esm.js → selfieManager-ByEF2GBP.esm.js} +5 -5
- package/dist/{selfieStateMachine-BhfKsl3I.esm.js → selfieStateMachine-51U3Eg7M.esm.js} +3 -3
- package/dist/{session-Bu7djCsj.esm.js → session-DeVZisMV.esm.js} +4 -5
- package/dist/session.d.ts +1 -1
- package/dist/session.esm.js +5 -5
- package/dist/{sessionInitializer-jxbfCnE-.esm.js → sessionInitializer-CAkf7oYe.esm.js} +7 -8
- package/dist/{setup-C38QkrSP.esm.js → setup-C-4p0WsB.esm.js} +93 -198
- package/dist/{setup-lgkQ8dJW.d.ts → setup-YPKTUCY5.d.ts} +23 -5
- package/dist/signature.esm.js +2 -2
- package/dist/{signatureManager-qqXyH93e.esm.js → signatureManager-f_dPAGXn.esm.js} +2 -2
- package/dist/{signatureStateMachine-BkRSrkKp.esm.js → signatureStateMachine-C-nUq_oq.esm.js} +1 -2
- package/dist/{stats-DmG_tkGk.esm.js → stats-BEvexnKC.esm.js} +1 -2
- package/dist/stats.esm.js +1 -1
- package/dist/{trueSightService-BWgRVGIT.esm.js → trueSightService-D-whOVP1.esm.js} +914 -225
- package/dist/trust-graph.esm.js +1 -1
- package/dist/{types-BGXxRwQM.d.ts → types-B6HsSzpk.d.ts} +1 -1
- package/dist/{types-CyPUIpzf.d.ts → types-BAxn7MpY.d.ts} +5 -5
- package/dist/{types-DTYr9pCB.d.ts → types-CK8Jbjkp.d.ts} +1 -1
- package/dist/{types-BvFmYX5c.d.ts → types-Ckir_BEs.d.ts} +1 -1
- package/dist/{types-zpO4fU6a.d.ts → types-yPH8lAH-.d.ts} +3 -3
- package/dist/types-zOgZqMni.d.ts +6 -0
- package/dist/video-selfie.d.ts +3 -3
- package/dist/video-selfie.esm.js +6 -7
- package/dist/{videoSelfieCameraWarmer-C366RKqu.esm.js → videoSelfieCameraWarmer-BBQSGW1l.esm.js} +7 -8
- package/dist/{videoSelfieStepDetector-C5QHOvWb.d.ts → videoSelfieStepDetector-CGsTltqB.d.ts} +4 -4
- package/dist/{warmup-DMtbyb2l.esm.js → warmup-BElYWRh7.esm.js} +322 -3
- package/dist/{warmup-BUPPRAA1.d.ts → warmup-BO57syF6.d.ts} +1 -0
- package/dist/wasm.d.ts +2 -2
- package/dist/wasm.esm.js +3 -4
- package/dist/{wasmDsFlagSync-BXCGqP48.esm.js → wasmDsFlagSync-CYj5LL9z.esm.js} +1 -1
- package/dist/{wasmWebClient-kbYZ2sxw.esm.js → wasmWebClient-BjZw_7OB.esm.js} +315 -17
- package/dist/watchlist-for-business.esm.js +2 -2
- package/dist/watchlist.esm.js +2 -2
- package/dist/{watchlistForBusinessManager-BvF4jK3S.esm.js → watchlistForBusinessManager-CZZOFkGl.esm.js} +2 -2
- package/dist/{watchlistForBusinessStateMachine-121XK-Et.esm.js → watchlistForBusinessStateMachine-B66E7jud.esm.js} +1 -2
- package/dist/{watchlistManager-CNWEOYSq.esm.js → watchlistManager-DXtX94SN.esm.js} +2 -2
- package/dist/{watchlistStateMachine-Dgfm42w2.esm.js → watchlistStateMachine-McALifag.esm.js} +1 -2
- package/dist/workflow.d.ts +1 -1
- package/dist/workflow.esm.js +35 -36
- package/package.json +2 -2
- package/dist/BrowserDiagnosticsCryptoProvider-BuzqvbhN.esm.js +0 -77
- package/dist/api-Bx3gKW_y.esm.js +0 -129
- package/dist/types-C71Zo6vl.d.ts +0 -6
|
@@ -1,153 +1,15 @@
|
|
|
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 { n as eventModuleNames } from "./events-
|
|
4
|
-
import { i as isApiConfigured, n as getApi, o as setClient, r as getToken, t as api } from "./api-
|
|
5
|
-
import { t as
|
|
6
|
-
import { t as SDK_VERSION } from "./sdkVersion-F5YZbjip.esm.js";
|
|
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-DnWdXHbH.esm.js";
|
|
7
6
|
import { n as reportHttpError, t as HttpRequestError } from "./HttpRequestError-Bh0pNGSC.esm.js";
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
/** The WASM version bundled and tested with this SDK release. */
|
|
11
|
-
const DEFAULT_WASM_VERSION = "v2.14.50-post1";
|
|
12
|
-
const PATH_KEYS = [
|
|
13
|
-
"wasmPath",
|
|
14
|
-
"wasmSimdPath",
|
|
15
|
-
"glueCodePath",
|
|
16
|
-
"glueCodeSimdPath",
|
|
17
|
-
"modelsBasePath"
|
|
18
|
-
];
|
|
19
|
-
function isBlank(value) {
|
|
20
|
-
return value.trim() === "";
|
|
21
|
-
}
|
|
22
|
-
function hasDisallowedControlChars(value) {
|
|
23
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
24
|
-
const code = value.charCodeAt(i);
|
|
25
|
-
if (code < 32 || code === 127) return true;
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
function isMalformedPath(value) {
|
|
30
|
-
const trimmed = value.trim();
|
|
31
|
-
if (trimmed === "") return false;
|
|
32
|
-
if (hasDisallowedControlChars(trimmed)) return true;
|
|
33
|
-
if (/^https?:\/\//i.test(trimmed)) try {
|
|
34
|
-
new URL(trimmed);
|
|
35
|
-
return false;
|
|
36
|
-
} catch {
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
function warnInvalidWasmPath(field, reason) {
|
|
42
|
-
if (reason === "empty") {
|
|
43
|
-
console.warn(`[Incode SDK] wasm.${field} is empty. Falling back to CDN default.`);
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
console.warn(`[Incode SDK] wasm.${field} appears malformed. Falling back to CDN default.`);
|
|
47
|
-
}
|
|
48
|
-
function warnMalformedWasmPath(field) {
|
|
49
|
-
console.warn(`[Incode SDK] wasm.${field} appears malformed. Check the URL or path.`);
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Validates merged WASM path strings. When overrides contained blank path
|
|
53
|
-
* values, callers should have already fallen back to defaults before this runs.
|
|
54
|
-
*/
|
|
55
|
-
function validateWasmPaths(config) {
|
|
56
|
-
for (const key of PATH_KEYS) {
|
|
57
|
-
const value = config[key];
|
|
58
|
-
if (typeof value === "string" && isMalformedPath(value)) warnMalformedWasmPath(key);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function pickPath(key, overrides, defaultValue) {
|
|
62
|
-
if (!overrides || !(key in overrides)) return defaultValue;
|
|
63
|
-
const raw = overrides[key];
|
|
64
|
-
if (raw === void 0) return defaultValue;
|
|
65
|
-
if (typeof raw !== "string") return defaultValue;
|
|
66
|
-
if (isBlank(raw)) {
|
|
67
|
-
warnInvalidWasmPath(key, "empty");
|
|
68
|
-
return defaultValue;
|
|
69
|
-
}
|
|
70
|
-
if (isMalformedPath(raw)) {
|
|
71
|
-
warnInvalidWasmPath(key, "malformed");
|
|
72
|
-
return defaultValue;
|
|
73
|
-
}
|
|
74
|
-
return raw.trim();
|
|
75
|
-
}
|
|
76
|
-
function pickSource(key, overrides, defaultValue) {
|
|
77
|
-
if (!(key in overrides) || overrides[key] === void 0) return defaultValue;
|
|
78
|
-
const source = overrides[key];
|
|
79
|
-
if (typeof source !== "string") return source;
|
|
80
|
-
return pickPath(key, overrides, String(defaultValue));
|
|
81
|
-
}
|
|
82
|
-
function resolveBasePath(overrides) {
|
|
83
|
-
const defaultValue = `${WASM_CDN_BASE_URL}/${DEFAULT_WASM_VERSION}`;
|
|
84
|
-
if (!("basePath" in overrides) || overrides.basePath === void 0) return defaultValue;
|
|
85
|
-
const basePath = overrides.basePath;
|
|
86
|
-
if (typeof basePath !== "string" || isBlank(basePath) || isMalformedPath(basePath)) throw new Error("[Incode SDK] wasm.basePath is invalid.");
|
|
87
|
-
return basePath.trim();
|
|
88
|
-
}
|
|
89
|
-
/** Builds a WarmupConfig from a directory that mirrors the ml-wasm-kit CDN layout. */
|
|
90
|
-
function buildWasmConfigFromBasePath(basePath) {
|
|
91
|
-
const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
92
|
-
return {
|
|
93
|
-
wasmPath: `${base}/webLib.wasm`,
|
|
94
|
-
wasmSimdPath: `${base}/webLibSimd.wasm`,
|
|
95
|
-
glueCodePath: `${base}/webLib.js`,
|
|
96
|
-
glueCodeSimdPath: `${base}/webLibSimd.js`,
|
|
97
|
-
modelsBasePath: `${base}/models`
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
/** Builds the standard WarmupConfig from the Incode CDN. */
|
|
101
|
-
function buildDefaultWasmConfig() {
|
|
102
|
-
return buildWasmConfigFromBasePath(`${WASM_CDN_BASE_URL}/${DEFAULT_WASM_VERSION}`);
|
|
103
|
-
}
|
|
104
|
-
const BARCODE_ENHANCE_MODEL_FILE = WASM_MODEL_FILES.barcode;
|
|
105
|
-
/**
|
|
106
|
-
* URL of the lazily-loaded PDF417 barcode enhancement model (US Smart Capture).
|
|
107
|
-
* Derived from the resolved `modelsBasePath` so self-hosted / air-gapped
|
|
108
|
-
* deployments resolve it correctly, falling back to the CDN default. Loaded on
|
|
109
|
-
* demand via `IIdCaptureCapability.setBarcodeEnhancementModel` — deliberately
|
|
110
|
-
* NOT part of any warmup model bundle.
|
|
111
|
-
*/
|
|
112
|
-
function getBarcodeEnhanceModelUrl(modelsBasePath, variant = "current") {
|
|
113
|
-
const source = buildModelVariantSources(modelsBasePath ?? `${WASM_CDN_BASE_URL}/v2.14.50-post1/models`, BARCODE_ENHANCE_MODEL_FILE);
|
|
114
|
-
return typeof source === "string" ? source : source[variant];
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Merges optional WASM path overrides with CDN defaults. Explicit non-blank
|
|
118
|
-
* paths take precedence. Blank override strings log a warning and fall back to defaults.
|
|
119
|
-
*/
|
|
120
|
-
function resolveWasmConfig(overrides) {
|
|
121
|
-
if (!overrides) return buildDefaultWasmConfig();
|
|
122
|
-
const defaults = buildWasmConfigFromBasePath(resolveBasePath(overrides));
|
|
123
|
-
const resolvedWasmPath = pickSource("wasmPath", overrides, defaults.wasmPath);
|
|
124
|
-
const simdDefault = overrides.selfHosted ? resolvedWasmPath : defaults.wasmSimdPath;
|
|
125
|
-
const modelsDefault = defaults.modelsBasePath;
|
|
126
|
-
const resolvedWasmSimdPath = pickSource("wasmSimdPath", overrides, simdDefault);
|
|
127
|
-
const glueCodeSimdDefault = typeof resolvedWasmSimdPath === "string" ? deriveSiblingJsPath(resolvedWasmSimdPath) : defaults.glueCodeSimdPath;
|
|
128
|
-
const merged = {
|
|
129
|
-
wasmPath: resolvedWasmPath,
|
|
130
|
-
wasmSimdPath: resolvedWasmSimdPath,
|
|
131
|
-
glueCodePath: pickPath("glueCodePath", overrides, defaults.glueCodePath),
|
|
132
|
-
glueCodeSimdPath: pickPath("glueCodeSimdPath", overrides, glueCodeSimdDefault),
|
|
133
|
-
modelsBasePath: pickPath("modelsBasePath", overrides, modelsDefault),
|
|
134
|
-
useSimd: overrides.useSimd,
|
|
135
|
-
showLogs: overrides.showLogs,
|
|
136
|
-
pipelines: overrides.pipelines,
|
|
137
|
-
pipelineModels: overrides.pipelineModels,
|
|
138
|
-
modelUrls: overrides.modelUrls,
|
|
139
|
-
legacyModelUrls: overrides.legacyModelUrls,
|
|
140
|
-
loadGlue: overrides.loadGlue,
|
|
141
|
-
loadGlueSimd: overrides.loadGlueSimd,
|
|
142
|
-
selfHosted: overrides.selfHosted
|
|
143
|
-
};
|
|
144
|
-
validateWasmPaths(merged);
|
|
145
|
-
return merged;
|
|
146
|
-
}
|
|
7
|
+
/** Dirty-timer half of the interrupted-snapshot rebuild debounce (module boundaries + timer). */
|
|
8
|
+
const SNAPSHOT_DIRTY_INTERVAL_MS = 15e3;
|
|
147
9
|
/** Hard cap per log message, UTF-8 bytes (contract). */
|
|
148
10
|
const MESSAGE_MAX_BYTES = 8 * 1024;
|
|
149
11
|
const TRUESIGHT_SDK_ID = "IncdOnboarding";
|
|
150
|
-
/** localStorage key prefix for persisted
|
|
12
|
+
/** localStorage key prefix for persisted snapshot envelopes (A3: one slot per session). */
|
|
151
13
|
const PENDING_STORAGE_KEY_PREFIX = "incode.truesight.pending.";
|
|
152
14
|
//#endregion
|
|
153
15
|
//#region src/internal/trueSight/sanitize.ts
|
|
@@ -167,7 +29,18 @@ const PENDING_STORAGE_KEY_PREFIX = "incode.truesight.pending.";
|
|
|
167
29
|
* blob would otherwise split it into sub-64-char fragments that escape
|
|
168
30
|
* redaction.
|
|
169
31
|
*
|
|
170
|
-
*
|
|
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
|
|
171
44
|
* .agents/guides/privacy.md). Known hardening backlog (D4, Phase 4): formatted
|
|
172
45
|
* phone numbers, unicode digits/emails, fuzz/property tests.
|
|
173
46
|
*/
|
|
@@ -176,13 +49,22 @@ const JWT_PATTERN = /\b[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}\b/g;
|
|
|
176
49
|
/**
|
|
177
50
|
* Credential-ish key with a `:`/`=` value (quoted, `Bearer …`, or bare token).
|
|
178
51
|
* The key may carry prefixes (`access_token`, `x-api-key`, `sessionToken`) —
|
|
179
|
-
* a plain \b boundary would miss `_token` since `_` is a word character
|
|
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).
|
|
180
55
|
*/
|
|
181
|
-
const CREDENTIAL_KV_PATTERN = /([\w-]*(?:token|api[-_]?key|authorization|auth|secret|password|passwd))
|
|
56
|
+
const CREDENTIAL_KV_PATTERN = /([\w-]*(?:token|api[-_]?key|authorization|auth|secret|password|passwd))"?\s*[:=]\s*("[^"]*"|'[^']*'|(?:Bearer\s+)?[^\s,;&]+)/gi;
|
|
182
57
|
/** Userinfo credentials in an absolute URL (`https://user:pass@host/…`). */
|
|
183
58
|
const URL_USERINFO_PATTERN = /(\bhttps?:\/\/)[^\s/@]*(?::[^\s/@]*)?@/gi;
|
|
184
59
|
/** Query string / fragment of an absolute URL — kills signed URLs. */
|
|
185
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;
|
|
186
68
|
const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
187
69
|
/** base64-ish blobs (media, keys) — 64+ chars of the base64/base64url alphabet. */
|
|
188
70
|
const BASE64_BLOB_PATTERN = /[-A-Za-z0-9+/_]{64,}={0,2}/g;
|
|
@@ -192,7 +74,58 @@ const TRUNCATION_SUFFIX = "…[truncated]";
|
|
|
192
74
|
/** Regex-cost bound: clamp raw input before any pattern runs (see header note). */
|
|
193
75
|
const REGEX_INPUT_MAX_BYTES = MESSAGE_MAX_BYTES * 2;
|
|
194
76
|
function sanitizeMessage(message) {
|
|
195
|
-
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);
|
|
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);
|
|
78
|
+
}
|
|
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
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return text.length;
|
|
196
129
|
}
|
|
197
130
|
/**
|
|
198
131
|
* UTF-8 byte length without TextEncoder (banned browser global in L1 —
|
|
@@ -224,6 +157,100 @@ function truncateUtf8(value, maxBytes) {
|
|
|
224
157
|
}
|
|
225
158
|
return result + TRUNCATION_SUFFIX;
|
|
226
159
|
}
|
|
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);
|
|
195
|
+
}
|
|
196
|
+
function copyOf(entry) {
|
|
197
|
+
return { ...entry };
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
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
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
227
254
|
//#endregion
|
|
228
255
|
//#region ../infra/src/crypto/randomUUID.ts
|
|
229
256
|
/**
|
|
@@ -232,11 +259,16 @@ function truncateUtf8(value, maxBytes) {
|
|
|
232
259
|
* Lives in infra (L0) so Core (L1) can mint secure identifiers without touching
|
|
233
260
|
* the `crypto` browser API directly — Core delegates here, keeping it free of
|
|
234
261
|
* browser globals (enforced by the `no-browser-api-in-core` lint rule). Backed
|
|
235
|
-
* by `crypto.randomUUID`,
|
|
262
|
+
* by `crypto.randomUUID`, which browsers expose only in secure contexts — check
|
|
263
|
+
* `isRandomUUIDAvailable()` where a plain-`http` page must not throw.
|
|
236
264
|
*/
|
|
237
265
|
function randomUUID() {
|
|
238
266
|
return crypto.randomUUID();
|
|
239
267
|
}
|
|
268
|
+
/** `false` on insecure (plain-`http`) pages, where `randomUUID()` throws. */
|
|
269
|
+
function isRandomUUIDAvailable() {
|
|
270
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function";
|
|
271
|
+
}
|
|
240
272
|
//#endregion
|
|
241
273
|
//#region ../infra/src/time/epochMsToIsoString.ts
|
|
242
274
|
/**
|
|
@@ -266,7 +298,8 @@ function epochMsToIsoString(epochMs) {
|
|
|
266
298
|
* - empty/missing `sessionId` or `startedAt` → returns `null` (upload skipped —
|
|
267
299
|
* expected for sessions that die before `/omni/start`).
|
|
268
300
|
* - `failureReason` present if-and-only-if `outcome === 'failure'` (defaults to
|
|
269
|
-
* `'unknown'` when a failure arrives without one,
|
|
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).
|
|
270
303
|
* - fresh record `id` per build; `endedAt` stamped at build time.
|
|
271
304
|
*/
|
|
272
305
|
function buildDiagnosticsRecord(meta, logs, outcome, failureReason, recordId) {
|
|
@@ -291,7 +324,7 @@ function buildDiagnosticsRecord(meta, logs, outcome, failureReason, recordId) {
|
|
|
291
324
|
flowId: meta.flowId,
|
|
292
325
|
workflowId: meta.workflowId,
|
|
293
326
|
outcome,
|
|
294
|
-
...outcome === "failure" ? { failureReason: failureReason
|
|
327
|
+
...outcome === "failure" ? { failureReason: failureReason?.trim() || "unknown" } : {},
|
|
295
328
|
startedAt: epochMsToIsoString(meta.startedAtMs),
|
|
296
329
|
endedAt: epochMsToIsoString(getTimestamp()),
|
|
297
330
|
testing: meta.testing,
|
|
@@ -300,12 +333,26 @@ function buildDiagnosticsRecord(meta, logs, outcome, failureReason, recordId) {
|
|
|
300
333
|
externalCustomerId: meta.hasExternalCustomerId,
|
|
301
334
|
hasProvidedSessionId: meta.hasProvidedSessionId,
|
|
302
335
|
token: meta.hasToken,
|
|
303
|
-
externalToken: meta.hasExternalToken
|
|
336
|
+
externalToken: meta.hasExternalToken,
|
|
337
|
+
...meta.wasmProfile ? { webProfile: toWebProfile(meta.wasmProfile) } : {}
|
|
304
338
|
},
|
|
305
339
|
logs: logs.map(toContractEntry)
|
|
306
340
|
};
|
|
307
341
|
}
|
|
308
342
|
/**
|
|
343
|
+
* `session.webProfile`, field by field: TrueSight decodes it strictly
|
|
344
|
+
* (unknown keys reject the record), so nothing is spread from the source.
|
|
345
|
+
*/
|
|
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
|
+
} };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
309
356
|
* Wire `flowType` when no orchestrator was OBSERVED (an observed one — set by
|
|
310
357
|
* the bridge on any flow event — always wins). Stage ingest requires a
|
|
311
358
|
* non-null enum (probe 2026-07-23: null/omitted/off-enum all 400), so the
|
|
@@ -340,6 +387,14 @@ function toContractEntry(entry) {
|
|
|
340
387
|
category: entry.category
|
|
341
388
|
};
|
|
342
389
|
}
|
|
390
|
+
//#endregion
|
|
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;
|
|
343
398
|
function createInitialState() {
|
|
344
399
|
return {
|
|
345
400
|
sessionId: null,
|
|
@@ -363,16 +418,67 @@ function createInitialState() {
|
|
|
363
418
|
}
|
|
364
419
|
let state = createInitialState();
|
|
365
420
|
let sources = {};
|
|
421
|
+
function stampStartedAtOnce() {
|
|
422
|
+
if (state.startedAtMs === null) state.startedAtMs = getTimestamp();
|
|
423
|
+
}
|
|
366
424
|
/** Tap for `createSession` — sessionId comes from the returned `interviewId`. */
|
|
367
|
-
function noteSessionCreated(session, options = {}) {
|
|
368
|
-
|
|
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();
|
|
442
|
+
}
|
|
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
|
+
}
|
|
369
471
|
/**
|
|
370
472
|
* `testing` heuristic: devMode OR non-prod API host pattern. Always a boolean —
|
|
371
473
|
* the contract requires the field present on every record. Signals MERGE across
|
|
372
474
|
* calls (a later apiUrl-only call must not erase an earlier devMode signal);
|
|
373
475
|
* `testing` is recomputed from the merged pair.
|
|
374
476
|
*/
|
|
375
|
-
function noteTestingSignals(signals) {
|
|
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
|
+
}
|
|
376
482
|
/**
|
|
377
483
|
* Live read of the merged testing signal (devMode OR non-prod host). Used to
|
|
378
484
|
* gate host-console self-health noise so production host pages stay quiet while
|
|
@@ -383,12 +489,25 @@ function isTrueSightTesting() {
|
|
|
383
489
|
return state.testing;
|
|
384
490
|
}
|
|
385
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
|
+
/**
|
|
386
500
|
* Wiring tap for every `initializeSession({ token })`: a token activated
|
|
387
501
|
* before any `createSession` was observed on this page is host-minted, i.e.
|
|
388
502
|
* external. (The exact `token`/`externalToken` web-semantics are a pending
|
|
389
503
|
* Phase 0 contract ask — this inference is the interim mapping.)
|
|
390
504
|
*/
|
|
391
|
-
function noteTokenActivated() {
|
|
505
|
+
function noteTokenActivated() {
|
|
506
|
+
if (state.sessionId === null) noteExternalTokenProvided();
|
|
507
|
+
}
|
|
508
|
+
function configureTrueSightMetadataSources(next) {
|
|
509
|
+
sources = next;
|
|
510
|
+
}
|
|
392
511
|
/** Explicit literal — the metadata half handed to the record builder. */
|
|
393
512
|
function buildSessionMetadata() {
|
|
394
513
|
return {
|
|
@@ -408,7 +527,8 @@ function buildSessionMetadata() {
|
|
|
408
527
|
hasExternalCustomerId: state.hasExternalCustomerId,
|
|
409
528
|
hasProvidedSessionId: state.hasProvidedSessionId,
|
|
410
529
|
hasToken: readHasToken(),
|
|
411
|
-
hasExternalToken: state.hasExternalToken
|
|
530
|
+
hasExternalToken: state.hasExternalToken,
|
|
531
|
+
wasmProfile: readWasmProfile()
|
|
412
532
|
};
|
|
413
533
|
}
|
|
414
534
|
/** Fail-open: diagnostics must never throw into the host flow. */
|
|
@@ -420,6 +540,14 @@ function readE2eEncryptionEnabled() {
|
|
|
420
540
|
}
|
|
421
541
|
}
|
|
422
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. */
|
|
423
551
|
function readHasToken() {
|
|
424
552
|
try {
|
|
425
553
|
return sources.getHasToken?.() ?? false;
|
|
@@ -451,7 +579,7 @@ async function registerPendingSlot(sessionId, deps) {
|
|
|
451
579
|
} catch {}
|
|
452
580
|
}
|
|
453
581
|
/**
|
|
454
|
-
* Writes the sealed snapshot into the session's slot. `BrowserStorageProvider`
|
|
582
|
+
* Writes the sealed snapshot envelope into the session's slot. `BrowserStorageProvider`
|
|
455
583
|
* executes the underlying `localStorage.setItem` synchronously in the first
|
|
456
584
|
* slice of the async call, which is what makes this safe to fire (void) from
|
|
457
585
|
* a page-hide handler.
|
|
@@ -982,10 +1110,15 @@ const createWasmApi = async (config) => {
|
|
|
982
1110
|
* host called `setup()` without `wasm` and warmed WASM separately), swap to
|
|
983
1111
|
* the WASM client first. The module is already loaded — the payload came
|
|
984
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.
|
|
985
1117
|
*/
|
|
986
1118
|
function bindWebApiTransport(client) {
|
|
987
1119
|
return async (url, body, config) => {
|
|
988
|
-
const
|
|
1120
|
+
const activeClient = config.ie === true && storedHttpConfig && !storedHttpConfig.isWasm ? await ensureWasmHttpClient() : client;
|
|
1121
|
+
const response = await withTiming("POST", url, () => activeClient.request({
|
|
989
1122
|
method: "POST",
|
|
990
1123
|
url,
|
|
991
1124
|
body,
|
|
@@ -993,7 +1126,7 @@ function bindWebApiTransport(client) {
|
|
|
993
1126
|
params: config.params,
|
|
994
1127
|
timeout: config.timeout,
|
|
995
1128
|
ie: config.ie
|
|
996
|
-
});
|
|
1129
|
+
}));
|
|
997
1130
|
return {
|
|
998
1131
|
data: response.data,
|
|
999
1132
|
status: response.status
|
|
@@ -1067,19 +1200,35 @@ async function upgradeToWasmHttpClient(resolved) {
|
|
|
1067
1200
|
const stored = storedHttpConfig;
|
|
1068
1201
|
if (!stored || stored.isWasm) return;
|
|
1069
1202
|
pendingUpgrade ??= (async () => {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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
|
+
}
|
|
1083
1232
|
})().finally(() => {
|
|
1084
1233
|
pendingUpgrade = void 0;
|
|
1085
1234
|
});
|
|
@@ -1100,10 +1249,127 @@ async function withHandshakeFailureContext(wantsEncryption, fn) {
|
|
|
1100
1249
|
throw error;
|
|
1101
1250
|
}
|
|
1102
1251
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1252
|
+
//#endregion
|
|
1253
|
+
//#region src/internal/trueSight/eventBridge.ts
|
|
1254
|
+
/**
|
|
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.
|
|
1261
|
+
*/
|
|
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
|
+
};
|
|
1312
|
+
/**
|
|
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.
|
|
1320
|
+
*/
|
|
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;
|
|
1334
|
+
/**
|
|
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`.
|
|
1338
|
+
*/
|
|
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]);
|
|
1107
1373
|
/**
|
|
1108
1374
|
* Coarse category for HTTP-failure entries, from the endpoint path (event
|
|
1109
1375
|
* map row 23: "by endpoint"). A conservative token heuristic — the category
|
|
@@ -1111,36 +1377,220 @@ eventModuleNames.home, eventModuleNames.workflow;
|
|
|
1111
1377
|
* scan bucket.
|
|
1112
1378
|
*/
|
|
1113
1379
|
function categoryForEndpoint(endpoint) {
|
|
1380
|
+
if (/\/omni\/es\//.test(endpoint)) return "other";
|
|
1114
1381
|
if (/face|selfie|video|speech|personhood|authenticate|recordings/.test(endpoint)) return "faceScan";
|
|
1115
|
-
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";
|
|
1116
1383
|
if (/start|flow|workflow|feature-config|onboarding|finish-status/.test(endpoint)) return "orchestration";
|
|
1117
1384
|
return "other";
|
|
1118
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
|
+
}
|
|
1119
1497
|
//#endregion
|
|
1120
1498
|
//#region src/internal/trueSight/uploader.ts
|
|
1121
|
-
let
|
|
1499
|
+
let sealerProviderPromise = null;
|
|
1122
1500
|
/**
|
|
1123
|
-
* Loads the browser sealer as a LAZY chunk, once —
|
|
1501
|
+
* Loads the browser sealer as a LAZY chunk, once — it stays off the host
|
|
1124
1502
|
* page's critical path until diagnostics actually need it (first upload or
|
|
1125
|
-
* 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.
|
|
1126
1515
|
*/
|
|
1127
|
-
function
|
|
1128
|
-
|
|
1129
|
-
|
|
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;
|
|
1130
1533
|
}
|
|
1131
1534
|
/**
|
|
1132
|
-
* Seals and posts one diagnostics record. Never rejects (fail-open)
|
|
1133
|
-
* guards per contract: warn approaching the
|
|
1134
|
-
* rebuild ONCE with the oldest half of the
|
|
1135
|
-
* 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.
|
|
1136
1540
|
*/
|
|
1137
|
-
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() {}
|
|
1138
1586
|
//#endregion
|
|
1139
1587
|
//#region src/internal/trueSight/trueSightService.ts
|
|
1140
1588
|
let active = false;
|
|
1589
|
+
/** The lifecycle's first `initialize` opted out — later calls cannot re-enable. */
|
|
1590
|
+
let optedOut = false;
|
|
1141
1591
|
let remoteKilled = false;
|
|
1142
1592
|
let finalized = false;
|
|
1143
|
-
let
|
|
1593
|
+
let sealerUnsupported = false;
|
|
1144
1594
|
let collectionStarted = false;
|
|
1145
1595
|
let flagResolutionStarted = false;
|
|
1146
1596
|
let lastError = null;
|
|
@@ -1151,15 +1601,26 @@ let options = defaultOptions();
|
|
|
1151
1601
|
let observedManagerKind = null;
|
|
1152
1602
|
let orchestratorConflictLogged = false;
|
|
1153
1603
|
let snapshotRecordId = null;
|
|
1604
|
+
let cachedPendingEnvelope = null;
|
|
1154
1605
|
let snapshotDirty = false;
|
|
1155
1606
|
let captureActive = false;
|
|
1156
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;
|
|
1157
1613
|
let sealFailureLogged = false;
|
|
1158
1614
|
let pendingSlotRegistered = false;
|
|
1159
1615
|
let recoveryStarted = false;
|
|
1616
|
+
let bridgeFailureCount = 0;
|
|
1617
|
+
let envSnapshotLogged = false;
|
|
1160
1618
|
let hintTransitionCount = 0;
|
|
1161
1619
|
let hintSuppressedCount = 0;
|
|
1162
1620
|
let lastHintStatus = null;
|
|
1621
|
+
let overflowBatchCount = 0;
|
|
1622
|
+
let lastFlushRecordId = null;
|
|
1623
|
+
let overflowDormant = false;
|
|
1163
1624
|
let overflowFlushInFlightUpTo = null;
|
|
1164
1625
|
let dirtyTimerHandle = null;
|
|
1165
1626
|
let unsubscribePageHide = null;
|
|
@@ -1168,20 +1629,45 @@ function defaultOptions() {
|
|
|
1168
1629
|
fetchFeatures: fetchFeatureConfig,
|
|
1169
1630
|
captureTransport: captureLiveTransport,
|
|
1170
1631
|
upload: uploadDiagnostics,
|
|
1171
|
-
|
|
1632
|
+
loadSealer: loadDiagnosticsEnvelopeProvider
|
|
1172
1633
|
};
|
|
1173
1634
|
}
|
|
1174
1635
|
/**
|
|
1175
|
-
* Boot-time entry, called from `setup()`. Idempotent — the first
|
|
1176
|
-
*
|
|
1177
|
-
* encryption lock); `reset()` → `resetTrueSight()` starts a new one.
|
|
1178
|
-
* `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).
|
|
1179
1642
|
*/
|
|
1180
|
-
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
|
+
}
|
|
1181
1667
|
/**
|
|
1182
1668
|
* Session-time entry, called from `initializeSession` once feature config is
|
|
1183
1669
|
* known. Collection is already running by then (buffering starts at
|
|
1184
|
-
* initialize); this resolves the remote flag and prewarms the
|
|
1670
|
+
* initialize); this resolves the remote flag and prewarms the sealer chunk.
|
|
1185
1671
|
* When the host's fetch failed, call with no argument — the service resolves
|
|
1186
1672
|
* the cached `fetchFeatureConfig()` itself (a token is present at this point).
|
|
1187
1673
|
*/
|
|
@@ -1189,9 +1675,10 @@ function startTrueSightCollection(features) {
|
|
|
1189
1675
|
if (!active || remoteKilled || finalized) return;
|
|
1190
1676
|
if (!collectionStarted) {
|
|
1191
1677
|
collectionStarted = true;
|
|
1192
|
-
|
|
1678
|
+
prewarmSealer();
|
|
1193
1679
|
}
|
|
1194
1680
|
if (features) {
|
|
1681
|
+
noteFeatureConfig(features);
|
|
1195
1682
|
if (!flagResolutionStarted) {
|
|
1196
1683
|
flagResolutionStarted = true;
|
|
1197
1684
|
applyFeatureResolution(features, false);
|
|
@@ -1199,6 +1686,7 @@ function startTrueSightCollection(features) {
|
|
|
1199
1686
|
} else if (!flagResolutionStarted) {
|
|
1200
1687
|
flagResolutionStarted = true;
|
|
1201
1688
|
options.fetchFeatures().then((fetched) => {
|
|
1689
|
+
noteFeatureConfig(fetched);
|
|
1202
1690
|
applyFeatureResolution(fetched, false);
|
|
1203
1691
|
}, () => {
|
|
1204
1692
|
applyFeatureResolution(null, true);
|
|
@@ -1211,38 +1699,64 @@ function startTrueSightCollection(features) {
|
|
|
1211
1699
|
* snapshot rebuilds are suppressed — the collector must never compete with
|
|
1212
1700
|
* 30 fps capture. Deactivation flushes the deferred rebuild.
|
|
1213
1701
|
*/
|
|
1214
|
-
function noteTrueSightCaptureActivity(activityActive) {
|
|
1702
|
+
function noteTrueSightCaptureActivity(activityActive) {
|
|
1703
|
+
if (activityActive && !captureActive) resetHintBudget();
|
|
1704
|
+
captureActive = activityActive;
|
|
1705
|
+
if (!activityActive) maybeRebuildSnapshot();
|
|
1706
|
+
}
|
|
1215
1707
|
/**
|
|
1216
1708
|
* Seals and uploads the one final record per session. The upload-relevant
|
|
1217
1709
|
* state — transport, metadata (live token/E2EE getters read NOW), buffered
|
|
1218
1710
|
* logs — is captured SYNCHRONOUSLY before the first await, because `reset()`
|
|
1219
1711
|
* calls `resetApi()` immediately after this returns. Finalize-once: later
|
|
1220
|
-
* 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.
|
|
1221
1714
|
*/
|
|
1222
1715
|
function finalizeTrueSight(outcome, failureReason) {
|
|
1223
1716
|
if (!active || remoteKilled || finalized) return Promise.resolve();
|
|
1224
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();
|
|
1225
1726
|
trueSightLog("info", "session", "orchestration", `finalizing outcome=${outcome}${failureReason !== void 0 ? ` reason=${failureReason}` : ""}`);
|
|
1226
1727
|
const metadata = buildSessionMetadata();
|
|
1227
1728
|
const logs = (buffer?.snapshot() ?? []).filter((entry) => overflowFlushInFlightUpTo === null || entry.index > overflowFlushInFlightUpTo);
|
|
1228
1729
|
const transport = safeCaptureTransport();
|
|
1229
|
-
if (transport === null ||
|
|
1730
|
+
if (transport === null || sealerUnsupported) return Promise.resolve();
|
|
1230
1731
|
const record = buildDiagnosticsRecord(metadata, logs, outcome, failureReason);
|
|
1231
1732
|
if (record === null) return Promise.resolve();
|
|
1232
|
-
|
|
1733
|
+
cachedPendingEnvelope = null;
|
|
1734
|
+
const storage = options.storage;
|
|
1735
|
+
const { sessionId } = record.session;
|
|
1233
1736
|
return options.upload(record, {
|
|
1234
1737
|
post: transport,
|
|
1235
1738
|
keepalive: true,
|
|
1236
|
-
|
|
1237
|
-
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 });
|
|
1238
1744
|
});
|
|
1239
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
|
+
}
|
|
1240
1754
|
/**
|
|
1241
1755
|
* Capture guidance-hint transition tap (event map 19b/19c): the moment the
|
|
1242
1756
|
* hint the user sees CHANGES, as the status CODE — never the localized text.
|
|
1243
1757
|
* Consecutive dedupe on top of the source dedupe, plus a per-attempt budget
|
|
1244
1758
|
* (~60, then suppress-with-counter) so a struggling capture can never flood
|
|
1245
|
-
* 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
|
|
1246
1760
|
* debug so the eviction fallback drops hints before warnings/errors.
|
|
1247
1761
|
*/
|
|
1248
1762
|
function trueSightHintTransition(subsystem, category, status) {
|
|
@@ -1260,10 +1774,33 @@ function trueSightHintTransition(subsystem, category, status) {
|
|
|
1260
1774
|
}
|
|
1261
1775
|
trueSightLog("debug", subsystem, category, `hint changed status=${status}`);
|
|
1262
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
|
+
}
|
|
1263
1800
|
/** Buffer tap for wiring points and self-health lines. No-op when inactive. */
|
|
1264
|
-
function trueSightLog(level, subsystem, category, message) {
|
|
1801
|
+
function trueSightLog(level, subsystem, category, message, timestampMs) {
|
|
1265
1802
|
if (buffer === null) return;
|
|
1266
|
-
buffer.append(level, subsystem, category, message);
|
|
1803
|
+
buffer.append(level, subsystem, category, message, timestampMs);
|
|
1267
1804
|
snapshotDirty = true;
|
|
1268
1805
|
}
|
|
1269
1806
|
/**
|
|
@@ -1282,7 +1819,11 @@ function notifyTrueSightFlowEvent(event, managerKind) {
|
|
|
1282
1819
|
trueSightLog("info", managerKind, "orchestration", "flow.started");
|
|
1283
1820
|
break;
|
|
1284
1821
|
case "flow.ready":
|
|
1285
|
-
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
|
+
});
|
|
1286
1827
|
trueSightLog("info", managerKind, "orchestration", `flow.ready ${managerKind === "workflow" ? "workflowId" : "flowId"}=${event.flowId ?? "unknown"}`);
|
|
1287
1828
|
break;
|
|
1288
1829
|
case "flow.module.started":
|
|
@@ -1293,8 +1834,8 @@ function notifyTrueSightFlowEvent(event, managerKind) {
|
|
|
1293
1834
|
break;
|
|
1294
1835
|
}
|
|
1295
1836
|
case "flow.error":
|
|
1296
|
-
lastError = sanitizeMessage(event.error
|
|
1297
|
-
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"}`);
|
|
1298
1839
|
break;
|
|
1299
1840
|
case "flow.completed":
|
|
1300
1841
|
trueSightLog("info", managerKind, "orchestration", "flow.completed");
|
|
@@ -1340,6 +1881,8 @@ function stampOrchestratorIdentity(managerKind) {
|
|
|
1340
1881
|
}
|
|
1341
1882
|
return false;
|
|
1342
1883
|
}
|
|
1884
|
+
noteIntegrationSurface(managerKind === "workflow" ? "startWorkflow" : "startFlow");
|
|
1885
|
+
noteFlowContext({ flowType: managerKind });
|
|
1343
1886
|
return true;
|
|
1344
1887
|
}
|
|
1345
1888
|
/** Full teardown, called from `reset()` AFTER finalize captured its snapshot. */
|
|
@@ -1354,28 +1897,63 @@ function resetTrueSight() {
|
|
|
1354
1897
|
}
|
|
1355
1898
|
buffer = null;
|
|
1356
1899
|
active = false;
|
|
1900
|
+
lifecycleGeneration += 1;
|
|
1901
|
+
optedOut = false;
|
|
1357
1902
|
remoteKilled = false;
|
|
1358
1903
|
finalized = false;
|
|
1359
|
-
|
|
1904
|
+
sealerUnsupported = false;
|
|
1360
1905
|
collectionStarted = false;
|
|
1361
1906
|
flagResolutionStarted = false;
|
|
1362
1907
|
lastError = null;
|
|
1363
1908
|
observedManagerKind = null;
|
|
1364
1909
|
orchestratorConflictLogged = false;
|
|
1365
1910
|
snapshotRecordId = null;
|
|
1911
|
+
cachedPendingEnvelope = null;
|
|
1366
1912
|
snapshotDirty = false;
|
|
1367
1913
|
captureActive = false;
|
|
1368
1914
|
rebuildInFlight = false;
|
|
1369
1915
|
sealFailureLogged = false;
|
|
1370
1916
|
pendingSlotRegistered = false;
|
|
1371
1917
|
recoveryStarted = false;
|
|
1918
|
+
bridgeFailureCount = 0;
|
|
1919
|
+
envSnapshotLogged = false;
|
|
1372
1920
|
hintTransitionCount = 0;
|
|
1373
1921
|
hintSuppressedCount = 0;
|
|
1374
1922
|
lastHintStatus = null;
|
|
1923
|
+
overflowBatchCount = 0;
|
|
1924
|
+
lastFlushRecordId = null;
|
|
1925
|
+
overflowDormant = false;
|
|
1375
1926
|
overflowFlushInFlightUpTo = null;
|
|
1927
|
+
externalDrain = null;
|
|
1376
1928
|
options = defaultOptions();
|
|
1377
1929
|
resetTrueSightMetadata();
|
|
1378
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
|
+
}
|
|
1379
1957
|
function applyFeatureResolution(features, unresolvable) {
|
|
1380
1958
|
if (!active || remoteKilled || finalized) return;
|
|
1381
1959
|
const entry = features?.features?.find((candidate) => candidate.feature === "TRUESIGHT_DIAGNOSTICS");
|
|
@@ -1392,6 +1970,70 @@ function remoteKill() {
|
|
|
1392
1970
|
unsubscribeEvents = null;
|
|
1393
1971
|
}
|
|
1394
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
|
+
/**
|
|
1395
2037
|
* Debounced interrupted-snapshot rebuild (plan §1.8): triggered by module
|
|
1396
2038
|
* boundaries and the dirty-interval tick, suppressed while a capture loop is
|
|
1397
2039
|
* active (C7). Seals the CURRENT state as a `failure/interrupted` record
|
|
@@ -1399,7 +2041,17 @@ function remoteKill() {
|
|
|
1399
2041
|
* handler writes only the cache — it never builds), and persists it.
|
|
1400
2042
|
*/
|
|
1401
2043
|
function maybeRebuildSnapshot() {
|
|
1402
|
-
|
|
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;
|
|
1403
2055
|
const metadata = buildSessionMetadata();
|
|
1404
2056
|
if (!metadata.sessionId) return;
|
|
1405
2057
|
const storage = options.storage;
|
|
@@ -1409,10 +2061,16 @@ function maybeRebuildSnapshot() {
|
|
|
1409
2061
|
if (record === null) return;
|
|
1410
2062
|
snapshotDirty = false;
|
|
1411
2063
|
rebuildInFlight = true;
|
|
1412
|
-
|
|
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;
|
|
1413
2070
|
if (!pendingSlotRegistered) {
|
|
1414
2071
|
pendingSlotRegistered = true;
|
|
1415
2072
|
await registerPendingSlot(sessionId, { storage });
|
|
2073
|
+
if (isStale()) return;
|
|
1416
2074
|
}
|
|
1417
2075
|
await writePendingEnvelope(sessionId, {
|
|
1418
2076
|
v: 1,
|
|
@@ -1420,16 +2078,33 @@ function maybeRebuildSnapshot() {
|
|
|
1420
2078
|
envelope
|
|
1421
2079
|
}, { storage });
|
|
1422
2080
|
}).catch(() => {
|
|
2081
|
+
if (generation !== lifecycleGeneration) return;
|
|
1423
2082
|
snapshotDirty = true;
|
|
1424
2083
|
if (!sealFailureLogged) {
|
|
1425
2084
|
sealFailureLogged = true;
|
|
1426
2085
|
trueSightLog("warning", "diagnostics", "other", "snapshot seal failed persistDisabled=false");
|
|
1427
2086
|
}
|
|
1428
2087
|
}).finally(() => {
|
|
1429
|
-
rebuildInFlight = false;
|
|
2088
|
+
if (generation === lifecycleGeneration) rebuildInFlight = false;
|
|
1430
2089
|
});
|
|
1431
2090
|
}
|
|
1432
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
|
+
/**
|
|
1433
2108
|
* Next-load recovery off the first-token-observed hook (A1) — runs once per
|
|
1434
2109
|
* lifecycle, fire-and-forget, and reports the A5 loss counters as a
|
|
1435
2110
|
* self-health line when there was anything to report.
|
|
@@ -1448,21 +2123,21 @@ function kickRecoveryOnce() {
|
|
|
1448
2123
|
});
|
|
1449
2124
|
}
|
|
1450
2125
|
/**
|
|
1451
|
-
* Resolves
|
|
2126
|
+
* Resolves sealer support ahead of finalize (and warms the lazy chunk so the
|
|
1452
2127
|
* terminal path never pays the import). Unsupported → collect-only mode with
|
|
1453
2128
|
* exactly one self-health warning.
|
|
1454
2129
|
*/
|
|
1455
|
-
function
|
|
1456
|
-
options.
|
|
1457
|
-
if (!provider.isSupported())
|
|
2130
|
+
function prewarmSealer() {
|
|
2131
|
+
options.loadSealer().then((provider) => {
|
|
2132
|
+
if (!provider.isSupported() || !isRandomUUIDAvailable()) markSealerUnsupported();
|
|
1458
2133
|
}, () => {
|
|
1459
|
-
|
|
2134
|
+
markSealerUnsupported();
|
|
1460
2135
|
});
|
|
1461
2136
|
}
|
|
1462
|
-
function
|
|
1463
|
-
if (
|
|
1464
|
-
|
|
1465
|
-
trueSightLog("warning", "diagnostics", "other", "
|
|
2137
|
+
function markSealerUnsupported() {
|
|
2138
|
+
if (sealerUnsupported) return;
|
|
2139
|
+
sealerUnsupported = true;
|
|
2140
|
+
trueSightLog("warning", "diagnostics", "other", "sealer unsupported collectOnly=true");
|
|
1466
2141
|
}
|
|
1467
2142
|
/**
|
|
1468
2143
|
* Default transport capture: a closure over the live client + token AS OF
|
|
@@ -1494,6 +2169,20 @@ function selfHealthWarn(message) {
|
|
|
1494
2169
|
if (isTrueSightTesting()) console.warn(message);
|
|
1495
2170
|
trueSightLog("warning", "diagnostics", "other", message);
|
|
1496
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
|
+
}
|
|
1497
2186
|
/** ` index=i total=n` suffix when the flow event carries module positioning. */
|
|
1498
2187
|
function modulePosition(payload) {
|
|
1499
2188
|
if (typeof payload !== "object" || payload === null) return "";
|
|
@@ -1501,4 +2190,4 @@ function modulePosition(payload) {
|
|
|
1501
2190
|
return `${typeof moduleIndex === "number" ? ` index=${moduleIndex}` : ""}${typeof totalModules === "number" ? ` total=${totalModules}` : ""}`;
|
|
1502
2191
|
}
|
|
1503
2192
|
//#endregion
|
|
1504
|
-
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 };
|