UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

490 lines 19.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthenticationLayer = void 0; const crypto = __importStar(require("crypto")); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const commandSanitizer_1 = require("./commandSanitizer"); const resourceManager_1 = require("./resourceManager"); const threadSafeState_1 = require("./threadSafeState"); class AuthenticationLayer { static generateApiKey() { return crypto.randomBytes(this.API_KEY_LENGTH / 2).toString('hex'); } static generateSessionToken() { return crypto.randomBytes(this.SESSION_TOKEN_LENGTH / 2).toString('hex'); } static async createUserCredentials(userId, permissions) { if (!userId || typeof userId !== 'string') { throw new Error('Invalid user ID'); } const userValidation = commandSanitizer_1.CommandSanitizer.validateProjectName(userId); if (!userValidation.isValid) { throw new Error(`Invalid user ID format: ${userValidation.errors.join(', ')}`); } if (!Array.isArray(permissions)) { throw new Error('Permissions must be an array'); } const apiKey = this.generateApiKey(); const credentialsDir = '/var/n8n/auth'; try { await fs.promises.access(credentialsDir); } catch { await fs.promises.mkdir(credentialsDir, { recursive: true, mode: 0o700 }); } const credentialsPath = path.join(credentialsDir, `${userId}.json`); const credentials = { userId, apiKeyHash: crypto.createHash('sha256').update(apiKey).digest('hex'), permissions, createdAt: Date.now(), lastUsed: null }; await fs.promises.writeFile(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 }); return { apiKey, userId, permissions }; } static async authenticateUser(userId, apiKey) { try { if (!userId || !apiKey) { return { authenticated: false, authorized: false, reason: 'Missing credentials' }; } const cacheKey = `${userId}:${crypto.createHash('sha256').update(apiKey).digest('hex').substring(0, 16)}`; const cached = this.authCache.safeGet(cacheKey); if (cached && (Date.now() - cached.cachedAt) < this.AUTH_CACHE_DURATION) { return cached.result; } const credentialsDir = '/var/n8n/auth'; const credentialsPath = path.join(credentialsDir, `${userId}.json`); try { await fs.promises.access(credentialsPath); } catch { const result = { authenticated: false, authorized: false, reason: 'User not found' }; await this.authCache.safeSet(cacheKey, { result, cachedAt: Date.now() }); return result; } const credentials = JSON.parse(await fs.promises.readFile(credentialsPath, 'utf8')); const providedKeyHash = crypto.createHash('sha256').update(apiKey).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(credentials.apiKeyHash, 'hex'), Buffer.from(providedKeyHash, 'hex'))) { const result = { authenticated: false, authorized: false, reason: 'Invalid API key' }; try { await this.authCache.safeSet(cacheKey, { result, cachedAt: Date.now() }); } catch (error) { console.warn('Failed to cache auth result:', error.message); } return result; } const sessionToken = this.generateSessionToken(); const expiresAt = Date.now() + this.SESSION_DURATION; await this.cleanupUserSessions(userId); const sessionCredentials = { userId, apiKey, sessionToken, permissions: credentials.permissions, expiresAt }; try { await this.activeSessions.safeSet(sessionToken, sessionCredentials); } catch (error) { console.error('Failed to store active session:', error.message); throw new Error('Failed to create session'); } credentials.lastUsed = Date.now(); await fs.promises.writeFile(credentialsPath, JSON.stringify(credentials, null, 2), { mode: 0o600 }); const result = { authenticated: true, authorized: true, userId, permissions: credentials.permissions, sessionToken }; try { await this.authCache.safeSet(cacheKey, { result, cachedAt: Date.now() }); } catch (error) { console.warn('Failed to cache auth result:', error.message); } return result; } catch (error) { return { authenticated: false, authorized: false, reason: `Authentication error: ${error.message}` }; } } static authenticateSession(sessionToken) { try { if (!sessionToken) { return { authenticated: false, authorized: false, reason: 'Missing session token' }; } const session = this.activeSessions.safeGet(sessionToken); if (!session) { return { authenticated: false, authorized: false, reason: 'Invalid session token' }; } if (Date.now() > session.expiresAt) { this.activeSessions.safeDelete(sessionToken).catch(error => { console.warn('Failed to delete expired session:', error.message); }); return { authenticated: false, authorized: false, reason: 'Session expired' }; } return { authenticated: true, authorized: true, userId: session.userId, permissions: session.permissions, sessionToken }; } catch (error) { return { authenticated: false, authorized: false, reason: `Session authentication error: ${error.message}` }; } } static authorizeOperation(operation, userPermissions) { try { if (!this.CRITICAL_OPERATIONS.has(operation)) { return { authorized: true }; } const requiredPermissions = this.OPERATION_PERMISSIONS[operation]; if (!requiredPermissions) { return { authorized: false, reason: 'Unknown operation' }; } const hasPermission = requiredPermissions.some(perm => userPermissions.includes(perm)); if (!hasPermission) { return { authorized: false, reason: `Missing required permissions: ${requiredPermissions.join(' or ')}` }; } return { authorized: true }; } catch (error) { return { authorized: false, reason: `Authorization error: ${error.message}` }; } } static async authenticateAndAuthorize(operation, credentials, context) { try { let authResult; if (credentials.sessionToken) { authResult = this.authenticateSession(credentials.sessionToken); } else if (credentials.userId && credentials.apiKey) { authResult = await this.authenticateUser(credentials.userId, credentials.apiKey); } else { return { authenticated: false, authorized: false, reason: 'No valid credentials provided' }; } if (!authResult.authenticated) { return authResult; } const authzResult = this.authorizeOperation(operation, authResult.permissions || []); if (!authzResult.authorized) { return { authenticated: true, authorized: false, userId: authResult.userId, permissions: authResult.permissions, reason: authzResult.reason }; } if (context) { await this.logOperation(authResult.userId, operation, context); } return authResult; } catch (error) { return { authenticated: false, authorized: false, reason: `Authentication/authorization error: ${error.message}` }; } } static async logOperation(userId, operation, context) { try { const logDir = '/var/log/n8n/auth'; try { await fs.promises.access(logDir); } catch { await fs.promises.mkdir(logDir, { recursive: true, mode: 0o700 }); } const logEntry = { timestamp: new Date().toISOString(), userId, operation, context: { resourceId: context.resourceId, requesterId: context.requesterId, metadata: context.metadata }, auditHash: '' }; logEntry.auditHash = crypto .createHash('sha256') .update(JSON.stringify(logEntry, Object.keys(logEntry).sort())) .digest('hex'); const logPath = path.join(logDir, `operations_${new Date().toISOString().split('T')[0]}.log`); await fs.promises.appendFile(logPath, JSON.stringify(logEntry) + '\n', { mode: 0o600 }); } catch (error) { console.warn('Failed to log operation:', error.message); } } static async cleanupUserSessions(userId) { try { const userSessions = this.activeSessions.safeEntries() .filter(([_, session]) => session.userId === userId); for (const [token, session] of userSessions) { if (Date.now() > session.expiresAt) { try { await this.activeSessions.safeDelete(token); } catch (error) { console.warn(`Failed to delete expired session ${token}:`, error.message); } } } const validUserSessions = this.activeSessions.safeEntries() .filter(([_, session]) => session.userId === userId) .sort((a, b) => b[1].expiresAt - a[1].expiresAt); if (validUserSessions.length >= this.MAX_SESSIONS_PER_USER) { const toRemove = validUserSessions.slice(this.MAX_SESSIONS_PER_USER - 1); for (const [token] of toRemove) { try { await this.activeSessions.safeDelete(token); } catch (error) { console.warn(`Failed to delete old session ${token}:`, error.message); } } } } catch (error) { console.error('Failed to cleanup user sessions:', error.message); } } static async revokeSession(sessionToken) { try { return await this.activeSessions.safeDelete(sessionToken); } catch (error) { console.error('Failed to revoke session:', error.message); return false; } } static async revokeUserSessions(userId) { let revokedCount = 0; try { const entries = this.activeSessions.safeEntries(); for (const [token, session] of entries) { if (session.userId === userId) { try { await this.activeSessions.safeDelete(token); revokedCount++; } catch (error) { console.warn(`Failed to revoke session ${token}:`, error.message); } } } } catch (error) { console.error('Failed to revoke user sessions:', error.message); } return revokedCount; } static getSessionStats() { const now = Date.now(); const fiveMinutes = 5 * 60 * 1000; const uniqueUsers = new Set(); let expiringSoon = 0; for (const session of this.activeSessions.safeValues()) { if (now < session.expiresAt) { uniqueUsers.add(session.userId); if (session.expiresAt - now < fiveMinutes) { expiringSoon++; } } } return { activeSessions: this.activeSessions.safeSize(), uniqueUsers: uniqueUsers.size, expiringSoon }; } static async initialize() { try { const authDir = '/var/n8n/auth'; try { await fs.promises.access(authDir); } catch { await fs.promises.mkdir(authDir, { recursive: true, mode: 0o700 }); } const files = await fs.promises.readdir(authDir); const hasAdmin = await Promise.all(files.map(async (file) => { try { const credPath = path.join(authDir, file); const creds = JSON.parse(await fs.promises.readFile(credPath, 'utf8')); return creds.permissions && creds.permissions.includes('admin'); } catch { return false; } })).then(results => results.some(Boolean)); if (!hasAdmin) { const defaultAdminId = 'system_admin'; const defaultPermissions = ['admin']; const { apiKey } = await this.createUserCredentials(defaultAdminId, defaultPermissions); console.log(`Created default admin credentials:`); console.log(`User ID: ${defaultAdminId}`); console.log(`API Key: ${apiKey}`); console.log(`Permissions: ${defaultPermissions.join(', ')}`); console.log(`Store these credentials securely!`); } resourceManager_1.resourceManager.createInterval(async () => { await this.performPeriodicCleanup(); }, 10 * 60 * 1000, 'Authentication periodic cleanup'); } catch (error) { console.warn('Failed to initialize authentication system:', error.message); } } static async performPeriodicCleanup() { const now = Date.now(); try { const sessionEntries = this.activeSessions.safeEntries(); for (const [token, session] of sessionEntries) { if (now > session.expiresAt) { try { await this.activeSessions.safeDelete(token); } catch (error) { console.warn(`Failed to delete expired session ${token}:`, error.message); } } } const cacheEntries = this.authCache.safeEntries(); for (const [key, cached] of cacheEntries) { if (now - cached.cachedAt > this.AUTH_CACHE_DURATION) { try { await this.authCache.safeDelete(key); } catch (error) { console.warn(`Failed to delete expired cache entry ${key}:`, error.message); } } } } catch (error) { console.error('Failed to perform periodic cleanup:', error.message); } } } exports.AuthenticationLayer = AuthenticationLayer; AuthenticationLayer.SESSION_DURATION = 2 * 60 * 60 * 1000; AuthenticationLayer.API_KEY_LENGTH = 64; AuthenticationLayer.SESSION_TOKEN_LENGTH = 32; AuthenticationLayer.MAX_SESSIONS_PER_USER = 5; AuthenticationLayer.AUTH_CACHE_DURATION = 5 * 60 * 1000; AuthenticationLayer.activeSessions = threadSafeState_1.stateManager.getMap('activeSessions'); AuthenticationLayer.authCache = threadSafeState_1.stateManager.getMap('authCache'); AuthenticationLayer.CRITICAL_OPERATIONS = new Set([ 'createProject', 'killSession', 'executeScheduledProject', 'addTeamMember', 'removeTeamMember', 'blockCommit', 'approveCommit', 'generateQAKeyPair', 'registerQAEngineer', 'createCryptographicQAApproval' ]); AuthenticationLayer.OPERATION_PERMISSIONS = { 'createProject': ['project.create', 'admin'], 'killSession': ['session.kill', 'admin'], 'executeScheduledProject': ['project.execute', 'admin'], 'addTeamMember': ['team.manage', 'admin'], 'removeTeamMember': ['team.manage', 'admin'], 'blockCommit': ['qa.block', 'qa.engineer', 'admin'], 'approveCommit': ['qa.approve', 'qa.engineer', 'admin'], 'generateQAKeyPair': ['qa.keys', 'admin'], 'registerQAEngineer': ['qa.register', 'admin'], 'createCryptographicQAApproval': ['qa.approve', 'qa.engineer', 'admin'] }; //# sourceMappingURL=authenticationLayer.js.map