fingerprintjs
206 строк · 6.3 Кб
1import { version } from '../package.json'
2import { requestIdleCallbackIfAvailable } from './utils/async'
3import { UnknownComponents } from './utils/entropy_source'
4import { x64hash128 } from './utils/hashing'
5import { errorToObject } from './utils/misc'
6import loadBuiltinSources, { BuiltinComponents } from './sources'
7import getConfidence, { Confidence } from './confidence'
8
9/**
10* Options for Fingerprint class loading
11*/
12export interface LoadOptions {
13/**
14* When browser doesn't support `requestIdleCallback` a `setTimeout` will be used. This number is only for Safari and
15* old Edge, because Chrome/Blink based browsers support `requestIdleCallback`. The value is in milliseconds.
16* @default 50
17*/
18delayFallback?: number
19/**
20* Whether to print debug messages to the console.
21* Required to ease investigations of problems.
22*/
23debug?: boolean
24}
25
26/**
27* Options for getting visitor identifier
28*/
29export interface GetOptions {
30/**
31* Whether to print debug messages to the console.
32*
33* @deprecated Use the `debug` option of `load()` instead
34*/
35debug?: boolean
36}
37
38/**
39* Result of getting a visitor identifier
40*/
41export interface GetResult {
42/**
43* The visitor identifier
44*/
45visitorId: string
46/**
47* A confidence score that tells how much the agent is sure about the visitor identifier
48*/
49confidence: Confidence
50/**
51* List of components that has formed the visitor identifier.
52*
53* Warning! The type of this property is specific but out of Semantic Versioning, i.e. may have incompatible changes
54* within a major version. If you want to avoid breaking changes, treat the property as having type
55* `UnknownComponents` that is more generic but guarantees backward compatibility within a major version.
56*/
57components: BuiltinComponents
58/**
59* The fingerprinting algorithm version
60*
61* @see https://github.com/fingerprintjs/fingerprintjs#version-policy For more details
62*/
63version: string
64}
65
66/**
67* Agent object that can get visitor identifier
68*/
69export interface Agent {
70/**
71* Gets the visitor identifier
72*/
73get(options?: Readonly<GetOptions>): Promise<GetResult>
74}
75
76function componentsToCanonicalString(components: UnknownComponents) {
77let result = ''
78for (const componentKey of Object.keys(components).sort()) {
79const component = components[componentKey]
80const value = 'error' in component ? 'error' : JSON.stringify(component.value)
81result += `${result ? '|' : ''}${componentKey.replace(/([:|\\])/g, '\\$1')}:${value}`
82}
83return result
84}
85
86export function componentsToDebugString(components: UnknownComponents): string {
87return JSON.stringify(
88components,
89(_key, value) => {
90if (value instanceof Error) {
91return errorToObject(value)
92}
93return value
94},
952,
96)
97}
98
99export function hashComponents(components: UnknownComponents): string {
100return x64hash128(componentsToCanonicalString(components))
101}
102
103/**
104* Makes a GetResult implementation that calculates the visitor id hash on demand.
105* Designed for optimisation.
106*/
107function makeLazyGetResult(components: BuiltinComponents): GetResult {
108let visitorIdCache: string | undefined
109
110// This function runs very fast, so there is no need to make it lazy
111const confidence = getConfidence(components)
112
113// A plain class isn't used because its getters and setters aren't enumerable.
114return {
115get visitorId(): string {
116if (visitorIdCache === undefined) {
117visitorIdCache = hashComponents(this.components)
118}
119return visitorIdCache
120},
121set visitorId(visitorId: string) {
122visitorIdCache = visitorId
123},
124confidence,
125components,
126version,
127}
128}
129
130/**
131* A delay is required to ensure consistent entropy components.
132* See https://github.com/fingerprintjs/fingerprintjs/issues/254
133* and https://github.com/fingerprintjs/fingerprintjs/issues/307
134* and https://github.com/fingerprintjs/fingerprintjs/commit/945633e7c5f67ae38eb0fea37349712f0e669b18
135*/
136export function prepareForSources(delayFallback = 50): Promise<void> {
137// A proper deadline is unknown. Let it be twice the fallback timeout so that both cases have the same average time.
138return requestIdleCallbackIfAvailable(delayFallback, delayFallback * 2)
139}
140
141/**
142* The function isn't exported from the index file to not allow to call it without `load()`.
143* The hiding gives more freedom for future non-breaking updates.
144*
145* A factory function is used instead of a class to shorten the attribute names in the minified code.
146* Native private class fields could've been used, but TypeScript doesn't allow them with `"target": "es5"`.
147*/
148function makeAgent(getComponents: () => Promise<BuiltinComponents>, debug?: boolean): Agent {
149const creationTime = Date.now()
150
151return {
152async get(options) {
153const startTime = Date.now()
154const components = await getComponents()
155const result = makeLazyGetResult(components)
156
157if (debug || options?.debug) {
158// console.log is ok here because it's under a debug clause
159// eslint-disable-next-line no-console
160console.log(`Copy the text below to get the debug data:
161
162\`\`\`
163version: ${result.version}
164userAgent: ${navigator.userAgent}
165timeBetweenLoadAndGet: ${startTime - creationTime}
166visitorId: ${result.visitorId}
167components: ${componentsToDebugString(components)}
168\`\`\``)
169}
170
171return result
172},
173}
174}
175
176/**
177* Sends an unpersonalized AJAX request to collect installation statistics
178*/
179function monitor() {
180// The FingerprintJS CDN (https://github.com/fingerprintjs/cdn) replaces `window.__fpjs_d_m` with `true`
181if (window.__fpjs_d_m || Math.random() >= 0.001) {
182return
183}
184try {
185const request = new XMLHttpRequest()
186request.open('get', `https://m1.openfpcdn.io/fingerprintjs/v${version}/npm-monitoring`, true)
187request.send()
188} catch (error) {
189// console.error is ok here because it's an unexpected error handler
190// eslint-disable-next-line no-console
191console.error(error)
192}
193}
194
195/**
196* Builds an instance of Agent and waits a delay required for a proper operation.
197*/
198export async function load(options: Readonly<LoadOptions> = {}): Promise<Agent> {
199if ((options as { monitoring?: boolean }).monitoring ?? true) {
200monitor()
201}
202const { delayFallback, debug } = options
203await prepareForSources(delayFallback)
204const getComponents = loadBuiltinSources({ cache: {}, debug })
205return makeAgent(getComponents, debug)
206}
207