mirror of https://github.com/openclaw/openclaw.git
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { clearCredentialsCache, extractGeminiCliCredentials } from "./oauth.credentials.js";
|
|
import {
|
|
buildAuthUrl,
|
|
generatePkce,
|
|
parseCallbackInput,
|
|
shouldUseManualOAuthFlow,
|
|
waitForLocalCallback,
|
|
} from "./oauth.flow.js";
|
|
import type { GeminiCliOAuthContext, GeminiCliOAuthCredentials } from "./oauth.shared.js";
|
|
import { exchangeCodeForTokens } from "./oauth.token.js";
|
|
|
|
export { clearCredentialsCache, extractGeminiCliCredentials };
|
|
export type { GeminiCliOAuthContext, GeminiCliOAuthCredentials };
|
|
|
|
export async function loginGeminiCliOAuth(
|
|
ctx: GeminiCliOAuthContext,
|
|
): Promise<GeminiCliOAuthCredentials> {
|
|
const needsManual = shouldUseManualOAuthFlow(ctx.isRemote);
|
|
await ctx.note(
|
|
needsManual
|
|
? [
|
|
"You are running in a remote/VPS environment.",
|
|
"A URL will be shown for you to open in your LOCAL browser.",
|
|
"After signing in, copy the redirect URL and paste it back here.",
|
|
].join("\n")
|
|
: [
|
|
"Browser will open for Google authentication.",
|
|
"Sign in with your Google account for Gemini CLI access.",
|
|
"The callback will be captured automatically on localhost:8085.",
|
|
].join("\n"),
|
|
"Gemini CLI OAuth",
|
|
);
|
|
|
|
const { verifier, challenge } = generatePkce();
|
|
const authUrl = buildAuthUrl(challenge, verifier);
|
|
|
|
if (needsManual) {
|
|
ctx.progress.update("OAuth URL ready");
|
|
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
|
|
ctx.progress.update("Waiting for you to paste the callback URL...");
|
|
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
|
|
const parsed = parseCallbackInput(callbackInput, verifier);
|
|
if ("error" in parsed) {
|
|
throw new Error(parsed.error);
|
|
}
|
|
if (parsed.state !== verifier) {
|
|
throw new Error("OAuth state mismatch - please try again");
|
|
}
|
|
ctx.progress.update("Exchanging authorization code for tokens...");
|
|
return exchangeCodeForTokens(parsed.code, verifier);
|
|
}
|
|
|
|
ctx.progress.update("Complete sign-in in browser...");
|
|
try {
|
|
await ctx.openUrl(authUrl);
|
|
} catch {
|
|
ctx.log(`\nOpen this URL in your browser:\n\n${authUrl}\n`);
|
|
}
|
|
|
|
try {
|
|
const { code } = await waitForLocalCallback({
|
|
expectedState: verifier,
|
|
timeoutMs: 5 * 60 * 1000,
|
|
onProgress: (msg) => ctx.progress.update(msg),
|
|
});
|
|
ctx.progress.update("Exchanging authorization code for tokens...");
|
|
return await exchangeCodeForTokens(code, verifier);
|
|
} catch (err) {
|
|
if (
|
|
err instanceof Error &&
|
|
(err.message.includes("EADDRINUSE") ||
|
|
err.message.includes("port") ||
|
|
err.message.includes("listen"))
|
|
) {
|
|
ctx.progress.update("Local callback server failed. Switching to manual mode...");
|
|
ctx.log(`\nOpen this URL in your LOCAL browser:\n\n${authUrl}\n`);
|
|
const callbackInput = await ctx.prompt("Paste the redirect URL here: ");
|
|
const parsed = parseCallbackInput(callbackInput, verifier);
|
|
if ("error" in parsed) {
|
|
throw new Error(parsed.error, { cause: err });
|
|
}
|
|
if (parsed.state !== verifier) {
|
|
throw new Error("OAuth state mismatch - please try again", { cause: err });
|
|
}
|
|
ctx.progress.update("Exchanging authorization code for tokens...");
|
|
return exchangeCodeForTokens(parsed.code, verifier);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|