doclyft
Version:
CLI for DocLyft - Interactive documentation generator with hosted documentation support
182 lines (181 loc) • 7.06 kB
JavaScript
;
/**
* 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;