← vibesdk
/**
* Unified API Client - Premium quality abstraction for all worker API calls
* Provides type-safe methods for all endpoints with proper error handling
* Features 401 response interception to trigger authentication modals
*/
import type{
ApiResponse,
AppsListData,
PublicAppsData,
FavoriteToggleData,
CreateAppData,
UpdateAppVisibilityData,
AppDeleteData,
AppDetailsData,
AppStarToggleData,
GitCloneTokenData,
PreviewTokenData,
UserAppsData,
ProfileUpdateData,
UserStatsData,
UserActivityData,
UserAnalyticsResponseData,
AgentAnalyticsResponseData,
ModelConfigsData,
ModelConfigData,
ModelConfigUpdateData,
ModelConfigTestData,
ModelConfigResetData,
ModelConfigDefaultsData,
ModelConfigDeleteData,
ByokProvidersData,
ModelConfigUpdate,
ModelProvidersListData,
ModelProviderCreateData,
ModelProviderUpdateData,
ModelProviderDeleteData,
ModelProviderTestData,
CreateProviderRequest,
UpdateProviderRequest,
TestProviderRequest,
SecretTemplatesData,
AgentConnectionData,
AgentStreamingResponse,
App,
ActiveSessionsData,
ApiKeysData,
LinkedIdentitiesData,
LoginResponseData,
RegisterResponseData,
ProfileResponseData,
AuthProvidersResponseData,
CsrfTokenResponseData,
CloudflareConnectRequestData,
CloudflareConnectResponseData,
OAuthProvider,
CodeGenArgs,
AgentPreviewResponse,
PlatformStatusData,
RateLimitError,
CapabilitiesData,
VaultConfigResponse,
VaultStatusResponse,
ListAppTablesResponse,
QueryAppTableResponse,
WipeAppDatabaseResponse,
ListAppBranchesResponse,
} from '@/api-types';
import {
RateLimitExceededError,
SecurityError,
SecurityErrorType,
} from '@/api-types';
import { toast } from 'sonner';
/**
* Global auth modal trigger for 401 interception
*/
let globalAuthModalTrigger: ((context?: string) => void) | null = null;
export function setGlobalAuthModalTrigger(trigger: (context?: string) => void) {
globalAuthModalTrigger = trigger;
}
/**
* API Client Error class with proper error context
*/
export class ApiError extends Error {
constructor(
public status: number,
public statusText: string,
message: string,
public endpoint: string,
) {
super(message);
this.name = 'ApiError';
}
}
/**
* Base API client configuration
*/
interface ApiClientConfig {
baseUrl?: string;
defaultHeaders?: Record<string, string>;
}
/**
* Request options for API calls
*/
interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
body?: unknown;
credentials?: RequestCredentials;
skipJsonParsing?: boolean; // Skip JSON parsing for streaming responses
}
/**
* Pagination parameters for paginated endpoints
*/
interface PaginationParams {
page?: number;
limit?: number;
sort?: string;
order?: 'asc' | 'desc';
}
/**
* User apps parameters with filtering and sorting
*/
interface UserAppsParams extends PaginationParams {
period?: 'today' | 'week' | 'month' | 'all';
framework?: string;
search?: string;
visibility?: 'private' | 'public' | 'team' | 'board';
status?: 'generating' | 'completed';
teamId?: string;
}
/**
* Public apps parameters with filtering and sorting
*/
interface PublicAppsParams extends PaginationParams {
period?: 'today' | 'week' | 'month' | 'all';
framework?: string;
search?: string;
boardId?: string;
}
/**
* Unified API Client class
*/
interface CSRFTokenInfo {
token: string;
expiresAt: number;
}
class ApiClient {
private baseUrl: string;
private defaultHeaders: Record<string, string>;
private csrfTokenInfo: CSRFTokenInfo | null = null;
private csrfTokenPromise: Promise<boolean> | null = null;
constructor(config: ApiClientConfig = {}) {
this.baseUrl = config.baseUrl || '';
this.defaultHeaders = {
'Content-Type': 'application/json',
...config.defaultHeaders,
};
}
/**
* Get authentication headers for API requests
*/
private async getAuthHeaders(): Promise<Record<string, string>> {
const headers: Record<string, string> = {};
// Add session token for anonymous users if not authenticated
// This will be handled automatically by cookies/credentials for authenticated users
const sessionToken = localStorage.getItem('anonymous_session_token');
if (sessionToken && !document.cookie.includes('session=')) {
headers['X-Session-Token'] = sessionToken;
}
// Add CSRF token for state-changing requests
if (this.csrfTokenInfo && !this.isCSRFTokenExpired()) {
headers['X-CSRF-Token'] = this.csrfTokenInfo.token;
}
// The Cloudflare OAuth token lives in an HttpOnly cookie. The browser
// attaches it automatically on same-origin requests; there is nothing
// for the API client to add.
return headers;
}
/**
* Fetch CSRF token from server with expiration handling
*/
private async fetchCsrfToken(): Promise<boolean> {
if (this.csrfTokenPromise) return this.csrfTokenPromise;
this.csrfTokenPromise = this.fetchCsrfTokenUncached();
try {
return await this.csrfTokenPromise;
} finally {
this.csrfTokenPromise = null;
}
}
private async fetchCsrfTokenUncached(): Promise<boolean> {
try {
const response = await fetch(`${this.baseUrl}/api/auth/csrf-token`, {
method: 'GET',
credentials: 'include',
});
if (response.ok) {
const data: ApiResponse<CsrfTokenResponseData> = await response.json();
if (data.data?.token) {
const expiresIn = data.data.expiresIn || 7200; // Default 2 hours
this.csrfTokenInfo = {
token: data.data.token,
expiresAt: Date.now() + (expiresIn * 1000)
};
return true;
}
}
return false;
} catch (error) {
console.warn('Failed to fetch CSRF token:', error);
return false;
}
}
/**
* Public method to refresh CSRF token
* Should be called after authentication operations that rotate the token
*/
async refreshCsrfToken(): Promise<void> {
await this.fetchCsrfToken();
}
/**
* Check if CSRF token is expired
*/
private isCSRFTokenExpired(): boolean {
if (!this.csrfTokenInfo) return true;
return Date.now() >= this.csrfTokenInfo.expiresAt;
}
/**
* Ensure CSRF token exists and is valid for state-changing requests
*/
private async ensureCsrfToken(method: string): Promise<boolean> {
if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(method.toUpperCase())) {
return true;
}
// Fetch new token if none exists or current one is expired
if (!this.csrfTokenInfo || this.isCSRFTokenExpired()) {
return await this.fetchCsrfToken();
}
return true;
}
/**
* Ensure session token exists for anonymous users
*/
private ensureSessionToken(): void {
if (
!localStorage.getItem('anonymous_session_token') &&
!document.cookie.includes('session=')
) {
localStorage.setItem(
'anonymous_session_token',
crypto.randomUUID(),
);
}
}
/**
* Get authentication context message based on endpoint
*/
private getAuthContextForEndpoint(endpoint: string): string {
if (endpoint.includes('/api/agent')) return 'to create applications';
if (endpoint.includes('/favorite')) return 'to favorite this app';
if (endpoint.includes('/star')) return 'to star this app';
// if (endpoint.includes('/fork')) return 'to fork this app';
// if (endpoint.includes('/apps')) return 'to access your apps';
if (endpoint.includes('/profile')) return 'to access your profile';
if (endpoint.includes('/settings')) return 'to access settings';
return 'to continue';
}
/**
* Check if endpoint should trigger auth modal on 401
* Auth checking endpoints should not auto-trigger modals
*/
private shouldTriggerAuthModal(endpoint: string): boolean {
// Don't trigger modal for auth state checking endpoints
if (endpoint === '/api/auth/profile') return false;
if (endpoint === '/api/auth/providers') return false;
if (endpoint === '/api/auth/sessions') return false;
return true;
}
private async request<T>(
endpoint: string,
options: RequestOptions = {},
noToast: boolean = false,
): Promise<ApiResponse<T>> {
const { data } = await this.requestRaw<T>(endpoint, options, false, noToast);
if (!data) {
throw new ApiError(
500,
'Internal Error',
'Unexpected null response data',
endpoint,
);
}
return data;
}
private async requestRaw<T>(
endpoint: string,
options: RequestOptions = {},
isRetry: boolean = false,
noToast: boolean = false,
): Promise<{ response: Response; data: ApiResponse<T> | null }> {
this.ensureSessionToken();
if (!await this.ensureCsrfToken(options.method || 'GET')) {
throw new ApiError(
500,
'Internal Error',
'Failed to obtain CSRF token',
endpoint,
);
}
const url = `${this.baseUrl}${endpoint}`;
const authHeaders = await this.getAuthHeaders();
const config: RequestInit = {
method: options.method || 'GET',
headers: {
...this.defaultHeaders,
...authHeaders,
...options.headers,
},
credentials: options.credentials || 'include',
};
if (options.body) {
config.body =
typeof options.body === 'string'
? options.body
: JSON.stringify(options.body);
}
try {
const response = await fetch(url, config);
// For streaming responses, skip JSON parsing if response is ok
if (options.skipJsonParsing && response.ok) {
return { response, data: null };
}
const data = await response.json() as ApiResponse<T>;
if (!response.ok) {
// Token refresh happens transparently on the backend via the
// HttpOnly cookie. A 401 here means the cookie is gone or the
// session itself is invalid - no client-side refresh to attempt.
if (
response.status === 401 &&
globalAuthModalTrigger &&
this.shouldTriggerAuthModal(endpoint)
) {
const authContext = this.getAuthContextForEndpoint(endpoint);
globalAuthModalTrigger(authContext);
}
const errorData = data.error;
if (errorData && errorData.type) {
if (
errorData.type === SecurityErrorType.CSRF_VIOLATION &&
response.status === 403 &&
!isRetry
) {
this.csrfTokenInfo = null;
return this.requestRaw(endpoint, options, true, noToast);
}
if (!noToast) {
toast.error(errorData.message);
}
switch (errorData.type) {
case SecurityErrorType.CSRF_VIOLATION:
break;
case SecurityErrorType.RATE_LIMITED:
// Handle rate limiting
console.log('Rate limited', errorData);
throw RateLimitExceededError.fromRateLimitError(errorData as unknown as RateLimitError);
default:
// Security error
throw new SecurityError(errorData.type, errorData.message);
}
}
throw new ApiError(
response.status,
response.statusText,
data.error?.message || data.message || 'Request failed',
endpoint,
);
}
return { response, data };
} catch (error) {
console.error(error);
if (error instanceof ApiError || error instanceof RateLimitExceededError || error instanceof SecurityError) {
throw error;
}
throw new ApiError(
0,
'Network Error',
error instanceof Error ? error.message : 'Unknown error',
endpoint,
);
}
}
// ===============================
// Platform Status API Methods
// ===============================
async getPlatformStatus(noToast: boolean = true): Promise<ApiResponse<PlatformStatusData>> {
return this.request<PlatformStatusData>('/api/status', undefined, noToast);
}
// ===============================
// Platform Capabilities API Methods
// ===============================
/**
* Get platform capabilities including available features
*/
async getCapabilities(noToast: boolean = true): Promise<ApiResponse<CapabilitiesData>> {
return this.request<CapabilitiesData>('/api/capabilities', undefined, noToast);
}
// ===============================
// Apps API Methods
// ===============================
/**
* Get all apps for the current user
*/
async getUserApps(): Promise<ApiResponse<AppsListData>> {
return this.request<AppsListData>('/api/apps');
}
/**
* Get recent apps (last 10)
*/
async getRecentApps(): Promise<ApiResponse<AppsListData>> {
return this.request<AppsListData>('/api/apps/recent');
}
/**
* Get favorite apps
*/
async getFavoriteApps(): Promise<ApiResponse<AppsListData>> {
return this.request<AppsListData>('/api/apps/favorites');
}
/**
* Get public apps feed with pagination
*/
async getPublicApps(
params?: PublicAppsParams,
): Promise<ApiResponse<PublicAppsData>> {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.limit) queryParams.set('limit', params.limit.toString());
if (params?.sort) queryParams.set('sort', params.sort);
if (params?.order) queryParams.set('order', params.order);
if (params?.period) queryParams.set('period', params.period);
if (params?.framework) queryParams.set('framework', params.framework);
if (params?.search) queryParams.set('search', params.search);
if (params?.boardId) queryParams.set('boardId', params.boardId);
const endpoint = `/api/apps/public${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
return this.request<PublicAppsData>(endpoint);
}
/**
* Create a new app
*/
async createApp(data: {
title: string;
description?: string;
}): Promise<ApiResponse<CreateAppData>> {
return this.request<CreateAppData>('/api/apps', {
method: 'POST',
body: data,
});
}
/**
* Toggle favorite status of an app
*/
async toggleFavorite(
appId: string,
): Promise<ApiResponse<FavoriteToggleData>> {
return this.request<FavoriteToggleData>(`/api/apps/${appId}/favorite`, {
method: 'POST',
});
}
/**
* Update app visibility
*/
async updateAppVisibility(
appId: string,
visibility: App['visibility'],
): Promise<ApiResponse<UpdateAppVisibilityData>> {
return this.request<UpdateAppVisibilityData>(
`/api/apps/${appId}/visibility`,
{
method: 'PUT',
body: { visibility },
},
);
}
/**
* Delete an app
*/
async deleteApp(appId: string): Promise<ApiResponse<AppDeleteData>> {
return this.request<AppDeleteData>(`/api/apps/${appId}`, {
method: 'DELETE',
});
}
// ===============================
// App View API Methods
// ===============================
/**
* Get detailed app information for viewing
*/
async getAppDetails(appId: string): Promise<ApiResponse<AppDetailsData>> {
return this.request<AppDetailsData>(`/api/apps/${appId}`);
}
/**
* Toggle star status of an app (different from favorite)
*/
async toggleAppStar(
appId: string,
): Promise<ApiResponse<AppStarToggleData>> {
return this.request<AppStarToggleData>(`/api/apps/${appId}/star`, {
method: 'POST',
});
}
/**
* Generate a short-lived token for git clone (private repos only)
*/
async generateGitCloneToken(
appId: string,
): Promise<ApiResponse<GitCloneTokenData>> {
return this.request<GitCloneTokenData>(`/api/apps/${appId}/git/token`, {
method: 'POST',
});
}
/**
* Generate a short-lived owner-preview token so the owner can open a
* private deployed app's URL on a preview subdomain.
*/
async generatePreviewToken(
appId: string,
): Promise<ApiResponse<PreviewTokenData>> {
return this.request<PreviewTokenData>(`/api/apps/${appId}/preview-token`, {
method: 'POST',
});
}
// /**
// * Fork an app
// */
// DISABLED: Has been disabled for initial alpha release, for security reasons
// async forkApp(appId: string): Promise<ApiResponse<ForkAppData>> {
// return this.request<ForkAppData>(`/api/apps/${appId}/fork`, {
// method: 'POST',
// });
// }
// ===============================
// User API Methods
// ===============================
/**
* Get user apps with pagination
*/
async getUserAppsWithPagination(
params?: UserAppsParams,
): Promise<ApiResponse<UserAppsData>> {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.limit) queryParams.set('limit', params.limit.toString());
if (params?.sort) queryParams.set('sort', params.sort);
if (params?.order) queryParams.set('order', params.order);
if (params?.period) queryParams.set('period', params.period);
if (params?.framework) queryParams.set('framework', params.framework);
if (params?.search) queryParams.set('search', params.search);
if (params?.visibility)
queryParams.set('visibility', params.visibility);
if (params?.status) queryParams.set('status', params.status);
if (params?.teamId) queryParams.set('teamId', params.teamId);
const endpoint = `/api/user/apps${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
return this.request<UserAppsData>(endpoint);
}
async createAgentSession(args: CodeGenArgs): Promise<AgentStreamingResponse> {
try {
const { response, data } = await this.requestRaw(
'/api/agent',
{
method: 'POST',
body: args,
skipJsonParsing: true, // Don't parse JSON for streaming response
},
false,
true,
);
// Check if response is ok
if (!response.ok) {
// Check if this is a usage limit error
if (response.status === 429 && data?.error?.errorType === 'USAGE_LIMIT_EXCEEDED') {
// Emit custom event for usage limit exceeded
window.dispatchEvent(new CustomEvent('usage-limit-exceeded', {
detail: {
message: data.error.message,
exceededLimits: data.error.exceededLimits,
hasUserToken: data.error.hasUserToken,
}
}));
const errorMessage = data.error.message || 'Free tier limits exceeded';
throw new Error(errorMessage);
}
// Parse error response if available
const errorMessage = data?.error?.message || `Agent creation failed with status: ${response.status}`;
throw new Error(errorMessage);
}
return {
success: true,
stream: response
};
} catch (error) {
// Handle any network or parsing errors
const errorMessage = error instanceof Error ? error.message : 'Failed to create agent session';
toast.error(errorMessage);
throw new Error(errorMessage);
}
}
/**
* Update user profile
*/
async updateProfile(data: {
displayName?: string;
username?: string;
bio?: string;
timezone?: string;
theme?: 'light' | 'dark' | 'system';
}): Promise<ApiResponse<ProfileUpdateData>> {
return this.request<ProfileUpdateData>('/api/user/profile', {
method: 'PUT',
body: data,
});
}
// ===============================
// Stats API Methods
// ===============================
/**
* Get user statistics
*/
async getUserStats(): Promise<ApiResponse<UserStatsData>> {
return this.request<UserStatsData>('/api/stats');
}
/**
* Get user activity timeline
*/
async getUserActivity(): Promise<ApiResponse<UserActivityData>> {
return this.request<UserActivityData>('/api/stats/activity');
}
// ===============================
// Analytics API Methods
// ===============================
/**
* Get user analytics (AI Gateway costs and usage)
*/
async getUserAnalytics(
userId: string,
days?: number,
): Promise<ApiResponse<UserAnalyticsResponseData>> {
const queryParams = days ? `?days=${days}` : '';
return this.request<UserAnalyticsResponseData>(
`/api/user/${userId}/analytics${queryParams}`,
);
}
/**
* Get agent analytics (AI Gateway costs and usage for specific app/chat)
*/
async getAgentAnalytics(
agentId: string,
days?: number,
): Promise<ApiResponse<AgentAnalyticsResponseData>> {
const queryParams = days ? `?days=${days}` : '';
return this.request<AgentAnalyticsResponseData>(
`/api/agent/${agentId}/analytics${queryParams}`,
);
}
// ===============================
// Model Config API Methods
// ===============================
/**
* Get all model configurations
*/
async getModelConfigs(): Promise<ApiResponse<ModelConfigsData>> {
return this.request<ModelConfigsData>('/api/model-configs');
}
/**
* Get BYOK providers and available models
* @param agentAction - Optional agent action to filter models by constraints
*/
async getByokProviders(agentAction?: string): Promise<ApiResponse<ByokProvidersData>> {
const endpoint = agentAction
? `/api/model-configs/byok-providers?agentAction=${encodeURIComponent(agentAction)}`
: '/api/model-configs/byok-providers';
return this.request<ByokProvidersData>(endpoint);
}
/**
* Get BYOK templates for dynamic provider configuration
*/
async getBYOKTemplates(): Promise<ApiResponse<SecretTemplatesData>> {
return this.request<SecretTemplatesData>(
'/api/secrets/templates?category=byok',
);
}
/**
* Reset model configuration to default
*/
async resetModelConfig(
agentAction: string,
): Promise<ApiResponse<ModelConfigResetData>> {
return this.request<ModelConfigResetData>(
`/api/model-configs/${agentAction}`,
{
method: 'DELETE',
},
);
}
/**
* Reset all model configurations to defaults
*/
async resetAllModelConfigs(): Promise<ApiResponse<ModelConfigResetData>> {
return this.request<ModelConfigResetData>(
'/api/model-configs/reset-all',
{
method: 'POST',
},
);
}
/**
* Get specific model configuration
*/
async getModelConfig(
actionKey: string,
): Promise<ApiResponse<ModelConfigData>> {
return this.request<ModelConfigData>(`/api/model-configs/${actionKey}`);
}
/**
* Update model configuration
*/
async updateModelConfig(
actionKey: string,
config: ModelConfigUpdate,
): Promise<ApiResponse<ModelConfigUpdateData>> {
return this.request<ModelConfigUpdateData>(
`/api/model-configs/${actionKey}`,
{
method: 'PUT',
body: config,
},
);
}
/**
* Test model configuration
*/
async testModelConfig(
actionKey: string,
tempConfig?: ModelConfigUpdate,
): Promise<ApiResponse<ModelConfigTestData>> {
return this.request<ModelConfigTestData>('/api/model-configs/test', {
method: 'POST',
body: {
agentActionName: actionKey,
useUserKeys: true,
...(tempConfig && { tempConfig }),
},
});
}
/**
* Reset all model configurations
*/
async resetAllConfigs(): Promise<ApiResponse<ModelConfigResetData>> {
return this.request<ModelConfigResetData>(
'/api/model-configs/reset-all',
{
method: 'POST',
},
);
}
/**
* Get default model configurations
*/
async getModelDefaults(): Promise<ApiResponse<ModelConfigDefaultsData>> {
return this.request<ModelConfigDefaultsData>(
'/api/model-configs/defaults',
);
}
/**
* Delete model configuration
*/
async deleteModelConfig(
actionKey: string,
): Promise<ApiResponse<ModelConfigDeleteData>> {
return this.request<ModelConfigDeleteData>(
`/api/model-configs/${actionKey}`,
{
method: 'DELETE',
},
);
}
// ===============================
// Model Providers API Methods
// ===============================
/**
* Get all custom model providers
*/
async getModelProviders(): Promise<ApiResponse<ModelProvidersListData>> {
return this.request<ModelProvidersListData>('/api/user/providers');
}
/**
* Create a new custom model provider
*/
async createModelProvider(
data: CreateProviderRequest,
): Promise<ApiResponse<ModelProviderCreateData>> {
return this.request<ModelProviderCreateData>('/api/user/providers', {
method: 'POST',
body: data,
});
}
/**
* Update an existing model provider
*/
async updateModelProvider(
providerId: string,
data: UpdateProviderRequest,
): Promise<ApiResponse<ModelProviderUpdateData>> {
return this.request<ModelProviderUpdateData>(
`/api/user/providers/${providerId}`,
{
method: 'PUT',
body: data,
},
);
}
/**
* Delete a model provider
*/
async deleteModelProvider(
providerId: string,
): Promise<ApiResponse<ModelProviderDeleteData>> {
return this.request<ModelProviderDeleteData>(
`/api/user/providers/${providerId}`,
{
method: 'DELETE',
},
);
}
/**
* Test a model provider connection
*/
async testModelProvider(
data: TestProviderRequest,
): Promise<ApiResponse<ModelProviderTestData>> {
return this.request<ModelProviderTestData>('/api/user/providers/test', {
method: 'POST',
body: data,
});
}
// ===============================
// Secrets API Methods
// ===============================
/**
* Get secret templates for BYOK providers
*/
async getSecretTemplates(): Promise<ApiResponse<SecretTemplatesData>> {
return this.request<SecretTemplatesData>('/api/secrets/templates');
}
// ===============================
// Vault API Methods
// ===============================
async getVaultStatus(): Promise<ApiResponse<VaultStatusResponse>> {
return this.request<VaultStatusResponse>('/api/vault/status');
}
async getVaultConfig(): Promise<ApiResponse<{ config: VaultConfigResponse }>> {
return this.request<{ config: VaultConfigResponse }>('/api/vault/config');
}
async setupVault(data: {
kdfAlgorithm: 'argon2id' | 'webauthn-prf';
kdfSalt: string;
kdfParams?: { time: number; mem: number; parallelism: number };
prfCredentialId?: string;
prfSalt?: string;
encryptedRecoveryCodes?: string;
recoveryCodesNonce?: string;
verificationBlob: string;
verificationNonce: string;
}): Promise<ApiResponse<{ success: boolean }>> {
return this.request<{ success: boolean }>('/api/vault/setup', {
method: 'POST',
body: data,
});
}
async resetVault(): Promise<ApiResponse<{ success: boolean }>> {
return this.request<{ success: boolean }>('/api/vault/reset', {
method: 'POST',
});
}
/**
* Initiate GitHub OAuth authorization for user repository access
* This redirects to GitHub OAuth
*/
initiateGitHubOAuth(): void {
const oauthUrl = new URL('/api/github-app/authorize', window.location.origin);
window.location.href = oauthUrl.toString();
}
/**
* Initiate GitHub export with OAuth flow
* Returns authorization URL for redirect
*/
async initiateGitHubExport(data: {
repositoryName: string;
description?: string;
isPrivate?: boolean;
agentId: string;
}): Promise<ApiResponse<{
authUrl?: string;
success?: boolean;
repositoryUrl?: string;
skippedOAuth?: boolean;
alreadyExists?: boolean;
existingRepositoryUrl?: string;
}>> {
return this.request('/api/github-app/export', {
method: 'POST',
body: data,
});
}
/**
* Check remote repository status
*/
async checkRemoteStatus(data: {
repositoryUrl: string;
agentId: string;
}): Promise<ApiResponse<{
compatible: boolean;
behindBy: number;
aheadBy: number;
divergedCommits: Array<{
sha: string;
message: string;
author: string;
date: string;
}>;
}>> {
return this.request('/api/github-app/check-remote', {
method: 'POST',
body: data,
});
}
// ===============================
// Agent/CodeGen API Methods
// ===============================
/**
* Connect to existing agent
*/
async connectToAgent(
agentId: string,
): Promise<ApiResponse<AgentConnectionData>> {
return this.request<AgentConnectionData>(
`/api/agent/${agentId}/connect`,
);
}
/**
* Deploy preview
*/
async deployPreview(
agentId: string,
): Promise<ApiResponse<AgentPreviewResponse>> {
return this.request<AgentPreviewResponse>(
`/api/agent/${agentId}/preview`,
);
}
// ===============================
// App Database (DB tab) API
// ===============================
/**
* List tables in the generated app's Durable Object SQLite storage
* for the current branch.
*/
async listAppTables(
agentId: string,
branch?: string,
): Promise<ApiResponse<ListAppTablesResponse>> {
const params = new URLSearchParams();
if (branch) params.set('branch', branch);
const qs = params.toString();
return this.request<ListAppTablesResponse>(
`/api/agent/${agentId}/db/tables${qs ? `?${qs}` : ''}`,
);
}
/**
* List the branches of the app's workspace repo (for the Repo tab's
* branch selector). Read-only.
*/
async listAppBranches(
agentId: string,
): Promise<ApiResponse<ListAppBranchesResponse>> {
return this.request<ListAppBranchesResponse>(
`/api/agent/${agentId}/branches`,
);
}
/**
* Read rows from a table inside the App's DO. Paginated, read-only.
*/
async queryAppTable(
agentId: string,
args: {
table: string;
limit?: number;
offset?: number;
orderBy?: string;
orderDir?: 'asc' | 'desc';
branch?: string;
},
): Promise<ApiResponse<QueryAppTableResponse>> {
const params = new URLSearchParams();
params.set('table', args.table);
if (args.limit !== undefined) params.set('limit', String(args.limit));
if (args.offset !== undefined) params.set('offset', String(args.offset));
if (args.orderBy) params.set('orderBy', args.orderBy);
if (args.orderDir) params.set('orderDir', args.orderDir);
if (args.branch) params.set('branch', args.branch);
return this.request<QueryAppTableResponse>(
`/api/agent/${agentId}/db/query?${params.toString()}`,
);
}
/**
* Drop every user table inside the App's Durable Object. The next
* request to the App recreates whatever schema its startup `CREATE
* TABLE IF NOT EXISTS` defines.
*/
async wipeAppDatabase(
agentId: string,
opts: { branch?: string } = {},
): Promise<ApiResponse<WipeAppDatabaseResponse>> {
const body: Record<string, unknown> = {};
if (opts.branch) body.branch = opts.branch;
return this.request<WipeAppDatabaseResponse>(
`/api/agent/${agentId}/db/wipe`,
{
method: 'POST',
body: JSON.stringify(body),
},
);
}
// ===============================
// Session Management API Methods
// ===============================
/**
* Get active user sessions
*/
async getActiveSessions(): Promise<ApiResponse<ActiveSessionsData>> {
return this.request<ActiveSessionsData>('/api/auth/sessions');
}
/**
* Revoke a specific session
*/
async revokeSession(
sessionId: string,
): Promise<ApiResponse<{ message: string }>> {
return this.request<{ message: string }>(
`/api/auth/sessions/${sessionId}`,
{
method: 'DELETE',
},
);
}
// ===============================
// API Keys Management Methods
// ===============================
/**
* Get user API keys
*/
async getApiKeys(): Promise<ApiResponse<ApiKeysData>> {
return this.request<ApiKeysData>('/api/auth/api-keys');
}
/**
* Create a new API key
*/
async createApiKey(data: {
name: string;
}): Promise<
ApiResponse<{
key: string;
keyPreview: string;
name: string;
message: string;
}>
> {
return this.request<{
key: string;
keyPreview: string;
name: string;
message: string;
}>('/api/auth/api-keys', {
method: 'POST',
body: data,
});
}
/**
* Revoke an API key
*/
async revokeApiKey(
keyId: string,
): Promise<ApiResponse<{ message: string }>> {
return this.request<{ message: string }>(
`/api/auth/api-keys/${keyId}`,
{
method: 'DELETE',
},
);
}
// ===============================
// Authentication API Methods
// ===============================
/**
* Login with email and password
*/
async loginWithEmail(credentials: {
email: string;
password: string;
}): Promise<ApiResponse<LoginResponseData>> {
return this.request<LoginResponseData>('/api/auth/login', {
method: 'POST',
body: credentials,
});
}
/**
* Register a new user
*/
async register(data: {
email: string;
password: string;
name?: string;
}): Promise<ApiResponse<RegisterResponseData>> {
return this.request<RegisterResponseData>('/api/auth/register', {
method: 'POST',
body: data,
});
}
/**
* Get CSRF token
*/
async getCsrfToken(): Promise<ApiResponse<CsrfTokenResponseData>> {
return this.request<CsrfTokenResponseData>('/api/auth/csrf-token');
}
/**
* Get current user profile
*/
async getProfile(noToast: boolean = false): Promise<ApiResponse<ProfileResponseData>> {
return this.request<ProfileResponseData>('/api/auth/profile', undefined, noToast);
}
/**
* Logout current user
*/
async logout(): Promise<ApiResponse<{ message: string }>> {
return this.request<{ message: string }>('/api/auth/logout', {
method: 'POST',
});
}
/**
* Get available authentication providers
*/
async getAuthProviders(): Promise<ApiResponse<AuthProvidersResponseData>> {
return this.request<AuthProvidersResponseData>('/api/auth/providers');
}
/**
* Initiate OAuth flow (redirects to provider)
*/
initiateOAuth(provider: OAuthProvider, redirectUrl?: string): void {
const oauthUrl = new URL(
`/api/auth/oauth/${provider}`,
window.location.origin,
);
if (redirectUrl) {
oauthUrl.searchParams.set('redirect_url', redirectUrl);
}
// Redirect to OAuth provider
window.location.href = oauthUrl.toString();
}
/**
* Get the current user's linked OAuth identities
*/
async getLinkedIdentities(): Promise<ApiResponse<LinkedIdentitiesData>> {
return this.request<LinkedIdentitiesData>('/api/auth/identities');
}
/**
* Unlink an OAuth provider from the current user
*/
async unlinkProvider(
provider: OAuthProvider,
): Promise<ApiResponse<{ message: string }>> {
return this.request<{ message: string }>(
`/api/auth/identities/${provider}`,
{
method: 'DELETE',
},
);
}
/**
* Initiate an authenticated account-link flow (redirects to provider)
*/
initiateProviderLink(provider: OAuthProvider): void {
const linkUrl = new URL(
`/api/auth/link/${provider}`,
window.location.origin,
);
window.location.href = linkUrl.toString();
}
// ===============================
// Usage Limits API Methods
// ===============================
/**
* Get user's usage limits and Cloudflare credits
*/
async getLimitsUsage(): Promise<ApiResponse<any>> {
return this.request<any>('/api/limits/usage');
}
// ===============================
// Cloudflare Account API Methods
// ===============================
/**
* Set user's selected Cloudflare account and gateway
*/
async setCloudflareSelection(accountId: string, gatewayId: string): Promise<ApiResponse<{ message: string }>> {
return this.request<{ message: string }>('/api/cloudflare/selection', {
method: 'PUT',
body: { accountId, gatewayId },
});
}
async connectCloudflare(returnUrl?: string): Promise<ApiResponse<CloudflareConnectResponseData>> {
const body: CloudflareConnectRequestData = { returnUrl };
return this.request<CloudflareConnectResponseData>('/api/cloudflare/connect', {
method: 'POST',
body,
}, true);
}
/**
* Disconnect the user's Cloudflare OAuth session by clearing the HttpOnly
* token cookie on the server.
*/
async disconnectCloudflare(): Promise<ApiResponse<{ message: string }>> {
return this.request<{ message: string }>('/api/cloudflare/connection', {
method: 'DELETE',
});
}
/**
* Get the user's resolved AI Gateway usage preference.
*/
async getAiGatewayPreference(): Promise<ApiResponse<{ enabled: boolean; isExplicit: boolean }>> {
return this.request<{ enabled: boolean; isExplicit: boolean }>('/api/cloudflare/ai-gateway-preference');
}
/**
* Set whether the user's own AI Gateway is used for inference.
*/
async setAiGatewayPreference(enabled: boolean): Promise<ApiResponse<{ enabled: boolean; isExplicit: boolean }>> {
return this.request<{ enabled: boolean; isExplicit: boolean }>('/api/cloudflare/ai-gateway-preference', {
method: 'PUT',
body: { enabled },
});
}
}
// Export singleton instance
export const apiClient = new ApiClient();
// Export class for testing/custom instances
export { ApiClient };