UNPKG

lever-ui-logger

Version:

Zero-dependency logging library with optional EventBus integration. Built-in PII redaction, multiple transports, and comprehensive logging capabilities.

1,613 lines (1,606 loc) 75.7 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/transports/index.ts var transports_exports = {}; __export(transports_exports, { BaseTransport: () => BaseTransport, BrowserStyles: () => BrowserStyles, Colors: () => Colors, ConsoleTransport: () => ConsoleTransport, Environment: () => Environment, ErrorMessageSanitizer: () => ErrorMessageSanitizer, EventBusTransport: () => EventBusTransport, Formatters: () => Formatters, SecureTokenHandler: () => SecureTokenHandler, SendBeaconTransport: () => SendBeaconTransport, TRANSPORTS_VERSION: () => TRANSPORTS_VERSION, TransportMiddleware: () => TransportMiddleware, addMetadata: () => addMetadata, batchCompressionMiddleware: () => batchCompressionMiddleware, compressString: () => compressString, compressionMiddleware: () => compressionMiddleware, createConsoleTransport: () => createConsoleTransport, createEventBusTransport: () => createEventBusTransport, createSendBeaconTransport: () => createSendBeaconTransport, defaultErrorSanitizer: () => defaultErrorSanitizer, defaultSecureTokenHandler: () => defaultSecureTokenHandler, enrichErrors: () => enrichErrors, filterByLevel: () => filterByLevel, isCompressionSupported: () => isCompressionSupported, rateLimit: () => rateLimit, sample: () => sample, transformEvent: () => transformEvent }); module.exports = __toCommonJS(transports_exports); // src/transports/transport-interface.ts var Environment = { /** Check if running in browser environment */ isBrowser: typeof window !== "undefined" && typeof window.document !== "undefined", /** Check if running in Node.js environment */ isNode: typeof process !== "undefined" && !!process?.versions?.node, /** Check if running in production environment */ isProduction: typeof process !== "undefined" && process?.env?.NODE_ENV === "production", /** Check if console methods support styling */ supportsConsoleStyles: typeof window !== "undefined" || typeof process !== "undefined" && !!process?.stdout?.isTTY }; var BaseTransport = class { constructor(name, config = {}) { this.name = name; this.config = config; } /** * Flush any pending logs (default: no-op) */ flush() { } /** * Close the transport and clean up resources (default: no-op) */ close() { } /** * Check if transport should be active in current environment */ isEnabled() { return true; } /** * Measure performance of a function call */ measurePerformance(fn, threshold = 1) { const start = performance.now(); const result = fn(); const duration = performance.now() - start; if (duration > threshold && !Environment.isProduction) { console.warn(`Transport "${this.name}" took ${duration.toFixed(2)}ms (threshold: ${threshold}ms)`); } return result; } }; var Formatters = { /** * Format timestamp with configurable format */ timestamp(timestamp, format = "HH:mm:ss.SSS") { const date = new Date(timestamp); if (format === "iso") { return date.toISOString(); } if (format === "HH:mm:ss.SSS") { return date.toISOString().slice(11, 23); } if (format === "HH:mm:ss") { return date.toISOString().slice(11, 19); } return date.toISOString().slice(11, 23); }, /** * Pretty-print objects with indentation */ prettyObject(obj, indent = 2) { try { return JSON.stringify(obj, null, indent); } catch { return String(obj); } }, /** * Compact object representation */ compactObject(obj) { try { return JSON.stringify(obj); } catch { return String(obj); } }, /** * Get log level priority for comparison */ getLogLevelPriority(level) { const priorities = { trace: 0, debug: 1, info: 2, warn: 3, error: 4 }; return priorities[level] ?? 2; } }; var Colors = { // Log level colors trace: "\x1B[36m", // Cyan debug: "\x1B[34m", // Blue info: "\x1B[32m", // Green warn: "\x1B[33m", // Yellow error: "\x1B[31m", // Red // Style codes reset: "\x1B[0m", // Reset bold: "\x1B[1m", // Bold dim: "\x1B[2m", // Dim // Component/context colors component: "\x1B[35m", // Magenta timestamp: "\x1B[90m" // Bright Black (Gray) }; var BrowserStyles = { trace: "color: #00bcd4; font-weight: normal;", debug: "color: #2196f3; font-weight: normal;", info: "color: #4caf50; font-weight: normal;", warn: "color: #ff9800; font-weight: bold;", error: "color: #f44336; font-weight: bold;", component: "color: #9c27b0; font-weight: bold;", timestamp: "color: #666; font-weight: normal;" }; // src/transports/console-transport.ts var ConsoleTransport = class extends BaseTransport { constructor(config = {}) { const mergedConfig = { name: "console", format: "pretty", colors: true, timestamps: true, timestampFormat: "HH:mm:ss.SSS", enableInProduction: false, performanceThreshold: 0.1, // 0.1ms threshold for console output consoleMethods: {}, ...config }; super(mergedConfig.name, mergedConfig); this.transportConfig = mergedConfig; this.consoleMethods = this.initializeConsoleMethods(); } /** * Write a log event to the console */ write(event) { if (!event) { throw new TypeError("LogEventData is required"); } if (!event.message || typeof event.message !== "string") { throw new TypeError("LogEventData must have a valid message"); } if (!this.isEnabled()) { return; } this.measurePerformance(() => { const formatted = this.formatEvent(event); const consoleMethod = this.consoleMethods[event.level]; if (this.transportConfig.colors && Environment.isBrowser) { this.writeWithBrowserStyles(formatted, consoleMethod); } else if (this.transportConfig.colors && Environment.supportsConsoleStyles) { this.writeWithAnsiColors(formatted, consoleMethod); } else { this.writeWithoutColors(formatted, consoleMethod); } }, this.transportConfig.performanceThreshold); } /** * Check if transport should be active in current environment */ isEnabled() { if (Environment.isProduction && !this.transportConfig.enableInProduction) { return false; } return typeof console !== "undefined"; } /** * Initialize console method mapping */ initializeConsoleMethods() { if (typeof console === "undefined") { const noop = () => { }; return { trace: noop, debug: noop, info: noop, warn: noop, error: noop }; } const defaultMethods = { trace: console.trace?.bind(console) || console.log?.bind(console) || (() => { }), debug: console.debug?.bind(console) || console.log?.bind(console) || (() => { }), info: console.info?.bind(console) || console.log?.bind(console) || (() => { }), warn: console.warn?.bind(console) || console.log?.bind(console) || (() => { }), error: console.error?.bind(console) || console.log?.bind(console) || (() => { }) }; const customMethods = this.transportConfig.consoleMethods; if (customMethods) { for (const [level, methodName] of Object.entries(customMethods)) { const method = console[methodName]; if (typeof method === "function") { defaultMethods[level] = method.bind(console); } } } return defaultMethods; } /** * Format a log event according to the configured format */ formatEvent(event) { const timestamp = this.transportConfig.timestamps ? Formatters.timestamp(event.timestamp, this.transportConfig.timestampFormat) : ""; const component = event.component ? `[${event.component}]` : ""; const level = event.level.toUpperCase().padEnd(5); let contextStr = ""; let argsStr = ""; if (Object.keys(event.context).length > 0) { contextStr = this.formatData(event.context, this.transportConfig.format); } if (event.args.length > 0) { argsStr = event.args.map((arg) => this.formatData(arg, this.transportConfig.format)).join(" "); } return { timestamp, level, component, message: event.message, context: contextStr, args: argsStr, raw: event }; } /** * Format data based on the configured format mode */ formatData(data, format) { switch (format) { case "json": return Formatters.compactObject(data); case "pretty": if (typeof data === "object" && data !== null) { return Formatters.prettyObject(data); } return String(data); case "compact": return typeof data === "object" && data !== null ? Formatters.compactObject(data) : String(data); default: return String(data); } } /** * Write to console with browser CSS styles */ writeWithBrowserStyles(formatted, consoleMethod) { const parts = []; const styles = []; if (formatted.timestamp) { parts.push(`%c${formatted.timestamp}`); styles.push(BrowserStyles.timestamp); } parts.push(`%c${formatted.level}`); styles.push(BrowserStyles[formatted.raw.level] || ""); if (formatted.component) { parts.push(`%c${formatted.component}`); styles.push(BrowserStyles.component); } parts.push(`%c${formatted.message}`); styles.push(""); const message = parts.join(" "); const logArgs = [message, ...styles]; if (formatted.context) { if (this.transportConfig.format === "pretty") { logArgs.push("\nContext:", formatted.context); } else { logArgs.push("Context:", formatted.context); } } if (formatted.args) { if (this.transportConfig.format === "pretty") { logArgs.push("\nArgs:", formatted.args); } else { logArgs.push("Args:", formatted.args); } } consoleMethod(...logArgs); } /** * Write to console with ANSI colors */ writeWithAnsiColors(formatted, consoleMethod) { const parts = []; if (formatted.timestamp) { parts.push(`${Colors.timestamp}${formatted.timestamp}${Colors.reset}`); } const levelColor = Colors[formatted.raw.level] || ""; parts.push(`${levelColor}${formatted.level}${Colors.reset}`); if (formatted.component) { parts.push(`${Colors.component}${formatted.component}${Colors.reset}`); } parts.push(formatted.message); let output = parts.join(" "); if (formatted.context) { output += this.transportConfig.format === "pretty" ? ` ${Colors.dim}Context:${Colors.reset} ${formatted.context}` : ` ${Colors.dim}Context:${Colors.reset} ${formatted.context}`; } if (formatted.args) { output += this.transportConfig.format === "pretty" ? ` ${Colors.dim}Args:${Colors.reset} ${formatted.args}` : ` ${Colors.dim}Args:${Colors.reset} ${formatted.args}`; } consoleMethod(output); } /** * Write to console without colors */ writeWithoutColors(formatted, consoleMethod) { const parts = []; if (formatted.timestamp) { parts.push(formatted.timestamp); } parts.push(formatted.level); if (formatted.component) { parts.push(formatted.component); } parts.push(formatted.message); let output = parts.join(" "); if (formatted.context) { output += this.transportConfig.format === "pretty" ? ` Context: ${formatted.context}` : ` Context: ${formatted.context}`; } if (formatted.args) { output += this.transportConfig.format === "pretty" ? ` Args: ${formatted.args}` : ` Args: ${formatted.args}`; } consoleMethod(output); } }; function createConsoleTransport(config) { return new ConsoleTransport(config); } // src/transports/error-sanitizer.ts var ErrorMessageSanitizer = class { constructor(config = {}) { this.config = { enableTokenDetection: config.enableTokenDetection ?? true, replacementStrategy: config.replacementStrategy ?? "mask", customPatterns: config.customPatterns ?? [], sensitiveFields: config.sensitiveFields ?? [ "token", "auth", "authorization", "bearer", "password", "secret", "key", "apikey", "api_key", "access_token", "refresh_token", "client_secret", "private_key", "credential", "session" ], maskRevealLength: config.maskRevealLength ?? 8 }; this.tokenPatterns = this.createTokenPatterns(); this.sensitiveFieldQuotedPattern = new RegExp( `(${this.config.sensitiveFields.join("|")})\\s*[=:]\\s*(['"])([^'"]+)\\2`, "gi" ); this.sensitiveFieldUnquotedPattern = new RegExp( `(${this.config.sensitiveFields.join("|")})\\s*[=:]\\s*([^\\s,}\\]\\)'"]+)`, "gi" ); } /** * Sanitize an error message or any string containing potentially sensitive data * * @param input - The string to sanitize * @returns Sanitized string with sensitive data masked/redacted */ sanitize(input) { if (!input || typeof input !== "string") { return String(input); } let sanitized = input; try { if (this.config.enableTokenDetection) { sanitized = this.sanitizeTokens(sanitized); } sanitized = this.sanitizeSensitiveFields(sanitized); sanitized = this.applyCustomPatterns(sanitized); return sanitized; } catch { return "[Error message sanitization failed - content redacted for security]"; } } /** * Sanitize sensitive key-value pairs in strings */ sanitizeSensitiveFields(input) { let sanitized = input; sanitized = sanitized.replace(this.sensitiveFieldQuotedPattern, (match, field, quote, value) => { const sanitizedValue = this.applySanitization(value); return `${field}=${quote}${sanitizedValue}${quote}`; }); sanitized = sanitized.replace(this.sensitiveFieldUnquotedPattern, (match, field, value) => { const sanitizedValue = this.applySanitization(value); return match.replace(value, sanitizedValue); }); return sanitized; } /** * Detect and sanitize various token patterns */ sanitizeTokens(input) { let sanitized = input; for (const [patternName, pattern] of Object.entries(this.tokenPatterns)) { if (pattern.global) { pattern.lastIndex = 0; } sanitized = sanitized.replace(pattern, (match) => { return this.applySanitization(match, `[${patternName.toUpperCase()}_TOKEN]`); }); } return sanitized; } /** * Apply custom user-defined patterns */ applyCustomPatterns(input) { let sanitized = input; for (const pattern of this.config.customPatterns) { sanitized = sanitized.replace(pattern, (match) => { return this.applySanitization(match, "[CUSTOM_REDACTED]"); }); } return sanitized; } /** * Apply the configured sanitization strategy to a detected sensitive value */ applySanitization(value, _placeholder) { switch (this.config.replacementStrategy) { case "redact": return "[REDACTED]"; case "hash": return this.hashValue(value); case "mask": default: return this.maskValue(value); } } /** * Mask a value showing first and last few characters */ maskValue(value) { if (value.length <= this.config.maskRevealLength) { return "*".repeat(value.length); } const revealLength = Math.floor(this.config.maskRevealLength / 2); const start = value.substring(0, revealLength); const end = value.substring(value.length - revealLength); const maskLength = Math.min(20, Math.max(4, value.length - revealLength * 2)); return `${start}${"*".repeat(maskLength)}${end}`; } /** * Create a hash of the value for logging purposes */ hashValue(value) { let hash = 0; for (let i = 0; i < value.length; i++) { const char = value.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; } return `[HASH:${Math.abs(hash).toString(16)}]`; } /** * Create comprehensive token detection patterns */ createTokenPatterns() { return { // JWT tokens: header.payload.signature (base64url encoded) jwt: /\b[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g, // Bearer tokens in various formats bearer: /bearer\s+[A-Za-z0-9+/=_-]{6,}/gi, // Generic API key patterns - match just the value after field identifiers genericApiKey: /(?<=(?:api[_-]?key|apikey|key|secret)[\s=:"']+)[A-Za-z0-9+/=_-]{6,}/gi, // URLs with embedded credentials urlWithCredentials: /\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s:]+:[^\s@]+@[^\s/]+/gi, // Basic PII patterns email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, phone: /\b(?:\+?1[-.)\s]?)?(?:\([0-9]{3}\)|[0-9]{3})[-.)\s]?[0-9]{3}[-.)\s]?[0-9]{4}\b/g, creditCard: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/g }; } /** * Check if a string contains any detectable sensitive data * * @param input - String to check * @returns True if sensitive data is detected */ hasSensitiveData(input) { if (!input || typeof input !== "string") { return false; } this.sensitiveFieldQuotedPattern.lastIndex = 0; this.sensitiveFieldUnquotedPattern.lastIndex = 0; if (this.sensitiveFieldQuotedPattern.test(input) || this.sensitiveFieldUnquotedPattern.test(input)) { return true; } if (this.config.enableTokenDetection) { for (const pattern of Object.values(this.tokenPatterns)) { if (pattern.global) { pattern.lastIndex = 0; } if (pattern.test(input)) { return true; } } } for (const pattern of this.config.customPatterns) { if (pattern.global) { pattern.lastIndex = 0; } if (pattern.test(input)) { return true; } } return false; } /** * Get statistics about what types of sensitive data were found * * @param input - String to analyze * @returns Object with counts of different sensitive data types found */ analyzeSensitiveData(input) { const analysis = {}; if (!input || typeof input !== "string") { return analysis; } if (this.config.enableTokenDetection) { for (const [patternName, pattern] of Object.entries(this.tokenPatterns)) { const matches = input.match(pattern); if (matches) { analysis[patternName] = matches.length; } } } const quotedFieldMatches = input.match(this.sensitiveFieldQuotedPattern); const unquotedFieldMatches = input.match(this.sensitiveFieldUnquotedPattern); const totalFieldMatches = (quotedFieldMatches?.length || 0) + (unquotedFieldMatches?.length || 0); if (totalFieldMatches > 0) { analysis.sensitiveFields = totalFieldMatches; } return analysis; } }; var defaultErrorSanitizer = new ErrorMessageSanitizer(); // src/transports/secure-token-handler.ts var SecureTokenHandler = class { constructor(config = {}) { this.tokenEntry = null; this.tokenProvider = null; this.disposed = false; this.obfuscationKey = null; this.config = { enableSecureMode: config.enableSecureMode ?? true, tokenTtl: config.tokenTtl ?? 36e5, // 1 hour default validateToken: config.validateToken ?? true, tokenValidator: config.tokenValidator ?? this.defaultTokenValidator.bind(this) }; this.errorSanitizer = new ErrorMessageSanitizer({ enableTokenDetection: true, replacementStrategy: "mask", maskRevealLength: 8 }); if (this.config.enableSecureMode) { this.makeNonSerializable(); } } /** * Set a static authentication token * * @param token - The authentication token to store securely * @throws Error if token is invalid or handler is disposed */ setToken(token) { this.checkDisposed(); if (token === null) { this.tokenEntry = null; this.tokenProvider = null; return; } if (this.config.validateToken && !this.config.tokenValidator(token)) { throw new Error("Invalid token format"); } const now = Date.now(); const value = this.config.enableSecureMode ? this.obfuscateToken(token) : token; this.tokenEntry = { value, timestamp: now, expiresAt: now + this.config.tokenTtl, source: "static", secure: this.config.enableSecureMode }; this.tokenProvider = null; } /** * Set a token provider function for dynamic token retrieval * * @param provider - Function that returns an authentication token * @throws Error if provider is invalid or handler is disposed */ setTokenProvider(provider) { this.checkDisposed(); this.tokenProvider = provider; this.tokenEntry = null; } /** * Retrieve the current authentication token * * @returns The current token or null if none available * @throws Error if token retrieval fails or handler is disposed */ async getToken() { this.checkDisposed(); if (this.tokenEntry && this.isTokenValid(this.tokenEntry)) { const value = this.tokenEntry.secure ? this.deobfuscateToken(this.tokenEntry.value) : this.tokenEntry.value; return value; } if (this.tokenProvider) { try { const token = await this.tokenProvider(); if (this.config.validateToken && !this.config.tokenValidator(token)) { throw new Error("Token provider returned invalid token"); } const now = Date.now(); const value = this.config.enableSecureMode ? this.obfuscateToken(token) : token; this.tokenEntry = { value, timestamp: now, expiresAt: now + this.config.tokenTtl, source: "function", secure: this.config.enableSecureMode }; return token; } catch (error) { const sanitizedError = this.sanitizeErrorMessage(error instanceof Error ? error.message : String(error)); console.error("SecureTokenHandler: Token provider failed:", sanitizedError); return null; } } return null; } /** * Check if a token is currently available and valid */ hasToken() { if (this.disposed) return false; if (this.tokenEntry && this.isTokenValid(this.tokenEntry)) { return true; } return this.tokenProvider !== null; } /** * Clear all token data and dispose resources * * This method should be called when the handler is no longer needed * to ensure tokens are properly cleared from memory. */ dispose() { if (this.disposed) return; if (this.tokenEntry) { if (typeof this.tokenEntry.value === "string") { this.tokenEntry.value = this.generateRandomString(this.tokenEntry.value.length); } } this.tokenEntry = null; this.tokenProvider = null; if (this.obfuscationKey) { this.obfuscationKey.fill(0); this.obfuscationKey = null; } this.disposed = true; if (this.config.enableSecureMode) { this.removeNonSerializable(); } } /** * Create a secure JSON replacer function that sanitizes sensitive data */ createSecureReplacer() { const seen = /* @__PURE__ */ new WeakSet(); return (_key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return "[Circular Reference]"; } seen.add(value); } if (typeof value === "string") { const keyValuePair = `${_key}=${value}`; const isSensitiveKey = this.errorSanitizer.hasSensitiveData(keyValuePair); if (isSensitiveKey) { return this.forceSanitizeValue(value); } if (this.errorSanitizer.hasSensitiveData(value)) { return this.errorSanitizer.sanitize(value); } } return value; }; } /** * Sanitize HTTP headers to remove or mask authentication tokens * * @param headers - Headers object to sanitize * @returns New headers object with sensitive values masked */ sanitizeHeaders(headers) { const sanitized = {}; const sensitiveHeaderNames = [ "authorization", "auth", "x-api-key", "x-auth-token", "x-access-token", "bearer", "token", "api-key", "apikey", "secret", "credential" ]; for (const [key, value] of Object.entries(headers)) { const lowerKey = key.toLowerCase(); if (sensitiveHeaderNames.includes(lowerKey)) { sanitized[key] = this.maskToken(value); } else { sanitized[key] = value; } } return sanitized; } /** * Mask a token showing first and last few characters * * @param token - Token to mask * @returns Masked token */ maskToken(token) { if (!token || typeof token !== "string") { return token; } if (token.length <= 8) { return "*".repeat(token.length); } const revealLength = 4; const start = token.substring(0, revealLength); const end = token.substring(token.length - revealLength); const maskLength = Math.min(20, Math.max(4, token.length - revealLength * 2)); return `${start}${"*".repeat(maskLength)}${end}`; } /** * Force sanitization of a value using masking strategy */ forceSanitizeValue(value) { if (!value || value.length <= 8) { return "*".repeat(value.length); } const revealLength = Math.floor(4 / 2); const start = value.substring(0, revealLength); const end = value.substring(value.length - revealLength); const maskLength = Math.min(20, Math.max(4, value.length - revealLength * 2)); return `${start}${"*".repeat(maskLength)}${end}`; } /** * Default token validator */ defaultTokenValidator(token) { if (!token || typeof token !== "string") return false; if (token.length < 8) return false; if (token.includes(" ") && !token.startsWith("Bearer ")) return false; return true; } /** * Check if handler is disposed */ checkDisposed() { if (this.disposed) { throw new Error("SecureTokenHandler has been disposed"); } } /** * Check if a token entry is valid and not expired */ isTokenValid(entry) { return entry.expiresAt > Date.now(); } /** * Multi-round obfuscation to protect tokens in memory using session key. * Uses a combination of XOR, character substitution, and encoding rounds * of transformation to protect tokens in memory against casual inspection * and memory dumps. Not meant for cryptographic security but provides * defense-in-depth against token extraction. */ obfuscateToken(token) { if (!token) return ""; try { const key = this.getObfuscationKey(); let obfuscated = token; obfuscated = this.xorCipher(obfuscated, key); obfuscated = this.substituteChars(obfuscated); obfuscated = this.encodeObfuscated(obfuscated); return obfuscated; } catch { return ""; } } /** * Reverse the multi-round obfuscation process */ deobfuscateToken(obfuscated) { if (!obfuscated) return ""; try { let decoded = this.decodeObfuscated(obfuscated); decoded = this.reverseSubstituteChars(decoded); const key = this.getObfuscationKey(); decoded = this.xorCipher(decoded, key); if (!/^[\x20-\x7E]+$/.test(decoded)) { throw new Error("Deobfuscated data contains non-printable characters"); } return decoded; } catch { return ""; } } /** * XOR cipher implementation */ xorCipher(text, key) { const result = new Array(text.length); for (let i = 0; i < text.length; i++) { result[i] = String.fromCharCode(text.charCodeAt(i) ^ key[i % key.length]); } return result.join(""); } /** * Character substitution using a custom mapping */ substituteChars(text) { const substitutionMap = this.createSubstitutionMap(); return text.split("").map((char) => substitutionMap[char] || char).join(""); } /** * Reverse character substitution */ reverseSubstituteChars(text) { const reverseMap = this.createReverseSubstitutionMap(); return text.split("").map((char) => reverseMap[char] || char).join(""); } /** * Encode the obfuscated string using base64 or similar */ encodeObfuscated(text) { try { return typeof btoa !== "undefined" ? btoa(text) : this.simpleEncode(text); } catch { return this.simpleEncode(text); } } /** * Decode the obfuscated string */ decodeObfuscated(encoded) { try { return typeof atob !== "undefined" ? atob(encoded) : this.simpleDecode(encoded); } catch { return this.simpleDecode(encoded); } } /** * Sanitize error messages to prevent token leakage */ sanitizeErrorMessage(errorMessage) { return this.errorSanitizer.sanitize(errorMessage); } /** * Create character substitution map */ createSubstitutionMap() { const map = {}; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; const shuffled = "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210/+="; for (let i = 0; i < chars.length; i++) { map[chars[i]] = shuffled[i]; } return map; } /** * Create reverse substitution map */ createReverseSubstitutionMap() { const map = {}; const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; const shuffled = "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210/+="; for (let i = 0; i < chars.length; i++) { map[shuffled[i]] = chars[i]; } return map; } /** * Simple encoding fallback for environments without btoa */ simpleEncode(text) { return text.split("").map((char) => String.fromCharCode(char.charCodeAt(0) + 1)).join(""); } /** * Simple decoding fallback for environments without atob */ simpleDecode(encoded) { try { return encoded.split("").map((char) => String.fromCharCode(char.charCodeAt(0) - 1)).join(""); } catch { return ""; } } /** * Generate a random string for token clearing */ generateRandomString(length) { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; for (let i = 0; i < length; i++) { result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } /** * Get or generate the session-specific obfuscation key */ getObfuscationKey() { if (!this.obfuscationKey) { this.obfuscationKey = new Uint8Array(8); if (typeof crypto !== "undefined" && crypto.getRandomValues) { crypto.getRandomValues(this.obfuscationKey); } else { for (let i = 0; i < this.obfuscationKey.length; i++) { this.obfuscationKey[i] = Math.floor(Math.random() * 256); } } } return this.obfuscationKey; } /** * Make object non-serializable to prevent token leakage */ makeNonSerializable() { Object.defineProperty(this, "toJSON", { value: () => "[SecureTokenHandler - Non-Serializable]", writable: false, enumerable: false }); Object.defineProperty(this, "toString", { value: () => "[SecureTokenHandler - Protected]", writable: false, enumerable: false }); Object.defineProperty(this, "valueOf", { value: () => "[SecureTokenHandler - Protected]", writable: false, enumerable: false }); } /** * Remove non-serializable protection (for disposal) */ removeNonSerializable() { try { Reflect.deleteProperty(this, "toJSON"); Reflect.deleteProperty(this, "toString"); Reflect.deleteProperty(this, "valueOf"); } catch { } } }; var defaultSecureTokenHandler = new SecureTokenHandler(); // src/transports/sendbeacon-transport.ts var SendBeaconTransport = class _SendBeaconTransport extends BaseTransport { /** * Create a new SendBeacon transport instance * * @param config - Configuration options for the transport * @param config.endpoint - Required endpoint URL for telemetry data * @param config.batchSize - Maximum events per batch (default: 50) * @param config.flushInterval - Flush interval in milliseconds (default: 5000) * @param config.maxPayloadSize - Maximum payload size in bytes (default: 64KB) * @param config.authToken - Authentication token or provider function * @param config.enableOfflineStorage - Enable localStorage fallback (default: true) * @param config.rateLimitPerMinute - Rate limit events per minute (default: 1000) * * @example * ```typescript * const transport = new SendBeaconTransport({ * endpoint: 'https://api.example.com/telemetry', * batchSize: 25, * flushInterval: 3000, * authToken: async () => await getApiToken(), * userIdProvider: () => user.id * }); * ``` */ constructor(config) { const mergedConfig = { name: "sendbeacon", batchSize: 50, flushInterval: 5e3, maxPayloadSize: 64 * 1024, // 64KB enableOfflineStorage: true, storageKeyPrefix: "lever_ui_logger_", maxRetries: 3, retryDelay: 1e3, headers: {}, enableCompression: false, sessionIdGenerator: () => _SendBeaconTransport.generateSessionId(), rateLimitPerMinute: 1e3, enableLifecycleHandling: true, enableSecureTokenHandling: true, ...config }; super(mergedConfig.name, mergedConfig); this.eventQueue = []; this.rateLimitCounter = 0; this.rateLimitResetTime = 0; this.isOnline = true; this.retryQueue = []; this.lifecycleHandlersAttached = false; this.transportConfig = mergedConfig; this.secureTokenHandler = new SecureTokenHandler({ enableSecureMode: mergedConfig.enableSecureTokenHandling, tokenTtl: 36e5, // 1 hour validateToken: true }); if (config.authToken) { if (typeof config.authToken === "string") { this.secureTokenHandler.setToken(config.authToken); } else { this.secureTokenHandler.setTokenProvider(config.authToken); } } this.userIdProvider = config.userIdProvider; this.sessionId = this.transportConfig.sessionIdGenerator(); if (Environment.isBrowser) { this.setupOnlineStatusMonitoring(); this.loadOfflineEvents(); if (this.transportConfig.enableLifecycleHandling) { this.setupLifecycleHandlers(); } } this.startFlushTimer(); } /** * Write a log event to the transport * * Adds the event to the batching queue and triggers immediate flush * if batch size or payload size limits are reached. Events are * subject to rate limiting. * * @param event - The log event to write * * @example * ```typescript * transport.write({ * level: 'info', * message: 'User logged in', * timestamp: Date.now(), * component: 'auth', * context: { userId: '123' }, * args: [] * }); * ``` */ write(event) { if (!event) { throw new TypeError("LogEventData is required"); } if (!event.message || typeof event.message !== "string") { throw new TypeError("LogEventData must have a valid message"); } if (!this.checkRateLimit()) { console.warn("SendBeacon transport: Rate limit exceeded, dropping event"); return; } const eventSize = this.estimateEventSize(event); const queuedEvent = { event, timestamp: Date.now(), retryCount: 0, size: eventSize }; this.eventQueue.push(queuedEvent); if (this.shouldFlushImmediately()) { this.flush(); } } /** * Flush all pending events immediately * * Sends all queued events and retry events in optimally-sized batches. * Respects payload size limits and creates multiple batches if necessary. * Automatically handles online/offline state and retry logic. * * @returns Promise that resolves when all events have been processed * * @example * ```typescript * // Manually flush before page unload * window.addEventListener('beforeunload', async () => { * await transport.flush(); * }); * ``` */ async flush() { if (this.eventQueue.length === 0 && this.retryQueue.length === 0) { return; } this.clearFlushTimer(); const eventsToSend = [...this.eventQueue, ...this.retryQueue]; this.eventQueue = []; this.retryQueue = []; const batches = this.createBatches(eventsToSend); for (const batch of batches) { await this.sendBatch(batch); } this.startFlushTimer(); } /** * Close the transport and clean up resources * * Performs a final flush of all pending events, clears timers, * removes event listeners, saves any remaining events to * offline storage if enabled, and securely disposes of token handler. * * @returns Promise that resolves when cleanup is complete * * @example * ```typescript * // Clean shutdown * await transport.close(); * ``` */ async close() { await this.flush(); this.clearFlushTimer(); if (this.lifecycleHandlersAttached) { this.removeLifecycleHandlers(); } if (this.transportConfig.enableOfflineStorage && this.eventQueue.length > 0) { this.saveOfflineEvents(this.eventQueue); } this.secureTokenHandler.dispose(); } /** * Check if immediate flush is needed */ shouldFlushImmediately() { if (this.eventQueue.length >= this.transportConfig.batchSize) { return true; } const totalSize = this.eventQueue.reduce((sum, evt) => sum + evt.size, 0); if (totalSize >= this.transportConfig.maxPayloadSize * 0.8) { return true; } return false; } /** * Create batches respecting size limits */ createBatches(events) { const batches = []; let currentBatch = []; let currentSize = 0; for (const event of events) { if (currentBatch.length >= this.transportConfig.batchSize || currentSize + event.size > this.transportConfig.maxPayloadSize * 0.9) { if (currentBatch.length > 0) { batches.push(currentBatch); currentBatch = []; currentSize = 0; } } currentBatch.push(event); currentSize += event.size; } if (currentBatch.length > 0) { batches.push(currentBatch); } return batches; } /** * Send a batch of events */ async sendBatch(batch) { if (!this.isOnline) { this.saveOfflineEvents(batch); return; } const envelope = await this.createEnvelope(batch.map((b) => b.event)); const payload = JSON.stringify(envelope); try { const success = await this.sendPayload(payload); if (!success) { this.handleSendFailure(batch); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const sanitizedMessage = this.secureTokenHandler.sanitizeErrorMessage(errorMessage); console.error("SendBeacon transport: Failed to send batch", sanitizedMessage); this.handleSendFailure(batch); } } /** * Send payload using sendBeacon or fetch with keepalive * * Attempts to use navigator.sendBeacon first for optimal performance, * then falls back to fetch with keepalive flag. Automatically handles * payload size limits and browser compatibility. * * @param payload - JSON string payload to send * @returns Promise resolving to true if send was successful * * @internal */ async sendPayload(payload) { const headers = await this.buildHeaders(); if (Environment.isBrowser && typeof navigator !== "undefined" && navigator.sendBeacon && payload.length < this.transportConfig.maxPayloadSize) { const blob = new Blob([payload], { type: "application/json" }); const success = navigator.sendBeacon(this.transportConfig.endpoint, blob); if (success) { return true; } } try { const response = await fetch(this.transportConfig.endpoint, { method: "POST", headers, body: payload, keepalive: true, mode: "cors", credentials: "same-origin" }); return response.ok; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const sanitizedMessage = this.secureTokenHandler.sanitizeErrorMessage(errorMessage); console.error("SendBeacon transport: Fetch failed", sanitizedMessage); return false; } } /** * Build request headers with secure token handling */ async buildHeaders() { const headers = { "Content-Type": "application/json", ...this.transportConfig.headers }; try { const token = await this.secureTokenHandler.getToken(); if (token) { headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`; } } catch (error) { console.error("SendBeacon transport: Failed to retrieve auth token:", error instanceof Error ? error.message : String(error)); } return headers; } /** * Create telemetry envelope with metadata and sanitized events * * Wraps log events in a telemetry envelope containing session context, * user information, and environment metadata. Automatically sanitizes * events to handle circular references and serialization issues. * * @param events - Array of log events to include in envelope * @returns Promise resolving to complete telemetry envelope * * @internal */ async createEnvelope(events) { const sanitizedEvents = events.map((event) => { try { return JSON.parse(JSON.stringify(event, this.getCircularReplacer())); } catch { return { level: event.level, message: event.message || "[Message could not be serialized]", timestamp: event.timestamp, component: event.component, context: "[Context could not be serialized]", args: [] }; } }); const envelope = { sessionId: this.sessionId, userId: this.userIdProvider?.(), userAgent: Environment.isBrowser && typeof navigator !== "undefined" ? navigator.userAgent : "node", timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timestamp: Date.now(), events: sanitizedEvents, eventCount: sanitizedEvents.length, sizeBytes: this.estimateEventSize(sanitizedEvents) }; return envelope; } /** * Handle send failure with retry logic */ handleSendFailure(batch) { for (const event of batch) { event.retryCount++; if (event.retryCount < this.transportConfig.maxRetries) { setTimeout(() => { this.retryQueue.push(event); }, this.transportConfig.retryDelay * Math.pow(2, event.retryCount)); } else { if (this.transportConfig.enableOfflineStorage) { this.saveOfflineEvents([event]); } else { console.error("SendBeacon transport: Event dropped after max retries", this.sanitizeEventForLogging(event.event)); } } } } /** * Setup online/offline status monitoring */ setupOnlineStatusMonitoring() { this.isOnline = typeof navigator !== "undefined" ? navigator.onLine : true; if (typeof window !== "undefined") { window.addEventListener("online", () => { this.isOnline = true; this.loadOfflineEvents(); this.flush(); }); window.addEventListener("offline", () => { this.isOnline = false; }); } } /** * Setup page lifecycle event handlers */ setupLifecycleHandlers() { if (this.lifecycleHandlersAttached) return; const flushHandler = () => { this.flush(); }; document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { flushHandler(); } }); if (typeof window !== "undefined") { window.addEventListener("beforeunload", flushHandler); window.addEventListener("pagehide", flushHandler); } this.lifecycleHandlersAttached = true; } /** * Remove lifecycle event handlers */ removeLifecycleHandlers() { this.lifecycleHandlersAttached = false; } /** * Save events to offline storage */ saveOfflineEvents(events) { if (!this.transportConfig.enableOfflineStorage || !Environment.isBrowser) { return; } try { const key = `${this.transportConfig.storageKeyPrefix}offline_events`; const existing = this.getOfflineEvents(); const combined = [...existing, ...events]; const limited = combined.slice(-1e3); localStorage.setItem(key, JSON.stringify(limited)); } catch (error) { console.error("SendBeacon transport: Failed to save offline events", error); } } /** * Load events from offline storage */ loadOfflineEvents() { if (!this.transportConfig.enableOfflineStorage || !Environment.isBrowser) { return; } const events = this.getOfflineEvents(); if (events.length > 0) { this.retryQueue.push(...events); this.clearOfflineEvents(); } } /** * Get events from offline storage */ getOfflineEvents() { if (!Environment.isBrowser) return []; try { const key = `${this.transportConfig.storageKeyPrefix}offline_events`; const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : []; } catch (error) { console.error("SendBeacon transport: Failed to load offline events", error); return []; } } /** * Clear offline storage */ clearOfflineEvents() { if (!Environment.isBrowser) return; try { const key = `${this.transportConfig.storageKeyPrefix}offline_events`; localStorage.removeItem(key); } catch (error) { console.error("SendBeacon transport: Failed to clear offline events", error); } } /** * Check rate limit */ checkRateLimit() { const now = Date.now(); const minute = 60 * 1e3; if (now > this.rateLimitResetTime) { this.rateLimitCounter = 0; this.rateLimitResetTime = now + minute; } this.rateLimitCounter++; return this.rateLimitCounter <= this.transportConfig.rateLimitPerMinute; } /** * Start flush timer */ startFlushTimer() { if (this.flushTimer) return; this.flushTimer = setTimeout(() => { this.flushTimer = void 0; this.flush(); }, this.transportConfig.flushInterval); } /** * Clear flush timer */ clearFlushTimer() { if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = void 0; } } /** * Estimate size of event(s) in bytes */ estimateEventSize(event) { try { const json = JSON.stringify(event); return new Blob([json]).size; } catch { try { return JSON.stringify(event, this.getCircularReplacer()).length * 2; } catch { return 1e3; } } } /** * Get a secure replacer function for handling circular references and sensitive data */ getCircularReplacer() { return this.secureTokenHandler.createSecureReplacer(); } /** * Sanitize event data for safe logging * * @param event - Event data to sanitize * @returns Sanitized event data safe for logging */ sanitizeEventForLogging(event) { try { const sanitized = JSON.parse(JSON.stringify(event, this.getCircularReplacer())); return { level: sanitized.level, message: sanitized.message, component: sanitized.component, timestamp: sanitized.timestamp, context: sanitized.context || "[sanitized]", argsCount: Array.isArray(sanitized.args) ? sanitized.args.length : 0 }; } catch { return { level: event.level || "unknown", message: typeof event.message === "string" ? event.message : "[non-string message]", component: event.component || "unknown", timestamp: event.timestamp || Date.now(), context: "[sanitization failed]", argsCount: Array.isArray(event.args) ? event.args.length : 0 }; } } /** * Generate a unique session ID */ static generateSessionId() { return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`; } }; function createSendBeaconTransport(config) { return new SendBeaconTransport(config); } // src/logger/events.ts var LoggerBaseEvent = class { /** * Creates a new logger base event. * * @param timestamp - Unix timestamp when event was created (defaults to Date.now()) * @param clientId - Unique client identifier for this browser session (auto-generated) */ constructor(timestamp = Date.now(), clientId = generateClientId()) { this.timestamp = timestamp; this.clientId = clientId; } }; var LogEvent = class extends Log