@thumbmarkjs/thumbmarkjs 0.19.0 → 0.20.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.
@@ -0,0 +1,69 @@
1
+ import { componentInterface, includeComponent } from '../../factory'
2
+
3
+ function getHardwareInfo(): Promise<componentInterface> {
4
+ return new Promise((resolve, reject) => {
5
+ const deviceMemory = (navigator.deviceMemory !== undefined) ? navigator.deviceMemory : 0
6
+ const memoryInfo = (window.performance && (window.performance as any).memory ) ? (window.performance as any).memory : 0
7
+ resolve(
8
+ {
9
+ 'videocard': getVideoCard(),
10
+ 'architecture': getArchitecture(),
11
+ 'deviceMemory': deviceMemory.toString() || 'undefined',
12
+ 'jsHeapSizeLimit': memoryInfo.jsHeapSizeLimit || 0,
13
+ }
14
+ )
15
+ });
16
+ }
17
+
18
+ function getVideoCard(): componentInterface | string {
19
+ const canvas = document.createElement('canvas');
20
+ const gl = canvas.getContext('webgl') ?? canvas.getContext('experimental-webgl');
21
+
22
+ if (gl && 'getParameter' in gl) {
23
+ try {
24
+ // Try standard parameters first
25
+ const vendor = (gl.getParameter(gl.VENDOR) || '').toString();
26
+ const renderer = (gl.getParameter(gl.RENDERER) || '').toString();
27
+
28
+ let result: componentInterface = {
29
+ vendor: vendor,
30
+ renderer: renderer,
31
+ version: (gl.getParameter(gl.VERSION) || '').toString(),
32
+ shadingLanguageVersion: (gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || '').toString(),
33
+ };
34
+
35
+ // Only try debug info if needed and available
36
+ if (!renderer.length || !vendor.length) {
37
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
38
+ if (debugInfo) {
39
+ const vendorUnmasked = (gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '').toString();
40
+ const rendererUnmasked = (gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '').toString();
41
+
42
+ // Only add unmasked values if they exist
43
+ if (vendorUnmasked) {
44
+ result.vendorUnmasked = vendorUnmasked;
45
+ }
46
+ if (rendererUnmasked) {
47
+ result.rendererUnmasked = rendererUnmasked;
48
+ }
49
+ }
50
+ }
51
+
52
+ return result;
53
+ } catch (error) {
54
+ // fail silently
55
+ }
56
+ }
57
+ return "undefined";
58
+ }
59
+
60
+ function getArchitecture(): number {
61
+ const f = new Float32Array(1);
62
+ const u8 = new Uint8Array(f.buffer);
63
+ f[0] = Infinity;
64
+ f[0] = f[0] - f[0];
65
+
66
+ return u8[3];
67
+ }
68
+
69
+ includeComponent('hardware', getHardwareInfo);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Does anyone have a cleaner way of doing this?
3
+ * I want to import all the components in this folder
4
+ * Feels a little dumb I'm doing this manually.
5
+ */
6
+
7
+ import './audio/audio'
8
+ import './canvas/canvas'
9
+ import './fonts/fonts'
10
+ import './hardware/hardware'
11
+ import './locales/locales'
12
+ import './permissions/permissions'
13
+ import './plugins/plugins'
14
+ import './screen/screen'
15
+ import './system/system'
16
+ import './webgl/webgl'
17
+ import './math/math'
@@ -0,0 +1,23 @@
1
+ import { componentInterface, includeComponent } from '../../factory'
2
+
3
+ function getLocales(): Promise<componentInterface> {
4
+ return new Promise((resolve) => {
5
+ resolve(
6
+ {
7
+ 'languages': getUserLanguage(),
8
+ 'timezone': getUserTimezone()
9
+ });
10
+ });
11
+ }
12
+
13
+ function getUserLanguage(): string {
14
+ const userLanguages: string[] = [];
15
+
16
+ return navigator.language;
17
+ }
18
+
19
+ function getUserTimezone(): string {
20
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
21
+ }
22
+
23
+ includeComponent('locales', getLocales);
@@ -0,0 +1,39 @@
1
+ import { componentInterface, includeComponent } from '../../factory'
2
+
3
+ const getMathInfo = async (): Promise<componentInterface> => {
4
+ return {
5
+ acos: Math.acos(0.5),
6
+ asin: integrate(Math.asin, -1, 1, 97),
7
+ atan: integrate(Math.atan, -1, 1, 97),
8
+ cos: integrate(Math.cos, 0, Math.PI, 97),
9
+ cosh: Math.cosh(9/7),
10
+ e: Math.E,
11
+ largeCos: Math.cos(1e20),
12
+ largeSin: Math.sin(1e20),
13
+ largeTan: Math.tan(1e20),
14
+ log: Math.log(1000),
15
+ pi: Math.PI,
16
+ sin: integrate(Math.sin, -Math.PI, Math.PI, 97),
17
+ sinh: integrate(Math.sinh, -9/7, 7/9, 97),
18
+ sqrt: Math.sqrt(2),
19
+ tan: integrate(Math.tan, 0, 2 * Math.PI, 97),
20
+ tanh: integrate(Math.tanh, -9/7, 7/9, 97),
21
+ }
22
+ }
23
+
24
+ /** This might be a little excessive, but I wasn't sure what number to pick for some of the
25
+ * trigonometric functions. Using an integral here, so a few numbers are calculated. However,
26
+ * I do this mainly for those integrals that sum up to a small value, otherwise there's no point.
27
+ */
28
+
29
+ const integrate = (f: (x: number) => number, a: number, b: number, n: number): number => {
30
+ const h = (b - a) / n;
31
+ let sum = 0;
32
+ for (let i = 0; i < n; i++) {
33
+ const x = a + (i + 0.5) * h;
34
+ sum += f(x);
35
+ }
36
+ return sum * h;
37
+ };
38
+
39
+ includeComponent('math', getMathInfo);
@@ -0,0 +1,58 @@
1
+ import { componentInterface, includeComponent } from '../../factory';
2
+ import { mostFrequentValuesInArrayOfDictionaries } from '../../utils/getMostFrequent';
3
+ import { options } from '../../fingerprint/options';
4
+
5
+ let permission_keys: PermissionName[];
6
+ function initializePermissionKeys() {
7
+ permission_keys = options?.permissions_to_check || [
8
+ 'accelerometer',
9
+ 'accessibility', 'accessibility-events',
10
+ 'ambient-light-sensor',
11
+ 'background-fetch', 'background-sync', 'bluetooth',
12
+ 'camera',
13
+ 'clipboard-read',
14
+ 'clipboard-write',
15
+ 'device-info', 'display-capture',
16
+ 'gyroscope', 'geolocation',
17
+ 'local-fonts',
18
+ 'magnetometer', 'microphone', 'midi',
19
+ 'nfc', 'notifications',
20
+ 'payment-handler',
21
+ 'persistent-storage',
22
+ 'push',
23
+ 'speaker', 'storage-access',
24
+ 'top-level-storage-access',
25
+ 'window-management',
26
+ 'query',
27
+ ] as PermissionName[];
28
+ }
29
+
30
+ export default async function getBrowserPermissions(): Promise<componentInterface> {
31
+ initializePermissionKeys();
32
+ const permissionPromises: Promise<componentInterface>[] = Array.from({length: options?.retries || 3}, () => getBrowserPermissionsOnce() );
33
+ return Promise.all(permissionPromises).then((resolvedPermissions) => {
34
+ const permission = mostFrequentValuesInArrayOfDictionaries(resolvedPermissions, permission_keys);
35
+ return permission;
36
+ });
37
+ }
38
+
39
+ async function getBrowserPermissionsOnce(): Promise<componentInterface> {
40
+
41
+ const permissionStatus: { [key: string]: string } = {};
42
+
43
+ for (const feature of permission_keys) {
44
+ try {
45
+ // Request permission status for each feature
46
+ const status = await navigator.permissions.query({ name: feature });
47
+
48
+ // Assign permission status to the object
49
+ permissionStatus[feature] = status.state.toString();
50
+ } catch (error) {
51
+ // In case of errors (unsupported features, etc.), do nothing. Not listing them is the same as not supported
52
+ }
53
+ }
54
+
55
+ return permissionStatus;
56
+ }
57
+
58
+ includeComponent("permissions", getBrowserPermissions);
@@ -0,0 +1,22 @@
1
+ import { componentInterface, includeComponent } from '../../factory'
2
+
3
+ export default function getInstalledPlugins(): Promise<componentInterface> {
4
+ const plugins: string[] = [];
5
+
6
+ if (navigator.plugins) {
7
+ for (let i = 0; i < navigator.plugins.length; i++) {
8
+ const plugin = navigator.plugins[i];
9
+ plugins.push([plugin.name, plugin.filename, plugin.description ].join("|"));
10
+ }
11
+ }
12
+
13
+ return new Promise((resolve) => {
14
+ resolve(
15
+ {
16
+ 'plugins': plugins
17
+ }
18
+ );
19
+ });
20
+ }
21
+
22
+ includeComponent('plugins', getInstalledPlugins);
@@ -0,0 +1,46 @@
1
+ import { componentInterface, includeComponent } from '../../factory';
2
+
3
+ function screenDetails(): Promise<componentInterface> {
4
+ return new Promise((resolve) => {
5
+ resolve(
6
+ {
7
+ 'is_touchscreen': navigator.maxTouchPoints > 0,
8
+ 'maxTouchPoints': navigator.maxTouchPoints,
9
+ 'colorDepth': screen.colorDepth,
10
+ 'mediaMatches': matchMedias(),
11
+ }
12
+ );
13
+ });
14
+ }
15
+
16
+ function matchMedias(): string[] {
17
+ let results: string[] = [];
18
+
19
+ /**
20
+ * @see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries
21
+ */
22
+
23
+ const mediaQueries: { [k: string]: string[] } = {
24
+ 'prefers-contrast': ['high', 'more', 'low', 'less', 'forced', 'no-preference'],
25
+ 'any-hover': ['hover', 'none'],
26
+ 'any-pointer': ['none', 'coarse', 'fine'],
27
+ 'pointer': ['none', 'coarse', 'fine'],
28
+ 'hover': ['hover', 'none'],
29
+ 'update': ['fast', 'slow'],
30
+ 'inverted-colors': ['inverted', 'none'],
31
+ 'prefers-reduced-motion': ['reduce', 'no-preference'],
32
+ 'prefers-reduced-transparency': ['reduce', 'no-preference'],
33
+ 'scripting': ['none', 'initial-only', 'enabled'],
34
+ 'forced-colors': ['active', 'none'],
35
+ };
36
+
37
+ Object.keys(mediaQueries).forEach((key) => {
38
+ mediaQueries[key].forEach((value) => {
39
+ if (matchMedia(`(${key}: ${value})`).matches)
40
+ results.push(`${key}: ${value}`);
41
+ })
42
+ });
43
+ return results;
44
+ }
45
+
46
+ includeComponent('screen', screenDetails);
@@ -0,0 +1,63 @@
1
+ // Define an interface for the browser result
2
+ interface BrowserResult {
3
+ name: string;
4
+ version: string;
5
+ }
6
+
7
+ // Define a function to parse the user agent string and return the browser name and version
8
+ export function getBrowser(): BrowserResult {
9
+ if (typeof navigator === 'undefined') {
10
+ return {
11
+ name: "unknown",
12
+ version: "unknown"
13
+ }
14
+ }
15
+ const ua = navigator.userAgent
16
+ // Define some regular expressions to match different browsers and their versions
17
+ const regexes = [
18
+ // Edge
19
+ /(?<name>Edge|Edg)\/(?<version>\d+(?:\.\d+)?)/,
20
+ // Chrome, Chromium, Opera, Vivaldi, Brave, etc.
21
+ /(?<name>(?:Chrome|Chromium|OPR|Opera|Vivaldi|Brave))\/(?<version>\d+(?:\.\d+)?)/,
22
+ // Firefox, Waterfox, etc.
23
+ /(?<name>(?:Firefox|Waterfox|Iceweasel|IceCat))\/(?<version>\d+(?:\.\d+)?)/,
24
+ // Safari, Mobile Safari, etc.
25
+ /(?<name>Safari)\/(?<version>\d+(?:\.\d+)?)/,
26
+ // Internet Explorer, IE Mobile, etc.
27
+ /(?<name>MSIE|Trident|IEMobile).+?(?<version>\d+(?:\.\d+)?)/,
28
+ // Other browsers that use the format "BrowserName/version"
29
+ /(?<name>[A-Za-z]+)\/(?<version>\d+(?:\.\d+)?)/,
30
+ // Samsung internet browser
31
+ /(?<name>SamsungBrowser)\/(?<version>\d+(?:\.\d+)?)/,
32
+ // Samsung browser (Tizen format)
33
+ /(?<name>samsung).*Version\/(?<version>\d+(?:\.\d+)?)/i
34
+ ];
35
+
36
+ // Define a map for browser name translations
37
+ const browserNameMap: { [key: string]: string } = {
38
+ 'edg': 'Edge',
39
+ 'opr': 'Opera',
40
+ 'samsung': 'SamsungBrowser'
41
+ };
42
+
43
+ // Loop through the regexes and try to find a match
44
+ for (const regex of regexes) {
45
+ const match = ua.match(regex);
46
+ if (match && match.groups) {
47
+ // Translate the browser name if necessary
48
+ const name = browserNameMap[match.groups.name.toLowerCase()] || match.groups.name;
49
+ // Return the browser name and version
50
+ return {
51
+ name: name,
52
+ version: match.groups.version
53
+ };
54
+ }
55
+ }
56
+
57
+ // If no match is found, return unknown
58
+ return {
59
+ name: "unknown",
60
+ version: "unknown"
61
+ };
62
+ }
63
+
@@ -0,0 +1,40 @@
1
+ import { componentInterface, includeComponent } from '../../factory';
2
+ import { getBrowser } from './browser'
3
+
4
+ function getSystemDetails(): Promise<componentInterface> {
5
+ return new Promise((resolve) => {
6
+ const browser = getBrowser()
7
+ resolve( {
8
+ 'platform': window.navigator.platform,
9
+ 'cookieEnabled': window.navigator.cookieEnabled,
10
+ 'productSub': navigator.productSub,
11
+ 'product': navigator.product,
12
+ 'useragent': navigator.userAgent,
13
+ 'hardwareConcurrency': navigator.hardwareConcurrency,
14
+ 'browser': {'name': browser.name, 'version': browser.version },
15
+ 'applePayVersion': getApplePayVersion()
16
+ });
17
+ });
18
+ }
19
+
20
+ /**
21
+ * @returns applePayCanMakePayments: boolean, applePayMaxSupportedVersion: number
22
+ */
23
+ function getApplePayVersion(): number {
24
+ if (window.location.protocol === 'https:' && typeof (window as any).ApplePaySession === 'function') {
25
+ try {
26
+ const versionCheck = (window as any).ApplePaySession.supportsVersion;
27
+ for (let i = 15; i > 0; i--) {
28
+ if (versionCheck(i)) {
29
+ return i;
30
+ }
31
+ }
32
+ } catch (error) {
33
+ return 0
34
+ }
35
+ }
36
+ return 0
37
+ }
38
+
39
+ includeComponent('system', getSystemDetails);
40
+
@@ -0,0 +1,148 @@
1
+ import { componentInterface, includeComponent } from '../../factory'
2
+ import { hash } from '../../utils/hash'
3
+ import { getCommonPixels } from '../../utils/commonPixels';
4
+ import { getBrowser } from '../system/browser';
5
+
6
+ const _RUNS = (getBrowser().name !== 'SamsungBrowser') ? 1 : 3;
7
+ let canvas: HTMLCanvasElement
8
+ let gl: WebGLRenderingContext | null = null;
9
+
10
+ function initializeCanvasAndWebGL() {
11
+ if (typeof document !== 'undefined') {
12
+ canvas = document.createElement('canvas');
13
+ canvas.width = 200;
14
+ canvas.height = 100;
15
+ gl = canvas.getContext('webgl');
16
+ }
17
+ }
18
+
19
+ async function createWebGLFingerprint(): Promise<componentInterface> {
20
+ initializeCanvasAndWebGL();
21
+
22
+ try {
23
+
24
+ if (!gl) {
25
+ throw new Error('WebGL not supported');
26
+ }
27
+
28
+
29
+ const imageDatas: ImageData[] = Array.from({length: _RUNS}, () => createWebGLImageData() );
30
+ // and then checking the most common bytes for each channel of each pixel
31
+ const commonImageData = getCommonPixels(imageDatas, canvas.width, canvas.height);
32
+ //const imageData = createWebGLImageData()
33
+
34
+ return {
35
+ 'commonImageHash': hash(commonImageData.data.toString()).toString(),
36
+ }
37
+ } catch (error) {
38
+ return {
39
+ 'webgl': 'unsupported'
40
+ }
41
+ }
42
+ }
43
+
44
+ function createWebGLImageData(): ImageData {
45
+ try {
46
+
47
+ if (!gl) {
48
+ throw new Error('WebGL not supported');
49
+ }
50
+
51
+ const vertexShaderSource = `
52
+ attribute vec2 position;
53
+ void main() {
54
+ gl_Position = vec4(position, 0.0, 1.0);
55
+ }
56
+ `;
57
+
58
+ const fragmentShaderSource = `
59
+ precision mediump float;
60
+ void main() {
61
+ gl_FragColor = vec4(0.812, 0.195, 0.553, 0.921); // Set line color
62
+ }
63
+ `;
64
+
65
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
66
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
67
+
68
+ if (!vertexShader || !fragmentShader) {
69
+ throw new Error('Failed to create shaders');
70
+ }
71
+
72
+ gl.shaderSource(vertexShader, vertexShaderSource);
73
+ gl.shaderSource(fragmentShader, fragmentShaderSource);
74
+
75
+ gl.compileShader(vertexShader);
76
+ if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
77
+ throw new Error('Vertex shader compilation failed: ' + gl.getShaderInfoLog(vertexShader));
78
+ }
79
+
80
+ gl.compileShader(fragmentShader);
81
+ if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
82
+ throw new Error('Fragment shader compilation failed: ' + gl.getShaderInfoLog(fragmentShader));
83
+ }
84
+
85
+ const shaderProgram = gl.createProgram();
86
+
87
+ if (!shaderProgram) {
88
+ throw new Error('Failed to create shader program');
89
+ }
90
+
91
+ gl.attachShader(shaderProgram, vertexShader);
92
+ gl.attachShader(shaderProgram, fragmentShader);
93
+ gl.linkProgram(shaderProgram);
94
+ if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
95
+ throw new Error('Shader program linking failed: ' + gl.getProgramInfoLog(shaderProgram));
96
+ }
97
+
98
+ gl.useProgram(shaderProgram);
99
+
100
+ // Set up vertices to form lines
101
+ const numSpokes: number = 137;
102
+ const vertices = new Float32Array(numSpokes * 4);
103
+ const angleIncrement = (2 * Math.PI) / numSpokes;
104
+
105
+ for (let i = 0; i < numSpokes; i++) {
106
+ const angle = i * angleIncrement;
107
+
108
+ // Define two points for each line (spoke)
109
+ vertices[i * 4] = 0; // Center X
110
+ vertices[i * 4 + 1] = 0; // Center Y
111
+ vertices[i * 4 + 2] = Math.cos(angle) * (canvas.width / 2); // Endpoint X
112
+ vertices[i * 4 + 3] = Math.sin(angle) * (canvas.height / 2); // Endpoint Y
113
+ }
114
+
115
+ const vertexBuffer = gl.createBuffer();
116
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
117
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
118
+
119
+ const positionAttribute = gl.getAttribLocation(shaderProgram, 'position');
120
+ gl.enableVertexAttribArray(positionAttribute);
121
+ gl.vertexAttribPointer(positionAttribute, 2, gl.FLOAT, false, 0, 0);
122
+
123
+ // Render
124
+ gl.viewport(0, 0, canvas.width, canvas.height);
125
+ gl.clearColor(0.0, 0.0, 0.0, 1.0);
126
+ gl.clear(gl.COLOR_BUFFER_BIT);
127
+ gl.drawArrays(gl.LINES, 0, numSpokes * 2);
128
+
129
+ const pixelData = new Uint8ClampedArray(canvas.width * canvas.height * 4);
130
+ gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);
131
+ const imageData = new ImageData(pixelData, canvas.width, canvas.height);
132
+
133
+ return imageData;
134
+ } catch (error) {
135
+ //console.error(error);
136
+ return new ImageData(1, 1);
137
+ } finally {
138
+ if (gl) {
139
+ // Reset WebGL state
140
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
141
+ gl.useProgram(null);
142
+ gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
143
+ gl.clearColor(0.0, 0.0, 0.0, 0.0);
144
+ }
145
+ }
146
+ }
147
+
148
+ includeComponent('webgl', createWebGLFingerprint);
@@ -0,0 +1,15 @@
1
+ interface ApplePaySession {
2
+ new(version: number, paymentRequest: any): ApplePaySession;
3
+ canMakePayments(): boolean;
4
+ supportsVersion(version: number): boolean;
5
+ }
6
+
7
+ interface Window {
8
+ webkitAudioContext: typeof AudioContext
9
+ webkitOfflineAudioContext: typeof OfflineAudioContext
10
+ ApplePaySession: typeof ApplePaySession
11
+ }
12
+
13
+ interface Navigator {
14
+ deviceMemory?: number,
15
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * This file is used to create the includeComponent function as well as the interfaces each of the
3
+ * fingerprint components must implement.
4
+ *
5
+ */
6
+
7
+ import { options, optionsInterface } from './fingerprint/options';
8
+
9
+ // the component interface is the form of the JSON object the function's promise must return
10
+ export interface componentInterface {
11
+ [key: string]: string | string[] | number | boolean | componentInterface;
12
+ }
13
+
14
+
15
+ // The component function's interface is simply the promise of the above
16
+ export interface componentFunctionInterface {
17
+ (): Promise<componentInterface>;
18
+ }
19
+
20
+ // components include a dictionary of name: function.
21
+ export const components: {[name: string]: componentFunctionInterface} = {};
22
+
23
+ //In case a promise time-outs, this is what we use as the value in place
24
+ export const timeoutInstance: componentInterface = {
25
+ 'timeout': "true"
26
+ }
27
+
28
+ /**
29
+ * includeComponent is the function each component function needs to call in order for the component to be included
30
+ * in the fingerprint.
31
+ * @param {string} name - the name identifier of the component
32
+ * @param {componentFunctionInterface} creationFunction - the function that implements the component
33
+ * @returns
34
+ */
35
+ export const includeComponent = (name:string, creationFunction: componentFunctionInterface) => {
36
+ if (typeof window !== 'undefined')
37
+ components[name] = creationFunction;
38
+ }
39
+
40
+ /**
41
+ * The function turns the map of component functions to a map of Promises when called
42
+ * @returns {[name: string]: <Promise>componentInterface}
43
+ */
44
+ export const getComponentPromises = () => {
45
+ return Object.fromEntries(
46
+ Object.entries(components)
47
+ .filter(([key]) => {
48
+ return !options?.exclude?.includes(key)}
49
+ )
50
+ .filter(([key]) => {
51
+ return options?.include?.some(e => e.includes('.'))
52
+ ? options?.include?.some(e => e.startsWith(key))
53
+ : options?.include?.length === 0 || options?.include?.includes(key)
54
+ }
55
+ )
56
+ .map(([key, value]) => [key, value()])
57
+ );
58
+ }
@@ -0,0 +1,34 @@
1
+ import {componentInterface} from '../factory'
2
+ import {filterFingerprintData} from './functions'
3
+
4
+ const test_components: componentInterface = {
5
+ 'one': '1',
6
+ 'two': 2,
7
+ 'three': {'a': true, 'b': false}
8
+ }
9
+
10
+ describe('component filtering tests', () => {
11
+ test("excluding top level works", () => {
12
+ expect(filterFingerprintData(test_components, ['one'], [])).toMatchObject({
13
+ 'two': 2, 'three': {'a': true, 'b': false}
14
+ })
15
+ });
16
+ test("including top level works", () => {
17
+ expect(filterFingerprintData(test_components, [], ['one', 'two'])).toMatchObject({
18
+ 'one': '1', 'two': 2
19
+ })
20
+ });
21
+ test("excluding low-level works", () => {
22
+ expect(filterFingerprintData(test_components, ['two', 'three.a'], [])).toMatchObject({
23
+ 'one': '1',
24
+ 'three': {'b': false}
25
+ })
26
+ });
27
+ test("including low-level works", () => {
28
+ expect(filterFingerprintData(test_components, [], ['one', 'three.b'])).toMatchObject({
29
+ 'one': '1',
30
+ 'three': {'b': false}
31
+ })
32
+ });
33
+
34
+ });