var Connection; (function (Connection) { Connection["Wifi"] = "wifi"; Connection["Cellular"] = "cellular"; Connection["Offline"] = "offline"; })(Connection || (Connection = {})); var ContentType; (function (ContentType) { ContentType["Dice"] = "Dice"; ContentType["Source"] = "Source"; ContentType["Compendium"] = "Compendium"; ContentType["Single"] = "Single"; ContentType["Bundle"] = "Bundle"; })(ContentType || (ContentType = {})); var Device; (function (Device) { Device["Tablet"] = "tablet"; Device["Smartphone"] = "smartphone"; Device["Unknown"] = "unknown"; })(Device || (Device = {})); var Orientation; (function (Orientation) { Orientation["Portrait"] = "portrait"; Orientation["Landscape"] = "landscape"; Orientation["Unknown"] = "unknown"; })(Orientation || (Orientation = {})); var Platform; (function (Platform) { Platform["Sigil"] = "Sigil"; Platform["Web"] = "Web"; Platform["MobileAndroid"] = "MobileAndroid"; Platform["MobileIos"] = "MobileIOS"; Platform["MobileWeb"] = "MobileWeb"; })(Platform || (Platform = {})); var Region; (function (Region) { Region["UsEast1"] = "us-east-1"; Region["UsEast2"] = "us-east-2"; })(Region || (Region = {})); var Source; (function (Source) { Source["Web"] = "ddb.web"; Source["Mobile"] = "ddb.mobile"; })(Source || (Source = {})); var SubContentType; (function (SubContentType) { SubContentType["Family"] = "Family"; SubContentType["Set"] = "Set"; SubContentType["Rule"] = "Rule"; SubContentType["Adventure"] = "Adventure"; SubContentType["Map"] = "Map"; SubContentType["Legendary"] = "Legendary"; SubContentType["Source"] = "Source"; SubContentType["MagicItem"] = "MagicItem"; SubContentType["Monster"] = "Monster"; SubContentType["Feat"] = "Feat"; SubContentType["Background"] = "Background"; SubContentType["Subrace"] = "Subrace"; SubContentType["Class"] = "Class"; SubContentType["Subclass"] = "Subclass"; SubContentType["Spell"] = "Spell"; SubContentType["Vehicle"] = "Vehicle"; SubContentType["Race"] = "Race"; })(SubContentType || (SubContentType = {})); var SubscriptionLevel; (function (SubscriptionLevel) { SubscriptionLevel["Visitor"] = "Visitor"; SubscriptionLevel["RegisteredUser"] = "Registered User"; SubscriptionLevel["Hero"] = "Hero"; SubscriptionLevel["Master"] = "Master"; SubscriptionLevel["Legendary"] = "Legendary"; })(SubscriptionLevel || (SubscriptionLevel = {})); const byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 0x100).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + '-' + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + '-' + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + '-' + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + '-' + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } let getRandomValues; const rnds8 = new Uint8Array(16); function rng() { if (!getRandomValues) { if (typeof crypto === 'undefined' || !crypto.getRandomValues) { throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported'); } getRandomValues = crypto.getRandomValues.bind(crypto); } return getRandomValues(rnds8); } const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto); var native = { randomUUID }; function _v4(options, buf, offset) { options = options || {}; const rnds = options.random ?? options.rng?.() ?? rng(); if (rnds.length < 16) { throw new Error('Random bytes length must be >= 16'); } rnds[6] = (rnds[6] & 0x0f) | 0x40; rnds[8] = (rnds[8] & 0x3f) | 0x80; return unsafeStringify(rnds); } function v4(options, buf, offset) { if (native.randomUUID && true && !options) { return native.randomUUID(); } return _v4(options); } function flattenPlugins(event) { const newObj = {}; Object.keys(event).forEach((key) => { if (key === "plugins" && Array.isArray(event[key])) { event[key].forEach((plugin) => { Object.keys(plugin).forEach((pluginKey) => { if (newObj[pluginKey]) { throw new Error(`duplicate key ${pluginKey} attempting to be set!`); } else { newObj[pluginKey] = plugin[pluginKey]; } }); }); } else { newObj[key] = event[key]; } }); return newObj; } // Regexps involved with splitting words in various case formats. const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu; const SPLIT_UPPER_UPPER_RE = /(\p{Lu})([\p{Lu}][\p{Ll}])/gu; // Used to iterate over the initial split result and separate numbers. const SPLIT_SEPARATE_NUMBER_RE = /(\d)\p{Ll}|(\p{L})\d/u; // Regexp involved with stripping non-word characters from the result. const DEFAULT_STRIP_REGEXP = /[^\p{L}\d]+/giu; // The replacement value for splits. const SPLIT_REPLACE_VALUE = "$1\0$2"; // The default characters to keep after transforming case. const DEFAULT_PREFIX_SUFFIX_CHARACTERS = ""; /** * Split any cased input strings into an array of words. */ function split(value) { let result = value.trim(); result = result .replace(SPLIT_LOWER_UPPER_RE, SPLIT_REPLACE_VALUE) .replace(SPLIT_UPPER_UPPER_RE, SPLIT_REPLACE_VALUE); result = result.replace(DEFAULT_STRIP_REGEXP, "\0"); let start = 0; let end = result.length; // Trim the delimiter from around the output string. while (result.charAt(start) === "\0") start++; if (start === end) return []; while (result.charAt(end - 1) === "\0") end--; return result.slice(start, end).split(/\0/g); } /** * Split the input string into an array of words, separating numbers. */ function splitSeparateNumbers(value) { const words = split(value); for (let i = 0; i < words.length; i++) { const word = words[i]; const match = SPLIT_SEPARATE_NUMBER_RE.exec(word); if (match) { const offset = match.index + (match[1] ?? match[2]).length; words.splice(i, 1, word.slice(0, offset), word.slice(offset)); } } return words; } /** * Convert a string to constant case (`FOO_BAR`). */ function constantCase(input, options) { const [prefix, words, suffix] = splitPrefixSuffix(input, options); return (prefix + words.map(upperFactory(options?.locale)).join("_") + suffix); } function upperFactory(locale) { return locale === false ? (input) => input.toUpperCase() : (input) => input.toLocaleUpperCase(locale); } function splitPrefixSuffix(input, options = {}) { const splitFn = options.split ?? (options.separateNumbers ? splitSeparateNumbers : split); const prefixCharacters = options.prefixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; const suffixCharacters = options.suffixCharacters ?? DEFAULT_PREFIX_SUFFIX_CHARACTERS; let prefixIndex = 0; let suffixIndex = input.length; while (prefixIndex < input.length) { const char = input.charAt(prefixIndex); if (!prefixCharacters.includes(char)) break; prefixIndex++; } while (suffixIndex > prefixIndex) { const index = suffixIndex - 1; const char = input.charAt(index); if (!suffixCharacters.includes(char)) break; suffixIndex = index; } return [ input.slice(0, prefixIndex), splitFn(input.slice(prefixIndex, suffixIndex)), input.slice(suffixIndex), ]; } function transformKeys(object, transformedObject = {}) { Object.entries(object).forEach(([key, value]) => { if (Array.isArray(value)) { value = object[key].map((item) => typeof item === "object" ? transformKeys(item) : item); } else if (typeof value === "object") { value = value ? transformKeys(value) : value; } transformedObject[constantCase(key)] = value ?? null; }); return transformedObject; } const createEventEntry = ({ detail, detailType, eventBusName, source, }) => { const eventTime = new Date(); const fullDetail = { ...detail, messageId: v4(), eventTime: eventTime.toISOString(), }; const flattenedDetail = flattenPlugins(fullDetail); const transformedDetail = transformKeys(flattenedDetail); return { Detail: JSON.stringify(transformedDetail), DetailType: detailType, EventBusName: eventBusName, Source: source, Time: eventTime, }; }; const visitorId = "ddb_vid"; const sessionId = "ddb_sid"; /** * Checks if the environment is a test environment. * This is useful to avoid setting secure cookies in test environments * where the `window` object may not be available or secure cookies are not needed. */ const isTestEnv = () => { return (typeof process !== "undefined" && process.env && (process.env.NODE_ENV === "test" || process.env.VITEST)); }; const getCookie = (name) => document.cookie.match(`${name}=([^;]*)`)?.[1]; const setCookie = (name, value, opts) => { const parts = [`${name}=${value}`, `max-age=${opts["max-age"]}`]; if (opts.secure && !isTestEnv()) { parts.push("secure", "domain=.dndbeyond.com", "path=/"); } return (document.cookie = parts.join(";")); }; const deleteCookie = (name) => setCookie(name, "", { "max-age": 0, secure: !isTestEnv() }); const createSessionAndVisitorCookies = () => { if (!getCookie(visitorId)) { setCookie(visitorId, v4(), { "max-age": 60 * 60 * 24 * 365, // 1 year secure: !isTestEnv(), }); } if (!getCookie(sessionId)) { setCookie(sessionId, v4(), { "max-age": 60 * 60 * 4, // 4 hours secure: !isTestEnv(), }); } else { extendSessionCookie(); } }; const initializeConsentCookieListener = (consentCallback) => { const maxRetries = 10; const retryInterval = 500; let retries = 0; let consentCallbackFulfilled = false; const tryInitialize = () => { if ("window" in globalThis && window.ketch) { /** * n.b. This callback will return immediately if the response is already available, * so this acts as both an initialization and a listener for future consent changes. */ window.ketch("on", "consent", (consentData) => { if (!consentCallbackFulfilled) { consentCallbackFulfilled = true; consentCallback?.(consentData.purposes || null); } if (!consentData.purposes.analytics) { deleteCookie(visitorId); deleteCookie(sessionId); } else { createSessionAndVisitorCookies(); } }); // For when the Ketch response never comes, we attempt a final callback setTimeout(() => { if (!consentCallbackFulfilled) { console.warn("Ketch callback never fired, invoking fallback."); consentCallbackFulfilled = true; consentCallback?.(window.ketchConsent || null); } }, 5_000); } else if (retries < maxRetries) { retries++; setTimeout(tryInitialize, retryInterval); } else { console.warn("Failed to initialize session cookies."); consentCallbackFulfilled = true; consentCallback?.(null); } }; tryInitialize(); }; const extendSessionCookie = () => { const session = getCookie(sessionId); if (session) { setCookie(sessionId, session, { "max-age": 60 * 60 * 4, // 4 hours secure: !isTestEnv(), }); } }; class EventPipeline { region; /** * Initializes the EventPipeline with the specified region and optional consent callback. * The consent callback will return with valid consent data or null if Ketch is not available. * * @param region - The region for the event pipeline. * @param consentCallback - Optional callback to handle Ketch consent data. */ constructor(region, consentCallback) { this.region = region; try { initializeConsentCookieListener(consentCallback); } catch (error) { console.warn("Failed to initialize session cookies", error); } } async sendEvent(detail, source) { try { try { extendSessionCookie(); } catch (error) { console.warn("Failed to extend session cookie", error); } const entry = createEventEntry({ detail, source, }); const response = await fetch(`https://${this.region === Region.UsEast2 ? "test-" : ""}api.dndbeyond.com/bi/events`, { body: JSON.stringify({ Entries: [entry], }), headers: { "Content-Type": "application/json", }, method: "POST", }); const result = { success: response.ok, ...(response.ok ? {} : { errorMessage: `Send to event pipeline failed (client-bi-event)`, }), }; response.ok || console.error(result.errorMessage); return result; } catch (error) { console.error(error); return { success: false, errorMessages: [error.message], }; } } } export { EventPipeline };