@sirmrmarty/n8n-nodes-tmux-orchestrator
Version:
n8n nodes for orchestrating Claude AI agents through tmux sessions
490 lines • 19.6 kB
JavaScript
;
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