@ginger-ai/ginger-js 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -0
- package/dist/ginger.cjs.js +2 -0
- package/dist/ginger.cjs.js.map +1 -0
- package/dist/ginger.esm.d.ts +294 -0
- package/dist/ginger.esm.js +2 -0
- package/dist/ginger.esm.js.map +1 -0
- package/dist/ginger.umd.js +2 -0
- package/dist/ginger.umd.js.map +1 -0
- package/dist/types/behaviour/index.d.ts +49 -0
- package/dist/types/behaviour/index.d.ts.map +1 -0
- package/dist/types/client/index.d.ts +35 -0
- package/dist/types/client/index.d.ts.map +1 -0
- package/dist/types/core/constants.d.ts +5 -0
- package/dist/types/core/constants.d.ts.map +1 -0
- package/dist/types/core/dto/bot-detector.dto.d.ts +30 -0
- package/dist/types/core/dto/bot-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/device-detector.dto.d.ts +54 -0
- package/dist/types/core/dto/device-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/fingerprint.dto.d.ts +29 -0
- package/dist/types/core/dto/fingerprint.dto.d.ts.map +1 -0
- package/dist/types/core/dto/ginger.dto.d.ts +73 -0
- package/dist/types/core/dto/ginger.dto.d.ts.map +1 -0
- package/dist/types/core/dto/incognito-detector.dto.d.ts +4 -0
- package/dist/types/core/dto/incognito-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/index.d.ts +10 -0
- package/dist/types/core/dto/index.d.ts.map +1 -0
- package/dist/types/core/dto/metrics.dto.d.ts +19 -0
- package/dist/types/core/dto/metrics.dto.d.ts.map +1 -0
- package/dist/types/core/dto/os-detector.dto.d.ts +6 -0
- package/dist/types/core/dto/os-detector.dto.d.ts.map +1 -0
- package/dist/types/core/dto/tor-detector.dto.d.ts +5 -0
- package/dist/types/core/dto/tor-detector.dto.d.ts.map +1 -0
- package/dist/types/core/helpers.d.ts +8 -0
- package/dist/types/core/helpers.d.ts.map +1 -0
- package/dist/types/core/http/httpClient.d.ts +28 -0
- package/dist/types/core/http/httpClient.d.ts.map +1 -0
- package/dist/types/core/http/request.d.ts +4 -0
- package/dist/types/core/http/request.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +6 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/core/util/error.d.ts +25 -0
- package/dist/types/core/util/error.d.ts.map +1 -0
- package/dist/types/core/util/generate-requestid.d.ts +2 -0
- package/dist/types/core/util/generate-requestid.d.ts.map +1 -0
- package/dist/types/device/components/audio/audio.d.ts +2 -0
- package/dist/types/device/components/audio/audio.d.ts.map +1 -0
- package/dist/types/device/components/canvas/canvas.d.ts +3 -0
- package/dist/types/device/components/canvas/canvas.d.ts.map +1 -0
- package/dist/types/device/components/extra/extra.d.ts +19 -0
- package/dist/types/device/components/extra/extra.d.ts.map +1 -0
- package/dist/types/device/components/fonts/fonts.d.ts +3 -0
- package/dist/types/device/components/fonts/fonts.d.ts.map +1 -0
- package/dist/types/device/components/hardware/hardware.d.ts +2 -0
- package/dist/types/device/components/hardware/hardware.d.ts.map +1 -0
- package/dist/types/device/components/index.d.ts +15 -0
- package/dist/types/device/components/index.d.ts.map +1 -0
- package/dist/types/device/components/locales/locales.d.ts +2 -0
- package/dist/types/device/components/locales/locales.d.ts.map +1 -0
- package/dist/types/device/components/math/math.d.ts +2 -0
- package/dist/types/device/components/math/math.d.ts.map +1 -0
- package/dist/types/device/components/permissions/permissions.d.ts +3 -0
- package/dist/types/device/components/permissions/permissions.d.ts.map +1 -0
- package/dist/types/device/components/plugins/plugins.d.ts +3 -0
- package/dist/types/device/components/plugins/plugins.d.ts.map +1 -0
- package/dist/types/device/components/screen/screen.d.ts +2 -0
- package/dist/types/device/components/screen/screen.d.ts.map +1 -0
- package/dist/types/device/components/screen/screenResolution.d.ts +16 -0
- package/dist/types/device/components/screen/screenResolution.d.ts.map +1 -0
- package/dist/types/device/components/system/browser.d.ts +22 -0
- package/dist/types/device/components/system/browser.d.ts.map +1 -0
- package/dist/types/device/components/system/emoji.d.ts +2 -0
- package/dist/types/device/components/system/emoji.d.ts.map +1 -0
- package/dist/types/device/components/system/system.d.ts +2 -0
- package/dist/types/device/components/system/system.d.ts.map +1 -0
- package/dist/types/device/components/webgl/imageHash.d.ts +3 -0
- package/dist/types/device/components/webgl/imageHash.d.ts.map +1 -0
- package/dist/types/device/components/webgl/webgl.d.ts +54 -0
- package/dist/types/device/components/webgl/webgl.d.ts.map +1 -0
- package/dist/types/device/factory.d.ts +26 -0
- package/dist/types/device/factory.d.ts.map +1 -0
- package/dist/types/device/index.d.ts +61 -0
- package/dist/types/device/index.d.ts.map +1 -0
- package/dist/types/device/modules/bot.d.ts +9 -0
- package/dist/types/device/modules/bot.d.ts.map +1 -0
- package/dist/types/device/modules/browserDetails.d.ts +6 -0
- package/dist/types/device/modules/browserDetails.d.ts.map +1 -0
- package/dist/types/device/modules/device/analyze-data.d.ts +7 -0
- package/dist/types/device/modules/device/analyze-data.d.ts.map +1 -0
- package/dist/types/device/modules/device/gather-data.d.ts +6 -0
- package/dist/types/device/modules/device/gather-data.d.ts.map +1 -0
- package/dist/types/device/modules/device/helpers.d.ts +9 -0
- package/dist/types/device/modules/device/helpers.d.ts.map +1 -0
- package/dist/types/device/modules/device/index.d.ts +3 -0
- package/dist/types/device/modules/device/index.d.ts.map +1 -0
- package/dist/types/device/modules/fp.d.ts +14 -0
- package/dist/types/device/modules/fp.d.ts.map +1 -0
- package/dist/types/device/modules/incognito.d.ts +7 -0
- package/dist/types/device/modules/incognito.d.ts.map +1 -0
- package/dist/types/device/modules/options.d.ts +12 -0
- package/dist/types/device/modules/options.d.ts.map +1 -0
- package/dist/types/device/modules/os.d.ts +3 -0
- package/dist/types/device/modules/os.d.ts.map +1 -0
- package/dist/types/device/modules/tor.d.ts +4 -0
- package/dist/types/device/modules/tor.d.ts.map +1 -0
- package/dist/types/device/utils/async.d.ts +33 -0
- package/dist/types/device/utils/async.d.ts.map +1 -0
- package/dist/types/device/utils/browser_.d.ts +103 -0
- package/dist/types/device/utils/browser_.d.ts.map +1 -0
- package/dist/types/device/utils/commonPixels.d.ts +2 -0
- package/dist/types/device/utils/commonPixels.d.ts.map +1 -0
- package/dist/types/device/utils/data.d.ts +33 -0
- package/dist/types/device/utils/data.d.ts.map +1 -0
- package/dist/types/device/utils/dom.d.ts +26 -0
- package/dist/types/device/utils/dom.d.ts.map +1 -0
- package/dist/types/device/utils/ephemeralIFrame.d.ts +5 -0
- package/dist/types/device/utils/ephemeralIFrame.d.ts.map +1 -0
- package/dist/types/device/utils/getMostFrequent.d.ts +6 -0
- package/dist/types/device/utils/getMostFrequent.d.ts.map +1 -0
- package/dist/types/device/utils/hash.d.ts +6 -0
- package/dist/types/device/utils/hash.d.ts.map +1 -0
- package/dist/types/device/utils/misc.d.ts +7 -0
- package/dist/types/device/utils/misc.d.ts.map +1 -0
- package/dist/types/device/utils/raceAll.d.ts +9 -0
- package/dist/types/device/utils/raceAll.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +52 -0
- package/src/behaviour/index.ts +279 -0
- package/src/client/index.ts +132 -0
- package/src/core/constants.ts +4 -0
- package/src/core/dto/bot-detector.dto.ts +32 -0
- package/src/core/dto/device-detector.dto.ts +67 -0
- package/src/core/dto/fingerprint.dto.ts +38 -0
- package/src/core/dto/ginger.dto.ts +89 -0
- package/src/core/dto/incognito-detector.dto.ts +2 -0
- package/src/core/dto/index.ts +18 -0
- package/src/core/dto/metrics.dto.ts +20 -0
- package/src/core/dto/os-detector.dto.ts +5 -0
- package/src/core/dto/tor-detector.dto.ts +4 -0
- package/src/core/helpers.ts +33 -0
- package/src/core/http/httpClient.ts +52 -0
- package/src/core/http/request.ts +32 -0
- package/src/core/index.ts +5 -0
- package/src/core/util/error.ts +40 -0
- package/src/core/util/generate-requestid.ts +63 -0
- package/src/device/components/audio/audio.ts +58 -0
- package/src/device/components/canvas/canvas.ts +88 -0
- package/src/device/components/extra/extra.ts +581 -0
- package/src/device/components/fonts/fonts.ts +143 -0
- package/src/device/components/hardware/hardware.ts +66 -0
- package/src/device/components/index.ts +14 -0
- package/src/device/components/locales/locales.ts +21 -0
- package/src/device/components/math/math.ts +39 -0
- package/src/device/components/permissions/permissions.ts +60 -0
- package/src/device/components/plugins/plugins.ts +22 -0
- package/src/device/components/screen/screen.ts +13 -0
- package/src/device/components/screen/screenResolution.ts +45 -0
- package/src/device/components/system/browser.ts +838 -0
- package/src/device/components/system/emoji.ts +134 -0
- package/src/device/components/system/system.ts +76 -0
- package/src/device/components/webgl/imageHash.ts +144 -0
- package/src/device/components/webgl/webgl.ts +302 -0
- package/src/device/factory.ts +54 -0
- package/src/device/index.ts +60 -0
- package/src/device/modules/bot.ts +25 -0
- package/src/device/modules/browserDetails.ts +11 -0
- package/src/device/modules/device/analyze-data.ts +150 -0
- package/src/device/modules/device/gather-data.ts +92 -0
- package/src/device/modules/device/helpers.ts +123 -0
- package/src/device/modules/device/index.ts +64 -0
- package/src/device/modules/fp.ts +138 -0
- package/src/device/modules/incognito.ts +253 -0
- package/src/device/modules/options.ts +17 -0
- package/src/device/modules/os.ts +15 -0
- package/src/device/modules/tor.ts +41 -0
- package/src/device/utils/async.ts +106 -0
- package/src/device/utils/browser_.ts +347 -0
- package/src/device/utils/commonPixels.ts +38 -0
- package/src/device/utils/data.ts +161 -0
- package/src/device/utils/dom.ts +148 -0
- package/src/device/utils/ephemeralIFrame.ts +35 -0
- package/src/device/utils/getMostFrequent.ts +39 -0
- package/src/device/utils/hash.ts +202 -0
- package/src/device/utils/misc.ts +18 -0
- package/src/device/utils/raceAll.ts +19 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This code is taken from https://github.com/LinusU/murmur-128/blob/master/index.js
|
|
3
|
+
* But instead of dependencies to encode-utf8 and fmix, I've implemented them here.
|
|
4
|
+
*/
|
|
5
|
+
export declare function hash(key: ArrayBuffer | string, seed?: number): string;
|
|
6
|
+
//# sourceMappingURL=hash.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../../../src/device/utils/hash.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoLH,wBAAgB,IAAI,CAAE,GAAG,EAAG,WAAW,GAAG,MAAM,EAAE,IAAI,SAAI,GAAI,MAAM,CAkBnE"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an error object to a plain object that can be used with `JSON.stringify`.
|
|
3
|
+
* If you just run `JSON.stringify(error)`, you'll get `'{}'`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function errorToObject(error: Readonly<Error>): Record<string, unknown>;
|
|
6
|
+
export declare function isFunctionNative(func: (...args: unknown[]) => unknown): boolean;
|
|
7
|
+
//# sourceMappingURL=misc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"misc.d.ts","sourceRoot":"","sources":["../../../src/device/utils/misc.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ3E;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,GAAG,OAAO,CAE/E"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type DelayedPromise<T> = Promise<T>;
|
|
2
|
+
export declare function delay<T>(t: number, val: T): DelayedPromise<T>;
|
|
3
|
+
export interface RaceResult<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
elapsed?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function raceAll<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<(T | undefined)[]>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=raceAll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raceAll.d.ts","sourceRoot":"","sources":["../../../src/device/utils/raceAll.ts"],"names":[],"mappings":"AAAA,KAAK,cAAc,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AAEpC,wBAAgB,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAI7D;AAGD,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC,CAIjH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAE,WAAW,EAAE,cAAc,EAAC,MAAM,QAAQ,CAAC;AACxH,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ginger-ai/ginger-js",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "GingerJs JavaScript agent for Single page application (SPA)",
|
|
6
|
+
"main": "dist/ginger.cjs.js",
|
|
7
|
+
"module": "dist/ginger.esm.js",
|
|
8
|
+
"types": "dist/ginger.esm.d.ts",
|
|
9
|
+
"unpkg": "dist/ginger.umd.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/ginger.esm.js",
|
|
13
|
+
"require": "./dist/ginger.cjs.js",
|
|
14
|
+
"types": "./dist/ginger.esm.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"fraud detection",
|
|
22
|
+
"fingerprinting",
|
|
23
|
+
"fingerprint",
|
|
24
|
+
"browser identification",
|
|
25
|
+
"bot detection",
|
|
26
|
+
"spoof detection"
|
|
27
|
+
],
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"src"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "rm -rf dist/* && rm -rf types/* && rollup -c"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@rollup/plugin-commonjs": "^25.0.8",
|
|
37
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
38
|
+
"@rollup/plugin-node-resolve": "^15.3.1",
|
|
39
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
40
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
41
|
+
"@types/jest": "^29.0.0",
|
|
42
|
+
"eslint": "^8.0.0",
|
|
43
|
+
"jest": "^29.0.0",
|
|
44
|
+
"rollup": "^4.0.0",
|
|
45
|
+
"rollup-plugin-dts": "^6.2.3",
|
|
46
|
+
"typescript": "^5.9.2"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@fingerprintjs/botd": "^1.9.1",
|
|
50
|
+
"ua-parser-js": "^2.0.4"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { GingerClientError, FieldMetrics, FillEnum, FillMethod, pageVisibility, Field } from "../core";
|
|
2
|
+
|
|
3
|
+
interface TrackedFieldMetrics extends FieldMetrics {
|
|
4
|
+
readonly element: HTMLInputElement | HTMLTextAreaElement;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface EventConfig {
|
|
8
|
+
readonly pauseThresholdMs: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { getCount, addListener, removeListener } = pageVisibility();
|
|
12
|
+
|
|
13
|
+
class EventTracker {
|
|
14
|
+
private static readonly DEFAULT_CONFIG: EventConfig = {
|
|
15
|
+
pauseThresholdMs: 1500,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
private readonly config: EventConfig;
|
|
19
|
+
private isInitialized = false;
|
|
20
|
+
private readonly fields: TrackedFieldMetrics[] = [];
|
|
21
|
+
private readonly trackedFieldIds = new Set<string>();
|
|
22
|
+
|
|
23
|
+
constructor(config: Partial<EventConfig> = {}) {
|
|
24
|
+
this.config = { ...EventTracker.DEFAULT_CONFIG, ...config };
|
|
25
|
+
addListener();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize tracking for multiple form fields
|
|
30
|
+
*/
|
|
31
|
+
public initializeTracking(fields: readonly Field[]): void {
|
|
32
|
+
const newFields = fields.filter(
|
|
33
|
+
(field) => !this.trackedFieldIds.has(field.id)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
newFields.forEach((field) => this.trackField(field));
|
|
37
|
+
this.isInitialized = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get all tracked metrics data (excluding DOM elements)
|
|
42
|
+
*/
|
|
43
|
+
get metrics(): FieldMetrics[] {
|
|
44
|
+
this.ensureInitialized();
|
|
45
|
+
return this.fields.map(({ element, ...field }) => field);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove tracking from specific field or all fields
|
|
50
|
+
*/
|
|
51
|
+
public removeTracking(fieldId?: string): void {
|
|
52
|
+
if (fieldId) {
|
|
53
|
+
this.removeFieldTracking(fieldId);
|
|
54
|
+
} else {
|
|
55
|
+
this.removeAllTracking();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Private implementation methods
|
|
60
|
+
|
|
61
|
+
private trackField(field: Field): void {
|
|
62
|
+
const { id, ltm } = field;
|
|
63
|
+
const element = this.getValidElement(id);
|
|
64
|
+
const fieldMetric = this.createFieldMetric(id, element, ltm);
|
|
65
|
+
|
|
66
|
+
this.fields.push(fieldMetric);
|
|
67
|
+
this.trackedFieldIds.add(id);
|
|
68
|
+
|
|
69
|
+
// Use bound method to maintain context
|
|
70
|
+
const boundHandler = this.createInputHandler(fieldMetric);
|
|
71
|
+
element.addEventListener("input", boundHandler);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private getValidElement(id: string): HTMLInputElement | HTMLTextAreaElement {
|
|
75
|
+
const element = document.getElementById(id);
|
|
76
|
+
|
|
77
|
+
if (!element) {
|
|
78
|
+
throw new GingerClientError(
|
|
79
|
+
`Element with ID "${id}" was not found in the DOM. Please ensure the ID is correctly assigned to an input element.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!this.isValidInputElement(element)) {
|
|
84
|
+
throw new GingerClientError(
|
|
85
|
+
`Element with ID "${id}" must be an HTMLInputElement or HTMLTextAreaElement.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return element;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private isValidInputElement(
|
|
93
|
+
element: Element
|
|
94
|
+
): element is HTMLInputElement | HTMLTextAreaElement {
|
|
95
|
+
return (
|
|
96
|
+
element instanceof HTMLInputElement ||
|
|
97
|
+
element instanceof HTMLTextAreaElement
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private createFieldMetric(
|
|
102
|
+
id: string,
|
|
103
|
+
element: HTMLInputElement | HTMLTextAreaElement,
|
|
104
|
+
ltm?: boolean
|
|
105
|
+
): TrackedFieldMetrics {
|
|
106
|
+
return {
|
|
107
|
+
field_name: id,
|
|
108
|
+
started_at: 0,
|
|
109
|
+
ended_at: 0,
|
|
110
|
+
interaction_count: 0,
|
|
111
|
+
fill_method: null,
|
|
112
|
+
paste_count: 0,
|
|
113
|
+
ltm: ltm ?? false,
|
|
114
|
+
corrections_count: 0,
|
|
115
|
+
pauses: 0,
|
|
116
|
+
pauseDurations: [],
|
|
117
|
+
element,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private createInputHandler(field: TrackedFieldMetrics) {
|
|
122
|
+
return (event: Event): void => {
|
|
123
|
+
this.handleInput(field, event as InputEvent);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private handleInput(
|
|
128
|
+
currentField: TrackedFieldMetrics,
|
|
129
|
+
event: InputEvent
|
|
130
|
+
): void {
|
|
131
|
+
const now = performance.now();
|
|
132
|
+
const lastInteractionTime = currentField.ended_at || 0;
|
|
133
|
+
|
|
134
|
+
const interactions = this.analyzeInteraction(
|
|
135
|
+
currentField,
|
|
136
|
+
event,
|
|
137
|
+
now,
|
|
138
|
+
lastInteractionTime
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
this.updateFieldMetrics(currentField, interactions, now);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private analyzeInteraction(
|
|
145
|
+
field: TrackedFieldMetrics,
|
|
146
|
+
event: InputEvent,
|
|
147
|
+
now: number,
|
|
148
|
+
lastInteractionTime: number
|
|
149
|
+
) {
|
|
150
|
+
const fillMethod = this.determineFillMethod(field, event);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
fillMethod,
|
|
154
|
+
isCorrection: this.isCorrection(event),
|
|
155
|
+
isPaste: this.isPaste(fillMethod, event),
|
|
156
|
+
isPause: this.isPause(now, lastInteractionTime),
|
|
157
|
+
pauseDuration: now - lastInteractionTime,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private updateFieldMetrics(
|
|
162
|
+
field: TrackedFieldMetrics,
|
|
163
|
+
interactions: ReturnType<typeof this.analyzeInteraction>,
|
|
164
|
+
now: number
|
|
165
|
+
): void {
|
|
166
|
+
// Initialize start time on first interaction
|
|
167
|
+
if (!field.started_at) {
|
|
168
|
+
field.started_at = now;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
field.ended_at = now;
|
|
172
|
+
field.fill_method = interactions.fillMethod;
|
|
173
|
+
field.interaction_count += 1;
|
|
174
|
+
|
|
175
|
+
if (interactions.isCorrection) field.corrections_count += 1;
|
|
176
|
+
if (interactions.isPaste) field.paste_count += 1;
|
|
177
|
+
if (interactions.isPause) {
|
|
178
|
+
field.pauses += 1;
|
|
179
|
+
field.pauseDurations.push(interactions.pauseDuration);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private determineFillMethod(
|
|
184
|
+
field: TrackedFieldMetrics,
|
|
185
|
+
event: InputEvent
|
|
186
|
+
): FillMethod {
|
|
187
|
+
const inputType = event.inputType;
|
|
188
|
+
if (!inputType) return FillEnum.paste;
|
|
189
|
+
|
|
190
|
+
const currentMethod: FillMethod =
|
|
191
|
+
inputType === "insertText"
|
|
192
|
+
? FillEnum.typed
|
|
193
|
+
: inputType === "insertFromPaste"
|
|
194
|
+
? FillEnum.paste
|
|
195
|
+
: FillEnum.mixed;
|
|
196
|
+
|
|
197
|
+
// If methods have been mixed, maintain "mixed" state
|
|
198
|
+
if (field.fill_method && field.fill_method !== currentMethod) {
|
|
199
|
+
return FillEnum.mixed;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return currentMethod;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private isCorrection(event: InputEvent): boolean {
|
|
206
|
+
return event.inputType === "deleteContentBackward";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private isPaste(fillMethod: FillMethod, event: InputEvent): boolean {
|
|
210
|
+
return (
|
|
211
|
+
fillMethod === FillEnum.paste || event.inputType === "insertFromPaste"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private isPause(now: number, lastInteractionTime: number): boolean {
|
|
216
|
+
if (lastInteractionTime === 0) return false;
|
|
217
|
+
|
|
218
|
+
const timeSinceLastKeystroke = now - lastInteractionTime;
|
|
219
|
+
return timeSinceLastKeystroke > this.config.pauseThresholdMs;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private removeFieldTracking(fieldId: string): void {
|
|
223
|
+
const index = this.fields.findIndex(
|
|
224
|
+
(field) => field.field_name === fieldId
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (index === -1) return;
|
|
228
|
+
|
|
229
|
+
const field = this.fields[index];
|
|
230
|
+
this.cleanupFieldTracking(field);
|
|
231
|
+
|
|
232
|
+
this.fields.splice(index, 1);
|
|
233
|
+
this.trackedFieldIds.delete(fieldId);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private removeAllTracking(): void {
|
|
237
|
+
this.fields.forEach((field) => this.cleanupFieldTracking(field));
|
|
238
|
+
this.fields.length = 0;
|
|
239
|
+
this.trackedFieldIds.clear();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private cleanupFieldTracking(field: TrackedFieldMetrics): void {
|
|
243
|
+
// Note: We can't remove the exact handler since we're using bound methods
|
|
244
|
+
// In a real implementation, you'd want to store the bound handlers
|
|
245
|
+
field.element.removeEventListener("input", this.createInputHandler(field));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private ensureInitialized(): void {
|
|
249
|
+
if (!this.isInitialized) {
|
|
250
|
+
throw new GingerClientError(
|
|
251
|
+
"Ginger.trackEvent must be initialized before data can be fetched."
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Singleton instance for the module
|
|
258
|
+
const eventTracker = new EventTracker();
|
|
259
|
+
|
|
260
|
+
// Public API exports
|
|
261
|
+
export const trackInputs = (fields: readonly Field[]): void => {
|
|
262
|
+
eventTracker.initializeTracking(fields);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
export const getTrackedFields = (): {
|
|
266
|
+
distractions_count: number;
|
|
267
|
+
fields: FieldMetrics[];
|
|
268
|
+
} => {
|
|
269
|
+
return {
|
|
270
|
+
distractions_count: getCount(),
|
|
271
|
+
fields: eventTracker.metrics,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export const removeTracking = (fieldId?: string): void => {
|
|
276
|
+
eventTracker.removeTracking(fieldId);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export { EventTracker, removeListener };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
trackInputs,
|
|
3
|
+
getTrackedFields,
|
|
4
|
+
removeListener,
|
|
5
|
+
} from "../behaviour";
|
|
6
|
+
|
|
7
|
+
import { buildInitialPayload } from "../device";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
generateRequestId,
|
|
11
|
+
GingerClientError,
|
|
12
|
+
HttpClient,
|
|
13
|
+
GingerHttpClient,
|
|
14
|
+
BehaviourPayloadResponse,
|
|
15
|
+
Configurations,
|
|
16
|
+
EVENT_TYPES,
|
|
17
|
+
EventParams,
|
|
18
|
+
Payload,
|
|
19
|
+
PayloadResponse,
|
|
20
|
+
BehaviourPayload,
|
|
21
|
+
BehaviourParams,
|
|
22
|
+
PayloadResponseData,
|
|
23
|
+
BehaviourPayloadResponseData,
|
|
24
|
+
} from "../core";
|
|
25
|
+
|
|
26
|
+
interface GingerClient {
|
|
27
|
+
initialize(configs: Configurations): Promise<PayloadResponse | undefined>;
|
|
28
|
+
trackEvent(params: BehaviourParams): void;
|
|
29
|
+
getTrackedData(): BehaviourPayload;
|
|
30
|
+
submitEvent(): Promise<BehaviourPayloadResponseData>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class GingerJsClient implements GingerClient {
|
|
34
|
+
private readonly requestId = generateRequestId();
|
|
35
|
+
private deviceResponse: PayloadResponseData | undefined;
|
|
36
|
+
private isInitialized: boolean = false;
|
|
37
|
+
private trackDetails: EventParams | undefined;
|
|
38
|
+
private httpClient: HttpClient;
|
|
39
|
+
|
|
40
|
+
constructor({ apikey }: Configurations) {
|
|
41
|
+
this.httpClient = new GingerHttpClient(apikey);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async initialize() {
|
|
45
|
+
try {
|
|
46
|
+
const sdkInfo = { name: '@ginger-ai/ginger-js', version: '0.0.1' };
|
|
47
|
+
const payload = await buildInitialPayload(this.requestId, sdkInfo);
|
|
48
|
+
const response = await this.httpClient.post<Payload, PayloadResponse>({
|
|
49
|
+
url: `/api/v1/devices`,
|
|
50
|
+
payload,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.isInitialized = true;
|
|
54
|
+
this.deviceResponse = response.data;
|
|
55
|
+
|
|
56
|
+
return response;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.isInitialized = false;
|
|
59
|
+
throw new GingerClientError(`Initialization failed: ${error}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
trackEvent({ event_type, track_fields, request_id }: BehaviourParams): void {
|
|
64
|
+
if (!this.isInitialized) {
|
|
65
|
+
throw new GingerClientError(
|
|
66
|
+
"Unsuccessful Initialization. Cannot track behaviour."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!EVENT_TYPES.includes(event_type)) {
|
|
71
|
+
throw new GingerClientError(
|
|
72
|
+
`Invalid event type: ${event_type}. Allowed types are: ${EVENT_TYPES.join(
|
|
73
|
+
", "
|
|
74
|
+
)}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
trackInputs(track_fields);
|
|
79
|
+
this.trackDetails = {
|
|
80
|
+
event_type,
|
|
81
|
+
request_id,
|
|
82
|
+
fingerprint_id: this.deviceResponse?.fingerprint_id || "",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get tracked data
|
|
88
|
+
*/
|
|
89
|
+
getTrackedData = () => {
|
|
90
|
+
if (!this.isInitialized) {
|
|
91
|
+
throw new GingerClientError(
|
|
92
|
+
"Unsuccessful Initialization. Cannot get tracked data."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!this.trackDetails) {
|
|
97
|
+
throw new GingerClientError(
|
|
98
|
+
"Tracking not initiated. Call `trackEvent` before getting tracked data."
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const trackData = getTrackedFields();
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
event_type: this.trackDetails.event_type,
|
|
106
|
+
request_id: this.trackDetails.request_id,
|
|
107
|
+
fingerprint_id: this.trackDetails.fingerprint_id,
|
|
108
|
+
data: { ...trackData },
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Call submitEvent to submit tracked data
|
|
114
|
+
*/
|
|
115
|
+
submitEvent = async () => {
|
|
116
|
+
const payload = this.getTrackedData();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await this.httpClient.post<
|
|
120
|
+
BehaviourPayload,
|
|
121
|
+
BehaviourPayloadResponse
|
|
122
|
+
>({ url: `/api/v1/events`, payload });
|
|
123
|
+
|
|
124
|
+
removeListener();
|
|
125
|
+
return response.data;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new GingerClientError(
|
|
128
|
+
`Tracked Data submission failed: ${String(error)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
declare const BotKind: {
|
|
2
|
+
readonly Awesomium: "awesomium";
|
|
3
|
+
readonly Cef: "cef";
|
|
4
|
+
readonly CefSharp: "cefsharp";
|
|
5
|
+
readonly CoachJS: "coachjs";
|
|
6
|
+
readonly Electron: "electron";
|
|
7
|
+
readonly FMiner: "fminer";
|
|
8
|
+
readonly Geb: "geb";
|
|
9
|
+
readonly NightmareJS: "nightmarejs";
|
|
10
|
+
readonly Phantomas: "phantomas";
|
|
11
|
+
readonly PhantomJS: "phantomjs";
|
|
12
|
+
readonly Rhino: "rhino";
|
|
13
|
+
readonly Selenium: "selenium";
|
|
14
|
+
readonly Sequentum: "sequentum";
|
|
15
|
+
readonly SlimerJS: "slimerjs";
|
|
16
|
+
readonly WebDriverIO: "webdriverio";
|
|
17
|
+
readonly WebDriver: "webdriver";
|
|
18
|
+
readonly HeadlessChrome: "headless_chrome";
|
|
19
|
+
readonly Unknown: "unknown";
|
|
20
|
+
None: "none";
|
|
21
|
+
};
|
|
22
|
+
type BotKind = (typeof BotKind)[keyof typeof BotKind];
|
|
23
|
+
|
|
24
|
+
export type BotDetectionResult =
|
|
25
|
+
| {
|
|
26
|
+
status: boolean;
|
|
27
|
+
botKind: BotKind;
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
status: boolean;
|
|
31
|
+
}
|
|
32
|
+
| null;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const ALLOWED_OS = [
|
|
2
|
+
"ios",
|
|
3
|
+
"android",
|
|
4
|
+
"windows",
|
|
5
|
+
"macos",
|
|
6
|
+
"linux",
|
|
7
|
+
] as const;
|
|
8
|
+
export const ALLOWED_DEVICE_TYPES = ["mobile", "desktop", "tablet"] as const;
|
|
9
|
+
|
|
10
|
+
export type AllowedOs = (typeof ALLOWED_OS)[number];
|
|
11
|
+
export type DeviceType = (typeof ALLOWED_DEVICE_TYPES)[number] | null;
|
|
12
|
+
|
|
13
|
+
export interface BrandInfo {
|
|
14
|
+
brand: string;
|
|
15
|
+
version: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ClaimedDevice {
|
|
19
|
+
platform: string;
|
|
20
|
+
mobile: boolean;
|
|
21
|
+
brands: BrandInfo[];
|
|
22
|
+
guessedDeviceType: DeviceType | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ScreenInfo {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
pixelRatio: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WebGLInfo {
|
|
32
|
+
vendor?: string;
|
|
33
|
+
renderer?: string;
|
|
34
|
+
error: string;
|
|
35
|
+
}
|
|
36
|
+
export interface DeviceSignals {
|
|
37
|
+
actualPlatform: string;
|
|
38
|
+
touchPoints: number;
|
|
39
|
+
screen: ScreenInfo;
|
|
40
|
+
webGL: WebGLInfo;
|
|
41
|
+
readonly userAgentString: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface DeviceAnalysis {
|
|
45
|
+
confidence: number;
|
|
46
|
+
spoofingConfidence: number;
|
|
47
|
+
isSpoofed: boolean;
|
|
48
|
+
detectionConflicts: string[];
|
|
49
|
+
realPlatform: AllowedOs | null;
|
|
50
|
+
realDeviceType: DeviceType;
|
|
51
|
+
}
|
|
52
|
+
export interface DeviceResults {
|
|
53
|
+
claimed: ClaimedDevice;
|
|
54
|
+
signals: DeviceSignals;
|
|
55
|
+
analysis: DeviceAnalysis;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RefinedDeviceResults extends DeviceResults {
|
|
59
|
+
deviceOs: AllowedOs | null;
|
|
60
|
+
deviceType: DeviceType;
|
|
61
|
+
tampering: deviceTampering;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface deviceTampering {
|
|
65
|
+
status: boolean;
|
|
66
|
+
confidence?: number;
|
|
67
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface FingerprintComponentValue {
|
|
2
|
+
[key: string]:
|
|
3
|
+
| string
|
|
4
|
+
| string[]
|
|
5
|
+
| number
|
|
6
|
+
| boolean
|
|
7
|
+
| undefined
|
|
8
|
+
| null
|
|
9
|
+
| FingerprintComponentValue
|
|
10
|
+
| FingerprintComponentValue[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RawDeviceData extends FingerprintComponentValue {
|
|
14
|
+
audio?: FingerprintComponentValue;
|
|
15
|
+
fonts?: FingerprintComponentValue;
|
|
16
|
+
hardware?: FingerprintComponentValue;
|
|
17
|
+
locales?: FingerprintComponentValue;
|
|
18
|
+
permissions?: FingerprintComponentValue;
|
|
19
|
+
screen?: FingerprintComponentValue;
|
|
20
|
+
system?: FingerprintComponentValue;
|
|
21
|
+
emojiFingerprint?: FingerprintComponentValue;
|
|
22
|
+
math?: FingerprintComponentValue;
|
|
23
|
+
vendorFlavour?: FingerprintComponentValue;
|
|
24
|
+
canvas?: FingerprintComponentValue;
|
|
25
|
+
webgl?: FingerprintComponentValue;
|
|
26
|
+
plugins?: FingerprintComponentValue;
|
|
27
|
+
domBlockers?: FingerprintComponentValue;
|
|
28
|
+
forcedColors?: FingerprintComponentValue;
|
|
29
|
+
colorGamut?: FingerprintComponentValue;
|
|
30
|
+
osCpu?: FingerprintComponentValue;
|
|
31
|
+
audioLatency?: FingerprintComponentValue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GingerJsFp {
|
|
35
|
+
hash: string;
|
|
36
|
+
data: RawDeviceData;
|
|
37
|
+
componentsUsedForHash: Array<keyof RawDeviceData>;
|
|
38
|
+
}
|