feat(antigravity-auth): add OAuth flow and token management

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
YeonGyu-Kim
2025-12-12 21:59:40 +09:00
parent 36b8576c78
commit 07e2e907c5
5 changed files with 667 additions and 18 deletions

View File

@@ -1 +1,166 @@
// Antigravity project context - to be implemented in Task 6
/**
* Antigravity project context management.
* Handles fetching GCP project ID via Google's loadCodeAssist API.
*/
import {
ANTIGRAVITY_DEFAULT_PROJECT_ID,
ANTIGRAVITY_ENDPOINT_FALLBACKS,
ANTIGRAVITY_API_VERSION,
ANTIGRAVITY_HEADERS,
} from "./constants"
import type {
AntigravityProjectContext,
AntigravityLoadCodeAssistResponse,
} from "./types"
/**
* In-memory cache for project context per access token.
* Prevents redundant API calls for the same token.
*/
const projectContextCache = new Map<string, AntigravityProjectContext>()
/**
* Client metadata for loadCodeAssist API request.
* Matches cliproxyapi implementation.
*/
const CODE_ASSIST_METADATA = {
ideType: "IDE_UNSPECIFIED",
platform: "PLATFORM_UNSPECIFIED",
pluginType: "GEMINI",
} as const
/**
* Extracts the project ID from a cloudaicompanionProject field.
* Handles both string and object formats.
*
* @param project - The cloudaicompanionProject value from API response
* @returns Extracted project ID string, or undefined if not found
*/
function extractProjectId(
project: string | { id: string } | undefined
): string | undefined {
if (!project) {
return undefined
}
// Handle string format
if (typeof project === "string") {
const trimmed = project.trim()
return trimmed || undefined
}
// Handle object format { id: string }
if (typeof project === "object" && "id" in project) {
const id = project.id
if (typeof id === "string") {
const trimmed = id.trim()
return trimmed || undefined
}
}
return undefined
}
/**
* Calls the loadCodeAssist API to get project context.
* Tries each endpoint in the fallback list until one succeeds.
*
* @param accessToken - Valid OAuth access token
* @returns API response or null if all endpoints fail
*/
async function callLoadCodeAssistAPI(
accessToken: string
): Promise<AntigravityLoadCodeAssistResponse | null> {
const requestBody = {
metadata: CODE_ASSIST_METADATA,
}
const headers: Record<string, string> = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"User-Agent": ANTIGRAVITY_HEADERS["User-Agent"],
"X-Goog-Api-Client": ANTIGRAVITY_HEADERS["X-Goog-Api-Client"],
"Client-Metadata": ANTIGRAVITY_HEADERS["Client-Metadata"],
}
// Try each endpoint in the fallback list
for (const baseEndpoint of ANTIGRAVITY_ENDPOINT_FALLBACKS) {
const url = `${baseEndpoint}/${ANTIGRAVITY_API_VERSION}:loadCodeAssist`
try {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(requestBody),
})
if (!response.ok) {
// Try next endpoint on failure
continue
}
const data =
(await response.json()) as AntigravityLoadCodeAssistResponse
return data
} catch {
// Network or parsing error, try next endpoint
continue
}
}
// All endpoints failed
return null
}
/**
* Fetch project context from Google's loadCodeAssist API.
* Extracts the cloudaicompanionProject from the response.
* Falls back to ANTIGRAVITY_DEFAULT_PROJECT_ID if API fails or returns empty.
*
* @param accessToken - Valid OAuth access token
* @returns Project context with cloudaicompanionProject ID
*/
export async function fetchProjectContext(
accessToken: string
): Promise<AntigravityProjectContext> {
// Check cache first
const cached = projectContextCache.get(accessToken)
if (cached) {
return cached
}
// Call the API
const response = await callLoadCodeAssistAPI(accessToken)
// Extract project ID from response
const projectId = response
? extractProjectId(response.cloudaicompanionProject)
: undefined
// Build result with fallback
const result: AntigravityProjectContext = {
cloudaicompanionProject: projectId || ANTIGRAVITY_DEFAULT_PROJECT_ID,
}
// Cache the result
if (projectId) {
projectContextCache.set(accessToken, result)
}
return result
}
/**
* Clear the project context cache.
* Call this when tokens are refreshed or invalidated.
*
* @param accessToken - Optional specific token to clear, or clears all if not provided
*/
export function clearProjectContextCache(accessToken?: string): void {
if (accessToken) {
projectContextCache.delete(accessToken)
} else {
projectContextCache.clear()
}
}