- Separate Google Antigravity auth to 'oh-my-opencode/google-auth' subpath - 'oh-my-opencode/auth' now exports OpenAI Codex auth plugin - Implement dynamic port allocation to avoid port conflicts - Add userAgent, requestId, sessionId fields for Antigravity API compatibility - Add debug logging for troubleshooting (ANTIGRAVITY_DEBUG=1) 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
296 lines
9.3 KiB
TypeScript
296 lines
9.3 KiB
TypeScript
/**
|
|
* 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<Auth>,
|
|
provider: Provider
|
|
): Promise<Record<string, unknown>> => {
|
|
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<AuthOuathResult> => {
|
|
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
|