/** * Google Antigravity Auth Plugin for OpenCode * * Provides OAuth authentication for Google models via Antigravity API. * This plugin integrates with OpenCode's auth system to enable: * - OAuth 2.0 with PKCE flow for Google authentication * - Automatic token refresh * - Request/response transformation for Antigravity API * * @example * ```json * // opencode.json * { * "plugin": ["oh-my-opencode"], * "provider": { * "google": { * "options": { * "clientId": "custom-client-id", * "clientSecret": "custom-client-secret" * } * } * } * } * ``` */ import type { Auth, Provider } from "@opencode-ai/sdk" import type { AuthHook, AuthOuathResult, PluginInput } from "@opencode-ai/plugin" import { ANTIGRAVITY_CLIENT_ID, ANTIGRAVITY_CLIENT_SECRET } from "./constants" import { buildAuthURL, exchangeCode, startCallbackServer, fetchUserInfo, decodeState, } from "./oauth" import { createAntigravityFetch } from "./fetch" import { fetchProjectContext } from "./project" import { formatTokenForStorage } from "./token" /** * Provider ID for Google models * Antigravity is an auth method for Google, not a separate provider */ const GOOGLE_PROVIDER_ID = "google" /** * Type guard to check if auth is OAuth type */ function isOAuthAuth( auth: Auth ): auth is { type: "oauth"; access: string; refresh: string; expires: number } { return auth.type === "oauth" } /** * Creates the Google Antigravity OAuth plugin for OpenCode. * * This factory function creates an auth plugin that: * 1. Provides OAuth flow for Google authentication * 2. Creates a custom fetch interceptor for Antigravity API * 3. Handles token management and refresh * * @param input - Plugin input containing the OpenCode client * @returns Hooks object with auth configuration * * @example * ```typescript * // Used by OpenCode automatically when plugin is loaded * const hooks = await createGoogleAntigravityAuthPlugin({ client, ... }) * ``` */ export async function createGoogleAntigravityAuthPlugin({ client, }: PluginInput): Promise<{ auth: AuthHook }> { // Cache for custom credentials from provider.options // These are populated by loader() and used by authorize() // Falls back to defaults if loader hasn't been called yet let cachedClientId: string = ANTIGRAVITY_CLIENT_ID let cachedClientSecret: string = ANTIGRAVITY_CLIENT_SECRET const authHook: AuthHook = { /** * Provider identifier - must be "google" as Antigravity is * an auth method for Google models, not a separate provider */ provider: GOOGLE_PROVIDER_ID, /** * Loader function called when auth is needed. * Reads credentials from provider.options and creates custom fetch. * * @param auth - Function to retrieve current auth state * @param provider - Provider configuration including options * @returns Object with custom fetch function */ loader: async ( auth: () => Promise, provider: Provider ): Promise> => { const currentAuth = await auth() if (process.env.ANTIGRAVITY_DEBUG === "1") { console.log("[antigravity-plugin] loader called") console.log("[antigravity-plugin] auth type:", currentAuth?.type) console.log("[antigravity-plugin] auth keys:", Object.keys(currentAuth || {})) } if (!isOAuthAuth(currentAuth)) { if (process.env.ANTIGRAVITY_DEBUG === "1") { console.log("[antigravity-plugin] NOT OAuth auth, returning empty") } return {} } if (process.env.ANTIGRAVITY_DEBUG === "1") { console.log("[antigravity-plugin] OAuth auth detected, creating custom fetch") } cachedClientId = (provider.options?.clientId as string) || ANTIGRAVITY_CLIENT_ID cachedClientSecret = (provider.options?.clientSecret as string) || ANTIGRAVITY_CLIENT_SECRET // Log if using custom credentials (for debugging) if ( process.env.ANTIGRAVITY_DEBUG === "1" && (cachedClientId !== ANTIGRAVITY_CLIENT_ID || cachedClientSecret !== ANTIGRAVITY_CLIENT_SECRET) ) { console.log( "[antigravity-plugin] Using custom credentials from provider.options" ) } // Create adapter for client.auth.set that matches fetch.ts AuthClient interface const authClient = { set: async ( providerId: string, authData: { access?: string; refresh?: string; expires?: number } ) => { await client.auth.set({ body: { type: "oauth", access: authData.access || "", refresh: authData.refresh || "", expires: authData.expires || 0, }, path: { id: providerId }, }) }, } // Create auth getter that returns compatible format for fetch.ts const getAuth = async (): Promise<{ access?: string refresh?: string expires?: number }> => { const authState = await auth() if (isOAuthAuth(authState)) { return { access: authState.access, refresh: authState.refresh, expires: authState.expires, } } return {} } const antigravityFetch = createAntigravityFetch( getAuth, authClient, GOOGLE_PROVIDER_ID, cachedClientId, cachedClientSecret ) return { fetch: antigravityFetch, apiKey: "antigravity-oauth", } }, /** * Authentication methods available for this provider. * Only OAuth is supported - no prompts for credentials. */ methods: [ { type: "oauth", label: "OAuth with Google (Antigravity)", // NO prompts - credentials come from provider.options or defaults // OAuth flow starts immediately when user selects this method /** * Starts the OAuth authorization flow. * Opens browser for Google OAuth and waits for callback. * * @returns Authorization result with URL and callback */ authorize: async (): Promise => { const serverHandle = startCallbackServer() const { url, verifier } = await buildAuthURL(undefined, cachedClientId, serverHandle.port) return { url, instructions: "Complete the sign-in in your browser. We'll automatically detect when you're done.", method: "auto", callback: async () => { try { const result = await serverHandle.waitForCallback() if (result.error) { if (process.env.ANTIGRAVITY_DEBUG === "1") { console.error(`[antigravity-plugin] OAuth error: ${result.error}`) } return { type: "failed" as const } } if (!result.code) { if (process.env.ANTIGRAVITY_DEBUG === "1") { console.error("[antigravity-plugin] No authorization code received") } return { type: "failed" as const } } const state = decodeState(result.state) if (state.verifier !== verifier) { if (process.env.ANTIGRAVITY_DEBUG === "1") { console.error("[antigravity-plugin] PKCE verifier mismatch") } return { type: "failed" as const } } const tokens = await exchangeCode(result.code, verifier, cachedClientId, cachedClientSecret, serverHandle.port) try { const userInfo = await fetchUserInfo(tokens.access_token) if (process.env.ANTIGRAVITY_DEBUG === "1") { console.log(`[antigravity-plugin] Authenticated as: ${userInfo.email}`) } } catch { // User info is optional } const projectContext = await fetchProjectContext(tokens.access_token) const formattedRefresh = formatTokenForStorage( tokens.refresh_token, projectContext.cloudaicompanionProject || "", projectContext.managedProjectId ) return { type: "success" as const, access: tokens.access_token, refresh: formattedRefresh, expires: Date.now() + tokens.expires_in * 1000, } } catch (error) { serverHandle.close() if (process.env.ANTIGRAVITY_DEBUG === "1") { console.error( `[antigravity-plugin] OAuth flow failed: ${ error instanceof Error ? error.message : "Unknown error" }` ) } return { type: "failed" as const } } }, } }, }, ], } return { auth: authHook, } } /** * Default export for OpenCode plugin system */ export default createGoogleAntigravityAuthPlugin /** * Named export for explicit imports */ export const GoogleAntigravityAuthPlugin = createGoogleAntigravityAuthPlugin