@fingerprintjs/fingerprintjs-pro-spa
Version:
FingerprintJS Pro JavaScript agent for Single-Page Applications (SPA)
458 lines (442 loc) • 16.8 kB
JavaScript
/**
* 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;