UNPKG

@civic/nexus-bridge

Version:

Stdio <-> HTTP/SSE MCP bridge with Civic auth handling

324 lines 11.8 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /** * logger.ts * * Provides centralized logging functionality with support for different log levels, * sensitive data masking, and configurable output formats. */ // Log levels in order of increasing verbosity export var LogLevel; (function (LogLevel) { LogLevel[LogLevel["ERROR"] = 0] = "ERROR"; LogLevel[LogLevel["WARN"] = 1] = "WARN"; LogLevel[LogLevel["INFO"] = 2] = "INFO"; LogLevel[LogLevel["DEBUG"] = 3] = "DEBUG"; LogLevel[LogLevel["TRACE"] = 4] = "TRACE"; })(LogLevel || (LogLevel = {})); // Map string log level names to enum values const LOG_LEVEL_MAP = { 'error': LogLevel.ERROR, 'warn': LogLevel.WARN, 'info': LogLevel.INFO, 'debug': LogLevel.DEBUG, 'trace': LogLevel.TRACE }; /** * Central logger class for the nexus bridge * Provides consistent log formatting and control over verbosity */ export class Logger { level; enableJsonFormatting; maskSensitiveData; includeTimestamps; name; // Sensitive data patterns to detect static SENSITIVE_PATTERNS = [ // Token field names /access[_-]?token/i, /refresh[_-]?token/i, /id[_-]?token/i, /auth[_-]?token/i, /jwt/i, /api[_-]?key/i, /secret/i, /password/i, // Token formats /^ey[I-L][a-zA-Z0-9_-]{5,}\.ey[I-L][a-zA-Z0-9_-]{5,}\.[a-zA-Z0-9_-]{10,}/, // JWT format /gh[ps]_[a-zA-Z0-9]{36,255}/, // GitHub token /sk-[a-zA-Z0-9]{30,}/, // OpenAI key format ]; constructor(options = {}) { // Set module name for better context this.name = options.name || 'bridge'; // Get log level from options, environment, or default to INFO const envLogLevel = process.env.LOG_LEVEL?.toLowerCase(); this.level = options.level !== undefined ? options.level : (envLogLevel && LOG_LEVEL_MAP[envLogLevel] !== undefined) ? LOG_LEVEL_MAP[envLogLevel] : process.env.DEBUG === 'true' ? LogLevel.DEBUG : LogLevel.INFO; // Whether to JSON.stringify objects this.enableJsonFormatting = options.enableJsonFormatting ?? true; // Whether to mask sensitive data this.maskSensitiveData = options.maskSensitiveData ?? (process.env.MASK_SENSITIVE_DATA !== 'false'); // Whether to include timestamps in logs this.includeTimestamps = options.includeTimestamps ?? (process.env.LOG_TIMESTAMPS === 'true'); } /** * Set the log level */ setLevel(level) { this.level = level; } /** * Get the current log level */ getLevel() { return this.level; } /** * Get log level as a string */ getLevelName() { switch (this.level) { case LogLevel.ERROR: return 'ERROR'; case LogLevel.WARN: return 'WARN'; case LogLevel.INFO: return 'INFO'; case LogLevel.DEBUG: return 'DEBUG'; case LogLevel.TRACE: return 'TRACE'; default: return 'UNKNOWN'; } } /** * Enable or disable JSON formatting for objects */ setJsonFormatting(enable) { this.enableJsonFormatting = enable; } /** * Enable or disable sensitive data masking */ setMaskSensitiveData(mask) { this.maskSensitiveData = mask; } /** * Enable or disable timestamps in log output */ setIncludeTimestamps(include) { this.includeTimestamps = include; } /** * Format message and args for logging */ format(message, ...args) { // Add timestamp prefix if enabled const timestamp = this.includeTimestamps ? `[${new Date().toISOString()}] ` : ''; const modulePrefix = this.name ? `[${this.name}] ` : ''; const formattedMessage = `${timestamp}${modulePrefix}${message}`; const formattedArgs = args.map(arg => { if (arg === null || arg === undefined) { return arg; } // If it's an object and JSON formatting is enabled if (typeof arg === 'object' && this.enableJsonFormatting) { try { if (this.maskSensitiveData) { // Create a deep copy to avoid modifying the original object // Use our special method that handles circular references const maskedObj = this.safeCloneAndMask(arg); return JSON.stringify(maskedObj, null, 2); } // Use a replacer function to handle circular references return JSON.stringify(arg, this.circularReplacer(), 2); } catch (formatError) { // Create a meaningful error message that includes the error type const errorMessage = formatError instanceof Error ? formatError.message : String(formatError); return `[Object: ${arg.constructor?.name || 'Unknown'} (stringify failed: ${errorMessage})]`; } } else if (typeof arg === 'string' && this.maskSensitiveData) { // Check if the string itself might be a sensitive token return this.maskSensitiveString(arg); } return arg; }); return [formattedMessage, ...formattedArgs]; } /** * Safe clone and mask an object, handling circular references */ safeCloneAndMask(obj) { // Use a cache to detect circular references const cache = new WeakMap(); const clone = (value) => { // Handle primitives and null if (value === null || typeof value !== 'object') { return value; } // Handle Date instances if (value instanceof Date) { return new Date(value); } // Handle RegExp instances if (value instanceof RegExp) { return new RegExp(value.source, value.flags); } // Handle Array instances if (Array.isArray(value)) { return value.map(item => clone(item)); } // Check for circular reference if (cache.has(value)) { return "[Circular Reference]"; } // Add to cache cache.set(value, true); // Create new object const result = {}; // Copy all properties for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { const propValue = value[key]; // Mask sensitive strings if (typeof propValue === 'string') { // Check if key matches sensitive pattern if (Logger.SENSITIVE_PATTERNS.some(pattern => pattern.test(key) || (typeof pattern === 'string' && key.toLowerCase().includes(pattern)))) { result[key] = this.maskToken(propValue); } // Check if value looks like sensitive data else if (propValue.length > 20 && this.looksLikeSensitiveValue(propValue)) { result[key] = this.maskToken(propValue); } else { result[key] = propValue; } } // Recursively process nested objects else if (propValue && typeof propValue === 'object') { result[key] = clone(propValue); } else { result[key] = propValue; } } } return result; }; return clone(obj); } /** * Create a replacer function for JSON.stringify that handles circular references */ circularReplacer() { const seen = new WeakSet(); return (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular Reference]'; } seen.add(value); } return value; }; } /** * Check if a string value looks like a sensitive token */ looksLikeSensitiveValue(value) { // Check if the string matches any sensitive token pattern return Logger.SENSITIVE_PATTERNS.some(pattern => typeof pattern !== 'string' && pattern.test(value)); } /** * Check and mask a string if it appears to contain sensitive data */ maskSensitiveString(str) { if (this.looksLikeSensitiveValue(str)) { return this.maskToken(str); } return str; } /** * Mask a token to show only prefix */ maskToken(token) { if (!token) return 'undefined'; if (token.length <= 10) return '***'; return token.substring(0, 4) + '...' + token.substring(token.length - 4); } /** * Write directly to stderr to avoid console redirection issues */ writeToStderr(prefix, formattedParts) { // Join all parts with spaces, handling objects and arrays const joinedMessage = formattedParts.map(part => typeof part === 'string' ? part : JSON.stringify(part)).join(' '); // Write with the specified prefix process.stderr.write(`${prefix}: ${joinedMessage}\n`); } /** * Log error message */ error(message, ...args) { if (this.level >= LogLevel.ERROR) { this.writeToStderr("ERROR", this.format(message, ...args)); } } /** * Log warning message */ warn(message, ...args) { if (this.level >= LogLevel.WARN) { this.writeToStderr("WARN", this.format(message, ...args)); } } /** * Log info message */ info(message, ...args) { if (this.level >= LogLevel.INFO) { this.writeToStderr("INFO", this.format(message, ...args)); } } /** * Log debug message - only shown when level is DEBUG or higher */ debug(message, ...args) { if (this.level >= LogLevel.DEBUG) { // Add DEBUG prefix to the message const formattedParts = this.format(message, ...args); formattedParts[0] = `[DEBUG] ${formattedParts[0]}`; this.writeToStderr("DEBUG", formattedParts); } } /** * Log trace message - most verbose level */ trace(message, ...args) { if (this.level >= LogLevel.TRACE) { // Add TRACE prefix to the message const formattedParts = this.format(message, ...args); formattedParts[0] = `[TRACE] ${formattedParts[0]}`; this.writeToStderr("TRACE", formattedParts); } } } // Export a singleton instance with configuration from environment export const logger = new Logger({ name: 'nexus-bridge', level: (() => { const envLogLevel = process.env.LOG_LEVEL?.toLowerCase(); if (envLogLevel && LOG_LEVEL_MAP[envLogLevel] !== undefined) { return LOG_LEVEL_MAP[envLogLevel]; } return process.env.DEBUG === 'true' ? LogLevel.DEBUG : LogLevel.INFO; })(), enableJsonFormatting: true, maskSensitiveData: process.env.MASK_SENSITIVE_DATA !== 'false', includeTimestamps: process.env.LOG_TIMESTAMPS === 'true' }); //# sourceMappingURL=logger.js.map