diff --git a/packages/ai/src/utils/oauth/anthropic.ts b/packages/ai/src/utils/oauth/anthropic.ts index ea772e94..74a2228c 100644 --- a/packages/ai/src/utils/oauth/anthropic.ts +++ b/packages/ai/src/utils/oauth/anthropic.ts @@ -2,25 +2,16 @@ * Anthropic OAuth flow (Claude Pro/Max) */ -import { createHash, randomBytes } from "crypto"; +import { generatePKCE } from "./pkce.js"; import type { OAuthCredentials } from "./types.js"; -const decode = (s: string) => Buffer.from(s, "base64").toString(); +const decode = (s: string) => atob(s); const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl"); const AUTHORIZE_URL = "https://claude.ai/oauth/authorize"; const TOKEN_URL = "https://console.anthropic.com/v1/oauth/token"; const REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback"; const SCOPES = "org:create_api_key user:profile user:inference"; -/** - * Generate PKCE code verifier and challenge - */ -function generatePKCE(): { verifier: string; challenge: string } { - const verifier = randomBytes(32).toString("base64url"); - const challenge = createHash("sha256").update(verifier).digest("base64url"); - return { verifier, challenge }; -} - /** * Login with Anthropic OAuth (device code flow) * @@ -31,7 +22,7 @@ export async function loginAnthropic( onAuthUrl: (url: string) => void, onPromptCode: () => Promise, ): Promise { - const { verifier, challenge } = generatePKCE(); + const { verifier, challenge } = await generatePKCE(); // Build authorization URL const authParams = new URLSearchParams({ diff --git a/packages/ai/src/utils/oauth/google-antigravity.ts b/packages/ai/src/utils/oauth/google-antigravity.ts index 80deb402..20ad3e17 100644 --- a/packages/ai/src/utils/oauth/google-antigravity.ts +++ b/packages/ai/src/utils/oauth/google-antigravity.ts @@ -1,14 +1,17 @@ /** * Antigravity OAuth flow (Gemini 3, Claude, GPT-OSS via Google Cloud) * Uses different OAuth credentials than google-gemini-cli for access to additional models. + * + * NOTE: This module uses Node.js http.createServer for the OAuth callback. + * It is only intended for CLI use, not browser environments. */ -import { createHash, randomBytes } from "crypto"; import { createServer, type Server } from "http"; +import { generatePKCE } from "./pkce.js"; import type { OAuthCredentials } from "./types.js"; // Antigravity OAuth credentials (different from Gemini CLI) -const decode = (s: string) => Buffer.from(s, "base64").toString(); +const decode = (s: string) => atob(s); const CLIENT_ID = decode( "MTA3MTAwNjA2MDU5MS10bWhzc2luMmgyMWxjcmUyMzV2dG9sb2poNGc0MDNlcC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbQ==", ); @@ -30,15 +33,6 @@ const TOKEN_URL = "https://oauth2.googleapis.com/token"; // Fallback project ID when discovery fails const DEFAULT_PROJECT_ID = "rising-fact-p41fc"; -/** - * Generate PKCE code verifier and challenge - */ -function generatePKCE(): { verifier: string; challenge: string } { - const verifier = randomBytes(32).toString("base64url"); - const challenge = createHash("sha256").update(verifier).digest("base64url"); - return { verifier, challenge }; -} - /** * Start a local HTTP server to receive the OAuth callback */ @@ -232,7 +226,7 @@ export async function loginAntigravity( onAuth: (info: { url: string; instructions?: string }) => void, onProgress?: (message: string) => void, ): Promise { - const { verifier, challenge } = generatePKCE(); + const { verifier, challenge } = await generatePKCE(); // Start local server for callback onProgress?.("Starting local server for OAuth callback..."); diff --git a/packages/ai/src/utils/oauth/google-gemini-cli.ts b/packages/ai/src/utils/oauth/google-gemini-cli.ts index 6c06d375..68a80492 100644 --- a/packages/ai/src/utils/oauth/google-gemini-cli.ts +++ b/packages/ai/src/utils/oauth/google-gemini-cli.ts @@ -1,13 +1,16 @@ /** * Gemini CLI OAuth flow (Google Cloud Code Assist) * Standard Gemini models only (gemini-2.0-flash, gemini-2.5-*) + * + * NOTE: This module uses Node.js http.createServer for the OAuth callback. + * It is only intended for CLI use, not browser environments. */ -import { createHash, randomBytes } from "crypto"; import { createServer, type Server } from "http"; +import { generatePKCE } from "./pkce.js"; import type { OAuthCredentials } from "./types.js"; -const decode = (s: string) => Buffer.from(s, "base64").toString(); +const decode = (s: string) => atob(s); const CLIENT_ID = decode( "NjgxMjU1ODA5Mzk1LW9vOGZ0Mm9wcmRybnA5ZTNhcWY2YXYzaG1kaWIxMzVqLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t", ); @@ -22,15 +25,6 @@ const AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; const TOKEN_URL = "https://oauth2.googleapis.com/token"; const CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com"; -/** - * Generate PKCE code verifier and challenge - */ -function generatePKCE(): { verifier: string; challenge: string } { - const verifier = randomBytes(32).toString("base64url"); - const challenge = createHash("sha256").update(verifier).digest("base64url"); - return { verifier, challenge }; -} - /** * Start a local HTTP server to receive the OAuth callback */ @@ -263,7 +257,7 @@ export async function loginGeminiCli( onAuth: (info: { url: string; instructions?: string }) => void, onProgress?: (message: string) => void, ): Promise { - const { verifier, challenge } = generatePKCE(); + const { verifier, challenge } = await generatePKCE(); // Start local server for callback onProgress?.("Starting local server for OAuth callback..."); diff --git a/packages/ai/src/utils/oauth/pkce.ts b/packages/ai/src/utils/oauth/pkce.ts new file mode 100644 index 00000000..bf7ac7d5 --- /dev/null +++ b/packages/ai/src/utils/oauth/pkce.ts @@ -0,0 +1,34 @@ +/** + * PKCE utilities using Web Crypto API. + * Works in both Node.js 20+ and browsers. + */ + +/** + * Encode bytes as base64url string. + */ +function base64urlEncode(bytes: Uint8Array): string { + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); +} + +/** + * Generate PKCE code verifier and challenge. + * Uses Web Crypto API for cross-platform compatibility. + */ +export async function generatePKCE(): Promise<{ verifier: string; challenge: string }> { + // Generate random verifier + const verifierBytes = new Uint8Array(32); + crypto.getRandomValues(verifierBytes); + const verifier = base64urlEncode(verifierBytes); + + // Compute SHA-256 challenge + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const challenge = base64urlEncode(new Uint8Array(hashBuffer)); + + return { verifier, challenge }; +}