import { APICredentials, FileContent } from '../utils/types.js';
import { RestApiError, CommandNotAllowedError } from '../utils/errors.js';
import axios, { AxiosInstance } from 'axios';
const ALLOWED_COMMANDS = new Set([
'graph:open',
'graph:open-local',
'editor:toggle-source',
'app:go-back',
'app:go-forward',
'global-search:open',
'workspace:new-file',
'file-explorer:reveal-active-file',
'workspace:split-vertical',
'workspace:split-horizontal',
]);
export class RestApiProvider {
private clients: Map<string, AxiosInstance> = new Map();
private getClient(credentials: APICredentials): AxiosInstance {
const key = `${credentials.host}:${credentials.port}`;
if (!this.clients.has(key)) {
const baseURL = credentials.useSecure
? `https://${credentials.host}:${credentials.port}`
: `http://${credentials.host}:${credentials.insecurePort || credentials.port}`;
this.clients.set(
key,
axios.create({
baseURL,
headers: {
Authorization: `Bearer ${credentials.apiKey}`,
'Content-Type': 'application/json',
},
timeout: 10000,
})
);
}
return this.clients.get(key)!;
}
async executeCommand(
credentials: APICredentials,
commandId: string,
args?: any
): Promise<any> {
this.validateCommand(commandId);
const client = this.getClient(credentials);
try {
const response = await this.retryWithBackoff(
async () => {
return await client.post('/commands/execute', {
command: commandId,
args,
});
},
3
);
return response.data;
} catch (error: any) {
if (error.response?.status === 401) {
throw new RestApiError('API key invalid or expired', 401);
}
throw new RestApiError(
`Failed to execute command: ${commandId}`,
error.response?.status
);
}
}
async openFile(credentials: APICredentials, filepath: string): Promise<void> {
const client = this.getClient(credentials);
try {
await this.retryWithBackoff(async () => {
return await client.post('/open', {
file: filepath,
});
}, 3);
} catch (error: any) {
throw new RestApiError(`Failed to open file: ${filepath}`, error.response?.status);
}
}
async getActiveFile(credentials: APICredentials): Promise<FileContent | null> {
const client = this.getClient(credentials);
try {
const response = await this.retryWithBackoff(async () => {
return await client.get('/active/');
}, 3);
if (!response.data) return null;
return {
content: response.data.content || '',
path: response.data.path || '',
size: response.data.content?.length || 0,
modified: new Date(),
};
} catch (error: any) {
throw new RestApiError('Failed to get active file', error.response?.status);
}
}
async openGraph(credentials: APICredentials): Promise<void> {
return this.executeCommand(credentials, 'graph:open');
}
private validateCommand(commandId: string): void {
if (!ALLOWED_COMMANDS.has(commandId)) {
throw new CommandNotAllowedError(
`Command not allowed: ${commandId}. Allowed commands: ${Array.from(ALLOWED_COMMANDS).join(', ')}`
);
}
}
private async retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number,
initialDelay: number = 500
): Promise<T> {
let lastError: any;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
lastError = error;
// Don't retry on 401 or 403
if (error.response?.status === 401 || error.response?.status === 403) {
throw error;
}
if (attempt < maxRetries) {
const delay = initialDelay * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
clearClients(): void {
this.clients.clear();
}
}