@thumbmarkjs/thumbmarkjs 1.9.0 → 1.9.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.
Files changed (46) hide show
  1. package/dist/thumbmark.cjs.js +1 -1
  2. package/dist/thumbmark.cjs.js.map +1 -1
  3. package/dist/thumbmark.esm.js +1 -1
  4. package/dist/thumbmark.esm.js.map +1 -1
  5. package/dist/thumbmark.umd.js +1 -1
  6. package/dist/thumbmark.umd.js.map +1 -1
  7. package/dist/types/components/audio/index.d.ts +2 -0
  8. package/dist/types/components/canvas/index.d.ts +3 -0
  9. package/dist/types/components/fonts/index.d.ts +4 -0
  10. package/dist/types/components/hardware/index.d.ts +2 -0
  11. package/dist/types/components/intl/index.d.ts +2 -0
  12. package/dist/types/components/locales/index.d.ts +2 -0
  13. package/dist/types/components/math/index.d.ts +2 -0
  14. package/dist/types/components/mathml/index.d.ts +2 -0
  15. package/dist/types/components/mediaDevices/index.d.ts +2 -0
  16. package/dist/types/components/permissions/index.d.ts +3 -0
  17. package/dist/types/components/plugins/index.d.ts +2 -0
  18. package/dist/types/components/screen/index.d.ts +2 -0
  19. package/dist/types/components/speech/index.d.ts +2 -0
  20. package/dist/types/components/system/browser.d.ts +9 -0
  21. package/dist/types/components/system/index.d.ts +2 -0
  22. package/dist/types/components/webgl/index.d.ts +13 -0
  23. package/dist/types/components/webrtc/index.d.ts +3 -0
  24. package/dist/types/factory.d.ts +66 -0
  25. package/dist/types/functions/api.d.ts +73 -0
  26. package/dist/types/functions/filterComponents.d.ts +15 -0
  27. package/dist/types/functions/index.d.ts +63 -0
  28. package/dist/types/functions/legacy_functions.d.ts +27 -0
  29. package/dist/types/index.d.ts +9 -0
  30. package/dist/types/options.d.ts +76 -0
  31. package/dist/types/thumbmark.d.ts +28 -0
  32. package/dist/types/utils/cache.d.ts +23 -0
  33. package/dist/types/utils/commonPixels.d.ts +1 -0
  34. package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
  35. package/dist/types/utils/getMostFrequent.d.ts +5 -0
  36. package/dist/types/utils/hash.d.ts +5 -0
  37. package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
  38. package/dist/types/utils/log.d.ts +9 -0
  39. package/dist/types/utils/raceAll.d.ts +10 -0
  40. package/dist/types/utils/sort.d.ts +8 -0
  41. package/dist/types/utils/stableStringify.d.ts +22 -0
  42. package/dist/types/utils/version.d.ts +4 -0
  43. package/dist/types/utils/visitorId.d.ts +14 -0
  44. package/package.json +3 -2
  45. package/src/functions/filterComponents.test.ts +35 -0
  46. package/src/functions/filterComponents.ts +2 -3
