truvaxia-ai-sdk 1.0.0

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/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @truvaxia/sdk
2
+
3
+ The Truvaxia Client SDK is a powerful, drop-in browser library designed to seamlessly integrate Zero-Trust identity verification and fraud prevention into any web application.
4
+
5
+ ## Core Capabilities
6
+
7
+ - **Biometric Verification**: Handles camera permissions, face matching, and liveness checks natively through an embedded UI widget.
8
+ - **Behavioral Telemetry**: Silently monitors typing cadence (Keystrokes Per Minute), mouse movements, and clipboard events (paste detection).
9
+ - **Device Fingerprinting**: Extracts hardware constraints, OS data, browser properties, and canvas fingerprints to detect spoofing or headless automation.
10
+ - **Secure Encrypted Payloads**: Packages all telemetry into a secure payload evaluated directly by the Truvaxia AI Backend.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @truvaxia/sdk
16
+ # or
17
+ yarn add @truvaxia/sdk
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### 1. Initialization
23
+ Initialize the SDK globally when your application loads.
24
+
25
+ ```javascript
26
+ import { Truvaxia } from '@truvaxia/sdk';
27
+
28
+ // Initialize the Zero-Trust SDK
29
+ Truvaxia.init({ staffId: 'USER_1234' }).catch(console.error);
30
+ ```
31
+
32
+ ### 2. Launching the Security Widget
33
+ Trigger the biometric verification widget when a user submits sensitive data.
34
+
35
+ ```javascript
36
+ const formData = { firstName: 'John', lastName: 'Doe', bvn: '12345678901' };
37
+
38
+ Truvaxia.verifyOnboarding(formData, {
39
+ onSuccess: (result) => {
40
+ console.log('Verification Passed!', result.score);
41
+ },
42
+ onFailure: (error) => {
43
+ console.error('Verification Failed!', error.reasons);
44
+ }
45
+ });
46
+ ```
47
+
48
+ ## Architecture
49
+ The SDK is built using vanilla TypeScript and compiles down to both CommonJS and ES Modules, ensuring compatibility with React, Vue, Angular, or vanilla HTML/JS projects.
@@ -0,0 +1,648 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/core/truvaxia.ts
21
+ var truvaxia_exports = {};
22
+ __export(truvaxia_exports, {
23
+ Truvaxia: () => Truvaxia
24
+ });
25
+ module.exports = __toCommonJS(truvaxia_exports);
26
+
27
+ // src/modules/behavioral/input-tracker.ts
28
+ var InputTracker = class {
29
+ staffId = null;
30
+ sessionStartTime = 0;
31
+ keystrokeCount = 0;
32
+ pasteCount = 0;
33
+ trackingInterval = null;
34
+ startTracking(staffId) {
35
+ if (this.staffId) return;
36
+ this.staffId = staffId;
37
+ this.sessionStartTime = Date.now();
38
+ if (typeof document !== "undefined") {
39
+ document.addEventListener("keydown", this.handleKeyDown);
40
+ document.addEventListener("paste", this.handlePaste);
41
+ }
42
+ this.trackingInterval = setInterval(() => {
43
+ this.evaluateTelemetry();
44
+ }, 1e4);
45
+ }
46
+ stopTracking() {
47
+ if (typeof document !== "undefined") {
48
+ document.removeEventListener("keydown", this.handleKeyDown);
49
+ document.removeEventListener("paste", this.handlePaste);
50
+ }
51
+ if (this.trackingInterval) {
52
+ clearInterval(this.trackingInterval);
53
+ this.trackingInterval = null;
54
+ }
55
+ this.staffId = null;
56
+ }
57
+ handleKeyDown = (event) => {
58
+ if (["Shift", "Control", "Alt", "Meta", "CapsLock"].includes(event.key)) {
59
+ return;
60
+ }
61
+ this.keystrokeCount++;
62
+ };
63
+ handlePaste = (event) => {
64
+ const target = event.target;
65
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")) {
66
+ this.pasteCount++;
67
+ }
68
+ };
69
+ evaluateTelemetry() {
70
+ if (!this.staffId) return;
71
+ const elapsedMinutes = (Date.now() - this.sessionStartTime) / 6e4;
72
+ const kpm = elapsedMinutes > 0 ? this.keystrokeCount / elapsedMinutes : 0;
73
+ const isAnomalous = kpm > 300 || this.pasteCount > 3;
74
+ const profile = {
75
+ staffId: this.staffId,
76
+ sessionStartTime: this.sessionStartTime,
77
+ keystrokesPerMinute: Math.round(kpm),
78
+ pasteEventsCount: this.pasteCount,
79
+ totalKeystrokes: this.keystrokeCount,
80
+ isAnomalous
81
+ };
82
+ if (isAnomalous) {
83
+ console.warn(`[Truvaxia:FraudAlert] Anomalous behavior detected for RO: ${this.staffId}`, profile);
84
+ }
85
+ }
86
+ getSessionData() {
87
+ const elapsedMinutes = (Date.now() - this.sessionStartTime) / 6e4;
88
+ const kpm = elapsedMinutes > 0 ? this.keystrokeCount / elapsedMinutes : 0;
89
+ return {
90
+ staffId: this.staffId || "",
91
+ sessionStartTime: this.sessionStartTime,
92
+ keystrokesPerMinute: Math.round(kpm),
93
+ pasteEventsCount: this.pasteCount,
94
+ totalKeystrokes: this.keystrokeCount,
95
+ isAnomalous: kpm > 300 || this.pasteCount > 3
96
+ };
97
+ }
98
+ };
99
+
100
+ // src/modules/biometrics/liveness.ts
101
+ var import_tasks_vision = require("@mediapipe/tasks-vision");
102
+ var LivenessDetector = class {
103
+ faceLandmarker = null;
104
+ videoElement = null;
105
+ lastVideoTime = -1;
106
+ isDetecting = false;
107
+ animationFrameId = null;
108
+ mediaRecorder = null;
109
+ recordedChunks = [];
110
+ currentStage = "BLINK" /* BLINK */;
111
+ onProgressCallback;
112
+ preloaded = false;
113
+ preloadingPromise = null;
114
+ /**
115
+ * Preloads the heavy MediaPipe WASM models without requesting the camera.
116
+ */
117
+ async preload() {
118
+ if (this.preloaded) return;
119
+ if (this.preloadingPromise) return this.preloadingPromise;
120
+ console.log("[Truvaxia:Liveness] Preloading MediaPipe WASM in background...");
121
+ this.preloadingPromise = (async () => {
122
+ try {
123
+ const vision = await import_tasks_vision.FilesetResolver.forVisionTasks(
124
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm"
125
+ );
126
+ this.faceLandmarker = await import_tasks_vision.FaceLandmarker.createFromOptions(vision, {
127
+ baseOptions: {
128
+ modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
129
+ delegate: "GPU"
130
+ },
131
+ runningMode: "VIDEO",
132
+ numFaces: 1,
133
+ outputFaceBlendshapes: true,
134
+ outputFacialTransformationMatrixes: true
135
+ });
136
+ this.preloaded = true;
137
+ console.log("[Truvaxia:Liveness] MediaPipe WASM preloaded successfully.");
138
+ } catch (error) {
139
+ console.error("[Truvaxia:Liveness] Failed to preload MediaPipe WASM:", error);
140
+ }
141
+ })();
142
+ return this.preloadingPromise;
143
+ }
144
+ /**
145
+ * Initializes the camera and begins the actual detection loop.
146
+ * This should be called when the user opens the scanner.
147
+ */
148
+ async start(onProgress) {
149
+ console.log("[Truvaxia:Liveness] Starting Liveness module...");
150
+ this.onProgressCallback = onProgress;
151
+ this.currentStage = "BLINK" /* BLINK */;
152
+ if (this.onProgressCallback) this.onProgressCallback(this.currentStage);
153
+ try {
154
+ if (!this.preloaded) {
155
+ await this.preload();
156
+ }
157
+ this.videoElement = document.createElement("video");
158
+ this.videoElement.setAttribute("autoplay", "");
159
+ this.videoElement.setAttribute("playsinline", "");
160
+ const stream = await navigator.mediaDevices.getUserMedia({
161
+ video: { width: 1280, height: 720, facingMode: "user" }
162
+ });
163
+ this.videoElement.srcObject = stream;
164
+ this.recordedChunks = [];
165
+ try {
166
+ this.mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm" });
167
+ this.mediaRecorder.ondataavailable = (event) => {
168
+ if (event.data.size > 0) {
169
+ this.recordedChunks.push(event.data);
170
+ }
171
+ };
172
+ this.mediaRecorder.start();
173
+ } catch (e) {
174
+ console.error("[Truvaxia:Liveness] Failed to start MediaRecorder:", e);
175
+ }
176
+ this.videoElement.addEventListener("loadeddata", () => {
177
+ this.isDetecting = true;
178
+ this.detectLoop();
179
+ });
180
+ return stream;
181
+ } catch (error) {
182
+ console.error("[Truvaxia:Liveness] Failed to start camera or load model:", error);
183
+ throw error;
184
+ }
185
+ }
186
+ advanceStage(nextStage) {
187
+ this.currentStage = nextStage;
188
+ console.log(`[Truvaxia:Liveness] Advanced to stage: ${nextStage}`);
189
+ if (this.onProgressCallback) {
190
+ this.onProgressCallback(nextStage);
191
+ }
192
+ }
193
+ /**
194
+ * Continuously analyzes the video stream for liveness indicators (e.g., blinking).
195
+ */
196
+ detectLoop = () => {
197
+ if (!this.isDetecting || !this.videoElement || !this.faceLandmarker) return;
198
+ const startTimeMs = performance.now();
199
+ if (this.lastVideoTime !== this.videoElement.currentTime) {
200
+ this.lastVideoTime = this.videoElement.currentTime;
201
+ const results = this.faceLandmarker.detectForVideo(this.videoElement, startTimeMs);
202
+ if (results.faceBlendshapes && results.faceBlendshapes.length > 0 && results.faceLandmarks && results.faceLandmarks.length > 0) {
203
+ const blendshapes = results.faceBlendshapes[0].categories;
204
+ const landmarks = results.faceLandmarks[0];
205
+ const leftBlink = blendshapes.find((b) => b.categoryName === "eyeBlinkLeft")?.score || 0;
206
+ const rightBlink = blendshapes.find((b) => b.categoryName === "eyeBlinkRight")?.score || 0;
207
+ const noseX = landmarks[1].x;
208
+ const leftCheekX = landmarks[234].x;
209
+ const rightCheekX = landmarks[454].x;
210
+ const distLeft = Math.abs(noseX - leftCheekX);
211
+ const distRight = Math.abs(noseX - rightCheekX);
212
+ const ratio = distLeft / (distRight + 1e-4);
213
+ switch (this.currentStage) {
214
+ case "BLINK" /* BLINK */:
215
+ if (leftBlink > 0.4 && rightBlink > 0.4) {
216
+ this.advanceStage("TURN_LEFT" /* TURN_LEFT */);
217
+ }
218
+ break;
219
+ case "TURN_LEFT" /* TURN_LEFT */:
220
+ if (ratio < 0.35) {
221
+ this.advanceStage("TURN_RIGHT" /* TURN_RIGHT */);
222
+ }
223
+ break;
224
+ case "TURN_RIGHT" /* TURN_RIGHT */:
225
+ if (ratio > 2.5) {
226
+ this.advanceStage("LOOK_STRAIGHT" /* LOOK_STRAIGHT */);
227
+ }
228
+ break;
229
+ case "LOOK_STRAIGHT" /* LOOK_STRAIGHT */:
230
+ if (ratio > 0.7 && ratio < 1.3) {
231
+ this.advanceStage("COMPLETED" /* COMPLETED */);
232
+ }
233
+ break;
234
+ case "COMPLETED" /* COMPLETED */:
235
+ break;
236
+ }
237
+ }
238
+ }
239
+ if (this.currentStage !== "COMPLETED" /* COMPLETED */) {
240
+ this.animationFrameId = requestAnimationFrame(this.detectLoop);
241
+ }
242
+ };
243
+ /**
244
+ * Called upon form submission. Evaluates liveness status and captures the frame.
245
+ */
246
+ async execute() {
247
+ console.log("[Truvaxia:Liveness] Executing final validation...");
248
+ this.isDetecting = false;
249
+ if (this.animationFrameId) {
250
+ cancelAnimationFrame(this.animationFrameId);
251
+ }
252
+ if (this.currentStage !== "COMPLETED" /* COMPLETED */) {
253
+ return { success: false, reason: "Liveness check failed: Sequence not completed." };
254
+ }
255
+ if (!this.videoElement) {
256
+ return { success: false, reason: "Video stream unavailable." };
257
+ }
258
+ const canvas = document.createElement("canvas");
259
+ canvas.width = this.videoElement.videoWidth;
260
+ canvas.height = this.videoElement.videoHeight;
261
+ const ctx = canvas.getContext("2d");
262
+ let base64Image;
263
+ if (ctx) {
264
+ ctx.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
265
+ base64Image = canvas.toDataURL("image/jpeg", 0.9);
266
+ }
267
+ let videoBase64;
268
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
269
+ videoBase64 = await new Promise((resolve) => {
270
+ this.mediaRecorder.onstop = () => {
271
+ const blob = new Blob(this.recordedChunks, { type: "video/webm" });
272
+ const reader = new FileReader();
273
+ reader.readAsDataURL(blob);
274
+ reader.onloadend = () => resolve(reader.result);
275
+ reader.onerror = () => resolve(void 0);
276
+ };
277
+ this.mediaRecorder.stop();
278
+ });
279
+ }
280
+ const stream = this.videoElement.srcObject;
281
+ if (stream) {
282
+ stream.getTracks().forEach((track) => track.stop());
283
+ }
284
+ this.videoElement.srcObject = null;
285
+ if (base64Image) {
286
+ return { success: true, frameBase64: base64Image, videoBase64 };
287
+ }
288
+ return { success: false, reason: "Failed to extract frame from canvas." };
289
+ }
290
+ };
291
+
292
+ // src/modules/policy/policy-client.ts
293
+ var PolicyClient = class {
294
+ currentPolicy = {
295
+ requireBiometrics: true,
296
+ fraudScoreThreshold: 80,
297
+ enableGeofencing: false
298
+ };
299
+ /**
300
+ * Fetches the organization's current policy from the Truvaxia Backend.
301
+ * @param staffId The staff ID to fetch the specific branch/org policy for.
302
+ */
303
+ async fetchPolicy(staffId) {
304
+ try {
305
+ console.log(`[Truvaxia:Policy] Fetching policies for staff: ${staffId}...`);
306
+ await new Promise((resolve) => setTimeout(resolve, 500));
307
+ console.log("[Truvaxia:Policy] Policies loaded successfully.", this.currentPolicy);
308
+ return this.currentPolicy;
309
+ } catch (error) {
310
+ console.error("[Truvaxia:Policy] Failed to fetch policy. Falling back to strict defaults.", error);
311
+ return this.currentPolicy;
312
+ }
313
+ }
314
+ getPolicy() {
315
+ return this.currentPolicy;
316
+ }
317
+ };
318
+
319
+ // src/modules/ui/widget-manager.ts
320
+ var WidgetManager = class {
321
+ container = null;
322
+ videoElement = null;
323
+ instructionText = null;
324
+ contentBox = null;
325
+ mount(customerName, onClose) {
326
+ if (this.container) return;
327
+ this.container = document.createElement("div");
328
+ this.container.id = "truvaxia-widget-root";
329
+ this.container.setAttribute("style", "position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 99999; display: flex; align-items: center; justify-content: center; padding: 1rem; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); font-family: ui-sans-serif, system-ui, sans-serif;");
330
+ this.container.innerHTML = `
331
+ <div style="width: 100%; max-width: 36rem; border-radius: 1rem; overflow: hidden; border: 1px solid rgba(0,255,178,0.2); background: rgba(10,15,20,0.85); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);">
332
+ <!-- Header -->
333
+ <div style="padding: 1.5rem; border-bottom: 1px solid rgba(0,255,178,0.2); display: flex; justify-content: space-between; align-items: center; background: rgba(5,10,15,0.9);">
334
+ <div>
335
+ <h3 style="font-size: 1.25rem; font-weight: 700; color: white; margin: 0;">Zero-Trust Identity Verification</h3>
336
+ <p style="font-size: 0.875rem; color: #94a3b8; margin: 0.25rem 0 0 0;">Subject: ${customerName || "Guest"}</p>
337
+ </div>
338
+ <button id="truvaxia-close-btn" style="background: none; border: none; color: #94a3b8; font-size: 1.5rem; cursor: pointer; padding: 0.5rem;">\u2715</button>
339
+ </div>
340
+
341
+ <!-- Content -->
342
+ <div id="truvaxia-content" style="padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px;">
343
+ <!-- Dynamic Content Here -->
344
+ </div>
345
+ </div>
346
+ `;
347
+ document.body.appendChild(this.container);
348
+ const closeBtn = document.getElementById("truvaxia-close-btn");
349
+ if (closeBtn) {
350
+ closeBtn.onclick = () => {
351
+ this.unmount();
352
+ onClose();
353
+ };
354
+ }
355
+ this.contentBox = document.getElementById("truvaxia-content");
356
+ this.showInitializing();
357
+ }
358
+ showInitializing() {
359
+ if (!this.contentBox) return;
360
+ this.contentBox.innerHTML = `
361
+ <div style="display: flex; flex-direction: column; align-items: center; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;">
362
+ <div style="width: 4rem; height: 4rem; border-radius: 9999px; border-top: 2px solid transparent; border-right: 2px solid #00ffb2; border-bottom: 2px solid #00ffb2; border-left: 2px solid #00ffb2; animation: spin 1s linear infinite; margin-bottom: 1rem;"></div>
363
+ <p style="color: #00ffb2; font-weight: 500; text-shadow: 0 0 10px rgba(0,255,178,0.5);">Initializing...</p>
364
+ </div>
365
+ <style>
366
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
367
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
368
+ </style>
369
+ `;
370
+ }
371
+ showScanning() {
372
+ if (!this.contentBox) throw new Error("Widget not mounted");
373
+ this.contentBox.innerHTML = `
374
+ <div style="display: flex; flex-direction: column; align-items: center; width: 100%;">
375
+ <div style="position: relative; width: 16rem; height: 16rem; border-radius: 9999px; border: 2px solid rgba(0,255,178,0.5); display: flex; align-items: center; justify-content: center; overflow: hidden; margin-bottom: 2rem;">
376
+ <div style="position: absolute; inset: 0; border-top: 4px solid #00ffb2; border-right: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid transparent; border-radius: 9999px; animation: spin 2s linear infinite; opacity: 0.7; z-index: 10;"></div>
377
+ <video id="truvaxia-video" autoplay playsinline muted style="width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1);"></video>
378
+ </div>
379
+ <div style="padding: 0.75rem 2rem; border-radius: 9999px; display: flex; align-items: center; gap: 0.75rem; background: rgba(5,10,15,0.9); border: 1px solid rgba(0,255,178,0.2);">
380
+ <div style="width: 0.5rem; height: 0.5rem; border-radius: 9999px; background: #00ffb2; box-shadow: 0 0 10px #00ffb2; animation: pulse 2s infinite;"></div>
381
+ <p id="truvaxia-instruction" style="color: white; font-weight: 500; letter-spacing: 0.025em; margin: 0;">Please wait...</p>
382
+ </div>
383
+ </div>
384
+ `;
385
+ this.videoElement = document.getElementById("truvaxia-video");
386
+ this.instructionText = document.getElementById("truvaxia-instruction");
387
+ return this.videoElement;
388
+ }
389
+ updateInstruction(text) {
390
+ if (this.instructionText) {
391
+ this.instructionText.innerText = text;
392
+ }
393
+ }
394
+ showProcessing() {
395
+ if (!this.contentBox) return;
396
+ this.contentBox.innerHTML = `
397
+ <div style="display: flex; flex-direction: column; align-items: center; animation: pulse 2s infinite;">
398
+ <div style="width: 4rem; height: 4rem; border-radius: 9999px; border-top: 2px solid transparent; border-right: 2px solid #00ffb2; border-bottom: 2px solid #00ffb2; border-left: 2px solid #00ffb2; animation: spin 1s linear infinite; margin-bottom: 1rem;"></div>
399
+ <p style="color: #00ffb2; font-weight: 600; letter-spacing: 0.1em;">ANALYZING...</p>
400
+ </div>
401
+ `;
402
+ }
403
+ unmount() {
404
+ if (this.container && this.container.parentNode) {
405
+ this.container.parentNode.removeChild(this.container);
406
+ }
407
+ this.container = null;
408
+ this.videoElement = null;
409
+ this.instructionText = null;
410
+ this.contentBox = null;
411
+ }
412
+ };
413
+
414
+ // src/modules/device/fingerprint.ts
415
+ var DeviceFingerprint = class {
416
+ profile = {};
417
+ constructor() {
418
+ this.collectBasicTelemetry();
419
+ }
420
+ collectBasicTelemetry() {
421
+ if (typeof window === "undefined") return;
422
+ this.profile.userAgent = navigator.userAgent;
423
+ this.profile.language = navigator.language;
424
+ this.profile.platform = navigator.platform || "";
425
+ this.profile.hardwareConcurrency = navigator.hardwareConcurrency || 0;
426
+ this.profile.deviceMemory = navigator.deviceMemory || 0;
427
+ if (window.screen) {
428
+ this.profile.screenResolution = `${window.screen.width}x${window.screen.height}x${window.screen.colorDepth}`;
429
+ }
430
+ this.profile.timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
431
+ this.profile.timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
432
+ }
433
+ /**
434
+ * Prompts the user for location access and returns the current coordinates.
435
+ * If denied, records that permission was refused.
436
+ */
437
+ async requestLocation() {
438
+ if (typeof navigator === "undefined" || !navigator.geolocation) {
439
+ this.profile.location = { lat: null, lng: null, permissionGranted: false };
440
+ return;
441
+ }
442
+ try {
443
+ const position = await new Promise((resolve, reject) => {
444
+ navigator.geolocation.getCurrentPosition(resolve, reject, {
445
+ enableHighAccuracy: true,
446
+ timeout: 5e3,
447
+ maximumAge: 0
448
+ });
449
+ });
450
+ this.profile.location = {
451
+ lat: position.coords.latitude,
452
+ lng: position.coords.longitude,
453
+ permissionGranted: true
454
+ };
455
+ } catch (error) {
456
+ console.warn("[Truvaxia] Location access denied or timed out.", error);
457
+ this.profile.location = { lat: null, lng: null, permissionGranted: false };
458
+ }
459
+ }
460
+ async collectIpTelemetry() {
461
+ if (typeof window === "undefined") return;
462
+ try {
463
+ const response = await fetch("https://ipapi.co/json/");
464
+ if (response.ok) {
465
+ const data = await response.json();
466
+ this.profile.ipData = {
467
+ ip: data.ip || "unknown",
468
+ city: data.city || "unknown",
469
+ region: data.region || "unknown",
470
+ country: data.country_name || "unknown",
471
+ org: data.org || "unknown"
472
+ };
473
+ }
474
+ } catch (e) {
475
+ console.warn("[Truvaxia] Could not fetch IP telemetry", e);
476
+ }
477
+ }
478
+ getProfile() {
479
+ return {
480
+ userAgent: this.profile.userAgent || "unknown",
481
+ language: this.profile.language || "unknown",
482
+ platform: this.profile.platform || "unknown",
483
+ hardwareConcurrency: this.profile.hardwareConcurrency || 0,
484
+ deviceMemory: this.profile.deviceMemory || 0,
485
+ screenResolution: this.profile.screenResolution || "unknown",
486
+ timezoneOffset: this.profile.timezoneOffset || 0,
487
+ timezoneName: this.profile.timezoneName || "unknown",
488
+ location: this.profile.location || { lat: null, lng: null, permissionGranted: false },
489
+ ipData: this.profile.ipData
490
+ };
491
+ }
492
+ };
493
+
494
+ // src/core/truvaxia.ts
495
+ var Truvaxia = class _Truvaxia {
496
+ static instance = null;
497
+ config = null;
498
+ inputTracker;
499
+ livenessDetector;
500
+ policyClient;
501
+ widgetManager;
502
+ deviceFingerprint;
503
+ constructor() {
504
+ this.inputTracker = new InputTracker();
505
+ this.livenessDetector = new LivenessDetector();
506
+ this.policyClient = new PolicyClient();
507
+ this.widgetManager = new WidgetManager();
508
+ this.deviceFingerprint = new DeviceFingerprint();
509
+ }
510
+ static async init(config) {
511
+ if (!this.instance) {
512
+ this.instance = new _Truvaxia();
513
+ this.instance.config = config;
514
+ this.instance.livenessDetector.preload().catch(console.error);
515
+ await this.instance.policyClient.fetchPolicy(config.staffId);
516
+ this.instance.inputTracker.startTracking(config.staffId);
517
+ console.log(`[Truvaxia] SDK initialized for staff: ${config.staffId}`);
518
+ }
519
+ return this.instance;
520
+ }
521
+ static get liveness() {
522
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
523
+ return {
524
+ start: async (onProgress) => {
525
+ return await this.instance.livenessDetector.start(onProgress);
526
+ },
527
+ execute: async () => {
528
+ return await this.instance.livenessDetector.execute();
529
+ }
530
+ };
531
+ }
532
+ /**
533
+ * Universal drop-in widget for identity verification.
534
+ * Mounts the scanner UI, captures biometrics, and handles the backend verification automatically.
535
+ */
536
+ static async verifyOnboarding(data, callbacks) {
537
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
538
+ const widget = this.instance.widgetManager;
539
+ const liveness = this.instance.livenessDetector;
540
+ let isCancelled = false;
541
+ widget.mount(`${data.firstName || ""} ${data.lastName || ""}`.trim(), () => {
542
+ isCancelled = true;
543
+ callbacks.onFailure({ message: "User cancelled verification" });
544
+ });
545
+ try {
546
+ await new Promise((resolve, reject) => {
547
+ liveness.start((stage) => {
548
+ if (isCancelled) return;
549
+ switch (stage) {
550
+ case "BLINK":
551
+ widget.updateInstruction("Please Blink");
552
+ break;
553
+ case "TURN_LEFT":
554
+ widget.updateInstruction("Turn Head Left");
555
+ break;
556
+ case "TURN_RIGHT":
557
+ widget.updateInstruction("Turn Head Right");
558
+ break;
559
+ case "LOOK_STRAIGHT":
560
+ widget.updateInstruction("Look Straight Ahead");
561
+ break;
562
+ case "COMPLETED":
563
+ widget.updateInstruction("Capturing...");
564
+ resolve();
565
+ break;
566
+ }
567
+ }).then((stream) => {
568
+ if (isCancelled) return;
569
+ const videoElement = widget.showScanning();
570
+ videoElement.srcObject = stream;
571
+ }).catch(reject);
572
+ });
573
+ if (isCancelled) return;
574
+ await new Promise((r) => setTimeout(r, 400));
575
+ if (isCancelled) return;
576
+ const result = await liveness.execute();
577
+ if (!result.success || !result.frameBase64) {
578
+ throw new Error("Liveness capture failed");
579
+ }
580
+ widget.showProcessing();
581
+ await Promise.all([
582
+ this.instance.deviceFingerprint.requestLocation(),
583
+ this.instance.deviceFingerprint.collectIpTelemetry()
584
+ ]);
585
+ const deviceProfile = this.instance.deviceFingerprint.getProfile();
586
+ const behavioralLogs = this.instance.inputTracker.getSessionData();
587
+ const payload = {
588
+ actionType: "ONBOARDING",
589
+ businessData: { ...data, biometricFrame: result.frameBase64, biometricVideo: result.videoBase64 },
590
+ securityData: {
591
+ behavioral: behavioralLogs,
592
+ device: deviceProfile,
593
+ staffId: this.instance.config?.staffId
594
+ }
595
+ };
596
+ const response = await fetch("http://localhost:3001/verify", {
597
+ method: "POST",
598
+ headers: { "Content-Type": "application/json" },
599
+ body: JSON.stringify(payload)
600
+ });
601
+ const backendResult = await response.json();
602
+ if (!isCancelled) {
603
+ widget.unmount();
604
+ if (response.ok && backendResult.status === "APPROVED") {
605
+ callbacks.onSuccess(backendResult);
606
+ } else {
607
+ callbacks.onFailure(backendResult);
608
+ }
609
+ }
610
+ } catch (e) {
611
+ if (!isCancelled) {
612
+ widget.unmount();
613
+ callbacks.onFailure({ message: e instanceof Error ? e.message : "Unknown error during onboarding" });
614
+ }
615
+ }
616
+ }
617
+ /**
618
+ * Dynamic Document Extraction
619
+ * Sends an image and a JSON schema to the backend, which parses the image using OCR + Groq LLM
620
+ * and returns perfectly structured JSON.
621
+ */
622
+ static async extractDocument(payload) {
623
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
624
+ try {
625
+ const response = await fetch("http://localhost:3001/api/v1/extract-document", {
626
+ method: "POST",
627
+ headers: { "Content-Type": "application/json" },
628
+ body: JSON.stringify(payload)
629
+ });
630
+ const result = await response.json();
631
+ if (!response.ok) {
632
+ throw new Error(result.error || "Failed to extract document data");
633
+ }
634
+ return result.data;
635
+ } catch (e) {
636
+ console.error("[Truvaxia] Document extraction error:", e);
637
+ throw e;
638
+ }
639
+ }
640
+ static async process(actionType, data, callbacks) {
641
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
642
+ callbacks.onFailure({ message: "Use verifyOnboarding() for the full widget experience." });
643
+ }
644
+ };
645
+ // Annotate the CommonJS export names for ESM import in node:
646
+ 0 && (module.exports = {
647
+ Truvaxia
648
+ });
@@ -0,0 +1,621 @@
1
+ // src/modules/behavioral/input-tracker.ts
2
+ var InputTracker = class {
3
+ staffId = null;
4
+ sessionStartTime = 0;
5
+ keystrokeCount = 0;
6
+ pasteCount = 0;
7
+ trackingInterval = null;
8
+ startTracking(staffId) {
9
+ if (this.staffId) return;
10
+ this.staffId = staffId;
11
+ this.sessionStartTime = Date.now();
12
+ if (typeof document !== "undefined") {
13
+ document.addEventListener("keydown", this.handleKeyDown);
14
+ document.addEventListener("paste", this.handlePaste);
15
+ }
16
+ this.trackingInterval = setInterval(() => {
17
+ this.evaluateTelemetry();
18
+ }, 1e4);
19
+ }
20
+ stopTracking() {
21
+ if (typeof document !== "undefined") {
22
+ document.removeEventListener("keydown", this.handleKeyDown);
23
+ document.removeEventListener("paste", this.handlePaste);
24
+ }
25
+ if (this.trackingInterval) {
26
+ clearInterval(this.trackingInterval);
27
+ this.trackingInterval = null;
28
+ }
29
+ this.staffId = null;
30
+ }
31
+ handleKeyDown = (event) => {
32
+ if (["Shift", "Control", "Alt", "Meta", "CapsLock"].includes(event.key)) {
33
+ return;
34
+ }
35
+ this.keystrokeCount++;
36
+ };
37
+ handlePaste = (event) => {
38
+ const target = event.target;
39
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")) {
40
+ this.pasteCount++;
41
+ }
42
+ };
43
+ evaluateTelemetry() {
44
+ if (!this.staffId) return;
45
+ const elapsedMinutes = (Date.now() - this.sessionStartTime) / 6e4;
46
+ const kpm = elapsedMinutes > 0 ? this.keystrokeCount / elapsedMinutes : 0;
47
+ const isAnomalous = kpm > 300 || this.pasteCount > 3;
48
+ const profile = {
49
+ staffId: this.staffId,
50
+ sessionStartTime: this.sessionStartTime,
51
+ keystrokesPerMinute: Math.round(kpm),
52
+ pasteEventsCount: this.pasteCount,
53
+ totalKeystrokes: this.keystrokeCount,
54
+ isAnomalous
55
+ };
56
+ if (isAnomalous) {
57
+ console.warn(`[Truvaxia:FraudAlert] Anomalous behavior detected for RO: ${this.staffId}`, profile);
58
+ }
59
+ }
60
+ getSessionData() {
61
+ const elapsedMinutes = (Date.now() - this.sessionStartTime) / 6e4;
62
+ const kpm = elapsedMinutes > 0 ? this.keystrokeCount / elapsedMinutes : 0;
63
+ return {
64
+ staffId: this.staffId || "",
65
+ sessionStartTime: this.sessionStartTime,
66
+ keystrokesPerMinute: Math.round(kpm),
67
+ pasteEventsCount: this.pasteCount,
68
+ totalKeystrokes: this.keystrokeCount,
69
+ isAnomalous: kpm > 300 || this.pasteCount > 3
70
+ };
71
+ }
72
+ };
73
+
74
+ // src/modules/biometrics/liveness.ts
75
+ import { FaceLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";
76
+ var LivenessDetector = class {
77
+ faceLandmarker = null;
78
+ videoElement = null;
79
+ lastVideoTime = -1;
80
+ isDetecting = false;
81
+ animationFrameId = null;
82
+ mediaRecorder = null;
83
+ recordedChunks = [];
84
+ currentStage = "BLINK" /* BLINK */;
85
+ onProgressCallback;
86
+ preloaded = false;
87
+ preloadingPromise = null;
88
+ /**
89
+ * Preloads the heavy MediaPipe WASM models without requesting the camera.
90
+ */
91
+ async preload() {
92
+ if (this.preloaded) return;
93
+ if (this.preloadingPromise) return this.preloadingPromise;
94
+ console.log("[Truvaxia:Liveness] Preloading MediaPipe WASM in background...");
95
+ this.preloadingPromise = (async () => {
96
+ try {
97
+ const vision = await FilesetResolver.forVisionTasks(
98
+ "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.3/wasm"
99
+ );
100
+ this.faceLandmarker = await FaceLandmarker.createFromOptions(vision, {
101
+ baseOptions: {
102
+ modelAssetPath: "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task",
103
+ delegate: "GPU"
104
+ },
105
+ runningMode: "VIDEO",
106
+ numFaces: 1,
107
+ outputFaceBlendshapes: true,
108
+ outputFacialTransformationMatrixes: true
109
+ });
110
+ this.preloaded = true;
111
+ console.log("[Truvaxia:Liveness] MediaPipe WASM preloaded successfully.");
112
+ } catch (error) {
113
+ console.error("[Truvaxia:Liveness] Failed to preload MediaPipe WASM:", error);
114
+ }
115
+ })();
116
+ return this.preloadingPromise;
117
+ }
118
+ /**
119
+ * Initializes the camera and begins the actual detection loop.
120
+ * This should be called when the user opens the scanner.
121
+ */
122
+ async start(onProgress) {
123
+ console.log("[Truvaxia:Liveness] Starting Liveness module...");
124
+ this.onProgressCallback = onProgress;
125
+ this.currentStage = "BLINK" /* BLINK */;
126
+ if (this.onProgressCallback) this.onProgressCallback(this.currentStage);
127
+ try {
128
+ if (!this.preloaded) {
129
+ await this.preload();
130
+ }
131
+ this.videoElement = document.createElement("video");
132
+ this.videoElement.setAttribute("autoplay", "");
133
+ this.videoElement.setAttribute("playsinline", "");
134
+ const stream = await navigator.mediaDevices.getUserMedia({
135
+ video: { width: 1280, height: 720, facingMode: "user" }
136
+ });
137
+ this.videoElement.srcObject = stream;
138
+ this.recordedChunks = [];
139
+ try {
140
+ this.mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm" });
141
+ this.mediaRecorder.ondataavailable = (event) => {
142
+ if (event.data.size > 0) {
143
+ this.recordedChunks.push(event.data);
144
+ }
145
+ };
146
+ this.mediaRecorder.start();
147
+ } catch (e) {
148
+ console.error("[Truvaxia:Liveness] Failed to start MediaRecorder:", e);
149
+ }
150
+ this.videoElement.addEventListener("loadeddata", () => {
151
+ this.isDetecting = true;
152
+ this.detectLoop();
153
+ });
154
+ return stream;
155
+ } catch (error) {
156
+ console.error("[Truvaxia:Liveness] Failed to start camera or load model:", error);
157
+ throw error;
158
+ }
159
+ }
160
+ advanceStage(nextStage) {
161
+ this.currentStage = nextStage;
162
+ console.log(`[Truvaxia:Liveness] Advanced to stage: ${nextStage}`);
163
+ if (this.onProgressCallback) {
164
+ this.onProgressCallback(nextStage);
165
+ }
166
+ }
167
+ /**
168
+ * Continuously analyzes the video stream for liveness indicators (e.g., blinking).
169
+ */
170
+ detectLoop = () => {
171
+ if (!this.isDetecting || !this.videoElement || !this.faceLandmarker) return;
172
+ const startTimeMs = performance.now();
173
+ if (this.lastVideoTime !== this.videoElement.currentTime) {
174
+ this.lastVideoTime = this.videoElement.currentTime;
175
+ const results = this.faceLandmarker.detectForVideo(this.videoElement, startTimeMs);
176
+ if (results.faceBlendshapes && results.faceBlendshapes.length > 0 && results.faceLandmarks && results.faceLandmarks.length > 0) {
177
+ const blendshapes = results.faceBlendshapes[0].categories;
178
+ const landmarks = results.faceLandmarks[0];
179
+ const leftBlink = blendshapes.find((b) => b.categoryName === "eyeBlinkLeft")?.score || 0;
180
+ const rightBlink = blendshapes.find((b) => b.categoryName === "eyeBlinkRight")?.score || 0;
181
+ const noseX = landmarks[1].x;
182
+ const leftCheekX = landmarks[234].x;
183
+ const rightCheekX = landmarks[454].x;
184
+ const distLeft = Math.abs(noseX - leftCheekX);
185
+ const distRight = Math.abs(noseX - rightCheekX);
186
+ const ratio = distLeft / (distRight + 1e-4);
187
+ switch (this.currentStage) {
188
+ case "BLINK" /* BLINK */:
189
+ if (leftBlink > 0.4 && rightBlink > 0.4) {
190
+ this.advanceStage("TURN_LEFT" /* TURN_LEFT */);
191
+ }
192
+ break;
193
+ case "TURN_LEFT" /* TURN_LEFT */:
194
+ if (ratio < 0.35) {
195
+ this.advanceStage("TURN_RIGHT" /* TURN_RIGHT */);
196
+ }
197
+ break;
198
+ case "TURN_RIGHT" /* TURN_RIGHT */:
199
+ if (ratio > 2.5) {
200
+ this.advanceStage("LOOK_STRAIGHT" /* LOOK_STRAIGHT */);
201
+ }
202
+ break;
203
+ case "LOOK_STRAIGHT" /* LOOK_STRAIGHT */:
204
+ if (ratio > 0.7 && ratio < 1.3) {
205
+ this.advanceStage("COMPLETED" /* COMPLETED */);
206
+ }
207
+ break;
208
+ case "COMPLETED" /* COMPLETED */:
209
+ break;
210
+ }
211
+ }
212
+ }
213
+ if (this.currentStage !== "COMPLETED" /* COMPLETED */) {
214
+ this.animationFrameId = requestAnimationFrame(this.detectLoop);
215
+ }
216
+ };
217
+ /**
218
+ * Called upon form submission. Evaluates liveness status and captures the frame.
219
+ */
220
+ async execute() {
221
+ console.log("[Truvaxia:Liveness] Executing final validation...");
222
+ this.isDetecting = false;
223
+ if (this.animationFrameId) {
224
+ cancelAnimationFrame(this.animationFrameId);
225
+ }
226
+ if (this.currentStage !== "COMPLETED" /* COMPLETED */) {
227
+ return { success: false, reason: "Liveness check failed: Sequence not completed." };
228
+ }
229
+ if (!this.videoElement) {
230
+ return { success: false, reason: "Video stream unavailable." };
231
+ }
232
+ const canvas = document.createElement("canvas");
233
+ canvas.width = this.videoElement.videoWidth;
234
+ canvas.height = this.videoElement.videoHeight;
235
+ const ctx = canvas.getContext("2d");
236
+ let base64Image;
237
+ if (ctx) {
238
+ ctx.drawImage(this.videoElement, 0, 0, canvas.width, canvas.height);
239
+ base64Image = canvas.toDataURL("image/jpeg", 0.9);
240
+ }
241
+ let videoBase64;
242
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
243
+ videoBase64 = await new Promise((resolve) => {
244
+ this.mediaRecorder.onstop = () => {
245
+ const blob = new Blob(this.recordedChunks, { type: "video/webm" });
246
+ const reader = new FileReader();
247
+ reader.readAsDataURL(blob);
248
+ reader.onloadend = () => resolve(reader.result);
249
+ reader.onerror = () => resolve(void 0);
250
+ };
251
+ this.mediaRecorder.stop();
252
+ });
253
+ }
254
+ const stream = this.videoElement.srcObject;
255
+ if (stream) {
256
+ stream.getTracks().forEach((track) => track.stop());
257
+ }
258
+ this.videoElement.srcObject = null;
259
+ if (base64Image) {
260
+ return { success: true, frameBase64: base64Image, videoBase64 };
261
+ }
262
+ return { success: false, reason: "Failed to extract frame from canvas." };
263
+ }
264
+ };
265
+
266
+ // src/modules/policy/policy-client.ts
267
+ var PolicyClient = class {
268
+ currentPolicy = {
269
+ requireBiometrics: true,
270
+ fraudScoreThreshold: 80,
271
+ enableGeofencing: false
272
+ };
273
+ /**
274
+ * Fetches the organization's current policy from the Truvaxia Backend.
275
+ * @param staffId The staff ID to fetch the specific branch/org policy for.
276
+ */
277
+ async fetchPolicy(staffId) {
278
+ try {
279
+ console.log(`[Truvaxia:Policy] Fetching policies for staff: ${staffId}...`);
280
+ await new Promise((resolve) => setTimeout(resolve, 500));
281
+ console.log("[Truvaxia:Policy] Policies loaded successfully.", this.currentPolicy);
282
+ return this.currentPolicy;
283
+ } catch (error) {
284
+ console.error("[Truvaxia:Policy] Failed to fetch policy. Falling back to strict defaults.", error);
285
+ return this.currentPolicy;
286
+ }
287
+ }
288
+ getPolicy() {
289
+ return this.currentPolicy;
290
+ }
291
+ };
292
+
293
+ // src/modules/ui/widget-manager.ts
294
+ var WidgetManager = class {
295
+ container = null;
296
+ videoElement = null;
297
+ instructionText = null;
298
+ contentBox = null;
299
+ mount(customerName, onClose) {
300
+ if (this.container) return;
301
+ this.container = document.createElement("div");
302
+ this.container.id = "truvaxia-widget-root";
303
+ this.container.setAttribute("style", "position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 99999; display: flex; align-items: center; justify-content: center; padding: 1rem; background: rgba(0,0,0,0.8); backdrop-filter: blur(4px); font-family: ui-sans-serif, system-ui, sans-serif;");
304
+ this.container.innerHTML = `
305
+ <div style="width: 100%; max-width: 36rem; border-radius: 1rem; overflow: hidden; border: 1px solid rgba(0,255,178,0.2); background: rgba(10,15,20,0.85); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);">
306
+ <!-- Header -->
307
+ <div style="padding: 1.5rem; border-bottom: 1px solid rgba(0,255,178,0.2); display: flex; justify-content: space-between; align-items: center; background: rgba(5,10,15,0.9);">
308
+ <div>
309
+ <h3 style="font-size: 1.25rem; font-weight: 700; color: white; margin: 0;">Zero-Trust Identity Verification</h3>
310
+ <p style="font-size: 0.875rem; color: #94a3b8; margin: 0.25rem 0 0 0;">Subject: ${customerName || "Guest"}</p>
311
+ </div>
312
+ <button id="truvaxia-close-btn" style="background: none; border: none; color: #94a3b8; font-size: 1.5rem; cursor: pointer; padding: 0.5rem;">\u2715</button>
313
+ </div>
314
+
315
+ <!-- Content -->
316
+ <div id="truvaxia-content" style="padding: 2rem; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 400px;">
317
+ <!-- Dynamic Content Here -->
318
+ </div>
319
+ </div>
320
+ `;
321
+ document.body.appendChild(this.container);
322
+ const closeBtn = document.getElementById("truvaxia-close-btn");
323
+ if (closeBtn) {
324
+ closeBtn.onclick = () => {
325
+ this.unmount();
326
+ onClose();
327
+ };
328
+ }
329
+ this.contentBox = document.getElementById("truvaxia-content");
330
+ this.showInitializing();
331
+ }
332
+ showInitializing() {
333
+ if (!this.contentBox) return;
334
+ this.contentBox.innerHTML = `
335
+ <div style="display: flex; flex-direction: column; align-items: center; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;">
336
+ <div style="width: 4rem; height: 4rem; border-radius: 9999px; border-top: 2px solid transparent; border-right: 2px solid #00ffb2; border-bottom: 2px solid #00ffb2; border-left: 2px solid #00ffb2; animation: spin 1s linear infinite; margin-bottom: 1rem;"></div>
337
+ <p style="color: #00ffb2; font-weight: 500; text-shadow: 0 0 10px rgba(0,255,178,0.5);">Initializing...</p>
338
+ </div>
339
+ <style>
340
+ @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
341
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
342
+ </style>
343
+ `;
344
+ }
345
+ showScanning() {
346
+ if (!this.contentBox) throw new Error("Widget not mounted");
347
+ this.contentBox.innerHTML = `
348
+ <div style="display: flex; flex-direction: column; align-items: center; width: 100%;">
349
+ <div style="position: relative; width: 16rem; height: 16rem; border-radius: 9999px; border: 2px solid rgba(0,255,178,0.5); display: flex; align-items: center; justify-content: center; overflow: hidden; margin-bottom: 2rem;">
350
+ <div style="position: absolute; inset: 0; border-top: 4px solid #00ffb2; border-right: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 4px solid transparent; border-radius: 9999px; animation: spin 2s linear infinite; opacity: 0.7; z-index: 10;"></div>
351
+ <video id="truvaxia-video" autoplay playsinline muted style="width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1);"></video>
352
+ </div>
353
+ <div style="padding: 0.75rem 2rem; border-radius: 9999px; display: flex; align-items: center; gap: 0.75rem; background: rgba(5,10,15,0.9); border: 1px solid rgba(0,255,178,0.2);">
354
+ <div style="width: 0.5rem; height: 0.5rem; border-radius: 9999px; background: #00ffb2; box-shadow: 0 0 10px #00ffb2; animation: pulse 2s infinite;"></div>
355
+ <p id="truvaxia-instruction" style="color: white; font-weight: 500; letter-spacing: 0.025em; margin: 0;">Please wait...</p>
356
+ </div>
357
+ </div>
358
+ `;
359
+ this.videoElement = document.getElementById("truvaxia-video");
360
+ this.instructionText = document.getElementById("truvaxia-instruction");
361
+ return this.videoElement;
362
+ }
363
+ updateInstruction(text) {
364
+ if (this.instructionText) {
365
+ this.instructionText.innerText = text;
366
+ }
367
+ }
368
+ showProcessing() {
369
+ if (!this.contentBox) return;
370
+ this.contentBox.innerHTML = `
371
+ <div style="display: flex; flex-direction: column; align-items: center; animation: pulse 2s infinite;">
372
+ <div style="width: 4rem; height: 4rem; border-radius: 9999px; border-top: 2px solid transparent; border-right: 2px solid #00ffb2; border-bottom: 2px solid #00ffb2; border-left: 2px solid #00ffb2; animation: spin 1s linear infinite; margin-bottom: 1rem;"></div>
373
+ <p style="color: #00ffb2; font-weight: 600; letter-spacing: 0.1em;">ANALYZING...</p>
374
+ </div>
375
+ `;
376
+ }
377
+ unmount() {
378
+ if (this.container && this.container.parentNode) {
379
+ this.container.parentNode.removeChild(this.container);
380
+ }
381
+ this.container = null;
382
+ this.videoElement = null;
383
+ this.instructionText = null;
384
+ this.contentBox = null;
385
+ }
386
+ };
387
+
388
+ // src/modules/device/fingerprint.ts
389
+ var DeviceFingerprint = class {
390
+ profile = {};
391
+ constructor() {
392
+ this.collectBasicTelemetry();
393
+ }
394
+ collectBasicTelemetry() {
395
+ if (typeof window === "undefined") return;
396
+ this.profile.userAgent = navigator.userAgent;
397
+ this.profile.language = navigator.language;
398
+ this.profile.platform = navigator.platform || "";
399
+ this.profile.hardwareConcurrency = navigator.hardwareConcurrency || 0;
400
+ this.profile.deviceMemory = navigator.deviceMemory || 0;
401
+ if (window.screen) {
402
+ this.profile.screenResolution = `${window.screen.width}x${window.screen.height}x${window.screen.colorDepth}`;
403
+ }
404
+ this.profile.timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
405
+ this.profile.timezoneName = Intl.DateTimeFormat().resolvedOptions().timeZone;
406
+ }
407
+ /**
408
+ * Prompts the user for location access and returns the current coordinates.
409
+ * If denied, records that permission was refused.
410
+ */
411
+ async requestLocation() {
412
+ if (typeof navigator === "undefined" || !navigator.geolocation) {
413
+ this.profile.location = { lat: null, lng: null, permissionGranted: false };
414
+ return;
415
+ }
416
+ try {
417
+ const position = await new Promise((resolve, reject) => {
418
+ navigator.geolocation.getCurrentPosition(resolve, reject, {
419
+ enableHighAccuracy: true,
420
+ timeout: 5e3,
421
+ maximumAge: 0
422
+ });
423
+ });
424
+ this.profile.location = {
425
+ lat: position.coords.latitude,
426
+ lng: position.coords.longitude,
427
+ permissionGranted: true
428
+ };
429
+ } catch (error) {
430
+ console.warn("[Truvaxia] Location access denied or timed out.", error);
431
+ this.profile.location = { lat: null, lng: null, permissionGranted: false };
432
+ }
433
+ }
434
+ async collectIpTelemetry() {
435
+ if (typeof window === "undefined") return;
436
+ try {
437
+ const response = await fetch("https://ipapi.co/json/");
438
+ if (response.ok) {
439
+ const data = await response.json();
440
+ this.profile.ipData = {
441
+ ip: data.ip || "unknown",
442
+ city: data.city || "unknown",
443
+ region: data.region || "unknown",
444
+ country: data.country_name || "unknown",
445
+ org: data.org || "unknown"
446
+ };
447
+ }
448
+ } catch (e) {
449
+ console.warn("[Truvaxia] Could not fetch IP telemetry", e);
450
+ }
451
+ }
452
+ getProfile() {
453
+ return {
454
+ userAgent: this.profile.userAgent || "unknown",
455
+ language: this.profile.language || "unknown",
456
+ platform: this.profile.platform || "unknown",
457
+ hardwareConcurrency: this.profile.hardwareConcurrency || 0,
458
+ deviceMemory: this.profile.deviceMemory || 0,
459
+ screenResolution: this.profile.screenResolution || "unknown",
460
+ timezoneOffset: this.profile.timezoneOffset || 0,
461
+ timezoneName: this.profile.timezoneName || "unknown",
462
+ location: this.profile.location || { lat: null, lng: null, permissionGranted: false },
463
+ ipData: this.profile.ipData
464
+ };
465
+ }
466
+ };
467
+
468
+ // src/core/truvaxia.ts
469
+ var Truvaxia = class _Truvaxia {
470
+ static instance = null;
471
+ config = null;
472
+ inputTracker;
473
+ livenessDetector;
474
+ policyClient;
475
+ widgetManager;
476
+ deviceFingerprint;
477
+ constructor() {
478
+ this.inputTracker = new InputTracker();
479
+ this.livenessDetector = new LivenessDetector();
480
+ this.policyClient = new PolicyClient();
481
+ this.widgetManager = new WidgetManager();
482
+ this.deviceFingerprint = new DeviceFingerprint();
483
+ }
484
+ static async init(config) {
485
+ if (!this.instance) {
486
+ this.instance = new _Truvaxia();
487
+ this.instance.config = config;
488
+ this.instance.livenessDetector.preload().catch(console.error);
489
+ await this.instance.policyClient.fetchPolicy(config.staffId);
490
+ this.instance.inputTracker.startTracking(config.staffId);
491
+ console.log(`[Truvaxia] SDK initialized for staff: ${config.staffId}`);
492
+ }
493
+ return this.instance;
494
+ }
495
+ static get liveness() {
496
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
497
+ return {
498
+ start: async (onProgress) => {
499
+ return await this.instance.livenessDetector.start(onProgress);
500
+ },
501
+ execute: async () => {
502
+ return await this.instance.livenessDetector.execute();
503
+ }
504
+ };
505
+ }
506
+ /**
507
+ * Universal drop-in widget for identity verification.
508
+ * Mounts the scanner UI, captures biometrics, and handles the backend verification automatically.
509
+ */
510
+ static async verifyOnboarding(data, callbacks) {
511
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
512
+ const widget = this.instance.widgetManager;
513
+ const liveness = this.instance.livenessDetector;
514
+ let isCancelled = false;
515
+ widget.mount(`${data.firstName || ""} ${data.lastName || ""}`.trim(), () => {
516
+ isCancelled = true;
517
+ callbacks.onFailure({ message: "User cancelled verification" });
518
+ });
519
+ try {
520
+ await new Promise((resolve, reject) => {
521
+ liveness.start((stage) => {
522
+ if (isCancelled) return;
523
+ switch (stage) {
524
+ case "BLINK":
525
+ widget.updateInstruction("Please Blink");
526
+ break;
527
+ case "TURN_LEFT":
528
+ widget.updateInstruction("Turn Head Left");
529
+ break;
530
+ case "TURN_RIGHT":
531
+ widget.updateInstruction("Turn Head Right");
532
+ break;
533
+ case "LOOK_STRAIGHT":
534
+ widget.updateInstruction("Look Straight Ahead");
535
+ break;
536
+ case "COMPLETED":
537
+ widget.updateInstruction("Capturing...");
538
+ resolve();
539
+ break;
540
+ }
541
+ }).then((stream) => {
542
+ if (isCancelled) return;
543
+ const videoElement = widget.showScanning();
544
+ videoElement.srcObject = stream;
545
+ }).catch(reject);
546
+ });
547
+ if (isCancelled) return;
548
+ await new Promise((r) => setTimeout(r, 400));
549
+ if (isCancelled) return;
550
+ const result = await liveness.execute();
551
+ if (!result.success || !result.frameBase64) {
552
+ throw new Error("Liveness capture failed");
553
+ }
554
+ widget.showProcessing();
555
+ await Promise.all([
556
+ this.instance.deviceFingerprint.requestLocation(),
557
+ this.instance.deviceFingerprint.collectIpTelemetry()
558
+ ]);
559
+ const deviceProfile = this.instance.deviceFingerprint.getProfile();
560
+ const behavioralLogs = this.instance.inputTracker.getSessionData();
561
+ const payload = {
562
+ actionType: "ONBOARDING",
563
+ businessData: { ...data, biometricFrame: result.frameBase64, biometricVideo: result.videoBase64 },
564
+ securityData: {
565
+ behavioral: behavioralLogs,
566
+ device: deviceProfile,
567
+ staffId: this.instance.config?.staffId
568
+ }
569
+ };
570
+ const response = await fetch("http://localhost:3001/verify", {
571
+ method: "POST",
572
+ headers: { "Content-Type": "application/json" },
573
+ body: JSON.stringify(payload)
574
+ });
575
+ const backendResult = await response.json();
576
+ if (!isCancelled) {
577
+ widget.unmount();
578
+ if (response.ok && backendResult.status === "APPROVED") {
579
+ callbacks.onSuccess(backendResult);
580
+ } else {
581
+ callbacks.onFailure(backendResult);
582
+ }
583
+ }
584
+ } catch (e) {
585
+ if (!isCancelled) {
586
+ widget.unmount();
587
+ callbacks.onFailure({ message: e instanceof Error ? e.message : "Unknown error during onboarding" });
588
+ }
589
+ }
590
+ }
591
+ /**
592
+ * Dynamic Document Extraction
593
+ * Sends an image and a JSON schema to the backend, which parses the image using OCR + Groq LLM
594
+ * and returns perfectly structured JSON.
595
+ */
596
+ static async extractDocument(payload) {
597
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
598
+ try {
599
+ const response = await fetch("http://localhost:3001/api/v1/extract-document", {
600
+ method: "POST",
601
+ headers: { "Content-Type": "application/json" },
602
+ body: JSON.stringify(payload)
603
+ });
604
+ const result = await response.json();
605
+ if (!response.ok) {
606
+ throw new Error(result.error || "Failed to extract document data");
607
+ }
608
+ return result.data;
609
+ } catch (e) {
610
+ console.error("[Truvaxia] Document extraction error:", e);
611
+ throw e;
612
+ }
613
+ }
614
+ static async process(actionType, data, callbacks) {
615
+ if (!this.instance) throw new Error("Truvaxia must be initialized first");
616
+ callbacks.onFailure({ message: "Use verifyOnboarding() for the full widget experience." });
617
+ }
618
+ };
619
+ export {
620
+ Truvaxia
621
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "truvaxia-ai-sdk",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "files": [
6
+ "dist"
7
+ ],
8
+ "main": "./dist/truvaxia.cjs",
9
+ "module": "./dist/truvaxia.js",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/truvaxia.js",
13
+ "require": "./dist/truvaxia.cjs"
14
+ }
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/AbdlKabeer/truvaxia-sdk.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/AbdlKabeer/truvaxia-sdk/issues"
25
+ },
26
+ "homepage": "https://github.com/AbdlKabeer/truvaxia-sdk#readme",
27
+ "scripts": {
28
+ "build": "tsup src/core/truvaxia.ts --format esm,cjs --clean"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^25.9.3",
32
+ "tsup": "^8.5.1",
33
+ "typescript": "^6.0.3"
34
+ },
35
+ "dependencies": {
36
+ "@mediapipe/tasks-vision": "^0.10.35"
37
+ }
38
+ }