UNPKG

@fingerprintjs/fingerprintjs-pro-spa

Version:

FingerprintJS Pro JavaScript agent for Single-Page Applications (SPA)

458 lines (442 loc) 16.8 kB
/** * FingerprintJS Pro SPA v1.3.3 - Copyright (c) FingerprintJS, Inc, 2025 (https://fingerprint.com) * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. */ 'use strict'; var tslib = require('tslib'); var FingerprintJSPro = require('@fingerprintjs/fingerprintjs-pro'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var FingerprintJSPro__namespace = /*#__PURE__*/_interopNamespaceDefault(FingerprintJSPro); const CACHE_KEY_PREFIX = '@fpjs@client@'; const MAX_CACHE_LIFE = 60 * 60 * 24; // 24 hours const DEFAULT_CACHE_LIFE = 60 * 60; // 1 hour const DEFAULT_NOW_PROVIDER = () => Date.now(); class CacheKey { constructor(options) { var _a; this.tag = options.tag || null; this.linkedId = options.linkedId || null; this.extendedResult = (_a = options.extendedResult) !== null && _a !== void 0 ? _a : false; } /** * Converts this `CacheKey` instance into a string for use in a cache * @returns A string representation of the key */ toKey() { return `${JSON.stringify(this.tag)}__${JSON.stringify(this.linkedId)}__${this.extendedResult}`; } } function getKeyWithPrefix(key, prefix) { return `${prefix}__${key}`; } function removePrefixFromKey(key, prefix) { return key.replace(`${prefix}__`, ''); } /** * Implementation of caching that uses local storage * */ class LocalStorageCache { constructor(prefix = CACHE_KEY_PREFIX) { this.prefix = prefix; } set(key, entry) { window.localStorage.setItem(getKeyWithPrefix(key, this.prefix), JSON.stringify(entry)); } get(key) { const json = window.localStorage.getItem(getKeyWithPrefix(key, this.prefix)); if (!json) { return; } try { return JSON.parse(json); } catch (e) { return; } } remove(key) { window.localStorage.removeItem(getKeyWithPrefix(key, this.prefix)); } allKeys() { return Object.keys(window.localStorage) .filter((key) => key.startsWith(this.prefix)) .map((key) => removePrefixFromKey(key, this.prefix)); } } /** * Implementation of caching that uses session storage * */ class SessionStorageCache { constructor(prefix = CACHE_KEY_PREFIX) { this.prefix = prefix; } /** * It takes a key and an entry, and sets the entry in the session storage with the key * @param {string} key - The key to store the entry under. * @param {Cacheable} entry - The value to be stored in the cache. */ set(key, entry) { window.sessionStorage.setItem(getKeyWithPrefix(key, this.prefix), JSON.stringify(entry)); } /** * It gets the value of the key from the session storage, parses it as JSON, and returns it * @param {string} key - The key to store the data under. * @returns The value of the key in the sessionStorage. */ get(key) { const json = window.sessionStorage.getItem(getKeyWithPrefix(key, this.prefix)); if (!json) { return; } try { return JSON.parse(json); } catch (e) { return; } } /** * It removes the item from session storage with the given key * @param {string} key - The key to store the value under. */ remove(key) { window.sessionStorage.removeItem(getKeyWithPrefix(key, this.prefix)); } /** * It returns an array of all the keys in the session storage that start with the prefix * @returns An array of all the keys in the sessionStorage that start with the prefix. */ allKeys() { return Object.keys(window.sessionStorage) .filter((key) => key.startsWith(this.prefix)) .map((key) => removePrefixFromKey(key, this.prefix)); } } /** * Wraps a cache implementation and adds expiration logic * */ class CacheManager { constructor(cache, cacheTime = DEFAULT_CACHE_LIFE, nowProvider) { this.cache = cache; this.cacheTime = cacheTime; this.nowProvider = nowProvider || DEFAULT_NOW_PROVIDER; } /** * It gets a cache entry from the cache, and if it's expired, it removes it from the cache * @param cacheKey - CacheKey<TExtended> * @returns A promise that resolves to a cache entry or undefined. */ get(cacheKey) { return tslib.__awaiter(this, void 0, void 0, function* () { const wrappedEntry = yield this.cache.get(cacheKey.toKey()); if (!wrappedEntry) { return; } const now = yield this.nowProvider(); const nowSeconds = Math.floor(now / 1000); if (wrappedEntry.expiresAt < nowSeconds) { yield this.cache.remove(cacheKey.toKey()); return; } return wrappedEntry.body; }); } /** * It takes a cache key and a cache entry, wraps the cache entry, and then sets the wrapped cache entry in the cache * @param cacheKey - CacheKey<TExtended> * @param {CacheEntry} entry - The cache entry to be stored. */ set(cacheKey, entry) { return tslib.__awaiter(this, void 0, void 0, function* () { const wrappedEntry = yield this.wrapCacheEntry(entry); yield this.cache.set(cacheKey.toKey(), wrappedEntry); }); } /** * It gets all the keys in the cache, and then removes them all */ clearCache() { return tslib.__awaiter(this, void 0, void 0, function* () { const keys = yield this.cache.allKeys(); yield Promise.all(keys.map((key) => this.cache.remove(key))); }); } wrapCacheEntry(entry) { return tslib.__awaiter(this, void 0, void 0, function* () { const now = yield this.nowProvider(); const expiresInTime = Math.floor(now / 1000) + this.cacheTime; return { body: entry, expiresAt: expiresInTime, }; }); } } /** * Implementation of caching that uses in-memory storage * */ class InMemoryCache { constructor() { this.enclosedCache = (function () { const cache = {}; return { set(key, entry) { cache[key] = entry; }, get(key) { const cacheEntry = cache[key]; if (!cacheEntry) { return; } return cacheEntry; }, remove(key) { delete cache[key]; }, allKeys() { return Object.keys(cache); }, }; })(); } } /** * Implementation of stub cache that is used when cache is disabled by user * */ class CacheStub { set() { } get() { return undefined; } remove() { } allKeys() { return []; } } exports.CacheLocation = void 0; (function (CacheLocation) { CacheLocation["Memory"] = "memory"; CacheLocation["LocalStorage"] = "localstorage"; CacheLocation["SessionStorage"] = "sessionstorage"; CacheLocation["NoCache"] = "nocache"; })(exports.CacheLocation || (exports.CacheLocation = {})); var version = "1.3.3"; const cacheLocationBuilders = { [exports.CacheLocation.Memory]: () => new InMemoryCache().enclosedCache, [exports.CacheLocation.LocalStorage]: (prefix) => new LocalStorageCache(prefix), [exports.CacheLocation.SessionStorage]: (prefix) => new SessionStorageCache(prefix), [exports.CacheLocation.NoCache]: () => new CacheStub(), }; const doesBrowserSupportCacheLocation = (cacheLocation) => { switch (cacheLocation) { case exports.CacheLocation.SessionStorage: try { window.sessionStorage.getItem('item'); } catch (e) { return false; } return true; case exports.CacheLocation.LocalStorage: try { window.localStorage.getItem('item'); } catch (e) { return false; } return true; default: return true; } }; const cacheFactory = (location) => { return cacheLocationBuilders[location]; }; /** * FingerprintJS SDK for Single Page Applications */ class FpjsClient { constructor(options) { var _a; this.inFlightRequests = new Map(); this.agentPromise = null; this.customAgent = options === null || options === void 0 ? void 0 : options.customAgent; this.agent = { collect: () => { throw new Error("FPJSAgent hasn't loaded yet. Make sure to call the init() method first."); }, get: () => { throw new Error("FPJSAgent hasn't loaded yet. Make sure to call the init() method first."); }, }; this.loadOptions = options === null || options === void 0 ? void 0 : options.loadOptions; if ((options === null || options === void 0 ? void 0 : options.cache) && (options === null || options === void 0 ? void 0 : options.cacheLocation)) { console.warn('Both `cache` and `cacheLocation` options have been specified in the FpjsClient configuration; ignoring `cacheLocation` and using `cache`.'); } let cache; if (options === null || options === void 0 ? void 0 : options.cache) { cache = options.cache; } else { this.cacheLocation = (options === null || options === void 0 ? void 0 : options.cacheLocation) || exports.CacheLocation.SessionStorage; if (!cacheFactory(this.cacheLocation)) { throw new Error(`Invalid cache location "${this.cacheLocation}"`); } if (!doesBrowserSupportCacheLocation(this.cacheLocation)) { this.cacheLocation = exports.CacheLocation.Memory; } cache = cacheFactory(this.cacheLocation)(options === null || options === void 0 ? void 0 : options.cachePrefix); } if ((options === null || options === void 0 ? void 0 : options.cacheTimeInSeconds) && options.cacheTimeInSeconds > MAX_CACHE_LIFE) { throw new Error(`Cache time cannot exceed 86400 seconds (24 hours)`); } const cacheTime = (_a = options === null || options === void 0 ? void 0 : options.cacheTimeInSeconds) !== null && _a !== void 0 ? _a : DEFAULT_CACHE_LIFE; this.cacheManager = new CacheManager(cache, cacheTime); } /** * Loads FPJS JS agent with certain settings and stores the instance in memory * [https://dev.fingerprint.com/docs/js-agent#agent-initialization] * * @param passedLoadOptions Additional load options to be passed to the agent, they will be merged with load options provided in the constructor. */ init(passedLoadOptions) { return tslib.__awaiter(this, void 0, void 0, function* () { var _a, _b, _c; if (!this.loadOptions && !passedLoadOptions) { throw new TypeError('No load options provided'); } const integrationInfo = // @ts-ignore ((_a = this.loadOptions) === null || _a === void 0 ? void 0 : _a.silent) === true ? [] : [ ...(((_b = this.loadOptions) === null || _b === void 0 ? void 0 : _b.integrationInfo) || []), ...((passedLoadOptions === null || passedLoadOptions === void 0 ? void 0 : passedLoadOptions.integrationInfo) || []), `spa-sdk/${version}`, ]; const loadOptions = Object.assign(Object.assign(Object.assign({}, this.loadOptions), passedLoadOptions), { integrationInfo }); if (!this.agentPromise) { const agentLoader = (_c = this.customAgent) !== null && _c !== void 0 ? _c : FingerprintJSPro__namespace; this.agentPromise = agentLoader .load(loadOptions) .then((agent) => { this.agent = agent; return agent; }) .catch((error) => { this.agentPromise = null; throw error; }); } return this.agentPromise; }); } /** * Returns visitor identification data based on the request options * [https://dev.fingerprint.com/docs/js-agent#visitor-identification] * * @param options * @param ignoreCache if set to true a request to the API will be made even if the data is present in cache */ getVisitorData() { return tslib.__awaiter(this, arguments, void 0, function* (options = {}, ignoreCache = false) { const cacheKey = FpjsClient.makeCacheKey(options); const key = cacheKey.toKey(); if (!this.inFlightRequests.has(key)) { const promise = this._identify(options, ignoreCache).finally(() => { this.inFlightRequests.delete(key); }); this.inFlightRequests.set(key, promise); } return (yield this.inFlightRequests.get(key)); }); } /** * Returns cached visitor data based on the request options, or undefined if the data is not present in cache * */ getVisitorDataFromCache() { return tslib.__awaiter(this, arguments, void 0, function* (options = {}) { const cacheKey = FpjsClient.makeCacheKey(options); const cacheResult = yield this.cacheManager.get(cacheKey); return cacheResult ? Object.assign(Object.assign({}, cacheResult), { cacheHit: true }) : undefined; }); } /** * Checks if request matching given options is present in cache * */ isInCache() { return tslib.__awaiter(this, arguments, void 0, function* (options = {}) { return Boolean(yield this.getVisitorDataFromCache(options)); }); } /** * Clears visitor data from cache regardless of the cache implementation */ clearCache() { return tslib.__awaiter(this, void 0, void 0, function* () { yield this.cacheManager.clearCache(); }); } /** * Makes a CacheKey object from GetOptions */ static makeCacheKey(options) { return new CacheKey(options); } _identify(options_1) { return tslib.__awaiter(this, arguments, void 0, function* (options, ignoreCache = false) { const key = FpjsClient.makeCacheKey(options); if (!ignoreCache) { const cacheResult = yield this.cacheManager.get(key); if (cacheResult) { return Object.assign(Object.assign({}, cacheResult), { cacheHit: true }); } } const agentResult = (yield this.agent.get(options)); yield this.cacheManager.set(key, agentResult); return Object.assign(Object.assign({}, agentResult), { cacheHit: false }); }); } } /** * @deprecated * * Use `FingerprintJSPro.defaultEndpoint` instead, this export will be removed in the next major version * */ const defaultEndpoint = FingerprintJSPro__namespace.defaultEndpoint; /** * @deprecated * * Use `FingerprintJSPro.defaultTlsEndpoint` instead, this export will be removed in the next major version */ const defaultTlsEndpoint = FingerprintJSPro__namespace.defaultTlsEndpoint; /** * @deprecated * * Use `FingerprintJSPro.defaultScriptUrlPattern` instead, this export will be removed in the next major version */ const defaultScriptUrlPattern = FingerprintJSPro__namespace.defaultScriptUrlPattern; exports.FingerprintJSPro = FingerprintJSPro__namespace; exports.CacheStub = CacheStub; exports.FpjsClient = FpjsClient; exports.InMemoryCache = InMemoryCache; exports.LocalStorageCache = LocalStorageCache; exports.SessionStorageCache = SessionStorageCache; exports.defaultEndpoint = defaultEndpoint; exports.defaultScriptUrlPattern = defaultScriptUrlPattern; exports.defaultTlsEndpoint = defaultTlsEndpoint;