@@ -0,0 +1,66 @@
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
+ import { optionsInterface } from './options';
7
+ import getAudio from "./components/audio";
8
+ import getCanvas from "./components/canvas";
9
+ import getFonts from "./components/fonts";
10
+ import getHardware from "./components/hardware";
11
+ import getLocales from "./components/locales";
12
+ import getMath from "./components/math";
13
+ import getPermissions from "./components/permissions";
14
+ import getPlugins from "./components/plugins";
15
+ import getScreen from "./components/screen";
16
+ import getSystem from "./components/system";
17
+ import getWebGL from "./components/webgl";
18
+ import getIntl from "./components/intl";
19
+ import getMediaDevices from "./components/mediaDevices";
20
+ import getWebRTC from "./components/webrtc";
21
+ import getMathML from "./components/mathml";
22
+ import getSpeech from "./components/speech";
23
+ /**
24
+ * @description key->function map of built-in components. Do not call the function here.
25
+ */
26
+ export declare const tm_component_promises: {
27
+ audio: typeof getAudio;
28
+ canvas: typeof getCanvas;
29
+ fonts: typeof getFonts;
30
+ hardware: typeof getHardware;
31
+ locales: typeof getLocales;
32
+ math: typeof getMath;
33
+ permissions: typeof getPermissions;
34
+ plugins: typeof getPlugins;
35
+ screen: typeof getScreen;
36
+ system: typeof getSystem;
37
+ webgl: typeof getWebGL;
38
+ webrtc: typeof getWebRTC;
39
+ speech: typeof getSpeech;
40
+ };
41
+ /**
42
+ * @description key->function map of experimental components. Only resolved during logging.
43
+ */
44
+ export declare const tm_experimental_component_promises: {
45
+ intl: typeof getIntl;
46
+ mathml: typeof getMathML;
47
+ mediadevices: typeof getMediaDevices;
48
+ };
49
+ export interface componentInterface {
50
+ [key: string]: string | string[] | number | boolean | componentInterface;
51
+ }
52
+ export interface componentFunctionInterface {
53
+ (options?: optionsInterface): Promise<componentInterface | null>;
54
+ }
55
+ export declare const customComponents: {
56
+ [name: string]: componentFunctionInterface | null;
57
+ };
58
+ export declare const timeoutInstance: componentInterface;
59
+ /**
60
+ * includeComponent is the function each custom component function needs to call in order for the component to be included
61
+ * in the fingerprint.
62
+ * @param {string} name - the name identifier of the component
63
+ * @param {componentFunctionInterface} creationFunction - the function that implements the component
64
+ * @returns nothing
65
+ */
66
+ export declare const includeComponent: (name: string, creationFunction: componentFunctionInterface, options?: optionsInterface) => void;
@@ -0,0 +1,73 @@
1
+ import { OptionsAfterDefaults } from '../options';
2
+ import { componentInterface } from '../factory';
3
+ /**
4
+ * Info returned from the API (IP, classification, uniqueness, etc)
5
+ */
6
+ export interface infoInterface {
7
+ ip_address?: {
8
+ ip_address: string;
9
+ ip_identifier: string;
10
+ autonomous_system_number: number;
11
+ ip_version: 'v6' | 'v4';
12
+ };
13
+ classification?: {
14
+ tor: boolean;
15
+ vpn: boolean;
16
+ bot: boolean;
17
+ datacenter: boolean;
18
+ danger_level: number;
19
+ };
20
+ uniqueness?: {
21
+ score: number | string;
22
+ };
23
+ country?: {
24
+ iso_code: string;
25
+ name: string;
26
+ continent: {
27
+ code: string;
28
+ name: string;
29
+ };
30
+ };
31
+ visitor?: {
32
+ id: string;
33
+ isNew: boolean;
34
+ firstSeen: string;
35
+ lastSeen: string;
36
+ };
37
+ signals?: {
38
+ timezone_country_mismatch?: boolean;
39
+ };
40
+ timed_out?: boolean;
41
+ }
42
+ /**
43
+ * API response structure
44
+ */
45
+ export interface apiResponse {
46
+ info?: infoInterface;
47
+ version?: string;
48
+ components?: componentInterface;
49
+ visitorId?: string;
50
+ thumbmark?: string;
51
+ requestId?: string;
52
+ metadata?: string | object;
53
+ }
54
+ export declare class ApiError extends Error {
55
+ status: number;
56
+ constructor(status: number);
57
+ }
58
+ /**
59
+ * Calls the Thumbmark API with the given components, using caching and deduplication.
60
+ * Returns a promise for the API response or null on error.
61
+ */
62
+ export declare const getApiPromise: (options: OptionsAfterDefaults, components: componentInterface) => Promise<apiResponse | null>;
63
+ /**
64
+ * If a valid cached api response exists, returns it
65
+ * @param options
66
+ */
67
+ export declare function getCachedApiResponse(options: Pick<OptionsAfterDefaults, 'property_name_factory'>): apiResponse | undefined;
68
+ /**
69
+ * Writes the api response to the cache according to the options
70
+ * @param options
71
+ * @param response
72
+ */
73
+ export declare function setCachedApiResponse(options: Pick<OptionsAfterDefaults, 'cache_api_call' | 'cache_lifetime_in_ms' | 'property_name_factory'>, response: apiResponse): void;
@@ -0,0 +1,15 @@
1
+ import { componentInterface } from "../factory";
2
+ import { optionsInterface } from "../options";
3
+ /**
4
+ * Filters a componentInterface object based on include/exclude and stabilization options.
5
+ *
6
+ * @param obj - The object to filter
7
+ * @param options - Filtering options
8
+ * @returns Filtered object
9
+ */
10
+ /**
11
+ * Builds the full exclusion list from user options and stabilization rules,
12
+ * taking browser detection into account.
13
+ */
14
+ export declare function getExcludeList(options?: optionsInterface, obj?: componentInterface): string[];
15
+ export declare function filterThumbmarkData(obj: componentInterface, options?: optionsInterface): componentInterface;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ThumbmarkJS: Main fingerprinting and API logic
3
+ *
4
+ * This module handles component collection, API calls, uniqueness scoring, and data filtering
5
+ * for the ThumbmarkJS browser fingerprinting library.
6
+ *
7
+ */
8
+ import { optionsInterface } from "../options";
9
+ import { componentInterface, componentFunctionInterface, includeComponent as globalIncludeComponent } from "../factory";
10
+ import { infoInterface } from "./api";
11
+ /**
12
+ * Final thumbmark response structure
13
+ */
14
+ export interface ThumbmarkError {
15
+ type: 'component_timeout' | 'component_error' | 'api_timeout' | 'api_error' | 'api_unauthorized' | 'network_error' | 'fatal';
16
+ message: string;
17
+ component?: string;
18
+ }
19
+ export interface ThumbmarkResponse {
20
+ /** Hash of all components - the main fingerprint identifier */
21
+ thumbmark: string;
22
+ /** All resolved fingerprint components */
23
+ components: componentInterface;
24
+ /** Information from the API (IP, classification, uniqueness score) */
25
+ info: infoInterface;
26
+ /** Library version */
27
+ version: string;
28
+ /** Persistent visitor identifier (requires API key) */
29
+ visitorId?: string;
30
+ /** Performance timing for each component (only when options.performance is true) */
31
+ elapsed?: Record<string, number>;
32
+ /** Structured error array. Present only when errors occurred. */
33
+ error?: ThumbmarkError[];
34
+ /** Experimental components (only when options.experimental is true) */
35
+ experimental?: componentInterface;
36
+ /** Unique identifier for this API request */
37
+ requestId?: string;
38
+ /** Metadata echoed back from the API */
39
+ metadata?: string | object;
40
+ }
41
+ /**
42
+ * Main entry point: collects all components, optionally calls API, and returns thumbmark data.
43
+ *
44
+ * @param options - Options for fingerprinting and API
45
+ * @returns ThumbmarkResponse (elapsed is present only if options.performance is true)
46
+ */
47
+ export declare function getThumbmark(options?: optionsInterface, instanceCustomComponents?: Record<string, componentFunctionInterface | null>): Promise<ThumbmarkResponse>;
48
+ /**
49
+ * Resolves and times all filtered component promises from a component function map.
50
+ *
51
+ * @param comps - Map of component functions
52
+ * @param options - Options for filtering and timing
53
+ * @returns Object with elapsed times, filtered resolved components, errors, and pipeline phase timings
54
+ */
55
+ export declare function resolveClientComponents(comps: {
56
+ [key: string]: (options?: optionsInterface) => Promise<componentInterface | null>;
57
+ }, options?: optionsInterface): Promise<{
58
+ elapsed: Record<string, number>;
59
+ resolvedComponents: componentInterface;
60
+ errors: ThumbmarkError[];
61
+ pipelineTimings: Record<string, number>;
62
+ }>;
63
+ export { globalIncludeComponent as includeComponent };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * This file is here to support legacy implementations.
3
+ * Eventually, these functions will be removed to keep the library small.
4
+ */
5
+ import { componentInterface } from '../factory';
6
+ /**
7
+ *
8
+ * @deprecated
9
+ */
10
+ export declare function getFingerprintData(): Promise<componentInterface>;
11
+ /**
12
+ *
13
+ * @param includeData boolean
14
+ * @deprecated this function is going to be removed. use getThumbmark or Thumbmark class instead.
15
+ */
16
+ export declare function getFingerprint(includeData?: false): Promise<string>;
17
+ export declare function getFingerprint(includeData: true): Promise<{
18
+ hash: string;
19
+ data: componentInterface;
20
+ }>;
21
+ /**
22
+ *
23
+ * @deprecated use Thumbmark or getThumbmark instead with options
24
+ */
25
+ export declare function getFingerprintPerformance(): Promise<{
26
+ elapsed: Record<string, number>;
27
+ }>;
@@ -0,0 +1,9 @@
1
+ import { getFingerprint, getFingerprintData, getFingerprintPerformance } from './functions/legacy_functions';
2
+ import { getThumbmark, ThumbmarkResponse, ThumbmarkError } from './functions';
3
+ import { getVersion } from './utils/version';
4
+ import { setOption, optionsInterface, stabilizationExclusionRules } from './options';
5
+ import { includeComponent } from './factory';
6
+ import { Thumbmark } from './thumbmark';
7
+ import { filterThumbmarkData } from './functions/filterComponents';
8
+ import { stableStringify } from './utils/stableStringify';
9
+ export { Thumbmark, getThumbmark, getVersion, ThumbmarkResponse, ThumbmarkError, filterThumbmarkData, optionsInterface, stabilizationExclusionRules, stableStringify, setOption, getFingerprint, getFingerprintData, getFingerprintPerformance, includeComponent };
@@ -0,0 +1,76 @@
1
+ type JSONValue = string | number | boolean | null | JSONValue[] | {
2
+ [key: string]: JSONValue;
3
+ };
4
+ export interface OptionsAfterDefaults {
5
+ /**
6
+ * A function to customise localStorage names used by thumbmark
7
+ * @param name Original name of the storage property eg. visitor_id
8
+ * @returns The name under which the storage property should be saved eg. myprefix_visitor_id
9
+ */
10
+ property_name_factory: (name: string) => string;
11
+ /**
12
+ * @deprecated use property_name_factory
13
+ */
14
+ storage_property_name?: string;
15
+ exclude?: string[];
16
+ include?: string[];
17
+ permissions_to_check?: PermissionName[];
18
+ timeout?: number;
19
+ logging?: boolean;
20
+ api_key?: string;
21
+ api_endpoint?: string;
22
+ /**
23
+ * @deprecated This will be removed in Thumbmarkjs 2.0, use cache_lifetime_in_ms instead
24
+ */
25
+ cache_api_call?: boolean;
26
+ /**
27
+ * How long the cache will be valid for, maximum is 72h (259_200_000)
28
+ */
29
+ cache_lifetime_in_ms: number;
30
+ performance?: boolean;
31
+ stabilize?: string[];
32
+ experimental?: boolean;
33
+ /**
34
+ * Optional metadata to pass through to the API and webhooks.
35
+ * Can be a static JSON object or a function that returns a JSON object (evaluated at request time).
36
+ * This field is excluded from fingerprint calculation.
37
+ * Maximum length: 1000 characters when stringified.
38
+ * @example metadata: { userId: "123", eventType: "login" }
39
+ * @example metadata: () => ({ timestamp: Date.now(), sessionId: "abc" })
40
+ */
41
+ metadata?: JSONValue | (() => JSONValue);
42
+ }
43
+ export type optionsInterface = Partial<OptionsAfterDefaults>;
44
+ export declare const DEFAULT_CACHE_LIFETIME = 0;
45
+ export declare const MAXIMUM_CACHE_LIFETIME = 259200000;
46
+ export declare const DEFAULT_STORAGE_PREFIX = "thumbmark";
47
+ export declare const DEFAULT_API_ENDPOINT = "https://api.thumbmarkjs.com";
48
+ export declare const defaultOptions: OptionsAfterDefaults;
49
+ export declare let options: OptionsAfterDefaults;
50
+ /**
51
+ *
52
+ * @param key @deprecated this function will be removed
53
+ * @param value
54
+ */
55
+ export declare function setOption<K extends keyof optionsInterface>(key: K, value: OptionsAfterDefaults[K]): void;
56
+ export declare const stabilizationExclusionRules: {
57
+ private: {
58
+ exclude: string[];
59
+ browsers: string[];
60
+ }[];
61
+ iframe: ({
62
+ exclude: string[];
63
+ browsers: string[];
64
+ } | {
65
+ exclude: string[];
66
+ browsers?: undefined;
67
+ })[];
68
+ vpn: {
69
+ exclude: string[];
70
+ }[];
71
+ always: {
72
+ exclude: string[];
73
+ browsers: string[];
74
+ }[];
75
+ };
76
+ export {};
@@ -0,0 +1,28 @@
1
+ import { optionsInterface } from "./options";
2
+ import { ThumbmarkResponse } from './functions';
3
+ import { componentInterface } from "./factory";
4
+ /**
5
+ * A client for generating thumbmarks with a persistent configuration.
6
+ */
7
+ export declare class Thumbmark {
8
+ private options;
9
+ private customComponents;
10
+ /**
11
+ * Creates a new Thumbmarker client instance.
12
+ * @param options - Default configuration options for this instance.
13
+ */
14
+ constructor(options?: optionsInterface);
15
+ /**
16
+ * Generates a thumbmark using the instance's configuration.
17
+ * @param overrideOptions - Options to override for this specific call.
18
+ * @returns The thumbmark result containing the fingerprint hash, components, and metadata.
19
+ */
20
+ get(overrideOptions?: optionsInterface): Promise<ThumbmarkResponse>;
21
+ getVersion(): string;
22
+ /**
23
+ * Register a custom component to be included in the fingerprint.
24
+ * @param key - The component name
25
+ * @param fn - The component function
26
+ */
27
+ includeComponent(key: string, fn: (options?: optionsInterface) => Promise<componentInterface | null>): void;
28
+ }
@@ -0,0 +1,23 @@
1
+ import { apiResponse } from "../functions/api";
2
+ import { OptionsAfterDefaults } from "../options";
3
+ export declare const CACHE_KEY = "cache";
4
+ export interface Cache {
5
+ apiResponse?: apiResponse;
6
+ apiResponseExpiry?: number;
7
+ }
8
+ /**
9
+ * Get all values from cache
10
+ * @param _options
11
+ */
12
+ export declare function getCache(_options: Pick<OptionsAfterDefaults, 'property_name_factory'>): Cache;
13
+ /**
14
+ * Write given values to cache
15
+ * @param _options
16
+ * @param values
17
+ */
18
+ export declare function setCache(_options: OptionsAfterDefaults, values: Partial<Cache>): void;
19
+ /**
20
+ * Returns the expiry time for cache
21
+ * @param _options
22
+ */
23
+ export declare function getApiResponseExpiry(_options: Pick<OptionsAfterDefaults, 'cache_lifetime_in_ms'>): number;
@@ -0,0 +1 @@
1
+ export declare function getCommonPixels(images: ImageData[], width: number, height: number): ImageData;
@@ -0,0 +1,4 @@
1
+ export declare function ephemeralIFrame(callback: ({ iframe }: {
2
+ iframe: Document;
3
+ }) => void): Promise<any>;
4
+ export declare function wait<T = void>(durationMs: number, resolveWith?: T): Promise<T>;
@@ -0,0 +1,5 @@
1
+ export declare function mostFrequentValuesInArrayOfDictionaries(arr: {
2
+ [key: string]: any;
3
+ }[], keys: string[]): {
4
+ [key: string]: any;
5
+ };
@@ -0,0 +1,5 @@
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;
@@ -0,0 +1 @@
1
+ export declare function imageDataToDataURL(imageData: ImageData): string;
@@ -0,0 +1,9 @@
1
+ import { componentInterface } from '../factory';
2
+ import { optionsInterface } from '../options';
3
+ import type { ThumbmarkError } from '../functions';
4
+ /**
5
+ * Logs thumbmark data to remote logging endpoint (only once per session)
6
+ * You can disable this by setting options.logging to false.
7
+ * @internal
8
+ */
9
+ export declare function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData?: componentInterface, errors?: ThumbmarkError[]): Promise<void>;
@@ -0,0 +1,10 @@
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
+ error?: string;
7
+ }
8
+ export declare function raceAllPerformance<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<RaceResult<T>[]>;
9
+ export declare function raceAll<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<(T | undefined)[]>;
10
+ export {};
@@ -0,0 +1,8 @@
1
+ import { componentInterface } from '../factory';
2
+ /**
3
+ * Recursively sorts the keys of a component object alphabetically.
4
+ * This ensures a consistent order for hashing.
5
+ * @param obj The component object to sort.
6
+ * @returns A new object with sorted keys.
7
+ */
8
+ export declare function sortComponentKeys(obj: componentInterface): componentInterface;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Stable JSON stringify implementation
3
+ * Based on fast-json-stable-stringify by Evgeny Poberezkin
4
+ * https://github.com/epoberezkin/fast-json-stable-stringify
5
+ *
6
+ * This implementation ensures consistent JSON serialization by sorting object keys,
7
+ * which is critical for generating stable hashes from fingerprint data.
8
+ */
9
+ /**
10
+ * Converts data to a stable JSON string with sorted keys
11
+ *
12
+ * @param data - The data to stringify
13
+ * @returns Stable JSON string representation
14
+ * @throws TypeError if circular reference is detected
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const obj = { b: 2, a: 1 };
19
+ * stableStringify(obj); // '{"a":1,"b":2}'
20
+ * ```
21
+ */
22
+ export declare function stableStringify(data: any): string;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns the current package version
3
+ */
4
+ export declare function getVersion(): string;
@@ -0,0 +1,14 @@
1
+ import { OptionsAfterDefaults } from "../options";
2
+ /**
3
+ * Get the storage property name for visitor id
4
+ * @param _options
5
+ */
6
+ export declare function getVisitorIdPropertyName(_options: Pick<OptionsAfterDefaults, 'storage_property_name' | 'property_name_factory'>): string;
7
+ /**
8
+ * Gets visitor ID from localStorage, returns null if unavailable
9
+ */
10
+ export declare function getVisitorId(_options: OptionsAfterDefaults): string | null;
11
+ /**
12
+ * Sets visitor ID in localStorage
13
+ */
14
+ export declare function setVisitorId(visitorId: string, _options: OptionsAfterDefaults): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thumbmarkjs/thumbmarkjs",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/thumbmark.cjs.js",
@@ -22,7 +22,8 @@
22
22
  "test": "jest",
