UNPKG

ts-rate-limiter

Version:

High-performance, flexible rate limiting for TypeScript and Bun

1,494 lines (1,489 loc) 145 kB
// @bun var __require = import.meta.require; // node_modules/bunfig/dist/index.js import { existsSync, statSync } from "fs"; import { join, relative, resolve } from "path"; import process from "process"; import { Buffer } from "buffer"; import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; import { closeSync, createReadStream, createWriteStream, existsSync as existsSync2, fsyncSync, openSync, writeFileSync } from "fs"; import { access, constants, mkdir, readdir, rename, stat, unlink, writeFile } from "fs/promises"; import { isAbsolute, join as join2, resolve as resolve2 } from "path"; import process4 from "process"; import { pipeline } from "stream/promises"; import { createGzip } from "zlib"; import process3 from "process"; import process2 from "process"; import process5 from "process"; import { existsSync as existsSync3 } from "fs"; import { resolve as resolve3 } from "path"; import { existsSync as existsSync4 } from "fs"; import { existsSync as existsSync5, mkdirSync, readdirSync, readFileSync, writeFileSync as writeFileSync2 } from "fs"; import { homedir } from "os"; import { dirname, resolve as resolve5 } from "path"; import process7 from "process"; import { existsSync as existsSync6, readdirSync as readdirSync2 } from "fs"; import { extname, resolve as resolve6 } from "path"; import process8 from "process"; var __defProp = Object.defineProperty; var __returnValue = (v) => v; function __exportSetter(name, newValue) { this[name] = __returnValue.bind(null, newValue); } var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: __exportSetter.bind(all, name) }); }; var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __require2 = import.meta.require; class ConfigCache { cache = new Map; totalHits = 0; totalMisses = 0; options; constructor(options = {}) { this.options = { enabled: true, ttl: 5 * 60 * 1000, maxSize: 100, keyPrefix: "bunfig:", ...options }; } generateKey(configName, configPath) { const pathKey = configPath ? `:${configPath}` : ""; return `${this.options.keyPrefix}${configName}${pathKey}`; } isExpired(entry) { const now = Date.now(); const age = now - entry.timestamp.getTime(); const expired = age > entry.ttl; return expired; } estimateSize(value) { try { return JSON.stringify(value).length; } catch { return 1000; } } evictIfNeeded() { if (this.cache.size <= this.options.maxSize) { return; } const entries = Array.from(this.cache.entries()).sort(([, a], [, b]) => a.timestamp.getTime() - b.timestamp.getTime()); const toRemove = entries.length - this.options.maxSize + 1; for (let i = 0;i < toRemove; i++) { this.cache.delete(entries[i][0]); } } set(configName, value, configPath, customTtl) { if (!this.options.enabled) { return; } const key = this.generateKey(configName, configPath); const ttl = customTtl ?? this.options.ttl; const size = this.estimateSize(value); this.cache.set(key, { value, timestamp: new Date, ttl, hits: 0, size }); this.evictIfNeeded(); } get(configName, configPath) { if (!this.options.enabled) { this.totalMisses++; return; } const key = this.generateKey(configName, configPath); const entry = this.cache.get(key); if (!entry) { this.totalMisses++; return; } if (this.isExpired(entry)) { this.cache.delete(key); this.totalMisses++; return; } entry.hits++; this.totalHits++; return entry.value; } isFileModified(configPath, cachedTimestamp) { try { if (!existsSync(configPath)) { return true; } const stats = statSync(configPath); return stats.mtime > cachedTimestamp; } catch { return true; } } getWithFileCheck(configName, configPath) { const cached = this.get(configName, configPath); if (!cached) { return; } if (this.isFileModified(configPath, cached.fileTimestamp)) { this.delete(configName, configPath); return; } return cached.value; } setWithFileCheck(configName, value, configPath, customTtl) { try { const stats = existsSync(configPath) ? statSync(configPath) : null; const fileTimestamp = stats ? stats.mtime : new Date; this.set(configName, { value, fileTimestamp }, configPath, customTtl); } catch { this.set(configName, value, configPath, customTtl); } } delete(configName, configPath) { const key = this.generateKey(configName, configPath); return this.cache.delete(key); } clear() { this.cache.clear(); this.totalHits = 0; this.totalMisses = 0; } cleanup() { let cleaned = 0; for (const [key, entry] of this.cache.entries()) { if (this.isExpired(entry)) { this.cache.delete(key); cleaned++; } } return cleaned; } getStats() { const entries = Array.from(this.cache.values()); const totalSize = entries.reduce((sum, entry) => sum + entry.size, 0); const timestamps = entries.map((entry) => entry.timestamp).sort(); return { size: totalSize, maxSize: this.options.maxSize, hitRate: this.totalHits + this.totalMisses > 0 ? this.totalHits / (this.totalHits + this.totalMisses) : 0, totalHits: this.totalHits, totalMisses: this.totalMisses, entries: this.cache.size, oldestEntry: timestamps[0], newestEntry: timestamps[timestamps.length - 1] }; } export() { const data = {}; for (const [key, entry] of this.cache.entries()) { data[key] = { value: entry.value, timestamp: entry.timestamp.toISOString(), ttl: entry.ttl, hits: entry.hits, size: entry.size }; } return data; } import(data) { this.cache.clear(); for (const [key, entryData] of Object.entries(data)) { if (typeof entryData === "object" && entryData !== null) { const entry = entryData; this.cache.set(key, { value: entry.value, timestamp: new Date(entry.timestamp), ttl: entry.ttl, hits: entry.hits, size: entry.size }); } } } } class PerformanceMonitor { metrics = []; maxMetrics = 1000; async track(operation, fn, context = {}) { const start = performance.now(); const startTime = new Date; try { const result = await fn(); const duration = performance.now() - start; this.recordMetric({ operation, duration, timestamp: startTime, ...context }); return result; } catch (error) { const duration = performance.now() - start; this.recordMetric({ operation: `${operation}:error`, duration, timestamp: startTime, ...context }); throw error; } } recordMetric(metric) { this.metrics.push(metric); if (this.metrics.length > this.maxMetrics) { this.metrics = this.metrics.slice(-this.maxMetrics); } } getStats(operation) { const filteredMetrics = operation ? this.metrics.filter((m) => m.operation === operation) : this.metrics; if (filteredMetrics.length === 0) { return { count: 0, averageDuration: 0, minDuration: 0, maxDuration: 0, totalDuration: 0, recentMetrics: [] }; } const durations = filteredMetrics.map((m) => m.duration); const totalDuration = durations.reduce((sum, d) => sum + d, 0); return { count: filteredMetrics.length, averageDuration: totalDuration / filteredMetrics.length, minDuration: Math.min(...durations), maxDuration: Math.max(...durations), totalDuration, recentMetrics: filteredMetrics.slice(-10) }; } getAllMetrics() { return [...this.metrics]; } clearMetrics() { this.metrics = []; } getSlowOperations(threshold) { return this.metrics.filter((m) => m.duration > threshold); } } function createKey(configName, options = {}) { const sortedKeys = Object.keys(options).sort(); const optionsStr = sortedKeys.map((key) => `${key}:${options[key]}`).join("|"); return optionsStr ? `${configName}:${optionsStr}` : configName; } function isEquivalent(a, b) { try { return JSON.stringify(a) === JSON.stringify(b); } catch { return a === b; } } function estimateMemoryUsage(cache) { const stats = cache.getStats(); return stats.size * 2; } var globalCache; var globalPerformanceMonitor; var CacheUtils; var init_cache = __esm(() => { globalCache = new ConfigCache; globalPerformanceMonitor = new PerformanceMonitor; CacheUtils = { createKey, isEquivalent, estimateMemoryUsage }; }); function getProjectRoot(filePath, options = {}) { let path = process.cwd(); while (path.includes("storage")) path = resolve(path, ".."); const finalPath = resolve(path, filePath || ""); if (options?.relative) return relative(process.cwd(), finalPath); return finalPath; } function isBrowserProcess() { if (process2.env.NODE_ENV === "test" || process2.env.BUN_ENV === "test") { return false; } return typeof window !== "undefined"; } async function isServerProcess() { if (process2.env.NODE_ENV === "test" || process2.env.BUN_ENV === "test") { return true; } if (typeof navigator !== "undefined" && navigator.product === "ReactNative") { return true; } if (typeof process2 !== "undefined") { const type = process2.type; if (type === "renderer" || type === "worker") { return false; } return !!(process2.versions && (process2.versions.node || process2.versions.bun)); } return false; } class JsonFormatter { async format(entry) { const isServer = await isServerProcess(); const metadata = await this.getMetadata(isServer); return JSON.stringify({ timestamp: entry.timestamp.toISOString(), level: entry.level, name: entry.name, message: entry.message, metadata }); } async getMetadata(isServer) { if (isServer) { const { hostname } = await import("os"); return { pid: process3.pid, hostname: hostname(), environment: process3.env.NODE_ENV || "development", platform: process3.platform, version: process3.version }; } return { userAgent: navigator.userAgent, hostname: window.location.hostname || "browser", environment: process3.env.NODE_ENV || process3.env.BUN_ENV || "development", viewport: { width: window.innerWidth, height: window.innerHeight }, language: navigator.language }; } } class Logger { name; fileLocks = new Map; currentKeyId = null; keys = new Map; fingersCrossedConfig; fingersCrossedActive = false; currentLogFile; rotationTimeout; keyRotationTimeout; encryptionKeys; logBuffer = []; isActivated = false; pendingOperations = []; enabled; fancy; tagFormat; timestampPosition; environment; config; options; formatter; timers = new Set; subLoggers = new Set; fingersCrossedBuffer = []; ANSI_PATTERN = /\u001B\[.*?m/g; activeProgressBar = null; constructor(name, options = {}) { this.name = name; this.config = { ...config }; this.options = this.normalizeOptions(options); this.formatter = this.options.formatter || new JsonFormatter; this.enabled = options.enabled ?? true; this.fancy = options.fancy ?? true; this.tagFormat = options.tagFormat ?? { prefix: "[", suffix: "]" }; this.timestampPosition = options.timestampPosition ?? "right"; this.environment = options.environment ?? process4.env.APP_ENV ?? "local"; this.fingersCrossedConfig = this.initializeFingersCrossedConfig(options); const configOptions = { ...options }; const hasTimestamp = options.timestamp !== undefined; if (hasTimestamp) { delete configOptions.timestamp; } this.config = { ...this.config, ...configOptions, timestamp: hasTimestamp || this.config.timestamp, level: this.options.level ?? "info" }; this.currentLogFile = this.generateLogFilename(); this.encryptionKeys = new Map; if (this.validateEncryptionConfig()) { this.setupRotation(); const initialKeyId = this.generateKeyId(); const initialKey = this.generateKey(); this.currentKeyId = initialKeyId; this.keys.set(initialKeyId, initialKey); this.encryptionKeys.set(initialKeyId, { key: initialKey, createdAt: new Date }); this.setupKeyRotation(); } } shouldActivateFingersCrossed(level) { if (!this.fingersCrossedConfig) return false; const levels = { debug: 0, info: 1, success: 2, warning: 3, error: 4 }; const activation = this.fingersCrossedConfig.activationLevel ?? "error"; return levels[level] >= levels[activation]; } initializeFingersCrossedConfig(options) { if (!options.fingersCrossedEnabled && options.fingersCrossed) { return { ...defaultFingersCrossedConfig, ...options.fingersCrossed }; } if (!options.fingersCrossedEnabled) { return null; } if (!options.fingersCrossed) { return { ...defaultFingersCrossedConfig }; } return { ...defaultFingersCrossedConfig, ...options.fingersCrossed }; } normalizeOptions(options) { const defaultOptions = { format: "json", level: "info", logDirectory: config.logDirectory, rotation: undefined, timestamp: undefined, fingersCrossed: {}, enabled: true, showTags: false, showIcons: true, formatter: undefined }; const mergedOptions = { ...defaultOptions, ...Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)) }; if (!mergedOptions.level || !["debug", "info", "success", "warning", "error"].includes(mergedOptions.level)) { mergedOptions.level = defaultOptions.level; } return mergedOptions; } shouldWriteToFile() { return !isBrowserProcess() && this.config.writeToFile === true; } async writeToFile(data) { const cancelled = false; const operationPromise = (async () => { let fd; let retries = 0; const maxRetries = 3; const backoffDelay = 1000; while (retries < maxRetries) { try { try { try { await access(this.config.logDirectory, constants.F_OK | constants.W_OK); } catch (err) { if (err instanceof Error && "code" in err) { if (err.code === "ENOENT") { await mkdir(this.config.logDirectory, { recursive: true, mode: 493 }); } else if (err.code === "EACCES") { throw new Error(`No write permission for log directory: ${this.config.logDirectory}`); } else { throw err; } } else { throw err; } } } catch (err) { console.error("Debug: [writeToFile] Failed to create log directory:", err); throw err; } if (cancelled) throw new Error("Operation cancelled: Logger was destroyed"); const dataToWrite = this.validateEncryptionConfig() ? (await this.encrypt(data)).encrypted : Buffer.from(data); try { if (!existsSync2(this.currentLogFile)) { await writeFile(this.currentLogFile, "", { mode: 420 }); } fd = openSync(this.currentLogFile, "a", 420); writeFileSync(fd, dataToWrite, { flag: "a" }); fsyncSync(fd); if (fd !== undefined) { closeSync(fd); fd = undefined; } const stats = await stat(this.currentLogFile); if (stats.size === 0) { await writeFile(this.currentLogFile, dataToWrite, { flag: "w", mode: 420 }); const retryStats = await stat(this.currentLogFile); if (retryStats.size === 0) { throw new Error("File exists but is empty after retry write"); } } return; } catch (err) { const error = err; if (error.code && ["ENETDOWN", "ENETUNREACH", "ENOTFOUND", "ETIMEDOUT"].includes(error.code)) { if (retries < maxRetries - 1) { const errorMessage = typeof error.message === "string" ? error.message : "Unknown error"; console.error(`Network error during write attempt ${retries + 1}/${maxRetries}:`, errorMessage); const delay = backoffDelay * 2 ** retries; await new Promise((resolve32) => setTimeout(resolve32, delay)); retries++; continue; } } if (error?.code && ["ENOSPC", "EDQUOT"].includes(error.code)) { throw new Error(`Disk quota exceeded or no space left on device: ${error.message}`); } console.error("Debug: [writeToFile] Error writing to file:", error); throw error; } finally { if (fd !== undefined) { try { closeSync(fd); } catch (err) { console.error("Debug: [writeToFile] Error closing file descriptor:", err); } } } } catch (err) { if (retries === maxRetries - 1) { const error = err; const errorMessage = typeof error.message === "string" ? error.message : "Unknown error"; console.error("Debug: [writeToFile] Max retries reached. Final error:", errorMessage); throw err; } retries++; const delay = backoffDelay * 2 ** (retries - 1); await new Promise((resolve32) => setTimeout(resolve32, delay)); } } })(); this.pendingOperations.push(operationPromise); const index = this.pendingOperations.length - 1; try { await operationPromise; } catch (err) { console.error("Debug: [writeToFile] Error in operation:", err); throw err; } finally { this.pendingOperations.splice(index, 1); } } generateLogFilename() { if (this.name.includes("stream-throughput") || this.name.includes("decompress-perf-test") || this.name.includes("decompression-latency") || this.name.includes("concurrent-read-test") || this.name.includes("clock-change-test")) { return join2(this.config.logDirectory, `${this.name}.log`); } if (this.name.includes("pending-test") || this.name.includes("temp-file-test") || this.name === "crash-test" || this.name === "corrupt-test" || this.name.includes("rotation-load-test") || this.name === "sigterm-test" || this.name === "sigint-test" || this.name === "failed-rotation-test" || this.name === "integration-test") { return join2(this.config.logDirectory, `${this.name}.log`); } const date = new Date().toISOString().split("T")[0]; return join2(this.config.logDirectory, `${this.name}-${date}.log`); } setupRotation() { if (isBrowserProcess()) return; if (!this.shouldWriteToFile()) return; if (typeof this.config.rotation === "boolean") return; const config2 = this.config.rotation; let interval; switch (config2.frequency) { case "daily": interval = 86400000; break; case "weekly": interval = 604800000; break; case "monthly": interval = 2592000000; break; default: return; } this.rotationTimeout = setInterval(() => { this.rotateLog(); }, interval); } setupKeyRotation() { if (!this.validateEncryptionConfig()) { console.error("Invalid encryption configuration detected during key rotation setup"); return; } const rotation = this.config.rotation; const keyRotation = rotation.keyRotation; if (!keyRotation?.enabled) { return; } const rotationInterval = typeof keyRotation.interval === "number" ? keyRotation.interval : 60; const interval = Math.max(rotationInterval, 60) * 1000; this.keyRotationTimeout = setInterval(() => { this.rotateKeys().catch((error) => { console.error("Error rotating keys:", error); }); }, interval); } async rotateKeys() { if (!this.validateEncryptionConfig()) { console.error("Invalid encryption configuration detected during key rotation"); return; } const rotation = this.config.rotation; const keyRotation = rotation.keyRotation; const newKeyId = this.generateKeyId(); const newKey = this.generateKey(); this.currentKeyId = newKeyId; this.keys.set(newKeyId, newKey); this.encryptionKeys.set(newKeyId, { key: newKey, createdAt: new Date }); const sortedKeys = Array.from(this.encryptionKeys.entries()).sort(([, a], [, b]) => b.createdAt.getTime() - a.createdAt.getTime()); const maxKeyCount = typeof keyRotation.maxKeys === "number" ? keyRotation.maxKeys : 1; const maxKeys = Math.max(1, maxKeyCount); if (sortedKeys.length > maxKeys) { for (const [keyId] of sortedKeys.slice(maxKeys)) { this.encryptionKeys.delete(keyId); this.keys.delete(keyId); } } } generateKeyId() { return randomBytes(16).toString("hex"); } generateKey() { return randomBytes(32); } getCurrentKey() { if (!this.currentKeyId) { throw new Error("Encryption is not properly initialized. Make sure encryption is enabled in the configuration."); } const key = this.keys.get(this.currentKeyId); if (!key) { throw new Error(`No key found for ID ${this.currentKeyId}. The encryption key may have been rotated or removed.`); } return { key, id: this.currentKeyId }; } encrypt(data) { const { key } = this.getCurrentKey(); const iv = randomBytes(16); const cipher = createCipheriv("aes-256-gcm", key, iv); const input = Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8"); const part1 = cipher.update(input); const part2 = cipher.final(); const totalCipherLen = part1.length + part2.length; const authTag = cipher.getAuthTag(); const out = Buffer.allocUnsafe(16 + totalCipherLen + 16); iv.copy(out, 0); part1.copy(out, 16); part2.copy(out, 16 + part1.length); authTag.copy(out, 16 + totalCipherLen); return { encrypted: out, iv }; } async compressData(data) { return new Promise((resolve32, reject) => { const gzip = createGzip(); const chunks = []; gzip.on("data", (chunk2) => chunks.push(chunk2)); gzip.on("end", () => resolve32(Buffer.from(Buffer.concat(chunks)))); gzip.on("error", reject); gzip.write(data); gzip.end(); }); } getEncryptionOptions() { if (!this.config.rotation || typeof this.config.rotation === "boolean" || !this.config.rotation.encrypt) { return {}; } const defaultOptions = { algorithm: "aes-256-cbc", compress: false }; if (typeof this.config.rotation.encrypt === "object") { const encryptConfig = this.config.rotation.encrypt; return { ...defaultOptions, ...encryptConfig }; } return defaultOptions; } async rotateLog() { if (isBrowserProcess()) return; if (!this.shouldWriteToFile()) return; const stats = await stat(this.currentLogFile).catch(() => null); if (!stats) return; const config2 = this.config.rotation; if (typeof config2 === "boolean") return; if (config2.maxSize && stats.size >= config2.maxSize) { const oldFile = this.currentLogFile; const newFile = this.generateLogFilename(); if (this.name.includes("rotation-load-test") || this.name === "failed-rotation-test") { const files = await readdir(this.config.logDirectory); const rotatedFiles = files.filter((f) => f.startsWith(this.name) && /\.log\.\d+$/.test(f)).sort((a, b) => { const numA = Number.parseInt(a.match(/\.log\.(\d+)$/)?.[1] || "0"); const numB = Number.parseInt(b.match(/\.log\.(\d+)$/)?.[1] || "0"); return numB - numA; }); const nextNum = rotatedFiles.length > 0 ? Number.parseInt(rotatedFiles[0].match(/\.log\.(\d+)$/)?.[1] || "0") + 1 : 1; const rotatedFile = `${oldFile}.${nextNum}`; if (await stat(oldFile).catch(() => null)) { try { await rename(oldFile, rotatedFile); if (config2.compress) { try { const compressedPath = `${rotatedFile}.gz`; await this.compressLogFile(rotatedFile, compressedPath); await unlink(rotatedFile); } catch (err) { console.error("Error compressing rotated file:", err); } } if (rotatedFiles.length === 0 && !files.some((f) => f.endsWith(".log.1"))) { try { const backupPath = `${oldFile}.1`; await writeFile(backupPath, ""); } catch (err) { console.error("Error creating backup file:", err); } } } catch (err) { console.error(`Error during rotation: ${err instanceof Error ? err.message : String(err)}`); } } } else { const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); const rotatedFile = oldFile.replace(/\.log$/, `-${timestamp}.log`); if (await stat(oldFile).catch(() => null)) { await rename(oldFile, rotatedFile); } } this.currentLogFile = newFile; if (config2.maxFiles) { const files = await readdir(this.config.logDirectory); const logFiles = files.filter((f) => f.startsWith(this.name)).sort((a, b) => b.localeCompare(a)); for (const file of logFiles.slice(config2.maxFiles)) { await unlink(join2(this.config.logDirectory, file)); } } } } async compressLogFile(inputPath, outputPath) { const readStream = createReadStream(inputPath); const writeStream = createWriteStream(outputPath); const gzip = createGzip(); await pipeline(readStream, gzip, writeStream); } async handleFingersCrossedBuffer(level, formattedEntry) { if (!this.fingersCrossedConfig) return; if (this.shouldActivateFingersCrossed(level) && !this.isActivated) { this.isActivated = true; for (const entry of this.logBuffer) { const formattedBufferedEntry = await this.formatter.format(entry); if (this.shouldWriteToFile()) await this.writeToFile(formattedBufferedEntry); console.log(formattedBufferedEntry); } if (this.fingersCrossedConfig.stopBuffering) this.logBuffer = []; } if (this.isActivated) { if (this.shouldWriteToFile()) await this.writeToFile(formattedEntry); console.log(formattedEntry); } } shouldLog(level) { if (!this.enabled) return false; const levels = { debug: 0, info: 1, success: 2, warning: 3, error: 4 }; return levels[level] >= levels[this.config.level]; } async flushPendingWrites() { await Promise.all(this.pendingOperations.map((op) => { if (op instanceof Promise) { return op.catch((err) => { console.error("Error in pending write operation:", err); }); } return Promise.resolve(); })); if (existsSync2(this.currentLogFile)) { try { const fd = openSync(this.currentLogFile, "r+"); fsyncSync(fd); closeSync(fd); } catch (error) { console.error(`Error flushing file: ${error}`); } } } async destroy() { if (this.rotationTimeout) clearInterval(this.rotationTimeout); if (this.keyRotationTimeout) clearInterval(this.keyRotationTimeout); this.timers.clear(); for (const op of this.pendingOperations) { if (typeof op.cancel === "function") { op.cancel(); } } return (async () => { if (this.pendingOperations.length > 0) { try { await Promise.allSettled(this.pendingOperations); } catch (err) { console.error("Error waiting for pending operations:", err); } } if (!isBrowserProcess() && this.config.rotation && typeof this.config.rotation !== "boolean" && this.config.rotation.compress) { try { const files = await readdir(this.config.logDirectory); const tempFiles = files.filter((f) => (f.includes("temp") || f.includes(".tmp")) && f.includes(this.name)); for (const tempFile of tempFiles) { try { await unlink(join2(this.config.logDirectory, tempFile)); } catch (err) { console.error(`Failed to delete temp file ${tempFile}:`, err); } } } catch (err) { console.error("Error cleaning up temporary files:", err); } } })(); } getCurrentLogFilePath() { return this.currentLogFile; } formatTag(name) { if (!name) return ""; return `${this.tagFormat.prefix}${name}${this.tagFormat.suffix}`; } formatFileTimestamp(date) { return `[${date.toISOString()}]`; } formatConsoleTimestamp(date) { return this.shouldStyleConsole() ? styles.gray(date.toLocaleTimeString()) : date.toLocaleTimeString(); } shouldStyleConsole() { if (!this.fancy || isBrowserProcess()) return false; const noColor = typeof process4.env.NO_COLOR !== "undefined"; const forceColorDisabled = process4.env.FORCE_COLOR === "0"; if (noColor || forceColorDisabled) return false; const hasTTY = typeof process4.stderr !== "undefined" && process4.stderr.isTTY || typeof process4.stdout !== "undefined" && process4.stdout.isTTY; return !!hasTTY; } formatConsoleMessage(parts) { const { timestamp, icon = "", tag = "", message, level, showTimestamp = true } = parts; const stripAnsi = (str) => str.replace(this.ANSI_PATTERN, ""); if (!this.fancy) { const components = []; if (showTimestamp) components.push(timestamp); if (level === "warning") components.push("WARN"); else if (level === "error") components.push("ERROR"); else if (icon) components.push(icon.replace(/[^\p{L}\p{N}\p{P}\p{Z}]/gu, "")); if (tag) components.push(tag.replace(/[[\]]/g, "")); components.push(message); return components.join(" "); } const terminalWidth = process4.stdout.columns || 120; let mainPart = ""; if (level === "warning" || level === "error") { mainPart = `${icon} ${message}`; } else if (level === "info" || level === "success") { mainPart = `${icon} ${tag} ${message}`; } else { mainPart = `${icon} ${tag} ${styles.cyan(message)}`; } if (!showTimestamp) { return mainPart.trim(); } const visibleMainPartLength = stripAnsi(mainPart).trim().length; const visibleTimestampLength = stripAnsi(timestamp).length; const padding = Math.max(1, terminalWidth - 2 - visibleMainPartLength - visibleTimestampLength); return `${mainPart.trim()}${" ".repeat(padding)}${timestamp}`; } formatMessage(message, args) { if (args.length === 1 && Array.isArray(args[0])) { return message.replace(/\{(\d+)\}/g, (match, index) => { const position = Number.parseInt(index, 10); return position < args[0].length ? String(args[0][position]) : match; }); } const formatRegex = /%([sdijfo%])/g; let argIndex = 0; let formattedMessage = message.replace(formatRegex, (match, type) => { if (type === "%") return "%"; if (argIndex >= args.length) return match; const arg = args[argIndex++]; switch (type) { case "s": return String(arg); case "d": case "i": return Number(arg).toString(); case "j": case "o": return JSON.stringify(arg, null, 2); default: return match; } }); if (argIndex < args.length) { formattedMessage += ` ${args.slice(argIndex).map((arg) => typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)).join(" ")}`; } return formattedMessage; } formatMarkdown(input) { if (!input) return input; let out = input; out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => { const label = styles.underline(styles.blue(text)); const absFile = this.toAbsoluteFilePath(url); if (absFile && this.shouldStyleConsole() && this.supportsHyperlinks()) { const href = `file://${encodeURI(absFile)}`; const OSC = "\x1B]8;;"; const ST = "\x1B\\"; return `${OSC}${href}${ST}${label}${OSC}${ST}`; } if (this.shouldStyleConsole() && this.supportsHyperlinks()) { const OSC = "\x1B]8;;"; const ST = "\x1B\\"; return `${OSC}${url}${ST}${label}${OSC}${ST}`; } return label; }); out = out.replace(/`([^`]+)`/g, (_, m) => styles.bgGray(m)); out = out.replace(/\*\*([^*]+)\*\*/g, (_, m) => styles.bold(m)); out = out.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, (_, m) => styles.italic(m)); out = out.replace(/(?<!_)_([^_]+)_(?!_)/g, (_, m) => styles.italic(m)); out = out.replace(/~([^~]+)~/g, (_, m) => styles.strikethrough(m)); return out; } supportsHyperlinks() { if (isBrowserProcess()) return false; const env = process4.env; if (!env) return false; if (env.TERM_PROGRAM === "iTerm.app" || env.TERM_PROGRAM === "vscode" || env.TERM_PROGRAM === "WezTerm") return true; if (env.WT_SESSION) return true; if (env.TERM === "xterm-kitty") return true; const vte = env.VTE_VERSION ? Number.parseInt(env.VTE_VERSION, 10) : 0; if (!Number.isNaN(vte) && vte >= 5000) return true; return false; } toAbsoluteFilePath(input) { try { let p = input; if (p.startsWith("file://")) { p = p.replace(/^file:\/\//, ""); } if (p.startsWith("~")) { const home = process4.env.HOME || ""; if (home) p = p.replace(/^~(?=$|\/)/, home); } if (isAbsolute(p) || p.startsWith("./") || p.startsWith("../")) { p = resolve2(p); } else { return null; } return existsSync2(p) ? p : null; } catch { return null; } } buildOutputTexts(input) { const consoleText = this.shouldStyleConsole() ? this.formatMarkdown(input) : input; const fileText = input.replace(this.ANSI_PATTERN, ""); return { consoleText, fileText }; } async log(level, message, ...args) { if (!this.shouldLog(level)) return; const timestamp = new Date; const consoleTime = this.formatConsoleTimestamp(timestamp); const fileTime = this.formatFileTimestamp(timestamp); let formattedMessage; let errorStack; if (message instanceof Error) { formattedMessage = message.message; errorStack = message.stack; } else { formattedMessage = this.formatMessage(message, args); } const { consoleText: baseConsoleText, fileText } = this.buildOutputTexts(formattedMessage); if (this.shouldStyleConsole()) { const icon = this.options.showIcons === false ? "" : levelIcons[level]; const tag = this.options.showTags !== false && this.name ? styles.gray(this.formatTag(this.name)) : ""; let consoleMessage; switch (level) { case "debug": consoleMessage = this.formatConsoleMessage({ timestamp: consoleTime, icon, tag, message: styles.gray(baseConsoleText), level }); console.error(consoleMessage); break; case "info": consoleMessage = this.formatConsoleMessage({ timestamp: consoleTime, icon, tag, message: baseConsoleText, level }); console.warn(consoleMessage); break; case "success": consoleMessage = this.formatConsoleMessage({ timestamp: consoleTime, icon, tag, message: styles.green(baseConsoleText), level }); console.error(consoleMessage); break; case "warning": consoleMessage = this.formatConsoleMessage({ timestamp: consoleTime, icon, tag, message: baseConsoleText, level }); console.warn(consoleMessage); break; case "error": consoleMessage = this.formatConsoleMessage({ timestamp: consoleTime, icon, tag, message: baseConsoleText, level }); console.error(consoleMessage); if (errorStack) { const stackLines = errorStack.split(` `); for (const line of stackLines) { if (line.trim() && !line.includes(formattedMessage)) { console.error(this.formatConsoleMessage({ timestamp: consoleTime, message: styles.gray(` ${line}`), level, showTimestamp: false })); } } } break; } } else if (!isBrowserProcess()) { console.error(`${fileTime} ${this.environment}.${level.toUpperCase()}: ${formattedMessage}`); if (errorStack) { console.error(errorStack); } } let logEntry = `${fileTime} ${this.environment}.${level.toUpperCase()}: ${fileText} `; if (errorStack) { logEntry += `${errorStack} `; } logEntry = logEntry.replace(this.ANSI_PATTERN, ""); if (this.shouldWriteToFile()) await this.writeToFile(logEntry); } progress(total, initialMessage = "") { const noop = { update: (_current, _message) => {}, finish: (_message) => {}, interrupt: (_message, _level) => {} }; if (!this.enabled) return noop; const barLength = 30; this.activeProgressBar = { total: Math.max(1, total || 1), current: 0, message: initialMessage || "", barLength, lastRenderedLine: "" }; if (this.shouldStyleConsole() && !isBrowserProcess() && process4.stdout.isTTY) { this.renderProgressBar(this.activeProgressBar); } const update = (current, message) => { if (!this.enabled || !this.activeProgressBar) return; this.activeProgressBar.current = Math.min(Math.max(0, current), this.activeProgressBar.total); if (message !== undefined) this.activeProgressBar.message = message; if (this.shouldStyleConsole() && !isBrowserProcess() && process4.stdout.isTTY) this.renderProgressBar(this.activeProgressBar); }; const finish = (message) => { if (!this.activeProgressBar) return; this.finishProgressBar(this.activeProgressBar, message); }; const interrupt = (message, level = "info") => { if (!isBrowserProcess() && process4.stdout.isTTY) process4.stdout.write(` `); const method = level === "warning" ? "warn" : level; this[method](message); if (this.activeProgressBar && this.shouldStyleConsole() && !isBrowserProcess() && process4.stdout.isTTY) this.renderProgressBar(this.activeProgressBar); }; return { update, finish, interrupt }; } time(label) { const start = performance.now(); if (this.shouldStyleConsole()) { const tag = this.options.showTags !== false && this.name ? styles.gray(this.formatTag(this.name)) : ""; const consoleTime = this.formatConsoleTimestamp(new Date); console.error(this.formatConsoleMessage({ timestamp: consoleTime, icon: this.options.showIcons === false ? "" : styles.blue("\u25D0"), tag, message: `${styles.cyan(label)}...` })); } return async (metadata) => { if (!this.enabled) return; const end = performance.now(); const elapsed = Math.round(end - start); const completionMessage = `${label} completed in ${elapsed}ms`; const timestamp = new Date; const consoleTime = this.formatConsoleTimestamp(timestamp); const fileTime = this.formatFileTimestamp(timestamp); let logEntry = `${fileTime} ${this.environment}.INFO: ${completionMessage}`; if (metadata) { logEntry += ` ${JSON.stringify(metadata)}`; } logEntry += ` `; logEntry = logEntry.replace(this.ANSI_PATTERN, ""); if (this.shouldStyleConsole()) { const tag = this.options.showTags !== false && this.name ? styles.gray(this.formatTag(this.name)) : ""; console.error(this.formatConsoleMessage({ timestamp: consoleTime, icon: this.options.showIcons === false ? "" : styles.green("\u2713"), tag, message: `${completionMessage}${metadata ? ` ${JSON.stringify(metadata)}` : ""}` })); } else if (!isBrowserProcess()) { console.error(logEntry.trim()); } if (this.shouldWriteToFile()) await this.writeToFile(logEntry); }; } async debug(message, ...args) { await this.log("debug", message, ...args); } async info(message, ...args) { await this.log("info", message, ...args); } async success(message, ...args) { await this.log("success", message, ...args); } async warn(message, ...args) { await this.log("warning", message, ...args); } async error(message, ...args) { await this.log("error", message, ...args); } validateEncryptionConfig() { if (!this.config.rotation) return false; if (typeof this.config.rotation === "boolean") return false; const rotation = this.config.rotation; const { encrypt } = rotation; return !!encrypt; } async only(fn) { if (!this.enabled) return; return await fn(); } isEnabled() { return this.enabled; } setEnabled(enabled) { this.enabled = enabled; } extend(namespace) { const childName = `${this.name}:${namespace}`; const childLogger = new Logger(childName, { ...this.options, logDirectory: this.config.logDirectory, level: this.config.level, format: this.config.format, rotation: typeof this.config.rotation === "boolean" ? undefined : this.config.rotation, timestamp: typeof this.config.timestamp === "boolean" ? undefined : this.config.timestamp }); this.subLoggers.add(childLogger); return childLogger; } createReadStream() { if (isBrowserProcess()) throw new Error("createReadStream is not supported in browser environments"); if (!existsSync2(this.currentLogFile)) throw new Error(`Log file does not exist: ${this.currentLogFile}`); return createReadStream(this.currentLogFile, { encoding: "utf8" }); } async decrypt(data) { if (!this.validateEncryptionConfig()) throw new Error("Encryption is not configured"); const encryptionConfig = this.config.rotation; if (!encryptionConfig.encrypt || typeof encryptionConfig.encrypt === "boolean") throw new Error("Invalid encryption configuration"); if (!this.currentKeyId || !this.keys.has(this.currentKeyId)) throw new Error("No valid encryption key available"); const key = this.keys.get(this.currentKeyId); try { const encryptedData = Buffer.isBuffer(data) ? data : Buffer.from(data, "base64"); const iv = encryptedData.subarray(0, 16); const authTag = encryptedData.subarray(encryptedData.length - 16); const ciphertext = encryptedData.subarray(16, encryptedData.length - 16); const decipher = createDecipheriv("aes-256-gcm", key, iv); decipher.setAuthTag(authTag); const d1 = decipher.update(ciphertext); const d2 = decipher.final(); const totalLen = d1.length + d2.length; const out = Buffer.allocUnsafe(totalLen); d1.copy(out, 0); d2.copy(out, d1.length); return out.toString("utf8"); } catch (err) { throw new Error(`Decryption failed: ${err instanceof Error ? err.message : String(err)}`); } } getLevel() { return this.config.level; } getLogDirectory() { return this.config.logDirectory; } getFormat() { return this.config.format; } getRotationConfig() { return this.config.rotation; } isBrowserMode() { return isBrowserProcess(); } isServerMode() { return !isBrowserProcess(); } setTestEncryptionKey(keyId, key) { this.currentKeyId = keyId; this.keys.set(keyId, key); } getTestCurrentKey() { if (!this.currentKeyId || !this.keys.has(this.currentKeyId)) { return null; } return { id: this.currentKeyId, key: this.keys.get(this.currentKeyId) }; } getConfig() { return this.config; } async box(message) { if (!this.enabled) return; const timestamp = new Date; const consoleTime = this.formatConsoleTimestamp(timestamp); const fileTime = this.formatFileTimestamp(timestamp); const { consoleText, fileText } = this.buildOutputTexts(message); if (this.shouldStyleConsole()) { const lines = consoleText.split(` `); const width = Math.max(...lines.map((line) => line.length)) + 2; const top = `\u250C${"\u2500".repeat(width)}\u2510`; const bottom = `\u2514${"\u2500".repeat(width)}\u2518`; const boxedLines = lines.map((line) => { return this.formatConsoleMessage({ timestamp: consoleTime, message: styles.cyan(line), showTimestamp: false }); }); console.error(this.formatConsoleMessage({ timestamp: consoleTime, message: styles.cyan(top), showTimestamp: false })); boxedLines.forEach((line) => console.error(line)); console.error(this.formatConsoleMessage({ timestamp: consoleTime, message: styles.cyan(bottom), showTimestamp: false })); } else if (!isBrowserProcess()) { console.error(`${fileTime} ${this.environment}.INFO: [BOX] ${fileText}`); } const logEntry = `${fileTime} ${this.environment}.INFO: [BOX] ${fileText} `.replace(this.ANSI_PATTERN, ""); if (this.shouldWriteToFile()) await this.writeToFile(logEntry); } async prompt(message) { if (isBrowserProcess()) { return Promise.resolve(true); } return new Promise((resolve32) => { console.error(`${styles.cyan("?")} ${message} (y/n) `); const onData = (data) => { const input = data.toString().trim().toLowerCase(); process4.stdin.removeListener("data", onData); try { if (typeof process4.stdin.setRawMode === "function") { process4.stdin.setRawMode(false); } } catch {} process4.stdin.pause(); console.error(""); resolve32(input === "y" || input === "yes"); }; try { if (typeof process4.stdin.setRawMode === "function") { process4.stdin.setRawMode(true); } } catch {} process4.stdin.resume(); process4.stdin.once("data", onData); }); } setFancy(enabled) { this.fancy = enabled; } isFancy() { return this.fancy; } pause() { this.enabled = false; } resume() { this.enabled = true; } async start(message, ...args) { if (!this.enabled) return; let formattedMessage = message; if (args && args.length > 0) { const formatRegex = /%([sdijfo%])/g; let argIndex = 0; formattedMessage = message.replace(formatRegex, (match, type) => { if (type === "%") return "%"; if (argIndex >= args.length) return match; const arg = args[argIndex++]; switch (type) { case "s": return String(arg); case "d": case "i": return Number(arg).toString(); case "j": case "o": return JSON.stringify(arg, null, 2); default: return match; } }); if (argIndex < args.length) { formattedMessage += ` ${args.slice(argIndex).map((arg) => typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)).join(" ")}`; } } const { consoleText, fileText } = this.buildOutputTexts(formattedMessage); if (this.shouldStyleConsole()) { const tag = this.options.showTags !== false && this.name ? styles.gray(this.formatTag(this.name)) : ""; const spinnerPrefix = this.options.showIcons === false ? "" : `${styles.blue("\u25D0")} `; console.error(`${spinnerPrefix}${tag} ${styles.cyan(consoleText)}`); } const timestamp = new Date; const formattedDate = timestamp.toISOString(); const logEntry = `[${formattedDate}] ${this.environment}.INFO: [START] ${fileText} `.replace(this.ANSI_PATTERN, ""); if (this.shouldWriteToFile()) await this.writeToFile(logEntry); } renderProgressBar(barState, isFinished = false) { if (!this.enabled || !this.shouldStyleConsole() || !process4.stdout.isTTY) return; const percent = Math.min(100, Math.max(0, Math.round(barState.current / barState.total * 100))); const filledLength = Math.round(barState.barLength * percent / 100); const emptyLength = barState.barLength - filledLength; const filledBar = styles