UNPKG

@pimzino/claude-code-spec-workflow

Version:

Automated workflows for Claude Code. Includes spec-driven development (Requirements → Design → Tasks → Implementation) with intelligent task execution, optional steering documents and streamlined bug fix workflow (Report → Analyze → Fix → Verify). We have

234 lines 7.32 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccessController = void 0; const crypto_1 = __importDefault(require("crypto")); class AccessController { constructor(_options = { readOnlyMode: false, rateLimitAttempts: 5, rateLimitWindow: 60000 }) { this._options = _options; this.passwords = new Map(); this.rateLimits = new Map(); this.readOnlySessions = new Set(); this.sessions = new Map(); this.MAX_ATTEMPTS = 5; this.RATE_LIMIT_WINDOW = 60000; // 1 minute this.SESSION_DURATION = 3600000; // 1 hour /** * Middleware to enforce read-only access */ this.enforceReadOnly = async (req, reply) => { // Skip if not in read-only mode if (!this._options.readOnlyMode) { return; } // Allow GET requests and WebSocket connections if (req.method === 'GET' || req.url === '/ws') { return; } // Block all other methods reply.code(403).send({ error: 'Read-only access', message: 'This dashboard is in read-only mode. Only viewing is allowed.' }); }; } /** * Set password for a tunnel */ setPassword(tunnelId, password) { const hash = this.hashPassword(password); this.passwords.set(tunnelId, { hash, attempts: 0, lastAttempt: 0 }); } /** * Validate password for a tunnel */ validatePassword(tunnelId, password, clientIp) { const entry = this.passwords.get(tunnelId); if (!entry) return true; // No password set // Check rate limit if (clientIp && !this.checkRateLimit(clientIp)) { throw new Error('Too many attempts. Please try again later.'); } const hash = this.hashPassword(password); const isValid = hash === entry.hash; if (!isValid) { entry.attempts++; entry.lastAttempt = Date.now(); if (clientIp) { this.incrementRateLimit(clientIp); } } return isValid; } /** * Create a read-only WebSocket wrapper */ wrapWebSocketForReadOnly(socket, sessionId) { if (!this._options.readOnlyMode) return; // Mark session as read-only this.readOnlySessions.add(sessionId); // Wrap the send method to add readOnly flag to messages const originalSend = socket.send.bind(socket); socket.send = (data) => { try { const message = typeof data === 'string' ? JSON.parse(data) : data; message.readOnly = true; originalSend(JSON.stringify(message)); } catch { // If we can't parse it, send as-is originalSend(data); } }; console.log(`WebSocket ${sessionId} marked as read-only`); // Clean up on close socket.on('close', () => { this.readOnlySessions.delete(sessionId); }); } /** * Check if a session is read-only */ isReadOnlySession(sessionId) { return this.readOnlySessions.has(sessionId); } /** * Filter WebSocket messages for read-only mode */ filterWebSocketMessage(data) { try { const str = data.toString(); const message = JSON.parse(str); // Filter out any interactive or write-related messages if (message.type === 'command' || message.type === 'action' || message.type === 'write') { return null; } // Add read-only indicator to messages if (message.type === 'initial' || message.type === 'update') { message.readOnly = true; } return JSON.stringify(message); } catch { // If we can't parse it, don't send it in read-only mode return null; } } /** * Hash password using SHA-256 */ hashPassword(password) { return crypto_1.default.createHash('sha256').update(password).digest('hex'); } /** * Check rate limit for an IP */ checkRateLimit(ip) { const entry = this.rateLimits.get(ip); const now = Date.now(); if (!entry || now > entry.resetAt) { return true; } return entry.count < (this._options.rateLimitAttempts || this.MAX_ATTEMPTS); } /** * Increment rate limit counter */ incrementRateLimit(ip) { const now = Date.now(); const entry = this.rateLimits.get(ip); if (!entry || now > entry.resetAt) { this.rateLimits.set(ip, { count: 1, resetAt: now + (this._options.rateLimitWindow || this.RATE_LIMIT_WINDOW) }); } else { entry.count++; } } /** * Clean up expired rate limit entries */ cleanupRateLimits() { const now = Date.now(); for (const [ip, entry] of this.rateLimits.entries()) { if (now > entry.resetAt) { this.rateLimits.delete(ip); } } } /** * Create a new session after successful authentication */ createSession(tunnelId) { const sessionToken = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const now = Date.now(); this.sessions.set(sessionToken, { tunnelId, createdAt: now, expiresAt: now + this.SESSION_DURATION }); // Clean up expired sessions periodically this.cleanupSessions(); return sessionToken; } /** * Validate a session token */ validateSession(sessionToken, tunnelId) { const session = this.sessions.get(sessionToken); if (!session) { return false; } const now = Date.now(); // Check if session is expired if (now > session.expiresAt) { this.sessions.delete(sessionToken); return false; } // Check if session is for the correct tunnel if (session.tunnelId !== tunnelId) { return false; } return true; } /** * Clean up expired sessions */ cleanupSessions() { const now = Date.now(); for (const [token, session] of this.sessions.entries()) { if (now > session.expiresAt) { this.sessions.delete(token); } } } /** * Get access statistics */ getStats() { return { activeSessions: this.readOnlySessions.size, authenticatedSessions: this.sessions.size, protectedTunnels: this.passwords.size, rateLimitedIps: this.rateLimits.size }; } } exports.AccessController = AccessController; //# sourceMappingURL=access-controller.js.map