@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,54 @@
|
|
|
1
|
+
import { options } from './modules/options';
|
|
2
|
+
|
|
3
|
+
// the component interface is the form of the JSON object the function's promise must return
|
|
4
|
+
export interface componentInterface {
|
|
5
|
+
[key: string]: string | string[] | number | boolean | componentInterface;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// The component function's interface is simply the promise of the above
|
|
10
|
+
export interface componentFunctionInterface {
|
|
11
|
+
(): Promise<componentInterface>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// components include a dictionary of name: function.
|
|
15
|
+
export const components: {[name: string]: componentFunctionInterface} = {};
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* includeComponent is the function each component function needs to call in order for the component to be included
|
|
20
|
+
* in the fingerprint.
|
|
21
|
+
* @param {string} name - the name identifier of the component
|
|
22
|
+
* @param {componentFunctionInterface} creationFunction - the function that implements the component
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
export const includeComponent = (name:string, creationFunction: componentFunctionInterface) => {
|
|
26
|
+
if (typeof window !== 'undefined')
|
|
27
|
+
components[name] = creationFunction;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The function turns the map of component functions to a map of Promises when called
|
|
33
|
+
* @returns {[name: string]: <Promise>componentInterface}
|
|
34
|
+
*/
|
|
35
|
+
export const getComponentPromises = () => {
|
|
36
|
+
return Object.fromEntries(
|
|
37
|
+
Object.entries(components)
|
|
38
|
+
.filter(([key]) => {
|
|
39
|
+
return !options?.exclude?.includes(key)}
|
|
40
|
+
)
|
|
41
|
+
.filter(([key]) => {
|
|
42
|
+
return options?.include?.some(e => e.includes('.'))
|
|
43
|
+
? options?.include?.some(e => e.startsWith(key))
|
|
44
|
+
: options?.include?.length === 0 || options?.include?.includes(key)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
.map(([key, value]) => [key, value()])
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
export const timeoutInstance: componentInterface = {
|
|
53
|
+
'timeout': "true"
|
|
54
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getFingerprint } from './modules/fp'
|
|
2
|
+
import { detectTorBrowser } from './modules/tor'
|
|
3
|
+
import { detectIncognito } from './modules/incognito'
|
|
4
|
+
import { botDetection } from './modules/bot'
|
|
5
|
+
import { detectBrowser } from './components/system/browser'
|
|
6
|
+
import { getOs } from './modules/os'
|
|
7
|
+
import { getDeviceDetails } from './modules/device'
|
|
8
|
+
import { getBrowserDetails } from './modules/browserDetails'
|
|
9
|
+
import './components'
|
|
10
|
+
|
|
11
|
+
export { getFingerprint, detectTorBrowser, detectIncognito, botDetection, detectBrowser, getOs, getDeviceDetails, getBrowserDetails }
|
|
12
|
+
|
|
13
|
+
interface SdkInfo {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const buildInitialPayload = async (request_id: string, sdkInfo: SdkInfo) => {
|
|
19
|
+
const [
|
|
20
|
+
fingerprint,
|
|
21
|
+
torResult,
|
|
22
|
+
botResult,
|
|
23
|
+
incognito,
|
|
24
|
+
osResult,
|
|
25
|
+
deviceDetails,
|
|
26
|
+
browserDetails,
|
|
27
|
+
] = await fetchModules();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
request_id,
|
|
31
|
+
device: {
|
|
32
|
+
fingerprint_id: fingerprint.hash,
|
|
33
|
+
type: "web",
|
|
34
|
+
os: deviceDetails.deviceOs,
|
|
35
|
+
os_version: osResult.version,
|
|
36
|
+
raw_device_data: fingerprint.data,
|
|
37
|
+
browserDetails: browserDetails,
|
|
38
|
+
bot: botResult,
|
|
39
|
+
tampering: deviceDetails.tampering,
|
|
40
|
+
incognito: incognito,
|
|
41
|
+
tor: torResult,
|
|
42
|
+
},
|
|
43
|
+
sdk: {
|
|
44
|
+
name: sdkInfo?.name,
|
|
45
|
+
version: sdkInfo?.version
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export async function fetchModules() {
|
|
51
|
+
return await Promise.all([
|
|
52
|
+
getFingerprint(),
|
|
53
|
+
detectTorBrowser(),
|
|
54
|
+
botDetection(),
|
|
55
|
+
detectIncognito(),
|
|
56
|
+
getOs(),
|
|
57
|
+
getDeviceDetails(),
|
|
58
|
+
getBrowserDetails(),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { load } from "@fingerprintjs/botd";
|
|
2
|
+
|
|
3
|
+
async function botDetection() {
|
|
4
|
+
try {
|
|
5
|
+
const botd = await load();
|
|
6
|
+
const result = botd.detect();
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if (result.bot) {
|
|
10
|
+
return {
|
|
11
|
+
status: true,
|
|
12
|
+
botKind: result.botKind,
|
|
13
|
+
};
|
|
14
|
+
} else {
|
|
15
|
+
return {
|
|
16
|
+
status: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(error);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { botDetection };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { DeviceResults, AllowedOs, DeviceType } from '../../../core';
|
|
2
|
+
import { normalizeOs, determineDeviceType, convertToTwoDP, isAllowedDeviceType, detectActualOs } from './helpers';
|
|
3
|
+
|
|
4
|
+
type ClaimedData = DeviceResults['claimed'];
|
|
5
|
+
type SignalData = DeviceResults['signals'];
|
|
6
|
+
type AnalysisData = DeviceResults['analysis'];
|
|
7
|
+
|
|
8
|
+
export const analyzeSpoofing = (claimed: ClaimedData, signals: SignalData): AnalysisData => {
|
|
9
|
+
const MAX_CONFIDENCE_SCORE = 1.0;
|
|
10
|
+
let score = MAX_CONFIDENCE_SCORE;
|
|
11
|
+
const conflicts: string[] = [];
|
|
12
|
+
const analysis: AnalysisData = {
|
|
13
|
+
confidence: MAX_CONFIDENCE_SCORE,
|
|
14
|
+
spoofingConfidence: 0.0,
|
|
15
|
+
isSpoofed: false,
|
|
16
|
+
detectionConflicts: [],
|
|
17
|
+
realPlatform: null,
|
|
18
|
+
realDeviceType: null,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const hasTouch = signals.touchPoints > 0;
|
|
22
|
+
const screenWidth = signals.screen.width;
|
|
23
|
+
|
|
24
|
+
const claimedOs = normalizeOs(claimed.platform);
|
|
25
|
+
const gpuOs = detectActualOs(signals.webGL.renderer?.toLowerCase() || '');
|
|
26
|
+
const platformOs = normalizeOs(signals.actualPlatform);
|
|
27
|
+
const actualOs = gpuOs || platformOs;
|
|
28
|
+
let suspectedOs: AllowedOs | null = claimedOs;
|
|
29
|
+
|
|
30
|
+
// --- Apply Rules ---
|
|
31
|
+
|
|
32
|
+
// Rule 1: Os Mismatch
|
|
33
|
+
if (claimedOs && actualOs && claimedOs !== actualOs) {
|
|
34
|
+
score -= 0.3;
|
|
35
|
+
conflicts.push(`Claimed OS "${claimedOs}" disagrees with actual OS signal "${actualOs}".`);
|
|
36
|
+
suspectedOs = actualOs; // Trust actual signal more initially
|
|
37
|
+
} else if (claimedOs && !actualOs && claimed.platform !== '') {
|
|
38
|
+
score -= 0.2;
|
|
39
|
+
conflicts.push(`Actual OS signal "${signals.actualPlatform}" is unrecognized.`);
|
|
40
|
+
suspectedOs = claimedOs;
|
|
41
|
+
} else if (!claimedOs && actualOs) {
|
|
42
|
+
score -= 0.2;
|
|
43
|
+
conflicts.push(`Claimed OS signal "${claimed.platform}" is unrecognized.`);
|
|
44
|
+
suspectedOs = actualOs;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Rule 2: GPU Vendor vs Determined Preliminary OS
|
|
48
|
+
const gpuVendor = (signals.webGL.vendor || "").toLowerCase();
|
|
49
|
+
const gpuRenderer = (signals.webGL.renderer || "").toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (gpuVendor && gpuVendor !== 'n/a' && gpuVendor !== 'n/a (masked)') {
|
|
52
|
+
let gpuConflict = false;
|
|
53
|
+
let gpuSuggestedOs: AllowedOs | null = null;
|
|
54
|
+
|
|
55
|
+
if (suspectedOs === 'android' && (gpuVendor.includes('apple') || gpuRenderer.includes('apple'))) { gpuConflict = true; gpuSuggestedOs = 'ios'; }
|
|
56
|
+
else if (suspectedOs === 'ios' && !gpuVendor.includes('apple') && !gpuRenderer.includes('apple')) {
|
|
57
|
+
gpuConflict = true;
|
|
58
|
+
if (gpuVendor.includes('qualcomm') || gpuVendor.includes('adreno') || gpuVendor.includes('mali') || gpuVendor.includes('powervr') || gpuVendor.includes('arm')) { gpuSuggestedOs = 'android'; }
|
|
59
|
+
else if (gpuVendor.includes('intel') || gpuRenderer.includes('intel') || gpuVendor.includes('nvidia') || gpuRenderer.includes('nvidia') || gpuVendor.includes('amd') || gpuRenderer.includes('amd') || gpuRenderer.includes('radeon')) { gpuSuggestedOs = 'windows'; }
|
|
60
|
+
else { gpuSuggestedOs = 'android'; }
|
|
61
|
+
}
|
|
62
|
+
else if (suspectedOs === 'windows' && (gpuVendor.includes('apple') || gpuRenderer.includes('apple'))) { gpuConflict = true; gpuSuggestedOs = 'macos'; }
|
|
63
|
+
else if (suspectedOs === 'macos' && !(gpuVendor.includes('apple') || gpuRenderer.includes('apple') || gpuVendor.includes('intel') || gpuRenderer.includes('intel') || gpuVendor.includes('amd') || gpuRenderer.includes('amd') || gpuRenderer.includes('radeon'))) {
|
|
64
|
+
gpuConflict = true;
|
|
65
|
+
if (gpuVendor.includes('qualcomm') || gpuVendor.includes('adreno') || gpuVendor.includes('mali') || gpuVendor.includes('powervr') || gpuVendor.includes('arm')) { gpuSuggestedOs = 'android'; }
|
|
66
|
+
else { gpuSuggestedOs = 'windows'; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (gpuConflict) {
|
|
70
|
+
score -= 0.5;
|
|
71
|
+
conflicts.push(`OS "${suspectedOs}" conflicts with GPU "${signals.webGL.vendor} / ${signals.webGL.renderer}". GPU suggests "${gpuSuggestedOs || 'unknown'}".`);
|
|
72
|
+
if (gpuSuggestedOs) { suspectedOs = gpuSuggestedOs; }
|
|
73
|
+
}
|
|
74
|
+
// Add logic here for suspicious but not conflicting GPUs if desired (e.g., Intel GPU on Android)
|
|
75
|
+
} else if (signals.webGL.error) {
|
|
76
|
+
score -= 0.1;
|
|
77
|
+
conflicts.push(`WebGL info unavailable (${signals.webGL.error}).`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
// Rule 3: Claimed Device Type vs Touch Support
|
|
82
|
+
let preliminaryRealDeviceType: DeviceType | null = null;
|
|
83
|
+
const normalizedClaimedType = isAllowedDeviceType(claimed.guessedDeviceType) ? claimed.guessedDeviceType : null;
|
|
84
|
+
|
|
85
|
+
if (claimed.mobile === true && !hasTouch) {
|
|
86
|
+
score -= 0.3;
|
|
87
|
+
conflicts.push(`Claimed mobile=true but no touch points detected.`);
|
|
88
|
+
preliminaryRealDeviceType = 'desktop';
|
|
89
|
+
} else if (claimed.mobile === false && hasTouch) {
|
|
90
|
+
score -= 0.15;
|
|
91
|
+
conflicts.push(`Claimed mobile=false but touch points detected.`);
|
|
92
|
+
preliminaryRealDeviceType = determineDeviceType(claimed.mobile, hasTouch, screenWidth, suspectedOs);
|
|
93
|
+
} else if (normalizedClaimedType) {
|
|
94
|
+
// If claimed type exists and doesn't conflict strongly with touch, use it as preliminary
|
|
95
|
+
preliminaryRealDeviceType = normalizedClaimedType;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// Rule 4: Screen Size vs Preliminary Device Type Sanity Check
|
|
100
|
+
const typicalMobileWidth = 768;
|
|
101
|
+
const typicalTabletWidth = 1024;
|
|
102
|
+
|
|
103
|
+
if (preliminaryRealDeviceType === 'mobile' && screenWidth > typicalTabletWidth) {
|
|
104
|
+
score -= 0.15;
|
|
105
|
+
conflicts.push(`Device type 'mobile', but screen width (${screenWidth}px) is large.`);
|
|
106
|
+
preliminaryRealDeviceType = 'tablet';
|
|
107
|
+
} else if (preliminaryRealDeviceType === 'desktop' && screenWidth < typicalMobileWidth) {
|
|
108
|
+
score -= 0.15;
|
|
109
|
+
conflicts.push(`Preliminary type 'desktop', but screen width (${screenWidth}px) is small.`);
|
|
110
|
+
preliminaryRealDeviceType = hasTouch ? 'mobile' : 'desktop';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Finalize Analysis ---
|
|
114
|
+
// We use the suspected Os but fallback to the claimed os of the ua if null
|
|
115
|
+
analysis.realPlatform = suspectedOs || claimedOs;
|
|
116
|
+
|
|
117
|
+
// Determine final device type
|
|
118
|
+
const determinedType = determineDeviceType(claimed.mobile, hasTouch, screenWidth, analysis.realPlatform);
|
|
119
|
+
|
|
120
|
+
if (!preliminaryRealDeviceType) {
|
|
121
|
+
analysis.realDeviceType = determinedType;
|
|
122
|
+
} else {
|
|
123
|
+
// Check if the determined type based on final signals contradicts the preliminary one
|
|
124
|
+
if (determinedType && determinedType !== preliminaryRealDeviceType) {
|
|
125
|
+
score -= 0.12; // Small penalty for internal inconsistency
|
|
126
|
+
conflicts.push(`Initial device type guess (${preliminaryRealDeviceType}) adjusted to ${determinedType} based on final signals.`);
|
|
127
|
+
analysis.realDeviceType = determinedType;
|
|
128
|
+
} else {
|
|
129
|
+
analysis.realDeviceType = preliminaryRealDeviceType;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Ensure device type is populated based on OS if still null
|
|
134
|
+
if (!analysis.realDeviceType) {
|
|
135
|
+
if (analysis.realPlatform === 'ios' || analysis.realPlatform === 'android') {
|
|
136
|
+
analysis.realDeviceType = determinedType || 'mobile';
|
|
137
|
+
} else if (analysis.realPlatform) { // windows, macos, linux
|
|
138
|
+
analysis.realDeviceType = determinedType || 'desktop';
|
|
139
|
+
} else { // OS unknown
|
|
140
|
+
analysis.realDeviceType = hasTouch ? (screenWidth > 768 ? 'tablet' : 'mobile') : 'desktop';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
analysis.confidence = convertToTwoDP(Math.max(0, score));
|
|
145
|
+
analysis.spoofingConfidence = convertToTwoDP(MAX_CONFIDENCE_SCORE - analysis.confidence);
|
|
146
|
+
analysis.isSpoofed = analysis.spoofingConfidence > 0.1;
|
|
147
|
+
analysis.detectionConflicts = conflicts;
|
|
148
|
+
|
|
149
|
+
return analysis;
|
|
150
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { DeviceResults, ClaimedDevice } from '../../../core';
|
|
2
|
+
import { guessDeviceTypeFromUA } from './helpers';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type SignalData = DeviceResults['signals'];
|
|
6
|
+
|
|
7
|
+
// What the device claims to be is gotten from the useragent string
|
|
8
|
+
export const getClaimedData = async (userAgentString: string): Promise<ClaimedDevice> => {
|
|
9
|
+
const claimed = {} as ClaimedDevice;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
if ((navigator as any).userAgentData) {
|
|
13
|
+
const uaData = await (navigator as any).userAgentData.getHighEntropyValues([
|
|
14
|
+
"platform", "platformVersion", "architecture", "model", "uaFullVersion", "brands", "mobile",
|
|
15
|
+
]);
|
|
16
|
+
claimed.platform = uaData.platform;
|
|
17
|
+
claimed.mobile = uaData.mobile ?? null;
|
|
18
|
+
claimed.brands = uaData.brands || [];
|
|
19
|
+
|
|
20
|
+
// Initial guess - will be refined later using signals if needed
|
|
21
|
+
if (claimed.mobile) {
|
|
22
|
+
claimed.guessedDeviceType = screen.width > 768 ? "tablet" : "mobile";
|
|
23
|
+
} else {
|
|
24
|
+
claimed.guessedDeviceType = "desktop";
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
const ua = userAgentString;
|
|
28
|
+
const lowerUa = ua.toLowerCase();
|
|
29
|
+
|
|
30
|
+
// Determine the device platform from user agent string
|
|
31
|
+
if (lowerUa.includes("android")) claimed.platform = "Android";
|
|
32
|
+
else if (lowerUa.includes("iphone") || lowerUa.includes("ipad")) claimed.platform = "iOS";
|
|
33
|
+
else if (lowerUa.includes("windows nt")) claimed.platform = "Windows";
|
|
34
|
+
else if (lowerUa.includes("mac os x") || lowerUa.includes("macintosh")) claimed.platform = "macOS";
|
|
35
|
+
else if (lowerUa.includes("linux")) claimed.platform = "Linux";
|
|
36
|
+
|
|
37
|
+
const guessedType = guessDeviceTypeFromUA(ua);
|
|
38
|
+
claimed.guessedDeviceType = guessedType;
|
|
39
|
+
claimed.mobile = guessedType === 'mobile' || guessedType === 'tablet';
|
|
40
|
+
|
|
41
|
+
const brandMatch = ua.match(/(Apple|Samsung|Google|Microsoft|Sony|LG|HTC|Nokia|Motorola|Huawei|Xiaomi|Pixel|Firefox|Safari|Edge|Chrome)/i);
|
|
42
|
+
if (brandMatch) {
|
|
43
|
+
const versionMatch = ua.match(new RegExp(brandMatch[0] + "[ /]([\\d._]+)"));
|
|
44
|
+
claimed.brands.push({ brand: brandMatch[0], version: versionMatch ? versionMatch[1] : "unknown" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error("Error getting User Agent Data:", e);
|
|
49
|
+
}
|
|
50
|
+
return claimed;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// What we actually think the device could be achieved from some of this signals
|
|
54
|
+
export const getSignalData = (): SignalData => {
|
|
55
|
+
const signals: SignalData = {
|
|
56
|
+
actualPlatform: (navigator as any).userAgentData?.platform || navigator.platform ,
|
|
57
|
+
touchPoints: navigator.maxTouchPoints || 0,
|
|
58
|
+
screen: {
|
|
59
|
+
width: screen.width,
|
|
60
|
+
height: screen.height,
|
|
61
|
+
pixelRatio: window.devicePixelRatio || 1,
|
|
62
|
+
},
|
|
63
|
+
webGL: { vendor: undefined, renderer: undefined, error: "" },
|
|
64
|
+
userAgentString: navigator.userAgent,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const canvas = document.createElement("canvas");
|
|
69
|
+
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
70
|
+
|
|
71
|
+
if (gl && 'getParameter' in gl) {
|
|
72
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
73
|
+
if (debugInfo) {
|
|
74
|
+
signals.webGL.vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "n/a";
|
|
75
|
+
signals.webGL.renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "n/a";
|
|
76
|
+
} else {
|
|
77
|
+
signals.webGL.vendor = gl.getParameter(gl.VENDOR) || "n/a (masked)";
|
|
78
|
+
signals.webGL.renderer = gl.getParameter(gl.RENDERER) || "n/a (masked)";
|
|
79
|
+
if (!signals.webGL.vendor || signals.webGL.vendor === "n/a (masked)") {
|
|
80
|
+
signals.webGL.error = "Debug info extension unavailable or vendor masked";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
signals.webGL.error = "WebGL context unavailable";
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("Error getting WebGL data:", error);
|
|
88
|
+
signals.webGL.error = error instanceof Error ? error.message : "Unknown WebGL error";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return signals;
|
|
92
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALLOWED_DEVICE_TYPES,
|
|
3
|
+
ALLOWED_OS,
|
|
4
|
+
AllowedOs,
|
|
5
|
+
DeviceType,
|
|
6
|
+
} from '../../../core';
|
|
7
|
+
|
|
8
|
+
// Normalize the gueesed Os
|
|
9
|
+
export const normalizeOs = (
|
|
10
|
+
platform: string | undefined | null
|
|
11
|
+
): AllowedOs | null => {
|
|
12
|
+
if (!platform) return null;
|
|
13
|
+
const lowerPlatform = platform.toLowerCase();
|
|
14
|
+
|
|
15
|
+
if (lowerPlatform.includes("android")) return "android";
|
|
16
|
+
if (
|
|
17
|
+
lowerPlatform.includes("iphone") ||
|
|
18
|
+
lowerPlatform.includes("ipad") ||
|
|
19
|
+
lowerPlatform === "ios"
|
|
20
|
+
)
|
|
21
|
+
return "ios";
|
|
22
|
+
if (lowerPlatform.includes("win")) return "windows";
|
|
23
|
+
if (lowerPlatform.includes("mac") || lowerPlatform.includes("macos"))
|
|
24
|
+
return "macos";
|
|
25
|
+
if (lowerPlatform.includes("linux")) return "linux";
|
|
26
|
+
// if (lowerPlatform.includes("cros")) return "linux"; // Example extension
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const patterns = {
|
|
32
|
+
ios: [/apple a\d+ gpu/i],
|
|
33
|
+
macos: [/apple m\d+/i, /opengl engine/i],
|
|
34
|
+
windows: [/angle.*direct3d/i],
|
|
35
|
+
android: [/adreno|mali|powervr/i],
|
|
36
|
+
linux: [/mesa|x11/i],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Detect the actual OS from the GPU vendor and renderer if available
|
|
40
|
+
export const detectActualOs = (renderer: string): AllowedOs | null => {
|
|
41
|
+
if (patterns.ios.some((pattern) => pattern.test(renderer))) {
|
|
42
|
+
return "ios";
|
|
43
|
+
} else if (patterns.macos.some((pattern) => pattern.test(renderer))) {
|
|
44
|
+
return "macos";
|
|
45
|
+
} else if (patterns.windows.some((pattern) => pattern.test(renderer))) {
|
|
46
|
+
return "windows";
|
|
47
|
+
} else if (patterns.android.some((pattern) => pattern.test(renderer))) {
|
|
48
|
+
return "android";
|
|
49
|
+
} else if (
|
|
50
|
+
patterns.linux.some((pattern) => pattern.test(renderer)) ||
|
|
51
|
+
/NVIDIA|AMD|Intel/i.test(renderer)
|
|
52
|
+
) {
|
|
53
|
+
return "linux";
|
|
54
|
+
} else {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const determineDeviceType = (
|
|
60
|
+
claimedMobile: boolean | null,
|
|
61
|
+
hasTouch: boolean,
|
|
62
|
+
screenWidth: number,
|
|
63
|
+
realOs: AllowedOs | null
|
|
64
|
+
): DeviceType | null => {
|
|
65
|
+
// Determine the real device type from the os
|
|
66
|
+
if (realOs === "ios" || realOs === "android") {
|
|
67
|
+
return screenWidth > 768 ? "tablet" : "mobile";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (realOs === "windows" || realOs === "macos" || realOs === "linux") {
|
|
71
|
+
return "desktop";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// check using touch attributes if the os couldn't determine device type
|
|
75
|
+
if (hasTouch) {
|
|
76
|
+
if (screenWidth < 600) return "mobile";
|
|
77
|
+
if (screenWidth <= 1024) return "tablet";
|
|
78
|
+
if (claimedMobile === false) return "desktop"; // Likely touch laptop
|
|
79
|
+
return "tablet"; // Default large touch to tablet
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!hasTouch) {
|
|
83
|
+
return "desktop";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const guessDeviceTypeFromUA = (ua: string): DeviceType | null => {
|
|
90
|
+
const lowerUa = ua.toLowerCase();
|
|
91
|
+
if (/(tablet|ipad|playbook|silk)|(android(?!.*mobi))/i.test(lowerUa)) {
|
|
92
|
+
return "tablet";
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
/Mobile|iP(hone|od)|Android|BlackBerry|IEMobile|Kindle|Silk-Accelerated|hpwos|Opera M(obi|ini)/i.test(
|
|
96
|
+
lowerUa
|
|
97
|
+
)
|
|
98
|
+
) {
|
|
99
|
+
return "mobile";
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
lowerUa.includes("windows nt") ||
|
|
103
|
+
lowerUa.includes("macintosh") ||
|
|
104
|
+
lowerUa.includes("x11")
|
|
105
|
+
) {
|
|
106
|
+
return "desktop";
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const convertToTwoDP = (num: number): number => {
|
|
112
|
+
return parseFloat(num.toFixed(2));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const isAllowedOs = (os: string): os is AllowedOs => {
|
|
116
|
+
return !!os && (ALLOWED_OS as readonly string[]).includes(os);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const isAllowedDeviceType = (
|
|
120
|
+
type: DeviceType | null
|
|
121
|
+
): type is DeviceType => {
|
|
122
|
+
return !!type && (ALLOWED_DEVICE_TYPES as readonly string[]).includes(type);
|
|
123
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RefinedDeviceResults,
|
|
3
|
+
deviceTampering,
|
|
4
|
+
DeviceResults,
|
|
5
|
+
} from "../../../core";
|
|
6
|
+
import { analyzeSpoofing } from "./analyze-data";
|
|
7
|
+
import { getClaimedData, getSignalData } from "./gather-data";
|
|
8
|
+
|
|
9
|
+
export const getDeviceDetails = async (): Promise<RefinedDeviceResults> => {
|
|
10
|
+
// 1. Initialize structure
|
|
11
|
+
const initialResults: DeviceResults = {
|
|
12
|
+
claimed: {
|
|
13
|
+
platform: "",
|
|
14
|
+
mobile: false,
|
|
15
|
+
brands: [],
|
|
16
|
+
guessedDeviceType: null,
|
|
17
|
+
},
|
|
18
|
+
signals: {
|
|
19
|
+
actualPlatform: "",
|
|
20
|
+
touchPoints: 0,
|
|
21
|
+
screen: { width: 0, height: 0, pixelRatio: 1 },
|
|
22
|
+
webGL: { vendor: undefined, renderer: undefined, error: "" },
|
|
23
|
+
userAgentString: "",
|
|
24
|
+
},
|
|
25
|
+
analysis: {
|
|
26
|
+
confidence: 1.0,
|
|
27
|
+
spoofingConfidence: 0.0,
|
|
28
|
+
isSpoofed: false,
|
|
29
|
+
detectionConflicts: [],
|
|
30
|
+
realPlatform: null,
|
|
31
|
+
realDeviceType: null,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 2. Gather Signals (Sync first)
|
|
36
|
+
const signals = getSignalData();
|
|
37
|
+
initialResults.signals = signals;
|
|
38
|
+
|
|
39
|
+
// 3. Gather Claimed Data (Async) - Pass UA string from signals
|
|
40
|
+
const claimed = await getClaimedData(signals.userAgentString);
|
|
41
|
+
initialResults.claimed = claimed;
|
|
42
|
+
|
|
43
|
+
// 4. Analyze Data
|
|
44
|
+
const analysis = analyzeSpoofing(claimed, signals);
|
|
45
|
+
initialResults.analysis = analysis;
|
|
46
|
+
|
|
47
|
+
// 5. Format Final Output
|
|
48
|
+
const tamperingResult = (): deviceTampering => {
|
|
49
|
+
if (analysis.isSpoofed) {
|
|
50
|
+
return { status: true, confidence: analysis.spoofingConfidence };
|
|
51
|
+
} else {
|
|
52
|
+
return { status: false };
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const finalResults: RefinedDeviceResults = {
|
|
57
|
+
...initialResults,
|
|
58
|
+
deviceOs: analysis.realPlatform,
|
|
59
|
+
deviceType: analysis.realDeviceType,
|
|
60
|
+
tampering: tamperingResult(),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return finalResults;
|
|
64
|
+
};
|