import type { JsonValue, NetworkRequestInit, PluginContext } from "cursor-byok:plugin"; import type { ResourceDraft, ResourceImportFile, ResourceImportResult, ResourceImportSupport, ResourceMetric, ResourcePatch, ResourceSnapshot, ResourceState, ResourceView, } from "cursor-byok:resource"; import { ANTIGRAVITY_CLIENT_HEADERS, ANTIGRAVITY_ENDPOINTS, ANTIGRAVITY_USER_AGENT, } from "./models.ts"; import { CLIENT_ID, CLIENT_SECRET } from "./google_oauth.ts"; export const RESOURCE_TYPE = "antigravity-account"; const REFRESH_TOKEN_URL = "https://oauth2.googleapis.com/token"; async function fetchText( network: PluginContext["network"] | undefined, url: string, init: NetworkRequestInit, ): Promise<{ status: number; body: string }> { if (network) { const response = await network.fetch(url, init); return { status: response.status, body: response.body }; } const response = await fetch(url, init); return { status: response.status, body: await response.text() }; } export type QuotaMetric = { remainingPercent: number; resetAtMs: number | null; }; export type AccountQuota = { planLabel: string | null; limitReached: boolean; coolingUntilMs: number | null; updatedAtMs: number; claude?: QuotaMetric | null; gemini?: QuotaMetric | null; }; export type AccountData = { accessToken: string; refreshToken: string | null; displayName: string; projectId?: string | null; expiresAtMs?: number | null; quota: AccountQuota | null; }; export type CredentialCandidate = { accessToken: string; refreshToken: string | null; displayName: string | null; projectId?: string | null; expiresAtMs?: number | null; quota?: AccountQuota | null; }; export async function fetchAccountProjectAndTier( accessToken: string, network: PluginContext["network"], ): Promise<{ projectId: string; planLabel: string }> { for (const endpoint of ANTIGRAVITY_ENDPOINTS) { try { const assistRes = await network.fetch(`${endpoint}/v1internal:loadCodeAssist`, { method: "POST", headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json", "user-agent": ANTIGRAVITY_USER_AGENT, ...ANTIGRAVITY_CLIENT_HEADERS, }, body: JSON.stringify({ metadata: { ideType: "ANTIGRAVITY" } }), }); if (assistRes.status >= 200 && assistRes.status < 300) { const body = object(JSON.parse(assistRes.body)); const project = text(body?.cloudaicompanionProject); const paid = object(body?.paidTier); const current = object(body?.currentTier); const tierName = text(paid?.name) ?? text(paid?.id) ?? text(current?.name) ?? text(current?.id); let planLabel = "FREE"; if (tierName) { const lower = tierName.toLowerCase(); if (lower.includes("ultra")) planLabel = "ULTRA"; else if ( lower.includes("pro") || lower.includes("premium") || lower.includes("advanced") ) planLabel = "PRO"; } return { projectId: project ?? "bamboo-precept-lgxtn", planLabel }; } } catch { // Continue next endpoint } } return { projectId: "bamboo-precept-lgxtn", planLabel: "FREE" }; } export async function queryAccountQuota( accessToken: string, network: PluginContext["network"], ): Promise<{ quota: AccountQuota | null; projectId: string }> { const { projectId, planLabel } = await fetchAccountProjectAndTier(accessToken, network); for (const endpoint of ANTIGRAVITY_ENDPOINTS) { try { const response = await network.fetch(`${endpoint}/v1internal:fetchAvailableModels`, { method: "POST", headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json", accept: "application/json", "user-agent": ANTIGRAVITY_USER_AGENT, ...ANTIGRAVITY_CLIENT_HEADERS, }, body: JSON.stringify({ project: projectId }), }); if (response.status < 200 || response.status >= 300) continue; const root = object(JSON.parse(response.body)); const models = object(root?.models); if (!models) continue; let claudeFraction: number | null = null; let claudeResetAtMs: number | null = null; let geminiFraction: number | null = null; let geminiResetAtMs: number | null = null; for (const [key, value] of Object.entries(models)) { const info = object(value); const quota = object(info?.quotaInfo); const fraction = typeof quota?.remainingFraction === "number" ? quota.remainingFraction : null; const resetTime = text(quota?.resetTime); const resetAtMs = resetTime ? Date.parse(resetTime) : null; if (fraction === null) continue; const k = key.toLowerCase(); if (k.includes("claude") || k.includes("sonnet") || k.includes("opus")) { if (claudeFraction === null || fraction < claudeFraction) { claudeFraction = fraction; claudeResetAtMs = resetAtMs; } } else if (k.includes("gemini") || k.includes("flash") || k.includes("pro")) { if (geminiFraction === null || fraction < geminiFraction) { geminiFraction = fraction; geminiResetAtMs = resetAtMs; } } } return { projectId, quota: { planLabel, limitReached: false, coolingUntilMs: null, updatedAtMs: Date.now(), claude: claudeFraction !== null ? { remainingPercent: Math.round(claudeFraction * 100), resetAtMs: claudeResetAtMs } : null, gemini: geminiFraction !== null ? { remainingPercent: Math.round(geminiFraction * 100), resetAtMs: geminiResetAtMs } : null, }, }; } catch { // Continue next endpoint } } return { projectId, quota: { planLabel, limitReached: false, coolingUntilMs: null, updatedAtMs: Date.now(), claude: null, gemini: null, }, }; } function object(value: unknown): Record | null { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : null; } function text(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } function decodeJwtPayload(token: string): Record | null { const parts = token.split("."); if (parts.length < 2) return null; try { const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/"); const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); const bytes = Uint8Array.from(atob(padded), (char) => char.charCodeAt(0)); return object(JSON.parse(new TextDecoder().decode(bytes))); } catch { return null; } } function claim(payload: Record | null, key: string): string | null { return payload ? text(payload[key]) : null; } export function isJwtExpired(token: string, bufferSeconds = 300): boolean { if (token.startsWith("AIza") || !token.includes(".")) return false; const payload = decodeJwtPayload(token); if (!payload) return false; const exp = typeof payload.exp === "number" ? payload.exp : null; if (!exp) return false; const nowSeconds = Math.floor(Date.now() / 1000); return exp <= (nowSeconds + bufferSeconds); } export function isTokenExpired(data: AccountData, bufferSeconds = 300): boolean { if (!data.refreshToken) return false; if (typeof data.expiresAtMs === "number" && data.expiresAtMs > 0) { return Date.now() >= data.expiresAtMs - bufferSeconds * 1000; } return isJwtExpired(data.accessToken, bufferSeconds); } async function tokenFingerprint(token: string): Promise { const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token)); return Array.from( new Uint8Array(digest).slice(0, 8), (byte) => byte.toString(16).padStart(2, "0"), ).join(""); } export async function accountIdentity( token: string, providedDisplayName?: string | null, ): Promise<{ key: string; displayName: string }> { const payload = decodeJwtPayload(token); const email = claim(payload, "email"); const sub = claim(payload, "sub"); const name = claim(payload, "name") ?? claim(payload, "preferred_username"); const fingerprint = await tokenFingerprint(token); const identity = (providedDisplayName && !providedDisplayName.includes("Antigravity")) ? providedDisplayName : (email ?? sub ?? fingerprint); const displayName = providedDisplayName ?? email ?? name ?? (token.startsWith("AIza") ? `API Key (${fingerprint.slice(0, 6)})` : identity); return { key: `antigravity:${identity}`, displayName }; } export async function credentialDraft(credential: CredentialCandidate): Promise { const identity = await accountIdentity(credential.accessToken, credential.displayName); const data: AccountData = { accessToken: credential.accessToken, refreshToken: credential.refreshToken, displayName: credential.displayName ?? identity.displayName, projectId: credential.projectId ?? "bamboo-precept-lgxtn", expiresAtMs: credential.expiresAtMs ?? (credential.refreshToken ? Date.now() + 3500 * 1000 : null), quota: credential.quota ?? null, }; return { key: identity.key, privateData: data as unknown as JsonValue }; } export function accountData(resource: ResourceSnapshot): AccountData { const data = object(resource.privateData); const accessToken = text(data?.accessToken); if (!accessToken) throw new Error("Antigravity account resource is missing its access token"); return { accessToken, refreshToken: text(data?.refreshToken), displayName: text(data?.displayName) ?? "Antigravity account", projectId: text(data?.projectId) ?? "bamboo-precept-lgxtn", expiresAtMs: typeof data?.expiresAtMs === "number" ? data.expiresAtMs : null, quota: (data?.quota ?? null) as AccountQuota | null, }; } export function accountHeaders(data: AccountData): Record { return { authorization: `Bearer ${data.accessToken}`, accept: "application/json", "user-agent": ANTIGRAVITY_USER_AGENT, ...ANTIGRAVITY_CLIENT_HEADERS, }; } export function quotaState(quota: AccountQuota | null, nowMs = Date.now()): ResourceState { if (!quota || !quota.limitReached) return { status: "ready" }; const coolingUntil = quota.coolingUntilMs; if (coolingUntil !== null && coolingUntil > nowMs) { return { status: "cooling", retryAtMs: coolingUntil, message: "Antigravity rate limit reached; cooling down", }; } return { status: "ready" }; } export function quotaExhaustedPatch( data: AccountData, error?: string, nowMs = Date.now(), ): ResourcePatch { let retryAfterMs = 60 * 1000; if (error) { const match = error.match(/retry(?:_after|\s+after)?\s*[:=]?\s*(\d+)/i); if (match?.[1]) { const parsed = Number(match[1]); if (Number.isFinite(parsed) && parsed > 0) { retryAfterMs = parsed > 10_000_000 ? parsed - nowMs : parsed * 1000; } } } const coolingUntilMs = nowMs + Math.max(5000, retryAfterMs); const quota: AccountQuota = { planLabel: data.quota?.planLabel ?? "Antigravity / Gemini", limitReached: true, coolingUntilMs, updatedAtMs: nowMs, claude: data.quota?.claude ?? null, gemini: data.quota?.gemini ?? null, }; return { privateData: { ...data, quota } as unknown as JsonValue, state: quotaState(quota, nowMs), }; } export function presentAccount(resource: ResourceSnapshot): ResourceView { const data = accountData(resource); const metrics: ResourceMetric[] = []; if (data.quota?.claude) { metrics.push({ id: "claude", label: { "en-US": "Claude", "zh-CN": "Claude" }, unit: "percent", value: data.quota.claude.remainingPercent, ...(data.quota.claude.resetAtMs ? { resetAtMs: data.quota.claude.resetAtMs } : {}), }); } if (data.quota?.gemini) { metrics.push({ id: "gemini", label: { "en-US": "Gemini", "zh-CN": "Gemini" }, unit: "percent", value: data.quota.gemini.remainingPercent, ...(data.quota.gemini.resetAtMs ? { resetAtMs: data.quota.gemini.resetAtMs } : {}), }); } return { displayName: data.displayName, ...(data.quota?.planLabel ? { description: data.quota.planLabel } : {}), ...(metrics.length > 0 ? { metrics } : {}), }; } export async function refreshAccount( resource: ResourceSnapshot, context: PluginContext, ): Promise { const data = accountData(resource); let accessToken = data.accessToken; let refreshToken = data.refreshToken; let projectId = data.projectId ?? "bamboo-precept-lgxtn"; let expiresAtMs = data.expiresAtMs ?? null; if (refreshToken) { const response = await context.network.fetch(REFRESH_TOKEN_URL, { method: "POST", headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, grant_type: "refresh_token", refresh_token: refreshToken, }).toString(), }); if (response.status < 200 || response.status >= 300) { const bodyText = response.body.toLowerCase(); // Only mark invalid if token is revoked or client is invalid if (bodyText.includes("invalid_grant") || bodyText.includes("unauthorized_client")) { return { state: { status: "invalid", message: "Google authorization expired or revoked; please sign in again", }, }; } // On network glitches or temporary Google server errors, keep ready return { state: { status: "ready" }, }; } const body = object(JSON.parse(response.body)); accessToken = text(body?.access_token) ?? accessToken; refreshToken = text(body?.refresh_token) ?? refreshToken; const expiresIn = typeof body?.expires_in === "number" ? body.expires_in : 3600; expiresAtMs = Date.now() + expiresIn * 1000; } // Fetch real-time quota and project ID const result = await queryAccountQuota(accessToken, context.network); projectId = result.projectId || projectId; const updatedData: AccountData = { ...data, accessToken, refreshToken, projectId, expiresAtMs, quota: result.quota ?? data.quota, }; return { privateData: updatedData as unknown as JsonValue, state: { status: "ready" }, }; } function firstText(source: Record, keys: string[]): string | null { for (const key of keys) { const value = text(source[key]); if (value) return value; } return null; } function collectCredentials(value: unknown, output: CredentialCandidate[]): void { if (Array.isArray(value)) { for (const item of value) collectCredentials(item, output); return; } const item = object(value); if (!item || item.disabled === true) return; for (const key of ["accounts", "credentials", "items", "keys"]) { if (Array.isArray(item[key])) { collectCredentials(item[key], output); return; } } const tokens = object(item.tokens) ?? item; let accessToken = firstText(tokens, [ "access", "accessToken", "access_token", "token", "apiKey", "api_key", "key", "GEMINI_API_KEY", "GOOGLE_API_KEY", "ANTIGRAVITY_API_KEY", ]) ?? firstText(item, [ "access", "accessToken", "access_token", "token", "apiKey", "api_key", "key", "GEMINI_API_KEY", "GOOGLE_API_KEY", "ANTIGRAVITY_API_KEY", ]); const refreshToken = firstText(tokens, ["refresh", "refresh_token", "refreshToken"]) ?? firstText(item, ["refresh", "refresh_token", "refreshToken"]); const displayName = firstText(item, ["email", "display_name", "displayName", "name"]) ?? firstText(tokens, ["email", "display_name", "displayName", "name"]); const projectId = firstText(item, ["project", "projectId", "project_id", "cloudaicompanionProject"]) ?? firstText(tokens, ["project", "projectId", "project_id", "cloudaicompanionProject"]); if (!accessToken && !refreshToken) return; if (!accessToken && refreshToken) { accessToken = refreshToken; } if (!accessToken) return; output.push({ accessToken, refreshToken, displayName, projectId }); } export async function parseCredentialFiles( files: ResourceImportFile[], network?: PluginContext["network"], ): Promise<{ credentials: CredentialCandidate[]; warnings: string[]; }> { const credentials: CredentialCandidate[] = []; const warnings: string[] = []; for (const file of files) { const raw = file.content.trim(); if (!raw) continue; // Check if file is raw API key or JWT token string if (raw.startsWith("AIza") || (raw.split(".").length === 3 && !raw.includes(" "))) { credentials.push({ accessToken: raw, refreshToken: null, displayName: file.name }); continue; } // Try parsing as JSON let content: unknown; try { content = JSON.parse(raw); } catch { const envMatch = raw.match( /(?:API_KEY|TOKEN|GEMINI_API_KEY|GOOGLE_API_KEY|ANTIGRAVITY_API_KEY)\s*=\s*["']?([^"'\r\n]+)/i, ); if (envMatch?.[1]) { credentials.push({ accessToken: envMatch[1].trim(), refreshToken: null, displayName: file.name, }); continue; } const keyMatch = raw.match(/AIza[0-9A-Za-z-_]{35}/); if (keyMatch?.[0]) { credentials.push({ accessToken: keyMatch[0], refreshToken: null, displayName: file.name }); continue; } warnings.push(`${file.name}: not valid JSON or API key`); continue; } if (typeof content === "string") { credentials.push({ accessToken: content.trim(), refreshToken: null, displayName: file.name }); continue; } const found: CredentialCandidate[] = []; collectCredentials(content, found); if (found.length === 0) { warnings.push(`${file.name}: no Google/Antigravity API key or token found`); continue; } for (const candidate of found) { if (candidate.refreshToken && candidate.accessToken === candidate.refreshToken) { try { const response = await fetchText(network, REFRESH_TOKEN_URL, { method: "POST", headers: { accept: "application/json", "content-type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, grant_type: "refresh_token", refresh_token: candidate.refreshToken, }).toString(), }); const body = object(JSON.parse(response.body)); if (response.status >= 200 && response.status < 300 && text(body?.access_token)) { candidate.accessToken = text(body?.access_token)!; candidate.refreshToken = text(body?.refresh_token) ?? candidate.refreshToken; candidate.expiresAtMs = Date.now() + ((typeof body?.expires_in === "number" ? body.expires_in : 3600) * 1000); } } catch { // Keep placeholder } } credentials.push(candidate); } } return { credentials, warnings }; } export const credentialImport: ResourceImportSupport = { displayName: { "en-US": "Import Google / Antigravity Credentials", "zh-CN": "导入 Google / Antigravity 凭证", }, description: { "en-US": "Import a JSON, TXT, or environment file containing Antigravity tokens or Google API keys.", "zh-CN": "导入包含 Antigravity Token 或 Google API Key 的 JSON、TXT 或环境变量文件。", }, accept: [".json", ".txt", ".key", ".env"], multiple: true, parse: async ( files: ResourceImportFile[], context: PluginContext, ): Promise => { const { credentials, warnings } = await parseCredentialFiles(files, context.network); if (credentials.length === 0) { throw new Error( warnings.join("; ") || "credential file does not contain a valid token or API key", ); } const drafts = await Promise.all( credentials.map(async (c) => { try { const res = await queryAccountQuota(c.accessToken, context.network); c.quota = res.quota; c.projectId = res.projectId; } catch { // ignore error } return credentialDraft(c); }), ); return { resources: drafts, ...(warnings.length > 0 ? { warnings } : {}), }; }, };