{"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../../src/utils/oauth/gemini.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAuB,WAAW,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAqD7G,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAgCzG;AA0JD;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE;IAC1C,MAAM,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/D,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1C,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmI5B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuCxF;AAED,eAAO,MAAM,mBAAmB,EAAE,sBAwBjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,wBAAsB,CAAC","sourcesContent":["/**\n * Google Gemini OAuth flow\n *\n * NOTE: This module uses Node.js http.createServer for the OAuth callback server.\n * It is only intended for CLI use, not browser environments.\n */\n\nimport type { Server } from \"node:http\";\nimport { oauthErrorHtml, oauthSuccessHtml } from \"./oauth-page.js\";\nimport { generatePKCE } from \"./pkce.js\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthPrompt, OAuthProviderInterface } from \"./types.js\";\n\ntype CallbackServerInfo = {\n\tserver: Server;\n\tredirectUri: string;\n\tcancelWait: () => void;\n\twaitForCode: () => Promise<{ code: string; state: string } | null>;\n};\n\ntype NodeApis = {\n\tcreateServer: typeof import(\"node:http\").createServer;\n};\n\nlet nodeApis: NodeApis | null = null;\nlet nodeApisPromise: Promise<NodeApis> | null = null;\n\nconst DEFAULT_CLIENT_ID = \"681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com\";\nconst AUTHORIZE_URL = \"https://accounts.google.com/o/oauth2/v2/auth\";\nconst TOKEN_URL = \"https://oauth2.googleapis.com/token\";\nconst CALLBACK_HOST = process.env.PI_OAUTH_CALLBACK_HOST || \"127.0.0.1\";\nconst CALLBACK_PORT = Number(process.env.GEMINI_OAUTH_CALLBACK_PORT || process.env.GOOGLE_OAUTH_CALLBACK_PORT || 53694);\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;\nconst SCOPES = [\n\t\"https://www.googleapis.com/auth/generative-language\",\n\t\"https://www.googleapis.com/auth/cloud-platform\",\n\t\"openid\",\n\t\"https://www.googleapis.com/auth/userinfo.email\",\n\t\"https://www.googleapis.com/auth/userinfo.profile\",\n].join(\" \");\n\nfunction getClientId(): string {\n\treturn process.env.GEMINI_OAUTH_CLIENT_ID || process.env.GOOGLE_OAUTH_CLIENT_ID || DEFAULT_CLIENT_ID;\n}\n\nfunction getClientSecret(): string | undefined {\n\treturn process.env.GEMINI_OAUTH_CLIENT_SECRET || process.env.GOOGLE_OAUTH_CLIENT_SECRET || undefined;\n}\n\nasync function getNodeApis(): Promise<NodeApis> {\n\tif (nodeApis) return nodeApis;\n\tif (!nodeApisPromise) {\n\t\tif (typeof process === \"undefined\" || (!process.versions?.node && !process.versions?.bun)) {\n\t\t\tthrow new Error(\"Gemini OAuth is only available in Node.js environments\");\n\t\t}\n\t\tnodeApisPromise = import(\"node:http\").then((httpModule) => ({\n\t\t\tcreateServer: httpModule.createServer,\n\t\t}));\n\t}\n\tnodeApis = await nodeApisPromise;\n\treturn nodeApis;\n}\n\nexport function parseAuthorizationInput(input: string): { code?: string; state?: string; apiKey?: string } {\n\tconst value = input.trim();\n\tif (!value) return {};\n\n\tif (value.startsWith(\"AIza\") || (value.startsWith(\"ya29.\") && !value.includes(\"=\") && !value.includes(\"&\"))) {\n\t\treturn { apiKey: value };\n\t}\n\n\ttry {\n\t\tconst url = new URL(value);\n\t\treturn {\n\t\t\tcode: url.searchParams.get(\"code\") ?? undefined,\n\t\t\tstate: url.searchParams.get(\"state\") ?? undefined,\n\t\t};\n\t} catch {\n\t\t// not a URL\n\t}\n\n\tif (value.includes(\"#\")) {\n\t\tconst [code, state] = value.split(\"#\", 2);\n\t\treturn { code, state };\n\t}\n\n\tif (value.includes(\"code=\")) {\n\t\tconst params = new URLSearchParams(value);\n\t\treturn {\n\t\t\tcode: params.get(\"code\") ?? undefined,\n\t\t\tstate: params.get(\"state\") ?? undefined,\n\t\t};\n\t}\n\n\treturn { code: value };\n}\n\nfunction formatErrorDetails(error: unknown): string {\n\tif (error instanceof Error) {\n\t\tconst details: string[] = [`${error.name}: ${error.message}`];\n\t\tconst errorWithCode = error as Error & { code?: string; errno?: number | string; cause?: unknown };\n\t\tif (errorWithCode.code) details.push(`code=${errorWithCode.code}`);\n\t\tif (typeof errorWithCode.errno !== \"undefined\") details.push(`errno=${String(errorWithCode.errno)}`);\n\t\tif (typeof error.cause !== \"undefined\") {\n\t\t\tdetails.push(`cause=${formatErrorDetails(error.cause)}`);\n\t\t}\n\t\tif (error.stack) {\n\t\t\tdetails.push(`stack=${error.stack}`);\n\t\t}\n\t\treturn details.join(\"; \");\n\t}\n\treturn String(error);\n}\n\nasync function startCallbackServer(expectedState: string): Promise<CallbackServerInfo> {\n\tconst { createServer } = await getNodeApis();\n\n\treturn new Promise((resolve, reject) => {\n\t\tlet settleWait: ((value: { code: string; state: string } | null) => void) | undefined;\n\t\tconst waitForCodePromise = new Promise<{ code: string; state: string } | null>((resolveWait) => {\n\t\t\tlet settled = false;\n\t\t\tsettleWait = (value) => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tresolveWait(value);\n\t\t\t};\n\t\t});\n\n\t\tconst server = createServer((req, res) => {\n\t\t\ttry {\n\t\t\t\tconst url = new URL(req.url || \"\", \"http://localhost\");\n\t\t\t\tif (url.pathname !== CALLBACK_PATH && url.pathname !== \"/callback\") {\n\t\t\t\t\tres.writeHead(404, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml(\"Callback route not found.\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst code = url.searchParams.get(\"code\");\n\t\t\t\tconst state = url.searchParams.get(\"state\");\n\t\t\t\tconst error = url.searchParams.get(\"error\");\n\n\t\t\t\tif (error) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml(\"Google Gemini authentication did not complete.\", `Error: ${error}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (!code || !state) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml(\"Missing code or state parameter.\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (state !== expectedState) {\n\t\t\t\t\tres.writeHead(400, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\t\tres.end(oauthErrorHtml(\"State mismatch.\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tres.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n\t\t\t\tres.end(oauthSuccessHtml(\"Google Gemini authentication completed. You can close this window.\"));\n\t\t\t\tsettleWait?.({ code, state });\n\t\t\t} catch {\n\t\t\t\tres.writeHead(500, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n\t\t\t\tres.end(\"Internal error\");\n\t\t\t}\n\t\t});\n\n\t\tserver.on(\"error\", (err) => {\n\t\t\treject(err);\n\t\t});\n\n\t\tserver.listen(CALLBACK_PORT, CALLBACK_HOST, () => {\n\t\t\tresolve({\n\t\t\t\tserver,\n\t\t\t\tredirectUri: REDIRECT_URI,\n\t\t\t\tcancelWait: () => {\n\t\t\t\t\tsettleWait?.(null);\n\t\t\t\t},\n\t\t\t\twaitForCode: () => waitForCodePromise,\n\t\t\t});\n\t\t});\n\t});\n}\n\nasync function postForm(url: string, params: Record<string, string>): Promise<string> {\n\tconst body = new URLSearchParams(params);\n\tconst response = await fetch(url, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/x-www-form-urlencoded\",\n\t\t\tAccept: \"application/json\",\n\t\t},\n\t\tbody: body.toString(),\n\t\tsignal: AbortSignal.timeout(30_000),\n\t});\n\n\tconst responseBody = await response.text();\n\n\tif (!response.ok) {\n\t\tthrow new Error(`HTTP request failed. status=${response.status}; url=${url}; body=${responseBody}`);\n\t}\n\n\treturn responseBody;\n}\n\nasync function exchangeAuthorizationCode(\n\tcode: string,\n\tverifier: string,\n\tredirectUri: string,\n): Promise<OAuthCredentials> {\n\tconst clientId = getClientId();\n\tconst clientSecret = getClientSecret();\n\tconst payload: Record<string, string> = {\n\t\tgrant_type: \"authorization_code\",\n\t\tclient_id: clientId,\n\t\tcode,\n\t\tredirect_uri: redirectUri,\n\t\tcode_verifier: verifier,\n\t};\n\tif (clientSecret) {\n\t\tpayload.client_secret = clientSecret;\n\t}\n\n\tlet responseBody: string;\n\ttry {\n\t\tresponseBody = await postForm(TOKEN_URL, payload);\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Token exchange request failed. url=${TOKEN_URL}; redirect_uri=${redirectUri}; details=${formatErrorDetails(error)}`,\n\t\t);\n\t}\n\n\tlet tokenData: { access_token: string; refresh_token?: string; expires_in?: number };\n\ttry {\n\t\ttokenData = JSON.parse(responseBody) as { access_token: string; refresh_token?: string; expires_in?: number };\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Token exchange returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`,\n\t\t);\n\t}\n\n\treturn {\n\t\taccess: tokenData.access_token,\n\t\trefresh: tokenData.refresh_token || \"\",\n\t\texpires: Date.now() + (tokenData.expires_in ?? 3600) * 1000 - 5 * 60 * 1000,\n\t};\n}\n\n/**\n * Login with Google Gemini OAuth (authorization code + PKCE)\n */\nexport async function loginGemini(options: {\n\tonAuth: (info: { url: string; instructions?: string }) => void;\n\tonPrompt: (prompt: OAuthPrompt) => Promise<string>;\n\tonProgress?: (message: string) => void;\n\tonManualCodeInput?: () => Promise<string>;\n}): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\tconst server = await startCallbackServer(verifier);\n\n\tlet code: string | undefined;\n\tlet state: string | undefined;\n\tlet manualApiKey: string | undefined;\n\tlet redirectUriForExchange = REDIRECT_URI;\n\n\ttry {\n\t\tconst clientId = getClientId();\n\t\tconst authParams = new URLSearchParams({\n\t\t\tclient_id: clientId,\n\t\t\tresponse_type: \"code\",\n\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\tscope: SCOPES,\n\t\t\tcode_challenge: challenge,\n\t\t\tcode_challenge_method: \"S256\",\n\t\t\tstate: verifier,\n\t\t\taccess_type: \"offline\",\n\t\t\tprompt: \"consent\",\n\t\t});\n\n\t\toptions.onAuth({\n\t\t\turl: `${AUTHORIZE_URL}?${authParams.toString()}`,\n\t\t\tinstructions:\n\t\t\t\t\"Complete login in your browser. If the browser is on another machine, paste the final redirect URL or an API key here.\",\n\t\t});\n\n\t\tif (options.onManualCodeInput) {\n\t\t\tlet manualInput: string | undefined;\n\t\t\tlet manualError: Error | undefined;\n\t\t\tconst manualPromise = options\n\t\t\t\t.onManualCodeInput()\n\t\t\t\t.then((input) => {\n\t\t\t\t\tmanualInput = input;\n\t\t\t\t\tserver.cancelWait();\n\t\t\t\t})\n\t\t\t\t.catch((err) => {\n\t\t\t\t\tmanualError = err instanceof Error ? err : new Error(String(err));\n\t\t\t\t\tserver.cancelWait();\n\t\t\t\t});\n\n\t\t\tconst result = await server.waitForCode();\n\n\t\t\tif (manualError) {\n\t\t\t\tthrow manualError;\n\t\t\t}\n\n\t\t\tif (result?.code) {\n\t\t\t\tcode = result.code;\n\t\t\t\tstate = result.state;\n\t\t\t\tredirectUriForExchange = REDIRECT_URI;\n\t\t\t} else if (manualInput) {\n\t\t\t\tconst parsed = parseAuthorizationInput(manualInput);\n\t\t\t\tif (parsed.apiKey) {\n\t\t\t\t\tmanualApiKey = parsed.apiKey;\n\t\t\t\t} else {\n\t\t\t\t\tif (parsed.state && parsed.state !== verifier) {\n\t\t\t\t\t\tthrow new Error(\"OAuth state mismatch\");\n\t\t\t\t\t}\n\t\t\t\t\tcode = parsed.code;\n\t\t\t\t\tstate = parsed.state ?? verifier;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!code && !manualApiKey) {\n\t\t\t\tawait manualPromise;\n\t\t\t\tif (manualError) {\n\t\t\t\t\tthrow manualError;\n\t\t\t\t}\n\t\t\t\tif (manualInput) {\n\t\t\t\t\tconst parsed = parseAuthorizationInput(manualInput);\n\t\t\t\t\tif (parsed.apiKey) {\n\t\t\t\t\t\tmanualApiKey = parsed.apiKey;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif (parsed.state && parsed.state !== verifier) {\n\t\t\t\t\t\t\tthrow new Error(\"OAuth state mismatch\");\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcode = parsed.code;\n\t\t\t\t\t\tstate = parsed.state ?? verifier;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst result = await server.waitForCode();\n\t\t\tif (result?.code) {\n\t\t\t\tcode = result.code;\n\t\t\t\tstate = result.state;\n\t\t\t\tredirectUriForExchange = REDIRECT_URI;\n\t\t\t}\n\t\t}\n\n\t\tif (!code && !manualApiKey) {\n\t\t\tconst input = await options.onPrompt({\n\t\t\t\tmessage: \"Paste the authorization code, redirect URL, or Gemini API key:\",\n\t\t\t\tplaceholder: REDIRECT_URI,\n\t\t\t});\n\t\t\tconst parsed = parseAuthorizationInput(input);\n\t\t\tif (parsed.apiKey) {\n\t\t\t\tmanualApiKey = parsed.apiKey;\n\t\t\t} else {\n\t\t\t\tif (parsed.state && parsed.state !== verifier) {\n\t\t\t\t\tthrow new Error(\"OAuth state mismatch\");\n\t\t\t\t}\n\t\t\t\tcode = parsed.code;\n\t\t\t\tstate = parsed.state ?? verifier;\n\t\t\t}\n\t\t}\n\n\t\tif (manualApiKey) {\n\t\t\treturn {\n\t\t\t\taccess: manualApiKey,\n\t\t\t\trefresh: \"\",\n\t\t\t\texpires: Date.now() + 365 * 24 * 60 * 60 * 1000,\n\t\t\t};\n\t\t}\n\n\t\tif (!code) {\n\t\t\tthrow new Error(\"Missing authorization code\");\n\t\t}\n\n\t\tif (!state) {\n\t\t\tthrow new Error(\"Missing OAuth state\");\n\t\t}\n\n\t\toptions.onProgress?.(\"Exchanging authorization code for tokens...\");\n\t\treturn exchangeAuthorizationCode(code, verifier, redirectUriForExchange);\n\t} finally {\n\t\tserver.server.close();\n\t}\n}\n\n/**\n * Refresh Google Gemini OAuth token\n */\nexport async function refreshGeminiToken(refreshToken: string): Promise<OAuthCredentials> {\n\tif (!refreshToken) {\n\t\tthrow new Error(\"No refresh token available for Google Gemini\");\n\t}\n\n\tconst clientId = getClientId();\n\tconst clientSecret = getClientSecret();\n\tconst payload: Record<string, string> = {\n\t\tgrant_type: \"refresh_token\",\n\t\tclient_id: clientId,\n\t\trefresh_token: refreshToken,\n\t};\n\tif (clientSecret) {\n\t\tpayload.client_secret = clientSecret;\n\t}\n\n\tlet responseBody: string;\n\ttry {\n\t\tresponseBody = await postForm(TOKEN_URL, payload);\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Google Gemini token refresh request failed. url=${TOKEN_URL}; details=${formatErrorDetails(error)}`,\n\t\t);\n\t}\n\n\tlet data: { access_token: string; refresh_token?: string; expires_in?: number };\n\ttry {\n\t\tdata = JSON.parse(responseBody) as { access_token: string; refresh_token?: string; expires_in?: number };\n\t} catch (error) {\n\t\tthrow new Error(\n\t\t\t`Google Gemini token refresh returned invalid JSON. url=${TOKEN_URL}; body=${responseBody}; details=${formatErrorDetails(error)}`,\n\t\t);\n\t}\n\n\treturn {\n\t\taccess: data.access_token,\n\t\trefresh: data.refresh_token || refreshToken,\n\t\texpires: Date.now() + (data.expires_in ?? 3600) * 1000 - 5 * 60 * 1000,\n\t};\n}\n\nexport const geminiOAuthProvider: OAuthProviderInterface = {\n\tid: \"google\",\n\tname: \"Google Gemini\",\n\tusesCallbackServer: true,\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginGemini({\n\t\t\tonAuth: callbacks.onAuth,\n\t\t\tonPrompt: callbacks.onPrompt,\n\t\t\tonProgress: callbacks.onProgress,\n\t\t\tonManualCodeInput: callbacks.onManualCodeInput,\n\t\t});\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\tif (!credentials.refresh) {\n\t\t\treturn credentials;\n\t\t}\n\t\treturn refreshGeminiToken(credentials.refresh);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n};\n\nexport const googleOAuthProvider = geminiOAuthProvider;\n"]}