@thumbmarkjs/thumbmarkjs 1.3.4 → 1.4.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.
@@ -1,6 +1,7 @@
1
1
  import { componentInterface } from '../../factory';
2
2
  import { hash } from '../../utils/hash';
3
3
  import { ephemeralIFrame } from '../../utils/ephemeralIFrame';
4
+ import { stableStringify } from '../../utils/stableStringify';
4
5
 
5
6
  const BLACKBOARD_BOLD = ['\uD835\uDD04', '\uD835\uDD05', '\u212D', '\uD835\uDD07', '\uD835\uDD08', '\uD835\uDD09', '\uD835\uDD38', '\uD835\uDD39', '\u2102', '\uD835\uDD3B', '\uD835\uDD3C', '\uD835\uDD3D'];
6
7
  const GREEK_SYMBOLS = ['\u03B2', '\u03C8', '\u03BB', '\u03B5', '\u03B6', '\u03B1', '\u03BE', '\u03BC', '\u03C1', '\u03C6', '\u03BA', '\u03C4', '\u03B7', '\u03C3', '\u03B9', '\u03C9', '\u03B3', '\u03BD', '\u03C7', '\u03B4', '\u03B8', '\u03C0', '\u03C5', '\u03BF'];
@@ -38,7 +39,7 @@ export default async function getMathML(): Promise<componentInterface | null> {
38
39
  });
39
40
  // Capture font style hash from the first structure (it's the same for all)
40
41
  if (i === 0 && measurement.fontInfo) {
41
- fontStyleHash = hash(JSON.stringify(measurement.fontInfo));
42
+ fontStyleHash = hash(stableStringify(measurement.fontInfo));
42
43
  }
43
44
  });
44
45
 
@@ -50,7 +51,7 @@ export default async function getMathML(): Promise<componentInterface | null> {
50
51
  resolve({
51
52
  //supported: true,
52
53
  details,
53
- hash: hash(JSON.stringify(details))
54
+ hash: hash(stableStringify(details))
54
55
  });
55
56
 
56
57
  } catch (error) {
@@ -92,7 +93,7 @@ function createMathML(name: string, content: string): string {
92
93
 
93
94
  function createComplexNestedStructure(): string {
94
95
  let nestedContent = '<mo>\u220F</mo>'; // Product symbol (∏)
95
-
96
+
96
97
  // Add all symbol combinations inside the main structure
97
98
  BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
98
99
  const startIdx = bbIndex * 2;
@@ -102,7 +103,7 @@ function createComplexNestedStructure(): string {
102
103
  nestedContent += `<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`;
103
104
  }
104
105
  });
105
-
106
+
106
107
  return createMathML('complex_nested',
107
108
  `<munderover><mmultiscripts>${nestedContent}</mmultiscripts></munderover>`
108
109
  );
@@ -110,7 +111,7 @@ function createComplexNestedStructure(): string {
110
111
 
111
112
  function createSymbolStructures(): string[] {
112
113
  const structures: string[] = [];
113
-
114
+
114
115
  // Use blackboard bold as base symbols with Greek symbols as subscripts/superscripts
115
116
  BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
116
117
  // Get 2 Greek symbols for this blackboard bold symbol (lower left, top right)
@@ -123,7 +124,7 @@ function createSymbolStructures(): string[] {
123
124
  ));
124
125
  }
125
126
  });
126
-
127
+
127
128
  return structures;
128
129
  }
129
130
 
@@ -1,5 +1,6 @@
1
1
  import { componentInterface } from '../../factory';
2
2
  import { hash } from '../../utils/hash';
3
+ import { stableStringify } from '../../utils/stableStringify';
3
4
 
4
5
  const VOICE_LOAD_TIMEOUT = 800; // milliseconds to wait for voices to load
5
6
 
@@ -53,12 +54,12 @@ export default async function getSpeech(): Promise<componentInterface | null> {
53
54
  // Create details object with count and hash
54
55
  const details = {
55
56
  voiceCount: voices.length,
56
- voicesHash: hash(JSON.stringify(voiceSignatures))
57
+ voicesHash: hash(stableStringify(voiceSignatures))
57
58
  };
58
59
 
59
60
  resolve({
60
61
  details,
61
- hash: hash(JSON.stringify(details))
62
+ hash: hash(stableStringify(details))
62
63
  });
63
64
 
64
65
  } catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { componentInterface } from '../../factory';
2
2
  import { hash } from '../../utils/hash';
3
+ import { stableStringify } from '../../utils/stableStringify';
3
4
 
