UNPKG

@thomkjel/logger

Version:

Security-focused event logging library for Next.js applications (Work in Progress)

236 lines (235 loc) 10 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Logger = void 0; class Logger { constructor(config) { this.environment = (config?.environment || process.env.NODE_ENV) || 'development'; this.sourceToken = config?.sourceToken || process.env.BETTERSTACK_SOURCE_TOKEN; this.betterStackEndpoint = config?.betterStackEndpoint || process.env.BETTERSTACK_ENDPOINT || 'https://s1285049.eu-nbg-2.betterstackdata.com'; this.config = { environment: this.environment, sourceToken: this.sourceToken, betterStackEndpoint: this.betterStackEndpoint, rateLimitPerMinute: 1000, // Default rate limit for future SaaS enableMetrics: true, ...config }; this.metrics = { totalLogs: 0, logsByLevel: { INFO: 0, WARN: 0, ERROR: 0, CRITICAL: 0 }, logsByCategory: { AUTHN: 0, AUTHZ: 0, SESSION: 0, UPLOAD: 0, INPUT: 0, MALICIOUS: 0, PRIVILEGE: 0, DATA: 0, SEQUENCE: 0, SYS: 0, USER: 0, EXCESS: 0, GENERAL: 0 }, lastLogTimestamp: new Date().toISOString(), apiKeyUsage: {} }; } static getInstance(config) { if (!Logger.instance) { Logger.instance = new Logger(config); } return Logger.instance; } async log(level, eventType, metadata = {}) { const category = this.getCategoryFromEvent(eventType); // Update metrics if enabled if (this.config.enableMetrics) { this.updateMetrics(level, category); } // Future SaaS feature: API key validation and rate limiting if (this.config.apiKey) { const rateLimitCheck = this.checkRateLimit(this.config.apiKey); if (!rateLimitCheck) { console.warn('Rate limit exceeded for API key'); return; } } const logData = { dt: new Date().toISOString(), level: level.toLowerCase(), event: eventType, category: category, message: this.formatMessage(eventType, metadata), context: { environment: this.config.environment || this.environment, ...(this.config.apiKey && { apiKey: this.config.apiKey }), ...metadata, }, }; // In development, log to console if (this.config.environment === 'development') { console.log(JSON.stringify(logData, null, 2)); } // In production, send to Better Stack - but don't block if (this.config.sourceToken) { this.sendToBetterStack(logData).catch((error) => { console.error('Logging error:', error.message); }); } } async sendToBetterStack(logData) { const response = await fetch(this.config.betterStackEndpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.sourceToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify(logData), }); if (!response.ok) { throw new Error(`Better Stack logging failed: ${response.statusText}`); } } updateMetrics(level, category) { this.metrics.totalLogs++; this.metrics.logsByLevel[level]++; this.metrics.logsByCategory[category]++; this.metrics.lastLogTimestamp = new Date().toISOString(); // Update API key usage metrics if applicable if (this.config.apiKey && this.metrics.apiKeyUsage) { this.metrics.apiKeyUsage[this.config.apiKey] = (this.metrics.apiKeyUsage[this.config.apiKey] || 0) + 1; } } checkRateLimit(apiKey) { // Future SaaS feature: implement actual rate limiting logic // For now, always return true return true; } getCategoryFromEvent(eventType) { if (eventType.startsWith('authn_')) return 'AUTHN'; if (eventType.startsWith('authz_')) return 'AUTHZ'; if (eventType.startsWith('session_')) return 'SESSION'; if (eventType.startsWith('upload_')) return 'UPLOAD'; if (eventType.startsWith('input_')) return 'INPUT'; if (eventType.startsWith('malicious_')) return 'MALICIOUS'; if (eventType.startsWith('privilege_')) return 'PRIVILEGE'; if (eventType.startsWith('sensitive_')) return 'DATA'; if (eventType.startsWith('sequence_')) return 'SEQUENCE'; if (eventType.startsWith('sys_')) return 'SYS'; if (eventType.startsWith('user_')) return 'USER'; if (eventType.startsWith('excess_')) return 'EXCESS'; return 'GENERAL'; } formatMessage(eventType, metadata) { switch (eventType) { // Authentication events case 'authn_login_success': return `User ${metadata.userid} logged in successfully`; case 'authn_login_successafterfail': return `User ${metadata.userid} logged in successfully after ${metadata.retries || 'multiple'} failed attempts`; case 'authn_login_fail': return `User ${metadata.userid} login failed`; case 'authn_login_fail_max': return `User ${metadata.userid} reached the login fail limit of ${metadata.maxlimit}`; case 'authn_login_lock': return `User ${metadata.userid} login locked because ${metadata.reason || 'unknown reason'}`; case 'authn_password_change': return `User ${metadata.userid} has successfully changed their password`; case 'authn_password_change_fail': return `User ${metadata.userid} failed to change their password`; case 'authn_impossible_travel': return `User ${metadata.userid} has accessed the application in two distant regions: ${metadata.region1 || 'unknown'} and ${metadata.region2 || 'unknown'}`; case 'authn_token_created': return `A token has been created for ${metadata.userid} with ${metadata.entitlement || 'unknown'} entitlements`; case 'authn_token_revoked': return `Token ID: ${metadata.tokenid || 'unknown'} was revoked for user ${metadata.userid}`; case 'authn_token_reuse': return `User ${metadata.userid} attempted to use token ID: ${metadata.tokenid || 'unknown'} which was previously revoked`; case 'authn_token_delete': return `The token for ${metadata.appid || metadata.userid || 'unknown'} has been deleted`; // Session events case 'session_created': return `User ${metadata.userid} has started a new session`; case 'session_renewed': return `User ${metadata.userid} was warned of expiring session and extended it`; case 'session_expired': return `User ${metadata.userid} session expired due to ${metadata.reason || 'timeout'}`; case 'session_use_after_expire': return `User ${metadata.userid} attempted access after session expired`; // Authorization events case 'authz_fail': return `User ${metadata.userid} attempted to access ${metadata.resource || 'a resource'} without entitlement`; case 'authz_change': return `User ${metadata.userid} access was changed from ${metadata.from || 'previous role'} to ${metadata.to || 'new role'}`; case 'authz_admin': return `Administrator ${metadata.userid} has performed ${metadata.event || 'an administrative action'}`; // User management events case 'user_created': return `User ${metadata.userid} created ${metadata.newuserid} with ${metadata.attributes || 'default'} privilege attributes`; case 'user_updated': return `User ${metadata.userid} updated ${metadata.onuserid} with attributes ${metadata.attributes || 'unknown'}`; case 'user_archived': return `User ${metadata.userid} archived ${metadata.onuserid}`; case 'user_deleted': return `User ${metadata.userid} has deleted ${metadata.onuserid}`; // Default case for events without specific formatting default: return eventType; } } // Basic logging methods info(eventType, metadata) { this.log('INFO', eventType, metadata); } warn(eventType, metadata) { this.log('WARN', eventType, metadata); } error(eventType, metadata) { this.log('ERROR', eventType, metadata); } critical(eventType, metadata) { this.log('CRITICAL', eventType, metadata); } // Future SaaS features getMetrics() { return { ...this.metrics }; } configure(config) { this.config = { ...this.config, ...config }; } // Reset metrics (useful for testing or periodic resets) resetMetrics() { this.metrics = { totalLogs: 0, logsByLevel: { INFO: 0, WARN: 0, ERROR: 0, CRITICAL: 0 }, logsByCategory: { AUTHN: 0, AUTHZ: 0, SESSION: 0, UPLOAD: 0, INPUT: 0, MALICIOUS: 0, PRIVILEGE: 0, DATA: 0, SEQUENCE: 0, SYS: 0, USER: 0, EXCESS: 0, GENERAL: 0 }, lastLogTimestamp: new Date().toISOString(), apiKeyUsage: {} }; } } exports.Logger = Logger;