niledatabase
Version:
Command line interface for Nile databases
240 lines (239 loc) • 11.2 kB
JavaScript
;
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