23
23
  "test:e2e": "npx playwright test",
24
24
  "build": "rm -rf dist/* && rm -rf types/* && rollup -c",
25
- "watch": "rollup -c -w"
25
+ "watch": "rollup -c -w",
26
+ "perf": "playwright test perf/perf.spec.ts"
26
27
  },
27
28
  "keywords": [
28
29
  "thumbmark",
@@ -161,6 +161,17 @@ describe('getExcludeList', () => {
161
161
  // Should not throw
162
162
  expect(() => getExcludeList({ ...defaultOptions }, obj)).not.toThrow();
163
163
  });
164
+
165
+ test('non-array string exclude does not throw and is ignored', () => {
166
+ const result = getExcludeList({ ...defaultOptions, exclude: 'one' as any, stabilize: [] });
167
+ expect(Array.isArray(result)).toBe(true);
168
+ expect(result).not.toContain('one');
169
+ });
170
+
171
+ test('non-array object exclude does not throw and is ignored', () => {
172
+ const result = getExcludeList({ ...defaultOptions, exclude: {} as any, stabilize: [] });
173
+ expect(Array.isArray(result)).toBe(true);
174
+ });
164
175
  });
165
176
 
166
177
  // ── filterThumbmarkData ─────────────────────────────────────────
@@ -235,4 +246,28 @@ describe('filterThumbmarkData', () => {
235
246
  expect((result.audio as componentInterface).other).toBe('jkl');
236
247
  expect((result.audio as componentInterface).sampleHash).toBeUndefined();
237
248
  });
