diff --git a/package.json b/package.json index 10c3d64..fc3532f 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,14 @@ "types": "./dist/auth.d.ts", "import": "./dist/auth.js" }, + "./google-auth": { + "types": "./dist/google-auth.d.ts", + "import": "./dist/google-auth.js" + }, "./schema.json": "./dist/oh-my-opencode.schema.json" }, "scripts": { - "build": "bun build src/index.ts src/auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun run build:schema", + "build": "bun build src/index.ts src/auth.ts src/google-auth.ts --outdir dist --target bun --format esm --external @ast-grep/napi && tsc --emitDeclarationOnly && bun run build:schema", "build:schema": "bun run script/build-schema.ts", "clean": "rm -rf dist", "prepublishOnly": "bun run clean && bun run build", diff --git a/src/auth.ts b/src/auth.ts index 6fa7ad5..74b8b56 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1 +1,11 @@ -export { createGoogleAntigravityAuthPlugin as default } from "./auth/antigravity" +/** + * OpenAI Codex Auth Plugin re-export + * + * This module re-exports the OpenAI Codex OAuth authentication plugin, + * enabling users to authenticate with their ChatGPT Plus/Pro subscription. + * + * For Google Antigravity auth, use `oh-my-opencode/google-auth` instead. + * + * @see https://github.com/numman-ali/opencode-openai-codex-auth + */ +export { OpenAIAuthPlugin as default } from "opencode-openai-codex-auth" diff --git a/src/auth/antigravity/oauth.ts b/src/auth/antigravity/oauth.ts index 74b542e..7e76b44 100644 --- a/src/auth/antigravity/oauth.ts +++ b/src/auth/antigravity/oauth.ts @@ -117,16 +117,10 @@ export function decodeState(encoded: string): OAuthState { } } -/** - * Build the OAuth authorization URL with PKCE. - * - * @param projectId - Optional GCP project ID to include in state - * @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID) - * @returns Authorization result with URL and verifier - */ export async function buildAuthURL( projectId?: string, - clientId: string = ANTIGRAVITY_CLIENT_ID + clientId: string = ANTIGRAVITY_CLIENT_ID, + port: number = ANTIGRAVITY_CALLBACK_PORT ): Promise { const pkce = await generatePKCEPair() @@ -135,9 +129,11 @@ export async function buildAuthURL( projectId, } + const redirectUri = `http://localhost:${port}/oauth-callback` + const url = new URL(GOOGLE_AUTH_URL) url.searchParams.set("client_id", clientId) - url.searchParams.set("redirect_uri", ANTIGRAVITY_REDIRECT_URI) + url.searchParams.set("redirect_uri", redirectUri) url.searchParams.set("response_type", "code") url.searchParams.set("scope", ANTIGRAVITY_SCOPES.join(" ")) url.searchParams.set("state", encodeState(state)) @@ -165,14 +161,16 @@ export async function exchangeCode( code: string, verifier: string, clientId: string = ANTIGRAVITY_CLIENT_ID, - clientSecret: string = ANTIGRAVITY_CLIENT_SECRET + clientSecret: string = ANTIGRAVITY_CLIENT_SECRET, + port: number = ANTIGRAVITY_CALLBACK_PORT ): Promise { + const redirectUri = `http://localhost:${port}/oauth-callback` const params = new URLSearchParams({ client_id: clientId, client_secret: clientSecret, code, grant_type: "authorization_code", - redirect_uri: ANTIGRAVITY_REDIRECT_URI, + redirect_uri: redirectUri, code_verifier: verifier, }) @@ -236,97 +234,88 @@ export async function fetchUserInfo( } } -/** - * Start a local HTTP server to receive OAuth callback. - * - * @param timeoutMs - Timeout in milliseconds (default: 5 minutes) - * @returns Promise that resolves with callback result - */ -export function startCallbackServer( - timeoutMs: number = 5 * 60 * 1000 -): Promise { - return new Promise((resolve, reject) => { - let server: ReturnType | null = null - let timeoutId: ReturnType | null = null - - const cleanup = () => { - if (timeoutId) { - clearTimeout(timeoutId) - timeoutId = null - } - if (server) { - server.stop() - server = null - } - } - - // Set timeout - timeoutId = setTimeout(() => { - cleanup() - reject(new Error("OAuth callback timeout")) - }, timeoutMs) - - try { - server = Bun.serve({ - port: ANTIGRAVITY_CALLBACK_PORT, - fetch(request: Request): Response { - const url = new URL(request.url) - - if (url.pathname === "/oauth-callback") { - const code = url.searchParams.get("code") || "" - const state = url.searchParams.get("state") || "" - const error = url.searchParams.get("error") || undefined - - // Respond to browser - let responseBody: string - if (code && !error) { - responseBody = - "

Login successful

You can close this window.

" - } else { - responseBody = - "

Login failed

Please check the CLI output.

" - } - - // Schedule cleanup and resolve - setTimeout(() => { - cleanup() - resolve({ code, state, error }) - }, 100) - - return new Response(responseBody, { - status: 200, - headers: { "Content-Type": "text/html" }, - }) - } - - return new Response("Not Found", { status: 404 }) - }, - }) - } catch (err) { - cleanup() - reject( - new Error( - `Failed to start callback server: ${err instanceof Error ? err.message : String(err)}` - ) - ) - } - }) +export interface CallbackServerHandle { + port: number + waitForCallback: () => Promise + close: () => void +} + +export function startCallbackServer( + timeoutMs: number = 5 * 60 * 1000 +): CallbackServerHandle { + let server: ReturnType | null = null + let timeoutId: ReturnType | null = null + let resolveCallback: ((result: CallbackResult) => void) | null = null + let rejectCallback: ((error: Error) => void) | null = null + + const cleanup = () => { + if (timeoutId) { + clearTimeout(timeoutId) + timeoutId = null + } + if (server) { + server.stop() + server = null + } + } + + server = Bun.serve({ + port: 0, + fetch(request: Request): Response { + const url = new URL(request.url) + + if (url.pathname === "/oauth-callback") { + const code = url.searchParams.get("code") || "" + const state = url.searchParams.get("state") || "" + const error = url.searchParams.get("error") || undefined + + let responseBody: string + if (code && !error) { + responseBody = + "

Login successful

You can close this window.

" + } else { + responseBody = + "

Login failed

Please check the CLI output.

" + } + + setTimeout(() => { + cleanup() + if (resolveCallback) { + resolveCallback({ code, state, error }) + } + }, 100) + + return new Response(responseBody, { + status: 200, + headers: { "Content-Type": "text/html" }, + }) + } + + return new Response("Not Found", { status: 404 }) + }, + }) + + const actualPort = server.port as number + + const waitForCallback = (): Promise => { + return new Promise((resolve, reject) => { + resolveCallback = resolve + rejectCallback = reject + + timeoutId = setTimeout(() => { + cleanup() + reject(new Error("OAuth callback timeout")) + }, timeoutMs) + }) + } + + return { + port: actualPort, + waitForCallback, + close: cleanup, + } } -/** - * Perform complete OAuth flow: - * 1. Start callback server - * 2. Build auth URL - * 3. Wait for callback - * 4. Exchange code for tokens - * 5. Fetch user info - * - * @param projectId - Optional GCP project ID - * @param openBrowser - Function to open URL in browser - * @param clientId - Optional custom client ID (defaults to ANTIGRAVITY_CLIENT_ID) - * @param clientSecret - Optional custom client secret (defaults to ANTIGRAVITY_CLIENT_SECRET) - * @returns Object with tokens and user info - */ export async function performOAuthFlow( projectId?: string, openBrowser?: (url: string) => Promise, @@ -337,43 +326,36 @@ export async function performOAuthFlow( userInfo: AntigravityUserInfo verifier: string }> { - // Build auth URL first to get the verifier - const auth = await buildAuthURL(projectId, clientId) + const serverHandle = startCallbackServer() - // Start callback server - const callbackPromise = startCallbackServer() + try { + const auth = await buildAuthURL(projectId, clientId, serverHandle.port) - // Open browser (caller provides implementation) - if (openBrowser) { - await openBrowser(auth.url) - } + if (openBrowser) { + await openBrowser(auth.url) + } - // Wait for callback - const callback = await callbackPromise + const callback = await serverHandle.waitForCallback() - if (callback.error) { - throw new Error(`OAuth error: ${callback.error}`) - } + if (callback.error) { + throw new Error(`OAuth error: ${callback.error}`) + } - if (!callback.code) { - throw new Error("No authorization code received") - } + if (!callback.code) { + throw new Error("No authorization code received") + } - // Verify state and extract verifier - const state = decodeState(callback.state) - if (state.verifier !== auth.verifier) { - throw new Error("PKCE verifier mismatch - possible CSRF attack") - } + const state = decodeState(callback.state) + if (state.verifier !== auth.verifier) { + throw new Error("PKCE verifier mismatch - possible CSRF attack") + } - // Exchange code for tokens - const tokens = await exchangeCode(callback.code, auth.verifier, clientId, clientSecret) + const tokens = await exchangeCode(callback.code, auth.verifier, clientId, clientSecret, serverHandle.port) + const userInfo = await fetchUserInfo(tokens.access_token) - // Fetch user info - const userInfo = await fetchUserInfo(tokens.access_token) - - return { - tokens, - userInfo, - verifier: auth.verifier, + return { tokens, userInfo, verifier: auth.verifier } + } catch (err) { + serverHandle.close() + throw err } } diff --git a/src/auth/antigravity/plugin.ts b/src/auth/antigravity/plugin.ts index c51012b..c679738 100644 --- a/src/auth/antigravity/plugin.ts +++ b/src/auth/antigravity/plugin.ts @@ -99,12 +99,24 @@ export async function createGoogleAntigravityAuthPlugin({ auth: () => Promise, provider: Provider ): Promise> => { - // Check if current auth is OAuth type 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)) { - // Not OAuth auth, return empty (fallback to default fetch) + 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 @@ -167,6 +179,7 @@ export async function createGoogleAntigravityAuthPlugin({ return { fetch: antigravityFetch, + apiKey: "antigravity-oauth", } }, @@ -188,10 +201,8 @@ export async function createGoogleAntigravityAuthPlugin({ * @returns Authorization result with URL and callback */ authorize: async (): Promise => { - const { url, verifier } = await buildAuthURL(undefined, cachedClientId) - - // Start local callback server to receive OAuth callback - const callbackPromise = startCallbackServer() + const serverHandle = startCallbackServer() + const { url, verifier } = await buildAuthURL(undefined, cachedClientId, serverHandle.port) return { url, @@ -199,35 +210,24 @@ export async function createGoogleAntigravityAuthPlugin({ "Complete the sign-in in your browser. We'll automatically detect when you're done.", method: "auto", - /** - * Callback function invoked when OAuth redirect is received. - * Exchanges code for tokens and fetches project context. - */ callback: async () => { try { - // Wait for OAuth callback - const result = await callbackPromise + const result = await serverHandle.waitForCallback() - // Check for errors if (result.error) { if (process.env.ANTIGRAVITY_DEBUG === "1") { - console.error( - `[antigravity-plugin] OAuth error: ${result.error}` - ) + 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" - ) + console.error("[antigravity-plugin] No authorization code received") } return { type: "failed" as const } } - // Verify state and extract original verifier const state = decodeState(result.state) if (state.verifier !== verifier) { if (process.env.ANTIGRAVITY_DEBUG === "1") { @@ -236,27 +236,19 @@ export async function createGoogleAntigravityAuthPlugin({ return { type: "failed" as const } } - const tokens = await exchangeCode(result.code, verifier, cachedClientId, cachedClientSecret) + const tokens = await exchangeCode(result.code, verifier, cachedClientId, cachedClientSecret, serverHandle.port) - // Fetch user info (optional, for logging) try { const userInfo = await fetchUserInfo(tokens.access_token) if (process.env.ANTIGRAVITY_DEBUG === "1") { - console.log( - `[antigravity-plugin] Authenticated as: ${userInfo.email}` - ) + console.log(`[antigravity-plugin] Authenticated as: ${userInfo.email}`) } } catch { - // User info is optional, continue without it + // User info is optional } - // Fetch project context for Antigravity API - const projectContext = await fetchProjectContext( - tokens.access_token - ) + const projectContext = await fetchProjectContext(tokens.access_token) - // Format refresh token with project info for storage - // Format: refreshToken|projectId|managedProjectId const formattedRefresh = formatTokenForStorage( tokens.refresh_token, projectContext.cloudaicompanionProject || "", @@ -270,6 +262,7 @@ export async function createGoogleAntigravityAuthPlugin({ expires: Date.now() + tokens.expires_in * 1000, } } catch (error) { + serverHandle.close() if (process.env.ANTIGRAVITY_DEBUG === "1") { console.error( `[antigravity-plugin] OAuth flow failed: ${ diff --git a/src/auth/antigravity/request.ts b/src/auth/antigravity/request.ts index fe1954b..72bcd46 100644 --- a/src/auth/antigravity/request.ts +++ b/src/auth/antigravity/request.ts @@ -128,30 +128,32 @@ export function getDefaultEndpoint(): string { return ANTIGRAVITY_ENDPOINT_FALLBACKS[0] } -/** - * Wrap a request body in Antigravity format. - * Creates a new object without modifying the original. - * - * @param body - Original request payload - * @param projectId - GCP project ID - * @param modelName - Model identifier - * @returns Wrapped request body in Antigravity format - */ +function generateRequestId(): string { + return `agent-${crypto.randomUUID()}` +} + +function generateSessionId(): string { + const n = Math.floor(Math.random() * 9_000_000_000_000_000_000) + return `-${n}` +} + export function wrapRequestBody( body: Record, projectId: string, modelName: string ): AntigravityRequestBody { - // Clone the body to avoid mutation const requestPayload = { ...body } - - // Remove model from inner request (it's in wrapper) delete requestPayload.model return { project: projectId, model: modelName, - request: requestPayload, + userAgent: "antigravity", + requestId: generateRequestId(), + request: { + ...requestPayload, + sessionId: generateSessionId(), + }, } } diff --git a/src/auth/antigravity/types.ts b/src/auth/antigravity/types.ts index d88d517..1f03fc3 100644 --- a/src/auth/antigravity/types.ts +++ b/src/auth/antigravity/types.ts @@ -73,6 +73,10 @@ export interface AntigravityRequestBody { project: string /** Model identifier (e.g., "gemini-3-pro-preview") */ model: string + /** User agent identifier */ + userAgent: string + /** Unique request ID */ + requestId: string /** The actual request payload */ request: Record } diff --git a/src/google-auth.ts b/src/google-auth.ts new file mode 100644 index 0000000..5a88e8d --- /dev/null +++ b/src/google-auth.ts @@ -0,0 +1,8 @@ +import type { Plugin } from "@opencode-ai/plugin" +import { createGoogleAntigravityAuthPlugin } from "./auth/antigravity" + +const GoogleAntigravityAuthPlugin: Plugin = async (ctx) => { + return createGoogleAntigravityAuthPlugin(ctx) +} + +export default GoogleAntigravityAuthPlugin