UNPKG

doclyft

Version:

CLI for DocLyft - Interactive documentation generator with hosted documentation support

182 lines (181 loc) 7.06 kB
"use strict"; /** * Session management service for CLI authentication * Handles token validation, session tracking, and token refresh */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionManager = void 0; const config_1 = __importDefault(require("../services/config")); const api_1 = __importDefault(require("../services/api")); const chalk_1 = __importDefault(require("chalk")); const auth_1 = require("../middleware/auth"); const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const crypto_1 = require("../utils/crypto"); class SessionManager { /** * Load session data from storage */ static async loadSession() { try { const dirPath = path_1.default.dirname(this.sessionFilePath); await fs_1.promises.mkdir(dirPath, { recursive: true }); // Try to read existing session file const encryptedContent = await fs_1.promises.readFile(this.sessionFilePath, 'utf-8'); const sessionContent = (0, crypto_1.decryptSessionData)(encryptedContent); const session = JSON.parse(sessionContent); // Validate session format if (!this.isValidSessionFormat(session)) { await this.clearSession(); return null; } this.sessionData = session; return session; } catch (error) { // If no session file or it's invalid, return null return null; } } /** * Save session data to storage */ static async saveSession(session) { try { const dirPath = path_1.default.dirname(this.sessionFilePath); await fs_1.promises.mkdir(dirPath, { recursive: true }); const sessionContent = JSON.stringify(session, null, 2); const encryptedContent = (0, crypto_1.encryptSessionData)(sessionContent); await fs_1.promises.writeFile(this.sessionFilePath, encryptedContent, 'utf-8'); this.sessionData = session; } catch (error) { console.log(chalk_1.default.yellow('⚠️ Warning: Could not save session data')); } } /** * Create a new session from token and user info */ static async createSession(token, userId, userEmail) { const deviceId = await this.getOrCreateDeviceId(); // Ensure token is trimmed to prevent whitespace issues const cleanToken = token.trim(); const session = { userId, userEmail, token: cleanToken, lastValidated: Date.now(), createdAt: Date.now(), deviceId }; await this.saveSession(session); return session; } /** * Validate an existing session * Checks for expiry and refreshes if needed */ static async validateSession() { const session = this.sessionData || await this.loadSession(); if (!session) { return false; } // Check if session is expired if (Date.now() - session.createdAt > this.SESSION_EXPIRY) { await this.clearSession(); throw new auth_1.AuthError(auth_1.AUTH_ERRORS.SESSION_EXPIRED, 'SESSION_EXPIRED'); } // Security: Always validate API keys on each session check to prevent // deleted/revoked keys from remaining authenticated try { // Validate token with API const isValid = await api_1.default.verifyToken(session.token); if (!isValid) { await this.clearSession(); throw new auth_1.AuthError(auth_1.AUTH_ERRORS.INVALID_TOKEN, 'TOKEN_INVALID'); } // Update last validated timestamp session.lastValidated = Date.now(); await this.saveSession(session); } catch (error) { // Security: Do not allow offline mode fallback for authentication // If we cannot validate the token, the session should be considered invalid await this.clearSession(); if (error instanceof auth_1.AuthError) { throw error; } // Treat network errors as authentication failures for security throw new auth_1.AuthError(auth_1.AUTH_ERRORS.NETWORK_ERROR, 'NETWORK_FAILURE'); } return true; } /** * Clear the current session */ static async clearSession() { try { await fs_1.promises.unlink(this.sessionFilePath); } catch { // Ignore errors if file doesn't exist } this.sessionData = null; // Also clear from config config_1.default.delete('token'); config_1.default.delete('user_id'); config_1.default.delete('user_email'); } /** * Get device ID or create one if it doesn't exist */ static async getOrCreateDeviceId() { const deviceIdFile = path_1.default.join(os_1.default.homedir(), '.doclyft', 'device_id'); try { const dirPath = path_1.default.dirname(deviceIdFile); await fs_1.promises.mkdir(dirPath, { recursive: true }); // Try to read existing device ID try { const deviceId = await fs_1.promises.readFile(deviceIdFile, 'utf-8'); if (deviceId && deviceId.length > 10) { return deviceId.trim(); } } catch { // No device ID file } // Generate new device ID using cryptographically secure random values const deviceId = (0, crypto_1.generateSecureDeviceId)(); await fs_1.promises.writeFile(deviceIdFile, deviceId, 'utf-8'); return deviceId; } catch (error) { // If we can't create a device ID file, use a secure fallback return 'cli-' + (0, crypto_1.generateSecureDeviceId)().substring(0, 16); } } /** * Validate session format */ static isValidSessionFormat(session) { if (typeof session !== 'object' || session === null) { return false; } const s = session; return (typeof s.userId === 'string' && typeof s.userEmail === 'string' && typeof s.token === 'string' && typeof s.lastValidated === 'number' && typeof s.createdAt === 'number'); } } exports.SessionManager = SessionManager; SessionManager.SESSION_EXPIRY = 14 * 24 * 60 * 60 * 1000; // 14 days // Security: Removed VALIDATION_INTERVAL - now validates on every session check SessionManager.sessionData = null; SessionManager.sessionFilePath = path_1.default.join(os_1.default.homedir(), '.doclyft', 'session.json'); exports.default = SessionManager;