optimus-init
Version:
Initialization utility for Optimus Security
235 lines (208 loc) • 7.41 kB
text/typescript
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;
}
}
}