4
5
  export default async function getWebRTC(): Promise<componentInterface | null> {
5
6
  return new Promise((resolve) => {
@@ -29,7 +30,7 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
29
30
  await connection.setLocalDescription(offer);
30
31
 
31
32
  const sdp = offer.sdp || '';
32
-
33
+
33
34
  // Extract RTP extensions
34
35
  const extensions = [...new Set((sdp.match(/extmap:\d+ [^\n\r]+/g) || []).map((x: string) => x.replace(/extmap:\d+ /, '')))].sort();
35
36
 
@@ -70,13 +71,13 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
70
71
  const compressedData = {
71
72
  audio: {
72
73
  count: audioCodecs.length,
73
- hash: hash(JSON.stringify(audioCodecs))
74
+ hash: hash(stableStringify(audioCodecs))
74
75
  },
75
76
  video: {
76
77
  count: videoCodecs.length,
77
- hash: hash(JSON.stringify(videoCodecs))
78
+ hash: hash(stableStringify(videoCodecs))
78
79
  },
79
- extensionsHash: hash(JSON.stringify(extensions))
80
+ extensionsHash: hash(stableStringify(extensions))
80
81
  };
81
82
 
82
83
  // Set up for ICE candidate collection with timeout
@@ -111,7 +112,7 @@ export default async function getWebRTC(): Promise<componentInterface | null> {
111
112
 
112
113
  resolve({
113
114
  details: result,
114
- hash: hash(JSON.stringify(result)),
115
+ hash: hash(stableStringify(result)),
115
116
  });
116
117
 
117
118
  } catch (error) {
@@ -3,6 +3,7 @@ import { componentInterface } from '../factory';
3
3
  import { getVisitorId, setVisitorId } from '../utils/visitorId';
4
4
  import { getVersion } from "../utils/version";
5
5
  import { hash } from '../utils/hash';
6
+ import { stableStringify } from '../utils/stableStringify';
6
7
 
7
8
  // ===================== Types & Interfaces =====================
8
9
 
@@ -37,6 +38,7 @@ interface apiResponse {
37
38
  version?: string;
38
39
  components?: componentInterface;
39
40
  visitorId?: string;
41
+ thumbmark?: string;
40
42
  }
41
43
 
42
44
  // ===================== API Call Logic =====================
@@ -66,16 +68,16 @@ export const getApiPromise = (
66
68
  const apiEndpoint = options.api_endpoint || DEFAULT_API_ENDPOINT;
67
69
  const endpoint = `${apiEndpoint}/thumbmark`;
68
70
  const visitorId = getVisitorId();
69
- const requestBody: any = {
70
- components,
71
- options,
72
- clientHash: hash(JSON.stringify(components)),
71
+ const requestBody: any = {
72
+ components,
73
+ options,
74
+ clientHash: hash(stableStringify(components)),
73
75
  version: getVersion()
74
76
  };
75
77
  if (visitorId) {
76
78
  requestBody.visitorId = visitorId;
77
79
  }
78
-
80
+
79
81
  const fetchPromise = fetch(endpoint, {
80
82
  method: 'POST',
81
83
  headers: {
@@ -7,13 +7,13 @@
7
7
  */
8
8
 
9
9
  import { defaultOptions, optionsInterface } from "../options";
10
- import {
11
- timeoutInstance,
12
- componentInterface,
13
- tm_component_promises,
14
- customComponents,
15
- tm_experimental_component_promises,
16
- includeComponent as globalIncludeComponent
10
+ import {
11
+ timeoutInstance,
12
+ componentInterface,
13
+ tm_component_promises,
14
+ customComponents,
15
+ tm_experimental_component_promises,
16
+ includeComponent as globalIncludeComponent
17
17
  } from "../factory";
18
18
  import { hash } from "../utils/hash";
19
19
  import { raceAllPerformance } from "../utils/raceAll";
@@ -21,20 +21,21 @@ import { getVersion } from "../utils/version";
21
21
  import { filterThumbmarkData } from './filterComponents'
22
22
  import { logThumbmarkData } from '../utils/log';
23
23
  import { getApiPromise, infoInterface } from "./api";
24
+ import { stableStringify } from "../utils/stableStringify";
24
25
 
25
26
 
26
27
  /**
27
28
  * Final thumbmark response structure
28
29
  */
29
30
  interface thumbmarkResponse {
30
- components: componentInterface,
31
- info: { [key: string]: any },
32
- version: string,
33
- thumbmark: string,
34
- visitorId?: string,
35
- elapsed?: any;
36
- error?: string;
37
- experimental?: componentInterface;
31
+ components: componentInterface,
32
+ info: { [key: string]: any },
33
+ version: string,
34
+ thumbmark: string,
35
+ visitorId?: string,
36
+ elapsed?: any;
37
+ error?: string;
38
+ experimental?: componentInterface;
38
39
  }
39
40
 
40
41
  /**
@@ -44,69 +45,73 @@ interface thumbmarkResponse {
44
45
  * @returns thumbmarkResponse (elapsed is present only if options.performance is true)
45
46
  */
46
47
  export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> {
47
- const _options = { ...defaultOptions, ...options };
48
-
49
- // Early logging decision
50
- const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
51
-
52
- // Merge built-in and user-registered components
53
- const allComponents = { ...tm_component_promises, ...customComponents };
54
- const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
55
-
56
- // Resolve experimental components only when logging
57
- let experimentalComponents = {};
58
- let experimentalElapsed = {};
59
- if (shouldLog || _options.experimental) {
60
- const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
61
- experimentalComponents = resolvedComponents;
62
- experimentalElapsed = expElapsed;
63
- }
48
+ const _options = { ...defaultOptions, ...options };
64
49
 
65
- const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
66
- let apiResult = null;
67
-
68
- if (apiPromise) {
69
- try {
70
- apiResult = await apiPromise;
71
- } catch (error) {
72
- // Handle API key/quota errors
73
- if (error instanceof Error && error.message === 'INVALID_API_KEY') {
74
- return {
75
- error: 'Invalid API key or quota exceeded',
76
- components: {},
77
- info: {},
78
- version: getVersion(),
79
- thumbmark: ''
80
- };
81
- }
82
- throw error; // Re-throw other errors
83
- }
84
- }
50
+ // Early logging decision
51
+ const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
52
+
53
+ // Merge built-in and user-registered components
54
+ const allComponents = { ...tm_component_promises, ...customComponents };
55
+ const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
56
+
57
+ // Resolve experimental components only when logging
58
+ let experimentalComponents = {};
59
+ let experimentalElapsed = {};
60
+ if (shouldLog || _options.experimental) {
61
+ const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
62
+ experimentalComponents = resolvedComponents;
63
+ experimentalElapsed = expElapsed;
64
+ }
65
+
66
+ const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
67
+ let apiResult = null;
85
68
 
86
- // Only add 'elapsed' if performance is true
87
- const allElapsed = { ...elapsed, ...experimentalElapsed };
88
- const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
89
- const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
90
- const components = {...clientComponentsResult, ...apiComponents};
91
- const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
92
- const thumbmark = hash(JSON.stringify(components));
93
- const version = getVersion();
94
- // Only log to server when not in debug mode
95
- if (shouldLog) {
96
- logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ });
69
+ if (apiPromise) {
70
+ try {
71
+ apiResult = await apiPromise;
72
+ } catch (error) {
73
+ // Handle API key/quota errors
74
+ if (error instanceof Error && error.message === 'INVALID_API_KEY') {
75
+ return {
76
+ error: 'Invalid API key or quota exceeded',
77
+ components: {},
78
+ info: {},
79
+ version: getVersion(),
80
+ thumbmark: ''
81
+ };
82
+ }
83
+ throw error; // Re-throw other errors
97
84
  }
98
-
99
- const result: thumbmarkResponse = {
100
- ...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
101
- thumbmark,
102
- components: components,
103
- info,
104
- version,
105
- ...maybeElapsed,
106
- ...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
107
- };
108
-
109
- return result;
85
+ }
86
+
87
+ // Only add 'elapsed' if performance is true
88
+ const allElapsed = { ...elapsed, ...experimentalElapsed };
89
+ const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
90
+ const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
91
+ const components = { ...clientComponentsResult, ...apiComponents };
92
+ const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
93
+
94
+ // Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
95
+ console.log(apiResult);
96
+ const thumbmark = apiResult?.thumbmark ?? hash(stableStringify(components));
97
+ const version = getVersion();
98
+
99
+ // Only log to server when not in debug mode
100
+ if (shouldLog) {
101
+ logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ });
102
+ }
103
+
104
+ const result: thumbmarkResponse = {
105
+ ...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
106
+ thumbmark,
107
+ components: components,
108
+ info,
109
+ version,
110
+ ...maybeElapsed,
111
+ ...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
112
+ };
113
+
114
+ return result;
110
115
  }
111
116
 
112
117
  // ===================== Component Resolution & Performance =====================
package/src/index.ts CHANGED
@@ -2,19 +2,24 @@ import {
2
2
  getFingerprint,
3
3
  getFingerprintData,
4
4
  getFingerprintPerformance
5
- } from './functions/legacy_functions'
5
+ } from './functions/legacy_functions'
6
6
  import { getThumbmark } from './functions'
7
7
  import { getVersion } from './utils/version';
8
8
  import { setOption, optionsInterface, stabilizationExclusionRules } from './options'
9
9
  import { includeComponent } from './factory'
10
10
  import { Thumbmark } from './thumbmark'
11
11
  import { filterThumbmarkData } from './functions/filterComponents'
12
+ import { stableStringify } from './utils/stableStringify'
13
+
14
+ export {
15
+ Thumbmark, getThumbmark, getVersion,
12
16
 
13
- export { Thumbmark, getThumbmark, getVersion,
14
-
15
17
  // Filtering functions for server-side use
16
18
  filterThumbmarkData, optionsInterface, stabilizationExclusionRules,
17
19
 
20
+ // Stable JSON stringify for consistent hashing
21
+ stableStringify,
22
+
18
23
  // deprecated functions. Don't use anymore.
19
24
  setOption, getFingerprint, getFingerprintData, getFingerprintPerformance, includeComponent
20
25
  }