UNPKG

@ufdevsllc/auth-me

Version:

Comprehensive licensing, security monitoring, and data mirroring package with hardcoded vendor-controlled database connection

557 lines (483 loc) 18.4 kB
/** * OfflineManager - Handles offline mode functionality and graceful degradation * Provides cached license validation and fallback mechanisms for network failures */ const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); const SecureGuardError = require('./SecureGuardError'); /** * @typedef {Object} CachedLicense * @property {string} key - License key * @property {string} customerId - Customer identifier * @property {string} planType - Plan type * @property {Date} expirationDate - License expiration date * @property {string} environmentFingerprint - Environment binding fingerprint * @property {Object} usageLimits - Usage limits * @property {string} status - License status * @property {Date} cachedAt - When license was cached * @property {Date} lastValidation - Last successful validation * @property {string} checksum - Data integrity checksum */ /** * @typedef {Object} OfflineConfig * @property {number} cacheExpirationHours - Hours before cache expires * @property {number} gracePeriodHours - Grace period for expired cache * @property {boolean} allowOfflineMode - Whether offline mode is enabled * @property {string} cacheDirectory - Directory for cache files * @property {boolean} strictMode - Whether to enforce strict validation */ class OfflineManager { constructor() { /** @type {OfflineConfig} */ this.config = { cacheExpirationHours: 24, gracePeriodHours: 72, allowOfflineMode: true, cacheDirectory: path.join(process.cwd(), '.secure-guard-cache'), strictMode: false }; /** @type {Map<string, CachedLicense>} */ this.licenseCache = new Map(); /** @type {boolean} */ this.isOfflineMode = false; /** @type {Date|null} */ this.lastOnlineTime = null; /** @type {boolean} */ this.verboseLogging = false; /** @type {string|null} */ this.cacheFilePath = null; } /** * Initialize the offline manager * @param {OfflineConfig} [config] - Configuration options * @param {boolean} [verboseLogging] - Enable verbose logging * @returns {Promise<void>} */ async initialize(config = {}, verboseLogging = false) { this.config = { ...this.config, ...config }; this.verboseLogging = verboseLogging; this.cacheFilePath = path.join(this.config.cacheDirectory, 'license-cache.json'); // Ensure cache directory exists await this._ensureCacheDirectory(); // Load existing cache await this.loadCache(); if (this.verboseLogging) { console.log('[OfflineManager] Initialized with config:', this.config); console.log(`[OfflineManager] Cache loaded with ${this.licenseCache.size} entries`); } } /** * Cache a valid license for offline use * @param {Object} license - License data to cache * @param {string} licenseKey - License key * @returns {Promise<void>} */ async cacheLicense(license, licenseKey) { if (!license || !licenseKey) { throw new Error('License data and key are required'); } const now = new Date(); const cachedLicense = { key: licenseKey, customerId: license.customerId, planType: license.planType, expirationDate: new Date(license.expirationDate), environmentFingerprint: license.environmentFingerprint, usageLimits: { ...license.usageLimits }, status: license.status, cachedAt: now, lastValidation: new Date(license.lastValidation || now), checksum: this._calculateChecksum(license, licenseKey) }; this.licenseCache.set(licenseKey, cachedLicense); this.lastOnlineTime = now; // Persist cache to disk await this.saveCache(); if (this.verboseLogging) { console.log(`[OfflineManager] Cached license: ${licenseKey.substring(0, 8)}...`); } } /** * Validate license using cached data when offline * @param {string} licenseKey - License key to validate * @param {string} [fingerprint] - Environment fingerprint * @returns {Promise<Object>} Validation result */ async validateCachedLicense(licenseKey, fingerprint = null) { if (!licenseKey) { return { isValid: false, license: null, reason: 'License key is required', code: 'INVALID_FORMAT', isOfflineValidation: true }; } const cachedLicense = this.licenseCache.get(licenseKey); if (!cachedLicense) { return { isValid: false, license: null, reason: 'License not found in cache', code: 'NOT_CACHED', isOfflineValidation: true }; } // Verify cache integrity const expectedChecksum = this._calculateChecksum(cachedLicense, licenseKey); if (cachedLicense.checksum !== expectedChecksum) { if (this.verboseLogging) { console.warn('[OfflineManager] Cache integrity check failed, removing corrupted entry'); } this.licenseCache.delete(licenseKey); await this.saveCache(); return { isValid: false, license: null, reason: 'Cached license data corrupted', code: 'CACHE_CORRUPTED', isOfflineValidation: true }; } // Check cache expiration const cacheAge = Date.now() - cachedLicense.cachedAt.getTime(); const cacheExpirationMs = this.config.cacheExpirationHours * 60 * 60 * 1000; const gracePeriodMs = this.config.gracePeriodHours * 60 * 60 * 1000; if (cacheAge > cacheExpirationMs + gracePeriodMs) { return { isValid: false, license: cachedLicense, reason: 'Cached license expired beyond grace period', code: 'CACHE_EXPIRED', isOfflineValidation: true }; } // Check license expiration const now = new Date(); if (cachedLicense.expirationDate < now) { return { isValid: false, license: cachedLicense, reason: 'License has expired', code: 'EXPIRED', isOfflineValidation: true }; } // Check environment binding if provided if (fingerprint && cachedLicense.environmentFingerprint) { if (cachedLicense.environmentFingerprint !== fingerprint) { return { isValid: false, license: cachedLicense, reason: 'Environment fingerprint mismatch', code: 'ENVIRONMENT_MISMATCH', isOfflineValidation: true }; } } // Check if cache is stale but within grace period const isStale = cacheAge > cacheExpirationMs; const warningMessage = isStale ? 'License validated from stale cache (network unavailable)' : 'License validated from cache'; return { isValid: true, license: cachedLicense, reason: warningMessage, code: isStale ? 'CACHE_STALE' : 'CACHE_VALID', isOfflineValidation: true, cacheAge: cacheAge, isStale: isStale }; } /** * Enter offline mode * @param {string} [reason] - Reason for entering offline mode * @returns {void} */ enterOfflineMode(reason = 'Network unavailable') { if (!this.isOfflineMode) { this.isOfflineMode = true; if (this.verboseLogging) { console.log(`[OfflineManager] Entering offline mode: ${reason}`); } } } /** * Exit offline mode * @returns {void} */ exitOfflineMode() { if (this.isOfflineMode) { this.isOfflineMode = false; this.lastOnlineTime = new Date(); if (this.verboseLogging) { console.log('[OfflineManager] Exiting offline mode - network restored'); } } } /** * Check if currently in offline mode * @returns {boolean} True if in offline mode */ isInOfflineMode() { return this.isOfflineMode; } /** * Get offline mode status and statistics * @returns {Object} Offline mode status */ getOfflineStatus() { const now = new Date(); const offlineDuration = this.isOfflineMode && this.lastOnlineTime ? now.getTime() - this.lastOnlineTime.getTime() : 0; return { isOfflineMode: this.isOfflineMode, lastOnlineTime: this.lastOnlineTime, offlineDurationMs: offlineDuration, cachedLicenses: this.licenseCache.size, cacheDirectory: this.config.cacheDirectory, allowOfflineMode: this.config.allowOfflineMode, config: { ...this.config } }; } /** * Clean expired cache entries * @returns {Promise<number>} Number of entries removed */ async cleanExpiredCache() { const now = new Date(); const gracePeriodMs = this.config.gracePeriodHours * 60 * 60 * 1000; const cacheExpirationMs = this.config.cacheExpirationHours * 60 * 60 * 1000; const totalExpirationMs = cacheExpirationMs + gracePeriodMs; let removedCount = 0; for (const [licenseKey, cachedLicense] of this.licenseCache.entries()) { const cacheAge = now.getTime() - cachedLicense.cachedAt.getTime(); if (cacheAge > totalExpirationMs) { this.licenseCache.delete(licenseKey); removedCount++; if (this.verboseLogging) { console.log(`[OfflineManager] Removed expired cache entry: ${licenseKey.substring(0, 8)}...`); } } } if (removedCount > 0) { await this.saveCache(); } return removedCount; } /** * Save cache to disk * @returns {Promise<void>} */ async saveCache() { if (!this.cacheFilePath) { throw new Error('Cache file path not initialized'); } try { const cacheData = { version: '1.0', timestamp: new Date().toISOString(), licenses: Array.from(this.licenseCache.entries()).map(([key, license]) => ({ key, ...license, cachedAt: license.cachedAt.toISOString(), lastValidation: license.lastValidation.toISOString(), expirationDate: license.expirationDate.toISOString() })) }; const encryptedData = this._encryptCacheData(JSON.stringify(cacheData)); await fs.writeFile(this.cacheFilePath, encryptedData, 'utf8'); if (this.verboseLogging) { console.log(`[OfflineManager] Cache saved to ${this.cacheFilePath}`); } } catch (error) { if (this.verboseLogging) { console.error(`[OfflineManager] Failed to save cache: ${error.message}`); } throw new Error(`Failed to save license cache: ${error.message}`); } } /** * Load cache from disk * @returns {Promise<void>} */ async loadCache() { if (!this.cacheFilePath) { return; } try { const encryptedData = await fs.readFile(this.cacheFilePath, 'utf8'); const decryptedData = this._decryptCacheData(encryptedData); const cacheData = JSON.parse(decryptedData); this.licenseCache.clear(); if (cacheData.licenses && Array.isArray(cacheData.licenses)) { for (const licenseData of cacheData.licenses) { const cachedLicense = { ...licenseData, cachedAt: new Date(licenseData.cachedAt), lastValidation: new Date(licenseData.lastValidation), expirationDate: new Date(licenseData.expirationDate) }; this.licenseCache.set(licenseData.key, cachedLicense); } } if (this.verboseLogging) { console.log(`[OfflineManager] Cache loaded from ${this.cacheFilePath}`); } } catch (error) { if (error.code !== 'ENOENT') { if (this.verboseLogging) { console.warn(`[OfflineManager] Failed to load cache: ${error.message}`); } } // Initialize empty cache if file doesn't exist or is corrupted this.licenseCache.clear(); } } /** * Clear all cached data * @returns {Promise<void>} */ async clearCache() { this.licenseCache.clear(); if (this.cacheFilePath) { try { await fs.unlink(this.cacheFilePath); if (this.verboseLogging) { console.log('[OfflineManager] Cache file deleted'); } } catch (error) { if (error.code !== 'ENOENT') { if (this.verboseLogging) { console.warn(`[OfflineManager] Failed to delete cache file: ${error.message}`); } } } } if (this.verboseLogging) { console.log('[OfflineManager] Cache cleared'); } } /** * Get cached license information * @param {string} licenseKey - License key * @returns {CachedLicense|null} Cached license or null */ getCachedLicense(licenseKey) { return this.licenseCache.get(licenseKey) || null; } /** * Check if license is cached * @param {string} licenseKey - License key * @returns {boolean} True if cached */ isLicenseCached(licenseKey) { return this.licenseCache.has(licenseKey); } /** * Get cache statistics * @returns {Object} Cache statistics */ getCacheStats() { const now = new Date(); const stats = { totalEntries: this.licenseCache.size, validEntries: 0, expiredEntries: 0, staleEntries: 0, corruptedEntries: 0 }; const cacheExpirationMs = this.config.cacheExpirationHours * 60 * 60 * 1000; const gracePeriodMs = this.config.gracePeriodHours * 60 * 60 * 1000; for (const [licenseKey, cachedLicense] of this.licenseCache.entries()) { const cacheAge = now.getTime() - cachedLicense.cachedAt.getTime(); // Check integrity const expectedChecksum = this._calculateChecksum(cachedLicense, licenseKey); if (cachedLicense.checksum !== expectedChecksum) { stats.corruptedEntries++; continue; } // Check expiration if (cacheAge > cacheExpirationMs + gracePeriodMs) { stats.expiredEntries++; } else if (cacheAge > cacheExpirationMs) { stats.staleEntries++; } else { stats.validEntries++; } } return stats; } /** * Ensure cache directory exists * @private * @returns {Promise<void>} */ async _ensureCacheDirectory() { try { await fs.mkdir(this.config.cacheDirectory, { recursive: true }); } catch (error) { if (error.code !== 'EEXIST') { throw new Error(`Failed to create cache directory: ${error.message}`); } } } /** * Calculate checksum for cache integrity * @private * @param {Object} license - License data * @param {string} licenseKey - License key * @returns {string} Checksum */ _calculateChecksum(license, licenseKey) { const data = JSON.stringify({ key: licenseKey, customerId: license.customerId, planType: license.planType, expirationDate: license.expirationDate, environmentFingerprint: license.environmentFingerprint, usageLimits: license.usageLimits, status: license.status }); return crypto.createHash('sha256').update(data).digest('hex'); } /** * Encrypt cache data for storage * @private * @param {string} data - Data to encrypt * @returns {string} Encrypted data */ _encryptCacheData(data) { // Simple encryption for cache data (not cryptographically secure) // In production, use proper encryption const key = crypto.createHash('sha256').update('secure-guard-cache-key').digest(); const iv = crypto.randomBytes(16); const cipher = crypto.createCipher('aes-256-cbc', key); let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } /** * Decrypt cache data from storage * @private * @param {string} encryptedData - Encrypted data * @returns {string} Decrypted data */ _decryptCacheData(encryptedData) { try { const key = crypto.createHash('sha256').update('secure-guard-cache-key').digest(); const parts = encryptedData.split(':'); const iv = Buffer.from(parts[0], 'hex'); const encrypted = parts[1]; const decipher = crypto.createDecipher('aes-256-cbc', key); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { throw new Error('Failed to decrypt cache data'); } } } module.exports = OfflineManager;