249
+
250
+ test('non-array string include does not throw and does not rescue excluded keys', () => {
251
+ const result = filterThumbmarkData(testData, {
252
+ ...defaultOptions,
253
+ exclude: ['one'],
254
+ include: 'one' as any,
255
+ stabilize: [],
256
+ });
257
+ // Bad include is treated as empty, so 'one' stays excluded
258
+ expect(result.one).toBeUndefined();
259
+ expect(result.two).toBe(2);
260
+ });
261
+
262
+ test('null include does not throw and behaves as empty include', () => {
263
+ const result = filterThumbmarkData(testData, {
264
+ ...defaultOptions,
265
+ exclude: ['two'],
266
+ include: null as any,
267
+ stabilize: [],
268
+ });
269
+ // null include treated as empty, so 'two' stays excluded
270
+ expect(result.two).toBeUndefined();
271
+ expect(result.one).toBe('1');
272
+ });
238
273
  });
@@ -27,7 +27,7 @@ export function getExcludeList(options?: optionsInterface, obj?: componentInterf
27
27
 
28
28
  const name = browser.name.toLowerCase();
29
29
  const majorVer = parseInt(browser.version.split('.')[0] || '0', 10);
30
- const excludeList = [...(options?.exclude || [])];
30
+ const excludeList = Array.isArray(options?.exclude) ? [...options.exclude] : [];
31
31
  const stabilizationOptions = [...new Set([...(options?.stabilize || []), 'always'])];
32
32
 
33
33
  for (const option of stabilizationOptions) {
@@ -52,7 +52,7 @@ export function filterThumbmarkData(
52
52
  options?: optionsInterface,
53
53
  ): componentInterface {
54
54
  const excludeList = getExcludeList(options, obj);
55
- const includeList = options?.include || [];
55
+ const includeList = Array.isArray(options?.include) ? options.include : [];
56
56
 
57
57
  /**
58
58
  * Inner recursive function to perform the actual filtering.
@@ -84,6 +84,5 @@ export function filterThumbmarkData(
84
84
  return result;
85
85
  }
86
86
 
87
- // Start the filtering process
88
87
  return performFilter(obj);
89
88
  }