import Conf from 'conf'; import { chmodSync } from 'node:fs'; import type { ApiResponse, Environment, EnvironmentHosts, IndemnConfig } from './types.js'; const DEFAULT_HOSTS: Record = { dev: { copilot: 'https://devcopilot.indemn.ai/api', evaluations: 'https://devevaluations.indemn.ai', middleware: 'https://devproxy.indemn.ai', }, production: { copilot: 'https://copilot.indemn.ai/api', evaluations: 'https://evaluations.indemn.ai', middleware: 'https://proxy.indemn.ai', }, }; export class IndemnClient { private apiKey: string; private orgId: string; private environment: Environment; private hosts: EnvironmentHosts; constructor(opts?: { apiKey?: string; orgId?: string; environment?: Environment }) { // Priority: explicit opts > env vars > config file const config = IndemnClient.loadConfig(); this.apiKey = opts?.apiKey || process.env.INDEMN_API_KEY || config?.api_key || ''; this.orgId = opts?.orgId || process.env.INDEMN_ORG_ID || config?.org_id || ''; this.environment = opts?.environment || (process.env.INDEMN_ENV as Environment) || config?.environment || 'dev'; const defaultHosts = DEFAULT_HOSTS[this.environment]; const configHosts = config?.hosts?.[this.environment]; this.hosts = { copilot: process.env.INDEMN_COPILOT_URL || configHosts?.copilot || defaultHosts.copilot, evaluations: process.env.INDEMN_EVALS_URL || configHosts?.evaluations || defaultHosts.evaluations, middleware: process.env.INDEMN_MIDDLEWARE_URL || configHosts?.middleware || defaultHosts.middleware, }; } static getConfigStore(): Conf { return new Conf({ projectName: 'indemn', configName: 'config', cwd: `${process.env.HOME}/.indemn`, }); } static loadConfig(): IndemnConfig | null { try { const store = IndemnClient.getConfigStore(); const apiKey = store.get('api_key'); if (!apiKey) return null; return { api_key: apiKey, org_id: store.get('org_id', ''), org_name: store.get('org_name', '') || undefined, user_id: store.get('user_id', ''), environment: store.get('environment', 'dev'), hosts: store.get('hosts', DEFAULT_HOSTS), }; } catch { return null; } } static saveConfig(config: Partial): void { const store = IndemnClient.getConfigStore(); for (const [key, value] of Object.entries(config)) { store.set(key as keyof IndemnConfig, value); } // Restrict config file to owner-only (contains API key) try { chmodSync(store.path, 0o600); } catch { // Non-fatal — Windows doesn't support Unix permissions } } static clearConfig(): void { const store = IndemnClient.getConfigStore(); store.clear(); } getEnvironment(): Environment { return this.environment; } getOrgId(): string { return this.orgId; } getApiKey(): string { return this.apiKey; } getHosts(): EnvironmentHosts { return this.hosts; } private ensureAuth(): void { if (!this.apiKey) { throw new Error('Not authenticated. Run `indemn login` first.'); } } private buildCopilotUrl(path: string): string { // Inject org_id into path templates const resolvedPath = path.replace(':org_id', this.orgId); return `${this.hosts.copilot}${resolvedPath}`; } private buildEvalsUrl(path: string): string { return `${this.hosts.evaluations}${path}`; } // Copilot Server requests (with /api/ prefix and org_id injection) async copilotGet(path: string, query?: Record): Promise { this.ensureAuth(); const url = new URL(this.buildCopilotUrl(path)); if (query) { for (const [k, v] of Object.entries(query)) { if (v !== undefined && v !== null) url.searchParams.set(k, v); } } const res = await fetch(url.toString(), { headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, }); return this.handleCopilotResponse(res); } async copilotPost(path: string, body?: unknown): Promise { this.ensureAuth(); const res = await fetch(this.buildCopilotUrl(path), { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); return this.handleCopilotResponse(res); } async copilotPut(path: string, body?: unknown): Promise { this.ensureAuth(); const res = await fetch(this.buildCopilotUrl(path), { method: 'PUT', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined, }); return this.handleCopilotResponse(res); } async copilotDelete(path: string): Promise { this.ensureAuth(); const res = await fetch(this.buildCopilotUrl(path), { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, }); return this.handleCopilotResponse(res); } // Evaluations Service requests (no /api/ prefix, no org_id, no auth in Phase 1) async evalsGet(path: string, query?: Record): Promise { const url = new URL(this.buildEvalsUrl(path)); if (query) { for (const [k, v] of Object.entries(query)) { if (v !== undefined && v !== null) url.searchParams.set(k, v); } } const res = await fetch(url.toString(), { headers: { 'Content-Type': 'application/json' }, }); return this.handleEvalsResponse(res); } async evalsPost(path: string, body?: unknown): Promise { const res = await fetch(this.buildEvalsUrl(path), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); return this.handleEvalsResponse(res); } async evalsPut(path: string, body?: unknown): Promise { const res = await fetch(this.buildEvalsUrl(path), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); return this.handleEvalsResponse(res); } async evalsDelete(path: string): Promise { const res = await fetch(this.buildEvalsUrl(path), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); return this.handleEvalsResponse(res); } // Unauthenticated Copilot Server requests (for login flow) async copilotPostUnauth(path: string, body?: unknown, headers?: Record): Promise { const res = await fetch(`${this.hosts.copilot}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers, }, body: body ? JSON.stringify(body) : undefined, }); return this.handleCopilotResponse(res); } private async handleCopilotResponse(res: Response): Promise { if (res.status === 401) { throw new Error('Authentication failed. Run `indemn login` to re-authenticate.'); } if (res.status === 403) { throw new Error('Access denied. You may not have permission for this operation.'); } if (res.status === 404) { throw new Error('Resource not found.'); } if (!res.ok) { const text = await res.text(); throw new Error(`API error (${res.status}): ${text}`); } const json = await res.json() as ApiResponse; // Copilot Server wraps responses in { success, data } if (json.success !== undefined) { if (!json.success) { throw new Error(json.msg || 'API request failed'); } return (json.data !== undefined ? json.data : json) as T; } return json as T; } private async handleEvalsResponse(res: Response): Promise { if (!res.ok) { const text = await res.text(); throw new Error(`Evaluations API error (${res.status}): ${text}`); } return res.json() as Promise; } }