@civic/nexus-bridge
Version:
Stdio <-> HTTP/SSE MCP bridge with Civic auth handling
324 lines • 11.8 kB
JavaScript
/* 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