/**
* Centralized API client for FastMode API requests
* Uses stored credentials with automatic refresh, or falls back to env var
*/
import { getValidCredentials, hasCredentials } from './credentials';
export interface ApiConfig {
apiUrl: string;
authToken: string | null;
}
/**
* Get API URL from environment or default
*/
export function getApiUrl(): string {
return process.env.FASTMODE_API_URL || 'https://api.fastmode.ai';
}
/**
* Get API configuration - tries stored credentials first, then env var
*/
export async function getApiConfigAsync(): Promise<ApiConfig> {
// First, try to get valid stored credentials
const credentials = await getValidCredentials();
if (credentials) {
return {
apiUrl: getApiUrl(),
authToken: credentials.accessToken,
};
}
// Fall back to environment variable (for backward compatibility)
return {
apiUrl: getApiUrl(),
authToken: process.env.FASTMODE_AUTH_TOKEN || null,
};
}
/**
* Synchronous version - only checks env var (for quick checks)
*/
export function getApiConfig(): ApiConfig {
return {
apiUrl: getApiUrl(),
authToken: process.env.FASTMODE_AUTH_TOKEN || null,
};
}
/**
* Check if authentication is configured (either stored credentials or env var)
*/
export function isAuthConfigured(): boolean {
return hasCredentials() || !!process.env.FASTMODE_AUTH_TOKEN;
}
/**
* Check if we need to trigger the device flow
*/
export async function needsAuthentication(): Promise<boolean> {
// Wait for any startup auth that might be in progress
const { waitForAuth } = await import('./auth-state');
await waitForAuth();
const credentials = await getValidCredentials();
if (credentials) return false;
if (process.env.FASTMODE_AUTH_TOKEN) return false;
return true;
}
/**
* Get a helpful message for authentication
*/
export function getAuthRequiredMessage(): string {
return `# Authentication Required
You need to authenticate to use this tool.
**Starting authentication flow...**
A browser window will open for you to log in. If it doesn't open automatically, you'll see a URL to visit.
`;
}
/**
* API request options
*/
export interface ApiRequestOptions {
tenantId?: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
}
/**
* API error response
*/
export interface ApiError {
success: false;
error: string;
statusCode: number;
needsAuth?: boolean;
}
/**
* Make an authenticated API request
* Uses stored credentials with auto-refresh, or falls back to env var
*/
export async function apiRequest<T>(
endpoint: string,
options: ApiRequestOptions = {}
): Promise<{ data: T } | ApiError> {
const config = await getApiConfigAsync();
if (!config.authToken) {
return {
success: false,
error: 'Not authenticated',
statusCode: 401,
needsAuth: true,
};
}
const url = `${config.apiUrl}${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
const headers: Record<string, string> = {
'Authorization': `Bearer ${config.authToken}`,
'Content-Type': 'application/json',
};
if (options.tenantId) {
headers['X-Tenant-Id'] = options.tenantId;
}
try {
const response = await fetch(url, {
method: options.method || 'GET',
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
const data = await response.json();
if (!response.ok) {
// Check if this is an auth error
if (response.status === 401) {
return {
success: false,
error: 'Authentication expired or invalid',
statusCode: 401,
needsAuth: true,
};
}
return {
success: false,
error: data.error || `API request failed with status ${response.status}`,
statusCode: response.status,
};
}
return data;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('fetch') || message.includes('network') || message.includes('ECONNREFUSED')) {
return {
success: false,
error: `Network error: Unable to connect to ${config.apiUrl}`,
statusCode: 0,
};
}
return {
success: false,
error: message,
statusCode: 0,
};
}
}
/**
* Check if a response is an error
*/
export function isApiError(response: unknown): response is ApiError {
return (
typeof response === 'object' &&
response !== null &&
'success' in response &&
(response as ApiError).success === false
);
}
/**
* Check if error requires authentication
*/
export function needsAuthError(error: ApiError): boolean {
return error.needsAuth === true || error.statusCode === 401;
}
/**
* Project/Tenant info (from GET /api/tenants)
*/
interface TenantWithRole {
id: string;
name: string;
subdomain: string;
role: string;
}
/**
* Resolve a project identifier to a tenant ID
* Accepts either a UUID or a project name
*/
export async function resolveProjectId(projectIdentifier: string): Promise<{ tenantId: string } | { error: string }> {
// Check if it looks like a UUID
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidPattern.test(projectIdentifier)) {
return { tenantId: projectIdentifier };
}
// Look up by name using /api/tenants
const response = await apiRequest<TenantWithRole[]>('/api/tenants');
if (isApiError(response)) {
return { error: `Failed to look up project: ${response.error}` };
}
const projects = response.data;
// Find by exact name match (case-insensitive)
const match = projects.find(
p => p.name.toLowerCase() === projectIdentifier.toLowerCase()
);
if (match) {
return { tenantId: match.id };
}
// Try partial match
const partialMatch = projects.find(
p => p.name.toLowerCase().includes(projectIdentifier.toLowerCase())
);
if (partialMatch) {
return { tenantId: partialMatch.id };
}
const availableProjects = projects.map(p => `- ${p.name} (${p.id})`).join('\n');
return {
error: `Project "${projectIdentifier}" not found.\n\nAvailable projects:\n${availableProjects || 'None'}`
};
}
/**
* Make an authenticated external API request
* Uses the /external/* endpoints with tenant context
*/
export async function externalApiRequest<T>(
tenantId: string,
endpoint: string,
options: Omit<ApiRequestOptions, 'tenantId'> = {}
): Promise<{ data: T } | ApiError> {
// External API endpoints are at /external/...
const fullEndpoint = endpoint.startsWith('/external')
? endpoint
: `/external${endpoint.startsWith('/') ? endpoint : '/' + endpoint}`;
return apiRequest<T>(fullEndpoint, { ...options, tenantId });
}