UNPKG

niledatabase

Version:

Command line interface for Nile databases

240 lines (239 loc) 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Auth = void 0; const http_1 = __importDefault(require("http")); const open_1 = __importDefault(require("open")); const axios_1 = __importDefault(require("axios")); const crypto_1 = __importDefault(require("crypto")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); class Auth { static generateCodeVerifier() { const buffer = crypto_1.default.randomBytes(32); return buffer .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, '') .substring(0, 128); } static async generateCodeChallenge(verifier) { const buffer = Buffer.from(verifier); const hash = crypto_1.default.createHash('sha256').update(buffer).digest(); return hash .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } static generateState() { return crypto_1.default.randomBytes(4).toString('hex'); } static async findAvailablePort() { return new Promise((resolve, reject) => { const server = http_1.default.createServer(); server.listen(0, () => { const address = server.address(); if (address && typeof address === 'object') { const port = address.port; server.close(() => resolve(port)); } else { server.close(() => reject(new Error('Could not find available port'))); } }); }); } static getAuthUrl(configManager) { const authDomain = configManager.getAuthUrl() || 'console.thenile.dev'; return `https://${authDomain}/authorize`; } static getTokenUrl(configManager) { const domain = configManager.getGlobalHost(); return `https://${domain}/oauth2/token`; } static getCallbackHtml() { try { let html = fs_1.default.readFileSync(path_1.default.join(__dirname, 'callback.html'), 'utf8'); const logoPath = path_1.default.join(__dirname, 'logo.jpg'); const logoBase64 = fs_1.default.readFileSync(logoPath, 'base64'); html = html.replace('LOGO_BASE64_PLACEHOLDER', logoBase64); return html; } catch { // Fallback HTML if file not found return ` <!DOCTYPE html> <html> <body style="font-family: sans-serif; text-align: center; padding: 2rem;"> <h1 style="color: #2C7A7B;">Authentication Successful</h1> <p>You can close this window and return to your terminal.</p> </body> </html> `; } } static async getAuthorizationToken(configManager) { if (configManager.getDebug()) { console.log('Debug - Starting auth with config:', { clientId: Auth.DEFAULT_CLIENT_ID, authUrl: configManager.getAuthUrl(), tokenUrl: this.getTokenUrl(configManager), debug: configManager.getDebug() }); } return new Promise((resolve, reject) => { const codeVerifier = this.generateCodeVerifier(); const state = this.generateState(); // Start the process this.findAvailablePort() .then(port => { if (configManager.getDebug()) { console.log('Debug - Auth parameters:', { clientId: Auth.DEFAULT_CLIENT_ID, authUrl: this.getAuthUrl(configManager), tokenUrl: this.getTokenUrl(configManager), port, codeVerifier: codeVerifier.substring(0, 5) + '...', state }); } const server = http_1.default.createServer(async (req, res) => { if (configManager.getDebug()) { console.log('Debug - Received callback request:', req.url); } // Immediately return for favicon requests if (req.url === '/favicon.ico') { res.writeHead(204); res.end(); return; } // Only process /callback path if (!req.url || !req.url.includes('/callback')) { res.writeHead(404); res.end('Not found'); return; } const url = new URL(req.url, `http://${req.headers.host}`); const code = url.searchParams.get('code'); const returnedState = url.searchParams.get('state'); const error = url.searchParams.get('error'); const errorDescription = url.searchParams.get('error_description'); // Close server and respond with error const closeServerAndReject = (status, message, error) => { res.writeHead(status, { 'Content-Type': 'text/html' }); res.end(message); server.close(() => { if (error) { reject(error); } }); }; // Handle authentication errors if (error) { closeServerAndReject(400, `Authentication failed: ${errorDescription || error}`, new Error(errorDescription || error)); return; } // Validate state parameter if (returnedState !== state) { if (configManager.getDebug()) { console.log('Debug - State mismatch:', { expected: state, received: returnedState }); } closeServerAndReject(400, 'Authentication failed: Invalid state parameter', new Error('Invalid state parameter')); return; } // Ensure we have an authorization code if (!code) { if (configManager.getDebug()) { console.log('Debug - No code received in callback'); } closeServerAndReject(400, 'Authentication failed: No authorization code received', new Error('No authorization code received')); return; } try { if (configManager.getDebug()) { console.log('Debug - Exchanging code for token...'); } const tokenResponse = await axios_1.default.post(this.getTokenUrl(configManager), new URLSearchParams({ grant_type: 'authorization_code', code, client_id: Auth.DEFAULT_CLIENT_ID, code_verifier: codeVerifier, redirect_uri: `http://localhost:${port}/callback`, }).toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (configManager.getDebug()) { console.log('Debug - Token exchange successful'); } // Send success response with custom HTML res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(this.getCallbackHtml()); // Close server and resolve with token server.close(() => { resolve(tokenResponse.data.access_token); }); } catch (error) { if (configManager.getDebug()) { console.log('Debug - Token exchange failed:', error); if (axios_1.default.isAxiosError(error) && error.response) { console.log('Debug - Error response:', error.response.data); } } closeServerAndReject(400, 'Authentication failed during token exchange. Please try again.', axios_1.default.isAxiosError(error) && error.response ? new Error(`Token exchange failed: ${error.response.data.error_description || error.response.data.error || error.message}`) : error); } }); // Set up server and timeout let timeoutId; server.listen(port, async () => { try { const codeChallenge = await this.generateCodeChallenge(codeVerifier); const params = new URLSearchParams({ client_id: Auth.DEFAULT_CLIENT_ID, state, response_type: 'code', redirect_uri: `http://localhost:${port}/callback`, code_challenge: codeChallenge, code_challenge_method: 'S256' }); const authUrl = `${this.getAuthUrl(configManager)}?${params.toString()}`; if (configManager.getDebug()) { console.log('Debug - Full auth URL:', authUrl); } console.log('Opening authorization URL:', authUrl); await (0, open_1.default)(authUrl); // Set timeout after server starts timeoutId = setTimeout(() => { server.close(); reject(new Error('Authentication timed out after 2 minutes')); }, Auth.AUTH_TIMEOUT); } catch (error) { server.close(); reject(error); } }); // Clean up on server close server.on('close', () => { if (timeoutId) { clearTimeout(timeoutId); } }); }); }); } } exports.Auth = Auth; Auth.DEFAULT_CLIENT_ID = 'nilecli'; Auth.AUTH_TIMEOUT = 120000; // 2 minutes