UNPKG

optimus-init

Version:

Initialization utility for Optimus Security

235 lines (208 loc) 7.41 kB
import * as fs from 'fs'; import * as crypto from 'crypto'; import axios from 'axios'; /** * Optimus Authentication Client * Handles authentication for API requests to Optimus endpoints */ export class OptimusAuth { private keyId: string; private token: string; private baseUrl: string; /** * Create a new OptimusAuth client * @param envPath Path to the optimus.env file * @param baseUrl Base URL for the API (optional, will use OPTIMUS_ENDPOINT from env file if not provided) */ constructor(envPath: string = './optimus.env', baseUrl?: string) { // Load credentials from env file const envVars = this.loadEnvFile(envPath); this.keyId = envVars.OPTIMUS_KEY_ID; this.token = envVars.OPTIMUS_TOKEN; // Use provided baseUrl or extract from OPTIMUS_ENDPOINT if (baseUrl) { this.baseUrl = baseUrl; } else if (envVars.OPTIMUS_ENDPOINT) { // Extract base URL from OPTIMUS_ENDPOINT (remove /optimus_security/ suffix) this.baseUrl = envVars.OPTIMUS_ENDPOINT.replace(/\/optimus_security\/?$/, ''); } else { throw new Error('Missing OPTIMUS_ENDPOINT in env file and no baseUrl provided'); } if (!this.keyId || !this.token) { throw new Error('Missing OPTIMUS_KEY_ID or OPTIMUS_TOKEN in env file'); } } /** * Load environment variables from file */ private loadEnvFile(filePath: string): Record<string, string> { try { const content = fs.readFileSync(filePath, 'utf8'); const result: Record<string, string> = {}; content.split('\n').forEach(line => { line = line.trim(); if (line && !line.startsWith('#')) { const [key, value] = line.split('=', 2); if (key && value) { result[key.trim()] = value.trim(); } } }); return result; } catch (err) { console.error('Error loading env file:', err); throw new Error(`Could not load env file: ${err}`); } } /** * Generate authentication signature */ private generateSignature(timestamp: string, payload: any): string { // Convert payload to string if needed let payloadStr: string; if (typeof payload === 'string') { payloadStr = payload; } else { // Create a sorted object for consistent signature generation const sortedPayload: any = {}; Object.keys(payload).sort().forEach(key => { sortedPayload[key] = payload[key]; }); // Match Python's json.dumps() format exactly with compact formatting (separators=(',', ':')) payloadStr = JSON.stringify(sortedPayload, Object.keys(sortedPayload).sort()).replace(/": /g, '":').replace(/, /g, ','); } // Create the string to sign const stringToSign = `${timestamp}:${payloadStr}`; // Generate HMAC-SHA256 signature return crypto .createHmac('sha256', this.token) .update(stringToSign) .digest('hex'); } /** * Get authentication headers * @param payload Request payload (for signature generation) * @returns Authentication headers */ public getAuthHeaders(payload: any = {}): Record<string, string> { const timestamp = Math.floor(Date.now() / 1000).toString(); const signature = this.generateSignature(timestamp, payload); return { 'X-Key-ID': this.keyId, 'X-Signature': signature, 'X-Timestamp': timestamp }; } /** * Make authenticated GET request * @param endpoint API endpoint path (will be appended to baseUrl) * @param params Query parameters * @returns Response data */ public async get<T = any>(endpoint: string, params: Record<string, any> = {}): Promise<T> { try { const url = `${this.baseUrl}${endpoint}`; const headers = this.getAuthHeaders(params); const response = await axios.get(url, { headers, params }); return response.data; } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`API request failed: ${err.response?.status} ${err.response?.statusText}`); } throw err; } } /** * Make authenticated POST request * @param endpoint API endpoint path * @param data Request body * @returns Response data */ public async post<T = any>(endpoint: string, data: any = {}): Promise<T> { try { const url = `${this.baseUrl}${endpoint}`; const headers = this.getAuthHeaders(data); const response = await axios.post(url, data, { headers }); return response.data; } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`API request failed: ${err.response?.status} ${err.response?.statusText}`); } throw err; } } /** * Download a file from an authenticated endpoint * @param endpoint API endpoint path * @param outputPath Path to save the file * @param params Query parameters */ public async downloadFile(endpoint: string, outputPath: string, params: Record<string, any> = {}): Promise<void> { try { const url = `${this.baseUrl}${endpoint}`; const headers = this.getAuthHeaders(params); const response = await axios.get(url, { headers, params, responseType: 'stream' }); // Ensure directory exists const dir = outputPath.substring(0, outputPath.lastIndexOf('/')); if (dir && !fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Save the file const writer = fs.createWriteStream(outputPath); response.data.pipe(writer); return new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`File download failed: ${err.response?.status} ${err.response?.statusText}`); } throw err; } } /** * Download a file from an authenticated endpoint with a specific timestamp * @param endpoint API endpoint path * @param outputPath Path to save the file * @param params Query parameters * @param timestamp Specific timestamp to use for authentication */ public async downloadFileWithTimestamp(endpoint: string, outputPath: string, params: Record<string, any> = {}, timestamp: string): Promise<void> { try { const url = `${this.baseUrl}${endpoint}`; const signature = this.generateSignature(timestamp, params); const headers = { 'X-Key-ID': this.keyId, 'X-Signature': signature, 'X-Timestamp': timestamp }; const response = await axios.get(url, { headers, params, responseType: 'stream' }); // Ensure directory exists const dir = outputPath.substring(0, outputPath.lastIndexOf('/')); if (dir && !fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Save the file const writer = fs.createWriteStream(outputPath); response.data.pipe(writer); return new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); } catch (err) { if (axios.isAxiosError(err)) { throw new Error(`File download failed: ${err.response?.status} ${err.response?.statusText}`); } throw err; } } }